From e5441580c99ebe6a709f88b9ef1867eddd569000 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 10 Feb 2026 15:17:50 +0900 Subject: [PATCH 01/62] user_dashboard: remove subMenu from side navigation --- .../components/left_bar/left_bar.module.scss | 19 ------------------ .../src/components/left_bar/left_bar.tsx | 20 ------------------- 2 files changed, 39 deletions(-) diff --git a/apps/user_dashboard/src/components/left_bar/left_bar.module.scss b/apps/user_dashboard/src/components/left_bar/left_bar.module.scss index 61ad733f4..4772a5b24 100644 --- a/apps/user_dashboard/src/components/left_bar/left_bar.module.scss +++ b/apps/user_dashboard/src/components/left_bar/left_bar.module.scss @@ -46,27 +46,8 @@ $header_height: 54px; } } -.subMenu { - display: flex; - flex-direction: column; - gap: 16px; -} - .icon { width: 20px; height: 20px; aspect-ratio: 1/1; } - -.featureRequestButton { - border: none; - background: none; - cursor: pointer; - width: 100%; - padding: 10px 12px; - text-align: left; - - &:hover { - background-color: var(--bg-primary-hover); - } -} diff --git a/apps/user_dashboard/src/components/left_bar/left_bar.tsx b/apps/user_dashboard/src/components/left_bar/left_bar.tsx index 906754d6c..1eb481d49 100644 --- a/apps/user_dashboard/src/components/left_bar/left_bar.tsx +++ b/apps/user_dashboard/src/components/left_bar/left_bar.tsx @@ -5,14 +5,8 @@ import cn from "classnames"; import { usePathname } from "next/navigation"; import type { FC } from "react"; -import { AccountInfoWithSubMenu } from "../account_info_with_sub_menu/account_info_with_sub_menu"; -import { ExternalLinkItem } from "../external_link_item/external_link_item"; import { navigationItems } from "./constant"; import styles from "./left_bar.module.scss"; -import { - OKO_FEATURE_REQUEST_ENDPOINT, - OKO_GET_SUPPORT_ENDPOINT, -} from "@oko-wallet-user-dashboard/fetch"; import { useViewState } from "@oko-wallet-user-dashboard/state/view"; export const LeftBar: FC = () => { @@ -39,20 +33,6 @@ export const LeftBar: FC = () => { /> ))} - -
- - -
- - Feature Request - - - - Get Support - -
-
); From 3d0a3676314a902c89b2da00c660fd788ac8b2b1 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 10 Feb 2026 15:31:33 +0900 Subject: [PATCH 02/62] user_dashboard: add user account menu to GNB --- .../dashboard_header.module.scss | 77 +++++++++ .../dashboard_header/dashboard_header.tsx | 152 +++++++++++++++++- apps/user_dashboard/src/paths.ts | 1 + 3 files changed, 223 insertions(+), 7 deletions(-) diff --git a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss index ad3f4c462..a5baf3809 100644 --- a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss +++ b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss @@ -15,6 +15,12 @@ } } +.rightSection { + display: flex; + align-items: center; + gap: 8px; +} + .menuIconWrapper { display: flex; align-items: center; @@ -24,3 +30,74 @@ display: none; } } + +// Account menu trigger +.accountTrigger { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 4px 8px; + border-radius: 8px; + + &:hover { + background-color: var(--bg-primary-hover); + } +} + +.authProviderIcon { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.accountEmail { + display: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 200px; + + @media (min-width: $desktop_min_width) { + display: block; + } +} + +.dotsButton { + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + width: 24px; + height: 24px; + flex-shrink: 0; +} + +// Dropdown menu +.accountMenu { + width: 252px; +} + +.menuHeader { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px 14px; + border-bottom: 1px solid var(--border-secondary); +} + +.menuUserInfo { + display: flex; + align-items: center; + gap: 8px; +} + +.menuEmail { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.menuSectionLabel { + padding-top: 4px; +} diff --git a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx index 75913e104..6406351e0 100644 --- a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx +++ b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx @@ -1,15 +1,55 @@ "use client"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import { AnchoredMenu } from "@oko-wallet/oko-common-ui/anchored_menu"; +import { DiscordIcon } from "@oko-wallet/oko-common-ui/icons/discord_icon"; +import { ExternalLinkOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/external_link_outlined"; +import { GoogleIcon } from "@oko-wallet/oko-common-ui/icons/google_icon"; +import { LogoutIcon } from "@oko-wallet/oko-common-ui/icons/logout"; +import { MailboxIcon } from "@oko-wallet/oko-common-ui/icons/mailbox"; import { MenuIcon } from "@oko-wallet/oko-common-ui/icons/menu"; +import { PasswordIcon } from "@oko-wallet/oko-common-ui/icons/password"; +import { TelegramIcon } from "@oko-wallet/oko-common-ui/icons/telegram_icon"; +import { ThreeDotsVerticalIcon } from "@oko-wallet/oko-common-ui/icons/three_dots_vertical"; import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close"; +import { XIcon } from "@oko-wallet/oko-common-ui/icons/x_icon"; import { Logo } from "@oko-wallet/oko-common-ui/logo"; +import { Typography } from "@oko-wallet/oko-common-ui/typography"; import type { Theme } from "@oko-wallet/oko-common-ui/theme"; import type { Property } from "csstype"; -import type { FC } from "react"; +import { useRouter } from "next/navigation"; +import type { FC, ReactNode } from "react"; import styles from "./dashboard_header.module.scss"; +import { + OKO_FEATURE_REQUEST_ENDPOINT, + OKO_GET_SUPPORT_ENDPOINT, +} from "@oko-wallet-user-dashboard/fetch"; +import { paths } from "@oko-wallet-user-dashboard/paths"; +import { + selectCosmosSDK, + useSDKState, +} from "@oko-wallet-user-dashboard/state/sdk"; +import { useUserInfoState } from "@oko-wallet-user-dashboard/state/user_info"; import { useViewState } from "@oko-wallet-user-dashboard/state/view"; +function getAuthProviderIcon(authType: AuthType | null, size = 16): ReactNode { + switch (authType) { + case "google": + return ; + case "discord": + return ; + case "telegram": + return ; + case "x": + return ; + case "auth0": + return ; + default: + return null; + } +} + export const DashboardHeader: FC<{ theme?: Theme; position?: Property.Position; @@ -17,16 +57,114 @@ export const DashboardHeader: FC<{ const isLeftBarOpen = useViewState((state) => state.isLeftBarOpen); const toggleLeftBarOpen = useViewState((state) => state.toggleLeftBarOpen); + const isSignedIn = useUserInfoState((state) => state.isSignedIn); + const email = useUserInfoState((state) => state.email); + const authType = useUserInfoState((state) => state.authType); + const clearUserInfo = useUserInfoState((state) => state.clearUserInfo); + const okoWallet = useSDKState(selectCosmosSDK)?.okoWallet; + const router = useRouter(); + return (
- - {isLeftBarOpen ? ( - - ) : ( - + +
+ {isSignedIn && ( + + + {getAuthProviderIcon(authType)} + + + {email} + + + + +
+ } + HeaderComponent={ +
+
+ + {getAuthProviderIcon(authType)} + + + {email} + +
+ + Security + +
+ } + menuItems={[ + { + id: "export-private-key", + label: "Export Private Key", + icon: , + onClick: () => { + router.push(paths.export_private_key); + }, + }, + { + id: "feature-request", + label: "Feature Request", + icon: , + onClick: () => { + window.open(OKO_FEATURE_REQUEST_ENDPOINT, "_blank"); + }, + }, + { + id: "get-support", + label: "Get Support", + icon: , + onClick: () => { + window.open(OKO_GET_SUPPORT_ENDPOINT, "_blank"); + }, + }, + { + id: "sign-out", + label: "Sign out", + icon: , + onClick: async () => { + if (!okoWallet) { + console.error("okoWallet is not initialized"); + return; + } + + await okoWallet.signOut(); + clearUserInfo(); + }, + }, + ]} + className={styles.accountMenu} + /> )} -
+ + + {isLeftBarOpen ? ( + + ) : ( + + )} + +
); }; diff --git a/apps/user_dashboard/src/paths.ts b/apps/user_dashboard/src/paths.ts index 49ad44630..e48d964c2 100644 --- a/apps/user_dashboard/src/paths.ts +++ b/apps/user_dashboard/src/paths.ts @@ -2,4 +2,5 @@ export const paths = { home: "/", sign_in: "/users/sign_in", transaction_history: "/transaction_history", + export_private_key: "/export_private_key", }; From 264c895bf15eceba9060047d180808535820b501 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 10 Feb 2026 16:00:03 +0900 Subject: [PATCH 03/62] user_dashboard: replace GNB logo --- .../src/app/users/sign_in/page.tsx | 2 +- .../dashboard_header.module.scss | 8 ++++++-- .../dashboard_header/dashboard_header.tsx | 16 +++++++++++----- .../src/components/left_bar/left_bar.module.scss | 2 +- .../whole_page_loading/whole_page_loading.tsx | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/user_dashboard/src/app/users/sign_in/page.tsx b/apps/user_dashboard/src/app/users/sign_in/page.tsx index d7a46a46e..4b70f53cb 100644 --- a/apps/user_dashboard/src/app/users/sign_in/page.tsx +++ b/apps/user_dashboard/src/app/users/sign_in/page.tsx @@ -15,7 +15,7 @@ export default function Page() { return ( <>
- +
diff --git a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss index a5baf3809..a45525cdb 100644 --- a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss +++ b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss @@ -1,12 +1,12 @@ @use "../../styles/viewport.module.scss" as *; .wrapper { - height: 54px; + height: 64px; display: flex; flex-shrink: 0; flex-grow: 0; align-items: center; - padding: 20px 16px; + padding: 16px 20px; z-index: 999; justify-content: space-between; @@ -15,6 +15,10 @@ } } +.logo { + flex-shrink: 0; +} + .rightSection { display: flex; align-items: center; diff --git a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx index 6406351e0..455a2353c 100644 --- a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx +++ b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx @@ -13,9 +13,7 @@ import { TelegramIcon } from "@oko-wallet/oko-common-ui/icons/telegram_icon"; import { ThreeDotsVerticalIcon } from "@oko-wallet/oko-common-ui/icons/three_dots_vertical"; import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close"; import { XIcon } from "@oko-wallet/oko-common-ui/icons/x_icon"; -import { Logo } from "@oko-wallet/oko-common-ui/logo"; import { Typography } from "@oko-wallet/oko-common-ui/typography"; -import type { Theme } from "@oko-wallet/oko-common-ui/theme"; import type { Property } from "csstype"; import { useRouter } from "next/navigation"; import type { FC, ReactNode } from "react"; @@ -50,10 +48,12 @@ function getAuthProviderIcon(authType: AuthType | null, size = 16): ReactNode { } } +const OKO_LOGO_URL = + "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/icons/oko_logo.png"; + export const DashboardHeader: FC<{ - theme?: Theme; position?: Property.Position; -}> = ({ theme = "light", position = "static" }) => { +}> = ({ position = "static" }) => { const isLeftBarOpen = useViewState((state) => state.isLeftBarOpen); const toggleLeftBarOpen = useViewState((state) => state.toggleLeftBarOpen); @@ -66,7 +66,13 @@ export const DashboardHeader: FC<{ return (
- + Oko
{isSignedIn && ( diff --git a/apps/user_dashboard/src/components/left_bar/left_bar.module.scss b/apps/user_dashboard/src/components/left_bar/left_bar.module.scss index 4772a5b24..8ace25b62 100644 --- a/apps/user_dashboard/src/components/left_bar/left_bar.module.scss +++ b/apps/user_dashboard/src/components/left_bar/left_bar.module.scss @@ -1,6 +1,6 @@ @use "../../styles/viewport.module.scss" as *; -$header_height: 54px; +$header_height: 64px; .wrapper { display: none; diff --git a/apps/user_dashboard/src/components/whole_page_loading/whole_page_loading.tsx b/apps/user_dashboard/src/components/whole_page_loading/whole_page_loading.tsx index 2050dd137..ccc1756ff 100644 --- a/apps/user_dashboard/src/components/whole_page_loading/whole_page_loading.tsx +++ b/apps/user_dashboard/src/components/whole_page_loading/whole_page_loading.tsx @@ -15,7 +15,7 @@ export const WholePageLoading: FC = () => { return ( <>
- +
From cd7eb0c8f68b92c8c363f053fc5768c16e97825b Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 10 Feb 2026 19:01:11 +0900 Subject: [PATCH 04/62] user_dashboard: add export private key step 1 page --- .../src/app/export_private_key/layout.tsx | 31 +++ .../app/export_private_key/page.module.scss | 106 ++++++++++ .../src/app/export_private_key/page.tsx | 184 ++++++++++++++++++ .../dashboard_header.module.scss | 12 +- .../dashboard_header/dashboard_header.tsx | 176 +++++++++++------ apps/user_dashboard/src/state/sdk.ts | 3 +- apps/user_dashboard/src/state/user_info.ts | 5 + .../styles/layout_with_left_bar.module.scss | 2 +- .../anchored_menu/anchored_menu.module.scss | 43 +++- .../src/anchored_menu/anchored_menu.tsx | 123 +++++++++--- ui/oko_common_ui/src/icons/discord_icon.tsx | 2 +- ui/oko_common_ui/src/icons/x_icon.tsx | 10 +- 12 files changed, 600 insertions(+), 97 deletions(-) create mode 100644 apps/user_dashboard/src/app/export_private_key/layout.tsx create mode 100644 apps/user_dashboard/src/app/export_private_key/page.module.scss create mode 100644 apps/user_dashboard/src/app/export_private_key/page.tsx diff --git a/apps/user_dashboard/src/app/export_private_key/layout.tsx b/apps/user_dashboard/src/app/export_private_key/layout.tsx new file mode 100644 index 000000000..8566174c5 --- /dev/null +++ b/apps/user_dashboard/src/app/export_private_key/layout.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from "react"; + +import "@oko-wallet/oko-common-ui/styles/colors.scss"; +import "@oko-wallet/oko-common-ui/styles/typography.scss"; +import "@oko-wallet/oko-common-ui/styles/shadow.scss"; + +import { Authorized } from "@oko-wallet-user-dashboard/components/authorized/authorized"; +import { DashboardBody } from "@oko-wallet-user-dashboard/components/dashboard_body/dashboard_body"; +import { DashboardHeader } from "@oko-wallet-user-dashboard/components/dashboard_header/dashboard_header"; +import { LeftBar } from "@oko-wallet-user-dashboard/components/left_bar/left_bar"; +import styles from "@oko-wallet-user-dashboard/styles/layout_with_left_bar.module.scss"; + +export default function ExportPrivateKeyLayout({ + children, +}: Readonly<{ + children: ReactNode; +}>) { + return ( + +
+ +
+ + +
{children}
+
+
+
+
+ ); +} diff --git a/apps/user_dashboard/src/app/export_private_key/page.module.scss b/apps/user_dashboard/src/app/export_private_key/page.module.scss new file mode 100644 index 000000000..1507cb927 --- /dev/null +++ b/apps/user_dashboard/src/app/export_private_key/page.module.scss @@ -0,0 +1,106 @@ +@use "../../styles/viewport.module.scss" as *; + +.container { + display: flex; + flex-wrap: wrap; + gap: 64px; + align-items: flex-start; +} + +.heading { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + min-width: 300px; +} + +.headingIcon { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + color: var(--fg-quaternary); +} + +.stepBadge { + display: flex; + align-items: center; + padding: 2px 8px; + border-radius: 9999px; + background-color: var(--bg-secondary); + border: 1px solid var(--border-secondary); +} + +.content { + flex: 1 0 0; + min-width: 0; + max-width: 360px; + padding: 2px 0; +} + +.loginSection { + display: flex; + flex-direction: column; + border-bottom: 1px solid var(--border-secondary); + padding-bottom: 32px; + margin-bottom: 32px; +} + +.loginLabel { + margin-bottom: 12px; +} + +.authCard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: 16px; + background-color: var(--bg-secondary); + border-radius: 12px; +} + +.authCardRow { + display: flex; + align-items: center; + gap: 4px; +} + +.warningSection { + display: flex; + flex-direction: column; + gap: 24px; + padding: 0 8px; + margin-bottom: 36px; +} + +.warningItem { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.warningIconWrap { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 28px; + height: 28px; + padding: 6px; + background-color: var(--bg-warning-secondary); + border-radius: 8px; + color: var(--text-warning-primary); +} + +.warningText { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-width: 0; +} diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx new file mode 100644 index 000000000..f8a9e052c --- /dev/null +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -0,0 +1,184 @@ +"use client"; + +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import { DiscordIcon } from "@oko-wallet/oko-common-ui/icons/discord_icon"; +import { GoogleIcon } from "@oko-wallet/oko-common-ui/icons/google_icon"; +import { MailboxIcon } from "@oko-wallet/oko-common-ui/icons/mailbox"; +import { PasswordIcon } from "@oko-wallet/oko-common-ui/icons/password"; +import { TelegramIcon } from "@oko-wallet/oko-common-ui/icons/telegram_icon"; +import { XIcon } from "@oko-wallet/oko-common-ui/icons/x_icon"; +import { Button } from "@oko-wallet/oko-common-ui/button"; +import { Typography } from "@oko-wallet/oko-common-ui/typography"; +import type { ReactNode } from "react"; + +import styles from "./page.module.scss"; +import { useUserInfoState } from "@oko-wallet-user-dashboard/state/user_info"; + +function getAuthProviderInfo(authType: AuthType | null): { + icon: ReactNode; + label: string; +} { + switch (authType) { + case "google": + return { + icon: , + label: "Google Login", + }; + case "discord": + return { icon: , label: "Discord" }; + case "telegram": + return { icon: , label: "Telegram" }; + case "x": + return { icon: , label: "X" }; + case "auth0": + return { icon: , label: "Email" }; + default: + return { icon: null, label: "" }; + } +} + +function LockIcon() { + return ( + + + + + ); +} + +function AlertTriangleIcon() { + return ( + + + + + + ); +} + +function KeyIcon() { + return ( + + + + ); +} + +export default function Page() { + const email = useUserInfoState((state) => state.email); + const name = useUserInfoState((state) => state.name); + const authType = useUserInfoState((state) => state.authType); + const authInfo = getAuthProviderInfo(authType); + const usesName = authType === "discord" || authType === "telegram" || authType === "x"; + const displayIdentifier = usesName ? name : email; + + return ( +
+
+ + + + + Export Private Key + + + + 1/2 + + +
+ +
+ + Log in again to reveal your private key + + +
+ +
+ + You're logged in with: + +
+
+ {authInfo.icon} + + {authInfo.label} + +
+ + {displayIdentifier} + +
+
+ +
+
+ + + +
+ + Keep your private key secret. + + + Anyone with it can take full control of your wallet and steal + your funds. + +
+
+
+ + + +
+ + Using or importing this key outside Oko changes how the wallet + is protected. + + + You'll be fully responsible for managing your wallet. + +
+
+
+ + +
+
+ ); +} diff --git a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss index a45525cdb..86d67dfc2 100644 --- a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss +++ b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.module.scss @@ -84,16 +84,16 @@ .menuHeader { display: flex; - flex-direction: column; - gap: 12px; - padding: 12px 14px; - border-bottom: 1px solid var(--border-secondary); + padding: 6px; } .menuUserInfo { display: flex; align-items: center; gap: 8px; + padding: 6px 8px; + flex: 1; + min-width: 0; } .menuEmail { @@ -101,7 +101,3 @@ text-overflow: ellipsis; white-space: nowrap; } - -.menuSectionLabel { - padding-top: 4px; -} diff --git a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx index 455a2353c..9c7743d93 100644 --- a/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx +++ b/apps/user_dashboard/src/components/dashboard_header/dashboard_header.tsx @@ -6,9 +6,7 @@ import { DiscordIcon } from "@oko-wallet/oko-common-ui/icons/discord_icon"; import { ExternalLinkOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/external_link_outlined"; import { GoogleIcon } from "@oko-wallet/oko-common-ui/icons/google_icon"; import { LogoutIcon } from "@oko-wallet/oko-common-ui/icons/logout"; -import { MailboxIcon } from "@oko-wallet/oko-common-ui/icons/mailbox"; import { MenuIcon } from "@oko-wallet/oko-common-ui/icons/menu"; -import { PasswordIcon } from "@oko-wallet/oko-common-ui/icons/password"; import { TelegramIcon } from "@oko-wallet/oko-common-ui/icons/telegram_icon"; import { ThreeDotsVerticalIcon } from "@oko-wallet/oko-common-ui/icons/three_dots_vertical"; import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close"; @@ -41,13 +39,62 @@ function getAuthProviderIcon(authType: AuthType | null, size = 16): ReactNode { return ; case "x": return ; - case "auth0": - return ; default: return null; } } +function MenuKeyIcon() { + return ( + + + + ); +} + +function MenuLightbulbIcon() { + return ( + + + + ); +} + +function MenuChatIcon() { + return ( + + + + ); +} + const OKO_LOGO_URL = "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/icons/oko_logo.png"; @@ -59,8 +106,12 @@ export const DashboardHeader: FC<{ const isSignedIn = useUserInfoState((state) => state.isSignedIn); const email = useUserInfoState((state) => state.email); + const name = useUserInfoState((state) => state.name); const authType = useUserInfoState((state) => state.authType); const clearUserInfo = useUserInfoState((state) => state.clearUserInfo); + const usesName = + authType === "discord" || authType === "telegram" || authType === "x"; + const displayIdentifier = usesName ? name : email; const okoWallet = useSDKState(selectCosmosSDK)?.okoWallet; const router = useRouter(); @@ -80,15 +131,17 @@ export const DashboardHeader: FC<{ placement="bottom-end" TriggerComponent={
- - {getAuthProviderIcon(authType)} - + {authType !== "auth0" && ( + + {getAuthProviderIcon(authType, 24)} + + )} - {email} + {displayIdentifier} @@ -98,67 +151,80 @@ export const DashboardHeader: FC<{ HeaderComponent={
- - {getAuthProviderIcon(authType)} - + {authType !== "auth0" && ( + + {getAuthProviderIcon(authType, 24)} + + )} - {email} + {displayIdentifier}
- - Security -
} - menuItems={[ - { - id: "export-private-key", - label: "Export Private Key", - icon: , - onClick: () => { - router.push(paths.export_private_key); - }, - }, + menuSections={[ { - id: "feature-request", - label: "Feature Request", - icon: , - onClick: () => { - window.open(OKO_FEATURE_REQUEST_ENDPOINT, "_blank"); - }, + id: "security", + label: "Security", + items: [ + { + id: "export-private-key", + label: "Export Private Key", + icon: , + onClick: () => { + router.push(paths.export_private_key); + }, + }, + ], }, { - id: "get-support", - label: "Get Support", - icon: , - onClick: () => { - window.open(OKO_GET_SUPPORT_ENDPOINT, "_blank"); - }, + id: "links", + items: [ + { + id: "feature-request", + label: "Feature Request", + icon: , + trailingIcon: , + onClick: () => { + window.open(OKO_FEATURE_REQUEST_ENDPOINT, "_blank"); + }, + }, + { + id: "get-support", + label: "Get Support", + icon: , + trailingIcon: , + onClick: () => { + window.open(OKO_GET_SUPPORT_ENDPOINT, "_blank"); + }, + }, + ], }, - { - id: "sign-out", - label: "Sign out", - icon: , - onClick: async () => { - if (!okoWallet) { - console.error("okoWallet is not initialized"); - return; - } + ]} + footerSection={{ + id: "account", + items: [ + { + id: "sign-out", + label: "Sign out", + icon: , + onClick: async () => { + if (!okoWallet) { + console.error("okoWallet is not initialized"); + return; + } - await okoWallet.signOut(); - clearUserInfo(); + await okoWallet.signOut(); + clearUserInfo(); + }, }, - }, - ]} + ], + }} className={styles.accountMenu} /> )} diff --git a/apps/user_dashboard/src/state/sdk.ts b/apps/user_dashboard/src/state/sdk.ts index 5baf5dde1..30e9279e5 100644 --- a/apps/user_dashboard/src/state/sdk.ts +++ b/apps/user_dashboard/src/state/sdk.ts @@ -190,9 +190,10 @@ export const useSDKState = create( // Setup auth state listener - updates user_info store directly okoCosmos.on({ type: "accountsChanged", - handler: ({ email, publicKey }) => { + handler: ({ email, name, publicKey }) => { useUserInfoState.getState().setUserInfo({ email: email || null, + name: name || null, publicKey: publicKey ? Buffer.from(publicKey).toString("hex") : null, diff --git a/apps/user_dashboard/src/state/user_info.ts b/apps/user_dashboard/src/state/user_info.ts index ef54f434f..29285fb40 100644 --- a/apps/user_dashboard/src/state/user_info.ts +++ b/apps/user_dashboard/src/state/user_info.ts @@ -4,6 +4,7 @@ import { combine, createJSONStorage, persist } from "zustand/middleware"; interface UserInfoState { email: string | null; + name: string | null; publicKey: string | null; isSignedIn: boolean; authType: AuthType | null; @@ -12,6 +13,7 @@ interface UserInfoState { interface UserInfoActions { setUserInfo: (info: { email: string | null; + name: string | null; publicKey: string | null; }) => void; setAuthType: (authType: AuthType | null) => void; @@ -23,6 +25,7 @@ export const useUserInfoState = create( combine( { email: null, + name: null, publicKey: null, isSignedIn: false, authType: null, @@ -31,6 +34,7 @@ export const useUserInfoState = create( setUserInfo: (info) => { set({ email: info.email, + name: info.name, publicKey: info.publicKey, isSignedIn: !!(info.email && info.publicKey), }); @@ -41,6 +45,7 @@ export const useUserInfoState = create( clearUserInfo: () => { set({ email: null, + name: null, publicKey: null, isSignedIn: false, authType: null, diff --git a/apps/user_dashboard/src/styles/layout_with_left_bar.module.scss b/apps/user_dashboard/src/styles/layout_with_left_bar.module.scss index 00371bfab..0915b6303 100644 --- a/apps/user_dashboard/src/styles/layout_with_left_bar.module.scss +++ b/apps/user_dashboard/src/styles/layout_with_left_bar.module.scss @@ -15,7 +15,7 @@ } .content { - padding-top: 24px; + padding-top: 28px; display: flex; flex-direction: column; diff --git a/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss b/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss index 9e6656d7a..2743d9b89 100644 --- a/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss +++ b/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss @@ -6,6 +6,7 @@ border-radius: 12px; border: 1px solid var(--border-secondary-alt); background: var(--bg-secondary-alt); + overflow: hidden; box-shadow: var(--shadow-lg); } @@ -16,6 +17,29 @@ padding: 0; } +.menuInner { + background: var(--bg-primary); + border: 1px solid var(--border-secondary); + border-radius: 12px 12px 16px 16px; +} + +.menuFooter { + list-style: none; + margin: 0; + padding: 4px 0 6px; +} + +.menuSection { + list-style: none; + margin: 0; + padding: 6px 0; + border-top: 1px solid var(--border-secondary); +} + +.menuSectionLabel { + padding: 6px 12px 4px; +} + .menuItem { display: flex; padding: 12px 14px 14px 14px; @@ -38,15 +62,30 @@ align-items: center; justify-content: center; - width: 16px; - height: 16px; + width: 24px; + height: 24px; margin-right: 8px; + flex-shrink: 0; color: var(--fg-quaternary); } .menuItemLabel { + flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.menuItemTrailingIcon { + display: flex; + align-items: center; + justify-content: center; + + width: 20px; + height: 20px; + margin-left: 8px; + flex-shrink: 0; + + color: var(--fg-quaternary); +} diff --git a/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx b/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx index fc0b35a8a..6a891e9d7 100644 --- a/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx +++ b/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx @@ -18,10 +18,38 @@ import { Typography } from "@oko-wallet/oko-common-ui/typography"; import styles from "./anchored_menu.module.scss"; +const MenuItemRow: FC<{ + item: AnchoredMenuItem; + onClick: (item: AnchoredMenuItem) => void; +}> = ({ item, onClick }) => ( +
  • onClick(item)} + > + {item.icon && {item.icon}} + + {item.label} + + {item.trailingIcon && ( + + {item.trailingIcon} + + )} +
  • +); + export const AnchoredMenu: FC = ({ TriggerComponent, HeaderComponent = null, menuItems, + menuSections, + footerSection, placement = "right-start", className, }) => { @@ -52,6 +80,45 @@ export const AnchoredMenu: FC = ({ setIsOpen(false); }, []); + const sectionsContent = menuSections ? ( + menuSections.map((section) => ( +
      + {section.label && ( +
    • + + {section.label} + +
    • + )} + {section.items.map((item) => ( + + ))} +
    + )) + ) : ( +
      + {menuItems?.map((item) => ( + + ))} +
    + ); + return ( <>
    = ({ className={cn(styles.menu, className)} {...getFloatingProps()} > - {HeaderComponent} -
      - {menuItems.map((item) => ( -
    • handleMenuItemClick(item)} - > - {item.icon && ( - {item.icon} - )} - - {item.label} - -
    • - ))} -
    + {footerSection ? ( +
    + {HeaderComponent} + {sectionsContent} +
    + ) : ( + <> + {HeaderComponent} + {sectionsContent} + + )} + {footerSection && ( +
      + {footerSection.items.map((item) => ( + + ))} +
    + )}
    )} @@ -106,11 +172,20 @@ export type AnchoredMenuItem = { label: string; onClick: () => void; icon?: ReactNode; + trailingIcon?: ReactNode; +}; + +export type AnchoredMenuSection = { + id: string; + label?: string; + items: AnchoredMenuItem[]; }; export type AnchoredMenuProps = { TriggerComponent: ReactNode; - menuItems: AnchoredMenuItem[]; + menuItems?: AnchoredMenuItem[]; + menuSections?: AnchoredMenuSection[]; + footerSection?: AnchoredMenuSection; placement?: Placement; disabled?: boolean; HeaderComponent?: ReactNode; diff --git a/ui/oko_common_ui/src/icons/discord_icon.tsx b/ui/oko_common_ui/src/icons/discord_icon.tsx index 9389f0241..07fc9acbf 100644 --- a/ui/oko_common_ui/src/icons/discord_icon.tsx +++ b/ui/oko_common_ui/src/icons/discord_icon.tsx @@ -9,7 +9,7 @@ export const DiscordIcon: FC = ({ size = 20 }) => { viewBox="0 0 21 21" fill="none" > - + = ({ size = 24 }) => { xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" > - + From 8b517d89bbb4adadb3a741b82d4a4142264bb876 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 10 Feb 2026 19:55:38 +0900 Subject: [PATCH 05/62] o --- .../anchored_menu/anchored_menu.module.scss | 15 +- .../src/anchored_menu/anchored_menu.tsx | 144 ++++++++++-------- 2 files changed, 86 insertions(+), 73 deletions(-) diff --git a/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss b/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss index b26149d29..24ddf1626 100644 --- a/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss +++ b/ui/oko_common_ui/src/anchored_menu/anchored_menu.module.scss @@ -14,7 +14,7 @@ box-shadow: var(--shadow-lg); } -/* Figma: Menu items wrapper (569:6265) + Menu items (569:6266) */ +/* Figma: Menu items wrapper (569:6265) + Menu items (569:6266) — flat list */ .menuList { list-style: none; margin: 0; @@ -28,16 +28,20 @@ overflow: clip; } +/* Sections wrapper — white inner card for sectioned menus */ .menuInner { - background: var(--bg-primary); + width: 100%; + background: var(--bg-primary, white); border: 1px solid var(--border-secondary); border-radius: 12px 12px 16px 16px; } +/* Footer section — sits outside menuInner, inherits gray bg from .menu */ .menuFooter { list-style: none; margin: 0; padding: 4px 0 6px; + width: 100%; } .menuSection { @@ -80,14 +84,10 @@ display: flex; align-items: center; justify-content: center; - width: 24px; height: 24px; - margin-right: 8px; flex-shrink: 0; - color: var(--fg-quaternary); - flex-shrink: 0; } .menuItemLabel { @@ -101,11 +101,8 @@ display: flex; align-items: center; justify-content: center; - width: 20px; height: 20px; - margin-left: 8px; flex-shrink: 0; - color: var(--fg-quaternary); } diff --git a/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx b/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx index 0cba2fbba..22f355fec 100644 --- a/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx +++ b/ui/oko_common_ui/src/anchored_menu/anchored_menu.tsx @@ -22,19 +22,25 @@ const MenuItemRow: FC<{ item: AnchoredMenuItem; onClick: (item: AnchoredMenuItem) => void; }> = ({ item, onClick }) => ( -
  • onClick(item)}> - {item.icon && {item.icon}} - - {item.label} - - {item.trailingIcon && ( - {item.trailingIcon} - )} +
  • onClick(item)} + > +
    + {item.icon && {item.icon}} + + {item.label} + + {item.trailingIcon && ( + {item.trailingIcon} + )} +
  • ); @@ -74,33 +80,6 @@ export const AnchoredMenu: FC = ({ setIsOpen(false); }, []); - const sectionsContent = menuSections ? ( - menuSections.map((section) => ( -
      - {section.label && ( -
    • - - {section.label} - -
    • - )} - {section.items.map((item) => ( - - ))} -
    - )) - ) : ( -
      - {menuItems?.map((item) => ( - - ))} -
    - ); - return ( <>
    = ({ className={cn(styles.menu, className)} {...getFloatingProps()} > - {HeaderComponent} -
      - {menuItems.map((item) => ( -
    • handleMenuItemClick(item)} - > -
      - {item.icon && ( - {item.icon} + {menuSections ? ( +
      + {HeaderComponent} + {menuSections.map((section) => ( +
        + {section.label && ( +
      • + + {section.label} + +
      • )} - - {item.label} - -
      -
    • - ))} -
    + {section.items.map((item) => ( + + ))} + + ))} +
    + ) : ( + <> + {HeaderComponent} +
      + {menuItems?.map((item) => ( + + ))} +
    + + )} + {footerSection && ( +
      + {footerSection.items.map((item) => ( + + ))} +
    + )}
    )} @@ -157,10 +166,17 @@ export type AnchoredMenuItem = { label: string; onClick: () => void; icon?: ReactNode; + trailingIcon?: ReactNode; className?: string; labelColor?: Parameters[0]["color"]; }; +export type AnchoredMenuSection = { + id: string; + label?: string; + items: AnchoredMenuItem[]; +}; + export type AnchoredMenuProps = { TriggerComponent: ReactNode; menuItems?: AnchoredMenuItem[]; From a97265c20af8d4e972f60de736d4e1ed1a7b96e7 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Wed, 11 Feb 2026 12:41:37 +0900 Subject: [PATCH 06/62] user_dashboard: add export private key step 2 page --- .../src/app/export_private_key/layout.tsx | 2 + .../app/export_private_key/page.module.scss | 70 ++++ .../src/app/export_private_key/page.tsx | 328 ++++++++++++++---- 3 files changed, 334 insertions(+), 66 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/layout.tsx b/apps/user_dashboard/src/app/export_private_key/layout.tsx index 8566174c5..16f1a96ab 100644 --- a/apps/user_dashboard/src/app/export_private_key/layout.tsx +++ b/apps/user_dashboard/src/app/export_private_key/layout.tsx @@ -8,6 +8,7 @@ import { Authorized } from "@oko-wallet-user-dashboard/components/authorized/aut import { DashboardBody } from "@oko-wallet-user-dashboard/components/dashboard_body/dashboard_body"; import { DashboardHeader } from "@oko-wallet-user-dashboard/components/dashboard_header/dashboard_header"; import { LeftBar } from "@oko-wallet-user-dashboard/components/left_bar/left_bar"; +import { ToastContainer } from "@oko-wallet-user-dashboard/components/toast"; import styles from "@oko-wallet-user-dashboard/styles/layout_with_left_bar.module.scss"; export default function ExportPrivateKeyLayout({ @@ -26,6 +27,7 @@ export default function ExportPrivateKeyLayout({
    + ); } diff --git a/apps/user_dashboard/src/app/export_private_key/page.module.scss b/apps/user_dashboard/src/app/export_private_key/page.module.scss index 1507cb927..87ba153a5 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.module.scss +++ b/apps/user_dashboard/src/app/export_private_key/page.module.scss @@ -41,6 +41,8 @@ padding: 2px 0; } +/* Step 1 styles */ + .loginSection { display: flex; flex-direction: column; @@ -104,3 +106,71 @@ flex: 1; min-width: 0; } + +/* Step 2 styles */ + +.privateKeySection { + display: flex; + flex-direction: column; +} + +.privateKeyLabel { + margin-bottom: 12px; +} + +.privateKeyField { + position: relative; + height: 68px; + width: 100%; + cursor: pointer; + + &:hover .privateKeyBg { + background-color: var(--bg-secondary-hover, #f5f5f5); + } +} + +.privateKeyBg { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 12px 16px; + background-color: var(--bg-secondary); + border-radius: 12px; + overflow: hidden; + word-break: break-all; +} + +.privateKeyTextBlurred { + filter: blur(4px); + opacity: 0.5; +} + +.privateKeyHint { + position: absolute; + inset: 0; + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; +} + +.eyeOffIcon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex-shrink: 0; + color: var(--fg-primary); +} + +.copyButtonIcon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex-shrink: 0; +} diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index f8a9e052c..2fb48146a 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -4,14 +4,19 @@ import type { AuthType } from "@oko-wallet/oko-types/auth"; import { DiscordIcon } from "@oko-wallet/oko-common-ui/icons/discord_icon"; import { GoogleIcon } from "@oko-wallet/oko-common-ui/icons/google_icon"; import { MailboxIcon } from "@oko-wallet/oko-common-ui/icons/mailbox"; -import { PasswordIcon } from "@oko-wallet/oko-common-ui/icons/password"; import { TelegramIcon } from "@oko-wallet/oko-common-ui/icons/telegram_icon"; import { XIcon } from "@oko-wallet/oko-common-ui/icons/x_icon"; import { Button } from "@oko-wallet/oko-common-ui/button"; import { Typography } from "@oko-wallet/oko-common-ui/typography"; -import type { ReactNode } from "react"; +import { type ReactNode, useCallback, useState } from "react"; import styles from "./page.module.scss"; +import { displayToast } from "@oko-wallet-user-dashboard/components/toast"; +import { useCopyToClipboard } from "@oko-wallet-user-dashboard/hooks/use_copy_to_clipboard"; +import { + selectCosmosSDK, + useSDKState, +} from "@oko-wallet-user-dashboard/state/sdk"; import { useUserInfoState } from "@oko-wallet-user-dashboard/state/user_info"; function getAuthProviderInfo(authType: AuthType | null): { @@ -91,14 +96,252 @@ function KeyIcon() { ); } +function CopyIcon() { + return ( + + + + ); +} + +function EyeOffIcon() { + return ( + + + + ); +} + +function Step1Content({ + authInfo, + displayIdentifier, + isLoading, + onContinue, +}: { + authInfo: { icon: ReactNode; label: string }; + displayIdentifier: string | null; + isLoading: boolean; + onContinue: () => void; +}) { + return ( + <> + + Log in again to reveal your private key + + +
    + +
    + + You're logged in with: + +
    +
    + {authInfo.icon} + + {authInfo.label} + +
    + + {displayIdentifier} + +
    +
    + +
    +
    + + + +
    + + Keep your private key secret. + + + Anyone with it can take full control of your wallet and steal your + funds. + +
    +
    +
    + + + +
    + + Using or importing this key outside Oko changes how the wallet is + protected. + + + You'll be fully responsible for managing your wallet. + +
    +
    +
    + + + + ); +} + +function Step2Content({ + privateKey, + isRevealed, + onToggleReveal, + onCopy, +}: { + privateKey: string; + isRevealed: boolean; + onToggleReveal: () => void; + onCopy: () => void; +}) { + return ( + <> + + View and copy your private key + + +
    + +
    + + Private Key + +
    { + if (e.key === "Enter" || e.key === " ") { + onToggleReveal(); + } + }} + > +
    + + {privateKey} + +
    + {!isRevealed && ( +
    + + + + + Click or tap to reveal your private key. +
    + Ensure no one else can see your screen. +
    +
    + )} +
    +
    + +
    + + + + ); +} + export default function Page() { const email = useUserInfoState((state) => state.email); const name = useUserInfoState((state) => state.name); const authType = useUserInfoState((state) => state.authType); const authInfo = getAuthProviderInfo(authType); - const usesName = authType === "discord" || authType === "telegram" || authType === "x"; + const usesName = + authType === "discord" || authType === "telegram" || authType === "x"; const displayIdentifier = usesName ? name : email; + const okoWallet = useSDKState(selectCosmosSDK)?.okoWallet; + + const [step, setStep] = useState<1 | 2>(1); + const [isLoading, setIsLoading] = useState(false); + const [isRevealed, setIsRevealed] = useState(false); + const [privateKey, setPrivateKey] = useState(null); + const { copy } = useCopyToClipboard(); + + const handleContinue = useCallback(async () => { + if (!okoWallet || !authType) { + return; + } + + try { + setIsLoading(true); + await okoWallet.signIn(authType === "auth0" ? "email" : authType); + + // TODO: Verify re-authenticated account matches the current account (prevent account switch) + // TODO: Replace with actual private key export when SDK API is available + const mockPrivateKey = "0x" + "0".repeat(64); + setPrivateKey(mockPrivateKey); + setStep(2); + } catch (error) { + console.error("Re-authentication failed:", error); + displayToast({ + variant: "error", + title: "Login Failed", + description: "Please try again.", + }); + } finally { + setIsLoading(false); + } + }, [okoWallet, authType]); + + const handleCopy = useCallback(async () => { + if (!privateKey) { + return; + } + const success = await copy(privateKey); + if (success) { + displayToast({ variant: "success", title: "Copied!" }); + } + }, [privateKey, copy]); + + const handleToggleReveal = useCallback(() => { + setIsRevealed((prev) => !prev); + }, []); + return (
    @@ -110,74 +353,27 @@ export default function Page() { - 1/2 + {step}/2
    - - Log in again to reveal your private key - - -
    - -
    - - You're logged in with: - -
    -
    - {authInfo.icon} - - {authInfo.label} - -
    - - {displayIdentifier} - -
    -
    - -
    -
    - - - -
    - - Keep your private key secret. - - - Anyone with it can take full control of your wallet and steal - your funds. - -
    -
    -
    - - - -
    - - Using or importing this key outside Oko changes how the wallet - is protected. - - - You'll be fully responsible for managing your wallet. - -
    -
    -
    - - + {step === 1 ? ( + + ) : ( + + )}
    ); From a4f47336fe0f0a583247785cdc0f1a30f540f705 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Wed, 11 Feb 2026 16:33:09 +0900 Subject: [PATCH 07/62] o --- apps/user_dashboard/src/app/export_private_key/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 2fb48146a..59e1a917c 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -319,7 +319,7 @@ export default function Page() { } catch (error) { console.error("Re-authentication failed:", error); displayToast({ - variant: "error", + variant: "confirm", title: "Login Failed", description: "Please try again.", }); From 84803a82af01a35ab6d055e07d254285271110a9 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Wed, 11 Feb 2026 19:41:21 +0900 Subject: [PATCH 08/62] attached: add export_private_key handler --- .../src/window_msgs/export_private_key.ts | 148 ++++++++++++++++++ embed/oko_attached/src/window_msgs/index.ts | 21 ++- 2 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 embed/oko_attached/src/window_msgs/export_private_key.ts diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts new file mode 100644 index 000000000..59c6faaef --- /dev/null +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -0,0 +1,148 @@ +import type { MsgEventContext } from "./types"; +import { OKO_SDK_TARGET } from "./target"; +import { useAppState } from "@oko-wallet-attached/store/app"; +import { + OKO_API_ENDPOINT, + USER_DASHBOARD_ORIGINS, +} from "@oko-wallet-attached/requests/endpoints"; + +// NOTE: This handler is user_dashboard-only (not exposed via SDK). +// Types are defined locally, following the __get_connected_apps__ pattern. + +type ExportPrivateKeyError = + | { type: "UNAUTHORIZED_ORIGIN" } + | { type: "NOT_AUTHENTICATED" } + | { type: "MISSING_KEYSHARE" } + | { type: "COMBINE_ERROR"; error: string } + | { type: "API_ERROR"; error: string }; + +type ExportPrivateKeyAckPayload = + | { success: true; data: { secp256k1: string; ed25519: string } } + | { success: false; error: ExportPrivateKeyError }; + +interface OkoWalletMsgExportPrivateKeyAck { + target: "oko_sdk"; + msg_type: "__export_private_key_ack__"; + payload: ExportPrivateKeyAckPayload; +} + +export async function handleExportPrivateKey( + ctx: MsgEventContext, +): Promise { + const { port, hostOrigin } = ctx; + + // 1. Origin validation + const allowedOrigins = USER_DASHBOARD_ORIGINS.split(",").map((o: string) => + o.trim(), + ); + if (!allowedOrigins.includes(hostOrigin)) { + const ack: OkoWalletMsgExportPrivateKeyAck = { + target: OKO_SDK_TARGET, + msg_type: "__export_private_key_ack__", + payload: { success: false, error: { type: "UNAUTHORIZED_ORIGIN" } }, + }; + port.postMessage(ack); + return; + } + + // 2. Auth token validation + const authToken = useAppState.getState().getAuthToken(hostOrigin); + if (!authToken) { + const ack: OkoWalletMsgExportPrivateKeyAck = { + target: OKO_SDK_TARGET, + msg_type: "__export_private_key_ack__", + payload: { success: false, error: { type: "NOT_AUTHENTICATED" } }, + }; + port.postMessage(ack); + return; + } + + // 3. Extract user shares from local state + const keyshare1 = useAppState.getState().getKeyshare_1(hostOrigin); + const keyPackageEd25519Hex = + useAppState.getState().getKeyPackageEd25519(hostOrigin); + + if (!keyshare1 || !keyPackageEd25519Hex) { + const ack: OkoWalletMsgExportPrivateKeyAck = { + target: OKO_SDK_TARGET, + msg_type: "__export_private_key_ack__", + payload: { success: false, error: { type: "MISSING_KEYSHARE" } }, + }; + port.postMessage(ack); + return; + } + + // 4. Get ed25519 public key for the response + const ed25519Wallet = + useAppState.getState().getWalletEd25519(hostOrigin); + const ed25519PublicKey = ed25519Wallet?.publicKey ?? null; + + // ------------------------------------------------------------------- + // TODO: Replace mock with actual implementation when oko_api is ready. + // + // Actual flow: + // + // (a) Fetch server shares from oko_api + // + // const serverSharesRes = await fetch( + // `${OKO_API_ENDPOINT}/user_dashboard/v1/export_private_key`, + // { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${authToken}`, + // }, + // }, + // ); + // const serverShares = await serverSharesRes.json(); + // // serverShares = { secp256k1: string (keyshare_0 hex), ed25519: string (server signing_share hex) } + // + // (b) Combine secp256k1 shares + // + // import * as wasmModule from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; + // + // const keyCombineInput = { + // shares: { + // 0: serverShares.secp256k1, // server's share (Participant 0) + // 1: keyshare1, // user's share (Participant 1) + // }, + // }; + // const fullSecp256k1Key = wasmModule.cli_combine_shares(keyCombineInput); + // // fullSecp256k1Key is a secp256k1 Scalar → convert to hex + // + // (c) Combine ed25519 shares + // + // Parse keyPackageEd25519Hex to get user's signing_share. + // const keyPackageRaw = JSON.parse( + // Buffer.from(keyPackageEd25519Hex, "hex").toString("utf-8"), + // ); + // const userSigningShare = keyPackageRaw.signing_share; // number[] + // + // Combine user's signing_share + server's signing_share using FROST sss_combine + // to recover the full ed25519 signing secret. + // + // const fullEd25519Secret = ... // 32 bytes + // const fullEd25519Key = hex(fullEd25519Secret) + hex(ed25519PublicKey) // 64 bytes + // + // ------------------------------------------------------------------- + + // [Mock] Return deterministic test keys for development + const mockSecp256k1 = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + const mockEd25519SigningSecret = "a".repeat(64); // 32 bytes + const mockEd25519PublicKey = ed25519PublicKey ?? "b".repeat(64); // 32 bytes + const mockEd25519 = mockEd25519SigningSecret + mockEd25519PublicKey; + + const ack: OkoWalletMsgExportPrivateKeyAck = { + target: OKO_SDK_TARGET, + msg_type: "__export_private_key_ack__", + payload: { + success: true, + data: { + secp256k1: mockSecp256k1, + ed25519: mockEd25519, + }, + }, + }; + port.postMessage(ack); +} diff --git a/embed/oko_attached/src/window_msgs/index.ts b/embed/oko_attached/src/window_msgs/index.ts index 497fdc545..70b87b927 100644 --- a/embed/oko_attached/src/window_msgs/index.ts +++ b/embed/oko_attached/src/window_msgs/index.ts @@ -15,16 +15,26 @@ import { handleGetCosmosChain } from "./get_cosmos_chain_info"; import { handleOAuthInfoPassV2 } from "./oauth_info_pass"; import { handleGetEthChain } from "./get_eth_chain_info"; import { handleGetConnectedApps } from "./get_connected_apps"; +import { handleExportPrivateKey } from "./export_private_key"; -// NOTE: Since this method can only be used within user_dashboard, -// Define ExtendedOkoWalletMsg to extend the type +// NOTE: These methods can only be used within user_dashboard, +// so they are not exposed via the SDK. Define extended types here. type OkoWalletMsgGetConnectedApps = { target: "oko_attached"; msg_type: "__get_connected_apps__"; payload: null; }; -type ExtendedOkoWalletMsg = OkoWalletMsg | OkoWalletMsgGetConnectedApps; +type OkoWalletMsgExportPrivateKey = { + target: "oko_attached"; + msg_type: "__export_private_key__"; + payload: null; +}; + +type ExtendedOkoWalletMsg = + | OkoWalletMsg + | OkoWalletMsgGetConnectedApps + | OkoWalletMsgExportPrivateKey; export function makeMsgHandler() { return async function msgHandler(event: MessageEvent) { @@ -126,6 +136,11 @@ export function makeMsgHandler() { break; } + case "__export_private_key__": { + await handleExportPrivateKey(ctx); + break; + } + default: console.error( `[attached] unimplemented, msg_type: ${message.msg_type}`, From bdc1f996c76a1e8069e4d6456eca96979b8e077f Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Thu, 12 Feb 2026 13:06:17 +0900 Subject: [PATCH 09/62] user_dashboard: add re-auth account mismatch check --- .../src/app/export_private_key/page.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 59e1a917c..68d19709e 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -309,9 +309,20 @@ export default function Page() { try { setIsLoading(true); + + const publicKeyBefore = await okoWallet.getPublicKey(); await okoWallet.signIn(authType === "auth0" ? "email" : authType); + const publicKeyAfter = await okoWallet.getPublicKey(); + + if (publicKeyBefore !== publicKeyAfter) { + displayToast({ + variant: "confirm", + title: "Login Failed", + description: "Please try again.", + }); + return; + } - // TODO: Verify re-authenticated account matches the current account (prevent account switch) // TODO: Replace with actual private key export when SDK API is available const mockPrivateKey = "0x" + "0".repeat(64); setPrivateKey(mockPrivateKey); From 1465c842dddd930aa5994712066bfa687991af06 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Thu, 12 Feb 2026 14:17:11 +0900 Subject: [PATCH 10/62] user_dashboard: integrate export private key --- .../app/export_private_key/page.module.scss | 4 +- .../src/app/export_private_key/page.tsx | 162 ++++++++++++++---- .../src/window_msgs/export_private_key.ts | 28 ++- 3 files changed, 149 insertions(+), 45 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.module.scss b/apps/user_dashboard/src/app/export_private_key/page.module.scss index 87ba153a5..7277424a7 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.module.scss +++ b/apps/user_dashboard/src/app/export_private_key/page.module.scss @@ -120,7 +120,7 @@ .privateKeyField { position: relative; - height: 68px; + min-height: 68px; width: 100%; cursor: pointer; @@ -130,8 +130,6 @@ } .privateKeyBg { - position: absolute; - inset: 0; display: flex; align-items: center; justify-content: center; diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 68d19709e..fb39ee660 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -210,20 +210,20 @@ function Step1Content({ } function Step2Content({ - privateKey, - isRevealed, + privateKeys, + revealedKeys, onToggleReveal, onCopy, }: { - privateKey: string; - isRevealed: boolean; - onToggleReveal: () => void; - onCopy: () => void; + privateKeys: { secp256k1: string; ed25519: string }; + revealedKeys: { secp256k1: boolean; ed25519: boolean }; + onToggleReveal: (key: "secp256k1" | "ed25519") => void; + onCopy: (key: string) => void; }) { return ( <> - View and copy your private key + View and copy your private keys
    @@ -235,16 +235,16 @@ function Step2Content({ color="secondary" className={styles.privateKeyLabel} > - Private Key + EVM/Cosmos Private Key
    onToggleReveal("secp256k1")} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { - onToggleReveal(); + onToggleReveal("secp256k1"); } }} > @@ -253,12 +253,16 @@ function Step2Content({ size="md" weight="medium" color="secondary" - className={isRevealed ? undefined : styles.privateKeyTextBlurred} + className={ + revealedKeys.secp256k1 + ? undefined + : styles.privateKeyTextBlurred + } > - {privateKey} + {privateKeys.secp256k1}
    - {!isRevealed && ( + {!revealedKeys.secp256k1 && (
    @@ -275,7 +279,65 @@ function Step2Content({
    - + +
    + +
    + + SVM Private Key + +
    onToggleReveal("ed25519")} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + onToggleReveal("ed25519"); + } + }} + > +
    + + {privateKeys.ed25519} + +
    + {!revealedKeys.ed25519 && ( +
    + + + + + Click or tap to reveal your private key. +
    + Ensure no one else can see your screen. +
    +
    + )} +
    +
    + +
    + + + + {error && ( +
    + {error} +
    + )} + + {txHash && ( +
    +
    Transaction Sent Successfully!
    +
    + + {txHash} + +
    +
    + + +
    +
    + )} +
    +
    + ); +} diff --git a/sandbox/sandbox_evm/src/app/playground/_components/SigningPlayground.tsx b/sandbox/sandbox_evm/src/app/playground/_components/SigningPlayground.tsx index adb469d9d..ddb8ddb64 100644 --- a/sandbox/sandbox_evm/src/app/playground/_components/SigningPlayground.tsx +++ b/sandbox/sandbox_evm/src/app/playground/_components/SigningPlayground.tsx @@ -5,6 +5,7 @@ import { PermitSignWidget } from "./PermitSignWidget"; import { SiweSignWidget } from "./SiweSignWidget"; import { SignatureVerificationWidget } from "./SignatureVerificationWidget"; import { NativeTransferWidget } from "./NativeTransferWidget"; +import { ERC20TransferWidget } from "./ERC20TransferWidget"; import { Eip712SignWidget } from "./EIP712SignWidget"; export function SigningPlayground() { @@ -25,6 +26,7 @@ export function SigningPlayground() { +
    From 5723045f76f0efabbd38e55a2faf55c0ea708785 Mon Sep 17 00:00:00 2001 From: Hyunjae Chung Date: Fri, 20 Feb 2026 11:30:14 +0900 Subject: [PATCH 24/62] oko_attached: handle fee_sponsorship state transition properly * oko_attached: Use CSS gradients for Base sponsorship background * oko_attached: Attach tooltip to icon wrapper and adjust styles * oko_attached: Use tertiary background for sponsored fee tooltip * oko_attached: prevent sponsored fee UI from showing when balance is sufficient for fee * oko_attached: fix fee calculation to use actual gas estimation and wait for L1 fee on OP Stack chains * oko_attached: hide next free transaction timer during signing and show keep window open message on success * oko_attached: Refetch fee sponsorship query every 10s * oko_attached: Refetch status on fee sponsorship error * oko_attached: Clamp remainingMs to not increase on update * oko_attached: Move tx receipt waiting into sponsorship flow * oko_attached: Use CSS classes for sponsorship backgrounds * oko_attached: Use viem Hex type for tx hash --- .../eth/tx_sig/hooks/use_tx_sig_modal.tsx | 106 ++++++++++-------- .../eth/tx_sig/make_tx_sig_modal.tsx | 2 +- .../base_sponsorship_background.module.scss | 26 +++++ .../base_sponsorship_background.tsx | 47 ++------ .../sponsored_fee/sponsored_fee.module.scss | 19 +--- .../tx_sig/sponsored_fee/sponsored_fee.tsx | 58 +++++----- .../src/requests/fee_sponsorship.ts | 6 + .../ethereum/queries/use_fee_sponsorship.ts | 25 ++++- 8 files changed, 158 insertions(+), 131 deletions(-) diff --git a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/hooks/use_tx_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/hooks/use_tx_sig_modal.tsx index fab9bc20c..75d5f6450 100644 --- a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/hooks/use_tx_sig_modal.tsx +++ b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/hooks/use_tx_sig_modal.tsx @@ -56,6 +56,8 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { const [estimatedFee, setEstimatedFee] = useState(null); const [hasSufficientBalanceForTotal, setHasSufficientBalanceForTotal] = useState(null); + const [hasSufficientBalanceForValue, setHasSufficientBalanceForValue] = + useState(null); const [primaryErrorMessage, setPrimaryErrorMessage] = useState( null, ); @@ -227,14 +229,19 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { hostOrigin, estimatedFeeWei: estimatedFee?.raw, hasSufficientBalance: hasSufficientBalanceForTotal, + publicClient: publicClient ?? undefined, enabled: isSponsorshipSupported && !isDemo, }); // Determine if sponsorship should be shown + // Show when the user can cover the tx value but not the total (value + fee) + // (sponsorship covers fee, not the transfer amount) const showSponsorship = isSponsorshipSupported && !isDemo && - (hasSufficientBalanceForTotal === false || isSponsored || isRateLimited); + hasSufficientBalanceForValue !== false && + hasSufficientBalanceForTotal === false && + sponsorshipState !== "unavailable"; // Create sponsored fee info for UI const sponsoredFeeInfo: SponsoredFeeInfo | null = showSponsorship @@ -310,13 +317,12 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { // calculate the estimated fee useEffect(() => { - let adjustedGasEstimation = gasEstimation; - if ( - originalTransaction.data === undefined || + const adjustedGasEstimation = + gasEstimation ?? + (originalTransaction.data === undefined || originalTransaction.data === "0x" - ) { - adjustedGasEstimation = DEFAULT_GAS_ESTIMATION; - } + ? DEFAULT_GAS_ESTIMATION + : undefined); if (adjustedGasEstimation === undefined) { return; @@ -326,6 +332,12 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { return; } + // On OP Stack chains, wait for L1 gas estimation before setting the fee + // to prevent a race condition where the fee is set without L1 data fee + if (isOpStack && !l1GasEstimation) { + return; + } + let total: bigint; // NOTE: as of now, we only support native currency as fee currency @@ -346,12 +358,13 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { }; setEstimatedFee(estimatedFee); - }, [originalTransaction, feeData, l1GasEstimation, gasEstimation]); + }, [originalTransaction, feeData, l1GasEstimation, gasEstimation, isOpStack]); // check if the balance is sufficient for the transaction useEffect(() => { if (isDemo) { setHasSufficientBalanceForTotal(true); + setHasSufficientBalanceForValue(true); return; } @@ -367,13 +380,11 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { return; } - const totalValue = - estimatedFee.raw + hexToBigInt(originalTransaction?.value ?? "0x0"); + const txValue = hexToBigInt(originalTransaction?.value ?? "0x0"); + const totalValue = estimatedFee.raw + txValue; - const hasSufficientBalanceForTotal = - feeCurrencyBalance.amount >= totalValue; - - setHasSufficientBalanceForTotal(hasSufficientBalanceForTotal); + setHasSufficientBalanceForTotal(feeCurrencyBalance.amount >= totalValue); + setHasSufficientBalanceForValue(feeCurrencyBalance.amount >= txValue); }, [estimatedFee, feeCurrencyBalance, originalTransaction]); // set the primary error message @@ -429,27 +440,37 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { return; } - if (hasSufficientBalanceForTotal === false) { - // If sponsorship is supported and available or sponsored, don't show error - if (isSponsorshipSupported && (isSponsorshipAvailable || isSponsored)) { - setPrimaryErrorMessage(""); - return; - } - // If sponsorship is rate limited, show the timer instead of error - if (isSponsorshipSupported && isRateLimited) { - setPrimaryErrorMessage(""); - return; - } - // If sponsorship failed, the error will be shown in sponsoredFeeInfo - if (isSponsorshipSupported && sponsorshipState === "error") { - setPrimaryErrorMessage(""); + if (isSponsorshipSupported) { + // Fee insufficient: value is covered but total (value + fee) is not + if (hasSufficientBalanceForValue === true && hasSufficientBalanceForTotal === false) { + const canSuppressInsufficientError = + isSponsorshipAvailable || + isSponsored || + isRateLimited || + sponsorshipState === "error" || + isSponsorshipChecking; + + if (canSuppressInsufficientError) { + setPrimaryErrorMessage(""); + return; + } + + setPrimaryErrorMessage("Insufficient balance to cover the transaction"); return; } - // If sponsorship is checking, don't show error yet - if (isSponsorshipSupported && isSponsorshipChecking) { - setPrimaryErrorMessage(""); + + // Value itself is insufficient + if (hasSufficientBalanceForValue === false) { + setPrimaryErrorMessage("Insufficient balance to cover the transaction"); return; } + + setPrimaryErrorMessage(""); + return; + } + + // Non-sponsorship-supported chains + if (hasSufficientBalanceForValue === false || hasSufficientBalanceForTotal === false) { setPrimaryErrorMessage("Insufficient balance to cover the transaction"); return; } @@ -462,6 +483,7 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { getGasEstimationError, getFeeCurrencyBalanceError, getL1GasEstimationError, + hasSufficientBalanceForValue, hasSufficientBalanceForTotal, isSponsorshipSupported, isSponsorshipAvailable, @@ -487,32 +509,20 @@ export function useTxSigModal(args: UseEthereumSigModalArgs) { return; } + setIsLoading(true); + // If balance is insufficient and sponsorship is available, request sponsorship first if ( hasSufficientBalanceForTotal === false && isSponsorshipAvailable && !isSponsored ) { - setIsLoading(true); - const topUpResult = await requestSponsorship(); - - // Wait for the top-up transaction to be confirmed - if (topUpResult?.txHash && publicClient) { - try { - await publicClient.waitForTransactionReceipt({ - hash: topUpResult.txHash as `0x${string}`, - confirmations: 1, - }); - } catch (e) { - console.warn("[fee-sponsorship] Failed to wait for tx receipt:", e); - // Continue anyway - the tx might still succeed - } + const sponsorshipResult = await requestSponsorship(); + if (!sponsorshipResult) { + return; } - // Continue with signing after sponsorship is confirmed } - setIsLoading(true); - if (simulatedTransaction === null) { // unreachable return; diff --git a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/make_tx_sig_modal.tsx b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/make_tx_sig_modal.tsx index 75f06302d..5a2c64d7b 100644 --- a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/make_tx_sig_modal.tsx +++ b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/make_tx_sig_modal.tsx @@ -136,7 +136,7 @@ export const MakeTxSigModal: FC = ({ fullWidth onClick={handleApproveClick} isLoading={isLoading} - disabled={!isApproveEnabled || isRateLimited} + disabled={!isApproveEnabled || (showSponsorship && isRateLimited)} > {isLoading ? "Signing..." : "Approve"} diff --git a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.module.scss b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.module.scss index dece2877e..8a2f122c1 100644 --- a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.module.scss +++ b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.module.scss @@ -1,3 +1,5 @@ +$base-asset-url: "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/assets/base_chain"; + .gradient { position: absolute; bottom: 0; @@ -9,6 +11,22 @@ background-repeat: no-repeat; pointer-events: none; z-index: 0; + + &.light_normal { + background-image: url("#{$base-asset-url}/Rectangle+7879.png"); + } + + &.dark_normal { + background-image: url("#{$base-asset-url}/Rectangle+7881.png"); + } + + &.light_error { + background-image: url("#{$base-asset-url}/Rectangle+7882.png"); + } + + &.dark_error { + background-image: url("#{$base-asset-url}/Rectangle+7883.png"); + } } .pattern { @@ -22,4 +40,12 @@ background-repeat: no-repeat; pointer-events: none; z-index: 1; + + &.blue { + background-image: url("#{$base-asset-url}/pattern-blue.png"); + } + + &.gray { + background-image: url("#{$base-asset-url}/pattern-gray.png"); + } } diff --git a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.tsx b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.tsx index 30c933805..efab205e4 100644 --- a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.tsx +++ b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/base_sponsorship_background.tsx @@ -3,25 +3,6 @@ import cn from "classnames"; import styles from "./base_sponsorship_background.module.scss"; -// S3 asset URLs for Base chain sponsorship -const BASE_ASSETS = { - // Pattern images - patternBlue: - "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/assets/base_chain/pattern-blue.png", - patternGray: - "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/assets/base_chain/pattern-gray.png", - - // Gradient images - gradientLightNormal: - "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/assets/base_chain/Rectangle+7879.png", - gradientDarkNormal: - "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/assets/base_chain/Rectangle+7881.png", - gradientLightError: - "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/assets/base_chain/Rectangle+7882.png", - gradientDarkError: - "https://oko-wallet.s3.ap-northeast-2.amazonaws.com/assets/base_chain/Rectangle+7883.png", -}; - export interface BaseSponsorshipBackgroundProps { isError?: boolean; theme?: "light" | "dark" | "system" | null; @@ -33,30 +14,22 @@ export const BaseSponsorshipBackground: FC = ({ }) => { const isDark = theme === "dark"; - const patternUrl = isError - ? BASE_ASSETS.patternGray - : BASE_ASSETS.patternBlue; + const gradientVariant = isDark + ? isError + ? styles.dark_error + : styles.dark_normal + : isError + ? styles.light_error + : styles.light_normal; - const gradientUrl = isError - ? isDark - ? BASE_ASSETS.gradientDarkError - : BASE_ASSETS.gradientLightError - : isDark - ? BASE_ASSETS.gradientDarkNormal - : BASE_ASSETS.gradientLightNormal; + const patternVariant = isError ? styles.gray : styles.blue; return ( <> {/* Gradient overlay */} -
    +
    {/* Pattern overlay */} -
    +
    ); }; diff --git a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.module.scss b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.module.scss index 742e83ad1..57e9c7168 100644 --- a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.module.scss +++ b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.module.scss @@ -18,7 +18,6 @@ display: flex; align-items: center; gap: 4px; - position: relative; } .right { @@ -65,6 +64,7 @@ align-items: center; justify-content: center; cursor: pointer; + position: relative; } .tooltip { @@ -81,17 +81,14 @@ text-align: center; border-radius: 12px; - background: var(--bg-primary-solid, #181d27); - box-shadow: 0px 12px 16px -4px rgba(10, 13, 18, 0.08), + background: #22262f; + box-shadow: + 0px 12px 16px -4px rgba(10, 13, 18, 0.08), 0px 4px 6px -2px rgba(10, 13, 18, 0.03); min-width: 200px; max-width: 280px; - [data-theme="dark"] & { - background: var(--bg-secondary, #22262f); - } - // Arrow &::after { content: ""; @@ -101,13 +98,7 @@ transform: translateX(-50%); border-width: 8px; border-style: solid; - border-color: var(--bg-primary-solid, #181d27) transparent transparent - transparent; - - [data-theme="dark"] & { - border-color: var(--bg-secondary, #22262f) transparent transparent - transparent; - } + border-color: #22262f transparent transparent transparent; } } diff --git a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.tsx b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.tsx index cefcd939b..170a8ee10 100644 --- a/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.tsx +++ b/embed/oko_attached/src/components/modal_variants/eth/tx_sig/sponsored_fee/sponsored_fee.tsx @@ -103,36 +103,38 @@ export const SponsoredFee: FC = ({
    - {/* Next free transaction line */} -
    -
    - - Next free transaction in - -
    - -
    - {tooltipVisible && showTooltip && } -
    -
    - {isSimulating || isLoading ? ( - - ) : isTimer ? ( + {/* Next free transaction line - hide during/after signing */} + {info.state !== "success" && ( +
    +
    - {info.formattedRemainingTime} + Next free transaction in - ) : ( - - {info.state === "success" ? "after 5 min" : "5 min"} - - )} +
    + + {tooltipVisible && showTooltip && } +
    +
    +
    + {isSimulating || isLoading ? ( + + ) : isTimer ? ( + + {info.formattedRemainingTime} + + ) : ( + + 5 min + + )} +
    -
    + )} {/* Error message */} {isError && info.errorMessage && ( @@ -145,7 +147,7 @@ export const SponsoredFee: FC = ({ )} {/* Loading message */} - {isLoading && ( + {(isLoading || info.state === "success") && (
    Keep this window open during the transaction... diff --git a/embed/oko_attached/src/requests/fee_sponsorship.ts b/embed/oko_attached/src/requests/fee_sponsorship.ts index cac10451f..06ad6d19f 100644 --- a/embed/oko_attached/src/requests/fee_sponsorship.ts +++ b/embed/oko_attached/src/requests/fee_sponsorship.ts @@ -129,6 +129,9 @@ export async function requestFeeTopUp( } try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30_000); + const resp = await fetch(`${FEE_SPONSORSHIP_ENDPOINT}/evm/top-up`, { method: "POST", headers: { @@ -136,8 +139,11 @@ export async function requestFeeTopUp( "X-API-Key": FEE_SPONSORSHIP_API_KEY, }, body: JSON.stringify(request), + signal: controller.signal, }); + clearTimeout(timeoutId); + if (!resp.ok) { const errorBody = await resp.json().catch(() => ({})); return { diff --git a/embed/oko_attached/src/web3/ethereum/queries/use_fee_sponsorship.ts b/embed/oko_attached/src/web3/ethereum/queries/use_fee_sponsorship.ts index 9453600c7..f6549a62c 100644 --- a/embed/oko_attached/src/web3/ethereum/queries/use_fee_sponsorship.ts +++ b/embed/oko_attached/src/web3/ethereum/queries/use_fee_sponsorship.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { useState, useEffect, useCallback } from "react"; +import type { Hex, PublicClient } from "viem"; import { checkFeeSponsorshipStatus, @@ -67,9 +68,11 @@ export function useFeeSponsorshipStatus({ return result.data; }, enabled: enabled && isSupported && hasApiKey && !!recipientAddress, - staleTime: 30 * 1000, // 30 seconds + staleTime: 0, gcTime: 5 * 60 * 1000, // 5 minutes retry: 1, + refetchInterval: 10_000, + refetchIntervalInBackground: false, }); return { @@ -154,7 +157,12 @@ export function useSponsorshipTimer({ useEffect(() => { if (remainingTimeMs !== undefined) { - setRemainingMs(remainingTimeMs); + setRemainingMs((prev) => { + if (prev <= 0) { + return remainingTimeMs; + } + return Math.min(prev, remainingTimeMs); + }); } }, [remainingTimeMs]); @@ -192,6 +200,7 @@ export interface UseBaseSponsorshipFlowProps { hostOrigin: string; estimatedFeeWei: bigint | undefined; hasSufficientBalance: boolean | null; + publicClient?: PublicClient; enabled?: boolean; } @@ -233,6 +242,7 @@ export function useBaseSponsorshipFlow({ hostOrigin, estimatedFeeWei, hasSufficientBalance, + publicClient, enabled = true, }: UseBaseSponsorshipFlowProps): UseBaseSponsorshipFlowResult { const [sponsorshipState, setSponsorshipState] = @@ -339,6 +349,14 @@ export function useBaseSponsorshipFlow({ const result = await requestTopUp(recipientAddress, amountWei); + if (publicClient) { + setSponsorshipState("waiting_confirmation"); + await publicClient.waitForTransactionReceipt({ + hash: result.txHash as Hex, + confirmations: 1, + }); + } + setSponsorshipState("success"); setIsSponsored(true); @@ -346,9 +364,10 @@ export function useBaseSponsorshipFlow({ } catch (err) { setSponsorshipState("error"); setError(err as FeeSponsorshipError); + refetchStatus(); return null; } - }, [estimatedFeeWei, recipientAddress, requestTopUp]); + }, [estimatedFeeWei, recipientAddress, requestTopUp, publicClient]); const resetSponsorship = useCallback(() => { setSponsorshipState("idle"); From eede4ce9f44dc4063b35fb0e64d98895ef827ea5 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 20 Feb 2026 14:27:26 +0900 Subject: [PATCH 25/62] crypto: add WASM bindings for seed SSS --- Cargo.toml | 2 +- .../cait_sith_keplr_wasm/wasm/src/lib.rs | 1 + .../wasm/src/seed_sss/combine.rs | 15 +++++++++++++ .../wasm/src/seed_sss/expand_shares.rs | 22 +++++++++++++++++++ .../wasm/src/seed_sss/mod.rs | 7 ++++++ .../wasm/src/seed_sss/split.rs | 22 +++++++++++++++++++ 6 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/combine.rs create mode 100644 crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/expand_shares.rs create mode 100644 crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/mod.rs create mode 100644 crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/split.rs diff --git a/Cargo.toml b/Cargo.toml index 13f0990b7..39c252a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ frost_core = { path = "crypto/teddsa/frost_core", version = "2.2.0", default-fea frost_ed25519_keplr = { path = "crypto/teddsa/frost_ed25519_keplr", version = "2.2.0", default-features = false } frost_rerandomized = { path = "crypto/teddsa/frost_rerandomized", version = "2.2.0", default-features = false } -cait_sith_keplr = { version = "=0.0.2-rc.4" } +cait_sith_keplr = { path = "crypto/tecdsa/cait_sith_keplr", version = "=0.0.2-rc.4" } criterion = "0.6" document-features = "0.2.7" hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/lib.rs b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/lib.rs index 0677ba18b..303e06acf 100644 --- a/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/lib.rs +++ b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/lib.rs @@ -1,6 +1,7 @@ mod keygen; mod presign; mod sign; +pub mod seed_sss; pub mod sss; mod triples; mod verify; diff --git a/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/combine.rs b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/combine.rs new file mode 100644 index 000000000..909f5dbf5 --- /dev/null +++ b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/combine.rs @@ -0,0 +1,15 @@ +use cait_sith_keplr::seed_sss::seed_combine; +use cait_sith_keplr::sss::Point256; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn seed_sss_combine(points: JsValue, t: u32) -> Result { + let points: Vec = points + .into_serde() + .map_err(|err| JsValue::from_str(&err.to_string()))?; + + let out = seed_combine(points, t).map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} diff --git a/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/expand_shares.rs b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/expand_shares.rs new file mode 100644 index 000000000..60e5a1745 --- /dev/null +++ b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/expand_shares.rs @@ -0,0 +1,22 @@ +use cait_sith_keplr::seed_sss::seed_expand_shares; +use cait_sith_keplr::sss::{Point256, ReshareResult}; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn seed_sss_expand_shares( + points: JsValue, + additional_ks_node_hashes: JsValue, + t: u32, +) -> Result { + let points: Vec = points + .into_serde() + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let additional_ks_node_hashes: Vec<[u8; 32]> = additional_ks_node_hashes + .into_serde() + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let out: ReshareResult = seed_expand_shares(points, additional_ks_node_hashes, t) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} diff --git a/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/mod.rs b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/mod.rs new file mode 100644 index 000000000..0f720639d --- /dev/null +++ b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/mod.rs @@ -0,0 +1,7 @@ +mod combine; +mod expand_shares; +mod split; + +pub use combine::*; +pub use expand_shares::*; +pub use split::*; diff --git a/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/split.rs b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/split.rs new file mode 100644 index 000000000..470a84242 --- /dev/null +++ b/crypto/tecdsa/cait_sith_keplr_wasm/wasm/src/seed_sss/split.rs @@ -0,0 +1,22 @@ +use cait_sith_keplr::seed_sss::seed_split; +use gloo_utils::format::JsValueSerdeExt; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn seed_sss_split( + secret: JsValue, + ks_node_hashes: JsValue, + t: u32, +) -> Result { + let secret: [u8; 32] = secret + .into_serde() + .map_err(|err| JsValue::from_str(&err.to_string()))?; + let ks_node_hashes: Vec<[u8; 32]> = ks_node_hashes + .into_serde() + .map_err(|err| JsValue::from_str(&err.to_string()))?; + + let out = seed_split(secret, ks_node_hashes, t) + .map_err(|err| JsValue::from_str(&err.to_string()))?; + + JsValue::from_serde(&out).map_err(|err| JsValue::from_str(&err.to_string())) +} From cc95c96f0fd508df50fbe93f47d13294355070b1 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 20 Feb 2026 14:52:03 +0900 Subject: [PATCH 26/62] oko_attached: replace secp256k1 SSS with seed SSS for seed operations --- .../oko_attached/src/crypto/keygen_ed25519.ts | 59 ++------- embed/oko_attached/src/crypto/reshare.ts | 114 ++++++++++++++++++ embed/oko_attached/src/crypto/reshare_v2.ts | 6 +- 3 files changed, 129 insertions(+), 50 deletions(-) diff --git a/embed/oko_attached/src/crypto/keygen_ed25519.ts b/embed/oko_attached/src/crypto/keygen_ed25519.ts index 082188945..2d238d327 100644 --- a/embed/oko_attached/src/crypto/keygen_ed25519.ts +++ b/embed/oko_attached/src/crypto/keygen_ed25519.ts @@ -187,49 +187,14 @@ export interface Ed25519KeygenSplitResult { ksnSeedShares: SeedShareByNode[]; } -// secp256k1 group order n -const SECP256K1_ORDER = new Uint8Array([ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, - 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41, -]); - -/** - * Compare two 32-byte big-endian unsigned integers. - * Returns true if a < b. - */ -function isLessThan(a: Uint8Array, b: Uint8Array): boolean { - for (let i = 0; i < 32; i++) { - if (a[i] < b[i]) { - return true; - } - if (a[i] > b[i]) { - return false; - } - } - return false; // equal -} - /** - * Generate a 32-byte seed that is less than the secp256k1 group order. - * Resamples if seed >= n (probability ≈ 1.5e-39, practically never happens). + * Generate a random 32-byte seed. + * All 32-byte values are valid because seed SSS uses a 257-bit prime (p = 2^256 + 297 > 2^256). */ function generateSeed(): Result { - for (let attempt = 0; attempt < 10; attempt++) { - const seedBytes = new Uint8Array(32); - crypto.getRandomValues(seedBytes); - - if (isLessThan(seedBytes, SECP256K1_ORDER)) { - const res = Bytes.fromUint8Array(seedBytes, 32); - if (res.success) { - return res; - } - } - } - return { - success: false, - err: "Failed to generate valid seed after 10 attempts", - }; + const seedBytes = new Uint8Array(32); + crypto.getRandomValues(seedBytes); + return Bytes.fromUint8Array(seedBytes, 32); } /** @@ -256,11 +221,11 @@ const SEED_SPLIT_USER_ID = "oko_seed_user"; /** * Run ed25519 keygen from seed and split both signing share and seed for distribution. * - * 1. Generate seed (32B, < secp256k1 order) + * 1. Generate seed (32B) * 2. Derive Ed25519 scalar from seed (SHA-512 + clamp + mod l) via WASM * 3. FROST split scalar into signing shares (existing flow) - * 4. secp256k1 SSS split seed into 2-of-2 (server + user) - * 5. secp256k1 SSS split user's seed share Y into t-of-n for KSN distribution + * 4. 257-bit prime SSS split seed into 2-of-2 (server + user) + * 5. 257-bit prime SSS split user's seed share Y into t-of-n for KSN distribution */ export async function runEd25519KeygenAndSplit( keyshareNodeMeta: KeyShareNodeMetaWithNodeStatusInfo, @@ -309,7 +274,7 @@ export async function runEd25519KeygenAndSplit( }; } - // 4. Seed 2-of-2 split via secp256k1 SSS (server + user) + // 4. Seed 2-of-2 split via 257-bit prime SSS (server + user) const seedIdHashesRes = await hashKeyshareNodeNames([ SEED_SPLIT_SERVER_ID, SEED_SPLIT_USER_ID, @@ -322,7 +287,7 @@ export async function runEd25519KeygenAndSplit( } const [serverHash, userHash] = seedIdHashesRes.data; - const seedSplitPoints: PointNumArr[] = secp256k1Wasm.sss_split( + const seedSplitPoints: PointNumArr[] = secp256k1Wasm.seed_sss_split( [...seed.toUint8Array()], [[...serverHash.toUint8Array()], [...userHash.toUint8Array()]], 2, @@ -337,7 +302,7 @@ export async function runEd25519KeygenAndSplit( } const userSeedY = seedSplitPoints[1].y; - // 5. User seed share Y → t-of-n split for KSN distribution via secp256k1 SSS + // 5. User seed share Y → t-of-n split for KSN distribution via 257-bit prime SSS const ksnHashesRes = await hashKeyshareNodeNames( keyshareNodeMeta.nodes.map((n) => n.name), ); @@ -349,7 +314,7 @@ export async function runEd25519KeygenAndSplit( } const ksnHashes = ksnHashesRes.data.map((b) => [...b.toUint8Array()]); - const ksnSeedSplitPoints: PointNumArr[] = secp256k1Wasm.sss_split( + const ksnSeedSplitPoints: PointNumArr[] = secp256k1Wasm.seed_sss_split( userSeedY, ksnHashes, keyshareNodeMeta.threshold, diff --git a/embed/oko_attached/src/crypto/reshare.ts b/embed/oko_attached/src/crypto/reshare.ts index eef24d54a..a22f40cb8 100644 --- a/embed/oko_attached/src/crypto/reshare.ts +++ b/embed/oko_attached/src/crypto/reshare.ts @@ -145,6 +145,120 @@ export async function reshareUserKeyShares( }; } +export async function runSeedExpandShares( + splitKeyShares: UserKeySharePointByNode[], + additionalKSNodes: NodeNameAndEndpoint[], + threshold: number, +): Promise> { + try { + if (threshold < 2) { + return { + success: false, + err: "Threshold must be at least 2", + }; + } + + if (splitKeyShares.length < threshold) { + return { + success: false, + err: "Number of user key shares is less than threshold", + }; + } + + const splitPoints: PointNumArr[] = splitKeyShares.map((splitKeyShare) => ({ + x: [...splitKeyShare.share.x.toUint8Array()], + y: [...splitKeyShare.share.y.toUint8Array()], + })); + + const additionalKSNodeHashesRes = await hashKeyshareNodeNames( + additionalKSNodes.map((node) => node.name), + ); + if (additionalKSNodeHashesRes.success === false) { + return { + success: false, + err: additionalKSNodeHashesRes.err, + }; + } + const additionalKSNodeHashes = additionalKSNodeHashesRes.data.map( + (bytes) => { + return [...bytes.toUint8Array()]; + }, + ); + + const reshareResult: { + t: number; + reshared_points: PointNumArr[]; + secret: number[]; + } = await wasmModule.seed_sss_expand_shares( + splitPoints, + additionalKSNodeHashes, + threshold, + ); + + const resharedPoints: UserKeySharePointByNode[] = []; + for (let i = 0; i < reshareResult.reshared_points.length; ++i) { + const xBytesRes = Bytes.fromUint8Array( + Uint8Array.from(reshareResult.reshared_points[i].x), + 32, + ); + if (xBytesRes.success === false) { + return { + success: false, + err: `Failed to convert reshared key share to bytes: ${xBytesRes.err}`, + }; + } + const yBytesRes = Bytes.fromUint8Array( + Uint8Array.from(reshareResult.reshared_points[i].y), + 32, + ); + if (yBytesRes.success === false) { + return { + success: false, + err: `Failed to convert reshared key share to bytes: ${yBytesRes.err}`, + }; + } + resharedPoints.push({ + node: + i < splitKeyShares.length + ? splitKeyShares[i].node + : { + name: additionalKSNodes[i - splitKeyShares.length].name, + endpoint: additionalKSNodes[i - splitKeyShares.length].endpoint, + }, + share: { + x: xBytesRes.data, + y: yBytesRes.data, + }, + }); + } + + const originalSecretBytesRes = Bytes.fromUint8Array( + Uint8Array.from(reshareResult.secret), + 32, + ); + if (originalSecretBytesRes.success === false) { + return { + success: false, + err: `Failed to convert original secret to bytes: ${originalSecretBytesRes.err}`, + }; + } + + return { + success: true, + data: { + t: reshareResult.t, + reshared_user_key_shares: resharedPoints, + original_secret: originalSecretBytesRes.data, + }, + }; + } catch (e) { + return { + success: false, + err: String(e), + }; + } +} + export async function runExpandShares( splitKeyShares: UserKeySharePointByNode[], additionalKSNodes: NodeNameAndEndpoint[], diff --git a/embed/oko_attached/src/crypto/reshare_v2.ts b/embed/oko_attached/src/crypto/reshare_v2.ts index 22bd9c15e..f642eae11 100644 --- a/embed/oko_attached/src/crypto/reshare_v2.ts +++ b/embed/oko_attached/src/crypto/reshare_v2.ts @@ -33,7 +33,7 @@ import { decodeKeyShareStringToPoint256, encodePoint256ToKeyShareString, } from "./key_share_utils"; -import { runExpandShares } from "./reshare"; +import { runExpandShares, runSeedExpandShares } from "./reshare"; import { expandTeddsaSigningShare, reconstructKeyPackage, @@ -219,9 +219,9 @@ export async function reshareUserKeySharesV2( resharedShares: ed25519ExpandRes.data.reshared_shares, }; - // 4.5. Process seed shares (secp256k1 SSS expand — same structure as secp256k1 key shares) + // 4.5. Process seed shares (257-bit prime SSS expand) const seedSharesByNode = convertSeedShares(sharesRes.data); - const seedExpandRes = await runExpandShares( + const seedExpandRes = await runSeedExpandShares( seedSharesByNode, additionalNodes, threshold, From 1ea8daabb7ec4927ab0c8a3ed09e3dba76cc32f5 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 20 Feb 2026 16:10:06 +0900 Subject: [PATCH 27/62] o --- e2e_tests/src/scripts/test_seed_export.ts | 184 ++++++++++++++++++ .../oko_attached/src/crypto/keygen_ed25519.ts | 28 ++- 2 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 e2e_tests/src/scripts/test_seed_export.ts diff --git a/e2e_tests/src/scripts/test_seed_export.ts b/e2e_tests/src/scripts/test_seed_export.ts new file mode 100644 index 000000000..1703bb18b --- /dev/null +++ b/e2e_tests/src/scripts/test_seed_export.ts @@ -0,0 +1,184 @@ +/** + * Temporary script to verify the seed export flow end-to-end. + * + * Tests: + * 1. Generate random 32-byte seed + * 2. Derive ed25519 public key from seed (standard derivation) + * 3. Split seed into 2-of-2 (server + user) via 257-bit prime SSS + * 4. Split user's seed share Y into 2-of-3 for KSN nodes + * 5. Combine 2 of 3 KSN shares → recover user Y + * 6. Combine server + user shares → recover original seed + * 7. Verify recovered seed matches & derive same ed25519 public key + * 8. Output in Phantom/SVM standard format: bs58(seed[32] || pubkey[32]) + * + * Run from the workspace root: + * npx tsx e2e_tests/src/scripts/test_seed_export.ts + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createHash, randomBytes } from "node:crypto"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Resolve workspace root (e2e_tests/src/scripts -> repo root) +const WORKSPACE_ROOT = resolve(__dirname, "../../.."); + +// --- Load WASM --- +const wasmJsPath = resolve( + WORKSPACE_ROOT, + "crypto/tecdsa/cait_sith_keplr_wasm/pkg/cait_sith_keplr_wasm.js", +); +const wasmBinPath = resolve( + WORKSPACE_ROOT, + "crypto/tecdsa/cait_sith_keplr_wasm/pkg/cait_sith_keplr_wasm_bg.wasm", +); + +const wasm = await import(wasmJsPath); +const wasmBytes = readFileSync(wasmBinPath); +wasm.initSync({ module: wasmBytes }); + +// --- Load ed25519 & bs58 --- +const { ed25519 } = await import("@noble/curves/ed25519"); +const bs58Module = await import("bs58"); +const bs58 = bs58Module.default; + +// --- Utilities --- +type Point = { x: number[]; y: number[] }; + +/** + * SHA-256 hash with first byte zeroed, matching hashKeyshareNodeNames + * from embed/oko_attached/src/crypto/hash.ts + * (used for KSN node identifiers) + */ +function hashName(name: string): number[] { + const hash = createHash("sha256").update(name).digest(); + hash[0] = 0; + return [...hash]; +} + +/** + * Seed SSS 2-of-2 identifier (big-endian 32-byte scalar). + * Matches Cait-Sith convention: client = 1, server = 2. + */ +function seedSplitId(scalar: number): number[] { + const id = new Array(32).fill(0); + id[31] = scalar; + return id; +} +const SEED_ID_CLIENT = seedSplitId(1); +const SEED_ID_SERVER = seedSplitId(2); + +function toHex(arr: number[] | Uint8Array | Buffer): string { + return Buffer.from(arr).toString("hex"); +} + +// --- Main Flow --- +console.log("=== Seed Export Flow Test ===\n"); + +// 1. Generate random 32-byte seed +const seed = randomBytes(32); +console.log("1. Generated seed:", toHex(seed)); + +// 2. Derive ed25519 public key from seed (standard ed25519 derivation) +const publicKey = ed25519.getPublicKey(seed); +console.log("2. Ed25519 public key:", toHex(publicKey)); + +// 3. Seed 2-of-2 split (server=2, client/user=1) — matching Cait-Sith convention +const seedSplitPoints: Point[] = wasm.seed_sss_split( + [...seed], + [SEED_ID_SERVER, SEED_ID_CLIENT], + 2, +); + +console.log("\n3. Seed 2-of-2 split (server + user):"); +console.log(" Server share (x):", toHex(seedSplitPoints[0].x)); +console.log(" Server share (y):", toHex(seedSplitPoints[0].y)); +console.log(" User share (x):", toHex(seedSplitPoints[1].x)); +console.log(" User share (y):", toHex(seedSplitPoints[1].y)); + +// 4. User's seed Y → 2-of-3 KSN split (simulating KSN node distribution) +const ksnNames = ["ksn_node_1", "ksn_node_2", "ksn_node_3"]; +const ksnHashes = ksnNames.map(hashName); + +const ksnSplitPoints: Point[] = wasm.seed_sss_split( + seedSplitPoints[1].y, // user's Y value as the secret + ksnHashes, + 2, // threshold +); + +console.log("\n4. User seed Y → 2-of-3 KSN split:"); +for (let i = 0; i < ksnSplitPoints.length; i++) { + console.log( + ` KSN[${i}] ${ksnNames[i]} (y): ${toHex(ksnSplitPoints[i].y)}`, + ); +} + +// 5. Combine 2 of 3 KSN shares → recover user Y +// Use shares [0] and [2] (skip [1]) to prove any 2-of-3 works +const ksnCombinedY: number[] = wasm.seed_sss_combine( + [ksnSplitPoints[0], ksnSplitPoints[2]], + 2, +); + +const userYMatches = Buffer.from(ksnCombinedY).equals( + Buffer.from(seedSplitPoints[1].y), +); +console.log("\n5. Combine KSN shares [0,2] → User Y:"); +console.log(" Recovered:", toHex(ksnCombinedY)); +console.log(" Original: ", toHex(seedSplitPoints[1].y)); +console.log(" Match:", userYMatches ? "OK" : "MISMATCH!"); + +// 6. Combine server + user → recover original seed +const reconstructedUserShare: Point = { + x: seedSplitPoints[1].x, + y: ksnCombinedY, +}; + +const recoveredSeed: number[] = wasm.seed_sss_combine( + [seedSplitPoints[0], reconstructedUserShare], + 2, +); + +const seedMatches = Buffer.from(recoveredSeed).equals(Buffer.from(seed)); +console.log("\n6. Combine server + user → Seed:"); +console.log(" Recovered:", toHex(recoveredSeed)); +console.log(" Original: ", toHex(seed)); +console.log(" Match:", seedMatches ? "OK" : "MISMATCH!"); + +// 7. Derive ed25519 from recovered seed & verify +const recoveredPublicKey = ed25519.getPublicKey(new Uint8Array(recoveredSeed)); +const pkMatches = Buffer.from(recoveredPublicKey).equals( + Buffer.from(publicKey), +); +console.log("\n7. Verify ed25519 public key from recovered seed:"); +console.log(" Recovered PK:", toHex(recoveredPublicKey)); +console.log(" Original PK: ", toHex(publicKey)); +console.log(" Match:", pkMatches ? "OK" : "MISMATCH!"); + +// 8. Export in Phantom/SVM standard format: bs58(seed[32] || pubkey[32]) +const keypairBytes = new Uint8Array(64); +keypairBytes.set(new Uint8Array(recoveredSeed)); +keypairBytes.set(recoveredPublicKey, 32); +const bs58Keypair = bs58.encode(keypairBytes); + +const solanaAddress = bs58.encode(recoveredPublicKey); + +console.log("\n========================================"); +console.log("Phantom/SVM Export Format:"); +console.log(""); +console.log(" Private Key (bs58):", bs58Keypair); +console.log(" Solana Address: ", solanaAddress); +console.log(""); +console.log(" Seed (hex): ", toHex(seed)); +console.log(" Public key (hex): ", toHex(publicKey)); +console.log("========================================"); + +// Overall result +const allPassed = userYMatches && seedMatches && pkMatches; +console.log( + `\n${allPassed ? "All checks passed!" : "Some checks FAILED!"}`, +); +process.exit(allPassed ? 0 : 1); diff --git a/embed/oko_attached/src/crypto/keygen_ed25519.ts b/embed/oko_attached/src/crypto/keygen_ed25519.ts index 2d238d327..babba5e89 100644 --- a/embed/oko_attached/src/crypto/keygen_ed25519.ts +++ b/embed/oko_attached/src/crypto/keygen_ed25519.ts @@ -214,9 +214,17 @@ function pointNumArrToSeedShare( return { success: true, data: { x: xRes.data, y: yRes.data } }; } -// Fixed identifiers for the 2-of-2 seed split (server + user) -const SEED_SPLIT_SERVER_ID = "oko_seed_server"; -const SEED_SPLIT_USER_ID = "oko_seed_user"; +/** + * Seed SSS 2-of-2 split identifiers (big-endian 32-byte scalars). + * Matches Cait-Sith convention: client = 1, server = 2. + */ +function seedSplitId(scalar: number): number[] { + const id = new Array(32).fill(0); + id[31] = scalar; + return id; +} +export const SEED_ID_CLIENT = seedSplitId(1); +export const SEED_ID_SERVER = seedSplitId(2); /** * Run ed25519 keygen from seed and split both signing share and seed for distribution. @@ -275,21 +283,9 @@ export async function runEd25519KeygenAndSplit( } // 4. Seed 2-of-2 split via 257-bit prime SSS (server + user) - const seedIdHashesRes = await hashKeyshareNodeNames([ - SEED_SPLIT_SERVER_ID, - SEED_SPLIT_USER_ID, - ]); - if (!seedIdHashesRes.success) { - return { - success: false, - err: { type: "sign_in_request_fail", error: seedIdHashesRes.err }, - }; - } - const [serverHash, userHash] = seedIdHashesRes.data; - const seedSplitPoints: PointNumArr[] = secp256k1Wasm.seed_sss_split( [...seed.toUint8Array()], - [[...serverHash.toUint8Array()], [...userHash.toUint8Array()]], + [SEED_ID_SERVER, SEED_ID_CLIENT], 2, ); From 2ce0c0c9ef12f936d02574c60c659b825eca6093 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 20 Feb 2026 16:48:25 +0900 Subject: [PATCH 28/62] Revert "o" This reverts commit e1e4cc7db8d7fa30d4ff200da0988081757950bd. --- .../src/routes/export_private_key.ts | 96 -------- .../user_dashboard_api/src/routes/index.ts | 4 - .../src/window_msgs/export_private_key.ts | 224 ++++++------------ 3 files changed, 75 insertions(+), 249 deletions(-) delete mode 100644 backend/user_dashboard_api/src/routes/export_private_key.ts diff --git a/backend/user_dashboard_api/src/routes/export_private_key.ts b/backend/user_dashboard_api/src/routes/export_private_key.ts deleted file mode 100644 index bf60a2834..000000000 --- a/backend/user_dashboard_api/src/routes/export_private_key.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Temporary API for testing the export_private_key flow. - -import { getWalletById } from "@oko-wallet/oko-pg-interface/oko_wallets"; -import { decryptDataAsync } from "@oko-wallet/crypto-js/node"; -import type { OkoApiResponse } from "@oko-wallet-types/api_response"; -import type { Response } from "express"; -import type { Pool } from "pg"; - -import type { UserAuthenticatedRequest } from "@oko-wallet-usrd-api/middleware/auth"; - -interface ExportPrivateKeyData { - secp256k1: string; - ed25519: string; -} - -export async function exportPrivateKey( - req: UserAuthenticatedRequest, - res: Response>, -) { - try { - const state = req.app.locals as { - db: Pool; - encryption_secret: string; - }; - const { wallet_id_secp256k1, wallet_id_ed25519 } = res.locals.user as { - email: string; - wallet_id_secp256k1: string; - wallet_id_ed25519: string; - }; - - // 1. Fetch secp256k1 wallet - const secp256k1WalletRes = await getWalletById( - state.db, - wallet_id_secp256k1, - ); - if (!secp256k1WalletRes.success || !secp256k1WalletRes.data) { - res.status(500).json({ - success: false, - code: "WALLET_NOT_FOUND", - msg: "secp256k1 wallet not found", - }); - return; - } - - // 2. Fetch ed25519 wallet - const ed25519WalletRes = await getWalletById(state.db, wallet_id_ed25519); - if (!ed25519WalletRes.success || !ed25519WalletRes.data) { - res.status(500).json({ - success: false, - code: "WALLET_NOT_FOUND", - msg: "ed25519 wallet not found", - }); - return; - } - - // 3. Decrypt server shares - const decryptedSecp256k1Share = await decryptDataAsync( - secp256k1WalletRes.data.enc_tss_share.toString("utf-8"), - state.encryption_secret, - ); - - const decryptedEd25519Share = await decryptDataAsync( - ed25519WalletRes.data.enc_tss_share.toString("utf-8"), - state.encryption_secret, - ); - - // secp256k1: the decrypted share is the hex scalar (keyshare_0) - // ed25519: the decrypted share is JSON { signing_share: number[], verifying_share: number[] } - const ed25519SharesData = JSON.parse(decryptedEd25519Share) as { - signing_share: number[]; - verifying_share: number[]; - }; - - // Convert ed25519 signing_share (number[]) to hex string - const ed25519SigningShareHex = Buffer.from( - ed25519SharesData.signing_share, - ).toString("hex"); - - res.status(200).json({ - success: true, - data: { - secp256k1: decryptedSecp256k1Share, - ed25519: ed25519SigningShareHex, - }, - }); - return; - } catch (error) { - console.error("Export private key error:", error); - res.status(500).json({ - success: false, - code: "UNKNOWN_ERROR", - msg: "Internal server error", - }); - return; - } -} diff --git a/backend/user_dashboard_api/src/routes/index.ts b/backend/user_dashboard_api/src/routes/index.ts index 885de3711..ab82285b0 100644 --- a/backend/user_dashboard_api/src/routes/index.ts +++ b/backend/user_dashboard_api/src/routes/index.ts @@ -39,7 +39,6 @@ import express, { type IRouter } from "express"; import type { Pool } from "pg"; import { changeCustomerPassword } from "./change_ct_password"; -import { exportPrivateKey } from "./export_private_key"; import { getConnectedApps } from "./get_connected_apps"; import { getCustomerApiKeys } from "./get_customer_api_keys"; import { getCustomerInfo } from "./get_customer_info"; @@ -95,8 +94,5 @@ export function makeUserRouter() { router.post("/get_connected_apps", userJwtMiddleware, getConnectedApps); - // Temporary API for testing the export_private_key flow. - router.post("/export_private_key", userJwtMiddleware, exportPrivateKey); - return router; } diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index 4c76833f1..05ddbc419 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -1,8 +1,4 @@ import bs58 from "bs58"; -import * as csWasmModule from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; -import { wasmModule as frostWasmModule } from "@oko-wallet/frost-ed25519-keplr-wasm"; -import { Bytes } from "@oko-wallet/bytes"; -import type { KeyPackageRaw } from "@oko-wallet/oko-types/teddsa"; import type { MsgEventContext } from "./types"; import { OKO_SDK_TARGET } from "./target"; @@ -11,7 +7,6 @@ import { OKO_API_ENDPOINT, USER_DASHBOARD_ORIGINS, } from "@oko-wallet-attached/requests/endpoints"; -import { computeVerifyingShare } from "@oko-wallet-attached/crypto/scalar"; // NOTE: This handler is user_dashboard-only (not exposed via SDK). // Types are defined locally, following the __get_connected_apps__ pattern. @@ -84,149 +79,80 @@ export async function handleExportPrivateKey( const ed25519Wallet = useAppState.getState().getWalletEd25519(hostOrigin); const ed25519PublicKey = ed25519Wallet?.publicKey ?? null; - try { - // 5. Fetch server shares from oko_api - const serverSharesRes = await fetch( - `${OKO_API_ENDPOINT}/user_dashboard/v1/export_private_key`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - }, - ); - - if (!serverSharesRes.ok) { - const ack: OkoWalletMsgExportPrivateKeyAck = { - target: OKO_SDK_TARGET, - msg_type: "__export_private_key_ack__", - payload: { - success: false, - error: { - type: "API_ERROR", - error: `HTTP ${serverSharesRes.status}`, - }, - }, - }; - port.postMessage(ack); - return; - } - - const serverSharesJson = (await serverSharesRes.json()) as { - success: boolean; - data?: { secp256k1: string; ed25519: string }; - msg?: string; - }; - - if (!serverSharesJson.success || !serverSharesJson.data) { - const ack: OkoWalletMsgExportPrivateKeyAck = { - target: OKO_SDK_TARGET, - msg_type: "__export_private_key_ack__", - payload: { - success: false, - error: { - type: "API_ERROR", - error: serverSharesJson.msg ?? "Unknown error", - }, - }, - }; - port.postMessage(ack); - return; - } - - const serverShares = serverSharesJson.data; - - // 6. Combine secp256k1 shares using cli_combine_shares - // keygen_1 (Participant 0) → user's keyshare_1 - // keygen_2 (Participant 1) → server's enc_tss_share - const fullSecp256k1Scalar = csWasmModule.cli_combine_shares({ - shares: { - 0: keyshare1, // user's share (Participant 0, from keygen_1) - 1: serverShares.secp256k1, // server's share (Participant 1, from keygen_2) - }, - }) as string; - const fullSecp256k1Key = `0x${fullSecp256k1Scalar}`; - - // 7. Combine ed25519 shares using FROST sss_combine - const hexToBytes = (hex: string): number[] => - (hex.match(/.{2}/g) ?? []).map((b) => Number.parseInt(b, 16)); - - // Parse user's key package from hex-encoded JSON - const keyPackageRaw: KeyPackageRaw = JSON.parse( - new TextDecoder().decode( - new Uint8Array(hexToBytes(keyPackageEd25519Hex)), - ), - ); - - // Convert server's signing_share hex to Bytes32 for computeVerifyingShare - const serverSigningShareArr = hexToBytes(serverShares.ed25519); - const serverSigningShareBytes = Bytes.fromUint8Array( - new Uint8Array(serverSigningShareArr), - 32, - ); - if (!serverSigningShareBytes.success) { - throw new Error( - `Invalid server signing_share: ${serverSigningShareBytes.err}`, - ); - } - const serverVerifyingShare = computeVerifyingShare( - serverSigningShareBytes.data, - ); - - // Construct server's KeyPackageRaw (server identifier = scalar 2 in LE) - const serverIdentifier = new Uint8Array(32); - serverIdentifier[0] = 2; - - const serverKeyPackageRaw: KeyPackageRaw = { - identifier: [...serverIdentifier], - signing_share: serverSigningShareArr, - verifying_share: [...serverVerifyingShare.toUint8Array()], - verifying_key: keyPackageRaw.verifying_key, - min_signers: keyPackageRaw.min_signers, - }; - - // Combine both key packages to recover the full ed25519 signing secret - const combinedSigningSecret: number[] = frostWasmModule.sss_combine([ - keyPackageRaw, - serverKeyPackageRaw, - ]); - - // Build 64-byte ed25519 keypair (secret + pubkey) and encode to base58 - const ed25519PubKeyBytes = ed25519PublicKey - ? hexToBytes(ed25519PublicKey) - : keyPackageRaw.verifying_key; - - const ed25519Keypair = new Uint8Array([ - ...combinedSigningSecret, - ...ed25519PubKeyBytes, - ]); - const fullEd25519Key = bs58.encode(ed25519Keypair); - - const ack: OkoWalletMsgExportPrivateKeyAck = { - target: OKO_SDK_TARGET, - msg_type: "__export_private_key_ack__", - payload: { - success: true, - data: { - secp256k1: fullSecp256k1Key, - ed25519: fullEd25519Key, - }, - }, - }; - port.postMessage(ack); - } catch (error) { - const ack: OkoWalletMsgExportPrivateKeyAck = { - target: OKO_SDK_TARGET, - msg_type: "__export_private_key_ack__", - payload: { - success: false, - error: { - type: "COMBINE_ERROR", - error: error instanceof Error ? error.message : String(error), - }, + // ------------------------------------------------------------------- + // TODO: Replace mock with actual implementation when oko_api is ready. + // + // Actual flow: + // + // (a) Fetch server shares from oko_api + // + // const serverSharesRes = await fetch( + // `${OKO_API_ENDPOINT}/user_dashboard/v1/export_private_key`, + // { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Authorization: `Bearer ${authToken}`, + // }, + // }, + // ); + // const serverShares = await serverSharesRes.json(); + // // serverShares = { secp256k1: string (keyshare_0 hex), ed25519: string (server signing_share hex) } + // + // (b) Combine secp256k1 shares + // + // import * as wasmModule from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; + // + // const keyCombineInput = { + // shares: { + // 0: serverShares.secp256k1, // server's share (Participant 0) + // 1: keyshare1, // user's share (Participant 1) + // }, + // }; + // const fullSecp256k1Key = wasmModule.cli_combine_shares(keyCombineInput); + // // fullSecp256k1Key is a secp256k1 Scalar → convert to hex + // + // (c) Combine ed25519 shares + // + // Parse keyPackageEd25519Hex to get user's signing_share. + // const keyPackageRaw = JSON.parse( + // Buffer.from(keyPackageEd25519Hex, "hex").toString("utf-8"), + // ); + // const userSigningShare = keyPackageRaw.signing_share; // number[] + // + // Combine user's signing_share + server's signing_share using FROST sss_combine + // to recover the full ed25519 signing secret. + // + // const fullEd25519Secret = ... // 32 bytes + // const fullEd25519Keypair = Uint8Array.from([...fullEd25519Secret, ...ed25519PublicKeyBytes]) // 64 bytes + // const fullEd25519Key = bs58.encode(fullEd25519Keypair) // base58 (Phantom/Solflare import format) + // + // ------------------------------------------------------------------- + + // [Mock] Return deterministic test keys for development + const mockSecp256k1 = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + const mockEd25519SigningSecret = "a".repeat(64); // 32 bytes hex + const mockEd25519PublicKey = ed25519PublicKey ?? "b".repeat(64); // 32 bytes hex + const mockEd25519Hex = mockEd25519SigningSecret + mockEd25519PublicKey; + + // Convert 64-byte ed25519 keypair (secret + pubkey) from hex to base58 + // This is the standard format used by Phantom, Solflare, etc. + const ed25519Bytes = new Uint8Array( + (mockEd25519Hex.match(/.{2}/g) ?? []).map((b) => Number.parseInt(b, 16)), + ); + const mockEd25519Base58 = bs58.encode(ed25519Bytes); + + const ack: OkoWalletMsgExportPrivateKeyAck = { + target: OKO_SDK_TARGET, + msg_type: "__export_private_key_ack__", + payload: { + success: true, + data: { + secp256k1: mockSecp256k1, + ed25519: mockEd25519Base58, }, - }; - port.postMessage(ack); - } + }, + }; + port.postMessage(ack); } From 0d3ee2b0df7c1516d56cc0e9e1cd9fa17c129020 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 20 Feb 2026 17:55:00 +0900 Subject: [PATCH 29/62] o --- backend/openapi/src/tss/user.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/openapi/src/tss/user.ts b/backend/openapi/src/tss/user.ts index 7bf539289..dfdeb4463 100644 --- a/backend/openapi/src/tss/user.ts +++ b/backend/openapi/src/tss/user.ts @@ -289,6 +289,18 @@ export const CheckEmailSuccessResponseV2Schema = registry.register( }), ); +export const ExportSharesRequestSchema = registry.register( + "TssExportSharesRequest", + z.object({ + auth_type: AuthTypeEnum.openapi({ + description: "Authentication provider type for re-authentication", + }), + id_token: z.string().openapi({ + description: "OAuth id_token from re-authentication", + }), + }), +); + const ExportSharesDataSchema = registry.register( "TssExportSharesData", z.object({ From 2789ed14108fab2ac20265d9d8b01a9461fedcb6 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Fri, 20 Feb 2026 18:22:08 +0900 Subject: [PATCH 30/62] oko_api: implement dual-auth for export shares endpoint --- .../server/src/routes/tss_v2/export_shares.ts | 170 ++++++++++++++++-- common/oko_types/src/user/index.ts | 10 ++ 2 files changed, 169 insertions(+), 11 deletions(-) diff --git a/backend/oko_api/server/src/routes/tss_v2/export_shares.ts b/backend/oko_api/server/src/routes/tss_v2/export_shares.ts index 2c80bb3e8..fd810329d 100644 --- a/backend/oko_api/server/src/routes/tss_v2/export_shares.ts +++ b/backend/oko_api/server/src/routes/tss_v2/export_shares.ts @@ -1,15 +1,38 @@ import type { Response } from "express"; import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { + ExportSharesRequest, + ExportSharesResponse, +} from "@oko-wallet/oko-types/user"; +import type { Result } from "@oko-wallet/stdlib-js"; import { decryptDataAsync } from "@oko-wallet/crypto-js/node"; import { ErrorResponseSchema, UserAuthHeaderSchema, } from "@oko-wallet/oko-api-openapi/common"; -import { ExportSharesSuccessResponseSchema } from "@oko-wallet/oko-api-openapi/tss"; +import { + ExportSharesRequestSchema, + ExportSharesSuccessResponseSchema, +} from "@oko-wallet/oko-api-openapi/tss"; import { registry } from "@oko-wallet/oko-api-openapi"; +import { getUserByEmailAndAuthType } from "@oko-wallet/oko-pg-interface/oko_users"; import { validateWalletEmailAndCurveType } from "@oko-wallet-api/api/tss/utils"; import { type UserAuthenticatedRequest } from "@oko-wallet-api/middleware/auth/keplr_auth"; +import { validateOAuthToken } from "@oko-wallet-api/middleware/auth/google_auth/validate"; +import { GOOGLE_CLIENT_ID } from "@oko-wallet-api/middleware/auth/google_auth/client_id"; +import { validateAuth0IdToken } from "@oko-wallet-api/middleware/auth/auth0_auth/validate"; +import { + AUTH0_CLIENT_ID, + AUTH0_DOMAIN, +} from "@oko-wallet-api/middleware/auth/auth0_auth/client_id"; +import { validateAccessTokenOfX } from "@oko-wallet-api/middleware/auth/x_auth/validate"; +import { + validateTelegramHash, + type TelegramUserData, +} from "@oko-wallet-api/middleware/auth/telegram_auth/validate"; +import { validateDiscordOAuthToken } from "@oko-wallet-api/middleware/auth/discord_auth/validate"; registry.registerPath({ method: "post", @@ -17,10 +40,17 @@ registry.registerPath({ tags: ["TSS"], summary: "Export server shares for wallet export", description: - "Exports the server's secp256k1 TSS share and ed25519 seed_share from the authenticated user's wallets. Used for private key export.", + "Exports the server's secp256k1 TSS share and ed25519 seed_share. Requires dual authentication: JWT (Authorization header) + OAuth re-authentication (request body).", security: [{ userAuth: [] }], request: { headers: UserAuthHeaderSchema, + body: { + content: { + "application/json": { + schema: ExportSharesRequestSchema, + }, + }, + }, }, responses: { 200: { @@ -32,7 +62,7 @@ registry.registerPath({ }, }, 401: { - description: "Unauthorized - Invalid token or wallet mismatch", + description: "Unauthorized - Invalid token or user mismatch", content: { "application/json": { schema: ErrorResponseSchema, @@ -50,13 +80,87 @@ registry.registerPath({ }, }); -interface ExportSharesResponse { - secp256k1_share: string; - ed25519_seed_share: string; +/** + * Validate OAuth id_token from request body (not from Authorization header). + * Reuses each provider's underlying validation function and constructs + * user_identifier with the same prefix convention as the auth middlewares. + */ +async function validateOAuthIdToken( + authType: AuthType, + idToken: string, + telegramBotToken?: string, +): Promise> { + switch (authType) { + case "google": { + const result = await validateOAuthToken(idToken, GOOGLE_CLIENT_ID); + if (!result.success) { + return { success: false, err: result.err }; + } + if (!result.data?.sub) { + return { success: false, err: "Can't get sub from Google token" }; + } + return { success: true, data: `google_${result.data.sub}` }; + } + case "auth0": { + const result = await validateAuth0IdToken({ + idToken, + clientId: AUTH0_CLIENT_ID, + domain: AUTH0_DOMAIN, + }); + if (!result.success) { + return { success: false, err: result.err }; + } + if (!result.data?.email) { + return { success: false, err: "Can't get email from Auth0 token" }; + } + return { success: true, data: result.data.email }; + } + case "x": { + const result = await validateAccessTokenOfX(idToken); + if (!result.success) { + return { success: false, err: result.err }; + } + if (!result.data?.id) { + return { success: false, err: "Can't get id from X token" }; + } + return { success: true, data: `x_${result.data.id}` }; + } + case "telegram": { + if (!telegramBotToken) { + return { success: false, err: "Telegram bot token not configured" }; + } + let userData: TelegramUserData; + try { + userData = JSON.parse(idToken) as TelegramUserData; + } catch { + return { success: false, err: "Invalid Telegram token format" }; + } + const result = validateTelegramHash(userData, telegramBotToken); + if (!result.success) { + return { success: false, err: result.err }; + } + if (!result.data?.id) { + return { success: false, err: "Can't get id from Telegram token" }; + } + return { success: true, data: `telegram_${result.data.id}` }; + } + case "discord": { + const result = await validateDiscordOAuthToken(idToken); + if (!result.success) { + return { success: false, err: result.err }; + } + if (!result.data?.id) { + return { success: false, err: "Can't get id from Discord token" }; + } + return { success: true, data: `discord_${result.data.id}` }; + } + default: + return { success: false, err: `Invalid auth_type: ${authType}` }; + } } export async function exportShares( - req: UserAuthenticatedRequest, + req: UserAuthenticatedRequest, res: Response>, ) { const state = req.app.locals; @@ -98,7 +202,52 @@ export async function exportShares( const secp256k1Wallet = secp256k1ValidateRes.data; const ed25519Wallet = ed25519ValidateRes.data; - // 3. Decrypt secp256k1 enc_tss_share (raw string, not JSON) + // 3. Dual-auth: Validate OAuth re-authentication token from body + const { auth_type, id_token } = req.body; + + const oauthResult = await validateOAuthIdToken( + auth_type, + id_token, + state.telegram_bot_token, + ); + if (!oauthResult.success) { + res.status(401).json({ + success: false, + code: "UNAUTHORIZED", + msg: `OAuth validation failed: ${oauthResult.err}`, + }); + return; + } + + // 4. Same-user verification: OAuth identity must match JWT-authenticated wallets + const oauthUserIdentifier = oauthResult.data; + const oauthUserRes = await getUserByEmailAndAuthType( + state.db, + oauthUserIdentifier, + auth_type, + ); + if (!oauthUserRes.success || !oauthUserRes.data) { + res.status(401).json({ + success: false, + code: "UNAUTHORIZED", + msg: "OAuth user not found", + }); + return; + } + + if ( + oauthUserRes.data.user_id !== secp256k1Wallet.user_id || + oauthUserRes.data.user_id !== ed25519Wallet.user_id + ) { + res.status(401).json({ + success: false, + code: "UNAUTHORIZED", + msg: "User mismatch: OAuth identity does not match JWT wallets", + }); + return; + } + + // 5. Decrypt secp256k1 enc_tss_share (raw string, not JSON) const secp256k1EncryptedShare = secp256k1Wallet.enc_tss_share.toString("utf-8"); const secp256k1Share = await decryptDataAsync( @@ -106,9 +255,8 @@ export async function exportShares( state.encryption_secret, ); - // 4. Decrypt ed25519 enc_tss_share (JSON with signing_share, verifying_share, seed_share) - const ed25519EncryptedShare = - ed25519Wallet.enc_tss_share.toString("utf-8"); + // 6. Decrypt ed25519 enc_tss_share (JSON with signing_share, verifying_share, seed_share) + const ed25519EncryptedShare = ed25519Wallet.enc_tss_share.toString("utf-8"); const ed25519Decrypted = await decryptDataAsync( ed25519EncryptedShare, state.encryption_secret, diff --git a/common/oko_types/src/user/index.ts b/common/oko_types/src/user/index.ts index 3369a70fa..754e85e8b 100644 --- a/common/oko_types/src/user/index.ts +++ b/common/oko_types/src/user/index.ts @@ -94,6 +94,16 @@ export interface SignInResponseV2 { }; } +export interface ExportSharesRequest { + auth_type: AuthType; + id_token: string; +} + +export interface ExportSharesResponse { + secp256k1_share: string; + ed25519_seed_share: string; +} + export interface SignInSilentlyResponse { token: string | null; // user: { From 3167280a64e5ac7396ee1cd7885af44bba22e3da Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 13:54:03 +0900 Subject: [PATCH 31/62] oko_attached: add re-auth popup route --- .../src/components/export/export_reauth.tsx | 78 +++++++ .../components/export/use_export_reauth.ts | 217 ++++++++++++++++++ embed/oko_attached/src/routeTree.gen.ts | 21 ++ .../src/routes/export/reauth/index.tsx | 7 + 4 files changed, 323 insertions(+) create mode 100644 embed/oko_attached/src/components/export/export_reauth.tsx create mode 100644 embed/oko_attached/src/components/export/use_export_reauth.ts create mode 100644 embed/oko_attached/src/routes/export/reauth/index.tsx diff --git a/embed/oko_attached/src/components/export/export_reauth.tsx b/embed/oko_attached/src/components/export/export_reauth.tsx new file mode 100644 index 000000000..fa88c6e88 --- /dev/null +++ b/embed/oko_attached/src/components/export/export_reauth.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; + +import type { AuthType } from "@oko-wallet/oko-types/auth"; + +import { useExportReauth } from "./use_export_reauth"; + +type ReauthStatus = "loading" | "redirecting" | "error"; + +/** + * Re-auth popup dispatcher. + * Reads `auth_type` from URL search params and dispatches to the appropriate flow: + * - google / x / discord → immediate OAuth redirect via useExportReauth hook + * - auth0 / telegram → dedicated UI component (Phase 7) + */ +export function ExportReauth() { + const params = new URLSearchParams(window.location.search); + const authType = params.get("auth_type") as AuthType | null; + + if (!authType) { + return
    Error: auth_type parameter is required
    ; + } + + switch (authType) { + case "google": + case "x": + case "discord": + return ; + + case "auth0": + // Phase 7: EmailReauth component + return
    Email re-authentication (coming soon)
    ; + + case "telegram": + // Phase 7: TelegramReauth component + return
    Telegram re-authentication (coming soon)
    ; + + default: + return
    Error: unsupported auth_type: {authType}
    ; + } +} + +function OAuthRedirect({ authType }: { authType: "google" | "x" | "discord" }) { + const [status, setStatus] = useState("loading"); + const [error, setError] = useState(null); + + const { startReauth } = useExportReauth(); + + useEffect(() => { + let cancelled = false; + + (async () => { + const result = await startReauth(authType); + + if (cancelled) { + return; + } + + if (!result.success) { + setStatus("error"); + setError(result.err); + return; + } + + setStatus("redirecting"); + // window.location.href is set inside startReauth — page will navigate away + })(); + + return () => { + cancelled = true; + }; + }, [authType, startReauth]); + + if (status === "error") { + return
    Error: {error}
    ; + } + + return
    Redirecting to {authType} authentication...
    ; +} diff --git a/embed/oko_attached/src/components/export/use_export_reauth.ts b/embed/oko_attached/src/components/export/use_export_reauth.ts new file mode 100644 index 000000000..c04d40b8e --- /dev/null +++ b/embed/oko_attached/src/components/export/use_export_reauth.ts @@ -0,0 +1,217 @@ +import { useCallback } from "react"; + +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { OAuthState } from "@oko-wallet/oko-sdk-core"; +import type { Result } from "@oko-wallet/stdlib-js"; + +// Client IDs (public constants, same as SDK) +const GOOGLE_CLIENT_ID = + "421793224165-cpmbt6enqrj6ad6n4ujokham8qdmnnln.apps.googleusercontent.com"; +const X_CLIENT_ID = "eWJPdVNYNlV6dEpNSTM3T01GRGI6MTpjaQ"; +const DISCORD_CLIENT_ID = "1445280712121913384"; + +function generateNonce(length = 8) { + return Array.from(crypto.getRandomValues(new Uint8Array(length))) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +function generateRandomString(length = 64): string { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + + let binary = ""; + for (let i = 0; i < array.length; i++) { + binary += String.fromCharCode(array[i]); + } + + const base64 = btoa(binary); + return base64.replace(/[+\/]|(=+)$/g, (match) => { + if (match === "+") { + return "-"; + } + if (match === "/") { + return "_"; + } + return ""; + }); +} + +async function sha256(input: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(input); + return crypto.subtle.digest("SHA-256", data); +} + +function base64UrlEncode(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let binary = ""; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + const base64 = btoa(binary); + return base64.replace(/[+\/]|(=+)$/g, (match) => { + if (match === "+") { + return "-"; + } + if (match === "/") { + return "_"; + } + return ""; + }); +} + +async function createPkcePair(): Promise<{ + codeVerifier: string; + codeChallenge: string; +}> { + const codeVerifier = generateRandomString(64); + const hash = await sha256(codeVerifier); + const codeChallenge = base64UrlEncode(hash); + return { codeVerifier, codeChallenge }; +} + +function findEmbeddedIframe(): Window | null { + if (!window.opener) { + return null; + } + + const targetOrigin = new URL(window.location.toString()).origin; + + for (let idx = 0; idx < window.opener.frames.length; idx += 1) { + try { + const frame = window.opener.frames[idx]; + if (frame.location.origin === targetOrigin) { + return frame; + } + } catch { + // Cross-origin frame, skip + } + } + + return null; +} + +function sendReauthParamsToIframe( + iframe: Window, + params: { nonce?: string; code_verifier?: string }, +): void { + const targetOrigin = new URL(window.location.toString()).origin; + + iframe.postMessage( + { + target: "oko_attached", + msg_type: "set_reauth_params", + payload: params, + }, + targetOrigin, + ); +} + +function buildGoogleOAuthUrl(nonce: string): string { + const redirectUri = `${window.location.origin}/google/callback`; + + const oauthState: OAuthState = { + apiKey: "", + targetOrigin: window.location.origin, + provider: "google", + }; + + const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth"); + authUrl.searchParams.set("client_id", GOOGLE_CLIENT_ID); + authUrl.searchParams.set("redirect_uri", redirectUri); + authUrl.searchParams.set("response_type", "token id_token"); + authUrl.searchParams.set("scope", "openid email profile"); + authUrl.searchParams.set("prompt", "login"); + authUrl.searchParams.set("nonce", nonce); + authUrl.searchParams.set("state", JSON.stringify(oauthState)); + + return authUrl.toString(); +} + +function buildXOAuthUrl(codeChallenge: string): string { + const redirectUri = `${window.location.origin}/x/callback`; + + const oauthState: OAuthState = { + apiKey: "", + targetOrigin: window.location.origin, + provider: "x", + }; + const oauthStateString = btoa(JSON.stringify(oauthState)); + + const authUrl = new URL("https://twitter.com/i/oauth2/authorize"); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("client_id", X_CLIENT_ID); + authUrl.searchParams.set("redirect_uri", redirectUri); + authUrl.searchParams.set("scope", "tweet.read users.read offline.access"); + authUrl.searchParams.set("code_challenge", codeChallenge); + authUrl.searchParams.set("code_challenge_method", "S256"); + authUrl.searchParams.set("state", oauthStateString); + + return authUrl.toString(); +} + +function buildDiscordOAuthUrl(codeChallenge: string): string { + const redirectUri = `${window.location.origin}/discord/callback`; + + const oauthState: OAuthState = { + apiKey: "", + targetOrigin: window.location.origin, + provider: "discord", + }; + const oauthStateString = btoa(JSON.stringify(oauthState)); + + const authUrl = new URL("https://discord.com/api/oauth2/authorize"); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("client_id", DISCORD_CLIENT_ID); + authUrl.searchParams.set("redirect_uri", redirectUri); + authUrl.searchParams.set("scope", "identify email"); + authUrl.searchParams.set("code_challenge", codeChallenge); + authUrl.searchParams.set("code_challenge_method", "S256"); + authUrl.searchParams.set("state", oauthStateString); + + return authUrl.toString(); +} + +// --- Hook --- + +export function useExportReauth() { + const startReauth = useCallback( + async ( + authType: "google" | "x" | "discord", + ): Promise> => { + const iframe = findEmbeddedIframe(); + if (!iframe) { + return { + success: false, + err: "Cannot find embedded iframe. Make sure this page was opened from the dashboard.", + }; + } + + let oauthUrl: string; + + if (authType === "google") { + const nonce = generateNonce(); + sendReauthParamsToIframe(iframe, { nonce }); + oauthUrl = buildGoogleOAuthUrl(nonce); + } else { + // X and Discord use PKCE + const { codeVerifier, codeChallenge } = await createPkcePair(); + sendReauthParamsToIframe(iframe, { code_verifier: codeVerifier }); + + if (authType === "x") { + oauthUrl = buildXOAuthUrl(codeChallenge); + } else { + oauthUrl = buildDiscordOAuthUrl(codeChallenge); + } + } + + window.location.href = oauthUrl; + + return { success: true, data: void 0 }; + }, + [], + ); + + return { startReauth }; +} diff --git a/embed/oko_attached/src/routeTree.gen.ts b/embed/oko_attached/src/routeTree.gen.ts index bdca7b6b4..8ce6113cc 100644 --- a/embed/oko_attached/src/routeTree.gen.ts +++ b/embed/oko_attached/src/routeTree.gen.ts @@ -17,6 +17,7 @@ import { Route as TelegramCallbackIndexRouteImport } from './routes/telegram/cal import { Route as GoogleCallbackIndexRouteImport } from './routes/google/callback/index' import { Route as EmailCallbackIndexRouteImport } from './routes/email/callback/index' import { Route as DiscordCallbackIndexRouteImport } from './routes/discord/callback/index' +import { Route as ExportReauthIndexRouteImport } from './routes/export/reauth/index' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -58,10 +59,16 @@ const DiscordCallbackIndexRoute = DiscordCallbackIndexRouteImport.update({ path: '/discord/callback/', getParentRoute: () => rootRouteImport, } as any) +const ExportReauthIndexRoute = ExportReauthIndexRouteImport.update({ + id: '/export/reauth/', + path: '/export/reauth/', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/email/': typeof EmailIndexRoute + '/export/reauth/': typeof ExportReauthIndexRoute '/telegram/': typeof TelegramIndexRoute '/discord/callback/': typeof DiscordCallbackIndexRoute '/email/callback/': typeof EmailCallbackIndexRoute @@ -72,6 +79,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/email': typeof EmailIndexRoute + '/export/reauth': typeof ExportReauthIndexRoute '/telegram': typeof TelegramIndexRoute '/discord/callback': typeof DiscordCallbackIndexRoute '/email/callback': typeof EmailCallbackIndexRoute @@ -83,6 +91,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/email/': typeof EmailIndexRoute + '/export/reauth/': typeof ExportReauthIndexRoute '/telegram/': typeof TelegramIndexRoute '/discord/callback/': typeof DiscordCallbackIndexRoute '/email/callback/': typeof EmailCallbackIndexRoute @@ -95,6 +104,7 @@ export interface FileRouteTypes { fullPaths: | '/' | '/email/' + | '/export/reauth/' | '/telegram/' | '/discord/callback/' | '/email/callback/' @@ -105,6 +115,7 @@ export interface FileRouteTypes { to: | '/' | '/email' + | '/export/reauth' | '/telegram' | '/discord/callback' | '/email/callback' @@ -115,6 +126,7 @@ export interface FileRouteTypes { | '__root__' | '/' | '/email/' + | '/export/reauth/' | '/telegram/' | '/discord/callback/' | '/email/callback/' @@ -126,6 +138,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute EmailIndexRoute: typeof EmailIndexRoute + ExportReauthIndexRoute: typeof ExportReauthIndexRoute TelegramIndexRoute: typeof TelegramIndexRoute DiscordCallbackIndexRoute: typeof DiscordCallbackIndexRoute EmailCallbackIndexRoute: typeof EmailCallbackIndexRoute @@ -150,6 +163,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof TelegramIndexRouteImport parentRoute: typeof rootRouteImport } + '/export/reauth/': { + id: '/export/reauth/' + path: '/export/reauth' + fullPath: '/export/reauth/' + preLoaderRoute: typeof ExportReauthIndexRouteImport + parentRoute: typeof rootRouteImport + } '/email/': { id: '/email/' path: '/email' @@ -198,6 +218,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, EmailIndexRoute: EmailIndexRoute, + ExportReauthIndexRoute: ExportReauthIndexRoute, TelegramIndexRoute: TelegramIndexRoute, DiscordCallbackIndexRoute: DiscordCallbackIndexRoute, EmailCallbackIndexRoute: EmailCallbackIndexRoute, diff --git a/embed/oko_attached/src/routes/export/reauth/index.tsx b/embed/oko_attached/src/routes/export/reauth/index.tsx new file mode 100644 index 000000000..4557b8e23 --- /dev/null +++ b/embed/oko_attached/src/routes/export/reauth/index.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { ExportReauth } from "@oko-wallet-attached/components/export/export_reauth"; + +export const Route = createFileRoute("/export/reauth/")({ + component: ExportReauth, +}); From 8c92604d9c2e3b5e4c51a4f058b3c6615a01a437 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 14:04:48 +0900 Subject: [PATCH 32/62] oko_attached: add email and telegram re-auth --- .../src/components/export/email_reauth.tsx | 258 ++++++++++++++++++ .../src/components/export/export_reauth.tsx | 14 +- .../src/components/export/telegram_reauth.tsx | 101 +++++++ .../components/export/use_export_reauth.ts | 8 +- 4 files changed, 366 insertions(+), 15 deletions(-) create mode 100644 embed/oko_attached/src/components/export/email_reauth.tsx create mode 100644 embed/oko_attached/src/components/export/telegram_reauth.tsx diff --git a/embed/oko_attached/src/components/export/email_reauth.tsx b/embed/oko_attached/src/components/export/email_reauth.tsx new file mode 100644 index 000000000..7be853cdf --- /dev/null +++ b/embed/oko_attached/src/components/export/email_reauth.tsx @@ -0,0 +1,258 @@ +import { type FormEvent, useEffect, useMemo, useState } from "react"; + +import type { OAuthState } from "@oko-wallet/oko-sdk-core"; +import { OtpInput } from "@oko-wallet/oko-common-ui/otp_input"; + +import { getAuth0WebAuth } from "@oko-wallet-attached/config/auth0"; +import { + sendEmailOTPCode, + verifyEmailOTPCode, +} from "@oko-wallet-attached/lib/auth0"; + +import { + findEmbeddedIframe, + generateNonce, + sendReauthParamsToIframe, +} from "./use_export_reauth"; + +const CODE_LENGTH = 6; +const RESEND_COOLDOWN_SECONDS = 180; +const LOG_PREFIX = "[attached][email_reauth]"; + +type Step = "enter_email" | "verify_code"; + +export function EmailReauth() { + const webAuth = useMemo(() => getAuth0WebAuth(), []); + + const [step, setStep] = useState("enter_email"); + const [email, setEmail] = useState(""); + const [otpDigits, setOtpDigits] = useState( + Array.from({ length: CODE_LENGTH }, () => ""), + ); + const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [resendTimer, setResendTimer] = useState(0); + const [iframeSent, setIframeSent] = useState(false); + + const isEmailValid = useMemo( + () => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()), + [email], + ); + const isOtpComplete = useMemo( + () => + otpDigits.filter((d) => d.trim().length > 0).length === CODE_LENGTH && + otpDigits.join("").length === CODE_LENGTH, + [otpDigits], + ); + + // Generate nonce and send to iframe on mount + const nonce = useMemo(() => generateNonce(), []); + const oauthState = useMemo( + () => ({ + apiKey: "", + targetOrigin: window.location.origin, + provider: "auth0", + }), + [], + ); + + useEffect(() => { + const iframe = findEmbeddedIframe(); + if (iframe) { + sendReauthParamsToIframe(iframe, { nonce }); + setIframeSent(true); + } else { + setErrorMessage( + "Cannot find embedded iframe. Make sure this page was opened from the dashboard.", + ); + } + }, [nonce]); + + // Resend timer countdown + useEffect(() => { + if (resendTimer <= 0) { + return; + } + + const timer = window.setInterval(() => { + setResendTimer((prev) => { + if (prev <= 1) { + window.clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => window.clearInterval(timer); + }, [resendTimer]); + + // Auto-verify when OTP is complete + useEffect(() => { + if (isOtpComplete && !isSubmitting && !errorMessage) { + void handleVerifyCode(); + } + }, [isOtpComplete, isSubmitting, errorMessage]); + + const resetError = () => setErrorMessage(null); + + const handleSubmitEmail = async () => { + if (!isEmailValid || isSubmitting || !iframeSent) { + return; + } + + try { + setIsSubmitting(true); + setErrorMessage(null); + console.log(`${LOG_PREFIX} requesting OTP for`, email.trim()); + await sendEmailOTPCode({ webAuth, email: email.trim() }); + setStep("verify_code"); + setOtpDigits(Array.from({ length: CODE_LENGTH }, () => "")); + setResendTimer(RESEND_COOLDOWN_SECONDS); + } catch (err) { + setErrorMessage( + err instanceof Error ? err.message : "Failed to request the code.", + ); + } finally { + setIsSubmitting(false); + } + }; + + const handleVerifyCode = async () => { + if (!isOtpComplete || isSubmitting) { + return; + } + + setIsSubmitting(true); + setErrorMessage(null); + + const callbackUrl = `${window.location.origin}/email/callback`; + + console.log(`${LOG_PREFIX} verifying OTP for`, email.trim()); + + verifyEmailOTPCode({ + webAuth, + email: email.trim(), + verificationCode: otpDigits.join(""), + callbackUrl, + nonce, + state: JSON.stringify(oauthState), + onError: (err) => { + console.error(`${LOG_PREFIX} verification error`, err); + const msg = err.message.toLowerCase(); + if ( + msg.includes("wrong email") || + msg.includes("verification code") || + msg.includes("invalid code") + ) { + setErrorMessage("Invalid code. Try again."); + } else { + setErrorMessage(err.message); + } + setIsSubmitting(false); + }, + }); + }; + + const handleResendCode = async () => { + if (resendTimer > 0 || isSubmitting) { + return; + } + + try { + setIsSubmitting(true); + setErrorMessage(null); + await sendEmailOTPCode({ webAuth, email: email.trim() }); + setResendTimer(RESEND_COOLDOWN_SECONDS); + } catch (err) { + setErrorMessage( + err instanceof Error ? err.message : "Failed to resend the code.", + ); + } finally { + setIsSubmitting(false); + } + }; + + const onSubmitEmail = (e: FormEvent) => { + e.preventDefault(); + void handleSubmitEmail(); + }; + + const onSubmitCode = (e: FormEvent) => { + e.preventDefault(); + void handleVerifyCode(); + }; + + if (step === "enter_email") { + return ( +
    +

    Email Re-Authentication

    +
    + { + resetError(); + setEmail(e.target.value); + }} + style={{ + width: "100%", + padding: "8px", + marginBottom: "8px", + boxSizing: "border-box", + }} + autoFocus + /> + + {errorMessage && ( +
    {errorMessage}
    + )} +
    +
    + ); + } + + return ( +
    +

    Check your email

    +

    Enter the 6-digit code sent to {email}.

    +
    + { + resetError(); + setOtpDigits(digits); + }} + disabled={isSubmitting} + isError={!!errorMessage} + /> + {errorMessage && ( +
    {errorMessage}
    + )} +
    + Didn't get the code? + + {resendTimer > 0 && {resendTimer}s} +
    + +
    + ); +} diff --git a/embed/oko_attached/src/components/export/export_reauth.tsx b/embed/oko_attached/src/components/export/export_reauth.tsx index fa88c6e88..5569a7ab4 100644 --- a/embed/oko_attached/src/components/export/export_reauth.tsx +++ b/embed/oko_attached/src/components/export/export_reauth.tsx @@ -3,15 +3,11 @@ import { useEffect, useState } from "react"; import type { AuthType } from "@oko-wallet/oko-types/auth"; import { useExportReauth } from "./use_export_reauth"; +import { EmailReauth } from "./email_reauth"; +import { TelegramReauth } from "./telegram_reauth"; type ReauthStatus = "loading" | "redirecting" | "error"; -/** - * Re-auth popup dispatcher. - * Reads `auth_type` from URL search params and dispatches to the appropriate flow: - * - google / x / discord → immediate OAuth redirect via useExportReauth hook - * - auth0 / telegram → dedicated UI component (Phase 7) - */ export function ExportReauth() { const params = new URLSearchParams(window.location.search); const authType = params.get("auth_type") as AuthType | null; @@ -27,12 +23,10 @@ export function ExportReauth() { return ; case "auth0": - // Phase 7: EmailReauth component - return
    Email re-authentication (coming soon)
    ; + return ; case "telegram": - // Phase 7: TelegramReauth component - return
    Telegram re-authentication (coming soon)
    ; + return ; default: return
    Error: unsupported auth_type: {authType}
    ; diff --git a/embed/oko_attached/src/components/export/telegram_reauth.tsx b/embed/oko_attached/src/components/export/telegram_reauth.tsx new file mode 100644 index 000000000..03dffbe33 --- /dev/null +++ b/embed/oko_attached/src/components/export/telegram_reauth.tsx @@ -0,0 +1,101 @@ +import { useEffect, useMemo, useState } from "react"; + +import type { OAuthState } from "@oko-wallet/oko-sdk-core"; +import { RedirectUriSearchParamsKey } from "@oko-wallet/oko-sdk-core"; + +import { TELEGRAM_BOT_NAME } from "@oko-wallet-attached/config/telegram"; + +import { + findEmbeddedIframe, + sendReauthParamsToIframe, +} from "./use_export_reauth"; + +const LOG_PREFIX = "[attached][telegram_reauth]"; + +export function TelegramReauth() { + const [errorMessage, setErrorMessage] = useState(null); + + // Build OAuthState for the callback to parse + const oauthState = useMemo( + () => ({ + apiKey: "", + targetOrigin: window.location.origin, + provider: "telegram", + }), + [], + ); + + const oauthStateString = useMemo( + () => JSON.stringify(oauthState), + [oauthState], + ); + + useEffect(() => { + // Send params to iframe (no nonce/PKCE needed for Telegram, but notify iframe) + const iframe = findEmbeddedIframe(); + if (!iframe) { + setErrorMessage( + "Cannot find embedded iframe. Make sure this page was opened from the dashboard.", + ); + return; + } + sendReauthParamsToIframe(iframe, {}); + + const cleanBotName = TELEGRAM_BOT_NAME.replace(/^@+/, "").trim(); + + const callbackUrl = new URL(`${window.location.origin}/telegram/callback`); + callbackUrl.searchParams.set( + RedirectUriSearchParamsKey.STATE, + oauthStateString, + ); + + console.log(`${LOG_PREFIX} inserting telegram widget`, { + botName: cleanBotName, + callbackUrl: callbackUrl.toString(), + }); + + const script = document.createElement("script"); + script.src = "https://telegram.org/js/telegram-widget.js?22"; + script.setAttribute("data-telegram-login", cleanBotName); + script.setAttribute("data-size", "medium"); + script.setAttribute("data-userpic", "false"); + script.setAttribute("data-auth-url", callbackUrl.toString()); + script.setAttribute("data-request-access", "write"); + script.async = true; + + const container = document.getElementById("telegram-reauth-container"); + if (container) { + container.appendChild(script); + } + + return () => { + if (container?.contains(script)) { + container.removeChild(script); + } + }; + }, [oauthStateString]); + + if (errorMessage) { + return ( +
    +
    {errorMessage}
    +
    + ); + } + + return ( +
    +

    Telegram Re-Authentication

    +

    Continue with Telegram to verify your identity.

    +
    +
    + ); +} diff --git a/embed/oko_attached/src/components/export/use_export_reauth.ts b/embed/oko_attached/src/components/export/use_export_reauth.ts index c04d40b8e..d7633d604 100644 --- a/embed/oko_attached/src/components/export/use_export_reauth.ts +++ b/embed/oko_attached/src/components/export/use_export_reauth.ts @@ -10,7 +10,7 @@ const GOOGLE_CLIENT_ID = const X_CLIENT_ID = "eWJPdVNYNlV6dEpNSTM3T01GRGI6MTpjaQ"; const DISCORD_CLIENT_ID = "1445280712121913384"; -function generateNonce(length = 8) { +export function generateNonce(length = 8) { return Array.from(crypto.getRandomValues(new Uint8Array(length))) .map((b) => b.toString(16).padStart(2, "0")) .join(""); @@ -71,7 +71,7 @@ async function createPkcePair(): Promise<{ return { codeVerifier, codeChallenge }; } -function findEmbeddedIframe(): Window | null { +export function findEmbeddedIframe(): Window | null { if (!window.opener) { return null; } @@ -92,7 +92,7 @@ function findEmbeddedIframe(): Window | null { return null; } -function sendReauthParamsToIframe( +export function sendReauthParamsToIframe( iframe: Window, params: { nonce?: string; code_verifier?: string }, ): void { @@ -173,8 +173,6 @@ function buildDiscordOAuthUrl(codeChallenge: string): string { return authUrl.toString(); } -// --- Hook --- - export function useExportReauth() { const startReauth = useCallback( async ( From 0f21b7d0fbb86460886303a30259d87407866d0d Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 14:49:27 +0900 Subject: [PATCH 33/62] oko_attached: implement key combine --- .../src/window_msgs/export_private_key.ts | 455 ++++++++++++++---- .../src/window_msgs/export_reauth_state.ts | 47 ++ 2 files changed, 395 insertions(+), 107 deletions(-) create mode 100644 embed/oko_attached/src/window_msgs/export_reauth_state.ts diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index 05ddbc419..f15452060 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -1,22 +1,48 @@ import bs58 from "bs58"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { + ExportSharesRequest, + ExportSharesResponse, +} from "@oko-wallet/oko-types/user"; +import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; + import type { MsgEventContext } from "./types"; import { OKO_SDK_TARGET } from "./target"; import { useAppState } from "@oko-wallet-attached/store/app"; +import { USER_DASHBOARD_ORIGINS } from "@oko-wallet-attached/requests/endpoints"; +import { + signInV2, + TSS_V2_ENDPOINT, +} from "@oko-wallet-attached/requests/oko_api"; +import { requestKeySharesWithBackup } from "@oko-wallet-attached/requests/ks_node_v2"; import { - OKO_API_ENDPOINT, - USER_DASHBOARD_ORIGINS, -} from "@oko-wallet-attached/requests/endpoints"; + commitAll, + createOkoApiCommitRevealParams, + type KsnCommitTarget, +} from "@oko-wallet-attached/crypto/commit_reveal"; +import { decodeSecp256k1SharesByNode } from "@oko-wallet-attached/crypto/key_share_utils"; +import { combineUserShares } from "@oko-wallet-attached/crypto/combine"; +import { convertSeedShares } from "@oko-wallet-attached/crypto/reshare_v2"; +import { + SEED_ID_CLIENT, + hexToSeedSharePoint, + hexToUint8Array, +} from "@oko-wallet-attached/crypto/keygen_ed25519"; -// NOTE: This handler is user_dashboard-only (not exposed via SDK). -// Types are defined locally, following the __get_connected_apps__ pattern. +import { setReAuthResolver } from "./export_reauth_state"; +import { checkUserExistsV2 } from "./oauth_info_pass/handlers/check_user"; type ExportPrivateKeyError = | { type: "UNAUTHORIZED_ORIGIN" } | { type: "NOT_AUTHENTICATED" } - | { type: "MISSING_KEYSHARE" } + | { type: "USER_NOT_FOUND" } + | { type: "ED25519_KEYGEN_REQUIRED" } + | { type: "USER_MISMATCH" } | { type: "COMBINE_ERROR"; error: string } - | { type: "API_ERROR"; error: string }; + | { type: "API_ERROR"; error: string } + | { type: "REAUTH_TIMEOUT" } + | { type: "REAUTH_ERROR"; error: string }; type ExportPrivateKeyAckPayload = | { success: true; data: { secp256k1: string; ed25519: string } } @@ -28,131 +54,346 @@ interface OkoWalletMsgExportPrivateKeyAck { payload: ExportPrivateKeyAckPayload; } +const REAUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes +const LOG_PREFIX = "[attached][export]"; + export async function handleExportPrivateKey( ctx: MsgEventContext, + payload?: { jwt: string; auth_type: AuthType } | null, ): Promise { const { port, hostOrigin } = ctx; - // 1. Origin validation - const allowedOrigins = USER_DASHBOARD_ORIGINS.split(",").map((o: string) => - o.trim(), - ); - if (!allowedOrigins.includes(hostOrigin)) { + function sendAck(ackPayload: ExportPrivateKeyAckPayload) { const ack: OkoWalletMsgExportPrivateKeyAck = { target: OKO_SDK_TARGET, msg_type: "__export_private_key_ack__", - payload: { success: false, error: { type: "UNAUTHORIZED_ORIGIN" } }, + payload: ackPayload, }; port.postMessage(ack); + } + + // 1. Origin validation + const allowedOrigins = USER_DASHBOARD_ORIGINS.split(",").map((o: string) => + o.trim(), + ); + if (!allowedOrigins.includes(hostOrigin)) { + sendAck({ success: false, error: { type: "UNAUTHORIZED_ORIGIN" } }); return; } // 2. Auth token validation const authToken = useAppState.getState().getAuthToken(hostOrigin); if (!authToken) { - const ack: OkoWalletMsgExportPrivateKeyAck = { - target: OKO_SDK_TARGET, - msg_type: "__export_private_key_ack__", - payload: { success: false, error: { type: "NOT_AUTHENTICATED" } }, - }; - port.postMessage(ack); + sendAck({ success: false, error: { type: "NOT_AUTHENTICATED" } }); return; } - // 3. Extract user shares from local state - const keyshare1 = useAppState.getState().getKeyshare_1(hostOrigin); - const keyPackageEd25519Hex = useAppState - .getState() - .getKeyPackageEd25519(hostOrigin); + // 3. Payload validation (Phase 9 wires up the actual payload) + if (!payload?.jwt || !payload?.auth_type) { + sendAck({ + success: false, + error: { type: "API_ERROR", error: "Missing payload (jwt, auth_type)" }, + }); + return; + } + const { jwt } = payload; - if (!keyshare1 || !keyPackageEd25519Hex) { - const ack: OkoWalletMsgExportPrivateKeyAck = { - target: OKO_SDK_TARGET, - msg_type: "__export_private_key_ack__", - payload: { success: false, error: { type: "MISSING_KEYSHARE" } }, - }; - port.postMessage(ack); + // 4. Capture first login publicKey from appState + const wallet = useAppState.getState().getWallet(hostOrigin); + const firstLoginPublicKey = wallet?.publicKey ?? null; + + // 5. Wait for re-auth credentials (popup → OAuth callback → interceptor) + let creds; + try { + const reAuthPromise = setReAuthResolver(); + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("REAUTH_TIMEOUT")), REAUTH_TIMEOUT_MS); + }); + creds = await Promise.race([reAuthPromise, timeoutPromise]); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (message === "REAUTH_TIMEOUT") { + sendAck({ success: false, error: { type: "REAUTH_TIMEOUT" } }); + } else { + sendAck({ + success: false, + error: { type: "REAUTH_ERROR", error: message }, + }); + } return; } - // 4. Get ed25519 public key for the response - const ed25519Wallet = useAppState.getState().getWalletEd25519(hostOrigin); - const ed25519PublicKey = ed25519Wallet?.publicKey ?? null; - - // ------------------------------------------------------------------- - // TODO: Replace mock with actual implementation when oko_api is ready. - // - // Actual flow: - // - // (a) Fetch server shares from oko_api - // - // const serverSharesRes = await fetch( - // `${OKO_API_ENDPOINT}/user_dashboard/v1/export_private_key`, - // { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // Authorization: `Bearer ${authToken}`, - // }, - // }, - // ); - // const serverShares = await serverSharesRes.json(); - // // serverShares = { secp256k1: string (keyshare_0 hex), ed25519: string (server signing_share hex) } - // - // (b) Combine secp256k1 shares - // - // import * as wasmModule from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; - // - // const keyCombineInput = { - // shares: { - // 0: serverShares.secp256k1, // server's share (Participant 0) - // 1: keyshare1, // user's share (Participant 1) - // }, - // }; - // const fullSecp256k1Key = wasmModule.cli_combine_shares(keyCombineInput); - // // fullSecp256k1Key is a secp256k1 Scalar → convert to hex - // - // (c) Combine ed25519 shares - // - // Parse keyPackageEd25519Hex to get user's signing_share. - // const keyPackageRaw = JSON.parse( - // Buffer.from(keyPackageEd25519Hex, "hex").toString("utf-8"), - // ); - // const userSigningShare = keyPackageRaw.signing_share; // number[] - // - // Combine user's signing_share + server's signing_share using FROST sss_combine - // to recover the full ed25519 signing secret. - // - // const fullEd25519Secret = ... // 32 bytes - // const fullEd25519Keypair = Uint8Array.from([...fullEd25519Secret, ...ed25519PublicKeyBytes]) // 64 bytes - // const fullEd25519Key = bs58.encode(fullEd25519Keypair) // base58 (Phantom/Solflare import format) - // - // ------------------------------------------------------------------- - - // [Mock] Return deterministic test keys for development - const mockSecp256k1 = - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; - const mockEd25519SigningSecret = "a".repeat(64); // 32 bytes hex - const mockEd25519PublicKey = ed25519PublicKey ?? "b".repeat(64); // 32 bytes hex - const mockEd25519Hex = mockEd25519SigningSecret + mockEd25519PublicKey; - - // Convert 64-byte ed25519 keypair (secret + pubkey) from hex to base58 - // This is the standard format used by Phantom, Solflare, etc. - const ed25519Bytes = new Uint8Array( - (mockEd25519Hex.match(/.{2}/g) ?? []).map((b) => Number.parseInt(b, 16)), - ); - const mockEd25519Base58 = bs58.encode(ed25519Bytes); + console.log(`${LOG_PREFIX} re-auth credentials received, starting sign-in`); - const ack: OkoWalletMsgExportPrivateKeyAck = { - target: OKO_SDK_TARGET, - msg_type: "__export_private_key_ack__", - payload: { + try { + // 6. Check user exists + const checkRes = await checkUserExistsV2( + creds.userIdentifier, + creds.authType, + ); + if (!checkRes.success) { + sendAck({ + success: false, + error: { type: "API_ERROR", error: "Failed to check user" }, + }); + return; + } + if (!checkRes.data.success) { + sendAck({ + success: false, + error: { type: "API_ERROR", error: checkRes.data.msg }, + }); + return; + } + + const checkData = checkRes.data.data; + + // Guard: user not found + if (!checkData.exists) { + sendAck({ success: false, error: { type: "USER_NOT_FOUND" } }); + return; + } + + // Guard: needs ed25519 keygen (can't export both keys) + if ("needs_keygen_ed25519" in checkData && checkData.needs_keygen_ed25519) { + sendAck({ success: false, error: { type: "ED25519_KEYGEN_REQUIRED" } }); + return; + } + + const { keyshare_node_meta } = checkData; + const { threshold, nodes } = keyshare_node_meta; + + // 7. Sign-in building blocks: commitAll → signInV2 + const ksnCommitTargets: KsnCommitTarget[] = nodes.map((node) => ({ + nodeUrl: node.endpoint, + operationType: "sign_in" as const, + })); + const commitRes = await commitAll( + "sign_in", + creds.authType, + creds.idToken, + ksnCommitTargets, + threshold, + ); + if (!commitRes.success) { + sendAck({ + success: false, + error: { type: "API_ERROR", error: `commit failed: ${commitRes.err}` }, + }); + return; + } + const { session, readyNodes, pendingCommits } = commitRes.data; + + const signInCommitRevealRes = createOkoApiCommitRevealParams( + session, + "signin", + ); + if (!signInCommitRevealRes.success) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `commit-reveal params failed: ${signInCommitRevealRes.err}`, + }, + }); + return; + } + + const signInResult = await signInV2( + creds.idToken, + creds.authType, + signInCommitRevealRes.data, + ); + if (!signInResult.success) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `signIn failed: ${signInResult.err.error}`, + }, + }); + return; + } + const signInResp = signInResult.data; + + // 8. User mismatch check (re-auth must match first login) + if ( + firstLoginPublicKey && + signInResp.user.public_key_secp256k1 !== firstLoginPublicKey + ) { + sendAck({ success: false, error: { type: "USER_MISMATCH" } }); + return; + } + + // 9. Request key shares from KSN + const requestSharesRes = await requestKeySharesWithBackup({ + idToken: creds.idToken, + authType: creds.authType, + wallets: { + secp256k1: signInResp.user.public_key_secp256k1, + ed25519: signInResp.user.public_key_ed25519, + }, + threshold, + session, + readyNodes, + pendingCommits, + allNodes: nodes, + }); + if (!requestSharesRes.success) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `insufficient shares: got ${requestSharesRes.err.got}/${requestSharesRes.err.need}`, + }, + }); + return; + } + const { shares: keySharesByNode } = requestSharesRes.data; + + // 10. Export API: get server shares (dual-auth: JWT + re-auth id_token) + const exportApiUrl = `${TSS_V2_ENDPOINT}/export_shares`; + const exportBody: ExportSharesRequest = { + auth_type: creds.authType, + id_token: creds.idToken, + }; + const exportRes = await fetch(exportApiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(exportBody), + }); + if (!exportRes.ok) { + const errText = await exportRes.text().catch(() => ""); + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `export API ${exportRes.status}: ${errText}`, + }, + }); + return; + } + const serverShares: ExportSharesResponse = await exportRes.json(); + + // --------------------------------------------------------------- + // 11. secp256k1 combine + // --------------------------------------------------------------- + + // 11a. Decode KSN secp256k1 shares → Point256 format + const secp256k1DecodeRes = + await decodeSecp256k1SharesByNode(keySharesByNode); + if (!secp256k1DecodeRes.success) { + sendAck({ + success: false, + error: { + type: "COMBINE_ERROR", + error: `secp256k1 decode: ${secp256k1DecodeRes.err.error}`, + }, + }); + return; + } + + // 11b. Combine KSN shares → user's keyshare_1 (Lagrange interpolation) + const userKeyshare1Res = await combineUserShares( + secp256k1DecodeRes.data, + threshold, + ); + if (!userKeyshare1Res.success) { + sendAck({ + success: false, + error: { + type: "COMBINE_ERROR", + error: `secp256k1 user combine: ${userKeyshare1Res.err}`, + }, + }); + return; + } + + // 11c. Combine server share (Participant 0) + user share (Participant 1) → full private key + const fullSecp256k1Scalar = secp256k1Wasm.cli_combine_shares({ + shares: { + "0": serverShares.secp256k1_share, + "1": userKeyshare1Res.data, + }, + }); + const secp256k1PrivateKey = `0x${fullSecp256k1Scalar}`; + + // --------------------------------------------------------------- + // 12. ed25519 seed 2-stage combine + // --------------------------------------------------------------- + + // 12a. Convert KSN seed shares to UserKeySharePointByNode format + const ksnSeedShares = convertSeedShares(keySharesByNode); + + // 12b. Convert to PointNumArr for WASM + const ksnSeedPoints = ksnSeedShares.map((s) => ({ + x: [...s.share.x.toUint8Array()], + y: [...s.share.y.toUint8Array()], + })); + + // 12c. Stage 1: Combine KSN seed shares → user_seed_Y + const userSeedY: number[] = secp256k1Wasm.seed_sss_combine( + ksnSeedPoints, + threshold, + ); + + // 12d. Parse server's seed share + const serverSeedShareRes = hexToSeedSharePoint( + serverShares.ed25519_seed_share, + ); + if (!serverSeedShareRes.success) { + sendAck({ + success: false, + error: { + type: "COMBINE_ERROR", + error: `server seed share parse: ${serverSeedShareRes.err}`, + }, + }); + return; + } + + // 12e. Stage 2: Combine server share + reconstructed user share → original seed + const serverSeedPoint = { + x: [...serverSeedShareRes.data.x.toUint8Array()], + y: [...serverSeedShareRes.data.y.toUint8Array()], + }; + const userSeedPoint = { + x: SEED_ID_CLIENT, + y: userSeedY, + }; + const recoveredSeed: number[] = secp256k1Wasm.seed_sss_combine( + [serverSeedPoint, userSeedPoint], + 2, + ); + + // 12f. Build ed25519 keypair: seed[32] || pubkey[32] → bs58 + const seedBytes = Uint8Array.from(recoveredSeed); + const pubkeyBytes = hexToUint8Array(signInResp.user.public_key_ed25519); + const keypairBytes = new Uint8Array(seedBytes.length + pubkeyBytes.length); + keypairBytes.set(seedBytes, 0); + keypairBytes.set(pubkeyBytes, seedBytes.length); + const ed25519Keypair = bs58.encode(keypairBytes); + + // 13. Return result + console.log(`${LOG_PREFIX} export complete`); + sendAck({ success: true, data: { - secp256k1: mockSecp256k1, - ed25519: mockEd25519Base58, + secp256k1: secp256k1PrivateKey, + ed25519: ed25519Keypair, + }, + }); + } catch (err) { + console.error(`${LOG_PREFIX} unexpected error`, err); + sendAck({ + success: false, + error: { + type: "COMBINE_ERROR", + error: err instanceof Error ? err.message : String(err), }, - }, - }; - port.postMessage(ack); + }); + } } diff --git a/embed/oko_attached/src/window_msgs/export_reauth_state.ts b/embed/oko_attached/src/window_msgs/export_reauth_state.ts new file mode 100644 index 000000000..b6a4ddd5d --- /dev/null +++ b/embed/oko_attached/src/window_msgs/export_reauth_state.ts @@ -0,0 +1,47 @@ +import type { AuthType } from "@oko-wallet/oko-types/auth"; + +export interface ReAuthCredentials { + idToken: string; + userIdentifier: string; + authType: AuthType; +} + +let reAuthResolver: ((creds: ReAuthCredentials) => void) | null = null; +let reAuthRejecter: ((err: Error) => void) | null = null; + +export function setReAuthResolver(): Promise { + if (reAuthRejecter) { + reAuthRejecter(new Error("Superseded by new export request")); + } + reAuthResolver = null; + reAuthRejecter = null; + + return new Promise((resolve, reject) => { + reAuthResolver = resolve; + reAuthRejecter = reject; + }); +} + +export function consumeReAuthResolver(creds: ReAuthCredentials): boolean { + if (!reAuthResolver) { + return false; + } + const resolver = reAuthResolver; + reAuthResolver = null; + reAuthRejecter = null; + resolver(creds); + return true; +} + +export function rejectReAuthResolver(error: string): void { + if (reAuthRejecter) { + const rejecter = reAuthRejecter; + reAuthResolver = null; + reAuthRejecter = null; + rejecter(new Error(error)); + } +} + +export function hasActiveReAuthResolver(): boolean { + return reAuthResolver !== null; +} From 6fbf935738cfd948c3fce92994045104dbc38251 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 15:29:13 +0900 Subject: [PATCH 34/62] oko_attached: add message wiring and re-auth interceptor --- embed/oko_attached/src/window_msgs/index.ts | 26 ++++++++++++++++-- .../src/window_msgs/oauth_info_pass/index.ts | 27 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/embed/oko_attached/src/window_msgs/index.ts b/embed/oko_attached/src/window_msgs/index.ts index 70b87b927..da3d8ae9f 100644 --- a/embed/oko_attached/src/window_msgs/index.ts +++ b/embed/oko_attached/src/window_msgs/index.ts @@ -1,6 +1,8 @@ import type { OkoWalletMsg } from "@oko-wallet/oko-sdk-core"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; import type { MsgEventContext } from "./types"; +import { useAppState } from "@oko-wallet-attached/store/app"; import { handleGetPublicKey } from "./get_public_key"; import { handleGetPublicKeyEd25519 } from "./get_public_key_ed25519"; import { handleSetOAuthNonce } from "./set_oauth_nonce"; @@ -28,7 +30,7 @@ type OkoWalletMsgGetConnectedApps = { type OkoWalletMsgExportPrivateKey = { target: "oko_attached"; msg_type: "__export_private_key__"; - payload: null; + payload: { jwt: string; auth_type: AuthType }; }; type ExtendedOkoWalletMsg = @@ -38,6 +40,26 @@ type ExtendedOkoWalletMsg = export function makeMsgHandler() { return async function msgHandler(event: MessageEvent) { + // Handle port-less messages (popup → iframe, fire-and-forget) + const data = event.data; + if ( + data?.target === "oko_attached" && + data?.msg_type === "set_reauth_params" + ) { + const appState = useAppState.getState(); + const payload = data.payload as + | { nonce?: string; code_verifier?: string } + | undefined; + if (payload?.nonce) { + appState.setNonce(event.origin, payload.nonce); + } + if (payload?.code_verifier) { + appState.setCodeVerifier(event.origin, payload.code_verifier); + } + console.debug("[attached] set_reauth_params received", event.origin); + return; + } + if (event.ports.length < 1) { // do nothing @@ -137,7 +159,7 @@ export function makeMsgHandler() { } case "__export_private_key__": { - await handleExportPrivateKey(ctx); + await handleExportPrivateKey(ctx, message.payload); break; } diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts index 8f5f414f9..c18a42965 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts @@ -43,6 +43,11 @@ import { } from "./handlers/ed25519_keygen"; import { bail } from "./errors"; import { getCredentialsFromPayload } from "./validate_social_login"; +import { + hasActiveReAuthResolver, + consumeReAuthResolver, + rejectReAuthResolver, +} from "../export_reauth_state"; export async function handleOAuthInfoPass( ctx: MsgEventContext, @@ -258,6 +263,28 @@ export async function handleOAuthInfoPassV2( return; } + // Re-auth interceptor: if an export request is waiting for re-auth credentials, + // extract OAuth credentials and resolve the pending promise. + // Skips api_key/hostOriginList checks (re-auth uses attached origin with no SDK API key). + if (hasActiveReAuthResolver()) { + const authType: AuthType = message.payload.auth_type; + const validateOauthRes = await getCredentialsFromPayload( + message.payload, + hostOrigin, + ); + + if (!validateOauthRes.success) { + rejectReAuthResolver(validateOauthRes.err.type); + } else { + consumeReAuthResolver({ + idToken: validateOauthRes.data.idToken, + userIdentifier: validateOauthRes.data.userIdentifier, + authType, + }); + } + return; // finally block handles ack + nonce cleanup + } + if (!appState.getHostOriginList().includes(hostOrigin)) { await bail(message, { type: "origin_not_registered" }); return; From 6c775bb2c0d49e11b6a54ecce338c737ad582231 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 16:06:26 +0900 Subject: [PATCH 35/62] o --- .../src/app/export_private_key/page.tsx | 69 ++++++++++++++----- .../src/components/export/email_reauth.tsx | 2 +- .../src/components/export/telegram_reauth.tsx | 2 +- .../components/export/use_export_reauth.ts | 6 +- embed/oko_attached/src/routeTree.gen.ts | 42 +++++------ .../src/window_msgs/export_private_key.ts | 15 ++-- embed/oko_attached/src/window_msgs/index.ts | 2 +- 7 files changed, 87 insertions(+), 51 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index fb39ee660..03126695b 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -347,6 +347,21 @@ function Step2Content({ ); } +function getExportErrorDescription(errorType: string): string { + switch (errorType) { + case "REAUTH_TIMEOUT": + return "Re-authentication timed out. Please try again."; + case "USER_MISMATCH": + return "Account mismatch. Please log in with the same account."; + case "USER_NOT_FOUND": + return "User not found."; + case "ED25519_KEYGEN_REQUIRED": + return "Ed25519 key generation required. Please try signing in first."; + default: + return "Please try again."; + } +} + export default function Page() { const email = useUserInfoState((state) => state.email); const name = useUserInfoState((state) => state.name); @@ -375,28 +390,41 @@ export default function Page() { return; } + let popup: Window | null = null; try { setIsLoading(true); - const publicKeyBefore = await okoWallet.getPublicKey(); - await okoWallet.signIn(authType === "auth0" ? "email" : authType); - const publicKeyAfter = await okoWallet.getPublicKey(); + // 1. Open re-auth popup at attached origin + const attachedOrigin = new URL(okoWallet.sdkEndpoint).origin; + popup = window.open( + `${attachedOrigin}/export/reauth?auth_type=${authType}`, + "oko_re_auth", + "width=600,height=700", + ); - if (publicKeyBefore !== publicKeyAfter) { - displayToast({ - variant: "confirm", - title: "Login Failed", - description: "Please try again.", - }); - return; - } - - const res = await okoWallet.sendMsgToIframe({ + // 2. Send export request to attached iframe + const resPromise = okoWallet.sendMsgToIframe({ target: "oko_attached", msg_type: "__export_private_key__", - payload: null, + payload: { auth_type: authType }, } as any); + // 3. Monitor popup close (user abandoned re-auth) + const popupClosePromise = new Promise((_, reject) => { + const timer = window.setInterval(() => { + if (!popup || popup.closed) { + window.clearInterval(timer); + reject(new Error("POPUP_CLOSED")); + } + }, 1000); + void resPromise.finally(() => window.clearInterval(timer)); + }); + + // 4. Wait for iframe result or popup close + const res = await Promise.race([resPromise, popupClosePromise]); + popup?.close(); + + // 5. Parse result const resAny = res as unknown as { msg_type: "__export_private_key_ack__"; payload: @@ -417,17 +445,24 @@ export default function Page() { setPrivateKeys(resAny.payload.data); setStep(2); } else { + const errorType = !resAny.payload.success + ? resAny.payload.error.type + : "unknown"; displayToast({ variant: "confirm", title: "Export Failed", - description: "Please try again.", + description: getExportErrorDescription(errorType), }); } } catch (error) { - console.error("Re-authentication failed:", error); + popup?.close(); + if (error instanceof Error && error.message === "POPUP_CLOSED") { + return; + } + console.error("Export failed:", error); displayToast({ variant: "confirm", - title: "Login Failed", + title: "Export Failed", description: "Please try again.", }); } finally { diff --git a/embed/oko_attached/src/components/export/email_reauth.tsx b/embed/oko_attached/src/components/export/email_reauth.tsx index 7be853cdf..f66626945 100644 --- a/embed/oko_attached/src/components/export/email_reauth.tsx +++ b/embed/oko_attached/src/components/export/email_reauth.tsx @@ -49,7 +49,7 @@ export function EmailReauth() { const nonce = useMemo(() => generateNonce(), []); const oauthState = useMemo( () => ({ - apiKey: "", + apiKey: "reauth", targetOrigin: window.location.origin, provider: "auth0", }), diff --git a/embed/oko_attached/src/components/export/telegram_reauth.tsx b/embed/oko_attached/src/components/export/telegram_reauth.tsx index 03dffbe33..3dd75010f 100644 --- a/embed/oko_attached/src/components/export/telegram_reauth.tsx +++ b/embed/oko_attached/src/components/export/telegram_reauth.tsx @@ -18,7 +18,7 @@ export function TelegramReauth() { // Build OAuthState for the callback to parse const oauthState = useMemo( () => ({ - apiKey: "", + apiKey: "reauth", targetOrigin: window.location.origin, provider: "telegram", }), diff --git a/embed/oko_attached/src/components/export/use_export_reauth.ts b/embed/oko_attached/src/components/export/use_export_reauth.ts index d7633d604..f8c85d6fe 100644 --- a/embed/oko_attached/src/components/export/use_export_reauth.ts +++ b/embed/oko_attached/src/components/export/use_export_reauth.ts @@ -112,7 +112,7 @@ function buildGoogleOAuthUrl(nonce: string): string { const redirectUri = `${window.location.origin}/google/callback`; const oauthState: OAuthState = { - apiKey: "", + apiKey: "reauth", targetOrigin: window.location.origin, provider: "google", }; @@ -133,7 +133,7 @@ function buildXOAuthUrl(codeChallenge: string): string { const redirectUri = `${window.location.origin}/x/callback`; const oauthState: OAuthState = { - apiKey: "", + apiKey: "reauth", targetOrigin: window.location.origin, provider: "x", }; @@ -155,7 +155,7 @@ function buildDiscordOAuthUrl(codeChallenge: string): string { const redirectUri = `${window.location.origin}/discord/callback`; const oauthState: OAuthState = { - apiKey: "", + apiKey: "reauth", targetOrigin: window.location.origin, provider: "discord", }; diff --git a/embed/oko_attached/src/routeTree.gen.ts b/embed/oko_attached/src/routeTree.gen.ts index 8ce6113cc..1994b25bf 100644 --- a/embed/oko_attached/src/routeTree.gen.ts +++ b/embed/oko_attached/src/routeTree.gen.ts @@ -15,9 +15,9 @@ import { Route as EmailIndexRouteImport } from './routes/email/index' import { Route as XCallbackIndexRouteImport } from './routes/x/callback/index' import { Route as TelegramCallbackIndexRouteImport } from './routes/telegram/callback/index' import { Route as GoogleCallbackIndexRouteImport } from './routes/google/callback/index' +import { Route as ExportReauthIndexRouteImport } from './routes/export/reauth/index' import { Route as EmailCallbackIndexRouteImport } from './routes/email/callback/index' import { Route as DiscordCallbackIndexRouteImport } from './routes/discord/callback/index' -import { Route as ExportReauthIndexRouteImport } from './routes/export/reauth/index' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -49,6 +49,11 @@ const GoogleCallbackIndexRoute = GoogleCallbackIndexRouteImport.update({ path: '/google/callback/', getParentRoute: () => rootRouteImport, } as any) +const ExportReauthIndexRoute = ExportReauthIndexRouteImport.update({ + id: '/export/reauth/', + path: '/export/reauth/', + getParentRoute: () => rootRouteImport, +} as any) const EmailCallbackIndexRoute = EmailCallbackIndexRouteImport.update({ id: '/email/callback/', path: '/email/callback/', @@ -59,19 +64,14 @@ const DiscordCallbackIndexRoute = DiscordCallbackIndexRouteImport.update({ path: '/discord/callback/', getParentRoute: () => rootRouteImport, } as any) -const ExportReauthIndexRoute = ExportReauthIndexRouteImport.update({ - id: '/export/reauth/', - path: '/export/reauth/', - getParentRoute: () => rootRouteImport, -} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/email/': typeof EmailIndexRoute - '/export/reauth/': typeof ExportReauthIndexRoute '/telegram/': typeof TelegramIndexRoute '/discord/callback/': typeof DiscordCallbackIndexRoute '/email/callback/': typeof EmailCallbackIndexRoute + '/export/reauth/': typeof ExportReauthIndexRoute '/google/callback/': typeof GoogleCallbackIndexRoute '/telegram/callback/': typeof TelegramCallbackIndexRoute '/x/callback/': typeof XCallbackIndexRoute @@ -79,10 +79,10 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/email': typeof EmailIndexRoute - '/export/reauth': typeof ExportReauthIndexRoute '/telegram': typeof TelegramIndexRoute '/discord/callback': typeof DiscordCallbackIndexRoute '/email/callback': typeof EmailCallbackIndexRoute + '/export/reauth': typeof ExportReauthIndexRoute '/google/callback': typeof GoogleCallbackIndexRoute '/telegram/callback': typeof TelegramCallbackIndexRoute '/x/callback': typeof XCallbackIndexRoute @@ -91,10 +91,10 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/email/': typeof EmailIndexRoute - '/export/reauth/': typeof ExportReauthIndexRoute '/telegram/': typeof TelegramIndexRoute '/discord/callback/': typeof DiscordCallbackIndexRoute '/email/callback/': typeof EmailCallbackIndexRoute + '/export/reauth/': typeof ExportReauthIndexRoute '/google/callback/': typeof GoogleCallbackIndexRoute '/telegram/callback/': typeof TelegramCallbackIndexRoute '/x/callback/': typeof XCallbackIndexRoute @@ -104,10 +104,10 @@ export interface FileRouteTypes { fullPaths: | '/' | '/email/' - | '/export/reauth/' | '/telegram/' | '/discord/callback/' | '/email/callback/' + | '/export/reauth/' | '/google/callback/' | '/telegram/callback/' | '/x/callback/' @@ -115,10 +115,10 @@ export interface FileRouteTypes { to: | '/' | '/email' - | '/export/reauth' | '/telegram' | '/discord/callback' | '/email/callback' + | '/export/reauth' | '/google/callback' | '/telegram/callback' | '/x/callback' @@ -126,10 +126,10 @@ export interface FileRouteTypes { | '__root__' | '/' | '/email/' - | '/export/reauth/' | '/telegram/' | '/discord/callback/' | '/email/callback/' + | '/export/reauth/' | '/google/callback/' | '/telegram/callback/' | '/x/callback/' @@ -138,10 +138,10 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute EmailIndexRoute: typeof EmailIndexRoute - ExportReauthIndexRoute: typeof ExportReauthIndexRoute TelegramIndexRoute: typeof TelegramIndexRoute DiscordCallbackIndexRoute: typeof DiscordCallbackIndexRoute EmailCallbackIndexRoute: typeof EmailCallbackIndexRoute + ExportReauthIndexRoute: typeof ExportReauthIndexRoute GoogleCallbackIndexRoute: typeof GoogleCallbackIndexRoute TelegramCallbackIndexRoute: typeof TelegramCallbackIndexRoute XCallbackIndexRoute: typeof XCallbackIndexRoute @@ -163,13 +163,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof TelegramIndexRouteImport parentRoute: typeof rootRouteImport } - '/export/reauth/': { - id: '/export/reauth/' - path: '/export/reauth' - fullPath: '/export/reauth/' - preLoaderRoute: typeof ExportReauthIndexRouteImport - parentRoute: typeof rootRouteImport - } '/email/': { id: '/email/' path: '/email' @@ -198,6 +191,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof GoogleCallbackIndexRouteImport parentRoute: typeof rootRouteImport } + '/export/reauth/': { + id: '/export/reauth/' + path: '/export/reauth' + fullPath: '/export/reauth/' + preLoaderRoute: typeof ExportReauthIndexRouteImport + parentRoute: typeof rootRouteImport + } '/email/callback/': { id: '/email/callback/' path: '/email/callback' @@ -218,10 +218,10 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, EmailIndexRoute: EmailIndexRoute, - ExportReauthIndexRoute: ExportReauthIndexRoute, TelegramIndexRoute: TelegramIndexRoute, DiscordCallbackIndexRoute: DiscordCallbackIndexRoute, EmailCallbackIndexRoute: EmailCallbackIndexRoute, + ExportReauthIndexRoute: ExportReauthIndexRoute, GoogleCallbackIndexRoute: GoogleCallbackIndexRoute, TelegramCallbackIndexRoute: TelegramCallbackIndexRoute, XCallbackIndexRoute: XCallbackIndexRoute, diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index f15452060..5786ab04e 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -59,7 +59,7 @@ const LOG_PREFIX = "[attached][export]"; export async function handleExportPrivateKey( ctx: MsgEventContext, - payload?: { jwt: string; auth_type: AuthType } | null, + payload?: { auth_type: AuthType } | null, ): Promise { const { port, hostOrigin } = ctx; @@ -88,19 +88,19 @@ export async function handleExportPrivateKey( return; } - // 3. Payload validation (Phase 9 wires up the actual payload) - if (!payload?.jwt || !payload?.auth_type) { + // 3. Payload validation + if (!payload?.auth_type) { sendAck({ success: false, - error: { type: "API_ERROR", error: "Missing payload (jwt, auth_type)" }, + error: { type: "API_ERROR", error: "Missing payload (auth_type)" }, }); return; } - const { jwt } = payload; - // 4. Capture first login publicKey from appState + // 4. Capture first login context from appState const wallet = useAppState.getState().getWallet(hostOrigin); const firstLoginPublicKey = wallet?.publicKey ?? null; + const apiKey = useAppState.getState().getApiKey(hostOrigin) ?? undefined; // 5. Wait for re-auth credentials (popup → OAuth callback → interceptor) let creds; @@ -203,6 +203,7 @@ export async function handleExportPrivateKey( creds.idToken, creds.authType, signInCommitRevealRes.data, + apiKey, ); if (!signInResult.success) { sendAck({ @@ -261,7 +262,7 @@ export async function handleExportPrivateKey( method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${jwt}`, + Authorization: `Bearer ${authToken}`, }, body: JSON.stringify(exportBody), }); diff --git a/embed/oko_attached/src/window_msgs/index.ts b/embed/oko_attached/src/window_msgs/index.ts index da3d8ae9f..c8c872a60 100644 --- a/embed/oko_attached/src/window_msgs/index.ts +++ b/embed/oko_attached/src/window_msgs/index.ts @@ -30,7 +30,7 @@ type OkoWalletMsgGetConnectedApps = { type OkoWalletMsgExportPrivateKey = { target: "oko_attached"; msg_type: "__export_private_key__"; - payload: { jwt: string; auth_type: AuthType }; + payload: { auth_type: AuthType }; }; type ExtendedOkoWalletMsg = From 8286c6b03de6ecfd25873df817baedfa73848d3e Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 16:42:34 +0900 Subject: [PATCH 36/62] o --- .../src/app/export_private_key/page.tsx | 43 ++++++++++++++++--- .../src/window_msgs/export_private_key.ts | 6 +++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 03126695b..dbb7cfbc5 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -409,19 +409,48 @@ export default function Page() { payload: { auth_type: authType }, } as any); - // 3. Monitor popup close (user abandoned re-auth) + // 3. Listen for re-auth completion signal from iframe + let reauthReceived = false; + const reauthHandler = (event: MessageEvent) => { + if (event.origin !== attachedOrigin) { + return; + } + if (event.data?.msg_type === "__export_reauth_received__") { + reauthReceived = true; + } + }; + window.addEventListener("message", reauthHandler); + + // 4. Monitor popup close — only reject if re-auth hasn't completed const popupClosePromise = new Promise((_, reject) => { const timer = window.setInterval(() => { if (!popup || popup.closed) { window.clearInterval(timer); - reject(new Error("POPUP_CLOSED")); + if (!reauthReceived) { + reject(new Error("POPUP_CLOSED")); + } + // re-auth completed → popup close is expected, don't reject } }, 1000); void resPromise.finally(() => window.clearInterval(timer)); }); - // 4. Wait for iframe result or popup close - const res = await Promise.race([resPromise, popupClosePromise]); + // 5. Backup timeout (in case export hangs after re-auth) + const timeoutPromise = new Promise((_, reject) => { + const timer = setTimeout( + () => reject(new Error("EXPORT_TIMEOUT")), + 3 * 60 * 1000, + ); + void resPromise.finally(() => clearTimeout(timer)); + }); + + // 6. Wait for iframe result, popup close, or timeout + const res = await Promise.race([ + resPromise, + popupClosePromise, + timeoutPromise, + ]); + window.removeEventListener("message", reauthHandler); popup?.close(); // 5. Parse result @@ -460,10 +489,14 @@ export default function Page() { return; } console.error("Export failed:", error); + const description = + error instanceof Error && error.message === "EXPORT_TIMEOUT" + ? "Export timed out. Please try again." + : "Please try again."; displayToast({ variant: "confirm", title: "Export Failed", - description: "Please try again.", + description, }); } finally { setIsLoading(false); diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index 5786ab04e..4a9123f8b 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -125,6 +125,12 @@ export async function handleExportPrivateKey( console.log(`${LOG_PREFIX} re-auth credentials received, starting sign-in`); + // 5a. Notify parent (UD) that re-auth completed — popup close is now expected + window.parent.postMessage( + { target: "oko_sdk", msg_type: "__export_reauth_received__" }, + hostOrigin, + ); + try { // 6. Check user exists const checkRes = await checkUserExistsV2( From d17b200a0c48678b2411257a6def2ecb91a3a89f Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 17:07:52 +0900 Subject: [PATCH 37/62] oko_attached: fix export API response parsing and participant ID mapping --- .../src/window_msgs/export_private_key.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index 4a9123f8b..0d7c523b8 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -5,6 +5,7 @@ import type { ExportSharesRequest, ExportSharesResponse, } from "@oko-wallet/oko-types/user"; +import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; import type { MsgEventContext } from "./types"; @@ -283,7 +284,19 @@ export async function handleExportPrivateKey( }); return; } - const serverShares: ExportSharesResponse = await exportRes.json(); + const exportJson: OkoApiResponse = + await exportRes.json(); + if (!exportJson.success) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `export API error: ${exportJson.msg}`, + }, + }); + return; + } + const serverShares = exportJson.data; // --------------------------------------------------------------- // 11. secp256k1 combine @@ -319,11 +332,11 @@ export async function handleExportPrivateKey( return; } - // 11c. Combine server share (Participant 0) + user share (Participant 1) → full private key + // 11c. Combine user share (Participant 0) + server share (Participant 1) → full private key const fullSecp256k1Scalar = secp256k1Wasm.cli_combine_shares({ shares: { - "0": serverShares.secp256k1_share, - "1": userKeyshare1Res.data, + "0": userKeyshare1Res.data, + "1": serverShares.secp256k1_share, }, }); const secp256k1PrivateKey = `0x${fullSecp256k1Scalar}`; From 39af29da941768407b5dbc26807189fb0dc31bf2 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 17:58:45 +0900 Subject: [PATCH 38/62] oko_attached: refactor email re-auth popup --- .../src/app/export_private_key/page.tsx | 8 +- .../export/email_reauth.module.scss | 362 ++++++++++++++++++ .../src/components/export/email_reauth.tsx | 195 ++++++---- 3 files changed, 494 insertions(+), 71 deletions(-) create mode 100644 embed/oko_attached/src/components/export/email_reauth.module.scss diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index dbb7cfbc5..90ec3aca2 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -394,12 +394,16 @@ export default function Page() { try { setIsLoading(true); - // 1. Open re-auth popup at attached origin + // 1. Open re-auth popup at attached origin (match sign-in popup sizes) const attachedOrigin = new URL(okoWallet.sdkEndpoint).origin; + const popupWidth = 440; + const popupHeight = authType === "telegram" ? 402 : 285; + const popupLeft = Math.max((window.screen.width - popupWidth) / 2, 0); + const popupTop = Math.max((window.screen.height - popupHeight) / 2, 0); popup = window.open( `${attachedOrigin}/export/reauth?auth_type=${authType}`, "oko_re_auth", - "width=600,height=700", + `width=${popupWidth},height=${popupHeight},left=${popupLeft},top=${popupTop},resizable=yes`, ); // 2. Send export request to attached iframe diff --git a/embed/oko_attached/src/components/export/email_reauth.module.scss b/embed/oko_attached/src/components/export/email_reauth.module.scss new file mode 100644 index 000000000..7b8fe8f6a --- /dev/null +++ b/embed/oko_attached/src/components/export/email_reauth.module.scss @@ -0,0 +1,362 @@ +.container { + position: relative; +} + +.body { + display: flex; + flex-direction: column; + position: relative; + align-items: center; + padding-top: 0; + justify-content: center; + min-height: 284px; +} + +.card { + width: 376px; + min-width: 360px; + min-height: 220px; + border-radius: 12px; + background: var(--bg-secondary); + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + padding: 0; +} + +.cardTop { + width: 376px; + height: 126px; + padding: var(--spacing-3xl) var(--spacing-2xl) var(--spacing-2xl) + var(--spacing-2xl); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; +} + +.cardBottom { + width: 376px; + height: 94px; + min-width: 200px; + padding: var(--spacing-2xl); + display: flex; + justify-content: center; +} + +.fieldHeader { + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-family-body); + font-weight: 600; + font-size: var(--font-size-text-md); + line-height: var(--line-height-text-md); + letter-spacing: 0; + color: var(--text-primary); +} + +.form { + display: flex; + flex-direction: column; + width: 100%; + max-width: 420px; + align-items: center; +} + +.emailRow { + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + width: 336px; + height: 42px; + border-radius: 12px; + border: 1px solid var(--border-secondary); + background: var(--bg-primary); + box-shadow: + 0 1px 2px rgba(15, 23, 42, 0.08), + 0 1px 1px rgba(15, 23, 42, 0.04); +} + +.emailInner { + width: 308px; + height: 22px; + display: flex; + align-items: center; + gap: 8px; +} + +.emailIcon { + color: var(--fg-quaternary); + flex-shrink: 0; +} + +.emailInput { + flex: 1 0 0; + border: none; + outline: none; + background: transparent; + color: var(--text-primary); + font-family: var(--font-family-body); + font-weight: 400; + font-style: normal; + font-size: var(--font-size-text-md); + line-height: var(--line-height-text-md); + letter-spacing: 0; + + &::placeholder { + color: var(--gray-500); + } +} + +.nextButton { + height: 22px !important; + padding: 0 4px !important; + color: var(--gray-500) !important; + background: none; + border: none; + cursor: pointer; + font: inherit; + flex-shrink: 0; + + &:disabled { + cursor: not-allowed !important; + color: var(--text-disabled, #717680) !important; + } + + &.nextButtonActive:not(:disabled) { + color: var(--text-primary) !important; + } +} + +.actions { + min-height: 32px; +} + +/* OTP step */ + +.otpShell { + width: 376px; + min-width: 360px; + min-height: 220px; + border-radius: 12px; + background: var(--bg-secondary); + display: flex; + flex-direction: column; + align-items: center; + padding: var(--spacing-2xl) 0; +} + +.otpForm { + align-items: center; +} + +.otpPanel { + width: 340px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.otpTitle { + font-family: var(--font-family-body); + font-weight: 600; + font-size: var(--font-size-text-lg); + line-height: var(--line-height-text-lg); + letter-spacing: 0; + color: var(--text-primary); + text-align: center; +} + +.otpSubtitle { + margin-top: 0; + text-align: center; + font-family: Inter, var(--font-family-body); + font-weight: 400; + font-style: normal; + font-size: 15px; + line-height: 150%; + letter-spacing: -0.01em; + color: var(--text-primary); +} + +.otpCodeSection { + display: flex; + flex-direction: column; + gap: 8px; + width: 340px; + align-items: center; +} + +.otpInputRow { + margin-top: 0; +} + +.otpInputRow > div { + gap: 6px !important; +} + +.otpInputRow input { + width: 48px !important; + height: 48px !important; + background: var(--bg-primary) !important; + border: 1px solid var(--border-primary) !important; + border-radius: 7.5px !important; + box-shadow: 0 0.75px 1.5px 0 var(--ColorsEffectsShadowsshadow-xs) !important; + color: var(--text-primary) !important; + caret-color: var(--text-primary) !important; + font-family: var(--font-family-body) !important; + font-weight: 500 !important; + font-style: normal !important; + font-size: 36px !important; + line-height: 45px !important; + letter-spacing: -0.02em !important; + text-align: center !important; + + &::placeholder { + color: var(--text-placeholder-subtle) !important; + opacity: 1 !important; + font-family: var(--font-family-body) !important; + font-weight: 500 !important; + font-style: normal !important; + font-size: 36px !important; + line-height: 45px !important; + letter-spacing: -0.02em !important; + text-align: center !important; + } + + &:not(:placeholder-shown) { + border: 1.5px solid var(--border-brand) !important; + } + + &:focus { + border: 1.5px solid var(--border-brand) !important; + background: var(--bg-primary) !important; + box-shadow: + 0 0.75px 1.5px 0 var(--ColorsEffectsShadowsshadow-xs), + 0 0 0 1.5px var(--bg-primary), + 0 0 0 3px var(--ColorsEffectsFocusringsfocus-ring) !important; + } +} + +.otpInputRowError input, +.otpInputRowError input:not(:placeholder-shown), +.otpInputRowError input:focus { + border: 1.5px solid var(--border-error) !important; + color: var(--text-error-primary) !important; + caret-color: var(--text-error-primary) !important; +} + +.otpInputRowError input::placeholder { + color: var(--text-error-primary) !important; +} + +.otpErrorMessage { + width: 318px; + padding: 0; + text-align: left; +} + +.resendRow { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + width: 340px; + height: 20px; +} + +.resendText { + font-family: var(--font-family-body); + font-weight: 500; + font-size: var(--font-size-text-sm); + line-height: var(--line-height-text-sm); + letter-spacing: 0; + color: var(--text-primary); +} + +.resendLink { + background: none; + border: none; + padding: 0; + font: inherit; + font-family: var(--font-family-body); + font-weight: 600; + font-size: var(--font-size-text-sm); + line-height: var(--line-height-text-sm); + letter-spacing: 0; + text-decoration: underline; + color: var(--text-primary); + cursor: pointer; + + &:disabled { + color: var(--text-disabled); + cursor: not-allowed; + } +} + +.resendTimer { + font-family: var(--font-family-body); + font-weight: 500; + font-size: var(--font-size-text-sm); + line-height: var(--line-height-text-sm); + letter-spacing: 0; + color: #535862; +} + +@media (hover: none) and (pointer: coarse) { + .body { + min-height: 100vh; + justify-content: flex-start; + } + + .card, + .otpShell { + width: 100%; + min-width: 0; + min-height: 100vh; + border-radius: 0; + } + + .cardTop { + width: 100%; + padding: 36px 20px 20px; + } + + .fieldHeader { + font-weight: 500; + font-size: var(--font-size-sm); + line-height: var(--font-line-height-sm); + } + + .cardBottom { + width: 100%; + padding: 20px; + } + + .form { + max-width: none; + } + + .emailRow { + width: 100%; + height: 42px; + padding: 10px 14px; + border-radius: 8px; + border-color: var(--border-primary); + box-shadow: var(--shadow-xs); + } + + .emailInner { + width: 100%; + } + + .nextButton { + height: auto !important; + padding: 0 !important; + } +} diff --git a/embed/oko_attached/src/components/export/email_reauth.tsx b/embed/oko_attached/src/components/export/email_reauth.tsx index f66626945..3bb36531e 100644 --- a/embed/oko_attached/src/components/export/email_reauth.tsx +++ b/embed/oko_attached/src/components/export/email_reauth.tsx @@ -1,7 +1,11 @@ -import { type FormEvent, useEffect, useMemo, useState } from "react"; +import { type FormEvent, useContext, useEffect, useMemo, useState } from "react"; import type { OAuthState } from "@oko-wallet/oko-sdk-core"; import { OtpInput } from "@oko-wallet/oko-common-ui/otp_input"; +import { Typography } from "@oko-wallet/oko-common-ui/typography"; +import { MailboxIcon } from "@oko-wallet/oko-common-ui/icons/mailbox"; +import { Logo } from "@oko-wallet/oko-common-ui/logo"; +import { ThemeContext } from "@oko-wallet/oko-common-ui/theme"; import { getAuth0WebAuth } from "@oko-wallet-attached/config/auth0"; import { @@ -14,6 +18,7 @@ import { generateNonce, sendReauthParamsToIframe, } from "./use_export_reauth"; +import styles from "./email_reauth.module.scss"; const CODE_LENGTH = 6; const RESEND_COOLDOWN_SECONDS = 180; @@ -22,6 +27,7 @@ const LOG_PREFIX = "[attached][email_reauth]"; type Step = "enter_email" | "verify_code"; export function EmailReauth() { + const theme = useContext(ThemeContext); const webAuth = useMemo(() => getAuth0WebAuth(), []); const [step, setStep] = useState("enter_email"); @@ -183,76 +189,127 @@ export function EmailReauth() { void handleVerifyCode(); }; - if (step === "enter_email") { - return ( -
    -

    Email Re-Authentication

    -
    - { - resetError(); - setEmail(e.target.value); - }} - style={{ - width: "100%", - padding: "8px", - marginBottom: "8px", - boxSizing: "border-box", - }} - autoFocus - /> - - {errorMessage && ( -
    {errorMessage}
    - )} -
    -
    - ); - } - return ( -
    -

    Check your email

    -

    Enter the 6-digit code sent to {email}.

    -
    - { - resetError(); - setOtpDigits(digits); - }} - disabled={isSubmitting} - isError={!!errorMessage} - /> - {errorMessage && ( -
    {errorMessage}
    +
    +
    + {step === "enter_email" ? ( +
    +
    + +
    + Enter your email to continue +
    +
    +
    + +
    +
    + + { + resetError(); + setEmail(e.target.value); + }} + className={styles.emailInput} + autoFocus + /> + +
    +
    + + {errorMessage && ( + + {errorMessage} + + )} + +
    + +
    +
    + ) : ( +
    +
    +
    +
    Check your email
    +
    + {`Enter the 6-digit code sent to ${email || "your email"}.`} +
    + +
    +
    + { + resetError(); + setOtpDigits(digits); + }} + disabled={isSubmitting} + isError={!!errorMessage} + /> +
    + + {errorMessage && ( + + {errorMessage} + + )} +
    + +
    + + Didn't get the code? + + + {resendTimer > 0 && ( + {`${resendTimer}s`} + )} +
    +
    + +
    + +
    )} -
    - Didn't get the code? - - {resendTimer > 0 && {resendTimer}s} -
    - +
    ); } From ff6f3b7fa11fa30f1d0ffb97c2bfe4fce8a22d5d Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 18:23:01 +0900 Subject: [PATCH 39/62] oko_attached: refactor telegram re-auth popup --- .../components/export/email_reauth.module.scss | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/embed/oko_attached/src/components/export/email_reauth.module.scss b/embed/oko_attached/src/components/export/email_reauth.module.scss index 7b8fe8f6a..6affa99bf 100644 --- a/embed/oko_attached/src/components/export/email_reauth.module.scss +++ b/embed/oko_attached/src/components/export/email_reauth.module.scss @@ -136,20 +136,6 @@ min-height: 32px; } -/* OTP step */ - -.otpShell { - width: 376px; - min-width: 360px; - min-height: 220px; - border-radius: 12px; - background: var(--bg-secondary); - display: flex; - flex-direction: column; - align-items: center; - padding: var(--spacing-2xl) 0; -} - .otpForm { align-items: center; } @@ -314,8 +300,7 @@ justify-content: flex-start; } - .card, - .otpShell { + .card { width: 100%; min-width: 0; min-height: 100vh; From dec8ef4e441109c9939301d8d6b9ca7dbac74adc Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 18:58:53 +0900 Subject: [PATCH 40/62] oko_attached: refactor telegram re-auth popup --- .../export/telegram_reauth.module.scss | 291 ++++++++++++++++++ .../src/components/export/telegram_reauth.tsx | 118 ++++++- 2 files changed, 394 insertions(+), 15 deletions(-) create mode 100644 embed/oko_attached/src/components/export/telegram_reauth.module.scss diff --git a/embed/oko_attached/src/components/export/telegram_reauth.module.scss b/embed/oko_attached/src/components/export/telegram_reauth.module.scss new file mode 100644 index 000000000..79d64c44f --- /dev/null +++ b/embed/oko_attached/src/components/export/telegram_reauth.module.scss @@ -0,0 +1,291 @@ +:global([data-theme="dark"]) { + html, + body { + background: var(--Colors-Background-bg-primary, #0c0e12) !important; + } +} + +.container { + position: relative; + width: 100%; + min-height: 100vh; + background: transparent; + + :global([data-theme="dark"]) & { + background: var(--Colors-Background-bg-primary, #0c0e12); + } +} + +.body { + display: flex; + flex-direction: column; + position: relative; + align-items: center; + padding-top: 0; + justify-content: center; + min-height: 100vh; + width: 100%; + + :global([data-theme="dark"]) & { + background: var(--Colors-Background-bg-primary, #0c0e12); + } +} + +.popupContainer { + display: flex; + padding: 32px; + flex-direction: column; + align-items: center; + background: var(--Colors-Background-bg-primary, #fff); + width: 100%; + height: 100%; + box-sizing: border-box; + + :global([data-theme="dark"]) & { + background: var(--Colors-Background-bg-primary, #0c0e12); + } +} + +.card { + display: flex; + width: 376px; + min-width: 360px; + padding: 56px var(--spacing-2xl, 20px); + flex-direction: column; + align-items: center; + border-radius: 12px; + background: var(--Colors-Background-bg-secondary, #fafafa); + gap: 0; + + :global([data-theme="dark"]) & { + background: var(--Colors-Background-bg-secondary, #13161b); + } +} + +.stepIndicator { + display: flex; + padding: 0 6px; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 3px; + background: var(--Colors-Background-bg-secondary, #fafafa); + gap: 8px; + margin-bottom: var(--Spacing-Block-125rem, 20px); + + :global([data-theme="dark"]) & { + background: var(--Colors-Background-bg-secondary, #13161b); + } +} + +.stepProgressBar { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; +} + +.stepNumberActive { + display: flex; + padding: 0 6px; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 3px; + background: var(--Colors-Background-bg-tertiary, #22262f); + color: var(--colors-text-text-secondary-700, #cecfd2); + text-align: center; + font-family: var(--font-family-body, Inter); + font-size: var(--Font-size-text-sm, 14px); + font-style: normal; + font-weight: 500; + line-height: var(--Line-height-text-sm, 20px); + + :global([data-theme="dark"]) & { + background: var(--Colors-Background-bg-tertiary, #f5f5f5); + color: var(--colors-text-text-secondary-700, #414651); + } +} + +.stepNumberInactive { + display: flex; + padding: 0 6px; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 3px; + background: var(--Colors-Background-bg-tertiary, #f5f5f5); + color: var(--colors-text-text-secondary-700, #414651); + text-align: center; + font-family: var(--font-family-body, Inter); + font-size: var(--Font-size-text-sm, 14px); + font-style: normal; + font-weight: 500; + line-height: var(--Line-height-text-sm, 20px); + + :global([data-theme="dark"]) & { + background: var(--Colors-Background-bg-tertiary, #22262f); + color: var(--colors-text-text-secondary-700, #cecfd2); + } +} + +.stepLine { + width: 30px; + height: 2px; + flex-shrink: 0; + display: block; +} + +.stepLine path { + stroke: var(--colors-text-text-primary-900, #181d27); + stroke-width: 1.22807; + stroke-linecap: round; + + :global([data-theme="dark"]) & { + stroke: var(--colors-text-text-primary-900, #f7f7f7); + } +} + +.stepText { + color: var(--colors-text-text-primary-900, #181d27); + font-family: var(--font-family-body, Inter); + font-size: var(--Font-size-text-sm, 14px); + font-style: normal; + font-weight: 500; + line-height: var(--Line-height-text-sm, 20px); + + :global([data-theme="dark"]) & { + color: var(--colors-text-text-primary-900, #f7f7f7); + } +} + +.cardTop { + display: flex; + padding: 0; + flex-direction: column; + align-items: center; + align-self: stretch; + margin-top: var(--spacing-3xl, 24px); +} + +.continueText { + color: var(--colors-text-text-primary-900, #181d27); + font-family: var(--font-family-body, Inter); + font-size: var(--Font-size-text-sm, 14px); + font-style: normal; + font-weight: 500; + line-height: var(--Line-height-text-sm, 20px); + margin-top: var(--Spacing-Block-125rem, 20px); + + :global([data-theme="dark"]) & { + color: var(--colors-text-text-primary-900, #f7f7f7); + } +} + +.telegramWidgetContainer { + display: flex; + justify-content: center; + align-items: center; + border-radius: 107.341px; + margin-top: var(--Spacing-Block-125rem, 20px); + margin-bottom: var(--spacing-2xl, 20px); +} + +.telegramWidgetContainer :global(iframe) { + border-radius: 107.341px !important; +} + +/* Error view — matches login_popup_error_view */ + +.errorContainer { + width: 440px; + display: flex; + flex-direction: column; + background: #ffffff; + border-radius: 12px; + overflow: hidden; +} + +.errorTopSection { + width: 440px; + height: 206px; + padding: 24px 24px 0 24px; + display: flex; + flex-direction: column; + align-items: center; +} + +.errorIconWrapper { + display: flex; + justify-content: center; + align-items: center; +} + +.errorTitle { + font-family: var(--font-family-body, Inter); + font-weight: 600; + font-size: var(--font-size-text-lg); + line-height: var(--line-height-text-lg); + letter-spacing: 0; + text-align: center; + margin: 7px 0 0 0; +} + +.errorMessageBox { + --radius-xl: 12px; + width: 392px; + background: var(--Colors-Background-bg-warning-primary, #fffaeb); + border-radius: var(--radius-xl); + padding: var(--spacing-lg); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + margin-top: 12px; +} + +.errorTextRow { + width: 368px; + min-height: 20px; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.errorMessageText { + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 1; + line-clamp: 1; + -webkit-box-orient: vertical; + color: var(--colors-text-text-warning-primary-600, #dc6803); + letter-spacing: 0; + font-family: var(--font-family-body, Inter); +} + +.errorSupportLink { + text-align: center; + text-decoration: underline; + text-decoration-style: solid; + text-decoration-thickness: auto; + color: var(--text-secondary); + cursor: pointer; + margin-top: 12px; + display: block; + font-family: var(--font-family-body, Inter); +} + +.errorBottomSection { + width: 440px; + height: 78px; + padding-bottom: 24px; + padding-left: 24px; + padding-right: 24px; + display: flex; + justify-content: center; + align-items: flex-end; + border-top: 1px solid var(--border-subtle); +} diff --git a/embed/oko_attached/src/components/export/telegram_reauth.tsx b/embed/oko_attached/src/components/export/telegram_reauth.tsx index 3dd75010f..4d07a7c24 100644 --- a/embed/oko_attached/src/components/export/telegram_reauth.tsx +++ b/embed/oko_attached/src/components/export/telegram_reauth.tsx @@ -1,7 +1,12 @@ -import { useEffect, useMemo, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import type { OAuthState } from "@oko-wallet/oko-sdk-core"; import { RedirectUriSearchParamsKey } from "@oko-wallet/oko-sdk-core"; +import { Typography } from "@oko-wallet/oko-common-ui/typography"; +import { Button } from "@oko-wallet/oko-common-ui/button"; +import { WarningIcon } from "@oko-wallet/oko-common-ui/icons/warning_icon"; +import { Logo } from "@oko-wallet/oko-common-ui/logo"; +import { ThemeContext } from "@oko-wallet/oko-common-ui/theme"; import { TELEGRAM_BOT_NAME } from "@oko-wallet-attached/config/telegram"; @@ -9,10 +14,12 @@ import { findEmbeddedIframe, sendReauthParamsToIframe, } from "./use_export_reauth"; +import styles from "./telegram_reauth.module.scss"; const LOG_PREFIX = "[attached][telegram_reauth]"; export function TelegramReauth() { + const theme = useContext(ThemeContext); const [errorMessage, setErrorMessage] = useState(null); // Build OAuthState for the callback to parse @@ -77,25 +84,106 @@ export function TelegramReauth() { if (errorMessage) { return ( -
    -
    {errorMessage}
    +
    +
    +
    +
    +
    +
    + +
    + + Request failed + +
    +
    + + {errorMessage} + +
    +
    + + Get Support + +
    +
    + +
    +
    +
    +
    ); } return ( -
    -

    Telegram Re-Authentication

    -

    Continue with Telegram to verify your identity.

    -
    +
    +
    +
    +
    +
    +
    +
    1
    + + + +
    2
    +
    +
    Step 1/2
    +
    +
    + +
    Continue with Telegram
    +
    +
    +
    +
    +
    +
    +
    ); } From bbe0079095411490bc9c2c204e41daeee8176120 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 19:34:58 +0900 Subject: [PATCH 41/62] o --- apps/user_dashboard/src/state/user_info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user_dashboard/src/state/user_info.ts b/apps/user_dashboard/src/state/user_info.ts index 29285fb40..2bb32b065 100644 --- a/apps/user_dashboard/src/state/user_info.ts +++ b/apps/user_dashboard/src/state/user_info.ts @@ -36,7 +36,7 @@ export const useUserInfoState = create( email: info.email, name: info.name, publicKey: info.publicKey, - isSignedIn: !!(info.email && info.publicKey), + isSignedIn: !!info.publicKey, }); }, setAuthType: (authType) => { From 00511ae398c12a389f367a7961e176489b1a571b Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Mon, 23 Feb 2026 19:51:05 +0900 Subject: [PATCH 42/62] o --- .../user_dashboard/src/app/export_private_key/page.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 90ec3aca2..4722fdb11 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -396,8 +396,14 @@ export default function Page() { // 1. Open re-auth popup at attached origin (match sign-in popup sizes) const attachedOrigin = new URL(okoWallet.sdkEndpoint).origin; - const popupWidth = 440; - const popupHeight = authType === "telegram" ? 402 : 285; + const isOAuthProvider = + authType === "google" || authType === "x" || authType === "discord"; + const popupWidth = isOAuthProvider ? 1200 : 440; + const popupHeight = isOAuthProvider + ? 800 + : authType === "telegram" + ? 402 + : 285; const popupLeft = Math.max((window.screen.width - popupWidth) / 2, 0); const popupTop = Math.max((window.screen.height - popupHeight) / 2, 0); popup = window.open( From 5214a1c3e57e218cc2205a3ff9f7152f8cf15ca2 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 14:07:07 +0900 Subject: [PATCH 43/62] oko_api: add export commit-reveal operation types and allowed apis --- .../oko_api/server/src/commit_reveal/allowed_apis.ts | 4 ++++ backend/openapi/src/tss/commit_reveal.ts | 2 ++ common/oko_types/src/commit_reveal/index.ts | 11 +++++++++-- key_share_node/ksn_interface/src/commit_reveal.ts | 4 +++- .../server/src/commit_reveal/allowed_apis.ts | 4 ++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/backend/oko_api/server/src/commit_reveal/allowed_apis.ts b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts index 9a20535a1..b177eeb59 100644 --- a/backend/oko_api/server/src/commit_reveal/allowed_apis.ts +++ b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts @@ -9,6 +9,8 @@ export const ALLOWED_APIS = { reshare: ["signin", "reshare"], add_ed25519: ["signin", "keygen_ed25519"], add_ed25519_with_reshare: ["signin", "keygen_ed25519", "reshare"], + export: ["export_shares"], + export_with_reshare: ["reshare", "export_shares"], }; export const FINAL_APIS = { @@ -17,6 +19,8 @@ export const FINAL_APIS = { reshare: "reshare", add_ed25519: "keygen_ed25519", add_ed25519_with_reshare: "reshare", + export: "export_shares", + export_with_reshare: "export_shares", }; export function isApiAllowed( diff --git a/backend/openapi/src/tss/commit_reveal.ts b/backend/openapi/src/tss/commit_reveal.ts index ad5a0be08..a6751b0e5 100644 --- a/backend/openapi/src/tss/commit_reveal.ts +++ b/backend/openapi/src/tss/commit_reveal.ts @@ -8,6 +8,8 @@ export const OperationTypeSchema = z "reshare", "add_ed25519", "add_ed25519_with_reshare", + "export", + "export_with_reshare", ]) .describe("Operation type for commit-reveal session"); diff --git a/common/oko_types/src/commit_reveal/index.ts b/common/oko_types/src/commit_reveal/index.ts index cc20c3ef8..5e350d70d 100644 --- a/common/oko_types/src/commit_reveal/index.ts +++ b/common/oko_types/src/commit_reveal/index.ts @@ -3,9 +3,16 @@ export type OperationType = | "sign_in" | "reshare" | "add_ed25519" - | "add_ed25519_with_reshare"; + | "add_ed25519_with_reshare" + | "export" + | "export_with_reshare"; -export type ApiName = "signin" | "keygen" | "reshare" | "keygen_ed25519"; +export type ApiName = + | "signin" + | "keygen" + | "reshare" + | "keygen_ed25519" + | "export_shares"; export type SessionState = "COMMITTED" | "COMPLETED"; diff --git a/key_share_node/ksn_interface/src/commit_reveal.ts b/key_share_node/ksn_interface/src/commit_reveal.ts index 7cbfe42ae..b880822ba 100644 --- a/key_share_node/ksn_interface/src/commit_reveal.ts +++ b/key_share_node/ksn_interface/src/commit_reveal.ts @@ -3,7 +3,9 @@ export type OperationType = | "sign_in" | "reshare" | "add_ed25519" - | "add_ed25519_with_reshare"; + | "add_ed25519_with_reshare" + | "export" + | "export_with_reshare"; export type ApiName = | "get_key_shares" diff --git a/key_share_node/server/src/commit_reveal/allowed_apis.ts b/key_share_node/server/src/commit_reveal/allowed_apis.ts index d9b23c9f3..f3de53935 100644 --- a/key_share_node/server/src/commit_reveal/allowed_apis.ts +++ b/key_share_node/server/src/commit_reveal/allowed_apis.ts @@ -9,6 +9,8 @@ export const ALLOWED_APIS: Record = { reshare: ["get_key_shares", "reshare"], add_ed25519: ["register_ed25519", "get_key_shares"], add_ed25519_with_reshare: ["register_ed25519", "get_key_shares", "reshare"], + export: ["get_key_shares"], + export_with_reshare: ["get_key_shares", "reshare"], }; export const FINAL_APIS: Record = { @@ -17,6 +19,8 @@ export const FINAL_APIS: Record = { reshare: "reshare", add_ed25519: "get_key_shares", add_ed25519_with_reshare: "reshare", + export: "get_key_shares", + export_with_reshare: "reshare", }; export function isApiAllowed( From 9ea6d08fedef05e982ed701667a09760393d0fb7 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 14:29:24 +0900 Subject: [PATCH 44/62] oko_api: add commitRevealMiddleware to export_shares route --- backend/oko_api/server/src/routes/tss_v2/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/oko_api/server/src/routes/tss_v2/index.ts b/backend/oko_api/server/src/routes/tss_v2/index.ts index bc9dfb671..ab8996056 100644 --- a/backend/oko_api/server/src/routes/tss_v2/index.ts +++ b/backend/oko_api/server/src/routes/tss_v2/index.ts @@ -196,7 +196,7 @@ export function makeTSSRouterV2() { router.post( "/export_shares", - [userJwtMiddlewareV2, tssActivateMiddleware], + [userJwtMiddlewareV2, commitRevealMiddleware("export_shares"), tssActivateMiddleware], exportShares, ); From 8f6203fba705cdd97adfde985841347510c9fc2a Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 15:42:17 +0900 Subject: [PATCH 45/62] oko_attached: remove signInV2 from export flow --- .../server/src/middleware/auth/keplr_auth.ts | 67 ++++++-- .../server/src/routes/tss_v2/export_shares.ts | 131 ++-------------- .../oko_api/server/src/routes/tss_v2/index.ts | 7 +- backend/openapi/src/tss/user.ts | 19 ++- common/oko_types/src/user/index.ts | 4 +- .../src/window_msgs/export_private_key.ts | 146 ++++++++---------- 6 files changed, 149 insertions(+), 225 deletions(-) diff --git a/backend/oko_api/server/src/middleware/auth/keplr_auth.ts b/backend/oko_api/server/src/middleware/auth/keplr_auth.ts index 289fe30b6..dfbc93e80 100644 --- a/backend/oko_api/server/src/middleware/auth/keplr_auth.ts +++ b/backend/oko_api/server/src/middleware/auth/keplr_auth.ts @@ -64,22 +64,16 @@ export async function userJwtMiddleware( } } -export async function userJwtMiddlewareV2( +/** + * Verify a V2 JWT token and set `res.locals.user` with the decoded payload. + * Shared by `userJwtMiddlewareV2` (header) and `userJwtFromBodyMiddleware` (body). + */ +function verifyJwtV2AndSetLocals( + token: string, req: UserAuthenticatedRequest, res: Response, next: NextFunction, -) { - const authHeader = req.headers.authorization; - - if (!authHeader || !authHeader.startsWith("Bearer ")) { - res - .status(401) - .json({ error: "Authorization header with Bearer token required" }); - return; - } - - const token = authHeader.substring(7); // skip "Bearer " - +): void { try { const state = req.app.locals; @@ -95,9 +89,13 @@ export async function userJwtMiddlewareV2( return; } - const payload = verifyTokenRes.data + const payload = verifyTokenRes.data; - if (!payload.email || !payload.wallet_id_secp256k1 || !payload.wallet_id_ed25519) { + if ( + !payload.email || + !payload.wallet_id_secp256k1 || + !payload.wallet_id_ed25519 + ) { res.status(401).json({ error: "Unauthorized: Invalid token", }); @@ -111,13 +109,50 @@ export async function userJwtMiddlewareV2( }; next(); - return; } catch (error) { res.status(500).json({ error: `Token validation failed: ${error instanceof Error ? error.message : String(error)}`, }); + } +} + +export async function userJwtMiddlewareV2( + req: UserAuthenticatedRequest, + res: Response, + next: NextFunction, +) { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + res + .status(401) + .json({ error: "Authorization header with Bearer token required" }); return; } + + const token = authHeader.substring(7); // skip "Bearer " + verifyJwtV2AndSetLocals(token, req, res, next); +} + +/** + * JWT middleware that reads the token from `body.first_login_jwt` instead of + * the Authorization header. Used by export_shares endpoint. + */ +export async function userJwtFromBodyMiddleware( + req: UserAuthenticatedRequest, + res: Response, + next: NextFunction, +) { + const firstLoginJwt = req.body?.first_login_jwt; + + if (!firstLoginJwt || typeof firstLoginJwt !== "string") { + res + .status(401) + .json({ error: "first_login_jwt is required in request body" }); + return; + } + + verifyJwtV2AndSetLocals(firstLoginJwt, req, res, next); } export function sendResponseWithNewToken( diff --git a/backend/oko_api/server/src/routes/tss_v2/export_shares.ts b/backend/oko_api/server/src/routes/tss_v2/export_shares.ts index fd810329d..4ffb62bdf 100644 --- a/backend/oko_api/server/src/routes/tss_v2/export_shares.ts +++ b/backend/oko_api/server/src/routes/tss_v2/export_shares.ts @@ -1,15 +1,13 @@ import type { Response } from "express"; import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; -import type { AuthType } from "@oko-wallet/oko-types/auth"; import type { ExportSharesRequest, ExportSharesResponse, } from "@oko-wallet/oko-types/user"; -import type { Result } from "@oko-wallet/stdlib-js"; import { decryptDataAsync } from "@oko-wallet/crypto-js/node"; import { ErrorResponseSchema, - UserAuthHeaderSchema, + OAuthHeaderSchema, } from "@oko-wallet/oko-api-openapi/common"; import { ExportSharesRequestSchema, @@ -20,19 +18,7 @@ import { getUserByEmailAndAuthType } from "@oko-wallet/oko-pg-interface/oko_user import { validateWalletEmailAndCurveType } from "@oko-wallet-api/api/tss/utils"; import { type UserAuthenticatedRequest } from "@oko-wallet-api/middleware/auth/keplr_auth"; -import { validateOAuthToken } from "@oko-wallet-api/middleware/auth/google_auth/validate"; -import { GOOGLE_CLIENT_ID } from "@oko-wallet-api/middleware/auth/google_auth/client_id"; -import { validateAuth0IdToken } from "@oko-wallet-api/middleware/auth/auth0_auth/validate"; -import { - AUTH0_CLIENT_ID, - AUTH0_DOMAIN, -} from "@oko-wallet-api/middleware/auth/auth0_auth/client_id"; -import { validateAccessTokenOfX } from "@oko-wallet-api/middleware/auth/x_auth/validate"; -import { - validateTelegramHash, - type TelegramUserData, -} from "@oko-wallet-api/middleware/auth/telegram_auth/validate"; -import { validateDiscordOAuthToken } from "@oko-wallet-api/middleware/auth/discord_auth/validate"; +import type { OAuthLocals } from "@oko-wallet-api/middleware/auth/types"; registry.registerPath({ method: "post", @@ -40,10 +26,10 @@ registry.registerPath({ tags: ["TSS"], summary: "Export server shares for wallet export", description: - "Exports the server's secp256k1 TSS share and ed25519 seed_share. Requires dual authentication: JWT (Authorization header) + OAuth re-authentication (request body).", + "Exports the server's secp256k1 TSS share and ed25519 seed_share. Requires dual authentication: JWT (body.first_login_jwt) + OAuth re-authentication (Authorization Bearer id_token). Protected by commit-reveal middleware.", security: [{ userAuth: [] }], request: { - headers: UserAuthHeaderSchema, + headers: OAuthHeaderSchema, body: { content: { "application/json": { @@ -80,88 +66,9 @@ registry.registerPath({ }, }); -/** - * Validate OAuth id_token from request body (not from Authorization header). - * Reuses each provider's underlying validation function and constructs - * user_identifier with the same prefix convention as the auth middlewares. - */ -async function validateOAuthIdToken( - authType: AuthType, - idToken: string, - telegramBotToken?: string, -): Promise> { - switch (authType) { - case "google": { - const result = await validateOAuthToken(idToken, GOOGLE_CLIENT_ID); - if (!result.success) { - return { success: false, err: result.err }; - } - if (!result.data?.sub) { - return { success: false, err: "Can't get sub from Google token" }; - } - return { success: true, data: `google_${result.data.sub}` }; - } - case "auth0": { - const result = await validateAuth0IdToken({ - idToken, - clientId: AUTH0_CLIENT_ID, - domain: AUTH0_DOMAIN, - }); - if (!result.success) { - return { success: false, err: result.err }; - } - if (!result.data?.email) { - return { success: false, err: "Can't get email from Auth0 token" }; - } - return { success: true, data: result.data.email }; - } - case "x": { - const result = await validateAccessTokenOfX(idToken); - if (!result.success) { - return { success: false, err: result.err }; - } - if (!result.data?.id) { - return { success: false, err: "Can't get id from X token" }; - } - return { success: true, data: `x_${result.data.id}` }; - } - case "telegram": { - if (!telegramBotToken) { - return { success: false, err: "Telegram bot token not configured" }; - } - let userData: TelegramUserData; - try { - userData = JSON.parse(idToken) as TelegramUserData; - } catch { - return { success: false, err: "Invalid Telegram token format" }; - } - const result = validateTelegramHash(userData, telegramBotToken); - if (!result.success) { - return { success: false, err: result.err }; - } - if (!result.data?.id) { - return { success: false, err: "Can't get id from Telegram token" }; - } - return { success: true, data: `telegram_${result.data.id}` }; - } - case "discord": { - const result = await validateDiscordOAuthToken(idToken); - if (!result.success) { - return { success: false, err: result.err }; - } - if (!result.data?.id) { - return { success: false, err: "Can't get id from Discord token" }; - } - return { success: true, data: `discord_${result.data.id}` }; - } - default: - return { success: false, err: `Invalid auth_type: ${authType}` }; - } -} - export async function exportShares( req: UserAuthenticatedRequest, - res: Response>, + res: Response, OAuthLocals & Record>, ) { const state = req.app.locals; const user = res.locals.user; @@ -202,28 +109,12 @@ export async function exportShares( const secp256k1Wallet = secp256k1ValidateRes.data; const ed25519Wallet = ed25519ValidateRes.data; - // 3. Dual-auth: Validate OAuth re-authentication token from body - const { auth_type, id_token } = req.body; - - const oauthResult = await validateOAuthIdToken( - auth_type, - id_token, - state.telegram_bot_token, - ); - if (!oauthResult.success) { - res.status(401).json({ - success: false, - code: "UNAUTHORIZED", - msg: `OAuth validation failed: ${oauthResult.err}`, - }); - return; - } - - // 4. Same-user verification: OAuth identity must match JWT-authenticated wallets - const oauthUserIdentifier = oauthResult.data; + // 3. Same-user verification: OAuth identity (from oauthMiddleware) must match JWT wallets + const { auth_type } = req.body; + const oauthUser = res.locals.oauth_user; const oauthUserRes = await getUserByEmailAndAuthType( state.db, - oauthUserIdentifier, + oauthUser.user_identifier, auth_type, ); if (!oauthUserRes.success || !oauthUserRes.data) { @@ -247,7 +138,7 @@ export async function exportShares( return; } - // 5. Decrypt secp256k1 enc_tss_share (raw string, not JSON) + // 4. Decrypt secp256k1 enc_tss_share (raw string, not JSON) const secp256k1EncryptedShare = secp256k1Wallet.enc_tss_share.toString("utf-8"); const secp256k1Share = await decryptDataAsync( @@ -255,7 +146,7 @@ export async function exportShares( state.encryption_secret, ); - // 6. Decrypt ed25519 enc_tss_share (JSON with signing_share, verifying_share, seed_share) + // 5. Decrypt ed25519 enc_tss_share (JSON with signing_share, verifying_share, seed_share) const ed25519EncryptedShare = ed25519Wallet.enc_tss_share.toString("utf-8"); const ed25519Decrypted = await decryptDataAsync( ed25519EncryptedShare, diff --git a/backend/oko_api/server/src/routes/tss_v2/index.ts b/backend/oko_api/server/src/routes/tss_v2/index.ts index ab8996056..ae5c214bd 100644 --- a/backend/oko_api/server/src/routes/tss_v2/index.ts +++ b/backend/oko_api/server/src/routes/tss_v2/index.ts @@ -4,7 +4,7 @@ import { oauthMiddleware } from "@oko-wallet-api/middleware/auth/oauth"; import { tssActivateMiddleware } from "@oko-wallet-api/middleware/auth/tss_activate"; import { commitRevealMiddleware } from "@oko-wallet-api/middleware/commit_reveal"; import { keygenV2 } from "./keygen"; -import { userJwtMiddlewareV2 } from "@oko-wallet-api/middleware/auth/keplr_auth"; +import { userJwtMiddlewareV2, userJwtFromBodyMiddleware } from "@oko-wallet-api/middleware/auth/keplr_auth"; import { presignStep1 } from "./presign_step_1"; import { presignStep2 } from "./presign_step_2"; import { presignStep3 } from "./presign_step_3"; @@ -196,7 +196,10 @@ export function makeTSSRouterV2() { router.post( "/export_shares", - [userJwtMiddlewareV2, commitRevealMiddleware("export_shares"), tssActivateMiddleware], + userJwtFromBodyMiddleware, + oauthMiddleware, + commitRevealMiddleware("export_shares"), + tssActivateMiddleware, exportShares, ); diff --git a/backend/openapi/src/tss/user.ts b/backend/openapi/src/tss/user.ts index dfdeb4463..8ff498f86 100644 --- a/backend/openapi/src/tss/user.ts +++ b/backend/openapi/src/tss/user.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { registry } from "../registry"; +import { CommitRevealRequestFieldsSchema } from "./commit_reveal"; const KsNodeStatusEnum = z.enum(["ACTIVE", "INACTIVE"]); const WalletStatusEnum = z.enum([ @@ -291,14 +292,16 @@ export const CheckEmailSuccessResponseV2Schema = registry.register( export const ExportSharesRequestSchema = registry.register( "TssExportSharesRequest", - z.object({ - auth_type: AuthTypeEnum.openapi({ - description: "Authentication provider type for re-authentication", - }), - id_token: z.string().openapi({ - description: "OAuth id_token from re-authentication", - }), - }), + z + .object({ + first_login_jwt: z.string().openapi({ + description: "JWT from the first login session", + }), + auth_type: AuthTypeEnum.openapi({ + description: "Authentication provider type for re-authentication", + }), + }) + .merge(CommitRevealRequestFieldsSchema), ); const ExportSharesDataSchema = registry.register( diff --git a/common/oko_types/src/user/index.ts b/common/oko_types/src/user/index.ts index 754e85e8b..b01af59d3 100644 --- a/common/oko_types/src/user/index.ts +++ b/common/oko_types/src/user/index.ts @@ -95,8 +95,10 @@ export interface SignInResponseV2 { } export interface ExportSharesRequest { + first_login_jwt: string; auth_type: AuthType; - id_token: string; + cr_session_id: string; + cr_signature: string; } export interface ExportSharesResponse { diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index 0d7c523b8..55d365909 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -12,10 +12,7 @@ import type { MsgEventContext } from "./types"; import { OKO_SDK_TARGET } from "./target"; import { useAppState } from "@oko-wallet-attached/store/app"; import { USER_DASHBOARD_ORIGINS } from "@oko-wallet-attached/requests/endpoints"; -import { - signInV2, - TSS_V2_ENDPOINT, -} from "@oko-wallet-attached/requests/oko_api"; +import { TSS_V2_ENDPOINT } from "@oko-wallet-attached/requests/oko_api"; import { requestKeySharesWithBackup } from "@oko-wallet-attached/requests/ks_node_v2"; import { commitAll, @@ -39,7 +36,7 @@ type ExportPrivateKeyError = | { type: "NOT_AUTHENTICATED" } | { type: "USER_NOT_FOUND" } | { type: "ED25519_KEYGEN_REQUIRED" } - | { type: "USER_MISMATCH" } + | { type: "RESHARE_REQUIRED" } | { type: "COMBINE_ERROR"; error: string } | { type: "API_ERROR"; error: string } | { type: "REAUTH_TIMEOUT" } @@ -82,9 +79,9 @@ export async function handleExportPrivateKey( return; } - // 2. Auth token validation - const authToken = useAppState.getState().getAuthToken(hostOrigin); - if (!authToken) { + // 2. Auth token validation (first login JWT — will be sent to export_shares as body param) + const firstLoginJwt = useAppState.getState().getAuthToken(hostOrigin); + if (!firstLoginJwt) { sendAck({ success: false, error: { type: "NOT_AUTHENTICATED" } }); return; } @@ -98,10 +95,21 @@ export async function handleExportPrivateKey( return; } - // 4. Capture first login context from appState + // 4. Get public keys from appState (no signInV2 needed) const wallet = useAppState.getState().getWallet(hostOrigin); - const firstLoginPublicKey = wallet?.publicKey ?? null; - const apiKey = useAppState.getState().getApiKey(hostOrigin) ?? undefined; + const ed25519Wallet = useAppState.getState().getWalletEd25519(hostOrigin); + if (!wallet?.publicKey || !ed25519Wallet?.publicKey) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: "Wallet public keys not found in appState", + }, + }); + return; + } + const secp256k1PubKey = wallet.publicKey; + const ed25519PubKey = ed25519Wallet.publicKey; // 5. Wait for re-auth credentials (popup → OAuth callback → interceptor) let creds; @@ -124,7 +132,7 @@ export async function handleExportPrivateKey( return; } - console.log(`${LOG_PREFIX} re-auth credentials received, starting sign-in`); + console.log(`${LOG_PREFIX} re-auth credentials received, starting export`); // 5a. Notify parent (UD) that re-auth completed — popup close is now expected window.parent.postMessage( @@ -167,16 +175,22 @@ export async function handleExportPrivateKey( return; } + // Guard: needs reshare + if ("needs_reshare" in checkData && checkData.needs_reshare) { + sendAck({ success: false, error: { type: "RESHARE_REQUIRED" } }); + return; + } + const { keyshare_node_meta } = checkData; const { threshold, nodes } = keyshare_node_meta; - // 7. Sign-in building blocks: commitAll → signInV2 + // 7. Commit to KSN nodes + oko_api with "export" operation type const ksnCommitTargets: KsnCommitTarget[] = nodes.map((node) => ({ nodeUrl: node.endpoint, - operationType: "sign_in" as const, + operationType: "export" as const, })); const commitRes = await commitAll( - "sign_in", + "export", creds.authType, creds.idToken, ksnCommitTargets, @@ -191,55 +205,13 @@ export async function handleExportPrivateKey( } const { session, readyNodes, pendingCommits } = commitRes.data; - const signInCommitRevealRes = createOkoApiCommitRevealParams( - session, - "signin", - ); - if (!signInCommitRevealRes.success) { - sendAck({ - success: false, - error: { - type: "API_ERROR", - error: `commit-reveal params failed: ${signInCommitRevealRes.err}`, - }, - }); - return; - } - - const signInResult = await signInV2( - creds.idToken, - creds.authType, - signInCommitRevealRes.data, - apiKey, - ); - if (!signInResult.success) { - sendAck({ - success: false, - error: { - type: "API_ERROR", - error: `signIn failed: ${signInResult.err.error}`, - }, - }); - return; - } - const signInResp = signInResult.data; - - // 8. User mismatch check (re-auth must match first login) - if ( - firstLoginPublicKey && - signInResp.user.public_key_secp256k1 !== firstLoginPublicKey - ) { - sendAck({ success: false, error: { type: "USER_MISMATCH" } }); - return; - } - - // 9. Request key shares from KSN + // 8. Request key shares from KSN (using appState public keys) const requestSharesRes = await requestKeySharesWithBackup({ idToken: creds.idToken, authType: creds.authType, wallets: { - secp256k1: signInResp.user.public_key_secp256k1, - ed25519: signInResp.user.public_key_ed25519, + secp256k1: secp256k1PubKey, + ed25519: ed25519PubKey, }, threshold, session, @@ -259,27 +231,45 @@ export async function handleExportPrivateKey( } const { shares: keySharesByNode } = requestSharesRes.data; - // 10. Export API: get server shares (dual-auth: JWT + re-auth id_token) + // 9. Export API: get server shares + // Authorization header = re-auth id_token (for commit-reveal middleware) + // Body = first_login_jwt + auth_type + commit-reveal params + const exportCrParamsRes = createOkoApiCommitRevealParams( + session, + "export_shares", + ); + if (!exportCrParamsRes.success) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `commit-reveal params failed: ${exportCrParamsRes.err}`, + }, + }); + return; + } + const exportApiUrl = `${TSS_V2_ENDPOINT}/export_shares`; const exportBody: ExportSharesRequest = { + first_login_jwt: firstLoginJwt, auth_type: creds.authType, - id_token: creds.idToken, + cr_session_id: exportCrParamsRes.data.cr_session_id, + cr_signature: exportCrParamsRes.data.cr_signature, }; const exportRes = await fetch(exportApiUrl, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, + Authorization: `Bearer ${creds.idToken}`, }, body: JSON.stringify(exportBody), }); if (!exportRes.ok) { - const errText = await exportRes.text().catch(() => ""); sendAck({ success: false, error: { type: "API_ERROR", - error: `export API ${exportRes.status}: ${errText}`, + error: `export API failed with status ${exportRes.status}`, }, }); return; @@ -299,10 +289,10 @@ export async function handleExportPrivateKey( const serverShares = exportJson.data; // --------------------------------------------------------------- - // 11. secp256k1 combine + // 10. secp256k1 combine // --------------------------------------------------------------- - // 11a. Decode KSN secp256k1 shares → Point256 format + // 10a. Decode KSN secp256k1 shares → Point256 format const secp256k1DecodeRes = await decodeSecp256k1SharesByNode(keySharesByNode); if (!secp256k1DecodeRes.success) { @@ -316,7 +306,7 @@ export async function handleExportPrivateKey( return; } - // 11b. Combine KSN shares → user's keyshare_1 (Lagrange interpolation) + // 10b. Combine KSN shares → user's keyshare_1 (Lagrange interpolation) const userKeyshare1Res = await combineUserShares( secp256k1DecodeRes.data, threshold, @@ -332,7 +322,7 @@ export async function handleExportPrivateKey( return; } - // 11c. Combine user share (Participant 0) + server share (Participant 1) → full private key + // 10c. Combine user share (Participant 0) + server share (Participant 1) → full private key const fullSecp256k1Scalar = secp256k1Wasm.cli_combine_shares({ shares: { "0": userKeyshare1Res.data, @@ -342,25 +332,25 @@ export async function handleExportPrivateKey( const secp256k1PrivateKey = `0x${fullSecp256k1Scalar}`; // --------------------------------------------------------------- - // 12. ed25519 seed 2-stage combine + // 11. ed25519 seed 2-stage combine // --------------------------------------------------------------- - // 12a. Convert KSN seed shares to UserKeySharePointByNode format + // 11a. Convert KSN seed shares to UserKeySharePointByNode format const ksnSeedShares = convertSeedShares(keySharesByNode); - // 12b. Convert to PointNumArr for WASM + // 11b. Convert to PointNumArr for WASM const ksnSeedPoints = ksnSeedShares.map((s) => ({ x: [...s.share.x.toUint8Array()], y: [...s.share.y.toUint8Array()], })); - // 12c. Stage 1: Combine KSN seed shares → user_seed_Y + // 11c. Stage 1: Combine KSN seed shares → user_seed_Y const userSeedY: number[] = secp256k1Wasm.seed_sss_combine( ksnSeedPoints, threshold, ); - // 12d. Parse server's seed share + // 11d. Parse server's seed share const serverSeedShareRes = hexToSeedSharePoint( serverShares.ed25519_seed_share, ); @@ -375,7 +365,7 @@ export async function handleExportPrivateKey( return; } - // 12e. Stage 2: Combine server share + reconstructed user share → original seed + // 11e. Stage 2: Combine server share + reconstructed user share → original seed const serverSeedPoint = { x: [...serverSeedShareRes.data.x.toUint8Array()], y: [...serverSeedShareRes.data.y.toUint8Array()], @@ -389,15 +379,15 @@ export async function handleExportPrivateKey( 2, ); - // 12f. Build ed25519 keypair: seed[32] || pubkey[32] → bs58 + // 11f. Build ed25519 keypair: seed[32] || pubkey[32] → bs58 const seedBytes = Uint8Array.from(recoveredSeed); - const pubkeyBytes = hexToUint8Array(signInResp.user.public_key_ed25519); + const pubkeyBytes = hexToUint8Array(ed25519PubKey); const keypairBytes = new Uint8Array(seedBytes.length + pubkeyBytes.length); keypairBytes.set(seedBytes, 0); keypairBytes.set(pubkeyBytes, seedBytes.length); const ed25519Keypair = bs58.encode(keypairBytes); - // 13. Return result + // 12. Return result console.log(`${LOG_PREFIX} export complete`); sendAck({ success: true, From d5626b403d494f055dad842a76568317f4c34f75 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 16:17:38 +0900 Subject: [PATCH 46/62] oko_attached: add reshare support to export handler --- .../src/window_msgs/export_private_key.ts | 135 +++++++++++++++++- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index 55d365909..f273e3ef3 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -7,6 +7,8 @@ import type { } from "@oko-wallet/oko-types/user"; import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; +import { Bytes } from "@oko-wallet/bytes"; +import type { PublicKeyPackageRaw } from "@oko-wallet/oko-types/teddsa"; import type { MsgEventContext } from "./types"; import { OKO_SDK_TARGET } from "./target"; @@ -21,7 +23,10 @@ import { } from "@oko-wallet-attached/crypto/commit_reveal"; import { decodeSecp256k1SharesByNode } from "@oko-wallet-attached/crypto/key_share_utils"; import { combineUserShares } from "@oko-wallet-attached/crypto/combine"; -import { convertSeedShares } from "@oko-wallet-attached/crypto/reshare_v2"; +import { + convertSeedShares, + reshareUserKeySharesV2, +} from "@oko-wallet-attached/crypto/reshare_v2"; import { SEED_ID_CLIENT, hexToSeedSharePoint, @@ -55,6 +60,32 @@ interface OkoWalletMsgExportPrivateKeyAck { const REAUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes const LOG_PREFIX = "[attached][export]"; +/** + * Extract server verifying share from hex-encoded PublicKeyPackageRaw. + * verifying_shares layout: [0]=client, [1]=server. + */ +function extractServerVerifyingShare(publicKeyPackageHex: string) { + try { + const jsonStr = new TextDecoder().decode( + hexToUint8Array(publicKeyPackageHex), + ); + const pkg: PublicKeyPackageRaw = JSON.parse(jsonStr); + const serverEntry = pkg.verifying_shares[1]; + if (!serverEntry) { + return { + success: false as const, + err: "server verifying share not found in publicKeyPackage", + }; + } + return Bytes.fromUint8Array(Uint8Array.from(serverEntry.share), 32); + } catch (err) { + return { + success: false as const, + err: `publicKeyPackage parse: ${err instanceof Error ? err.message : String(err)}`, + }; + } +} + export async function handleExportPrivateKey( ctx: MsgEventContext, payload?: { auth_type: AuthType } | null, @@ -175,15 +206,105 @@ export async function handleExportPrivateKey( return; } - // Guard: needs reshare - if ("needs_reshare" in checkData && checkData.needs_reshare) { - sendAck({ success: false, error: { type: "RESHARE_REQUIRED" } }); - return; - } - const { keyshare_node_meta } = checkData; const { threshold, nodes } = keyshare_node_meta; + // Handle reshare if needed (KSN node changes since last sign-in) + if ("needs_reshare" in checkData && checkData.needs_reshare) { + console.log( + `${LOG_PREFIX} needs_reshare detected, performing reshare before export`, + ); + + // Extract server verifying share from appState publicKeyPackage + const serverVerifyingShareRes = extractServerVerifyingShare( + ed25519Wallet.publicKeyPackage, + ); + if (!serverVerifyingShareRes.success) { + sendAck({ + success: false, + error: { + type: "COMBINE_ERROR", + error: `server verifying share: ${serverVerifyingShareRes.err}`, + }, + }); + return; + } + + // Parse public keys as Bytes for reshare + const secp256k1PubKeyBytes = Bytes.fromHexString(secp256k1PubKey, 33); + if (!secp256k1PubKeyBytes.success) { + sendAck({ + success: false, + error: { + type: "COMBINE_ERROR", + error: `secp256k1 pubkey parse: ${secp256k1PubKeyBytes.err}`, + }, + }); + return; + } + const ed25519PubKeyBytes = Bytes.fromHexString(ed25519PubKey, 32); + if (!ed25519PubKeyBytes.success) { + sendAck({ + success: false, + error: { + type: "COMBINE_ERROR", + error: `ed25519 pubkey parse: ${ed25519PubKeyBytes.err}`, + }, + }); + return; + } + + // Reshare session: commit ALL nodes with "export_with_reshare" + // (separate session from the subsequent export — KSN final API = "reshare") + const reshareKsnTargets: KsnCommitTarget[] = nodes.map((node) => ({ + nodeUrl: node.endpoint, + operationType: "export_with_reshare" as const, + })); + const reshareCommitRes = await commitAll( + "export_with_reshare", + creds.authType, + creds.idToken, + reshareKsnTargets, + nodes.length, // ALL nodes must commit for reshare + ); + if (!reshareCommitRes.success) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `reshare commit failed: ${reshareCommitRes.err}`, + }, + }); + return; + } + + // Perform reshare (get existing shares → expand → send to all nodes) + const reshareRes = await reshareUserKeySharesV2( + creds.idToken, + creds.authType, + keyshare_node_meta, + { publicKey: secp256k1PubKeyBytes.data }, + { + publicKey: ed25519PubKeyBytes.data, + serverVerifyingShare: serverVerifyingShareRes.data, + }, + reshareCommitRes.data.session, + ); + if (!reshareRes.success) { + sendAck({ + success: false, + error: { + type: "API_ERROR", + error: `reshare failed: ${reshareRes.err}`, + }, + }); + return; + } + + console.log(`${LOG_PREFIX} reshare complete, proceeding to export`); + // Fall through to normal export flow below + } + // 7. Commit to KSN nodes + oko_api with "export" operation type const ksnCommitTargets: KsnCommitTarget[] = nodes.map((node) => ({ nodeUrl: node.endpoint, From 5e96c141f87e031fc436e755ad804708ce7a7549 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 16:51:15 +0900 Subject: [PATCH 47/62] oko_attached: refactor export private key --- .../src/app/export_private_key/page.tsx | 9 +++++-- .../src/window_msgs/export_private_key.ts | 26 +++++++++++++++++-- .../src/window_msgs/oauth_info_pass/index.ts | 4 ++- .../src/openapi/schema/commit_reveal.ts | 2 ++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 4722fdb11..cfb2df6db 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -357,6 +357,8 @@ function getExportErrorDescription(errorType: string): string { return "User not found."; case "ED25519_KEYGEN_REQUIRED": return "Ed25519 key generation required. Please try signing in first."; + case "NODES_BELOW_THRESHOLD": + return "Service temporarily unavailable. Please try again later."; default: return "Please try again."; } @@ -391,6 +393,7 @@ export default function Page() { } let popup: Window | null = null; + let reauthHandler: ((event: MessageEvent) => void) | null = null; try { setIsLoading(true); @@ -421,7 +424,7 @@ export default function Page() { // 3. Listen for re-auth completion signal from iframe let reauthReceived = false; - const reauthHandler = (event: MessageEvent) => { + reauthHandler = (event: MessageEvent) => { if (event.origin !== attachedOrigin) { return; } @@ -460,7 +463,6 @@ export default function Page() { popupClosePromise, timeoutPromise, ]); - window.removeEventListener("message", reauthHandler); popup?.close(); // 5. Parse result @@ -509,6 +511,9 @@ export default function Page() { description, }); } finally { + if (reauthHandler) { + window.removeEventListener("message", reauthHandler); + } setIsLoading(false); } }, [okoWallet, authType]); diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index f273e3ef3..b7dd6abcf 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -32,6 +32,7 @@ import { hexToSeedSharePoint, hexToUint8Array, } from "@oko-wallet-attached/crypto/keygen_ed25519"; +import { getServerFrostIdentifier } from "@oko-wallet-attached/crypto/sss_ed25519"; import { setReAuthResolver } from "./export_reauth_state"; import { checkUserExistsV2 } from "./oauth_info_pass/handlers/check_user"; @@ -41,6 +42,7 @@ type ExportPrivateKeyError = | { type: "NOT_AUTHENTICATED" } | { type: "USER_NOT_FOUND" } | { type: "ED25519_KEYGEN_REQUIRED" } + | { type: "NODES_BELOW_THRESHOLD" } | { type: "RESHARE_REQUIRED" } | { type: "COMBINE_ERROR"; error: string } | { type: "API_ERROR"; error: string } @@ -66,11 +68,22 @@ const LOG_PREFIX = "[attached][export]"; */ function extractServerVerifyingShare(publicKeyPackageHex: string) { try { + const serverIdRes = getServerFrostIdentifier(); + if (!serverIdRes.success) { + return { + success: false as const, + err: `server identifier: ${serverIdRes.err}`, + }; + } + const serverIdentifierHex = serverIdRes.data.toHex(); + const jsonStr = new TextDecoder().decode( hexToUint8Array(publicKeyPackageHex), ); const pkg: PublicKeyPackageRaw = JSON.parse(jsonStr); - const serverEntry = pkg.verifying_shares[1]; + const serverEntry = pkg.verifying_shares.find( + (entry) => entry.identifier === serverIdentifierHex, + ); if (!serverEntry) { return { success: false as const, @@ -167,7 +180,7 @@ export async function handleExportPrivateKey( // 5a. Notify parent (UD) that re-auth completed — popup close is now expected window.parent.postMessage( - { target: "oko_sdk", msg_type: "__export_reauth_received__" }, + { target: "oko_user_dashboard", msg_type: "__export_reauth_received__" }, hostOrigin, ); @@ -194,6 +207,15 @@ export async function handleExportPrivateKey( const checkData = checkRes.data.data; + // Guard: active nodes below threshold + if (checkData.active_nodes_below_threshold) { + sendAck({ + success: false, + error: { type: "NODES_BELOW_THRESHOLD" }, + }); + return; + } + // Guard: user not found if (!checkData.exists) { sendAck({ success: false, error: { type: "USER_NOT_FOUND" } }); diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts index c18a42965..0cb25d12b 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts @@ -265,7 +265,9 @@ export async function handleOAuthInfoPassV2( // Re-auth interceptor: if an export request is waiting for re-auth credentials, // extract OAuth credentials and resolve the pending promise. - // Skips api_key/hostOriginList checks (re-auth uses attached origin with no SDK API key). + // Fires before api_key/hostOriginList checks (re-auth uses attached origin with no SDK API key). + // Mutual exclusion: only one resolver can be active at a time (module-level singleton in + // export_reauth_state.ts), so a normal sign-in callback cannot be intercepted during export. if (hasActiveReAuthResolver()) { const authType: AuthType = message.payload.auth_type; const validateOauthRes = await getCredentialsFromPayload( diff --git a/key_share_node/server/src/openapi/schema/commit_reveal.ts b/key_share_node/server/src/openapi/schema/commit_reveal.ts index dd85de63e..93c954fda 100644 --- a/key_share_node/server/src/openapi/schema/commit_reveal.ts +++ b/key_share_node/server/src/openapi/schema/commit_reveal.ts @@ -9,6 +9,8 @@ export const operationTypeSchema = z "reshare", "add_ed25519", "add_ed25519_with_reshare", + "export", + "export_with_reshare", ]) .describe("Operation type for commit-reveal session"); From 8041eff4bb124de1a65a0a860f2d8e93ba985504 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 18:03:11 +0900 Subject: [PATCH 48/62] oko_attached: store seedEd25519 in appState during sign-in/keygen/reshare flows --- .../oko_attached/src/crypto/keygen_ed25519.ts | 2 ++ embed/oko_attached/src/crypto/reshare_v2.ts | 8 ++++---- embed/oko_attached/src/store/app.ts | 20 +++++++++++++++++++ .../handlers/ed25519_keygen.ts | 4 ++++ .../oauth_info_pass/handlers/existing_user.ts | 19 +++++++++++++++++- .../oauth_info_pass/handlers/new_user.ts | 2 ++ .../oauth_info_pass/handlers/reshare.ts | 1 + .../src/window_msgs/oauth_info_pass/index.ts | 6 ++++++ embed/oko_attached/src/window_msgs/types.ts | 2 ++ 9 files changed, 59 insertions(+), 5 deletions(-) diff --git a/embed/oko_attached/src/crypto/keygen_ed25519.ts b/embed/oko_attached/src/crypto/keygen_ed25519.ts index babba5e89..77c258606 100644 --- a/embed/oko_attached/src/crypto/keygen_ed25519.ts +++ b/embed/oko_attached/src/crypto/keygen_ed25519.ts @@ -185,6 +185,7 @@ export interface Ed25519KeygenSplitResult { userKeyShares: TeddsaKeyShareByNode[]; serverSeedShare: SeedSharePoint; ksnSeedShares: SeedShareByNode[]; + userSeedEd25519: number[]; } /** @@ -342,6 +343,7 @@ export async function runEd25519KeygenAndSplit( userKeyShares: splitRes.data, serverSeedShare: serverSeedShareRes.data, ksnSeedShares, + userSeedEd25519: [...userSeedY], }, }; } diff --git a/embed/oko_attached/src/crypto/reshare_v2.ts b/embed/oko_attached/src/crypto/reshare_v2.ts index f642eae11..5cc7cb9a3 100644 --- a/embed/oko_attached/src/crypto/reshare_v2.ts +++ b/embed/oko_attached/src/crypto/reshare_v2.ts @@ -42,10 +42,7 @@ import { getServerFrostIdentifier, } from "./sss_ed25519"; import { computeVerifyingShare } from "./scalar"; -import { - hexToSeedSharePoint, - seedShareToHex, -} from "./keygen_ed25519"; +import { hexToSeedSharePoint, seedShareToHex } from "./keygen_ed25519"; /** * Convert V2 API response to secp256k1 UserKeySharePointByNode format. @@ -141,6 +138,7 @@ export interface ReshareV2Result { keyshare1Secp256k1: string; // hex string keyPackageEd25519: string; // hex-encoded KeyPackageRaw JSON publicKeyPackageEd25519: string; // hex-encoded PublicKeyPackageRaw JSON + seedEd25519: number[]; // combined ed25519 user seed share } export async function reshareUserKeySharesV2( @@ -231,6 +229,7 @@ export async function reshareUserKeySharesV2( } const seedResult = { resharedShares: seedExpandRes.data.reshared_user_key_shares, + originalSecret: [...seedExpandRes.data.original_secret.toUint8Array()], }; // 5. Send new shares to ALL nodes @@ -347,6 +346,7 @@ export async function reshareUserKeySharesV2( keyshare1Secp256k1: secp256k1Result.originalSecret, keyPackageEd25519: keyPackageRes.data.keyPackageEd25519, publicKeyPackageEd25519: keyPackageRes.data.publicKeyPackageEd25519, + seedEd25519: seedResult.originalSecret, }, }; } diff --git a/embed/oko_attached/src/store/app.ts b/embed/oko_attached/src/store/app.ts index 3fb1e2f94..a431cf0d9 100644 --- a/embed/oko_attached/src/store/app.ts +++ b/embed/oko_attached/src/store/app.ts @@ -29,6 +29,8 @@ interface PerOriginState { keyshare_1: string | null; /** hex-encoded KeyPackageRaw JSON (contains signing_share for ed25519) */ keyPackageEd25519: string | null; + /** JSON-encoded number[] — combined ed25519 user seed share */ + seedEd25519: string | null; nonce: string | null; codeVerifier: string | null; authToken: string | null; @@ -65,6 +67,9 @@ interface AppActions { getKeyPackageEd25519: (hostOrigin: string) => string | null; setKeyPackageEd25519: (hostOrigin: string, keyPackage: string | null) => void; + getSeedEd25519: (hostOrigin: string) => string | null; + setSeedEd25519: (hostOrigin: string, seedEd25519: string | null) => void; + getApiKey: (hostOrigin: string) => string | null; setApiKey: (hostOrigin: string, apiKey: string | null) => void; @@ -160,6 +165,7 @@ export const useAppState = create( apiKey: null, keyshare_1: null, keyPackageEd25519: null, + seedEd25519: null, nonce: null, codeVerifier: null, authToken: null, @@ -225,6 +231,20 @@ export const useAppState = create( getKeyPackageEd25519: (hostOrigin: string) => { return get().perOrigin[hostOrigin]?.keyPackageEd25519; }, + setSeedEd25519: (hostOrigin: string, seedEd25519: string | null) => { + set({ + perOrigin: { + ...get().perOrigin, + [hostOrigin]: { + ...get().perOrigin[hostOrigin], + seedEd25519, + }, + }, + }); + }, + getSeedEd25519: (hostOrigin: string) => { + return get().perOrigin[hostOrigin]?.seedEd25519; + }, getApiKey: (hostOrigin: string) => { return get().perOrigin[hostOrigin]?.apiKey; }, diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/ed25519_keygen.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/ed25519_keygen.ts index efbb13160..59f10b8b7 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/ed25519_keygen.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/ed25519_keygen.ts @@ -66,6 +66,7 @@ export async function handleExistingUserNeedsEd25519Keygen( userKeyShares: ed25519UserKeyShares, serverSeedShare: ed25519ServerSeedShare, ksnSeedShares: ed25519KsnSeedShares, + userSeedEd25519, } = ed25519KeygenSplitRes.data; // 2. Commit to oko_api and ks nodes @@ -259,6 +260,7 @@ export async function handleExistingUserNeedsEd25519Keygen( keyshare1Secp256k1, keyPackageEd25519: keyPackageEd25519Hex.keyPackage, publicKeyPackageEd25519: keyPackageEd25519Hex.publicKeyPackage, + seedEd25519: userSeedEd25519, isNewUser: false, email: reqKeygenEd25519Res.data.user.email ?? null, name: reqKeygenEd25519Res.data.user.name ?? null, @@ -306,6 +308,7 @@ export async function handleReshareAndEd25519Keygen( userKeyShares: ed25519UserKeyShares, serverSeedShare: ed25519ServerSeedShare, ksnSeedShares: ed25519KsnSeedShares, + userSeedEd25519, } = ed25519KeygenSplitRes.data; // 3. Commit to oko_api and ks nodes with "add_ed25519_with_reshare" operation type @@ -605,6 +608,7 @@ export async function handleReshareAndEd25519Keygen( keyshare1Secp256k1: secp256k1ExpandRes.data.original_secret.toHex(), keyPackageEd25519: keyPackageEd25519Hex.keyPackage, publicKeyPackageEd25519: keyPackageEd25519Hex.publicKeyPackage, + seedEd25519: userSeedEd25519, isNewUser: false, email: reqKeygenEd25519Res.data.user.email ?? null, name: reqKeygenEd25519Res.data.user.name ?? null, diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts index 7913f0d54..ab807e6b2 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts @@ -8,9 +8,14 @@ import { signInV2, reportKeyShareNotFound, } from "@oko-wallet-attached/requests/oko_api"; +import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; + import { combineUserShares } from "@oko-wallet-attached/crypto/combine"; import type { UserSignInResultV2 } from "@oko-wallet-attached/window_msgs/types"; -import { buildKeyPackageResult } from "@oko-wallet-attached/crypto/reshare_v2"; +import { + buildKeyPackageResult, + convertSeedShares, +} from "@oko-wallet-attached/crypto/reshare_v2"; import { requestKeySharesWithBackup } from "@oko-wallet-attached/requests/ks_node_v2"; import { commitAll, @@ -188,6 +193,17 @@ export async function handleExistingUserV2( } const keyshare1Secp256k1 = keyshare1Secp256k1Res.data; + // 7b. Combine ed25519 seed shares → user_seed_Y (for export) + const ksnSeedShares = convertSeedShares(keySharesByNode); + const ksnSeedPoints = ksnSeedShares.map((s) => ({ + x: [...s.share.x.toUint8Array()], + y: [...s.share.y.toUint8Array()], + })); + const seedEd25519: number[] = secp256k1Wasm.seed_sss_combine( + ksnSeedPoints, + threshold, + ); + // 8. Build KeyPackage and PublicKeyPackage const keyPackageRes = buildKeyPackageResult({ signingShare, @@ -225,6 +241,7 @@ export async function handleExistingUserV2( keyshare1Secp256k1, keyPackageEd25519: keyPackageRes.data.keyPackageEd25519, publicKeyPackageEd25519: keyPackageRes.data.publicKeyPackageEd25519, + seedEd25519, isNewUser: false, email: signInResp.user.email ?? null, name: signInResp.user.name ?? null, diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/new_user.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/new_user.ts index 6e1e35a6a..926c84d16 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/new_user.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/new_user.ts @@ -77,6 +77,7 @@ export async function handleNewUserV2( userKeyShares: ed25519UserKeyShares, serverSeedShare: ed25519ServerSeedShare, ksnSeedShares: ed25519KsnSeedShares, + userSeedEd25519, } = ed25519KeygenSplitRes.data; // 4. Commit to oko_api and ks nodes @@ -225,6 +226,7 @@ export async function handleNewUserV2( keyshare1Secp256k1: secp256k1Keygen1.tss_private_share.toHex(), keyPackageEd25519: keyPackageEd25519Hex.keyPackage, publicKeyPackageEd25519: keyPackageEd25519Hex.publicKeyPackage, + seedEd25519: userSeedEd25519, isNewUser: true, email: reqKeygenV2Res.data.user.email ?? null, name: reqKeygenV2Res.data.user.name ?? null, diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/reshare.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/reshare.ts index 0ce057c8c..0398e705f 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/reshare.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/reshare.ts @@ -144,6 +144,7 @@ export async function handleReshareV2( keyshare1Secp256k1: reshareRes.data.keyshare1Secp256k1, keyPackageEd25519: reshareRes.data.keyPackageEd25519, publicKeyPackageEd25519: reshareRes.data.publicKeyPackageEd25519, + seedEd25519: reshareRes.data.seedEd25519, isNewUser: false, email: signInResp.user.email ?? null, name: signInResp.user.name ?? null, diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts index 0cb25d12b..e2afd72fe 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/index.ts @@ -366,6 +366,12 @@ export async function handleOAuthInfoPassV2( // Store ed25519 key package (signing share) separately appState.setKeyPackageEd25519(hostOrigin, signInResult.keyPackageEd25519); + // Store combined ed25519 user seed share for export + appState.setSeedEd25519( + hostOrigin, + JSON.stringify(signInResult.seedEd25519), + ); + // Store ed25519 wallet info (without signing share) appState.setWalletEd25519(hostOrigin, { authType, diff --git a/embed/oko_attached/src/window_msgs/types.ts b/embed/oko_attached/src/window_msgs/types.ts index ee03f4313..1faa4a7c9 100644 --- a/embed/oko_attached/src/window_msgs/types.ts +++ b/embed/oko_attached/src/window_msgs/types.ts @@ -83,6 +83,8 @@ export interface UserSignInResultV2 { keyPackageEd25519: string; /** hex-encoded PublicKeyPackageRaw JSON for ed25519 */ publicKeyPackageEd25519: string; + /** combined ed25519 user seed share as number[] */ + seedEd25519: number[]; isNewUser: boolean; name: string | null; email: string | null; From ffec2b57e0d0a278cdf8d678724f8c047c235a00 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 20:38:40 +0900 Subject: [PATCH 49/62] o --- .../src/window_msgs/oauth_info_pass/handlers/existing_user.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts index ab807e6b2..2a25f8677 100644 --- a/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts +++ b/embed/oko_attached/src/window_msgs/oauth_info_pass/handlers/existing_user.ts @@ -3,13 +3,12 @@ import type { KeyShareNodeMetaWithNodeStatusInfo } from "@oko-wallet/oko-types/t import type { Result } from "@oko-wallet/stdlib-js"; import { type OAuthSignInError } from "@oko-wallet/oko-sdk-core"; import { Bytes } from "@oko-wallet/bytes"; +import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; import { signInV2, reportKeyShareNotFound, } from "@oko-wallet-attached/requests/oko_api"; -import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; - import { combineUserShares } from "@oko-wallet-attached/crypto/combine"; import type { UserSignInResultV2 } from "@oko-wallet-attached/window_msgs/types"; import { From 1135a265e05b958e8867f408bd71c7b545394ad1 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 20:47:15 +0900 Subject: [PATCH 50/62] oko_attached: refactor export private key --- .../server/src/routes/tss_v2/export_shares.ts | 2 +- .../oko_api/server/src/routes/tss_v2/index.ts | 1 - backend/openapi/src/tss/user.ts | 19 +- common/oko_types/src/user/index.ts | 2 - .../src/window_msgs/export_private_key.ts | 364 ++---------------- 5 files changed, 31 insertions(+), 357 deletions(-) diff --git a/backend/oko_api/server/src/routes/tss_v2/export_shares.ts b/backend/oko_api/server/src/routes/tss_v2/export_shares.ts index 4ffb62bdf..4b9dce80b 100644 --- a/backend/oko_api/server/src/routes/tss_v2/export_shares.ts +++ b/backend/oko_api/server/src/routes/tss_v2/export_shares.ts @@ -26,7 +26,7 @@ registry.registerPath({ tags: ["TSS"], summary: "Export server shares for wallet export", description: - "Exports the server's secp256k1 TSS share and ed25519 seed_share. Requires dual authentication: JWT (body.first_login_jwt) + OAuth re-authentication (Authorization Bearer id_token). Protected by commit-reveal middleware.", + "Exports the server's secp256k1 TSS share and ed25519 seed_share. Requires dual authentication: JWT (body.first_login_jwt) + OAuth re-authentication (Authorization Bearer id_token).", security: [{ userAuth: [] }], request: { headers: OAuthHeaderSchema, diff --git a/backend/oko_api/server/src/routes/tss_v2/index.ts b/backend/oko_api/server/src/routes/tss_v2/index.ts index ae5c214bd..02419c253 100644 --- a/backend/oko_api/server/src/routes/tss_v2/index.ts +++ b/backend/oko_api/server/src/routes/tss_v2/index.ts @@ -198,7 +198,6 @@ export function makeTSSRouterV2() { "/export_shares", userJwtFromBodyMiddleware, oauthMiddleware, - commitRevealMiddleware("export_shares"), tssActivateMiddleware, exportShares, ); diff --git a/backend/openapi/src/tss/user.ts b/backend/openapi/src/tss/user.ts index 8ff498f86..381eabdcd 100644 --- a/backend/openapi/src/tss/user.ts +++ b/backend/openapi/src/tss/user.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { registry } from "../registry"; -import { CommitRevealRequestFieldsSchema } from "./commit_reveal"; const KsNodeStatusEnum = z.enum(["ACTIVE", "INACTIVE"]); const WalletStatusEnum = z.enum([ @@ -292,16 +291,14 @@ export const CheckEmailSuccessResponseV2Schema = registry.register( export const ExportSharesRequestSchema = registry.register( "TssExportSharesRequest", - z - .object({ - first_login_jwt: z.string().openapi({ - description: "JWT from the first login session", - }), - auth_type: AuthTypeEnum.openapi({ - description: "Authentication provider type for re-authentication", - }), - }) - .merge(CommitRevealRequestFieldsSchema), + z.object({ + first_login_jwt: z.string().openapi({ + description: "JWT from the first login session", + }), + auth_type: AuthTypeEnum.openapi({ + description: "Authentication provider type for re-authentication", + }), + }), ); const ExportSharesDataSchema = registry.register( diff --git a/common/oko_types/src/user/index.ts b/common/oko_types/src/user/index.ts index b01af59d3..a2cc92e26 100644 --- a/common/oko_types/src/user/index.ts +++ b/common/oko_types/src/user/index.ts @@ -97,8 +97,6 @@ export interface SignInResponseV2 { export interface ExportSharesRequest { first_login_jwt: string; auth_type: AuthType; - cr_session_id: string; - cr_signature: string; } export interface ExportSharesResponse { diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index b7dd6abcf..cf7db9887 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -7,43 +7,26 @@ import type { } from "@oko-wallet/oko-types/user"; import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; -import { Bytes } from "@oko-wallet/bytes"; -import type { PublicKeyPackageRaw } from "@oko-wallet/oko-types/teddsa"; import type { MsgEventContext } from "./types"; import { OKO_SDK_TARGET } from "./target"; import { useAppState } from "@oko-wallet-attached/store/app"; import { USER_DASHBOARD_ORIGINS } from "@oko-wallet-attached/requests/endpoints"; import { TSS_V2_ENDPOINT } from "@oko-wallet-attached/requests/oko_api"; -import { requestKeySharesWithBackup } from "@oko-wallet-attached/requests/ks_node_v2"; -import { - commitAll, - createOkoApiCommitRevealParams, - type KsnCommitTarget, -} from "@oko-wallet-attached/crypto/commit_reveal"; -import { decodeSecp256k1SharesByNode } from "@oko-wallet-attached/crypto/key_share_utils"; -import { combineUserShares } from "@oko-wallet-attached/crypto/combine"; -import { - convertSeedShares, - reshareUserKeySharesV2, -} from "@oko-wallet-attached/crypto/reshare_v2"; import { SEED_ID_CLIENT, hexToSeedSharePoint, hexToUint8Array, } from "@oko-wallet-attached/crypto/keygen_ed25519"; -import { getServerFrostIdentifier } from "@oko-wallet-attached/crypto/sss_ed25519"; - -import { setReAuthResolver } from "./export_reauth_state"; -import { checkUserExistsV2 } from "./oauth_info_pass/handlers/check_user"; +import { + setReAuthResolver, + type ReAuthCredentials, +} from "./export_reauth_state"; type ExportPrivateKeyError = | { type: "UNAUTHORIZED_ORIGIN" } | { type: "NOT_AUTHENTICATED" } - | { type: "USER_NOT_FOUND" } - | { type: "ED25519_KEYGEN_REQUIRED" } - | { type: "NODES_BELOW_THRESHOLD" } - | { type: "RESHARE_REQUIRED" } + | { type: "MISSING_KEY_SHARES" } | { type: "COMBINE_ERROR"; error: string } | { type: "API_ERROR"; error: string } | { type: "REAUTH_TIMEOUT" } @@ -62,43 +45,6 @@ interface OkoWalletMsgExportPrivateKeyAck { const REAUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes const LOG_PREFIX = "[attached][export]"; -/** - * Extract server verifying share from hex-encoded PublicKeyPackageRaw. - * verifying_shares layout: [0]=client, [1]=server. - */ -function extractServerVerifyingShare(publicKeyPackageHex: string) { - try { - const serverIdRes = getServerFrostIdentifier(); - if (!serverIdRes.success) { - return { - success: false as const, - err: `server identifier: ${serverIdRes.err}`, - }; - } - const serverIdentifierHex = serverIdRes.data.toHex(); - - const jsonStr = new TextDecoder().decode( - hexToUint8Array(publicKeyPackageHex), - ); - const pkg: PublicKeyPackageRaw = JSON.parse(jsonStr); - const serverEntry = pkg.verifying_shares.find( - (entry) => entry.identifier === serverIdentifierHex, - ); - if (!serverEntry) { - return { - success: false as const, - err: "server verifying share not found in publicKeyPackage", - }; - } - return Bytes.fromUint8Array(Uint8Array.from(serverEntry.share), 32); - } catch (err) { - return { - success: false as const, - err: `publicKeyPackage parse: ${err instanceof Error ? err.message : String(err)}`, - }; - } -} - export async function handleExportPrivateKey( ctx: MsgEventContext, payload?: { auth_type: AuthType } | null, @@ -124,7 +70,8 @@ export async function handleExportPrivateKey( } // 2. Auth token validation (first login JWT — will be sent to export_shares as body param) - const firstLoginJwt = useAppState.getState().getAuthToken(hostOrigin); + const appState = useAppState.getState(); + const firstLoginJwt = appState.getAuthToken(hostOrigin); if (!firstLoginJwt) { sendAck({ success: false, error: { type: "NOT_AUTHENTICATED" } }); return; @@ -139,24 +86,19 @@ export async function handleExportPrivateKey( return; } - // 4. Get public keys from appState (no signInV2 needed) - const wallet = useAppState.getState().getWallet(hostOrigin); - const ed25519Wallet = useAppState.getState().getWalletEd25519(hostOrigin); - if (!wallet?.publicKey || !ed25519Wallet?.publicKey) { - sendAck({ - success: false, - error: { - type: "API_ERROR", - error: "Wallet public keys not found in appState", - }, - }); + // 4. Get client shares from appState (stored during sign-in/keygen) + const keyshare1 = appState.getKeyshare_1(hostOrigin); + const seedEd25519Str = appState.getSeedEd25519(hostOrigin); + const ed25519Wallet = appState.getWalletEd25519(hostOrigin); + if (!keyshare1 || !seedEd25519Str || !ed25519Wallet?.publicKey) { + sendAck({ success: false, error: { type: "MISSING_KEY_SHARES" } }); return; } - const secp256k1PubKey = wallet.publicKey; + const seedEd25519: number[] = JSON.parse(seedEd25519Str); const ed25519PubKey = ed25519Wallet.publicKey; // 5. Wait for re-auth credentials (popup → OAuth callback → interceptor) - let creds; + let creds: ReAuthCredentials; try { const reAuthPromise = setReAuthResolver(); const timeoutPromise = new Promise((_, reject) => { @@ -185,219 +127,11 @@ export async function handleExportPrivateKey( ); try { - // 6. Check user exists - const checkRes = await checkUserExistsV2( - creds.userIdentifier, - creds.authType, - ); - if (!checkRes.success) { - sendAck({ - success: false, - error: { type: "API_ERROR", error: "Failed to check user" }, - }); - return; - } - if (!checkRes.data.success) { - sendAck({ - success: false, - error: { type: "API_ERROR", error: checkRes.data.msg }, - }); - return; - } - - const checkData = checkRes.data.data; - - // Guard: active nodes below threshold - if (checkData.active_nodes_below_threshold) { - sendAck({ - success: false, - error: { type: "NODES_BELOW_THRESHOLD" }, - }); - return; - } - - // Guard: user not found - if (!checkData.exists) { - sendAck({ success: false, error: { type: "USER_NOT_FOUND" } }); - return; - } - - // Guard: needs ed25519 keygen (can't export both keys) - if ("needs_keygen_ed25519" in checkData && checkData.needs_keygen_ed25519) { - sendAck({ success: false, error: { type: "ED25519_KEYGEN_REQUIRED" } }); - return; - } - - const { keyshare_node_meta } = checkData; - const { threshold, nodes } = keyshare_node_meta; - - // Handle reshare if needed (KSN node changes since last sign-in) - if ("needs_reshare" in checkData && checkData.needs_reshare) { - console.log( - `${LOG_PREFIX} needs_reshare detected, performing reshare before export`, - ); - - // Extract server verifying share from appState publicKeyPackage - const serverVerifyingShareRes = extractServerVerifyingShare( - ed25519Wallet.publicKeyPackage, - ); - if (!serverVerifyingShareRes.success) { - sendAck({ - success: false, - error: { - type: "COMBINE_ERROR", - error: `server verifying share: ${serverVerifyingShareRes.err}`, - }, - }); - return; - } - - // Parse public keys as Bytes for reshare - const secp256k1PubKeyBytes = Bytes.fromHexString(secp256k1PubKey, 33); - if (!secp256k1PubKeyBytes.success) { - sendAck({ - success: false, - error: { - type: "COMBINE_ERROR", - error: `secp256k1 pubkey parse: ${secp256k1PubKeyBytes.err}`, - }, - }); - return; - } - const ed25519PubKeyBytes = Bytes.fromHexString(ed25519PubKey, 32); - if (!ed25519PubKeyBytes.success) { - sendAck({ - success: false, - error: { - type: "COMBINE_ERROR", - error: `ed25519 pubkey parse: ${ed25519PubKeyBytes.err}`, - }, - }); - return; - } - - // Reshare session: commit ALL nodes with "export_with_reshare" - // (separate session from the subsequent export — KSN final API = "reshare") - const reshareKsnTargets: KsnCommitTarget[] = nodes.map((node) => ({ - nodeUrl: node.endpoint, - operationType: "export_with_reshare" as const, - })); - const reshareCommitRes = await commitAll( - "export_with_reshare", - creds.authType, - creds.idToken, - reshareKsnTargets, - nodes.length, // ALL nodes must commit for reshare - ); - if (!reshareCommitRes.success) { - sendAck({ - success: false, - error: { - type: "API_ERROR", - error: `reshare commit failed: ${reshareCommitRes.err}`, - }, - }); - return; - } - - // Perform reshare (get existing shares → expand → send to all nodes) - const reshareRes = await reshareUserKeySharesV2( - creds.idToken, - creds.authType, - keyshare_node_meta, - { publicKey: secp256k1PubKeyBytes.data }, - { - publicKey: ed25519PubKeyBytes.data, - serverVerifyingShare: serverVerifyingShareRes.data, - }, - reshareCommitRes.data.session, - ); - if (!reshareRes.success) { - sendAck({ - success: false, - error: { - type: "API_ERROR", - error: `reshare failed: ${reshareRes.err}`, - }, - }); - return; - } - - console.log(`${LOG_PREFIX} reshare complete, proceeding to export`); - // Fall through to normal export flow below - } - - // 7. Commit to KSN nodes + oko_api with "export" operation type - const ksnCommitTargets: KsnCommitTarget[] = nodes.map((node) => ({ - nodeUrl: node.endpoint, - operationType: "export" as const, - })); - const commitRes = await commitAll( - "export", - creds.authType, - creds.idToken, - ksnCommitTargets, - threshold, - ); - if (!commitRes.success) { - sendAck({ - success: false, - error: { type: "API_ERROR", error: `commit failed: ${commitRes.err}` }, - }); - return; - } - const { session, readyNodes, pendingCommits } = commitRes.data; - - // 8. Request key shares from KSN (using appState public keys) - const requestSharesRes = await requestKeySharesWithBackup({ - idToken: creds.idToken, - authType: creds.authType, - wallets: { - secp256k1: secp256k1PubKey, - ed25519: ed25519PubKey, - }, - threshold, - session, - readyNodes, - pendingCommits, - allNodes: nodes, - }); - if (!requestSharesRes.success) { - sendAck({ - success: false, - error: { - type: "API_ERROR", - error: `insufficient shares: got ${requestSharesRes.err.got}/${requestSharesRes.err.need}`, - }, - }); - return; - } - const { shares: keySharesByNode } = requestSharesRes.data; - - // 9. Export API: get server shares - // Authorization header = re-auth id_token (for commit-reveal middleware) - // Body = first_login_jwt + auth_type + commit-reveal params - const exportCrParamsRes = createOkoApiCommitRevealParams( - session, - "export_shares", - ); - if (!exportCrParamsRes.success) { - sendAck({ - success: false, - error: { - type: "API_ERROR", - error: `commit-reveal params failed: ${exportCrParamsRes.err}`, - }, - }); - return; - } - + // 6. Export API: get server shares (no commit-reveal) const exportApiUrl = `${TSS_V2_ENDPOINT}/export_shares`; const exportBody: ExportSharesRequest = { first_login_jwt: firstLoginJwt, auth_type: creds.authType, - cr_session_id: exportCrParamsRes.data.cr_session_id, - cr_signature: exportCrParamsRes.data.cr_signature, }; const exportRes = await fetch(exportApiUrl, { method: "POST", @@ -431,69 +165,16 @@ export async function handleExportPrivateKey( } const serverShares = exportJson.data; - // --------------------------------------------------------------- - // 10. secp256k1 combine - // --------------------------------------------------------------- - - // 10a. Decode KSN secp256k1 shares → Point256 format - const secp256k1DecodeRes = - await decodeSecp256k1SharesByNode(keySharesByNode); - if (!secp256k1DecodeRes.success) { - sendAck({ - success: false, - error: { - type: "COMBINE_ERROR", - error: `secp256k1 decode: ${secp256k1DecodeRes.err.error}`, - }, - }); - return; - } - - // 10b. Combine KSN shares → user's keyshare_1 (Lagrange interpolation) - const userKeyshare1Res = await combineUserShares( - secp256k1DecodeRes.data, - threshold, - ); - if (!userKeyshare1Res.success) { - sendAck({ - success: false, - error: { - type: "COMBINE_ERROR", - error: `secp256k1 user combine: ${userKeyshare1Res.err}`, - }, - }); - return; - } - - // 10c. Combine user share (Participant 0) + server share (Participant 1) → full private key + // 7. secp256k1: combine user share (appState) + server share → full private key const fullSecp256k1Scalar = secp256k1Wasm.cli_combine_shares({ shares: { - "0": userKeyshare1Res.data, + "0": keyshare1, "1": serverShares.secp256k1_share, }, }); const secp256k1PrivateKey = `0x${fullSecp256k1Scalar}`; - // --------------------------------------------------------------- - // 11. ed25519 seed 2-stage combine - // --------------------------------------------------------------- - - // 11a. Convert KSN seed shares to UserKeySharePointByNode format - const ksnSeedShares = convertSeedShares(keySharesByNode); - - // 11b. Convert to PointNumArr for WASM - const ksnSeedPoints = ksnSeedShares.map((s) => ({ - x: [...s.share.x.toUint8Array()], - y: [...s.share.y.toUint8Array()], - })); - - // 11c. Stage 1: Combine KSN seed shares → user_seed_Y - const userSeedY: number[] = secp256k1Wasm.seed_sss_combine( - ksnSeedPoints, - threshold, - ); - - // 11d. Parse server's seed share + // 8. ed25519: combine user seed (appState) + server seed share → original seed const serverSeedShareRes = hexToSeedSharePoint( serverShares.ed25519_seed_share, ); @@ -508,21 +189,20 @@ export async function handleExportPrivateKey( return; } - // 11e. Stage 2: Combine server share + reconstructed user share → original seed const serverSeedPoint = { x: [...serverSeedShareRes.data.x.toUint8Array()], y: [...serverSeedShareRes.data.y.toUint8Array()], }; const userSeedPoint = { x: SEED_ID_CLIENT, - y: userSeedY, + y: seedEd25519, }; const recoveredSeed: number[] = secp256k1Wasm.seed_sss_combine( [serverSeedPoint, userSeedPoint], 2, ); - // 11f. Build ed25519 keypair: seed[32] || pubkey[32] → bs58 + // 8a. Build ed25519 keypair: seed[32] || pubkey[32] → bs58 const seedBytes = Uint8Array.from(recoveredSeed); const pubkeyBytes = hexToUint8Array(ed25519PubKey); const keypairBytes = new Uint8Array(seedBytes.length + pubkeyBytes.length); @@ -530,7 +210,7 @@ export async function handleExportPrivateKey( keypairBytes.set(pubkeyBytes, seedBytes.length); const ed25519Keypair = bs58.encode(keypairBytes); - // 12. Return result + // 9. Return result console.log(`${LOG_PREFIX} export complete`); sendAck({ success: true, From 181e146797d715633d1898dfe8475c3912596551 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 20:52:14 +0900 Subject: [PATCH 51/62] ks_node: remove export commit-reveal operation types --- backend/oko_api/server/src/commit_reveal/allowed_apis.ts | 4 ---- backend/openapi/src/tss/commit_reveal.ts | 2 -- common/oko_types/src/commit_reveal/index.ts | 7 ++----- key_share_node/ksn_interface/src/commit_reveal.ts | 4 +--- key_share_node/server/src/commit_reveal/allowed_apis.ts | 4 ---- key_share_node/server/src/openapi/schema/commit_reveal.ts | 2 -- 6 files changed, 3 insertions(+), 20 deletions(-) diff --git a/backend/oko_api/server/src/commit_reveal/allowed_apis.ts b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts index b177eeb59..9a20535a1 100644 --- a/backend/oko_api/server/src/commit_reveal/allowed_apis.ts +++ b/backend/oko_api/server/src/commit_reveal/allowed_apis.ts @@ -9,8 +9,6 @@ export const ALLOWED_APIS = { reshare: ["signin", "reshare"], add_ed25519: ["signin", "keygen_ed25519"], add_ed25519_with_reshare: ["signin", "keygen_ed25519", "reshare"], - export: ["export_shares"], - export_with_reshare: ["reshare", "export_shares"], }; export const FINAL_APIS = { @@ -19,8 +17,6 @@ export const FINAL_APIS = { reshare: "reshare", add_ed25519: "keygen_ed25519", add_ed25519_with_reshare: "reshare", - export: "export_shares", - export_with_reshare: "export_shares", }; export function isApiAllowed( diff --git a/backend/openapi/src/tss/commit_reveal.ts b/backend/openapi/src/tss/commit_reveal.ts index a6751b0e5..ad5a0be08 100644 --- a/backend/openapi/src/tss/commit_reveal.ts +++ b/backend/openapi/src/tss/commit_reveal.ts @@ -8,8 +8,6 @@ export const OperationTypeSchema = z "reshare", "add_ed25519", "add_ed25519_with_reshare", - "export", - "export_with_reshare", ]) .describe("Operation type for commit-reveal session"); diff --git a/common/oko_types/src/commit_reveal/index.ts b/common/oko_types/src/commit_reveal/index.ts index 5e350d70d..5ce3ecb83 100644 --- a/common/oko_types/src/commit_reveal/index.ts +++ b/common/oko_types/src/commit_reveal/index.ts @@ -3,16 +3,13 @@ export type OperationType = | "sign_in" | "reshare" | "add_ed25519" - | "add_ed25519_with_reshare" - | "export" - | "export_with_reshare"; + | "add_ed25519_with_reshare"; export type ApiName = | "signin" | "keygen" | "reshare" - | "keygen_ed25519" - | "export_shares"; + | "keygen_ed25519"; export type SessionState = "COMMITTED" | "COMPLETED"; diff --git a/key_share_node/ksn_interface/src/commit_reveal.ts b/key_share_node/ksn_interface/src/commit_reveal.ts index b880822ba..7cbfe42ae 100644 --- a/key_share_node/ksn_interface/src/commit_reveal.ts +++ b/key_share_node/ksn_interface/src/commit_reveal.ts @@ -3,9 +3,7 @@ export type OperationType = | "sign_in" | "reshare" | "add_ed25519" - | "add_ed25519_with_reshare" - | "export" - | "export_with_reshare"; + | "add_ed25519_with_reshare"; export type ApiName = | "get_key_shares" diff --git a/key_share_node/server/src/commit_reveal/allowed_apis.ts b/key_share_node/server/src/commit_reveal/allowed_apis.ts index f3de53935..d9b23c9f3 100644 --- a/key_share_node/server/src/commit_reveal/allowed_apis.ts +++ b/key_share_node/server/src/commit_reveal/allowed_apis.ts @@ -9,8 +9,6 @@ export const ALLOWED_APIS: Record = { reshare: ["get_key_shares", "reshare"], add_ed25519: ["register_ed25519", "get_key_shares"], add_ed25519_with_reshare: ["register_ed25519", "get_key_shares", "reshare"], - export: ["get_key_shares"], - export_with_reshare: ["get_key_shares", "reshare"], }; export const FINAL_APIS: Record = { @@ -19,8 +17,6 @@ export const FINAL_APIS: Record = { reshare: "reshare", add_ed25519: "get_key_shares", add_ed25519_with_reshare: "reshare", - export: "get_key_shares", - export_with_reshare: "reshare", }; export function isApiAllowed( diff --git a/key_share_node/server/src/openapi/schema/commit_reveal.ts b/key_share_node/server/src/openapi/schema/commit_reveal.ts index 93c954fda..dd85de63e 100644 --- a/key_share_node/server/src/openapi/schema/commit_reveal.ts +++ b/key_share_node/server/src/openapi/schema/commit_reveal.ts @@ -9,8 +9,6 @@ export const operationTypeSchema = z "reshare", "add_ed25519", "add_ed25519_with_reshare", - "export", - "export_with_reshare", ]) .describe("Operation type for commit-reveal session"); From 48d758b7218a3cc6d2719191b0bc16b23405a56d Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Tue, 24 Feb 2026 21:06:56 +0900 Subject: [PATCH 52/62] o --- embed/oko_attached/src/components/export/email_reauth.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/embed/oko_attached/src/components/export/email_reauth.tsx b/embed/oko_attached/src/components/export/email_reauth.tsx index 3bb36531e..3166770a7 100644 --- a/embed/oko_attached/src/components/export/email_reauth.tsx +++ b/embed/oko_attached/src/components/export/email_reauth.tsx @@ -58,6 +58,7 @@ export function EmailReauth() { apiKey: "reauth", targetOrigin: window.location.origin, provider: "auth0", + modalId: "reauth", }), [], ); @@ -132,7 +133,7 @@ export function EmailReauth() { setIsSubmitting(true); setErrorMessage(null); - const callbackUrl = `${window.location.origin}/email/callback`; + const callbackUrl = `${window.location.origin}/email/callback?modal_id=reauth`; console.log(`${LOG_PREFIX} verifying OTP for`, email.trim()); From b0f9cf7f1f1a1688eb52129bb40b24b8a9d4bef4 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Wed, 25 Feb 2026 11:52:15 +0900 Subject: [PATCH 53/62] o --- embed/oko_attached/src/components/export/export_reauth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embed/oko_attached/src/components/export/export_reauth.tsx b/embed/oko_attached/src/components/export/export_reauth.tsx index 5569a7ab4..741fbdca0 100644 --- a/embed/oko_attached/src/components/export/export_reauth.tsx +++ b/embed/oko_attached/src/components/export/export_reauth.tsx @@ -68,5 +68,5 @@ function OAuthRedirect({ authType }: { authType: "google" | "x" | "discord" }) { return
    Error: {error}
    ; } - return
    Redirecting to {authType} authentication...
    ; + return null; } From 5667d475beb2eb3118ffe3c1652a99ddc0c350da Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Wed, 25 Feb 2026 13:13:37 +0900 Subject: [PATCH 54/62] o --- .../src/app/export_private_key/page.tsx | 30 ++++++++++--------- .../src/components/export/email_reauth.tsx | 2 +- .../src/components/export/export_reauth.tsx | 4 +-- .../src/components/export/telegram_reauth.tsx | 2 +- .../components/export/use_export_reauth.ts | 1 - 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index cfb2df6db..506464f56 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -19,10 +19,10 @@ import { } from "@oko-wallet-user-dashboard/state/sdk"; import { useUserInfoState } from "@oko-wallet-user-dashboard/state/user_info"; -function getAuthProviderInfo(authType: AuthType | null): { +const getAuthProviderInfo = (authType: AuthType | null): { icon: ReactNode; label: string; -} { +} => { switch (authType) { case "google": return { @@ -42,7 +42,7 @@ function getAuthProviderInfo(authType: AuthType | null): { } } -function LockIcon() { +const LockIcon = () => { return ( { return ( { return ( { return ( { return ( void; -}) { +}) => { return ( <> @@ -209,7 +209,7 @@ function Step1Content({ ); } -function Step2Content({ +const Step2Content = ({ privateKeys, revealedKeys, onToggleReveal, @@ -219,7 +219,7 @@ function Step2Content({ revealedKeys: { secp256k1: boolean; ed25519: boolean }; onToggleReveal: (key: "secp256k1" | "ed25519") => void; onCopy: (key: string) => void; -}) { +}) => { return ( <> @@ -347,7 +347,7 @@ function Step2Content({ ); } -function getExportErrorDescription(errorType: string): string { +const getExportErrorDescription = (errorType: string): string => { switch (errorType) { case "REAUTH_TIMEOUT": return "Re-authentication timed out. Please try again."; @@ -364,7 +364,7 @@ function getExportErrorDescription(errorType: string): string { } } -export default function Page() { +const Page = () => { const email = useUserInfoState((state) => state.email); const name = useUserInfoState((state) => state.name); const authType = useUserInfoState((state) => state.authType); @@ -567,4 +567,6 @@ export default function Page() {
    ); -} +}; + +export default Page; diff --git a/embed/oko_attached/src/components/export/email_reauth.tsx b/embed/oko_attached/src/components/export/email_reauth.tsx index 3166770a7..74c447295 100644 --- a/embed/oko_attached/src/components/export/email_reauth.tsx +++ b/embed/oko_attached/src/components/export/email_reauth.tsx @@ -26,7 +26,7 @@ const LOG_PREFIX = "[attached][email_reauth]"; type Step = "enter_email" | "verify_code"; -export function EmailReauth() { +export const EmailReauth = () => { const theme = useContext(ThemeContext); const webAuth = useMemo(() => getAuth0WebAuth(), []); diff --git a/embed/oko_attached/src/components/export/export_reauth.tsx b/embed/oko_attached/src/components/export/export_reauth.tsx index 741fbdca0..bcc0fa51a 100644 --- a/embed/oko_attached/src/components/export/export_reauth.tsx +++ b/embed/oko_attached/src/components/export/export_reauth.tsx @@ -8,7 +8,7 @@ import { TelegramReauth } from "./telegram_reauth"; type ReauthStatus = "loading" | "redirecting" | "error"; -export function ExportReauth() { +export const ExportReauth = () => { const params = new URLSearchParams(window.location.search); const authType = params.get("auth_type") as AuthType | null; @@ -33,7 +33,7 @@ export function ExportReauth() { } } -function OAuthRedirect({ authType }: { authType: "google" | "x" | "discord" }) { +const OAuthRedirect = ({ authType }: { authType: "google" | "x" | "discord" }) => { const [status, setStatus] = useState("loading"); const [error, setError] = useState(null); diff --git a/embed/oko_attached/src/components/export/telegram_reauth.tsx b/embed/oko_attached/src/components/export/telegram_reauth.tsx index 4d07a7c24..7491e0455 100644 --- a/embed/oko_attached/src/components/export/telegram_reauth.tsx +++ b/embed/oko_attached/src/components/export/telegram_reauth.tsx @@ -18,7 +18,7 @@ import styles from "./telegram_reauth.module.scss"; const LOG_PREFIX = "[attached][telegram_reauth]"; -export function TelegramReauth() { +export const TelegramReauth = () => { const theme = useContext(ThemeContext); const [errorMessage, setErrorMessage] = useState(null); diff --git a/embed/oko_attached/src/components/export/use_export_reauth.ts b/embed/oko_attached/src/components/export/use_export_reauth.ts index f8c85d6fe..d1fe328d9 100644 --- a/embed/oko_attached/src/components/export/use_export_reauth.ts +++ b/embed/oko_attached/src/components/export/use_export_reauth.ts @@ -1,6 +1,5 @@ import { useCallback } from "react"; -import type { AuthType } from "@oko-wallet/oko-types/auth"; import type { OAuthState } from "@oko-wallet/oko-sdk-core"; import type { Result } from "@oko-wallet/stdlib-js"; From 33b0dde94c4100a7b1be641ba9969f6919e457a1 Mon Sep 17 00:00:00 2001 From: chihunmanse Date: Wed, 25 Feb 2026 14:24:39 +0900 Subject: [PATCH 55/62] o --- embed/oko_attached/src/window_msgs/export_private_key.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index cf7db9887..32807c666 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -23,6 +23,9 @@ import { type ReAuthCredentials, } from "./export_reauth_state"; +// NOTE: Since this method can only be used within user_dashboard, +// it is not exposed to the SDK, and its type is also defined within this file. + type ExportPrivateKeyError = | { type: "UNAUTHORIZED_ORIGIN" } | { type: "NOT_AUTHENTICATED" } From dbd67937f6225c8c1582e75213b13df4471501c9 Mon Sep 17 00:00:00 2001 From: Elden Park Date: Wed, 25 Feb 2026 19:10:40 -0800 Subject: [PATCH 56/62] o --- .../src/components/export/email_reauth.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/embed/oko_attached/src/components/export/email_reauth.tsx b/embed/oko_attached/src/components/export/email_reauth.tsx index 74c447295..5b97d36e1 100644 --- a/embed/oko_attached/src/components/export/email_reauth.tsx +++ b/embed/oko_attached/src/components/export/email_reauth.tsx @@ -1,24 +1,22 @@ -import { type FormEvent, useContext, useEffect, useMemo, useState } from "react"; - -import type { OAuthState } from "@oko-wallet/oko-sdk-core"; -import { OtpInput } from "@oko-wallet/oko-common-ui/otp_input"; -import { Typography } from "@oko-wallet/oko-common-ui/typography"; import { MailboxIcon } from "@oko-wallet/oko-common-ui/icons/mailbox"; import { Logo } from "@oko-wallet/oko-common-ui/logo"; +import { OtpInput } from "@oko-wallet/oko-common-ui/otp_input"; import { ThemeContext } from "@oko-wallet/oko-common-ui/theme"; +import { Typography } from "@oko-wallet/oko-common-ui/typography"; +import type { OAuthState } from "@oko-wallet/oko-sdk-core"; +import { useContext, useEffect, useMemo, useState } from "react"; -import { getAuth0WebAuth } from "@oko-wallet-attached/config/auth0"; -import { - sendEmailOTPCode, - verifyEmailOTPCode, -} from "@oko-wallet-attached/lib/auth0"; - +import styles from "./email_reauth.module.scss"; import { findEmbeddedIframe, generateNonce, sendReauthParamsToIframe, } from "./use_export_reauth"; -import styles from "./email_reauth.module.scss"; +import { getAuth0WebAuth } from "@oko-wallet-attached/config/auth0"; +import { + sendEmailOTPCode, + verifyEmailOTPCode, +} from "@oko-wallet-attached/lib/auth0"; const CODE_LENGTH = 6; const RESEND_COOLDOWN_SECONDS = 180; @@ -95,6 +93,7 @@ export const EmailReauth = () => { }, [resendTimer]); // Auto-verify when OTP is complete + // biome-ignore lint/correctness/useExhaustiveDependencies: rendering infinite loop useEffect(() => { if (isOtpComplete && !isSubmitting && !errorMessage) { void handleVerifyCode(); @@ -125,7 +124,7 @@ export const EmailReauth = () => { } }; - const handleVerifyCode = async () => { + async function handleVerifyCode() { if (!isOtpComplete || isSubmitting) { return; } @@ -159,7 +158,7 @@ export const EmailReauth = () => { setIsSubmitting(false); }, }); - }; + } const handleResendCode = async () => { if (resendTimer > 0 || isSubmitting) { @@ -180,12 +179,12 @@ export const EmailReauth = () => { } }; - const onSubmitEmail = (e: FormEvent) => { + const onSubmitEmail = (e: React.SubmitEvent) => { e.preventDefault(); void handleSubmitEmail(); }; - const onSubmitCode = (e: FormEvent) => { + const onSubmitCode = (e: React.SubmitEvent) => { e.preventDefault(); void handleVerifyCode(); }; @@ -216,7 +215,6 @@ export const EmailReauth = () => { setEmail(e.target.value); }} className={styles.emailInput} - autoFocus />
    ); -} +}; From c91da64dc4447ce1cd4a7e8bca5c37e482452d8f Mon Sep 17 00:00:00 2001 From: Elden Park Date: Wed, 25 Feb 2026 19:14:07 -0800 Subject: [PATCH 57/62] o --- .../src/app/export_private_key/page.tsx | 27 ++++++++++--------- .../src/window_msgs/get_connected_apps.ts | 11 +++++--- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 506464f56..3317a8128 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -1,13 +1,13 @@ "use client"; -import type { AuthType } from "@oko-wallet/oko-types/auth"; +import { Button } from "@oko-wallet/oko-common-ui/button"; import { DiscordIcon } from "@oko-wallet/oko-common-ui/icons/discord_icon"; import { GoogleIcon } from "@oko-wallet/oko-common-ui/icons/google_icon"; import { MailboxIcon } from "@oko-wallet/oko-common-ui/icons/mailbox"; import { TelegramIcon } from "@oko-wallet/oko-common-ui/icons/telegram_icon"; import { XIcon } from "@oko-wallet/oko-common-ui/icons/x_icon"; -import { Button } from "@oko-wallet/oko-common-ui/button"; import { Typography } from "@oko-wallet/oko-common-ui/typography"; +import type { AuthType } from "@oko-wallet/oko-types/auth"; import { type ReactNode, useCallback, useState } from "react"; import styles from "./page.module.scss"; @@ -19,7 +19,9 @@ import { } from "@oko-wallet-user-dashboard/state/sdk"; import { useUserInfoState } from "@oko-wallet-user-dashboard/state/user_info"; -const getAuthProviderInfo = (authType: AuthType | null): { +const getAuthProviderInfo = ( + authType: AuthType | null, +): { icon: ReactNode; label: string; } => { @@ -40,7 +42,7 @@ const getAuthProviderInfo = (authType: AuthType | null): { default: return { icon: null, label: "" }; } -} +}; const LockIcon = () => { return ( @@ -58,7 +60,7 @@ const LockIcon = () => { ); -} +}; const AlertTriangleIcon = () => { return ( @@ -77,7 +79,7 @@ const AlertTriangleIcon = () => { ); -} +}; const KeyIcon = () => { return ( @@ -94,7 +96,7 @@ const KeyIcon = () => { ); -} +}; const CopyIcon = () => { return ( @@ -111,7 +113,7 @@ const CopyIcon = () => { ); -} +}; const EyeOffIcon = () => { return ( @@ -128,7 +130,7 @@ const EyeOffIcon = () => { ); -} +}; const Step1Content = ({ authInfo, @@ -207,7 +209,7 @@ const Step1Content = ({ ); -} +}; const Step2Content = ({ privateKeys, @@ -345,7 +347,7 @@ const Step2Content = ({ ); -} +}; const getExportErrorDescription = (errorType: string): string => { switch (errorType) { @@ -362,7 +364,7 @@ const getExportErrorDescription = (errorType: string): string => { default: return "Please try again."; } -} +}; const Page = () => { const email = useUserInfoState((state) => state.email); @@ -466,6 +468,7 @@ const Page = () => { popup?.close(); // 5. Parse result + // TODO: Use imported type const resAny = res as unknown as { msg_type: "__export_private_key_ack__"; payload: diff --git a/embed/oko_attached/src/window_msgs/get_connected_apps.ts b/embed/oko_attached/src/window_msgs/get_connected_apps.ts index cd698688b..4eed373a7 100644 --- a/embed/oko_attached/src/window_msgs/get_connected_apps.ts +++ b/embed/oko_attached/src/window_msgs/get_connected_apps.ts @@ -1,10 +1,10 @@ -import type { MsgEventContext } from "./types"; import { OKO_SDK_TARGET } from "./target"; -import { useAppState } from "@oko-wallet-attached/store/app"; +import type { MsgEventContext } from "./types"; import { OKO_API_ENDPOINT, USER_DASHBOARD_ORIGINS, } from "@oko-wallet-attached/requests/endpoints"; +import { useAppState } from "@oko-wallet-attached/store/app"; // NOTE: Since this method can only be used within user_dashboard, // it is not exposed to the SDK, and its type is also defined within that file. @@ -21,15 +21,20 @@ type GetConnectedAppsError = | { type: "UNAUTHORIZED_ORIGIN" } | { type: "NOT_AUTHENTICATED" } | { type: "FETCH_ERROR"; error: string }; + interface GetConnectedAppsAckSuccessPayload { success: true; data: ConnectedApp[]; } + interface GetConnectedAppsAckErrorPayload { success: false; error: GetConnectedAppsError; } -type GetConnectedAppsAckPayload = GetConnectedAppsAckSuccessPayload | GetConnectedAppsAckErrorPayload; + +type GetConnectedAppsAckPayload = + | GetConnectedAppsAckSuccessPayload + | GetConnectedAppsAckErrorPayload; interface OkoWalletMsgGetConnectedAppsAck { target: "oko_sdk"; From fd11b2cbdaf3f92d13520e04333d080ee4da6f16 Mon Sep 17 00:00:00 2001 From: Elden Park Date: Wed, 25 Feb 2026 19:29:48 -0800 Subject: [PATCH 58/62] o --- .../src/app/export_private_key/page.tsx | 5 ++ .../src/hooks/use_connected_apps.ts | 5 +- .../src/window_msgs/export_private_key.ts | 51 ++++++------------- .../src/types/msg/export_priv_key.ts | 18 +++++++ sdk/oko_sdk_core/src/types/msg/index.ts | 8 +-- 5 files changed, 47 insertions(+), 40 deletions(-) create mode 100644 sdk/oko_sdk_core/src/types/msg/export_priv_key.ts diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 3317a8128..0f57e915f 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -56,6 +56,7 @@ const LockIcon = () => { strokeLinecap="round" strokeLinejoin="round" > + lock @@ -74,6 +75,7 @@ const AlertTriangleIcon = () => { strokeLinecap="round" strokeLinejoin="round" > + alert triangle @@ -93,6 +95,7 @@ const KeyIcon = () => { strokeLinecap="round" strokeLinejoin="round" > + key ); @@ -110,6 +113,7 @@ const CopyIcon = () => { strokeLinecap="round" strokeLinejoin="round" > + copy ); @@ -127,6 +131,7 @@ const EyeOffIcon = () => { strokeLinecap="round" strokeLinejoin="round" > + eye off ); diff --git a/apps/user_dashboard/src/hooks/use_connected_apps.ts b/apps/user_dashboard/src/hooks/use_connected_apps.ts index 0486c5019..e1237929c 100644 --- a/apps/user_dashboard/src/hooks/use_connected_apps.ts +++ b/apps/user_dashboard/src/hooks/use_connected_apps.ts @@ -1,5 +1,5 @@ -import { useQuery } from "@tanstack/react-query"; import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; +import { useQuery } from "@tanstack/react-query"; import { selectCosmosSDK, @@ -27,14 +27,15 @@ interface GetConnectedAppsAckPayload { error?: GetConnectedAppsError; } - type UseConnectedAppsResult = UseConnectedAppsSuccess | UseConnectedAppsError; + interface UseConnectedAppsSuccess { isSuccess: true; data: ConnectedApp[]; isLoading: boolean; error: null; } + interface UseConnectedAppsError { isSuccess: false; error: GetConnectedAppsError; diff --git a/embed/oko_attached/src/window_msgs/export_private_key.ts b/embed/oko_attached/src/window_msgs/export_private_key.ts index 32807c666..4582d5a3c 100644 --- a/embed/oko_attached/src/window_msgs/export_private_key.ts +++ b/embed/oko_attached/src/window_msgs/export_private_key.ts @@ -1,49 +1,30 @@ -import bs58 from "bs58"; - +import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; +import type { + ExportPrivateKeyAckPayload, + OkoWalletMsgExportPrivateKeyAck, +} from "@oko-wallet/oko-sdk-core"; +import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; import type { AuthType } from "@oko-wallet/oko-types/auth"; import type { ExportSharesRequest, ExportSharesResponse, } from "@oko-wallet/oko-types/user"; -import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; -import * as secp256k1Wasm from "@oko-wallet/cait-sith-keplr-wasm/pkg/cait_sith_keplr_wasm"; +import bs58 from "bs58"; -import type { MsgEventContext } from "./types"; +import { + type ReAuthCredentials, + setReAuthResolver, +} from "./export_reauth_state"; import { OKO_SDK_TARGET } from "./target"; -import { useAppState } from "@oko-wallet-attached/store/app"; -import { USER_DASHBOARD_ORIGINS } from "@oko-wallet-attached/requests/endpoints"; -import { TSS_V2_ENDPOINT } from "@oko-wallet-attached/requests/oko_api"; +import type { MsgEventContext } from "./types"; import { - SEED_ID_CLIENT, hexToSeedSharePoint, hexToUint8Array, + SEED_ID_CLIENT, } from "@oko-wallet-attached/crypto/keygen_ed25519"; -import { - setReAuthResolver, - type ReAuthCredentials, -} from "./export_reauth_state"; - -// NOTE: Since this method can only be used within user_dashboard, -// it is not exposed to the SDK, and its type is also defined within this file. - -type ExportPrivateKeyError = - | { type: "UNAUTHORIZED_ORIGIN" } - | { type: "NOT_AUTHENTICATED" } - | { type: "MISSING_KEY_SHARES" } - | { type: "COMBINE_ERROR"; error: string } - | { type: "API_ERROR"; error: string } - | { type: "REAUTH_TIMEOUT" } - | { type: "REAUTH_ERROR"; error: string }; - -type ExportPrivateKeyAckPayload = - | { success: true; data: { secp256k1: string; ed25519: string } } - | { success: false; error: ExportPrivateKeyError }; - -interface OkoWalletMsgExportPrivateKeyAck { - target: "oko_sdk"; - msg_type: "__export_private_key_ack__"; - payload: ExportPrivateKeyAckPayload; -} +import { USER_DASHBOARD_ORIGINS } from "@oko-wallet-attached/requests/endpoints"; +import { TSS_V2_ENDPOINT } from "@oko-wallet-attached/requests/oko_api"; +import { useAppState } from "@oko-wallet-attached/store/app"; const REAUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes const LOG_PREFIX = "[attached][export]"; diff --git a/sdk/oko_sdk_core/src/types/msg/export_priv_key.ts b/sdk/oko_sdk_core/src/types/msg/export_priv_key.ts new file mode 100644 index 000000000..94bec59e8 --- /dev/null +++ b/sdk/oko_sdk_core/src/types/msg/export_priv_key.ts @@ -0,0 +1,18 @@ +export type ExportPrivateKeyError = + | { type: "UNAUTHORIZED_ORIGIN" } + | { type: "NOT_AUTHENTICATED" } + | { type: "MISSING_KEY_SHARES" } + | { type: "COMBINE_ERROR"; error: string } + | { type: "API_ERROR"; error: string } + | { type: "REAUTH_TIMEOUT" } + | { type: "REAUTH_ERROR"; error: string }; + +export type ExportPrivateKeyAckPayload = + | { success: true; data: { secp256k1: string; ed25519: string } } + | { success: false; error: ExportPrivateKeyError }; + +export interface OkoWalletMsgExportPrivateKeyAck { + target: "oko_sdk"; + msg_type: "__export_private_key_ack__"; + payload: ExportPrivateKeyAckPayload; +} diff --git a/sdk/oko_sdk_core/src/types/msg/index.ts b/sdk/oko_sdk_core/src/types/msg/index.ts index 8a6c7f5ce..e1391b6b1 100644 --- a/sdk/oko_sdk_core/src/types/msg/index.ts +++ b/sdk/oko_sdk_core/src/types/msg/index.ts @@ -1,18 +1,20 @@ import type { ChainInfo } from "@keplr-wallet/types"; -import type { Result } from "@oko-wallet/stdlib-js"; import type { Bytes32 } from "@oko-wallet/bytes"; import type { AuthType } from "@oko-wallet/oko-types/auth"; +import type { Result } from "@oko-wallet/stdlib-js"; +import type { InitPayload } from "@oko-wallet-sdk-core/types/init"; import type { OpenModalAckPayload, OpenModalPayload, } from "@oko-wallet-sdk-core/types/modal"; -import type { InitPayload } from "@oko-wallet-sdk-core/types/init"; -import type { OAuthSignInError } from "@oko-wallet-sdk-core/types/sign_in"; import type { OAuthPayload, OAuthTokenRequestPayload, } from "@oko-wallet-sdk-core/types/oauth"; +import type { OAuthSignInError } from "@oko-wallet-sdk-core/types/sign_in"; + +export * from "./export_priv_key"; export type OkoWalletMsgGetPublicKey = { target: "oko_attached"; From 5473635f8670991d9fabed61ea270a0e5b9a27c4 Mon Sep 17 00:00:00 2001 From: Elden Park Date: Wed, 25 Feb 2026 19:30:57 -0800 Subject: [PATCH 59/62] project: version bump to v0.1.2-alpha.6 --- common/oko_types/package.json | 4 +- crypto/bytes/package.json | 4 +- crypto/crypto_js/package.json | 6 +-- crypto/tecdsa/tecdsa_interface/package.json | 2 +- key_share_node/ksn_interface/package.json | 6 +-- lerna.json | 2 +- lib/dotenv/package.json | 4 +- lib/stdlib_js/package.json | 2 +- sdk/oko_cosmos_kit/package.json | 6 +-- sdk/oko_interchain_kit/package.json | 6 +-- sdk/oko_sdk_core/package.json | 8 ++-- sdk/oko_sdk_cosmos/package.json | 6 +-- sdk/oko_sdk_eth/package.json | 6 +-- sdk/oko_sdk_svm/package.json | 4 +- yarn.lock | 48 ++++++++++----------- 15 files changed, 57 insertions(+), 57 deletions(-) diff --git a/common/oko_types/package.json b/common/oko_types/package.json index bffbe95c4..0b4ffdabd 100644 --- a/common/oko_types/package.json +++ b/common/oko_types/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-types", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "publishConfig": { "access": "public" @@ -33,7 +33,7 @@ "./user_dashboard": "./dist/user_dashboard/index.js" }, "dependencies": { - "@oko-wallet/bytes": "^0.1.2-alpha.5", + "@oko-wallet/bytes": "^0.1.2-alpha.6", "@oko-wallet/stdlib-js": "0.0.2-rc.41", "@oko-wallet/tecdsa-interface": "0.0.2-alpha.22", "del-cli": "^6.0.0", diff --git a/crypto/bytes/package.json b/crypto/bytes/package.json index 168abd3a0..ca6f91c77 100644 --- a/crypto/bytes/package.json +++ b/crypto/bytes/package.json @@ -3,7 +3,7 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "publishConfig": { "access": "public" }, @@ -19,7 +19,7 @@ "test": "jest" }, "dependencies": { - "@oko-wallet/stdlib-js": "^0.1.2-alpha.5" + "@oko-wallet/stdlib-js": "^0.1.2-alpha.6" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/crypto/crypto_js/package.json b/crypto/crypto_js/package.json index c6ab0ca5f..f1b0be921 100644 --- a/crypto/crypto_js/package.json +++ b/crypto/crypto_js/package.json @@ -1,7 +1,7 @@ { "name": "@oko-wallet/crypto-js", "type": "module", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "publishConfig": { "access": "public" }, @@ -23,8 +23,8 @@ "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", - "@oko-wallet/bytes": "^0.1.2-alpha.5", - "@oko-wallet/stdlib-js": "^0.1.2-alpha.5", + "@oko-wallet/bytes": "^0.1.2-alpha.6", + "@oko-wallet/stdlib-js": "^0.1.2-alpha.6", "bcryptjs": "^3.0.2", "supertest": "^7.0.0" }, diff --git a/crypto/tecdsa/tecdsa_interface/package.json b/crypto/tecdsa/tecdsa_interface/package.json index 99100c198..1dabc2869 100644 --- a/crypto/tecdsa/tecdsa_interface/package.json +++ b/crypto/tecdsa/tecdsa_interface/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/tecdsa-interface", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/key_share_node/ksn_interface/package.json b/key_share_node/ksn_interface/package.json index 008c46648..ff32d6c1b 100644 --- a/key_share_node/ksn_interface/package.json +++ b/key_share_node/ksn_interface/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/ksn-interface", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -55,8 +55,8 @@ "directory": "key_share_node/ksn_interface" }, "dependencies": { - "@oko-wallet/bytes": "^0.1.2-alpha.5", - "@oko-wallet/oko-types": "^0.1.2-alpha.5" + "@oko-wallet/bytes": "^0.1.2-alpha.6", + "@oko-wallet/oko-types": "^0.1.2-alpha.6" }, "devDependencies": { "@types/jest": "^29.5.14", diff --git a/lerna.json b/lerna.json index 22781dcce..b14538538 100644 --- a/lerna.json +++ b/lerna.json @@ -44,6 +44,6 @@ "ui/oko_common_ui" ], "npmClient": "yarn", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/lib/dotenv/package.json b/lib/dotenv/package.json index 455662a1d..f904e1e92 100644 --- a/lib/dotenv/package.json +++ b/lib/dotenv/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/dotenv", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "main": "./dist/index.js", "publishConfig": { @@ -16,7 +16,7 @@ "directory": "stdlib_js" }, "dependencies": { - "@oko-wallet/stdlib-js": "^0.1.2-alpha.5", + "@oko-wallet/stdlib-js": "^0.1.2-alpha.6", "dotenv": "^16.4.5", "zod": "^4.1.12" }, diff --git a/lib/stdlib_js/package.json b/lib/stdlib_js/package.json index f2018a00f..b266df708 100644 --- a/lib/stdlib_js/package.json +++ b/lib/stdlib_js/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/stdlib-js", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/sdk/oko_cosmos_kit/package.json b/sdk/oko_cosmos_kit/package.json index a2d3d0f9c..19150620b 100644 --- a/sdk/oko_cosmos_kit/package.json +++ b/sdk/oko_cosmos_kit/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-cosmos-kit", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "description": "cosmos-kit wallet connector for Oko Wallet", "author": "oko-wallet", @@ -38,8 +38,8 @@ "dependencies": { "@cosmos-kit/core": "^2.16.7", "@keplr-wallet/types": "0.12.297", - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.5", - "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.5" + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", + "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.6" }, "peerDependencies": { "@cosmjs/amino": ">= 0.28", diff --git a/sdk/oko_interchain_kit/package.json b/sdk/oko_interchain_kit/package.json index 509ba2f6e..7b2dc2858 100644 --- a/sdk/oko_interchain_kit/package.json +++ b/sdk/oko_interchain_kit/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-interchain-kit", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "description": "interchain-kit wallet connector for Oko Wallet", "author": "oko-wallet", @@ -38,8 +38,8 @@ "dependencies": { "@interchain-kit/core": "^0.3.55", "@keplr-wallet/types": "0.12.297", - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.5", - "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.5" + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", + "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.6" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.0", diff --git a/sdk/oko_sdk_core/package.json b/sdk/oko_sdk_core/package.json index 5dd5a7318..7c9df72f9 100644 --- a/sdk/oko_sdk_core/package.json +++ b/sdk/oko_sdk_core/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-core", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "scripts": { "build": "tsx ./scripts/build.ts", @@ -32,9 +32,9 @@ }, "dependencies": { "@keplr-wallet/types": "0.12.297", - "@oko-wallet/bytes": "^0.1.2-alpha.5", - "@oko-wallet/oko-types": "^0.1.2-alpha.5", - "@oko-wallet/stdlib-js": "^0.1.2-alpha.5", + "@oko-wallet/bytes": "^0.1.2-alpha.6", + "@oko-wallet/oko-types": "^0.1.2-alpha.6", + "@oko-wallet/stdlib-js": "^0.1.2-alpha.6", "preact": "^10.25.4" }, "devDependencies": { diff --git a/sdk/oko_sdk_cosmos/package.json b/sdk/oko_sdk_cosmos/package.json index 18df05453..a7802fb8e 100644 --- a/sdk/oko_sdk_cosmos/package.json +++ b/sdk/oko_sdk_cosmos/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-cosmos", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "type": "module", "publishConfig": { "access": "public" @@ -39,8 +39,8 @@ "@keplr-wallet/types": "0.12.297", "@noble/curves": "1.9.7", "@noble/hashes": "^1.8.0", - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.5", - "@oko-wallet/stdlib-js": "^0.1.2-alpha.5", + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", + "@oko-wallet/stdlib-js": "^0.1.2-alpha.6", "bech32": "^2.0.0", "buffer": "^6.0.3", "uuid": "^13.0.0" diff --git a/sdk/oko_sdk_eth/package.json b/sdk/oko_sdk_eth/package.json index e525520c1..baec5dd94 100644 --- a/sdk/oko_sdk_eth/package.json +++ b/sdk/oko_sdk_eth/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-eth", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "main": "./dist/index.js", "types": "./dist/index.d.ts", "type": "module", @@ -39,8 +39,8 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.5", - "@oko-wallet/stdlib-js": "^0.1.2-alpha.5", + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", + "@oko-wallet/stdlib-js": "^0.1.2-alpha.6", "eventemitter3": "^5.0.1", "uuid": "^13.0.0", "viem": "^2.41.2" diff --git a/sdk/oko_sdk_svm/package.json b/sdk/oko_sdk_svm/package.json index 7568ec4bc..a86f2f396 100644 --- a/sdk/oko_sdk_svm/package.json +++ b/sdk/oko_sdk_svm/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-svm", - "version": "0.1.2-alpha.5", + "version": "0.1.2-alpha.6", "main": "./dist/index.js", "types": "./dist/index.d.ts", "type": "module", @@ -36,7 +36,7 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.5", + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", "@oko-wallet/stdlib-js": "^0.0.2-rc.42", "@solana/wallet-standard-features": "^1.3.0", "@solana/web3.js": "^1.98.0", diff --git a/yarn.lock b/yarn.lock index 2607453ac..84b068f98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10387,12 +10387,12 @@ __metadata: languageName: node linkType: hard -"@oko-wallet/bytes@npm:^0.1.2-alpha.1, @oko-wallet/bytes@npm:^0.1.2-alpha.2, @oko-wallet/bytes@npm:^0.1.2-alpha.5, @oko-wallet/bytes@workspace:*, @oko-wallet/bytes@workspace:crypto/bytes": +"@oko-wallet/bytes@npm:^0.1.2-alpha.1, @oko-wallet/bytes@npm:^0.1.2-alpha.2, @oko-wallet/bytes@npm:^0.1.2-alpha.6, @oko-wallet/bytes@workspace:*, @oko-wallet/bytes@workspace:crypto/bytes": version: 0.0.0-use.local resolution: "@oko-wallet/bytes@workspace:crypto/bytes" dependencies: "@jest/globals": "npm:^29.7.0" - "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.5" + "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^24.10.1" del-cli: "npm:^6.0.0" @@ -10510,8 +10510,8 @@ __metadata: "@jest/globals": "npm:^29.7.0" "@noble/curves": "npm:2.0.1" "@noble/hashes": "npm:2.0.1" - "@oko-wallet/bytes": "npm:^0.1.2-alpha.5" - "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.5" + "@oko-wallet/bytes": "npm:^0.1.2-alpha.6" + "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@types/bcryptjs": "npm:^2.4.6" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^24.10.1" @@ -10669,7 +10669,7 @@ __metadata: version: 0.0.0-use.local resolution: "@oko-wallet/dotenv@workspace:lib/dotenv" dependencies: - "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.5" + "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^24.10.1" del-cli: "npm:^6.0.0" @@ -10849,8 +10849,8 @@ __metadata: version: 0.0.0-use.local resolution: "@oko-wallet/ksn-interface@workspace:key_share_node/ksn_interface" dependencies: - "@oko-wallet/bytes": "npm:^0.1.2-alpha.5" - "@oko-wallet/oko-types": "npm:^0.1.2-alpha.5" + "@oko-wallet/bytes": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-types": "npm:^0.1.2-alpha.6" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^24.10.1" del-cli: "npm:^6.0.0" @@ -11130,8 +11130,8 @@ __metadata: dependencies: "@cosmos-kit/core": "npm:^2.16.7" "@keplr-wallet/types": "npm:0.12.297" - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.5" - "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.5" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.6" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" "@rollup/plugin-typescript": "npm:^11.0.0" @@ -11152,8 +11152,8 @@ __metadata: dependencies: "@interchain-kit/core": "npm:^0.3.55" "@keplr-wallet/types": "npm:0.12.297" - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.5" - "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.5" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.6" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" "@rollup/plugin-typescript": "npm:^11.0.0" @@ -11201,14 +11201,14 @@ __metadata: languageName: node linkType: hard -"@oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.5, @oko-wallet/oko-sdk-core@workspace:sdk/oko_sdk_core": +"@oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.6, @oko-wallet/oko-sdk-core@workspace:sdk/oko_sdk_core": version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-core@workspace:sdk/oko_sdk_core" dependencies: "@keplr-wallet/types": "npm:0.12.297" - "@oko-wallet/bytes": "npm:^0.1.2-alpha.5" - "@oko-wallet/oko-types": "npm:^0.1.2-alpha.5" - "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.5" + "@oko-wallet/bytes": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-types": "npm:^0.1.2-alpha.6" + "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-json": "npm:^6.1.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" @@ -11250,7 +11250,7 @@ __metadata: languageName: node linkType: hard -"@oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.5, @oko-wallet/oko-sdk-cosmos@workspace:sdk/oko_sdk_cosmos": +"@oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.6, @oko-wallet/oko-sdk-cosmos@workspace:sdk/oko_sdk_cosmos": version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-cosmos@workspace:sdk/oko_sdk_cosmos" dependencies: @@ -11260,8 +11260,8 @@ __metadata: "@keplr-wallet/types": "npm:0.12.297" "@noble/curves": "npm:1.9.7" "@noble/hashes": "npm:^1.8.0" - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.5" - "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.5" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" + "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-json": "npm:^6.1.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" @@ -11288,8 +11288,8 @@ __metadata: version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-eth@workspace:sdk/oko_sdk_eth" dependencies: - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.5" - "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.5" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" + "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-json": "npm:^6.1.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" @@ -11317,7 +11317,7 @@ __metadata: version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-svm@workspace:sdk/oko_sdk_svm" dependencies: - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.5" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" "@oko-wallet/stdlib-js": "npm:^0.0.2-rc.42" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" @@ -11358,11 +11358,11 @@ __metadata: languageName: node linkType: hard -"@oko-wallet/oko-types@npm:^0.1.2-alpha.1, @oko-wallet/oko-types@npm:^0.1.2-alpha.2, @oko-wallet/oko-types@npm:^0.1.2-alpha.5, @oko-wallet/oko-types@workspace:*, @oko-wallet/oko-types@workspace:common/oko_types": +"@oko-wallet/oko-types@npm:^0.1.2-alpha.1, @oko-wallet/oko-types@npm:^0.1.2-alpha.2, @oko-wallet/oko-types@npm:^0.1.2-alpha.6, @oko-wallet/oko-types@workspace:*, @oko-wallet/oko-types@workspace:common/oko_types": version: 0.0.0-use.local resolution: "@oko-wallet/oko-types@workspace:common/oko_types" dependencies: - "@oko-wallet/bytes": "npm:^0.1.2-alpha.5" + "@oko-wallet/bytes": "npm:^0.1.2-alpha.6" "@oko-wallet/stdlib-js": "npm:0.0.2-rc.41" "@oko-wallet/tecdsa-interface": "npm:0.0.2-alpha.22" "@types/jest": "npm:^29.5.14" @@ -11537,7 +11537,7 @@ __metadata: languageName: node linkType: hard -"@oko-wallet/stdlib-js@npm:^0.1.2-alpha.5, @oko-wallet/stdlib-js@workspace:*, @oko-wallet/stdlib-js@workspace:lib/stdlib_js": +"@oko-wallet/stdlib-js@npm:^0.1.2-alpha.6, @oko-wallet/stdlib-js@workspace:*, @oko-wallet/stdlib-js@workspace:lib/stdlib_js": version: 0.0.0-use.local resolution: "@oko-wallet/stdlib-js@workspace:lib/stdlib_js" dependencies: From 80b716d050d8ebc5f9561601177e017620cf4535 Mon Sep 17 00:00:00 2001 From: Elden Park Date: Wed, 25 Feb 2026 19:50:28 -0800 Subject: [PATCH 60/62] o --- .../src/app/export_private_key/page.tsx | 15 +----- .../src/hooks/use_connected_apps.ts | 21 +++++---- .../src/window_msgs/get_connected_apps.ts | 38 +-------------- embed/oko_attached/src/window_msgs/index.ts | 47 ++++++++----------- sdk/oko_sdk_core/src/types/index.ts | 12 ++--- .../src/types/msg/export_priv_key.ts | 8 ++++ .../src/types/msg/get_conn_apps.ts | 39 +++++++++++++++ sdk/oko_sdk_core/src/types/msg/index.ts | 1 + 8 files changed, 88 insertions(+), 93 deletions(-) create mode 100644 sdk/oko_sdk_core/src/types/msg/get_conn_apps.ts diff --git a/apps/user_dashboard/src/app/export_private_key/page.tsx b/apps/user_dashboard/src/app/export_private_key/page.tsx index 0f57e915f..033ff40d1 100644 --- a/apps/user_dashboard/src/app/export_private_key/page.tsx +++ b/apps/user_dashboard/src/app/export_private_key/page.tsx @@ -10,6 +10,7 @@ import { Typography } from "@oko-wallet/oko-common-ui/typography"; import type { AuthType } from "@oko-wallet/oko-types/auth"; import { type ReactNode, useCallback, useState } from "react"; +import type { OkoWalletMsgExportPrivateKeyAck } from "../../../../../sdk/oko_sdk_core/dist/types"; import styles from "./page.module.scss"; import { displayToast } from "@oko-wallet-user-dashboard/components/toast"; import { useCopyToClipboard } from "@oko-wallet-user-dashboard/hooks/use_copy_to_clipboard"; @@ -473,19 +474,7 @@ const Page = () => { popup?.close(); // 5. Parse result - // TODO: Use imported type - const resAny = res as unknown as { - msg_type: "__export_private_key_ack__"; - payload: - | { - success: true; - data: { secp256k1: string; ed25519: string }; - } - | { - success: false; - error: { type: string; error?: string }; - }; - }; + const resAny = res as unknown as OkoWalletMsgExportPrivateKeyAck; if ( resAny.msg_type === "__export_private_key_ack__" && diff --git a/apps/user_dashboard/src/hooks/use_connected_apps.ts b/apps/user_dashboard/src/hooks/use_connected_apps.ts index e1237929c..d66445d4c 100644 --- a/apps/user_dashboard/src/hooks/use_connected_apps.ts +++ b/apps/user_dashboard/src/hooks/use_connected_apps.ts @@ -1,6 +1,10 @@ import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; import { useQuery } from "@tanstack/react-query"; +import type { + OkoWalletMsgGetConnectedApps, + OkoWalletMsgGetConnectedAppsAck, +} from "../../../../sdk/oko_sdk_core/dist/types"; import { selectCosmosSDK, useSDKState, @@ -42,7 +46,8 @@ interface UseConnectedAppsError { isLoading: boolean; data: never[]; } -//NOTE The __get_connected_apps__ message should only be called from the user_dashboard, + +// NOTE The __get_connected_apps__ message should only be called from the user_dashboard, // so it is not added to the SDK and is instead called separately in useConnectedApp. export function useConnectedApps(): UseConnectedAppsResult { const cosmosSDK = useSDKState(selectCosmosSDK); @@ -58,19 +63,15 @@ export function useConnectedApps(): UseConnectedAppsResult { target: "oko_attached", msg_type: "__get_connected_apps__", payload: null, - } as any); + } as OkoWalletMsgGetConnectedApps as any); - //NOTE: get_connected_apps is a msg specific to user_dashboard, so it need to be casted to as unknown here. - const resAny = res as unknown as { - msg_type: "__get_connected_apps_ack__"; - payload: GetConnectedAppsAckPayload; - }; + const resAny = res as unknown as OkoWalletMsgGetConnectedAppsAck; if (resAny.msg_type === "__get_connected_apps_ack__") { const payload = resAny.payload; - if (payload.success && payload.data) { + + if (payload.success) { return payload.data; - } - if (payload.error) { + } else { throw payload.error; } } diff --git a/embed/oko_attached/src/window_msgs/get_connected_apps.ts b/embed/oko_attached/src/window_msgs/get_connected_apps.ts index 4eed373a7..48c756a95 100644 --- a/embed/oko_attached/src/window_msgs/get_connected_apps.ts +++ b/embed/oko_attached/src/window_msgs/get_connected_apps.ts @@ -1,3 +1,5 @@ +import type { OkoWalletMsgGetConnectedAppsAck } from "@oko-wallet/oko-sdk-core"; + import { OKO_SDK_TARGET } from "./target"; import type { MsgEventContext } from "./types"; import { @@ -6,42 +8,6 @@ import { } from "@oko-wallet-attached/requests/endpoints"; import { useAppState } from "@oko-wallet-attached/store/app"; -// NOTE: Since this method can only be used within user_dashboard, -// it is not exposed to the SDK, and its type is also defined within that file. - -interface ConnectedApp { - customer_id: string; - label: string | null; - logo_url: string | null; - url: string | null; - connected_at: string; - state: string; -} -type GetConnectedAppsError = - | { type: "UNAUTHORIZED_ORIGIN" } - | { type: "NOT_AUTHENTICATED" } - | { type: "FETCH_ERROR"; error: string }; - -interface GetConnectedAppsAckSuccessPayload { - success: true; - data: ConnectedApp[]; -} - -interface GetConnectedAppsAckErrorPayload { - success: false; - error: GetConnectedAppsError; -} - -type GetConnectedAppsAckPayload = - | GetConnectedAppsAckSuccessPayload - | GetConnectedAppsAckErrorPayload; - -interface OkoWalletMsgGetConnectedAppsAck { - target: "oko_sdk"; - msg_type: "__get_connected_apps_ack__"; - payload: GetConnectedAppsAckPayload; -} - export async function handleGetConnectedApps( ctx: MsgEventContext, ): Promise { diff --git a/embed/oko_attached/src/window_msgs/index.ts b/embed/oko_attached/src/window_msgs/index.ts index c8c872a60..beb762b9b 100644 --- a/embed/oko_attached/src/window_msgs/index.ts +++ b/embed/oko_attached/src/window_msgs/index.ts @@ -1,38 +1,29 @@ -import type { OkoWalletMsg } from "@oko-wallet/oko-sdk-core"; +import type { + OkoWalletMsg, + OkoWalletMsgExportPrivateKey, + OkoWalletMsgGetConnectedApps, +} from "@oko-wallet/oko-sdk-core"; import type { AuthType } from "@oko-wallet/oko-types/auth"; -import type { MsgEventContext } from "./types"; -import { useAppState } from "@oko-wallet-attached/store/app"; -import { handleGetPublicKey } from "./get_public_key"; -import { handleGetPublicKeyEd25519 } from "./get_public_key_ed25519"; -import { handleSetOAuthNonce } from "./set_oauth_nonce"; -import { handleSetCodeVerifier } from "./set_code_verifier"; -import { handleOpenModal } from "./open_modal"; -import { handleSignOut } from "./sign_out"; +import { handleExportPrivateKey } from "./export_private_key"; +import { handleGetAuthType } from "./get_auth_type"; +import { handleGetConnectedApps } from "./get_connected_apps"; +import { handleGetCosmosChain } from "./get_cosmos_chain_info"; import { handleGetEmail } from "./get_email"; +import { handleGetEthChain } from "./get_eth_chain_info"; import { handleGetName } from "./get_name"; +import { handleGetPublicKey } from "./get_public_key"; +import { handleGetPublicKeyEd25519 } from "./get_public_key_ed25519"; import { handleGetWalletInfo } from "./get_wallet_info"; -import { handleGetAuthType } from "./get_auth_type"; -import { handleGetCosmosChain } from "./get_cosmos_chain_info"; import { handleOAuthInfoPassV2 } from "./oauth_info_pass"; -import { handleGetEthChain } from "./get_eth_chain_info"; -import { handleGetConnectedApps } from "./get_connected_apps"; -import { handleExportPrivateKey } from "./export_private_key"; - -// NOTE: These methods can only be used within user_dashboard, -// so they are not exposed via the SDK. Define extended types here. -type OkoWalletMsgGetConnectedApps = { - target: "oko_attached"; - msg_type: "__get_connected_apps__"; - payload: null; -}; - -type OkoWalletMsgExportPrivateKey = { - target: "oko_attached"; - msg_type: "__export_private_key__"; - payload: { auth_type: AuthType }; -}; +import { handleOpenModal } from "./open_modal"; +import { handleSetCodeVerifier } from "./set_code_verifier"; +import { handleSetOAuthNonce } from "./set_oauth_nonce"; +import { handleSignOut } from "./sign_out"; +import type { MsgEventContext } from "./types"; +import { useAppState } from "@oko-wallet-attached/store/app"; +// NOTE: Some types are used only within certain apps, such as "user_dashboard" type ExtendedOkoWalletMsg = | OkoWalletMsg | OkoWalletMsgGetConnectedApps diff --git a/sdk/oko_sdk_core/src/types/index.ts b/sdk/oko_sdk_core/src/types/index.ts index 924389a81..7c1b3d391 100644 --- a/sdk/oko_sdk_core/src/types/index.ts +++ b/sdk/oko_sdk_core/src/types/index.ts @@ -1,9 +1,9 @@ -export * from "./oko_wallet"; -export * from "./msg"; -export * from "./sign"; -export * from "./modal"; -export * from "./event"; -export * from "./oauth"; export * from "./cosmos_sign"; +export * from "./event"; export * from "./init"; +export * from "./modal"; +export * from "./msg"; +export * from "./oauth"; +export * from "./oko_wallet"; +export * from "./sign"; export * from "./sign_in"; diff --git a/sdk/oko_sdk_core/src/types/msg/export_priv_key.ts b/sdk/oko_sdk_core/src/types/msg/export_priv_key.ts index 94bec59e8..d0551ed19 100644 --- a/sdk/oko_sdk_core/src/types/msg/export_priv_key.ts +++ b/sdk/oko_sdk_core/src/types/msg/export_priv_key.ts @@ -1,3 +1,11 @@ +import type { AuthType } from "@oko-wallet/oko-types/auth"; + +export type OkoWalletMsgExportPrivateKey = { + target: "oko_attached"; + msg_type: "__export_private_key__"; + payload: { auth_type: AuthType }; +}; + export type ExportPrivateKeyError = | { type: "UNAUTHORIZED_ORIGIN" } | { type: "NOT_AUTHENTICATED" } diff --git a/sdk/oko_sdk_core/src/types/msg/get_conn_apps.ts b/sdk/oko_sdk_core/src/types/msg/get_conn_apps.ts new file mode 100644 index 000000000..3adcc77d1 --- /dev/null +++ b/sdk/oko_sdk_core/src/types/msg/get_conn_apps.ts @@ -0,0 +1,39 @@ +export type OkoWalletMsgGetConnectedApps = { + target: "oko_attached"; + msg_type: "__get_connected_apps__"; + payload: null; +}; + +export interface ConnectedApp { + customer_id: string; + label: string | null; + logo_url: string | null; + url: string | null; + connected_at: string; + state: string; +} + +export type GetConnectedAppsError = + | { type: "UNAUTHORIZED_ORIGIN" } + | { type: "NOT_AUTHENTICATED" } + | { type: "FETCH_ERROR"; error: string }; + +export interface GetConnectedAppsAckSuccessPayload { + success: true; + data: ConnectedApp[]; +} + +export interface GetConnectedAppsAckErrorPayload { + success: false; + error: GetConnectedAppsError; +} + +export type GetConnectedAppsAckPayload = + | GetConnectedAppsAckSuccessPayload + | GetConnectedAppsAckErrorPayload; + +export interface OkoWalletMsgGetConnectedAppsAck { + target: "oko_sdk"; + msg_type: "__get_connected_apps_ack__"; + payload: GetConnectedAppsAckPayload; +} diff --git a/sdk/oko_sdk_core/src/types/msg/index.ts b/sdk/oko_sdk_core/src/types/msg/index.ts index e1391b6b1..177c109c9 100644 --- a/sdk/oko_sdk_core/src/types/msg/index.ts +++ b/sdk/oko_sdk_core/src/types/msg/index.ts @@ -15,6 +15,7 @@ import type { import type { OAuthSignInError } from "@oko-wallet-sdk-core/types/sign_in"; export * from "./export_priv_key"; +export * from "./get_conn_apps"; export type OkoWalletMsgGetPublicKey = { target: "oko_attached"; From 72eb7d2dafa78c5ae9765bf811a4c9e65e89c513 Mon Sep 17 00:00:00 2001 From: Elden Park Date: Wed, 25 Feb 2026 19:51:54 -0800 Subject: [PATCH 61/62] o --- .../src/hooks/use_connected_apps.ts | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/apps/user_dashboard/src/hooks/use_connected_apps.ts b/apps/user_dashboard/src/hooks/use_connected_apps.ts index d66445d4c..d82cf887f 100644 --- a/apps/user_dashboard/src/hooks/use_connected_apps.ts +++ b/apps/user_dashboard/src/hooks/use_connected_apps.ts @@ -1,36 +1,16 @@ -import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response"; -import { useQuery } from "@tanstack/react-query"; - import type { + ConnectedApp, + GetConnectedAppsError, OkoWalletMsgGetConnectedApps, OkoWalletMsgGetConnectedAppsAck, -} from "../../../../sdk/oko_sdk_core/dist/types"; +} from "@oko-wallet/oko-sdk-core"; +import { useQuery } from "@tanstack/react-query"; + import { selectCosmosSDK, useSDKState, } from "@oko-wallet-user-dashboard/state/sdk"; -// Types for connected apps (internal to user_dashboard) -export interface ConnectedApp { - customer_id: string; - label: string | null; - logo_url: string | null; - url: string | null; - connected_at: string; - state: string; -} - -export type GetConnectedAppsError = - | { type: "UNAUTHORIZED_ORIGIN" } - | { type: "NOT_AUTHENTICATED" } - | { type: "FETCH_ERROR"; msg: string }; - -interface GetConnectedAppsAckPayload { - success: boolean; - data?: ConnectedApp[]; - error?: GetConnectedAppsError; -} - type UseConnectedAppsResult = UseConnectedAppsSuccess | UseConnectedAppsError; interface UseConnectedAppsSuccess { @@ -47,7 +27,7 @@ interface UseConnectedAppsError { data: never[]; } -// NOTE The __get_connected_apps__ message should only be called from the user_dashboard, +// NOTE: The __get_connected_apps__ message should only be called from the user_dashboard, // so it is not added to the SDK and is instead called separately in useConnectedApp. export function useConnectedApps(): UseConnectedAppsResult { const cosmosSDK = useSDKState(selectCosmosSDK); From 99efd48136fc1f8913f254a7a7bf85e64d7c6298 Mon Sep 17 00:00:00 2001 From: Elden Park Date: Wed, 25 Feb 2026 19:52:38 -0800 Subject: [PATCH 62/62] project: version bump to v0.1.2-alpha.7 --- lerna.json | 2 +- sdk/oko_cosmos_kit/package.json | 6 +++--- sdk/oko_interchain_kit/package.json | 6 +++--- sdk/oko_sdk_core/package.json | 2 +- sdk/oko_sdk_cosmos/package.json | 4 ++-- sdk/oko_sdk_eth/package.json | 4 ++-- sdk/oko_sdk_svm/package.json | 4 ++-- yarn.lock | 18 +++++++++--------- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lerna.json b/lerna.json index b14538538..ccfa414b5 100644 --- a/lerna.json +++ b/lerna.json @@ -44,6 +44,6 @@ "ui/oko_common_ui" ], "npmClient": "yarn", - "version": "0.1.2-alpha.6", + "version": "0.1.2-alpha.7", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/sdk/oko_cosmos_kit/package.json b/sdk/oko_cosmos_kit/package.json index 19150620b..a00767f8f 100644 --- a/sdk/oko_cosmos_kit/package.json +++ b/sdk/oko_cosmos_kit/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-cosmos-kit", - "version": "0.1.2-alpha.6", + "version": "0.1.2-alpha.7", "type": "module", "description": "cosmos-kit wallet connector for Oko Wallet", "author": "oko-wallet", @@ -38,8 +38,8 @@ "dependencies": { "@cosmos-kit/core": "^2.16.7", "@keplr-wallet/types": "0.12.297", - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", - "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.7", + "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.7" }, "peerDependencies": { "@cosmjs/amino": ">= 0.28", diff --git a/sdk/oko_interchain_kit/package.json b/sdk/oko_interchain_kit/package.json index 7b2dc2858..1c5ab5ba8 100644 --- a/sdk/oko_interchain_kit/package.json +++ b/sdk/oko_interchain_kit/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-interchain-kit", - "version": "0.1.2-alpha.6", + "version": "0.1.2-alpha.7", "type": "module", "description": "interchain-kit wallet connector for Oko Wallet", "author": "oko-wallet", @@ -38,8 +38,8 @@ "dependencies": { "@interchain-kit/core": "^0.3.55", "@keplr-wallet/types": "0.12.297", - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", - "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.7", + "@oko-wallet/oko-sdk-cosmos": "^0.1.2-alpha.7" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.0", diff --git a/sdk/oko_sdk_core/package.json b/sdk/oko_sdk_core/package.json index 7c9df72f9..dd97aadda 100644 --- a/sdk/oko_sdk_core/package.json +++ b/sdk/oko_sdk_core/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-core", - "version": "0.1.2-alpha.6", + "version": "0.1.2-alpha.7", "type": "module", "scripts": { "build": "tsx ./scripts/build.ts", diff --git a/sdk/oko_sdk_cosmos/package.json b/sdk/oko_sdk_cosmos/package.json index a7802fb8e..40cbd5221 100644 --- a/sdk/oko_sdk_cosmos/package.json +++ b/sdk/oko_sdk_cosmos/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-cosmos", - "version": "0.1.2-alpha.6", + "version": "0.1.2-alpha.7", "type": "module", "publishConfig": { "access": "public" @@ -39,7 +39,7 @@ "@keplr-wallet/types": "0.12.297", "@noble/curves": "1.9.7", "@noble/hashes": "^1.8.0", - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.7", "@oko-wallet/stdlib-js": "^0.1.2-alpha.6", "bech32": "^2.0.0", "buffer": "^6.0.3", diff --git a/sdk/oko_sdk_eth/package.json b/sdk/oko_sdk_eth/package.json index baec5dd94..6fd60c93f 100644 --- a/sdk/oko_sdk_eth/package.json +++ b/sdk/oko_sdk_eth/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-eth", - "version": "0.1.2-alpha.6", + "version": "0.1.2-alpha.7", "main": "./dist/index.js", "types": "./dist/index.d.ts", "type": "module", @@ -39,7 +39,7 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.7", "@oko-wallet/stdlib-js": "^0.1.2-alpha.6", "eventemitter3": "^5.0.1", "uuid": "^13.0.0", diff --git a/sdk/oko_sdk_svm/package.json b/sdk/oko_sdk_svm/package.json index a86f2f396..8ef669e77 100644 --- a/sdk/oko_sdk_svm/package.json +++ b/sdk/oko_sdk_svm/package.json @@ -1,6 +1,6 @@ { "name": "@oko-wallet/oko-sdk-svm", - "version": "0.1.2-alpha.6", + "version": "0.1.2-alpha.7", "main": "./dist/index.js", "types": "./dist/index.d.ts", "type": "module", @@ -36,7 +36,7 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.6", + "@oko-wallet/oko-sdk-core": "^0.1.2-alpha.7", "@oko-wallet/stdlib-js": "^0.0.2-rc.42", "@solana/wallet-standard-features": "^1.3.0", "@solana/web3.js": "^1.98.0", diff --git a/yarn.lock b/yarn.lock index 84b068f98..11077543f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11130,8 +11130,8 @@ __metadata: dependencies: "@cosmos-kit/core": "npm:^2.16.7" "@keplr-wallet/types": "npm:0.12.297" - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" - "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.7" + "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.7" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" "@rollup/plugin-typescript": "npm:^11.0.0" @@ -11152,8 +11152,8 @@ __metadata: dependencies: "@interchain-kit/core": "npm:^0.3.55" "@keplr-wallet/types": "npm:0.12.297" - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" - "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.7" + "@oko-wallet/oko-sdk-cosmos": "npm:^0.1.2-alpha.7" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-node-resolve": "npm:^15.0.0" "@rollup/plugin-typescript": "npm:^11.0.0" @@ -11201,7 +11201,7 @@ __metadata: languageName: node linkType: hard -"@oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.6, @oko-wallet/oko-sdk-core@workspace:sdk/oko_sdk_core": +"@oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-core@npm:^0.1.2-alpha.7, @oko-wallet/oko-sdk-core@workspace:sdk/oko_sdk_core": version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-core@workspace:sdk/oko_sdk_core" dependencies: @@ -11250,7 +11250,7 @@ __metadata: languageName: node linkType: hard -"@oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.6, @oko-wallet/oko-sdk-cosmos@workspace:sdk/oko_sdk_cosmos": +"@oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.1, @oko-wallet/oko-sdk-cosmos@npm:^0.1.2-alpha.7, @oko-wallet/oko-sdk-cosmos@workspace:sdk/oko_sdk_cosmos": version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-cosmos@workspace:sdk/oko_sdk_cosmos" dependencies: @@ -11260,7 +11260,7 @@ __metadata: "@keplr-wallet/types": "npm:0.12.297" "@noble/curves": "npm:1.9.7" "@noble/hashes": "npm:^1.8.0" - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.7" "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-json": "npm:^6.1.0" @@ -11288,7 +11288,7 @@ __metadata: version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-eth@workspace:sdk/oko_sdk_eth" dependencies: - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.7" "@oko-wallet/stdlib-js": "npm:^0.1.2-alpha.6" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-json": "npm:^6.1.0" @@ -11317,7 +11317,7 @@ __metadata: version: 0.0.0-use.local resolution: "@oko-wallet/oko-sdk-svm@workspace:sdk/oko_sdk_svm" dependencies: - "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.6" + "@oko-wallet/oko-sdk-core": "npm:^0.1.2-alpha.7" "@oko-wallet/stdlib-js": "npm:^0.0.2-rc.42" "@rollup/plugin-commonjs": "npm:^25.0.0" "@rollup/plugin-node-resolve": "npm:^15.0.0"