From 930663d495613877928e34539641406fa5f1eeae Mon Sep 17 00:00:00 2001 From: Alois Date: Sun, 5 Jul 2026 01:43:06 +0200 Subject: [PATCH] (feat): migrate ttp to mtp (wip): crypto migration --- apps/electron/src/main/tray.ts | 4 +- apps/web/src/components/modals/basic.tsx | 10 +- apps/web/src/components/modals/profile.tsx | 12 +- apps/web/src/components/navbar.tsx | 4 +- .../web/src/components/screens/login/form.tsx | 22 +- apps/web/src/components/sidebar.tsx | 15 +- apps/web/src/routes/app/home.tsx | 6 +- apps/web/src/routes/settings/profile.tsx | 26 +- apps/web/src/routes/settings/security.tsx | 177 +++++-------- .../call/src/components/buttons/invite.tsx | 2 +- packages/call/src/components/invitePopup.tsx | 6 +- packages/call/src/components/modals/base.tsx | 12 +- packages/call/src/components/top.tsx | 6 +- packages/call/src/store.tsx | 12 +- packages/call/src/views/preview.tsx | 2 +- packages/chat/src/behaviour/conversation.ts | 12 +- packages/chat/src/components/input.tsx | 8 +- packages/chat/src/components/message.tsx | 25 +- packages/chat/src/context.tsx | 56 ++-- packages/chat/src/screen.tsx | 232 +++++++++-------- packages/chat/src/values.ts | 2 +- packages/crypto/src/context.tsx | 204 ++------------- packages/mtp/src/context.tsx | 245 +++++++++--------- packages/notifications/src/context.tsx | 26 +- packages/shared/src/data.ts | 120 ++++----- packages/storage/src/session.tsx | 2 +- packages/tauth/src/context.tsx | 32 ++- packages/user/src/context.tsx | 8 +- pnpm-lock.yaml | 16 +- pnpm-workspace.yaml | 2 +- 30 files changed, 558 insertions(+), 748 deletions(-) diff --git a/apps/electron/src/main/tray.ts b/apps/electron/src/main/tray.ts index 9e91017..774f7bf 100644 --- a/apps/electron/src/main/tray.ts +++ b/apps/electron/src/main/tray.ts @@ -21,5 +21,5 @@ export function initTray() { tray.on("click", (event) => { console.log(event); - }) -} \ No newline at end of file + }); +} diff --git a/apps/web/src/components/modals/basic.tsx b/apps/web/src/components/modals/basic.tsx index eb9a27b..01794cb 100644 --- a/apps/web/src/components/modals/basic.tsx +++ b/apps/web/src/components/modals/basic.tsx @@ -19,13 +19,13 @@ export function Basic({ extra?: React.ReactNode; }) { return ( - +
- + - {user.display.slice(0, 2).toUpperCase()} + {user.Display.slice(0, 2).toUpperCase()} @@ -49,7 +49,7 @@ export function Basic({
-

{user.display}

+

{user.Display}

{extra}
@@ -58,5 +58,5 @@ export function Basic({ } export function Loading() { - return ; + return ; } diff --git a/apps/web/src/components/modals/profile.tsx b/apps/web/src/components/modals/profile.tsx index e34a9ac..9e01949 100644 --- a/apps/web/src/components/modals/profile.tsx +++ b/apps/web/src/components/modals/profile.tsx @@ -30,7 +30,7 @@ export default function Profile({ user }: { user: User }) { void (async () => { try { const ownId = await load("user_id"); - const privateKey = await load("private_key"); + const privateKey = await load("mtp_keyring"); const ownData = await get(ownId); const secret = await getSharedSecret( privateKey, @@ -61,17 +61,17 @@ export default function Profile({ user }: { user: User }) {
- + - {user.display.slice(0, 2).toUpperCase()} + {user.Display.slice(0, 2).toUpperCase()}
-

{user.display}

-

{user.username}

+

{user.Display}

+

{user.Username}

- + } diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index b1ff1a3..a963c60 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -24,7 +24,7 @@ const fetchedUser = z.object({ const formSchema = z.object({ username: z.string().min(1).max(255), - private_key: z.string().min(1).max(92), + mtp_keyring: z.string().min(1).max(92), }); /** @@ -61,9 +61,7 @@ function parseTuFileContent(rawFileContent: string): { ? Number(userIdString.split("@")[0]) : Number(userIdString); - const domain = userIdString.includes("@") - ? userIdString.split("@")[1] - : null; + const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null; if (!userId || !privateKey) { throw new Error("Invalid file"); @@ -97,9 +95,9 @@ export default function Form() { await save("session_id", Date.now()); await save("user_id", parsed.userId); - await save("private_key", parsed.privateKey); + await save("mtp_keyring", parsed.privateKey); if (parsed.domain) { - await save("mtp_url", `https://${parsed.domain}/`); + await save("omega_url", `https://${parsed.domain}/`); } location.href = "/"; @@ -240,9 +238,9 @@ export default function Form() { await save("session_id", Date.now()); await save("user_id", user.user_id); - await save("private_key", inputParse.data.private_key); + await save("mtp_keyring", inputParse.data.mtp_keyring); if (domain) { - await save("mtp_url", `https://${domain}/`); + await save("omega_url", `https://${domain}/`); } location.href = "/"; @@ -273,10 +271,10 @@ export default function Form() { await save("session_id", Date.now()); await save("user_id", userId); - await save("private_key", privateKey); + await save("mtp_keyring", privateKey); if (domain) { - await save("mtp_url", `https://${domain}/`); + await save("omega_url", `https://${domain}/`); } location.href = "/"; @@ -335,8 +333,8 @@ export default function Form() {
- - + +
} /> - - - Are you sure? - - Exposing this QR code is the same as sharing your .tu - file. Since changing your private key is quite tedious, - only do this when you're confident the key won't be - compromised. - - - - Cancel - setQrCodeVisible(true)}> - Show QR Code - - } - /> - - - - - - )} +
+

+ It's best not to touch these! They can be exploited to gain access to + your account! +

+
+ +
+ setDraftMtpUrl(e.target.value)} + /> + +
+
+
+ +
+ setDraftForcedOmikronUrl(e.target.value)} + /> + setDraftForcedOmikronPublicKey(e.target.value)} + /> + +
); diff --git a/packages/call/src/components/buttons/invite.tsx b/packages/call/src/components/buttons/invite.tsx index d7ccf82..d049a5a 100644 --- a/packages/call/src/components/buttons/invite.tsx +++ b/packages/call/src/components/buttons/invite.tsx @@ -78,7 +78,7 @@ export default function InviteButton({ }); }} > - {user.display} + {user.Display} )} /> diff --git a/packages/call/src/components/invitePopup.tsx b/packages/call/src/components/invitePopup.tsx index 579d87d..15d9b1a 100644 --- a/packages/call/src/components/invitePopup.tsx +++ b/packages/call/src/components/invitePopup.tsx @@ -28,12 +28,12 @@ export default function InvitePopup({ - + - {user.display.slice(0, 2).toUpperCase()} + {user.Display.slice(0, 2).toUpperCase()} -

{user.display}

+

{user.Display}

); @@ -209,14 +209,14 @@ export default function Base({ }, [participant, get]); useEffect(() => { - if (type !== "user" || !user?.avatar) { + if (type !== "user" || !user?.Avatar) { setAvatarBackgroundColor(undefined); return; } let active = true; - void getAverageImageColor(user.avatar).then((color) => { + void getAverageImageColor(user.Avatar).then((color) => { if (active) { setAvatarBackgroundColor(color); } @@ -225,7 +225,7 @@ export default function Base({ return () => { active = false; }; - }, [type, user?.avatar]); + }, [type, user?.Avatar]); // Avatar calc const currentCard = useRef(null); @@ -342,13 +342,13 @@ export default function Base({ height: "32cqh", }} > - + - {user.display.slice(0, 2).toUpperCase()} + {user.Display.slice(0, 2).toUpperCase()} )} diff --git a/packages/call/src/components/top.tsx b/packages/call/src/components/top.tsx index ce5b7cf..f573dc4 100644 --- a/packages/call/src/components/top.tsx +++ b/packages/call/src/components/top.tsx @@ -75,9 +75,9 @@ export default function TopBar() { - + - {user.display.slice(0, 2).toUpperCase()} + {user.Display.slice(0, 2).toUpperCase()} } @@ -86,7 +86,7 @@ export default function TopBar() { side="bottom" portalProps={{ container: portalContainer }} > - {user.display} + {user.Display} diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 2412b99..e14aea6 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -52,7 +52,7 @@ type IncomingCallInvite = { senderId: number; }; type CurrentCallData = - (z.infer & { exists: boolean }) | null; + (z.infer & { exists: boolean }) | null; type NavigateFn = (options: { to: string; @@ -626,7 +626,7 @@ export async function sendCallInvite(userId: number) { } const ownUserId = (await runtime.load("user_id")) as number; - const privateKey = await runtime.load("private_key"); + const privateKey = await runtime.load("mtp_keyring"); const ownPublicKey = await runtime .getUser(ownUserId) .then((data) => data.PublicKey); @@ -897,7 +897,7 @@ export async function joinCall( if (callSecret) { try { const sharedSecret = await runtime.getSharedSecret( - await runtime.load("private_key"), + await runtime.load("mtp_keyring"), await runtime .getUser((await runtime.load("user_id")) as number) .then((res) => res.PublicKey), @@ -1178,7 +1178,7 @@ export function useInitializeCall() { // listen to call invites useEffect(() => { subscribePush(async (message) => { - if (message.type !== "call_invite") return; + if (message.type !== "CallInvite") return; const { CallId, CallSecret, SenderId } = message.data as { CallId: string; @@ -1500,10 +1500,10 @@ export function useInitializeCall() { return; } - send("call_data", { CallId: callId }) + send("CallData", { CallId: callId }) .then((data) => { setCurrentCallData({ - ...(data.data as z.infer), + ...(data.data as z.infer), exists: true, }); }) diff --git a/packages/call/src/views/preview.tsx b/packages/call/src/views/preview.tsx index d489dba..401332d 100644 --- a/packages/call/src/views/preview.tsx +++ b/packages/call/src/views/preview.tsx @@ -45,7 +45,7 @@ export default function Preview() { {data.map((user) => { return (

- User: {user.display} + User: {user.Display}

); })} diff --git a/packages/chat/src/behaviour/conversation.ts b/packages/chat/src/behaviour/conversation.ts index 4081b72..1cf6bc8 100644 --- a/packages/chat/src/behaviour/conversation.ts +++ b/packages/chat/src/behaviour/conversation.ts @@ -11,13 +11,13 @@ import type { RawMessages } from "../values"; */ export async function getMessages( send: BoundSendFn, - amount: number, - offset: number, + Amount: number, + Offset: number, UserId: number, ): Promise { - const messages = await send("messages_get", { - amount: amount, - offset: offset, + const messages = await send("MessagesGet", { + Amount, + Offset, UserId, }); @@ -25,5 +25,5 @@ export async function getMessages( throw new Error(messages.type); } - return messages.data.messages; + return messages.data.Messages; } diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index 49600f3..df83c18 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -72,10 +72,9 @@ export default function InputComponent({ log(3, "chat", "purple", "Message send init, adding live message ..."); const reference = addLiveMessage({ - height: 0, NotEncrypted: true, SendTime: time, - content: currentValue, + Content: currentValue, SentBySelf: true, MessageState: "awaiting", }); @@ -94,9 +93,8 @@ export default function InputComponent({ log(3, "chat", "purple", "Content encrypted, sending message..."); - send("message_send", { - height: 0, - content: encryptedContext, + send("MessageSend", { + Content: encryptedContext, ReceiverId: userId, SendTime: time, }).catch((e) => { diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index 621a815..4312b94 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -64,14 +64,14 @@ function MessageComponent({ const [isValidURL, setIsValidURL] = useState(false); useEffect(() => { try { - if (message.content.split(" ").length > 1) throw new Error(); + if (message.Content.split(" ").length > 1) throw new Error(); - new URL(message.content); + new URL(message.Content); setIsValidURL(true); } catch { setIsValidURL(false); } - }, [message.content]); + }, [message.Content]); // Message states const { load } = useStorage(); @@ -82,13 +82,13 @@ function MessageComponent({ if (readConfirmations) { if (!user?.UserId) return; - await send("message_state", { + await send("MessageState", { ChatPartnerId: user?.UserId, SendTime: message.SendTime, MessageState: "read", }); } else { - await send("message_state", { + await send("MessageState", { ChatPartnerId: user?.UserId, SendTime: message.SendTime, MessageState: "received", @@ -131,7 +131,7 @@ function MessageComponent({ className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`} >
) : ( - + - {user.display.slice(0, 2).toUpperCase()} + {user.Display.slice(0, 2).toUpperCase()} )} @@ -190,7 +190,7 @@ function MessageComponent({ {!grouped && (
-

{user.display}

+

{user.Display}

{new Date(message.SendTime).toLocaleString([], { hour: "2-digit", @@ -214,9 +214,9 @@ function MessageComponent({

)} {isValidURL ? ( - + ) : ( - + )}
@@ -232,8 +232,7 @@ function MessageComponent({ export default React.memo(MessageComponent, (prev, next) => { return ( prev.message.SendTime === next.message.SendTime && - prev.message.content === next.message.content && - prev.message.height === next.message.height && + prev.message.Content === next.message.Content && prev.message.SentBySelf === next.message.SentBySelf && prev.message.MessageState === next.message.MessageState && prev.message.failed === next.message.failed && diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index f1688eb..b9d580d 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -50,18 +50,16 @@ function updateMessageStateBySendTime< }; } -/** - * Executes Provider. - * @param props Parameter props. - * @returns unknown. - */ -export default function Provider(props: { children: ReactNode }) { +export default function Provider({ children }: { children: ReactNode }) { const { getSharedSecret, decryptText } = useCrypto(); const { get } = useUser(); const { load } = useStorage(); const { send, subscribePush } = useMTP(); const { moveUserIdToTop } = useSession(); + const [error, setError] = useState(""); + const [errorDescription, setErrorDescription] = useState(""); + const [liveMessagesState, setLiveMessagesState] = useState([]); const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{ userId: number; @@ -101,21 +99,40 @@ export default function Provider(props: { children: ReactNode }) { try { const recipientData = await get(userIdValue); const ownId = await load("user_id"); - const privateKey = await load("private_key"); + const privateKey = await load("mtp_keyring"); const ownData = await get(ownId); + + log(3, "chat", "purple", "Getting shared secret...", { + recipientData, + ownData, + }); + const sharedSecret = await getSharedSecret( privateKey, ownData.PublicKey, recipientData.PublicKey, ); + log(2, "chat", "purple", "Got shared secret", { + sharedSecret, + }); + if (active) { setCurrentSharedSecretState({ userId: userIdValue, value: sharedSecret, }); } - } catch { + } catch (err) { + log( + 1, + "chat", + "red", + "An unknown error occured while getting a shared secret", + err, + ); + setError(err instanceof Error ? err.name : "Unknown Error"); + setErrorDescription(err instanceof Error ? err.message : String(err)); if (active) { setCurrentSharedSecretState({ userId: userIdValue, @@ -132,9 +149,9 @@ export default function Provider(props: { children: ReactNode }) { const getMessages = useCallback( async (amount: number, offset: number) => { - const messages = await send("messages_get", { - amount, - offset, + const messages = await send("MessagesGet", { + Amount: amount, + Offset: offset, UserId: userIdValue, }); @@ -142,7 +159,7 @@ export default function Provider(props: { children: ReactNode }) { throw new Error(messages.type); } - const rawMessages = messages.data.messages; + const rawMessages = messages.data.Messages; const sorted = [...rawMessages].sort((a, b) => a.SendTime - b.SendTime); if (sorted.length > 0) { @@ -162,7 +179,7 @@ export default function Provider(props: { children: ReactNode }) { try { return { ...message, - content: await decryptText(currentSharedSecret, message.content), + content: await decryptText(currentSharedSecret, message.Content), }; } catch { return message; @@ -214,7 +231,7 @@ export default function Provider(props: { children: ReactNode }) { // Get live updates for message states useEffect(() => { return subscribePush((message) => { - if (message.type !== "message_state") { + if (message.type !== "MessageState") { return; } @@ -318,9 +335,11 @@ export default function Provider(props: { children: ReactNode }) { sharedSecret: currentSharedSecret, userId: userIdValue, inputBoxRef, + error, + errorDescription, }} > - {props.children} + {children} ); @@ -336,13 +355,10 @@ type contextType = { sharedSecret: string; userId: number; inputBoxRef: React.RefObject; + error: string; + errorDescription: string; }; -/** - * Executes useChat. - * @param none This function has no parameters. - * @returns contextType. - */ export function useChat(): contextType { const ctx = useContext(context); if (!ctx) { diff --git a/packages/chat/src/screen.tsx b/packages/chat/src/screen.tsx index b49ec04..b5cf724 100644 --- a/packages/chat/src/screen.tsx +++ b/packages/chat/src/screen.tsx @@ -22,12 +22,6 @@ type MessageChunk = { startIndex: number; }; -function getEstimatedMessageHeight(message: { height?: number }) { - return typeof message.height === "number" && Number.isFinite(message.height) - ? Math.max(1, Math.ceil(message.height)) - : FALLBACK_MESSAGE_HEIGHT; -} - function shouldFetchPreviousPage({ entry, hasNextPage, @@ -80,8 +74,15 @@ function buildMessageChunks( * @returns Chat screen JSX. */ export default function Screen() { - const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } = - useChat(); + const { + getMessages, + liveMessages, + clearLiveMessages, + userId, + sharedSecret, + error, + errorDescription, + } = useChat(); const { get: getUser } = useUser(); const { load } = useStorage(); @@ -251,16 +252,7 @@ export default function Screen() { return FALLBACK_MESSAGE_HEIGHT; } - const chunk = messageChunks[index]; - - if (!chunk) { - return FALLBACK_MESSAGE_HEIGHT; - } - - return chunk.messages.reduce( - (total, message) => total + getEstimatedMessageHeight(message), - 0, - ); + return FALLBACK_MESSAGE_HEIGHT; }, [messageChunks, shouldShowConversationStart], ); @@ -479,112 +471,122 @@ export default function Screen() { if (!hasValidChatUser) { return ( -
- Invalid user +
+

Invalid User

); } return (
-
-
+ {error !== "" && errorDescription !== "" ? ( +
+

{error}

+

{errorDescription}

+
+ ) : ( + <>
- {virtualizer.getVirtualItems().map((virtualRow) => { - if ( - shouldShowConversationStart && - virtualRow.index === messageChunks.length - ) { - return ( -
-
-
- Conversation start + ref={scrollRef} + id="chat_container" + className="min-h-0 flex-1 overflow-y-auto" + style={{ + overflowAnchor: "none", + paddingTop: "22px", + transform: "scaleY(-1)", + }} + onScroll={handleContainerScroll} + > +
+
+ {virtualizer.getVirtualItems().map((virtualRow) => { + if ( + shouldShowConversationStart && + virtualRow.index === messageChunks.length + ) { + return ( +
+
+
+ Conversation start +
+
+
+ ); + } + + const chunkIndex = virtualRow.index; + const chunk = messageChunks[chunkIndex]; + if (!chunk) { + return null; + } + + return ( +
+
+ {chunk.messages.map((message, chunkMessageIndex) => { + const messageIndex = + chunk.startIndex + chunkMessageIndex; + const lastMessage = messages[messageIndex - 1]; + const isGrouped = + lastMessage && + lastMessage.SentBySelf === message.SentBySelf && + Math.round(lastMessage.SendTime / 10000) === + Math.round(message.SendTime / 10000); + + return ( + + ); + })}
-
- ); - } - - const chunkIndex = virtualRow.index; - const chunk = messageChunks[chunkIndex]; - if (!chunk) { - return null; - } - - return ( -
-
- {chunk.messages.map((message, chunkMessageIndex) => { - const messageIndex = chunk.startIndex + chunkMessageIndex; - const lastMessage = messages[messageIndex - 1]; - const isGrouped = - lastMessage && - lastMessage.SentBySelf === message.SentBySelf && - Math.round(lastMessage.SendTime / 10000) === - Math.round(message.SendTime / 10000); - - return ( - - ); - })} -
-
- ); - })} -
-
-
- -
+ ); + })} +
+
+
+ +
+ + )}
); } diff --git a/packages/chat/src/values.ts b/packages/chat/src/values.ts index 675c0f8..0d7af81 100644 --- a/packages/chat/src/values.ts +++ b/packages/chat/src/values.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { mtp } from "@tensamin/shared/data"; -export type RawMessages = z.infer["messages"]; +export type RawMessages = z.infer["Messages"]; export type RawMessage = RawMessages[number]; diff --git a/packages/crypto/src/context.tsx b/packages/crypto/src/context.tsx index 6275de6..c871348 100644 --- a/packages/crypto/src/context.tsx +++ b/packages/crypto/src/context.tsx @@ -1,5 +1,5 @@ -import * as React from "react"; -import * as Comlink from "comlink"; +import { createContext, useContext } from "react"; +import { crypto } from "mtp"; type CryptoContextType = { decrypt: ( @@ -19,198 +19,26 @@ type CryptoContextType = { ) => Promise; }; -type ApiRef = { - encrypt: ( - secret: string, - input: Uint8Array, - ) => Promise>; - decrypt: ( - secret: string, - input: Uint8Array, - ) => Promise>; - decryptText: (secret: string, ciphertext: string) => Promise; - encryptText: (secret: string, plaintext: string) => Promise; - getSharedSecret: ( - ownPrivateKey: string, - ownPublicKey: string, - otherPublicKey: string, - ) => Promise; -}; +export const context = createContext(undefined); -export function bytesToBase64(bytes: Uint8Array): string { - let binary = ""; - for (const b of bytes) binary += String.fromCharCode(b); - return btoa(binary); -} - -export function base64ToBytes(base64: string): Uint8Array { - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); - return bytes; -} - -export const context = React.createContext( - undefined, -); - -/** - * Provides cryptographic actions backed by a worker without coupling to UI state. - * @param props Component props with children. - * @returns Crypto context provider JSX. - */ export default function Provider(props: { children: React.ReactNode }) { - const apiRef = React.useRef(null); - - const value = React.useMemo( - () => ({ - encrypt: async (secret, plaintext) => { - const api = apiRef.current; - if (!api) throw new Error("API not initialized"); - return await api.encrypt(secret, plaintext); - }, - decrypt: async (secret, ciphertext) => { - const api = apiRef.current; - if (!api) throw new Error("API not initialized"); - return await api.decrypt(secret, ciphertext); - }, - encryptText: async (secret, plaintext) => { - const api = apiRef.current; - if (!api) throw new Error("API not initialized"); - return await api.encryptText(secret, plaintext); - }, - decryptText: async (secret, ciphertext) => { - const api = apiRef.current; - if (!api) throw new Error("API not initialized"); - return await api.decryptText(secret, ciphertext); - }, - getSharedSecret: async (ownPrivateKey, ownPublicKey, otherPublicKey) => { - const api = apiRef.current; - if (!api) throw new Error("API not initialized"); - return await api.getSharedSecret( - ownPrivateKey, - ownPublicKey, - otherPublicKey, - ); - }, - }), - [], + return ( + + {props.children} + ); - - React.useEffect(() => { - const worker = new Worker(new URL("./worker.ts", import.meta.url), { - type: "module", - }); - - apiRef.current = Comlink.wrap(worker); - - return () => { - apiRef.current = null; - worker.terminate(); - }; - }, []); - - return {props.children}; } -/** - * Creates crypto action functions that safely delegate to the worker API. - * @param getApiRef Function that returns the worker API reference. - * @returns Typed crypto action functions. - */ -export function createCryptoActions( - getApiRef: () => ApiRef | null, -): CryptoContextType { - /** - * Encrypts bytes by delegating to the crypto worker API. - * @param secret Hex-encoded shared secret. - * @param input Plaintext bytes to encrypt. - * @returns Ciphertext bytes. - */ - const encrypt = async ( - secret: string, - input: Uint8Array, - ): Promise> => { - const api = getApiRef(); - if (!api) throw new Error("API not initialized"); - return await api.encrypt(secret, input); - }; - - /** - * Decrypts bytes by delegating to the crypto worker API. - * @param secret Hex-encoded shared secret. - * @param input Ciphertext bytes to decrypt. - * @returns Plaintext bytes. - */ - const decrypt = async ( - secret: string, - input: Uint8Array, - ): Promise> => { - const api = getApiRef(); - if (!api) throw new Error("API not initialized"); - return await api.decrypt(secret, input); - }; - - /** - * Encrypts plaintext text by delegating to the crypto worker API. - * @param secret Hex-encoded shared secret. - * @param plaintext Plaintext to encrypt. - * @returns Base64 ciphertext. - */ - const encryptText = async ( - secret: string, - plaintext: string, - ): Promise => { - const api = getApiRef(); - if (!api) throw new Error("API not initialized"); - return await api.encryptText(secret, plaintext); - }; - - /** - * Decrypts base64 ciphertext text by delegating to the crypto worker API. - * @param secret Hex-encoded shared secret. - * @param ciphertext Base64 ciphertext to decrypt. - * @returns Decrypted plaintext. - */ - const decryptText = async ( - secret: string, - ciphertext: string, - ): Promise => { - const api = getApiRef(); - if (!api) throw new Error("API not initialized"); - return await api.decryptText(secret, ciphertext); - }; - - /** - * Derives a shared secret from local and peer key material via the worker API. - * @param ownPrivateKey Local private key. - * @param ownPublicKey Local public key. - * @param otherPublicKey Peer public key. - * @returns Hex-encoded shared secret. - */ - const getSharedSecret = async ( - ownPrivateKey: string, - ownPublicKey: string, - otherPublicKey: string, - ): Promise => { - const api = getApiRef(); - if (!api) throw new Error("API not initialized"); - return await api.getSharedSecret( - ownPrivateKey, - ownPublicKey, - otherPublicKey, - ); - }; - - return { encrypt, decrypt, encryptText, decryptText, getSharedSecret }; -} - -/** - * Returns the crypto actions from the nearest provider. - * Throws when used outside of the crypto provider tree. - */ export function useCrypto(): CryptoContextType { - const ctx = React.useContext(context); + const ctx = useContext(context); if (!ctx) { throw new Error("useCrypto must be used within a CryptoProvider"); } diff --git a/packages/mtp/src/context.tsx b/packages/mtp/src/context.tsx index 37eb314..ddd1b15 100644 --- a/packages/mtp/src/context.tsx +++ b/packages/mtp/src/context.tsx @@ -12,10 +12,8 @@ import { isTauri } from "@tauri-apps/api/core"; import { onResume } from "tauri-plugin-app-events-api"; import { MTPClient } from "mtp"; import { type z } from "zod"; -import { ConnectionState } from "./values"; -import createAsyncQueue, { - createQueuedFunc, -} from "@tensamin/shared/asyncQueue"; +import { ConnectionState } from "mtp"; +import createAsyncQueue from "@tensamin/shared/asyncQueue"; import { toast as sonnerToast } from "@tensamin/ui"; import { Loader2 } from "lucide-react"; @@ -47,36 +45,6 @@ function base64ToUint8Array(b64: string) { return out; } -const PUSH_TYPES = [ - "message_live", - "message_state", - "call_invite", - "error_no_iota", -] as const; - -const WIRE_TYPES = { - temp_cool_type: "TempCoolType", - get_user_data: "GetUserData", - change_user_data: "ChangeUserData", - ping: "AppPing", - message_live: "MessageLive", - messages_get: "MessagesGet", - message_send: "MessageSend", - add_conversation: "AddConversation", - message_state: "MessageState", - load_txt_record: "LoadTxtRecord", - authenticate_app: "AuthenticateApp", - create_app: "CreateApp", - call_token: "CallToken", - call_data: "CallData", - call_invite: "CallInvite", - error_no_iota: "ErrorNoIota", -} as const satisfies Record; - -const APP_TYPES = Object.fromEntries( - Object.entries(WIRE_TYPES).map(([appType, wireType]) => [wireType, appType]), -) as Record; - export type ProtocolMessage< T extends keyof Schemas & string = keyof Schemas & string, > = { @@ -139,10 +107,8 @@ function validateResponse( type: T, message: { id?: number; type: string; data: unknown }, ): ProtocolMessage { - const appType = APP_TYPES[message.type] ?? message.type; - - if (appType.startsWith("error")) { - return { ...message, type: appType } as ProtocolMessage; + if (message.type.startsWith("Error")) { + return message as ProtocolMessage; } const schema = schemas[type]?.response; @@ -159,7 +125,7 @@ function validateResponse( return { id: message.id, - type: appType, + type: message.type, data: parsed.data, } as ProtocolMessage; } @@ -192,7 +158,7 @@ export function Provider(props: { // MTP url const [mtpUrl, setMtpUrl] = useState(null); useEffect(() => { - load("mtp_url").then(setMtpUrl); + load("omega_url").then(setMtpUrl); }, [load]); // Validation override functions @@ -205,7 +171,7 @@ export function Provider(props: { } const message = await client.request( - WIRE_TYPES[type], + type, (data ?? {}) as Record, options, ); @@ -220,7 +186,7 @@ export function Provider(props: { return () => {}; } - return client.subscribe(WIRE_TYPES[type], (message) => { + return client.subscribe(type, (message) => { handler(validateResponse(type, message)); }); }, []); @@ -231,8 +197,13 @@ export function Provider(props: { return () => {}; } - const unsubscribers = PUSH_TYPES.map((type) => - client.subscribe(WIRE_TYPES[type], (message) => { + const unsubscribers = [ + "MessageLive", + "MessageState", + "CallInvite", + "ErrorNoIota", + ].map((type) => + client.subscribe(type, (message) => { handler(validateResponse(type as keyof Schemas & string, message)); }), ); @@ -242,23 +213,6 @@ export function Provider(props: { }; }, []); - // No Iota check - useEffect(() => { - if (!connected) return; - - return subscribe("error_no_iota", () => { - setIdentified(false); - setIdentifying(false); - sonnerToast.error("We couldn't reach your Iota", { - description: - "Check your network connection and try restarting your Iota", - icon: null, - duration: Infinity, - closeButton: true, - }); - }); - }, [connected, subscribe]); - // Custom Pings useEffect(() => { if (!connected || !identified) { @@ -268,7 +222,7 @@ export function Provider(props: { const interval = setInterval(async () => { try { const originalNow = Date.now(); - const data = await send("ping", { LastPing: originalNow }); + const data = await send("Ping", { LastPing: originalNow }); setOwnPing(Date.now() - originalNow); const remotePing = data.data.PingIota; @@ -286,6 +240,7 @@ export function Provider(props: { }, [connected, identified, send]); // Reconnect stuff + const resolveConnectionRef = useRef(() => {}); useEffect(() => { if (!mtpUrl) return; @@ -309,12 +264,10 @@ export function Provider(props: { reconnectResetTimer = null; }; - let resolveConnection: (() => void) | null = null; - if (!props.blockConnection) { sonnerToast.promise( new Promise((resolve) => { - resolveConnection = resolve; + resolveConnectionRef.current = resolve; }), { id: "mtp-connection-toast", @@ -346,61 +299,64 @@ export function Provider(props: { await MTPClient.init(); - log(2, "mtp", "purple", "Fetching Omikron data."); - const data = await fetch( - `${mtpUrl}api/get/omikron/${await load("user_id")}`, - ); + const forcedOmikronUrl = await load("forced_omikron_url"); + const forcedOmikronPublicKey = await load("forced_omikron_public_key"); - if (data.status === 404) { - sonnerToast.error("We couldn't reach your Iota", { - description: - "Check your network connection and try restarting your Iota", - icon: null, - duration: Infinity, - closeButton: true, - }); - resolveConnection?.(); - cleanup(); - return; + let url = null; + let omikronPublicKey = null; + if (forcedOmikronUrl && forcedOmikronPublicKey) { + url = forcedOmikronUrl; + omikronPublicKey = forcedOmikronPublicKey; + } else { + log(2, "mtp", "purple", "Fetching Omikron data."); + const data = await fetch( + `${mtpUrl}api/get/omikron/${await load("user_id")}`, + ); + + if (data.status === 404) { + sonnerToast.error("We couldn't reach your Iota", { + description: + "Check your network connection and try restarting your Iota", + icon: null, + duration: Infinity, + closeButton: true, + }); + resolveConnectionRef.current?.(); + cleanup(); + return; + } + const omikronData = (await data.json()) as { + id: number; + ip_address: string; + port: number; + public_key: string; + status: string; + }; + + if ( + !omikronData.ip_address || + !omikronData.port || + !omikronData.public_key + ) + throw new Error("Invalid Omikron data"); + + url = `https://${omikronData.ip_address}:${omikronData.port}`; + omikronPublicKey = omikronData.public_key; } - - const omikronData = (await data.json()) as { - id: number; - ip_address: string; - port: number; - public_key: string; - status: string; - }; //codec.decode(new Uint8Array(await res.arrayBuffer())), - if ( - !omikronData.ip_address || - !omikronData.port || - !omikronData.public_key - ) - throw new Error("Invalid Omikron data"); - - const url = `https://${omikronData.ip_address}:${omikronData.port}`; + if (!url || !omikronPublicKey) + throw new Error("Missing Omikron URL or Public Key"); log(2, "mtp", "green", "Connecting to: " + url); const client = await MTPClient.create({ url, - storage: { - getItem: (key) => { - console.log(key); - return key; - }, - removeItem: (key) => { - console.log(key); - }, - setItem: console.log, - }, credentials: { clientId: await load("user_id"), - keyring: base64ToUint8Array(await load("private_key")), + keyring: base64ToUint8Array(await load("mtp_keyring")), }, - hostPublicKey: omikronData.public_key, + hostPublicKey: omikronPublicKey, descriptor: "client", pings: true, logger: (event) => { @@ -410,12 +366,24 @@ export function Provider(props: { ); } - if (event.type !== "Pong") { + if (event.type !== "Pong" && event.type !== "Ping") { log( 2, "mtp", - event.type === "state" ? "cyan" : "blue", - event.type === "state" ? event.data : event.type, + event.type === "state" + ? "purple" + : event.direction === "recv" + ? "cyan" + : event.direction === "send" + ? "gray" + : "blue", + event.type === "state" + ? event.data + : event.direction === "recv" + ? "< " + event.type + : event.direction === "send" + ? "> " + event.type + : event.type, event, ); } @@ -436,19 +404,22 @@ export function Provider(props: { return; } - const authPayload = new Promise>( - (resolve, reject) => { - const unsubscribe = client.subscribe("TempCoolType", (message) => { + const authPayload = new Promise< + ProtocolMessage<"IdentificationResponse"> + >((resolve, reject) => { + const unsubscribe = client.subscribe( + "IdentificationResponse", + (message) => { try { unsubscribe(); - resolve(validateResponse("temp_cool_type", message)); + resolve(validateResponse("IdentificationResponse", message)); } catch (authPayloadError) { unsubscribe(); reject(authPayloadError); } - }); - }, - ); + }, + ); + }); clearReconnectTimer(); @@ -467,12 +438,12 @@ export function Provider(props: { if (disposed || clientRef.current !== client) return; - setFreshContacts(finalResponse.data.contacts); - setFreshCommunities(finalResponse.data.communities ?? []); - setFreshCalls(finalResponse.data.calls); + setFreshContacts(finalResponse.data.Contacts); + setFreshCommunities(finalResponse.data.Communities); + setFreshCalls(finalResponse.data.Calls); setIdentifying(false); setIdentified(true); - resolveConnection?.(); + resolveConnectionRef.current?.(); } catch (connectError) { if (disposed) return; cleanup(); @@ -492,7 +463,7 @@ export function Provider(props: { id: "mtp-connection-toast", description: connectError instanceof Error - ? connectError.message + ? connectError.message.split(":")[0] : String(connectError ?? "Unknown error"), icon: null, duration: Infinity, @@ -558,6 +529,24 @@ export function Provider(props: { }; }, [mtpUrl, props.blockConnection, load]); + // No Iota check + useEffect(() => { + if (!connected) return; + + return subscribe("ErrorNoIota", () => { + setIdentified(false); + setIdentifying(false); + sonnerToast.error("We couldn't reach your Iota", { + description: + "Check your network connection and try restarting your Iota", + icon: null, + duration: Infinity, + closeButton: true, + }); + resolveConnectionRef.current?.(); + }); + }, [connected, subscribe]); + // Async queue const loadingDescription = useMemo(() => { if (!mtpUrl) return "Loading connection details"; @@ -587,10 +576,18 @@ export function Provider(props: { } }, [connected, identified, mtpUrl, send, subscribe, subscribePush, mtpRef]); + const sendQueued: BoundSendFn = useMemo( + () => async (type, data, options) => { + const mtp = await mtpRef.get(); + return mtp.send(type, data, options); + }, + [mtpRef], + ); + return ( (contextReady ? send : null)), + send: sendQueued, subscribe, subscribePush, readyState, diff --git a/packages/notifications/src/context.tsx b/packages/notifications/src/context.tsx index 799f695..1879852 100644 --- a/packages/notifications/src/context.tsx +++ b/packages/notifications/src/context.tsx @@ -6,7 +6,7 @@ import { useMTP } from "@tensamin/mtp"; import { createContext, useEffect, useContext } from "react"; import z from "zod"; import { toast as sonnerToast } from "sonner"; -import { message as messageSchema } from "@tensamin/shared/data"; +import { Message as MessageSchema } from "@tensamin/shared/data"; import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui"; import { isTauri } from "@tauri-apps/api/core"; import { useSession } from "@tensamin/storage/session"; @@ -34,9 +34,9 @@ export default function Provider(props: { children: React.ReactNode }) { useEffect(() => { return subscribePush(async (ttpMessage) => { - if (ttpMessage.type === "message_live") { + if (ttpMessage.type === "MessageLive") { const { message, SenderId } = ttpMessage.data as { - message: z.infer; + message: z.infer; SenderId: number; }; @@ -44,18 +44,18 @@ export default function Provider(props: { children: React.ReactNode }) { const decryptedContent = await decryptText( await getSharedSecret( - await load("private_key"), + await load("mtp_keyring"), await get(await load("user_id")).then((data) => data.PublicKey), user.PublicKey, ), - message.content, + message.Content, ); // Update message state if (userId === SenderId) { addLiveMessage({ ...message, - content: decryptedContent, + Content: decryptedContent, SentBySelf: false, }); return; @@ -67,7 +67,7 @@ export default function Provider(props: { children: React.ReactNode }) { if (await load("settings.receive_confirmations")) { void send( - "message_state", + "MessageState", { MessageState: "received", }, @@ -83,10 +83,10 @@ export default function Provider(props: { children: React.ReactNode }) { const hasPermissions = await requestNotificationPermission(); if (hasPermissions) { - const notification = new Notification(user.display, { + const notification = new Notification(user.Display, { body: decryptedContent, - icon: user.avatar || user.display.slice(0, 2).toUpperCase(), - badge: user.avatar || user.display.slice(0, 2).toUpperCase(), + icon: user.Avatar || user.Display.slice(0, 2).toUpperCase(), + badge: user.Avatar || user.Display.slice(0, 2).toUpperCase(), tag: `message-${user.UserId}`, silent: true, }); @@ -100,16 +100,16 @@ export default function Provider(props: { children: React.ReactNode }) { notification.close(); }; } else { - sonnerToast(user.display, { + sonnerToast(user.Display, { classNames: { content: "pl-4", }, description: decryptedContent, icon: ( - + - {user.display.slice(0, 2).toUpperCase()} + {user.Display.slice(0, 2).toUpperCase()} ), diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index d5deefc..82070d4 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -12,23 +12,22 @@ const fileFromMessage = z.object({ type: z.enum(["image", "image_top_right", "file"]), }); -export const message = z.object({ - height: z.number(), +export const Message = z.object({ NotEncrypted: z.boolean().optional(), SentBySelf: z.boolean().optional(), SendTime: z.number(), - content: z.base64(), - files: z.array(fileFromMessage).optional(), - tint: z.string().length(7).startsWith("#").optional(), - avatar: z.boolean().optional(), - display: z.boolean().optional(), + Content: z.base64(), + Files: z.array(fileFromMessage).optional(), + Tint: z.string().length(7).startsWith("#").optional(), + Avatar: z.boolean().optional(), + Display: z.boolean().optional(), MessageState: z .enum(["read", "received", "sent", "sending", "awaiting"]) // awaiting for 'internal' use .default("received"), }); export const failedUser = { - display: "Failed", + Display: "Failed", IotaId: 0, OmikronConnections: [], OnlineStatus: "user_borked", @@ -36,12 +35,12 @@ export const failedUser = { SubEnd: 0, SubLevel: 0, UserId: 0, - username: "unknown", -} as z.infer; + Username: "unknown", +} as z.infer; const authPayload = z.object({ - communities: z.array(z.object({})).optional(), - contacts: z.array( + Communities: z.array(z.object({})).default([]), + Contacts: z.array( z.object({ LastMessageAt: z.number(), UserId: z.number(), @@ -51,21 +50,21 @@ const authPayload = z.object({ SenderId: z.number(), }) .optional(), - messages: z.array(message), + Messages: z.array(Message), }), - ), - calls: z.array( + ).default([]), + Calls: z.array( z.object({ CallId: z.string(), CallSecret: z.base64().optional(), CallMembers: z.array(z.number()), }), - ), + ).default([]), }); -export type Contacts = z.infer; -export type Communities = z.infer; -export type Calls = z.infer; +export type Contacts = z.infer; +export type Communities = z.infer; +export type Calls = z.infer; type Base16Palette = Record< | "base00" @@ -89,9 +88,9 @@ type Base16Palette = Record< // MTP const user = z.object({ - about: z.string().max(255).optional(), - avatar: z.string().optional(), - display: z.string().min(1).max(15), + About: z.string().max(255).optional(), + Avatar: z.string().optional(), + Display: z.string().min(1).max(15), IotaId: z.number(), OmikronConnections: z.array(z.number()), OmikronId: z.number().optional(), @@ -107,29 +106,29 @@ const user = z.object({ "iota_borked", ]), PublicKey: z.base64(), - status: z.string().max(15).optional(), + Status: z.string().max(15).optional(), SubEnd: z.number(), SubLevel: z.number(), UserId: z.number(), - username: z.string().min(1).max(15), + Username: z.string().min(1).max(15), }); export const mtp = { - temp_cool_type: { + IdentificationResponse: { request: z.object({}).optional(), response: authPayload, }, - get_user_data: { + GetUserData: { request: z.object({ UserId: z.number().optional(), - username: z.string().optional(), + Username: z.string().optional(), }), response: user, }, - change_user_data: { + ChangeUserData: { request: user.partial(), response: z.object({}), }, - ping: { + Ping: { request: z.object({ LastPing: z.number(), }), @@ -137,76 +136,75 @@ export const mtp = { PingIota: z.number(), }), }, - message_live: { + MessageLive: { request: z.object({}).optional(), response: z.object({ SenderId: z.number(), - message, + Message, }), }, - messages_get: { + MessagesGet: { request: z.object({ UserId: z.number(), - amount: z.number(), - offset: z.number(), + Amount: z.number(), + Offset: z.number(), }), response: z.object({ - messages: z.array(message), + Messages: z.array(Message), }), }, - message_send: { + MessageSend: { request: z.object({ - height: z.number(), - content: z.base64(), + Content: z.base64(), ReceiverId: z.number(), SendTime: z.number(), - files: z.array(fileFromMessage).optional(), + Files: z.array(fileFromMessage).optional(), }), response: z.object({}), }, - add_conversation: { + AddConversation: { request: z.object({ ChatPartnerId: z.number().optional(), ChatPartnerName: z.string().min(1).max(15).optional(), }), response: z.object({}), }, - message_state: { + MessageState: { request: z .object({ ChatPartnerId: z.number(), SendTime: z.number(), - MessageState: message.shape.MessageState, + MessageState: Message.shape.MessageState, }) .or( z.object({ - MessageState: message.shape.MessageState, + MessageState: Message.shape.MessageState, }), ), response: z.object({ ChatPartnerId: z.number(), - MessageState: message.shape.MessageState, + MessageState: Message.shape.MessageState, SendTime: z.number(), }), }, - load_txt_record: { + LoadTxtRecord: { request: z.object({ - path: z.string(), + Path: z.string(), }), response: z.object({ - content: z.string(), + Content: z.string(), }), }, - authenticate_app: { + AuthenticateApp: { request: z.object({ AppIdentifier: z.string(), }), response: z.object({ - challenge: z.base64(), + Challenge: z.base64(), }), }, - create_app: { + CreateApp: { request: z.object({ AppPublicKey: z.base64(), AppIdentifier: z.string(), @@ -215,7 +213,7 @@ export const mtp = { }, // Calls - call_token: { + CallToken: { request: z.object({ CallId: z.string(), }), @@ -223,7 +221,7 @@ export const mtp = { CallToken: z.string(), }), }, - call_data: { + CallData: { request: z.object({ CallId: z.string(), }), @@ -231,7 +229,7 @@ export const mtp = { UserIds: z.array(z.number()), }), }, - call_invite: { + CallInvite: { request: z.object({ CallId: z.string(), CallSecret: z.base64(), @@ -243,7 +241,7 @@ export const mtp = { SenderId: z.number().optional(), }), }, - error_no_iota: { + ErrorNoIota: { request: z.object({}).optional(), response: z.object({}), }, @@ -255,7 +253,7 @@ export type MTP = typeof mtp; export interface Storage extends SettingsStorageDefaults { session_id: number; user_id: number; - private_key: string; + mtp_keyring: string; ppandtos_done: boolean; accepted_terms_of_service: boolean; accepted_privacy_policy: boolean; @@ -265,7 +263,9 @@ export interface Storage extends SettingsStorageDefaults { legal_docs: z.infer; cached_contacts: Contacts; cached_communities: Communities; - mtp_url: string; + omega_url: string; + forced_omikron_url: string | undefined; + forced_omikron_public_key: string | undefined; call_mute_range_start: number; call_mute_range_end: number; theme_color: string; @@ -287,7 +287,7 @@ export interface Storage extends SettingsStorageDefaults { export const storageDefaults: Storage = { session_id: 0, user_id: 0, - private_key: "", + mtp_keyring: "", ppandtos_done: false, accepted_terms_of_service: false, accepted_privacy_policy: false, @@ -314,7 +314,9 @@ export const storageDefaults: Storage = { }, cached_contacts: [], cached_communities: [], - mtp_url: "https://omega.tensamin.net", + omega_url: "https://omega.tensamin.net", + forced_omikron_url: undefined, + forced_omikron_public_key: undefined, call_mute_range_start: -55, call_mute_range_end: -45, theme_color: "", @@ -355,7 +357,7 @@ export const storageDefaults: Storage = { // User Status export function getStatusColor( - status: z.infer, + status: z.infer, ) { switch (status) { case "user_online": diff --git a/packages/storage/src/session.tsx b/packages/storage/src/session.tsx index 992a4df..f237192 100644 --- a/packages/storage/src/session.tsx +++ b/packages/storage/src/session.tsx @@ -86,7 +86,7 @@ export default function SessionProvider({ children }: { children: ReactNode }) { const newUser = { UserId: userId, LastMessageAt: new Date().getTime(), - messages: [], + Messages: [], } satisfies Contacts[0]; return [newUser, ...prevContacts]; diff --git a/packages/tauth/src/context.tsx b/packages/tauth/src/context.tsx index 5dea659..e2ca153 100644 --- a/packages/tauth/src/context.tsx +++ b/packages/tauth/src/context.tsx @@ -33,6 +33,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { const [redirect, setRedirect] = useState(null); const [challenge, setChallenge] = useState(null); const [appPublicKey, setAppPublicKey] = useState(null); + const [sessionId, setSessionId] = useState(null); const [allowChildern, setAllowChildern] = useState(false); const { deeplinks } = useDeeplinks(); @@ -44,9 +45,9 @@ export default function Wrapper({ children }: { children: ReactNode }) { // Get Data const user = await get(await load("user_id")); const { - data: { content: appPublicKeyHash }, - } = await send("load_txt_record", { - path: "tauth." + identifier, + data: { Content: appPublicKeyHash }, + } = await send("LoadTxtRecord", { + Path: "tauth." + identifier, }); const verifiedAppPublicKeyHash = await sha256Hex(appPublicKey); @@ -61,7 +62,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { // Get Shared Secret const sharedSecret = await getSharedSecret( - await load("private_key"), + await load("mtp_keyring"), user.PublicKey, appPublicKey, ).catch((err) => { @@ -86,12 +87,13 @@ export default function Wrapper({ children }: { children: ReactNode }) { finalUrl.searchParams.set("userId", String(await load("user_id"))); finalUrl.searchParams.set("challenge", solvedChallenge); finalUrl.searchParams.set("originalChallenge", challenge); - - const session = new Date().getTime(); - finalUrl.searchParams.set("sessionId", String(session)); + finalUrl.searchParams.set( + "sessionId", + String(sessionId || new Date().getTime()), + ); // Save Session - await send("create_app", { + await send("CreateApp", { AppPublicKey: appPublicKey, AppIdentifier: identifier, }); @@ -105,12 +107,13 @@ export default function Wrapper({ children }: { children: ReactNode }) { setRedirect(null); setChallenge(null); setAppPublicKey(null); + setSessionId(null); toast("success", "App authorized successfully"); return; } else { navigate({ - to: finalUrl.toString(), + href: finalUrl.toString(), }); return; } @@ -124,6 +127,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { setRedirect(null); setChallenge(null); setAppPublicKey(null); + setSessionId(null); } }; @@ -156,6 +160,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { const redirect = params.get("redirect"); const challenge = params.get("challenge"); const appPublicKey = params.get("public_key"); + const urlSessionId = params.get("sessionId"); if (!identifier || !redirect) { setAllowChildern(true); return; @@ -165,6 +170,10 @@ export default function Wrapper({ children }: { children: ReactNode }) { if (!challenge) { const newUrl = new URL(redirect); newUrl.searchParams.set("userId", String(userId)); + newUrl.searchParams.set( + "sessionId", + String(urlSessionId || new Date().getTime()), + ); window.location.href = newUrl.toString(); return; } @@ -181,6 +190,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { setRedirect(redirect); setChallenge(hexToBase64(challenge || "")); setAppPublicKey(appPublicKey); + setSessionId(urlSessionId); setDialogOpen(true); }); }, [searchStr, load]); @@ -192,12 +202,14 @@ export default function Wrapper({ children }: { children: ReactNode }) { const identifier = url.searchParams.get("identifier"); const redirect = url.searchParams.get("redirect"); const appPublicKey = url.searchParams.get("public_key"); + const urlSessionId = url.searchParams.get("sessionId"); if (identifier && redirect && appPublicKey) { setIdentifier(identifier); setRedirect(redirect); setChallenge(hexToBase64(url.searchParams.get("challenge") || "")); setAppPublicKey(appPublicKey); + setSessionId(urlSessionId); setDialogOpen(true); } } @@ -215,6 +227,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { setRedirect(null); setChallenge(null); setAppPublicKey(null); + setSessionId(null); } }} > @@ -251,6 +264,7 @@ export default function Wrapper({ children }: { children: ReactNode }) { setRedirect(null); setChallenge(null); setAppPublicKey(null); + setSessionId(null); }} > Deny diff --git a/packages/user/src/context.tsx b/packages/user/src/context.tsx index 84e56ec..001d5ae 100644 --- a/packages/user/src/context.tsx +++ b/packages/user/src/context.tsx @@ -4,7 +4,7 @@ import { useMTP } from "@tensamin/mtp"; import { mtp as schemas } from "@tensamin/shared/data"; import type z from "zod"; -export type User = z.infer; +export type User = z.infer; interface contextValue { get(userId: number): Promise; @@ -47,11 +47,11 @@ export default function UserProvider(props: { children: React.ReactNode }) { } const request = (async () => { - const userData = await send("get_user_data", { UserId: userId }); + const userData = await send("GetUserData", { UserId: userId }); const user = { ...userData.data, - avatar: userData.data.avatar - ? `data:image/webp;base64,${atob(userData.data.avatar)}` + avatar: userData.data.Avatar + ? `data:image/webp;base64,${atob(userData.data.Avatar)}` : undefined, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f9796e..f6a0c4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: overrides: '@tensamin/ui': https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz - mtp: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz + mtp: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz importers: @@ -16,8 +16,8 @@ importers: specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -661,8 +661,8 @@ importers: specifier: ^1.14.0 version: 1.23.0(react@19.2.7) mtp: - specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz - version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz + specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz + version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz react: specifier: ^19.2.0 version: 19.2.7 @@ -3752,8 +3752,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz: - resolution: {integrity: sha512-ixykS56llG6Azoxn4ff2v83LlrkCsd/s5wXmEUsxGtM63v8cYvX/HXh1AmWfJeaL0QZiGw1QyyOC0vplXZjwVg==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz} + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz: + resolution: {integrity: sha512-jv3tONmaEMVyoX3HyQhZfkt46MkoaZAkoy3B9HjFUOh6ff42yutAKgt/WQgX9MP5AyFd8oVlvTgMpaOGk7046A==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz} version: 0.1.0 nanoid@3.3.15: @@ -7879,7 +7879,7 @@ snapshots: ms@2.1.3: {} - mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz: {} + mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz: {} nanoid@3.3.15: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e9940f7..19faeb0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,4 @@ allowBuilds: esbuild: true overrides: "@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz" - mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz" + mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz"