diff --git a/.changeset/empty-planets-float.md b/.changeset/empty-planets-float.md new file mode 100644 index 00000000..3edc9c0b --- /dev/null +++ b/.changeset/empty-planets-float.md @@ -0,0 +1,5 @@ +--- +"@treasure-dev/tdk-react": patch +--- + +Updated to handle attempts to log in with custom auth endpoint diff --git a/.changeset/orange-turkeys-walk.md b/.changeset/orange-turkeys-walk.md new file mode 100644 index 00000000..8bf7e526 --- /dev/null +++ b/.changeset/orange-turkeys-walk.md @@ -0,0 +1,5 @@ +--- +"@treasure-dev/tdk-core": patch +--- + +Added support for login with custom auth endpoint diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 7a080121..c3abbf75 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -9,6 +9,8 @@ import "../middleware/chain"; import "../middleware/swagger"; import type { LoginBody, + LoginCustomBody, + LoginCustomReply, LoginReply, ReadLoginPayloadQuerystring, ReadLoginPayloadReply, @@ -16,18 +18,27 @@ import type { import { type ErrorReply, loginBodySchema, + loginCustomBodySchema, + loginCustomReplySchema, loginReplySchema, readLoginPayloadQuerystringSchema, readLoginPayloadReplySchema, } from "../schema"; import type { TdkApiContext } from "../types"; import { USER_PROFILE_SELECT_FIELDS, USER_SELECT_FIELDS } from "../utils/db"; +import { throwUnauthorizedError } from "../utils/error"; import { log } from "../utils/log"; import { getThirdwebUser, parseThirdwebUserEmail, transformUserProfileResponseFields, } from "../utils/user"; +import { validateWanderersUser } from "../utils/wanderers"; + +type LoginCustomPayload = { + wanderersCookie?: string; + wanderersToken?: string; +}; export const authRoutes = ({ @@ -193,4 +204,37 @@ export const authRoutes = }); }, ); + + app.post<{ Body: LoginCustomBody; Reply: LoginCustomReply | ErrorReply }>( + "/login/custom", + { + schema: { + summary: "Log in with custom auth", + description: "Log in with a custom auth payload", + body: loginCustomBodySchema, + response: { + 200: loginCustomReplySchema, + }, + }, + }, + async (req, reply) => { + let payload: LoginCustomPayload | undefined; + + try { + payload = JSON.parse(req.body.payload); + } catch (err) { + log.error("Error parsing custom login payload:", err); + } + + if (payload?.wanderersCookie || payload?.wanderersToken) { + const user = await validateWanderersUser( + payload.wanderersCookie, + payload.wanderersToken, + ); + return reply.send(user); + } + + throwUnauthorizedError("Invalid request"); + }, + ); }; diff --git a/apps/api/src/schema/auth.ts b/apps/api/src/schema/auth.ts index 9d0dfa8b..565bb9dc 100644 --- a/apps/api/src/schema/auth.ts +++ b/apps/api/src/schema/auth.ts @@ -49,9 +49,21 @@ export const loginReplySchema = Type.Object({ ]), }); +export const loginCustomBodySchema = Type.Object({ + payload: Type.String(), +}); + +export const loginCustomReplySchema = Type.Object({ + userId: Type.String(), + email: Type.String(), + exp: Type.Number(), +}); + export type ReadLoginPayloadQuerystring = Static< typeof readLoginPayloadQuerystringSchema >; export type ReadLoginPayloadReply = Static; export type LoginBody = Static; export type LoginReply = Static; +export type LoginCustomBody = Static; +export type LoginCustomReply = Static; diff --git a/apps/api/src/utils/wanderers.ts b/apps/api/src/utils/wanderers.ts new file mode 100644 index 00000000..cba32e4f --- /dev/null +++ b/apps/api/src/utils/wanderers.ts @@ -0,0 +1,35 @@ +import type { LoginCustomReply } from "../schema"; + +const WANDERERS_API_URL = "https://id.wanderers.ai/sessions/whoami"; + +type WanderersSession = { + active: boolean; + expires_at: string; + identity: { + id: string; + traits: { + email: string; + }; + }; +}; + +export const validateWanderersUser = async ( + cookie?: string, + token?: string, +): Promise => { + const response = await fetch(WANDERERS_API_URL, { + headers: cookie ? { Cookie: cookie } : { "X-Session-Token": token ?? "" }, + }); + + const session: WanderersSession = await response.json(); + const expiresAt = new Date(session.expires_at).getTime(); + if (!session.active || expiresAt < Date.now()) { + return undefined; + } + + return { + userId: session.identity.id, + email: session.identity.traits.email, + exp: Math.floor(expiresAt / 1000), + }; +}; diff --git a/examples/connect-core/src/main.ts b/examples/connect-core/src/main.ts index 2dd047cb..9cd46c74 100644 --- a/examples/connect-core/src/main.ts +++ b/examples/connect-core/src/main.ts @@ -57,6 +57,27 @@ document.querySelector("#app")!.innerHTML = ` +

+ or +

+
+

Connect with Custom Auth

+
+ + + +
+

Logged in as

@@ -86,6 +107,13 @@ document.querySelector("#app")!.innerHTML = ` const emailButton = emailContainer.querySelector("button"); const codeInput = codeContainer.querySelector("input")!; const codeButton = codeContainer.querySelector("button"); + const customAuthKeyInput = + document.querySelector("#custom-auth-key")!; + const customAuthValueInput = + document.querySelector("#custom-auth-value")!; + const customAuthButton = document.querySelector( + "#custom-auth-container button", + ); const mintButton = userContainer.querySelector("#mint"); const logOutButton = userContainer.querySelector("#log-out"); @@ -169,6 +197,34 @@ document.querySelector("#app")!.innerHTML = ` codeButton.disabled = false; }); + // Set up Connect with Custom Auth flow + customAuthButton?.addEventListener("click", async () => { + customAuthButton.disabled = true; + try { + const result = await logIn({ + client, + ecosystemId: import.meta.env.VITE_TDK_ECOSYSTEM_ID, + ecosystemPartnerId: import.meta.env.VITE_TDK_ECOSYSTEM_PARTNER_ID, + method: "auth_endpoint", + payload: JSON.stringify({ + [customAuthKeyInput.value]: customAuthValueInput.value, + }), + apiUri, + chainId, + sessionOptions, + }); + tdk = result.tdk; + user = result.user; + userEmail.innerHTML = user.email || user.id; + connectContainer.hidden = true; + userContainer.hidden = false; + } catch (err) { + console.error("Error logging in with email:", err); + } + + customAuthButton.disabled = false; + }); + // Set up Mint button mintButton?.addEventListener("click", async () => { mintButton.disabled = true; diff --git a/package.json b/package.json index 393fc302..7ef99dd0 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dev:launcher": "pnpm --filter ./packages/launcher dev", "dev:api": "pnpm --filter ./apps/api dev", "dev:react": "pnpm --filter ./packages/react dev", + "dev:connect-core": "pnpm --filter ./examples/connect-core dev", "dev:connect-electron": "pnpm --filter ./examples/connect-electron dev", "dev:connect-react": "pnpm --filter ./examples/connect-react dev", "start:api": "pnpm --filter ./apps/api start", diff --git a/packages/core/src/connect/login.ts b/packages/core/src/connect/login.ts index e908909c..5dad7b30 100644 --- a/packages/core/src/connect/login.ts +++ b/packages/core/src/connect/login.ts @@ -47,7 +47,8 @@ export type ConnectMethod = | (typeof SUPPORTED_SOCIAL_OPTIONS)[number] | "email" | "passkey" - | "wallet"; + | "wallet" + | "auth_endpoint"; type ConnectWalletConfig = { client: TreasureConnectClient; @@ -70,6 +71,10 @@ type ConnectWalletConfig = { passkeyName?: string; hasStoredPasskey?: boolean; } + | { + method: "auth_endpoint"; + payload: string; + } ); export const isSocialConnectMethod = (method: ConnectMethod) => @@ -115,6 +120,15 @@ export const connectEcosystemWallet = async (params: ConnectWalletConfig) => { type: hasPasskey ? "sign-in" : "sign-up", passkeyName: params.passkeyName, }); + } else if (params.method === "auth_endpoint") { + // Connect with auth endpoint + await wallet.connect({ + client, + chain, + strategy: "auth_endpoint", + payload: params.payload, + encryptionKey: "any", // Unused with enclave ecosystem wallets + }); } else { // Connect with social await wallet.connect({ @@ -214,7 +228,11 @@ export const logIn = async (params: ConnectWalletConfig & ConnectConfig) => { ...connectWalletParams } = params; - const wallet = await connectWallet({ client, ...connectWalletParams }); + const wallet = await connectWallet({ + client, + chainId, + ...connectWalletParams, + }); const tdk = new TDKAPI({ baseUri: apiUri, diff --git a/packages/react/src/components/connect/ConnectModal.tsx b/packages/react/src/components/connect/ConnectModal.tsx index bc2f94cf..d60ff07d 100644 --- a/packages/react/src/components/connect/ConnectModal.tsx +++ b/packages/react/src/components/connect/ConnectModal.tsx @@ -214,6 +214,10 @@ export const ConnectModal = ({ setError(err); onConnectError?.(method, err); } + } else if (method === "auth_endpoint") { + throw new Error( + "Auth endpoint not supported in Treasure Connect modal. Use TDK Core package to connect.", + ); } else { // Handle connecting with social / passkey try {