(feat): prepare for new ident flow
(fix): some bugs
This commit is contained in:
parent
20018f09a9
commit
8e831fd993
8 changed files with 177 additions and 232 deletions
|
|
@ -238,19 +238,9 @@ function RootShell() {
|
||||||
<LoginWrapper>
|
<LoginWrapper>
|
||||||
<LegalWrapper>
|
<LegalWrapper>
|
||||||
<Crypto>
|
<Crypto>
|
||||||
<MTPProvider>
|
<DesktopMediaProvider>
|
||||||
<Session>
|
<Outlet />
|
||||||
<UserContext>
|
</DesktopMediaProvider>
|
||||||
<CallInit />
|
|
||||||
<CallPopout />
|
|
||||||
<TAuthWrapper>
|
|
||||||
<DesktopMediaProvider>
|
|
||||||
<Outlet />
|
|
||||||
</DesktopMediaProvider>
|
|
||||||
</TAuthWrapper>
|
|
||||||
</UserContext>
|
|
||||||
</Session>
|
|
||||||
</MTPProvider>
|
|
||||||
</Crypto>
|
</Crypto>
|
||||||
</LegalWrapper>
|
</LegalWrapper>
|
||||||
</LoginWrapper>
|
</LoginWrapper>
|
||||||
|
|
@ -264,13 +254,23 @@ function RootShell() {
|
||||||
|
|
||||||
function AppShell() {
|
function AppShell() {
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<MTPProvider>
|
||||||
<ChatContext>
|
<Session>
|
||||||
<NotificationsProvider>
|
<UserContext>
|
||||||
<Outlet />
|
<CallInit />
|
||||||
</NotificationsProvider>
|
<CallPopout />
|
||||||
</ChatContext>
|
<TAuthWrapper>
|
||||||
</AppLayout>
|
<AppLayout>
|
||||||
|
<ChatContext>
|
||||||
|
<NotificationsProvider>
|
||||||
|
<Outlet />
|
||||||
|
</NotificationsProvider>
|
||||||
|
</ChatContext>
|
||||||
|
</AppLayout>
|
||||||
|
</TAuthWrapper>
|
||||||
|
</UserContext>
|
||||||
|
</Session>
|
||||||
|
</MTPProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, normalizePath, type Plugin } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { mtp } from "mtp/vite";
|
import { mtp } from "mtp/vite";
|
||||||
|
|
@ -51,6 +51,38 @@ function deepFilterAssetHeaders(rootDir: string): Plugin {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restartOnMtpSourceChange(srcDir: string): Plugin {
|
||||||
|
const watchedDir = normalizePath(realpathSync(srcDir));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "restart-on-mtp-source-change",
|
||||||
|
apply: "serve",
|
||||||
|
configureServer(server) {
|
||||||
|
server.watcher.add(watchedDir);
|
||||||
|
let restartTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const restart = (file: string) => {
|
||||||
|
if (!normalizePath(file).startsWith(`${watchedDir}/`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restartTimer) {
|
||||||
|
clearTimeout(restartTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
restartTimer = setTimeout(() => {
|
||||||
|
restartTimer = null;
|
||||||
|
void server.restart();
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
server.watcher.on("add", restart);
|
||||||
|
server.watcher.on("change", restart);
|
||||||
|
server.watcher.on("unlink", restart);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "./",
|
base: "./",
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
|
@ -105,7 +137,6 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
envPrefix: ["VITE_", "TAURI_ENV_*"],
|
envPrefix: ["VITE_", "TAURI_ENV_*"],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
// Annoying Vite 8 stuff
|
|
||||||
include: [
|
include: [
|
||||||
"react-redux",
|
"react-redux",
|
||||||
"use-sync-external-store/shim/with-selector",
|
"use-sync-external-store/shim/with-selector",
|
||||||
|
|
@ -114,7 +145,28 @@ export default defineConfig({
|
||||||
"eventemitter3",
|
"eventemitter3",
|
||||||
"react-is",
|
"react-is",
|
||||||
],
|
],
|
||||||
exclude: ["mtp", "mtp/raw", "mtp/type-map", "@tensamin/shared"],
|
exclude: [
|
||||||
|
"mtp",
|
||||||
|
"mtp/raw",
|
||||||
|
"mtp/type-map",
|
||||||
|
"@tensamin/call",
|
||||||
|
"@tensamin/call/utils",
|
||||||
|
"@tensamin/chat",
|
||||||
|
"@tensamin/crypto",
|
||||||
|
"@tensamin/crypto/context",
|
||||||
|
"@tensamin/crypto/worker",
|
||||||
|
"@tensamin/markdown",
|
||||||
|
"@tensamin/mtp",
|
||||||
|
"@tensamin/notifications",
|
||||||
|
"@tensamin/shared",
|
||||||
|
"@tensamin/shared/data",
|
||||||
|
"@tensamin/shared/log",
|
||||||
|
"@tensamin/storage",
|
||||||
|
"@tensamin/storage/context",
|
||||||
|
"@tensamin/tauri",
|
||||||
|
"@tensamin/tauth",
|
||||||
|
"@tensamin/user",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
||||||
|
|
@ -122,6 +174,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
deepFilterAssetHeaders(resolve(appDir, "public")),
|
deepFilterAssetHeaders(resolve(appDir, "public")),
|
||||||
|
restartOnMtpSourceChange(resolve(appDir, "../../packages/mtp/src")),
|
||||||
mtp({ typeMaps: resolve(appDir, "../../type-maps.yaml") }),
|
mtp({ typeMaps: resolve(appDir, "../../type-maps.yaml") }),
|
||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
|
|
||||||
4
bun.lock
4
bun.lock
|
|
@ -347,7 +347,7 @@
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz",
|
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz",
|
||||||
"mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-c4a52c6/mtp-0.1.0.tgz",
|
"mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-8ae8377/mtp-0.1.0.tgz",
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||||
|
|
@ -1540,7 +1540,7 @@
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"mtp": ["mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-c4a52c6/mtp-0.1.0.tgz", {}, "sha512-KIhmkIlALo0PdMnx/QzhM87jNCuC90DaQ7uEWpMjbkI3xvGWmkiX75zqt0UZcO03JYWfBAsCO4+51egoOUq6TA=="],
|
"mtp": ["mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-8ae8377/mtp-0.1.0.tgz", {}, "sha512-W0l7Jc+1XI8BdrmRP9rlY/di6kZ5a/srv4mQvzwyJd+Tfg7BWZ9IUYUXIPze6ayx9dLY1f9Y71OgbhjstMzYjA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz",
|
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/latest/tensamin-ui.tgz",
|
||||||
"mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-c4a52c6/mtp-0.1.0.tgz"
|
"mtp": "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-8ae8377/mtp-0.1.0.tgz"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tensamin/ui": "*",
|
"@tensamin/ui": "*",
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,7 @@ type IncomingCallInvite = {
|
||||||
senderId: number;
|
senderId: number;
|
||||||
};
|
};
|
||||||
type CurrentCallData =
|
type CurrentCallData =
|
||||||
| (z.infer<typeof mtp.call_data.response> & { exists: boolean })
|
(z.infer<typeof mtp.call_data.response> & { exists: boolean }) | null;
|
||||||
| null;
|
|
||||||
|
|
||||||
type NavigateFn = (options: {
|
type NavigateFn = (options: {
|
||||||
to: string;
|
to: string;
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ import { onResume } from "tauri-plugin-app-events-api";
|
||||||
import { MTPClient } from "mtp";
|
import { MTPClient } from "mtp";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { decryptText } from "@tensamin/crypto/worker";
|
|
||||||
import { useCrypto } from "@tensamin/crypto/context";
|
|
||||||
import {
|
import {
|
||||||
type Calls,
|
type Calls,
|
||||||
type Communities,
|
type Communities,
|
||||||
|
|
@ -33,19 +31,21 @@ import {
|
||||||
RETRY_INTERVAL,
|
RETRY_INTERVAL,
|
||||||
} from "./values";
|
} from "./values";
|
||||||
|
|
||||||
const APP_VERSION = "0.0.10";
|
|
||||||
|
|
||||||
const READY_STATE = {
|
const READY_STATE = {
|
||||||
CLOSED: 0,
|
CLOSED: 0,
|
||||||
CONNECTING: 1,
|
CONNECTING: 1,
|
||||||
OPEN: 2,
|
OPEN: 2,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const PUSH_TYPES = ["message_live", "message_state", "call_invite", "error_no_iota"] as const;
|
const PUSH_TYPES = [
|
||||||
|
"message_live",
|
||||||
|
"message_state",
|
||||||
|
"call_invite",
|
||||||
|
"error_no_iota",
|
||||||
|
] as const;
|
||||||
|
|
||||||
const WIRE_TYPES = {
|
const WIRE_TYPES = {
|
||||||
identification: "AppIdentification",
|
temp_cool_type: "TempCoolType",
|
||||||
challenge_response: "AppChallengeResponse",
|
|
||||||
get_user_data: "GetUserData",
|
get_user_data: "GetUserData",
|
||||||
change_user_data: "ChangeUserData",
|
change_user_data: "ChangeUserData",
|
||||||
ping: "AppPing",
|
ping: "AppPing",
|
||||||
|
|
@ -67,18 +67,9 @@ const APP_TYPES = Object.fromEntries(
|
||||||
Object.entries(WIRE_TYPES).map(([appType, wireType]) => [wireType, appType]),
|
Object.entries(WIRE_TYPES).map(([appType, wireType]) => [wireType, appType]),
|
||||||
) as Record<string, keyof Schemas & string>;
|
) as Record<string, keyof Schemas & string>;
|
||||||
|
|
||||||
const FATAL_IDENTIFICATION_ERROR_TYPES = new Set([
|
export type ProtocolMessage<
|
||||||
"error",
|
T extends keyof Schemas & string = keyof Schemas & string,
|
||||||
"error_invalid_user_id",
|
> = {
|
||||||
"error_no_user_id",
|
|
||||||
"error_invalid_challenge",
|
|
||||||
"error_invalid_secret",
|
|
||||||
"error_invalid_private_key",
|
|
||||||
"error_invalid_public_key",
|
|
||||||
"error_not_authenticated",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type ProtocolMessage<T extends keyof Schemas & string = keyof Schemas & string> = {
|
|
||||||
id?: number;
|
id?: number;
|
||||||
type: T | string;
|
type: T | string;
|
||||||
data: z.infer<Schemas[T]["response"]>;
|
data: z.infer<Schemas[T]["response"]>;
|
||||||
|
|
@ -114,29 +105,16 @@ function isTauriMobile() {
|
||||||
return isTauri() && /Android|iPhone|iPad|iPod/.test(navigator.userAgent);
|
return isTauri() && /Android|iPhone|iPad|iPod/.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFatalIdentificationError(error: unknown) {
|
|
||||||
if (typeof error === "object" && error !== null && "type" in error) {
|
|
||||||
const type = (error as { type?: unknown }).type;
|
|
||||||
if (typeof type === "string" && FATAL_IDENTIFICATION_ERROR_TYPES.has(type)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error instanceof Error && (
|
|
||||||
error.message.includes("Missing or invalid user id") ||
|
|
||||||
error.message.includes("Missing private key") ||
|
|
||||||
error.message.includes("Identification challenge was rejected") ||
|
|
||||||
error.message.includes("timed out after") ||
|
|
||||||
error.message.includes("Response validation failed")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProtocolErrorDetails(error: unknown) {
|
function getProtocolErrorDetails(error: unknown) {
|
||||||
if (typeof error !== "object" || error === null || !("type" in error)) {
|
if (typeof error !== "object" || error === null || !("type" in error)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocolError = error as { id?: unknown; type?: unknown; data?: unknown };
|
const protocolError = error as {
|
||||||
|
id?: unknown;
|
||||||
|
type?: unknown;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
id: protocolError.id,
|
id: protocolError.id,
|
||||||
type: protocolError.type,
|
type: protocolError.type,
|
||||||
|
|
@ -193,7 +171,9 @@ function validateResponse<T extends keyof Schemas & string>(
|
||||||
|
|
||||||
const parsed = schema.safeParse(data);
|
const parsed = schema.safeParse(data);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error(`Response validation failed for ${type}: ${parsed.error.message}`);
|
throw new Error(
|
||||||
|
`Response validation failed for ${type}: ${parsed.error.message}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -203,9 +183,11 @@ function validateResponse<T extends keyof Schemas & string>(
|
||||||
} as ProtocolMessage<T>;
|
} as ProtocolMessage<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Provider(props: { children: ReactNode; blockConnection?: boolean }) {
|
export function Provider(props: {
|
||||||
|
children: ReactNode;
|
||||||
|
blockConnection?: boolean;
|
||||||
|
}) {
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
const { decrypt, getSharedSecret } = useCrypto();
|
|
||||||
|
|
||||||
const [readyState, setReadyState] = useState<number>(READY_STATE.CLOSED);
|
const [readyState, setReadyState] = useState<number>(READY_STATE.CLOSED);
|
||||||
const [connected, setConnected] = useState<boolean>(false);
|
const [connected, setConnected] = useState<boolean>(false);
|
||||||
|
|
@ -222,9 +204,9 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
const [freshContacts, setFreshContacts] = useState<Contacts>([]);
|
const [freshContacts, setFreshContacts] = useState<Contacts>([]);
|
||||||
const [freshCalls, setFreshCalls] = useState<Calls>([]);
|
const [freshCalls, setFreshCalls] = useState<Calls>([]);
|
||||||
|
|
||||||
const clientRef = useRef<Awaited<ReturnType<typeof MTPClient.create>> | null>(null);
|
const clientRef = useRef<Awaited<ReturnType<typeof MTPClient.create>> | null>(
|
||||||
const identificationStartedRef = useRef(false);
|
null,
|
||||||
const identificationCancelRef = useRef(false);
|
);
|
||||||
|
|
||||||
const [mtpUrl, setMtpUrl] = useState<string | null>(null);
|
const [mtpUrl, setMtpUrl] = useState<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -281,7 +263,6 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
if (!connected) return;
|
if (!connected) return;
|
||||||
|
|
||||||
return subscribe("error_no_iota", () => {
|
return subscribe("error_no_iota", () => {
|
||||||
identificationCancelRef.current = true;
|
|
||||||
setIdentified(false);
|
setIdentified(false);
|
||||||
setIdentifying(false);
|
setIdentifying(false);
|
||||||
setError("We couldn't reach your Iota");
|
setError("We couldn't reach your Iota");
|
||||||
|
|
@ -381,13 +362,14 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setIdentified(false);
|
setIdentified(false);
|
||||||
setIdentifying(false);
|
setIdentifying(false);
|
||||||
identificationStartedRef.current = false;
|
|
||||||
|
|
||||||
await MTPClient.init();
|
await MTPClient.init();
|
||||||
const client = await MTPClient.create({
|
const client = await MTPClient.create({
|
||||||
url,
|
url,
|
||||||
pings: true,
|
pings: true,
|
||||||
logger: (event) => log(event.hint === "error" ? 0 : 2, "mtp", "blue", event),
|
logger: (event) => {
|
||||||
|
log(2, "mtp", event.type === "state" ? "cyan" : "blue", event.type === "state" ? event.data : event.type, event);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (disposed) {
|
if (disposed) {
|
||||||
|
|
@ -403,13 +385,40 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authPayload = new Promise<ProtocolMessage<"temp_cool_type">>(
|
||||||
|
(resolve, reject) => {
|
||||||
|
const unsubscribe = client.subscribe("TempCoolType", (message) => {
|
||||||
|
try {
|
||||||
|
unsubscribe();
|
||||||
|
resolve(validateResponse("temp_cool_type", message));
|
||||||
|
} catch (authPayloadError) {
|
||||||
|
unsubscribe();
|
||||||
|
reject(authPayloadError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
scheduleReconnectReset();
|
scheduleReconnectReset();
|
||||||
identificationCancelRef.current = false;
|
|
||||||
setReadyState(READY_STATE.OPEN);
|
setReadyState(READY_STATE.OPEN);
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
|
setIdentifying(true);
|
||||||
setError("");
|
setError("");
|
||||||
setErrorDescription("");
|
setErrorDescription("");
|
||||||
|
|
||||||
|
await client.auth();
|
||||||
|
const finalResponse = await authPayload;
|
||||||
|
|
||||||
|
if (disposed || clientRef.current !== client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFreshContacts(finalResponse.data.contacts);
|
||||||
|
setFreshCommunities(finalResponse.data.communities ?? []);
|
||||||
|
setFreshCalls(finalResponse.data.calls);
|
||||||
|
setIdentifying(false);
|
||||||
|
setIdentified(true);
|
||||||
} catch (connectError) {
|
} catch (connectError) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
|
||||||
|
|
@ -420,7 +429,13 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setIdentified(false);
|
setIdentified(false);
|
||||||
setIdentifying(false);
|
setIdentifying(false);
|
||||||
log(0, "mtp", "red", "Connection attempt failed", connectError);
|
log(
|
||||||
|
0,
|
||||||
|
"mtp",
|
||||||
|
"red",
|
||||||
|
"Connection/authentication attempt failed",
|
||||||
|
getProtocolErrorDetails(connectError) ?? connectError,
|
||||||
|
);
|
||||||
scheduleReconnect(connectError);
|
scheduleReconnect(connectError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -464,116 +479,9 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setIdentified(false);
|
setIdentified(false);
|
||||||
setIdentifying(false);
|
setIdentifying(false);
|
||||||
identificationStartedRef.current = false;
|
|
||||||
};
|
};
|
||||||
}, [mtpUrl, props.blockConnection]);
|
}, [mtpUrl, props.blockConnection]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!connected) {
|
|
||||||
identificationStartedRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identificationCancelRef.current || identificationStartedRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
identificationStartedRef.current = true;
|
|
||||||
let cancelled = false;
|
|
||||||
setIdentifying(true);
|
|
||||||
setIdentified(false);
|
|
||||||
|
|
||||||
const identify = async () => {
|
|
||||||
try {
|
|
||||||
const sessionId = await load("session_id");
|
|
||||||
const userId = await load("user_id");
|
|
||||||
const privateKey = await load("private_key");
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Number.isSafeInteger(userId) ||
|
|
||||||
userId <= 0 ||
|
|
||||||
privateKey.trim() === "" ||
|
|
||||||
!Number.isSafeInteger(sessionId) ||
|
|
||||||
sessionId <= 0
|
|
||||||
) {
|
|
||||||
throw new Error("Invalid credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
const challengeEnvelope = await send("identification", {
|
|
||||||
version: APP_VERSION,
|
|
||||||
session_id: sessionId,
|
|
||||||
user_id: userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sharedSecret = await getSharedSecret(
|
|
||||||
privateKey,
|
|
||||||
"",
|
|
||||||
challengeEnvelope.data.public_key,
|
|
||||||
);
|
|
||||||
|
|
||||||
const decryptedChallenge = await decryptText(
|
|
||||||
sharedSecret,
|
|
||||||
challengeEnvelope.data.challenge,
|
|
||||||
);
|
|
||||||
|
|
||||||
const finalResponse = await send("challenge_response", {
|
|
||||||
challenge: decryptedChallenge,
|
|
||||||
}).catch((challengeError) => {
|
|
||||||
if (!identificationCancelRef.current) {
|
|
||||||
setError("Identification Failed");
|
|
||||||
setErrorDescription(
|
|
||||||
"Unable to complete secure identification. Please verify your credentials and try again.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw challengeError;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFreshContacts(finalResponse.data.contacts);
|
|
||||||
setFreshCommunities(finalResponse.data.communities ?? []);
|
|
||||||
setFreshCalls(finalResponse.data.calls);
|
|
||||||
|
|
||||||
if (cancelled || identificationCancelRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setError("");
|
|
||||||
setErrorDescription("");
|
|
||||||
setIdentified(true);
|
|
||||||
} catch (identificationError) {
|
|
||||||
if (cancelled || identificationCancelRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFatal = isFatalIdentificationError(identificationError);
|
|
||||||
log(
|
|
||||||
isFatal ? 0 : 1,
|
|
||||||
"mtp",
|
|
||||||
isFatal ? "red" : "yellow",
|
|
||||||
"Identification handshake failed",
|
|
||||||
getProtocolErrorDetails(identificationError) ?? identificationError,
|
|
||||||
);
|
|
||||||
|
|
||||||
setIdentified(false);
|
|
||||||
setError("Identification Failed");
|
|
||||||
setErrorDescription(
|
|
||||||
isFatal
|
|
||||||
? "Unable to complete secure identification. Please verify your credentials and try again."
|
|
||||||
: "Unable to complete secure identification because the transport request failed.",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (!cancelled && !identificationCancelRef.current) {
|
|
||||||
setIdentifying(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void identify();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [connected, decrypt, getSharedSecret, load, send]);
|
|
||||||
|
|
||||||
const progress = useMemo(() => {
|
const progress = useMemo(() => {
|
||||||
if (!mtpUrl) return 10;
|
if (!mtpUrl) return 10;
|
||||||
if (readyState === READY_STATE.CONNECTING) return 30;
|
if (readyState === READY_STATE.CONNECTING) return 30;
|
||||||
|
|
@ -585,7 +493,8 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
|
|
||||||
const loadingTitle = useMemo(() => {
|
const loadingTitle = useMemo(() => {
|
||||||
if (!mtpUrl) return "Looking up configuration";
|
if (!mtpUrl) return "Looking up configuration";
|
||||||
if (readyState === READY_STATE.CONNECTING || !connected) return "Connecting to Tensamin";
|
if (readyState === READY_STATE.CONNECTING || !connected)
|
||||||
|
return "Connecting to Tensamin";
|
||||||
if (identifying || !identified) return "Identifying secure session";
|
if (identifying || !identified) return "Identifying secure session";
|
||||||
return "Loading";
|
return "Loading";
|
||||||
}, [connected, identified, identifying, readyState, mtpUrl]);
|
}, [connected, identified, identifying, readyState, mtpUrl]);
|
||||||
|
|
@ -595,7 +504,7 @@ export function Provider(props: { children: ReactNode; blockConnection?: boolean
|
||||||
if (readyState === READY_STATE.CONNECTING || !connected) {
|
if (readyState === READY_STATE.CONNECTING || !connected) {
|
||||||
return "Establishing transport channel";
|
return "Establishing transport channel";
|
||||||
}
|
}
|
||||||
if (identifying || !identified) return "Verifying challenge-response handshake";
|
if (identifying || !identified) return "Waiting for authenticated session";
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [connected, identified, identifying, readyState, mtpUrl]);
|
}, [connected, identified, identifying, readyState, mtpUrl]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,33 @@ export const failedUser = {
|
||||||
username: "unknown",
|
username: "unknown",
|
||||||
} as z.infer<typeof mtp.get_user_data.response>;
|
} as z.infer<typeof mtp.get_user_data.response>;
|
||||||
|
|
||||||
export type Contacts = z.infer<
|
const authPayload = z.object({
|
||||||
typeof mtp.challenge_response.response.shape.contacts
|
communities: z.array(z.object({})).optional(),
|
||||||
>;
|
contacts: z.array(
|
||||||
export type Communities = z.infer<
|
z.object({
|
||||||
typeof mtp.challenge_response.response.shape.communities
|
last_message_at: z.number(),
|
||||||
>;
|
user_id: z.number(),
|
||||||
export type Calls = z.infer<typeof mtp.challenge_response.response.shape.calls>;
|
last_message: z
|
||||||
|
.object({
|
||||||
|
content: z.base64(),
|
||||||
|
sender_id: z.number(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
messages: z.array(message),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
calls: z.array(
|
||||||
|
z.object({
|
||||||
|
call_id: z.string(),
|
||||||
|
call_secret: z.base64().optional(),
|
||||||
|
call_members: z.array(z.number()),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Contacts = z.infer<typeof authPayload.shape.contacts>;
|
||||||
|
export type Communities = z.infer<typeof authPayload.shape.communities>;
|
||||||
|
export type Calls = z.infer<typeof authPayload.shape.calls>;
|
||||||
|
|
||||||
type Base16Palette = Record<
|
type Base16Palette = Record<
|
||||||
| "base00"
|
| "base00"
|
||||||
|
|
@ -94,44 +114,9 @@ const user = z.object({
|
||||||
username: z.string().min(1).max(15),
|
username: z.string().min(1).max(15),
|
||||||
});
|
});
|
||||||
export const mtp = {
|
export const mtp = {
|
||||||
identification: {
|
temp_cool_type: {
|
||||||
request: z.object({
|
request: z.object({}).optional(),
|
||||||
version: z.string(),
|
response: authPayload,
|
||||||
session_id: z.number(),
|
|
||||||
user_id: z.number(),
|
|
||||||
}),
|
|
||||||
response: z.object({
|
|
||||||
challenge: z.string(),
|
|
||||||
public_key: z.base64(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
challenge_response: {
|
|
||||||
request: z.object({
|
|
||||||
challenge: z.base64(),
|
|
||||||
}),
|
|
||||||
response: z.object({
|
|
||||||
communities: z.array(z.object({})).optional(),
|
|
||||||
contacts: z.array(
|
|
||||||
z.object({
|
|
||||||
last_message_at: z.number(),
|
|
||||||
user_id: z.number(),
|
|
||||||
last_message: z
|
|
||||||
.object({
|
|
||||||
content: z.base64(),
|
|
||||||
sender_id: z.number(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
messages: z.array(message),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
calls: z.array(
|
|
||||||
z.object({
|
|
||||||
call_id: z.string(),
|
|
||||||
call_secret: z.base64().optional(),
|
|
||||||
call_members: z.array(z.number()),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
get_user_data: {
|
get_user_data: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ protocol_version: "0.0"
|
||||||
type_maps:
|
type_maps:
|
||||||
"0.0":
|
"0.0":
|
||||||
CommunicationTypes:
|
CommunicationTypes:
|
||||||
AppIdentification: 32
|
TempCoolType: 32
|
||||||
AppChallengeResponse: 33
|
|
||||||
GetUserData: 34
|
GetUserData: 34
|
||||||
ChangeUserData: 35
|
ChangeUserData: 35
|
||||||
MessageLive: 36
|
MessageLive: 36
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue