From fb742bd0682f734702b62d91ebc09dabb38545b0 Mon Sep 17 00:00:00 2001 From: Alois Date: Tue, 14 Apr 2026 01:12:34 +0200 Subject: [PATCH] (feat): add tauth (fix): formatted --- apps/web/src/components/navbar.tsx | 7 +- apps/web/src/routes/app/home.tsx | 5 +- apps/web/src/routes/screens/login.tsx | 2 +- apps/web/src/routes/settings/security.tsx | 2 +- bun.lock | 7 +- package.json | 2 +- packages/chat/src/components/message.tsx | 36 ++++---- packages/crypto/src/worker.test.ts | 5 +- packages/shared/src/data.ts | 20 +++-- packages/shared/src/settings.ts | 16 ++-- packages/tauth/package.json | 1 + packages/tauth/src/context.tsx | 102 +++++++++++++++++----- packages/ttp/src/context.tsx | 6 +- packages/ttp/src/index.ts | 2 +- scripts/copy-releases.ts | 2 +- 15 files changed, 141 insertions(+), 74 deletions(-) diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index b59e9b4..573f965 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -4,12 +4,7 @@ import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; import { useCall } from "@tensamin/call/context"; import Wrapper from "@tensamin/user/wrapper"; import { Skeleton } from "@tensamin/ui"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, -} from "@tensamin/ui"; +import { Select, SelectContent, SelectItem, SelectTrigger } from "@tensamin/ui"; import { displayCallId } from "@tensamin/call/utils"; import { useState } from "react"; import { SidebarTrigger, useSidebar } from "@tensamin/ui"; diff --git a/apps/web/src/routes/app/home.tsx b/apps/web/src/routes/app/home.tsx index a677a78..90f6a26 100644 --- a/apps/web/src/routes/app/home.tsx +++ b/apps/web/src/routes/app/home.tsx @@ -6,8 +6,9 @@ import { DialogHeader, DialogTitle, DialogTrigger, - Input, Button, - useIsMobile + Input, + Button, + useIsMobile, } from "@tensamin/ui"; import z from "zod"; import { toast } from "@tensamin/shared/log"; diff --git a/apps/web/src/routes/screens/login.tsx b/apps/web/src/routes/screens/login.tsx index e37b0c5..dc85810 100644 --- a/apps/web/src/routes/screens/login.tsx +++ b/apps/web/src/routes/screens/login.tsx @@ -1,5 +1,5 @@ import Form from "@/components/screens/login/form"; -import {CreateScreen} from "@tensamin/ui"; +import { CreateScreen } from "@tensamin/ui"; /** * Executes Page. diff --git a/apps/web/src/routes/settings/security.tsx b/apps/web/src/routes/settings/security.tsx index 72918c3..15a6155 100644 --- a/apps/web/src/routes/settings/security.tsx +++ b/apps/web/src/routes/settings/security.tsx @@ -11,7 +11,7 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, - Button + Button, } from "@tensamin/ui"; export default function Page() { diff --git a/bun.lock b/bun.lock index d3d04c1..b90c411 100644 --- a/bun.lock +++ b/bun.lock @@ -192,6 +192,7 @@ "version": "0.0.0", "dependencies": { "@tanstack/react-router": "^1.0.0", + "@tauri-apps/api": "^2.10.1", "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", @@ -238,7 +239,7 @@ }, }, "overrides": { - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.4.tar.gz", + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.9.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.25.tar.gz", }, "packages": { @@ -690,7 +691,7 @@ "@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], - "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.4.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.58.1", "globals": "^17.5.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", "zod": "^4.3.6" } }, "sha512-FVtxXH9DBwHz2GpuFZFO2KifWefBOoK4/dv6yH4xyvDIS0PIVRc7UrBAgf2HQ3+N9jWaDw9FwyJkZJCAdVCNJQ=="], + "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.9.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.58.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", "zod": "^4.3.6" } }, "sha512-V9EK/t/tcBe1oNqzUMxtn41wc3UIS0mkLvGeR1mGfZaKn4d9bwwWQ2VmSmRSxodPYiQUJkziJV4fZvdzGjnzFw=="], "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.25.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-pYkl4quhAhV/l/j1Yy5m4DDV5fh42H4G8v+FE6qEMCcJIOeA1LU7brIG7X9zURe7F4gyn1LQxMf2yABuV/NsMQ=="], @@ -770,6 +771,8 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + "@webtransport-bun/webtransport": ["@webtransport-bun/webtransport@0.3.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-/OX/TCgBD64n/0BjMNytObq5NK2pAM0KOoXBEw49l3sTmbVXNTPP1ZRPIdIyV43agM+mKtZBaV3Er3k3YoB6sw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], diff --git a/package.json b/package.json index 8dc1e52..2427ec0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "overrides": { "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.25.tar.gz", - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.4.tar.gz" + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.9.tar.gz" }, "dependencies": { "@tensamin/ui": "*", diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index 0e2e81f..c46e2c4 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -6,11 +6,7 @@ import { log } from "@tensamin/shared/log"; import Text from "@tensamin/markdown/text"; import { AlertTriangle, Clock } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@tensamin/ui"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; import { useIsMobile } from "@tensamin/ui"; import { ContextMenu, @@ -36,7 +32,11 @@ function getSafeMessageHeight(height: number | undefined) { * @param props Parameter props. * @returns unknown. */ -function MessageComponent({ message, notEncrypted, measureRef }: { +function MessageComponent({ + message, + notEncrypted, + measureRef, +}: { message: RawMessage & { failed?: boolean; }; @@ -113,19 +113,17 @@ function MessageComponent({ message, notEncrypted, measureRef }: { : "bg-muted" } ${!isReady && "animate-pulse"} ${isMobile && "select-none"}`} > - {message.failed && - message.message_state === "awaiting" && ( - - -

Failed to send message

-
- } /> -
- )} - {!message.failed && - message.message_state === "awaiting" && ( - - )} + {message.failed && message.message_state === "awaiting" && ( + + +

Failed to send message

+
+ } /> +
+ )} + {!message.failed && message.message_state === "awaiting" && ( + + )} {isReady ? ( ) : ( diff --git a/packages/crypto/src/worker.test.ts b/packages/crypto/src/worker.test.ts index eae1c3c..1243837 100644 --- a/packages/crypto/src/worker.test.ts +++ b/packages/crypto/src/worker.test.ts @@ -25,10 +25,7 @@ function bytesToB64u(value: Uint8Array): string { output += index + 2 < value.length ? alphabet[chunk & 63] : "="; } - return output - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, ""); + return output.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } /** diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index ba830d8..83471cf 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -1,7 +1,10 @@ import { z } from "zod"; import { legalDocsSchema } from "./features/legal/schema"; -import { settingsStorageDefaults, type SettingsStorageDefaults } from "./settings"; +import { + settingsStorageDefaults, + type SettingsStorageDefaults, +} from "./settings"; const fileFromMessage = z.object({ name: z.string(), @@ -155,6 +158,14 @@ export const ttp = { }), }, + load_txt_record: { + request: z.object({ + path: z.string(), + }), + response: z.object({ + content: z.string(), + }), + }, authenticate_app: { request: z.object({ app_identifier: z.string(), @@ -163,13 +174,12 @@ export const ttp = { challenge: z.base64(), }), }, - get_app: { + create_app: { request: z.object({ + app_public_key: z.base64(), app_identifier: z.string(), }), - response: z.object({ - app_public_key: z.base64(), - }), + response: z.object({}), }, // Calls diff --git a/packages/shared/src/settings.ts b/packages/shared/src/settings.ts index 70212d2..5fbbb89 100644 --- a/packages/shared/src/settings.ts +++ b/packages/shared/src/settings.ts @@ -49,10 +49,11 @@ type BooleanSettingNames = { export type SettingsStorageKey = `settings.${BooleanSettingNames}`; -export type SettingsStorageDefaults = - { - [K in SettingsStorageKey]: boolean; - }; +export type SettingsStorageDefaults< + T extends SettingsSchema = typeof settings, +> = { + [K in SettingsStorageKey]: boolean; +}; function buildSettingsStorageDefaults( schema: T, @@ -62,7 +63,10 @@ function buildSettingsStorageDefaults( for (const category of Object.values(schema)) { for (const page of Object.values(category)) { for (const [settingName, setting] of Object.entries(page)) { - if (setting.type === "boolean" && typeof setting.default === "boolean") { + if ( + setting.type === "boolean" && + typeof setting.default === "boolean" + ) { const key = `settings.${settingName}` as SettingsStorageKey; defaults[key] = setting.default; } @@ -73,4 +77,4 @@ function buildSettingsStorageDefaults( return defaults as SettingsStorageDefaults; } -export const settingsStorageDefaults = buildSettingsStorageDefaults(settings); \ No newline at end of file +export const settingsStorageDefaults = buildSettingsStorageDefaults(settings); diff --git a/packages/tauth/package.json b/packages/tauth/package.json index 83fe33d..3f5044a 100644 --- a/packages/tauth/package.json +++ b/packages/tauth/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@tanstack/react-router": "^1.0.0", + "@tauri-apps/api": "^2.10.1", "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", diff --git a/packages/tauth/src/context.tsx b/packages/tauth/src/context.tsx index e6ac17d..4d56956 100644 --- a/packages/tauth/src/context.tsx +++ b/packages/tauth/src/context.tsx @@ -16,77 +16,128 @@ import { useDeeplinks } from "@tensamin/tauri/deeplinkHandler"; import { useTTP } from "@tensamin/ttp"; import { log, toast } from "@tensamin/shared/log"; import { Loader2 } from "lucide-react"; +import { isTauri } from "@tauri-apps/api/core"; export default function Wrapper({ children }: { children: ReactNode }) { - const { getSharedSecret } = useCrypto(); const { get } = useUser(); const { load } = useStorage(); const { send } = useTTP(); - const { decrypt } = useCrypto(); + const { decrypt, getSharedSecret } = useCrypto(); const { searchStr } = useLocation(); const [dialogOpen, setDialogOpen] = useState(false); const [loading, setLoading] = useState(false); const [identifier, setIdentifier] = useState(null); const [redirect, setRedirect] = useState(null); + const [challenge, setChallenge] = useState(null); + const [allowChildern, setAllowChildern] = useState(false); const { deeplinks } = useDeeplinks(); const authorizeApp = async () => { - if (!identifier || !redirect) return; + if (!identifier || !redirect || !challenge) return; try { // Get Data const user = await get(await load("user_id")); - const app = await send("get_app", { - app_identifier: identifier, + const { + data: { content: appPublicKey }, + } = await send("load_txt_record", { + path: "tauth." + identifier, }); + + // Get Shared Secret const sharedSecret = await getSharedSecret( await load("private_key"), user.public_key, - app.data.app_public_key, - ); - - // Get Challenge - const res = await send("authenticate_app", { - app_identifier: identifier, + appPublicKey, + ).catch((err) => { + log(1, "tauth", "red", "Failed to get shared secret", err, { + appPublicKey, + }); + throw new Error("Failed to get shared secret"); }); - const challenge = res.data.challenge; // Solve Challenge - const solvedChallenge = await decrypt(sharedSecret, challenge); + const solvedChallenge = await decrypt(sharedSecret, challenge).catch( + (err) => { + log(1, "tauth", "red", "Failed to solve challenge", err, { + challenge, + }); + throw new Error("Failed to solve challenge"); + }, + ); // Craft Redirect URL const finalUrl = new URL(redirect); + finalUrl.searchParams.set("userId", String(await load("user_id"))); finalUrl.searchParams.set("challenge", solvedChallenge); + finalUrl.searchParams.set("originalChallenge", challenge); - const session = crypto.randomUUID(); - finalUrl.searchParams.set("session", session); + const session = Date.now(); + finalUrl.searchParams.set("sessionId", String(session)); + + // Save Session + await send("create_app", { + app_public_key: appPublicKey, + app_identifier: identifier, + }); // Open Redirect URL - window.open(finalUrl.toString(), "_blank"); + if (isTauri()) { + window.open(finalUrl.toString(), "_blank"); + setLoading(false); + setDialogOpen(false); + setIdentifier(null); + setRedirect(null); + setChallenge(null); + + toast("success", "App authorized successfully"); + return; + } else { + window.location.href = finalUrl.toString(); + return; + } } catch (err) { toast("error", "Failed to authorize app"); log(1, "tauth", "red", "Failed to authorize app", err); - } finally { - setDialogOpen(false); + setLoading(false); + setDialogOpen(false); setIdentifier(null); setRedirect(null); + setChallenge(null); } }; + function hexToBase64(hex: string): string { + const bytes = hex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? []; + const binaryString = String.fromCharCode(...bytes); + return btoa(binaryString); + } + useEffect(() => { const params = new URLSearchParams(searchStr); const identifier = params.get("identifier"); const redirect = params.get("redirect"); + const challenge = params.get("challenge"); if (!identifier || !redirect) return; - // @eslint-disable-next-line react-hooks/exhaustive-deps - setIdentifier(identifier); - setRedirect(redirect); - setDialogOpen(true); - }, [searchStr]); + load("user_id").then((userId) => { + if (!challenge) { + const newUrl = new URL(redirect); + newUrl.searchParams.set("userId", String(userId)); + window.location.href = newUrl.toString(); + return; + } + + setAllowChildern(true); + setIdentifier(identifier); + setRedirect(redirect); + setChallenge(hexToBase64(challenge || "")); + setDialogOpen(true); + }); + }, [searchStr, load]); useEffect(() => { deeplinks.forEach((link) => { @@ -98,6 +149,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { if (identifier && redirect) { setIdentifier(identifier); setRedirect(redirect); + setChallenge(hexToBase64(url.searchParams.get("challenge") || "")); setDialogOpen(true); } } @@ -113,6 +165,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { if (!value) { setIdentifier(null); setRedirect(null); + setChallenge(null); } }} > @@ -147,6 +200,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { setDialogOpen(false); setIdentifier(null); setRedirect(null); + setChallenge(null); }} > Deny @@ -154,7 +208,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { - {children} + {allowChildern && children} ); } diff --git a/packages/ttp/src/context.tsx b/packages/ttp/src/context.tsx index 85421ca..6932e10 100644 --- a/packages/ttp/src/context.tsx +++ b/packages/ttp/src/context.tsx @@ -11,7 +11,11 @@ import { import { useCrypto } from "@tensamin/crypto/context"; import { log } from "@tensamin/shared/log"; import { useStorage } from "@tensamin/storage/context"; -import { createTransportClient, READY_STATE, type BoundSendFn } from "@tensamin/ttp-core"; +import { + createTransportClient, + READY_STATE, + type BoundSendFn, +} from "@tensamin/ttp-core"; import type { PushHandler } from "@tensamin/ttp-core"; import { PING_INTERVAL, diff --git a/packages/ttp/src/index.ts b/packages/ttp/src/index.ts index 1279632..e1a274f 100644 --- a/packages/ttp/src/index.ts +++ b/packages/ttp/src/index.ts @@ -1,3 +1,3 @@ export * from "@tensamin/ttp-core"; export * from "./context"; -export * from "./values"; \ No newline at end of file +export * from "./values"; diff --git a/scripts/copy-releases.ts b/scripts/copy-releases.ts index 2aa93ab..e840169 100644 --- a/scripts/copy-releases.ts +++ b/scripts/copy-releases.ts @@ -77,4 +77,4 @@ const moveBundleArtifact = ( moveBundleArtifact(debDir, ".deb", `Tensamin-${version}.deb`); moveBundleArtifact(rpmDir, ".rpm", `Tensamin-${version}.rpm`); -console.log("Releases copied successfully."); \ No newline at end of file +console.log("Releases copied successfully.");