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.");