@tangle-network/agent-app 0.49.37 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { type Auth, type BetterAuthOptions, type Session, type User } from 'better-auth';
21
21
  import { type AuthGuard } from '../platform/guards';
22
- import { type TangleSsoAccountStore, type TangleSsoAuthClient, type TangleSsoHandlers, type TangleOidcSsoAccountStore, type TangleOidcSsoAuthClient } from '../platform/sso';
22
+ import { type TangleSsoAccountStore, type TangleSsoAuthClient, type TangleSsoHandlers, type TangleIdentitySsoAccountStore, type TangleIdentitySsoAuthClient, type TangleOidcSsoAccountStore, type TangleOidcSsoAuthClient } from '../platform/sso';
23
23
  /** Structural slice of a Resend-style client — no `resend` import. */
24
24
  export interface AppAuthEmailClient {
25
25
  emails: {
@@ -96,6 +96,12 @@ export interface AppAuthOidcSsoConfig extends Omit<AppAuthSsoConfig, 'protocol'
96
96
  challenge: string;
97
97
  }>;
98
98
  }
99
+ /** First-party identity login; no Platform API key is issued or stored. */
100
+ export interface AppAuthIdentitySsoConfig extends Omit<AppAuthSsoConfig, 'protocol' | 'client' | 'store'> {
101
+ protocol: 'identity';
102
+ client: TangleIdentitySsoAuthClient;
103
+ store: TangleIdentitySsoAccountStore;
104
+ }
99
105
  /** Define the structure for application authentication data including users, sessions, accounts, and verifications */
100
106
  export interface AppAuthSchema {
101
107
  users: unknown;
@@ -143,7 +149,7 @@ export interface AppAuthConfig {
143
149
  * Default 300. */
144
150
  sessionCookieCacheSeconds?: number | false;
145
151
  /** Tangle cross-site SSO (start/callback handlers). */
146
- sso?: AppAuthSsoConfig | AppAuthOidcSsoConfig;
152
+ sso?: AppAuthSsoConfig | AppAuthOidcSsoConfig | AppAuthIdentitySsoConfig;
147
153
  /** Where guards redirect unauthenticated page requests. Default '/login'. */
148
154
  loginPath?: string;
149
155
  /** Merged over the factory's `advanced` block (cookiePrefix stays unless
@@ -2,7 +2,7 @@ import {
2
2
  createAuthGuard,
3
3
  createBetterAuthSessionCookieMinter,
4
4
  createTangleSsoHandlers
5
- } from "../chunk-MVIIH2EQ.js";
5
+ } from "../chunk-MUABCTFT.js";
6
6
  import "../chunk-JML7WKWU.js";
7
7
  import "../chunk-EA4UVS4T.js";
8
8
  import "../chunk-TXD5HXLE.js";
@@ -164,7 +164,7 @@ function createAppAuth(config) {
164
164
  "createAppAuth: sso requires `secret` (or sso.stateSecret) \u2014 the signed-state CSRF cookie needs an HMAC secret"
165
165
  );
166
166
  }
167
- const transport = config.sso.protocol === "oidc" ? { protocol: "oidc", auth: config.sso.client, store: config.sso.store, createPkcePair: config.sso.createPkcePair } : { protocol: "legacy", auth: config.sso.client, store: config.sso.store };
167
+ const transport = config.sso.protocol === "oidc" ? { protocol: "oidc", auth: config.sso.client, store: config.sso.store, createPkcePair: config.sso.createPkcePair } : config.sso.protocol === "identity" ? { protocol: "identity", auth: config.sso.client, store: config.sso.store } : { protocol: "legacy", auth: config.sso.client, store: config.sso.store };
168
168
  sso = createTangleSsoHandlers({
169
169
  ...transport,
170
170
  stateSecret,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/app-auth/index.ts"],"sourcesContent":["/**\n * better-auth config factory for agent products (#188 Phase 1). Every product\n * hand-rolls the same ~70–150 line setup — drizzle adapter over the standard\n * users/sessions/accounts/verifications tables, email+password with Resend\n * reset/verification mail, env-gated GitHub/Google social providers, session\n * cookie cache, and a per-app cookie prefix — and tax additionally\n * re-implemented better-auth's cookie signing for its Tangle SSO callback.\n * `createAppAuth` owns that mechanism once and returns the configured\n * better-auth instance plus the request guards products actually use.\n *\n * Domain stays a parameter: the drizzle db + schema, email client, provider\n * credentials, and SSO store/client all come from the product. No product\n * import, no engine re-implementation — the SSO path composes the existing\n * `platform/sso` cookie minter (`createBetterAuthSessionCookieMinter`), which\n * is byte-compatible with better-auth's own `makeSignature` contract.\n *\n * better-auth is an OPTIONAL peer: only this subpath imports it, so it is\n * deliberately NOT re-exported from the root barrel.\n */\n\nimport { betterAuth, type Auth, type BetterAuthOptions, type Session, type User } from 'better-auth'\nimport { drizzleAdapter } from 'better-auth/adapters/drizzle'\nimport { createAuthGuard, type AuthGuard } from '../platform/guards'\nimport {\n createBetterAuthSessionCookieMinter,\n createTangleSsoHandlers,\n type TangleSsoAccountStore,\n type TangleSsoAuthClient,\n type TangleSsoHandlers,\n type TangleOidcSsoAccountStore,\n type TangleOidcSsoAuthClient,\n} from '../platform/sso'\n\nconst DEFAULT_STATE_COOKIE = 'tangle_sso_state'\nconst DEFAULT_SESSION_COOKIE_CACHE_SECONDS = 5 * 60\n\n/** Structural slice of a Resend-style client — no `resend` import. */\nexport interface AppAuthEmailClient {\n emails: {\n send(message: {\n from: string\n to: string\n subject: string\n html: string\n text?: string\n }): Promise<unknown>\n }\n}\n\n/** Define email configuration for app authentication including client, sender, verification, and warning options */\nexport interface AppAuthEmailConfig {\n /** A Resend-style client, or a lazy getter returning null when the API key\n * is absent (the products' dev default — mail is skipped with a warning,\n * sign-up itself must not crash). */\n resend: AppAuthEmailClient | (() => AppAuthEmailClient | null)\n /** RFC 5322 From, e.g. `'Legal Agent <noreply@legal.tangle.tools>'`. */\n from: string\n /** Send a verification email on sign-up and auto-sign-in after verifying\n * (the tax/gtm behavior). Default false (the legal behavior). */\n verifyOnSignUp?: boolean\n /** Receives the \"email client unavailable\" warning. Default console.warn. */\n warn?: (message: string) => void\n}\n\n/** Env-shaped: pass the env vars straight through; the provider is registered\n * only when BOTH values are non-empty, so unset env disables it. */\nexport interface AppAuthSocialProviderConfig {\n clientId?: string\n clientSecret?: string\n}\n\n/** Define social authentication configuration options for GitHub and Google providers */\nexport interface AppAuthSocialConfig {\n github?: AppAuthSocialProviderConfig\n google?: AppAuthSocialProviderConfig\n}\n\n/** Cross-site Tangle SSO wiring. The factory supplies the better-auth side —\n * `setSessionCookie` via `createBetterAuthSessionCookieMinter(auth)` (signed\n * `__Secure-`/prefixed cookie that `auth.api.getSession` accepts) — so the\n * product no longer touches `better-auth/crypto` itself. */\nexport interface AppAuthSsoConfig {\n protocol?: 'legacy'\n /** Platform wire client (authorizeUrl + exchange). */\n client: TangleSsoAuthClient\n /** Product persistence: user upsert, session row, platform link. */\n store: TangleSsoAccountStore\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n /** Default 'tangle_sso_state'. */\n stateCookieName?: string\n /** HMAC secret for the CSRF state cookie. Default: the auth `secret`. */\n stateSecret?: string\n /** Default: `baseURL` is https. Must match better-auth's own\n * secure-cookie decision or the cookie name lookup diverges. */\n secureCookies?: boolean\n sessionTtlSeconds?: number\n stateTtlSeconds?: number\n /** Post-login redirect fallback. Default '/app'. */\n defaultRedirectPath?: string\n /** Default: the top-level `loginPath`. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n}\n\n/** Registered OIDC client configuration. The caller supplies Runtime PKCE; login does not mint an API key. */\nexport interface AppAuthOidcSsoConfig extends Omit<AppAuthSsoConfig, 'protocol' | 'client' | 'store'> {\n protocol: 'oidc'\n client: TangleOidcSsoAuthClient\n store: TangleOidcSsoAccountStore\n /** Pass the Runtime's `createPkcePair` helper from the OIDC-only caller. */\n createPkcePair: () => Promise<{ verifier: string; challenge: string }>\n}\n\n/** Define the structure for application authentication data including users, sessions, accounts, and verifications */\nexport interface AppAuthSchema {\n users: unknown\n sessions: unknown\n accounts: unknown\n verifications: unknown\n}\n\n/** Define configuration settings for app authentication including app name, base URL, secrets, and trusted origins */\nexport interface AppAuthConfig {\n /** Product name — used in email subjects and as the cookie-prefix default. */\n appName: string\n /** Absolute origin better-auth serves from (`BETTER_AUTH_URL`). */\n baseURL: string\n /** better-auth HMAC secret. Optional only because better-auth falls back to\n * the BETTER_AUTH_SECRET env var; `sso` needs it explicitly (or\n * `sso.stateSecret`). */\n secret?: string\n trustedOrigins?: string[]\n /**\n * Session-cookie prefix. Default: slugified `appName`. A per-app prefix is\n * load-bearing, not cosmetic: the platform (id.tangle.tools) mints its own\n * better-auth cookie `Domain=.tangle.tools`-wide under the DEFAULT name, and\n * the platform's (older) cookie wins the Cookie-header order — an app on the\n * default prefix reads the platform's token, fails its own signature check,\n * and every fresh login lands back on /login.\n */\n cookiePrefix?: string\n /** Drizzle database instance; wired through better-auth's drizzle adapter\n * together with `schema`. */\n db?: unknown\n /** The product's users/sessions/accounts/verifications tables (mapped to\n * better-auth's user/session/account/verification models). */\n schema?: AppAuthSchema\n /** Drizzle dialect. Default 'sqlite' (D1). */\n provider?: 'sqlite' | 'pg' | 'mysql'\n /** Escape hatch: a pre-built better-auth database adapter (e.g.\n * `memoryAdapter` in tests). Wins over `db`/`schema`. */\n database?: BetterAuthOptions['database']\n /** Email+password sign-in. Default true (all current products enable it). */\n emailAndPassword?: boolean\n /** Reset/verification mail. Omit to disable password reset entirely. */\n email?: AppAuthEmailConfig\n social?: AppAuthSocialConfig\n /** Session cookie-cache TTL in seconds; `false` disables the cache.\n * Default 300. */\n sessionCookieCacheSeconds?: number | false\n /** Tangle cross-site SSO (start/callback handlers). */\n sso?: AppAuthSsoConfig | AppAuthOidcSsoConfig\n /** Where guards redirect unauthenticated page requests. Default '/login'. */\n loginPath?: string\n /** Merged over the factory's `advanced` block (cookiePrefix stays unless\n * overridden here). */\n advanced?: BetterAuthOptions['advanced']\n}\n\n/** The configured better-auth instance, typed at better-auth's base surface\n * (`handler`, `api`, `$context`, `$Infer`). */\nexport type AppAuthInstance = Auth\n\n/** What `getSession`/guards resolve: better-auth's base session + user rows. */\nexport interface AppAuthSession {\n session: Session\n user: User\n}\n\n/** Define authentication guard with session retrieval and optional SSO handlers for app requests */\nexport interface AppAuth extends AuthGuard<AppAuthSession> {\n auth: AppAuthInstance\n /** `auth.api.getSession` over a `Request` — the seam the guards consume. */\n getSession(request: Request): Promise<AppAuthSession | null>\n /** Tangle SSO start/callback handlers; null unless `sso` was configured. */\n sso: TangleSsoHandlers | null\n}\n\n/** Lowercased, non-alphanumerics collapsed to '-': `'Legal Agent'` → `'legal-agent'`. */\nfunction slugify(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\nfunction resolveEmailClient(email: AppAuthEmailConfig): AppAuthEmailClient | null {\n return typeof email.resend === 'function' ? email.resend() : email.resend\n}\n\ntype DrizzleAuthAdapter = ReturnType<ReturnType<typeof drizzleAdapter>>\n\n/** Better Auth reads adapter identity during startup, before Workers inject D1.\n * Build the Drizzle adapter on its first database operation. */\nfunction deferDrizzleAdapter(\n db: Record<string, unknown>,\n config: Parameters<typeof drizzleAdapter>[1],\n): ReturnType<typeof drizzleAdapter> {\n return (options) => {\n let adapter: DrizzleAuthAdapter | undefined\n const resolve = () => (adapter ??= drizzleAdapter(db, config)(options))\n\n return {\n id: 'drizzle',\n create: (data) => resolve().create(data),\n findOne: (data) => resolve().findOne(data),\n findMany: (data) => resolve().findMany(data),\n count: (data) => resolve().count(data),\n update: (data) => resolve().update(data),\n updateMany: (data) => resolve().updateMany(data),\n delete: (data) => resolve().delete(data),\n deleteMany: (data) => resolve().deleteMany(data),\n consumeOne: (data) => resolve().consumeOne(data),\n incrementOne: (data) => resolve().incrementOne(data),\n transaction: (callback) => resolve().transaction(callback),\n }\n }\n}\n\n/** Resend reports failures in the resolved value, not by rejecting — surface\n * them loud so better-auth's flow (and the caller's logs) see the failure. */\nasync function sendEmail(\n client: AppAuthEmailClient,\n message: { from: string; to: string; subject: string; html: string; text: string },\n): Promise<void> {\n const result = (await client.emails.send(message)) as { error?: { message?: string } | null } | null | undefined\n if (result && typeof result === 'object' && result.error) {\n throw new Error(`[app-auth] email send failed: ${result.error.message ?? 'unknown error'}`)\n }\n}\n\nfunction resolveDatabase(config: AppAuthConfig): NonNullable<BetterAuthOptions['database']> {\n if (config.database) return config.database\n if (config.db && config.schema) {\n return deferDrizzleAdapter(config.db as Record<string, unknown>, {\n provider: config.provider ?? 'sqlite',\n schema: {\n user: config.schema.users,\n session: config.schema.sessions,\n account: config.schema.accounts,\n verification: config.schema.verifications,\n } as Record<string, unknown>,\n })\n }\n throw new Error(\n 'createAppAuth requires a database: pass `db` + `schema` (drizzle) or `database` (a better-auth adapter)',\n )\n}\n\nfunction resolveSocialProviders(social: AppAuthSocialConfig | undefined): BetterAuthOptions['socialProviders'] {\n const providers: NonNullable<BetterAuthOptions['socialProviders']> = {}\n if (social?.github?.clientId && social.github.clientSecret) {\n providers.github = { clientId: social.github.clientId, clientSecret: social.github.clientSecret }\n }\n if (social?.google?.clientId && social.google.clientSecret) {\n providers.google = { clientId: social.google.clientId, clientSecret: social.google.clientSecret }\n }\n return Object.keys(providers).length > 0 ? providers : undefined\n}\n\nfunction emailAndPasswordOptions(config: AppAuthConfig): BetterAuthOptions['emailAndPassword'] {\n const email = config.email\n if (!email) return { enabled: true }\n const warn = email.warn ?? ((message: string) => console.warn(message))\n return {\n enabled: true,\n sendResetPassword: async ({ user, url }) => {\n const client = resolveEmailClient(email)\n if (!client) {\n warn('[app-auth] email client unavailable — password reset email not sent')\n return\n }\n await sendEmail(client, {\n from: email.from,\n to: user.email,\n subject: 'Reset your password',\n html: `<p>Click the link below to reset your password:</p><p><a href=\"${url}\">${url}</a></p><p>This link expires in 1 hour.</p>`,\n text: `Reset your password:\\n\\n${url}\\n\\nThis link expires in 1 hour.`,\n })\n },\n }\n}\n\nfunction emailVerificationOptions(config: AppAuthConfig): BetterAuthOptions['emailVerification'] {\n const email = config.email\n if (!email?.verifyOnSignUp) return undefined\n const warn = email.warn ?? ((message: string) => console.warn(message))\n return {\n sendOnSignUp: true,\n autoSignInAfterVerification: true,\n sendVerificationEmail: async ({ user, url }) => {\n const client = resolveEmailClient(email)\n if (!client) {\n warn('[app-auth] email client unavailable — verification email not sent')\n return\n }\n await sendEmail(client, {\n from: email.from,\n to: user.email,\n subject: `Verify your ${config.appName} email`,\n html: `<p>Verify your email to finish accessing ${config.appName}:</p><p><a href=\"${url}\">${url}</a></p><p>This link expires in 1 hour.</p>`,\n text: `Verify your email to finish accessing ${config.appName}:\\n\\n${url}\\n\\nThis link expires in 1 hour.`,\n })\n },\n }\n}\n\n/**\n * Build the product's better-auth instance plus the request-boundary helpers:\n * `getSession` (Request → session|null), the `createAuthGuard` quartet\n * (`requireUser` 302, `requireApiUser` JSON 401, `requireSession`,\n * `getOptionalSession`), and — when `sso` is configured — the Tangle SSO\n * start/callback handlers with the session cookie minted through better-auth's\n * own name/attributes/signing (no `better-auth/crypto` in product code).\n */\nexport function createAppAuth(config: AppAuthConfig): AppAuth {\n if (!config.appName) throw new Error('createAppAuth: appName is required')\n if (!config.baseURL) throw new Error('createAppAuth: baseURL is required')\n\n const cookiePrefix = config.cookiePrefix ?? slugify(config.appName)\n if (!cookiePrefix) throw new Error('createAppAuth: cookiePrefix (or a slugifiable appName) is required')\n\n const socialProviders = resolveSocialProviders(config.social)\n const options: BetterAuthOptions = {\n appName: config.appName,\n baseURL: config.baseURL,\n ...(config.secret ? { secret: config.secret } : {}),\n ...(config.trustedOrigins ? { trustedOrigins: config.trustedOrigins } : {}),\n database: resolveDatabase(config),\n ...(config.emailAndPassword === false ? {} : { emailAndPassword: emailAndPasswordOptions(config) }),\n ...(config.email?.verifyOnSignUp ? { emailVerification: emailVerificationOptions(config) } : {}),\n ...(socialProviders ? { socialProviders } : {}),\n ...(config.sessionCookieCacheSeconds === false\n ? {}\n : {\n session: {\n cookieCache: {\n enabled: true,\n maxAge: config.sessionCookieCacheSeconds ?? DEFAULT_SESSION_COOKIE_CACHE_SECONDS,\n },\n },\n }),\n advanced: { cookiePrefix, ...config.advanced },\n }\n\n const auth: AppAuthInstance = betterAuth(options)\n\n const getSession = async (request: Request): Promise<AppAuthSession | null> => {\n const session = await auth.api.getSession({ headers: request.headers })\n return (session as AppAuthSession | null) ?? null\n }\n\n const loginPath = config.loginPath ?? '/login'\n const guard = createAuthGuard<AppAuthSession>({ getSession, loginPath })\n\n let sso: TangleSsoHandlers | null = null\n if (config.sso) {\n const stateSecret = config.sso.stateSecret ?? config.secret\n if (!stateSecret) {\n throw new Error(\n 'createAppAuth: sso requires `secret` (or sso.stateSecret) — the signed-state CSRF cookie needs an HMAC secret',\n )\n }\n const transport = config.sso.protocol === 'oidc'\n ? { protocol: 'oidc' as const, auth: config.sso.client, store: config.sso.store, createPkcePair: config.sso.createPkcePair }\n : { protocol: 'legacy' as const, auth: config.sso.client, store: config.sso.store }\n sso = createTangleSsoHandlers({\n ...transport,\n stateSecret,\n callbackUrl: config.sso.callbackUrl,\n stateCookieName: config.sso.stateCookieName ?? DEFAULT_STATE_COOKIE,\n setSessionCookie: createBetterAuthSessionCookieMinter(auth),\n secureCookies: config.sso.secureCookies ?? config.baseURL.startsWith('https:'),\n ...(config.sso.sessionTtlSeconds !== undefined ? { sessionTtlSeconds: config.sso.sessionTtlSeconds } : {}),\n ...(config.sso.stateTtlSeconds !== undefined ? { stateTtlSeconds: config.sso.stateTtlSeconds } : {}),\n ...(config.sso.defaultRedirectPath ? { defaultRedirectPath: config.sso.defaultRedirectPath } : {}),\n loginPath: config.sso.loginPath ?? loginPath,\n ...(config.sso.log ? { log: config.sso.log } : {}),\n })\n }\n\n return { auth, getSession, ...guard, sso }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/app-auth/index.ts"],"sourcesContent":["/**\n * better-auth config factory for agent products (#188 Phase 1). Every product\n * hand-rolls the same ~70–150 line setup — drizzle adapter over the standard\n * users/sessions/accounts/verifications tables, email+password with Resend\n * reset/verification mail, env-gated GitHub/Google social providers, session\n * cookie cache, and a per-app cookie prefix — and tax additionally\n * re-implemented better-auth's cookie signing for its Tangle SSO callback.\n * `createAppAuth` owns that mechanism once and returns the configured\n * better-auth instance plus the request guards products actually use.\n *\n * Domain stays a parameter: the drizzle db + schema, email client, provider\n * credentials, and SSO store/client all come from the product. No product\n * import, no engine re-implementation — the SSO path composes the existing\n * `platform/sso` cookie minter (`createBetterAuthSessionCookieMinter`), which\n * is byte-compatible with better-auth's own `makeSignature` contract.\n *\n * better-auth is an OPTIONAL peer: only this subpath imports it, so it is\n * deliberately NOT re-exported from the root barrel.\n */\n\nimport { betterAuth, type Auth, type BetterAuthOptions, type Session, type User } from 'better-auth'\nimport { drizzleAdapter } from 'better-auth/adapters/drizzle'\nimport { createAuthGuard, type AuthGuard } from '../platform/guards'\nimport {\n createBetterAuthSessionCookieMinter,\n createTangleSsoHandlers,\n type TangleSsoAccountStore,\n type TangleSsoAuthClient,\n type TangleSsoHandlers,\n type TangleIdentitySsoAccountStore,\n type TangleIdentitySsoAuthClient,\n type TangleOidcSsoAccountStore,\n type TangleOidcSsoAuthClient,\n} from '../platform/sso'\n\nconst DEFAULT_STATE_COOKIE = 'tangle_sso_state'\nconst DEFAULT_SESSION_COOKIE_CACHE_SECONDS = 5 * 60\n\n/** Structural slice of a Resend-style client — no `resend` import. */\nexport interface AppAuthEmailClient {\n emails: {\n send(message: {\n from: string\n to: string\n subject: string\n html: string\n text?: string\n }): Promise<unknown>\n }\n}\n\n/** Define email configuration for app authentication including client, sender, verification, and warning options */\nexport interface AppAuthEmailConfig {\n /** A Resend-style client, or a lazy getter returning null when the API key\n * is absent (the products' dev default — mail is skipped with a warning,\n * sign-up itself must not crash). */\n resend: AppAuthEmailClient | (() => AppAuthEmailClient | null)\n /** RFC 5322 From, e.g. `'Legal Agent <noreply@legal.tangle.tools>'`. */\n from: string\n /** Send a verification email on sign-up and auto-sign-in after verifying\n * (the tax/gtm behavior). Default false (the legal behavior). */\n verifyOnSignUp?: boolean\n /** Receives the \"email client unavailable\" warning. Default console.warn. */\n warn?: (message: string) => void\n}\n\n/** Env-shaped: pass the env vars straight through; the provider is registered\n * only when BOTH values are non-empty, so unset env disables it. */\nexport interface AppAuthSocialProviderConfig {\n clientId?: string\n clientSecret?: string\n}\n\n/** Define social authentication configuration options for GitHub and Google providers */\nexport interface AppAuthSocialConfig {\n github?: AppAuthSocialProviderConfig\n google?: AppAuthSocialProviderConfig\n}\n\n/** Cross-site Tangle SSO wiring. The factory supplies the better-auth side —\n * `setSessionCookie` via `createBetterAuthSessionCookieMinter(auth)` (signed\n * `__Secure-`/prefixed cookie that `auth.api.getSession` accepts) — so the\n * product no longer touches `better-auth/crypto` itself. */\nexport interface AppAuthSsoConfig {\n protocol?: 'legacy'\n /** Platform wire client (authorizeUrl + exchange). */\n client: TangleSsoAuthClient\n /** Product persistence: user upsert, session row, platform link. */\n store: TangleSsoAccountStore\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n /** Default 'tangle_sso_state'. */\n stateCookieName?: string\n /** HMAC secret for the CSRF state cookie. Default: the auth `secret`. */\n stateSecret?: string\n /** Default: `baseURL` is https. Must match better-auth's own\n * secure-cookie decision or the cookie name lookup diverges. */\n secureCookies?: boolean\n sessionTtlSeconds?: number\n stateTtlSeconds?: number\n /** Post-login redirect fallback. Default '/app'. */\n defaultRedirectPath?: string\n /** Default: the top-level `loginPath`. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n}\n\n/** Registered OIDC client configuration. The caller supplies Runtime PKCE; login does not mint an API key. */\nexport interface AppAuthOidcSsoConfig extends Omit<AppAuthSsoConfig, 'protocol' | 'client' | 'store'> {\n protocol: 'oidc'\n client: TangleOidcSsoAuthClient\n store: TangleOidcSsoAccountStore\n /** Pass the Runtime's `createPkcePair` helper from the OIDC-only caller. */\n createPkcePair: () => Promise<{ verifier: string; challenge: string }>\n}\n\n/** First-party identity login; no Platform API key is issued or stored. */\nexport interface AppAuthIdentitySsoConfig extends Omit<AppAuthSsoConfig, 'protocol' | 'client' | 'store'> {\n protocol: 'identity'\n client: TangleIdentitySsoAuthClient\n store: TangleIdentitySsoAccountStore\n}\n\n/** Define the structure for application authentication data including users, sessions, accounts, and verifications */\nexport interface AppAuthSchema {\n users: unknown\n sessions: unknown\n accounts: unknown\n verifications: unknown\n}\n\n/** Define configuration settings for app authentication including app name, base URL, secrets, and trusted origins */\nexport interface AppAuthConfig {\n /** Product name — used in email subjects and as the cookie-prefix default. */\n appName: string\n /** Absolute origin better-auth serves from (`BETTER_AUTH_URL`). */\n baseURL: string\n /** better-auth HMAC secret. Optional only because better-auth falls back to\n * the BETTER_AUTH_SECRET env var; `sso` needs it explicitly (or\n * `sso.stateSecret`). */\n secret?: string\n trustedOrigins?: string[]\n /**\n * Session-cookie prefix. Default: slugified `appName`. A per-app prefix is\n * load-bearing, not cosmetic: the platform (id.tangle.tools) mints its own\n * better-auth cookie `Domain=.tangle.tools`-wide under the DEFAULT name, and\n * the platform's (older) cookie wins the Cookie-header order — an app on the\n * default prefix reads the platform's token, fails its own signature check,\n * and every fresh login lands back on /login.\n */\n cookiePrefix?: string\n /** Drizzle database instance; wired through better-auth's drizzle adapter\n * together with `schema`. */\n db?: unknown\n /** The product's users/sessions/accounts/verifications tables (mapped to\n * better-auth's user/session/account/verification models). */\n schema?: AppAuthSchema\n /** Drizzle dialect. Default 'sqlite' (D1). */\n provider?: 'sqlite' | 'pg' | 'mysql'\n /** Escape hatch: a pre-built better-auth database adapter (e.g.\n * `memoryAdapter` in tests). Wins over `db`/`schema`. */\n database?: BetterAuthOptions['database']\n /** Email+password sign-in. Default true (all current products enable it). */\n emailAndPassword?: boolean\n /** Reset/verification mail. Omit to disable password reset entirely. */\n email?: AppAuthEmailConfig\n social?: AppAuthSocialConfig\n /** Session cookie-cache TTL in seconds; `false` disables the cache.\n * Default 300. */\n sessionCookieCacheSeconds?: number | false\n /** Tangle cross-site SSO (start/callback handlers). */\n sso?: AppAuthSsoConfig | AppAuthOidcSsoConfig | AppAuthIdentitySsoConfig\n /** Where guards redirect unauthenticated page requests. Default '/login'. */\n loginPath?: string\n /** Merged over the factory's `advanced` block (cookiePrefix stays unless\n * overridden here). */\n advanced?: BetterAuthOptions['advanced']\n}\n\n/** The configured better-auth instance, typed at better-auth's base surface\n * (`handler`, `api`, `$context`, `$Infer`). */\nexport type AppAuthInstance = Auth\n\n/** What `getSession`/guards resolve: better-auth's base session + user rows. */\nexport interface AppAuthSession {\n session: Session\n user: User\n}\n\n/** Define authentication guard with session retrieval and optional SSO handlers for app requests */\nexport interface AppAuth extends AuthGuard<AppAuthSession> {\n auth: AppAuthInstance\n /** `auth.api.getSession` over a `Request` — the seam the guards consume. */\n getSession(request: Request): Promise<AppAuthSession | null>\n /** Tangle SSO start/callback handlers; null unless `sso` was configured. */\n sso: TangleSsoHandlers | null\n}\n\n/** Lowercased, non-alphanumerics collapsed to '-': `'Legal Agent'` → `'legal-agent'`. */\nfunction slugify(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\nfunction resolveEmailClient(email: AppAuthEmailConfig): AppAuthEmailClient | null {\n return typeof email.resend === 'function' ? email.resend() : email.resend\n}\n\ntype DrizzleAuthAdapter = ReturnType<ReturnType<typeof drizzleAdapter>>\n\n/** Better Auth reads adapter identity during startup, before Workers inject D1.\n * Build the Drizzle adapter on its first database operation. */\nfunction deferDrizzleAdapter(\n db: Record<string, unknown>,\n config: Parameters<typeof drizzleAdapter>[1],\n): ReturnType<typeof drizzleAdapter> {\n return (options) => {\n let adapter: DrizzleAuthAdapter | undefined\n const resolve = () => (adapter ??= drizzleAdapter(db, config)(options))\n\n return {\n id: 'drizzle',\n create: (data) => resolve().create(data),\n findOne: (data) => resolve().findOne(data),\n findMany: (data) => resolve().findMany(data),\n count: (data) => resolve().count(data),\n update: (data) => resolve().update(data),\n updateMany: (data) => resolve().updateMany(data),\n delete: (data) => resolve().delete(data),\n deleteMany: (data) => resolve().deleteMany(data),\n consumeOne: (data) => resolve().consumeOne(data),\n incrementOne: (data) => resolve().incrementOne(data),\n transaction: (callback) => resolve().transaction(callback),\n }\n }\n}\n\n/** Resend reports failures in the resolved value, not by rejecting — surface\n * them loud so better-auth's flow (and the caller's logs) see the failure. */\nasync function sendEmail(\n client: AppAuthEmailClient,\n message: { from: string; to: string; subject: string; html: string; text: string },\n): Promise<void> {\n const result = (await client.emails.send(message)) as { error?: { message?: string } | null } | null | undefined\n if (result && typeof result === 'object' && result.error) {\n throw new Error(`[app-auth] email send failed: ${result.error.message ?? 'unknown error'}`)\n }\n}\n\nfunction resolveDatabase(config: AppAuthConfig): NonNullable<BetterAuthOptions['database']> {\n if (config.database) return config.database\n if (config.db && config.schema) {\n return deferDrizzleAdapter(config.db as Record<string, unknown>, {\n provider: config.provider ?? 'sqlite',\n schema: {\n user: config.schema.users,\n session: config.schema.sessions,\n account: config.schema.accounts,\n verification: config.schema.verifications,\n } as Record<string, unknown>,\n })\n }\n throw new Error(\n 'createAppAuth requires a database: pass `db` + `schema` (drizzle) or `database` (a better-auth adapter)',\n )\n}\n\nfunction resolveSocialProviders(social: AppAuthSocialConfig | undefined): BetterAuthOptions['socialProviders'] {\n const providers: NonNullable<BetterAuthOptions['socialProviders']> = {}\n if (social?.github?.clientId && social.github.clientSecret) {\n providers.github = { clientId: social.github.clientId, clientSecret: social.github.clientSecret }\n }\n if (social?.google?.clientId && social.google.clientSecret) {\n providers.google = { clientId: social.google.clientId, clientSecret: social.google.clientSecret }\n }\n return Object.keys(providers).length > 0 ? providers : undefined\n}\n\nfunction emailAndPasswordOptions(config: AppAuthConfig): BetterAuthOptions['emailAndPassword'] {\n const email = config.email\n if (!email) return { enabled: true }\n const warn = email.warn ?? ((message: string) => console.warn(message))\n return {\n enabled: true,\n sendResetPassword: async ({ user, url }) => {\n const client = resolveEmailClient(email)\n if (!client) {\n warn('[app-auth] email client unavailable — password reset email not sent')\n return\n }\n await sendEmail(client, {\n from: email.from,\n to: user.email,\n subject: 'Reset your password',\n html: `<p>Click the link below to reset your password:</p><p><a href=\"${url}\">${url}</a></p><p>This link expires in 1 hour.</p>`,\n text: `Reset your password:\\n\\n${url}\\n\\nThis link expires in 1 hour.`,\n })\n },\n }\n}\n\nfunction emailVerificationOptions(config: AppAuthConfig): BetterAuthOptions['emailVerification'] {\n const email = config.email\n if (!email?.verifyOnSignUp) return undefined\n const warn = email.warn ?? ((message: string) => console.warn(message))\n return {\n sendOnSignUp: true,\n autoSignInAfterVerification: true,\n sendVerificationEmail: async ({ user, url }) => {\n const client = resolveEmailClient(email)\n if (!client) {\n warn('[app-auth] email client unavailable — verification email not sent')\n return\n }\n await sendEmail(client, {\n from: email.from,\n to: user.email,\n subject: `Verify your ${config.appName} email`,\n html: `<p>Verify your email to finish accessing ${config.appName}:</p><p><a href=\"${url}\">${url}</a></p><p>This link expires in 1 hour.</p>`,\n text: `Verify your email to finish accessing ${config.appName}:\\n\\n${url}\\n\\nThis link expires in 1 hour.`,\n })\n },\n }\n}\n\n/**\n * Build the product's better-auth instance plus the request-boundary helpers:\n * `getSession` (Request → session|null), the `createAuthGuard` quartet\n * (`requireUser` 302, `requireApiUser` JSON 401, `requireSession`,\n * `getOptionalSession`), and — when `sso` is configured — the Tangle SSO\n * start/callback handlers with the session cookie minted through better-auth's\n * own name/attributes/signing (no `better-auth/crypto` in product code).\n */\nexport function createAppAuth(config: AppAuthConfig): AppAuth {\n if (!config.appName) throw new Error('createAppAuth: appName is required')\n if (!config.baseURL) throw new Error('createAppAuth: baseURL is required')\n\n const cookiePrefix = config.cookiePrefix ?? slugify(config.appName)\n if (!cookiePrefix) throw new Error('createAppAuth: cookiePrefix (or a slugifiable appName) is required')\n\n const socialProviders = resolveSocialProviders(config.social)\n const options: BetterAuthOptions = {\n appName: config.appName,\n baseURL: config.baseURL,\n ...(config.secret ? { secret: config.secret } : {}),\n ...(config.trustedOrigins ? { trustedOrigins: config.trustedOrigins } : {}),\n database: resolveDatabase(config),\n ...(config.emailAndPassword === false ? {} : { emailAndPassword: emailAndPasswordOptions(config) }),\n ...(config.email?.verifyOnSignUp ? { emailVerification: emailVerificationOptions(config) } : {}),\n ...(socialProviders ? { socialProviders } : {}),\n ...(config.sessionCookieCacheSeconds === false\n ? {}\n : {\n session: {\n cookieCache: {\n enabled: true,\n maxAge: config.sessionCookieCacheSeconds ?? DEFAULT_SESSION_COOKIE_CACHE_SECONDS,\n },\n },\n }),\n advanced: { cookiePrefix, ...config.advanced },\n }\n\n const auth: AppAuthInstance = betterAuth(options)\n\n const getSession = async (request: Request): Promise<AppAuthSession | null> => {\n const session = await auth.api.getSession({ headers: request.headers })\n return (session as AppAuthSession | null) ?? null\n }\n\n const loginPath = config.loginPath ?? '/login'\n const guard = createAuthGuard<AppAuthSession>({ getSession, loginPath })\n\n let sso: TangleSsoHandlers | null = null\n if (config.sso) {\n const stateSecret = config.sso.stateSecret ?? config.secret\n if (!stateSecret) {\n throw new Error(\n 'createAppAuth: sso requires `secret` (or sso.stateSecret) — the signed-state CSRF cookie needs an HMAC secret',\n )\n }\n const transport = config.sso.protocol === 'oidc'\n ? { protocol: 'oidc' as const, auth: config.sso.client, store: config.sso.store, createPkcePair: config.sso.createPkcePair }\n : config.sso.protocol === 'identity'\n ? { protocol: 'identity' as const, auth: config.sso.client, store: config.sso.store }\n : { protocol: 'legacy' as const, auth: config.sso.client, store: config.sso.store }\n sso = createTangleSsoHandlers({\n ...transport,\n stateSecret,\n callbackUrl: config.sso.callbackUrl,\n stateCookieName: config.sso.stateCookieName ?? DEFAULT_STATE_COOKIE,\n setSessionCookie: createBetterAuthSessionCookieMinter(auth),\n secureCookies: config.sso.secureCookies ?? config.baseURL.startsWith('https:'),\n ...(config.sso.sessionTtlSeconds !== undefined ? { sessionTtlSeconds: config.sso.sessionTtlSeconds } : {}),\n ...(config.sso.stateTtlSeconds !== undefined ? { stateTtlSeconds: config.sso.stateTtlSeconds } : {}),\n ...(config.sso.defaultRedirectPath ? { defaultRedirectPath: config.sso.defaultRedirectPath } : {}),\n loginPath: config.sso.loginPath ?? loginPath,\n ...(config.sso.log ? { log: config.sso.log } : {}),\n })\n }\n\n return { auth, getSession, ...guard, sso }\n}\n"],"mappings":"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","names":[]}
@@ -204,17 +204,17 @@ function createTangleSsoHandlers(opts) {
204
204
  if (!opts.callbackUrl) throw new Error("TangleSsoHandlerOptions.callbackUrl is required");
205
205
  if (!opts.stateCookieName) throw new Error("TangleSsoHandlerOptions.stateCookieName is required");
206
206
  const callbackUrl = new URL(opts.callbackUrl);
207
- if (opts.protocol === "oidc") {
208
- if (opts.stateSecret.length < 32) throw new Error("OIDC stateSecret must contain at least 32 characters");
207
+ if (opts.protocol === "oidc" || opts.protocol === "identity") {
208
+ if (opts.stateSecret.length < 32) throw new Error("SSO stateSecret must contain at least 32 characters");
209
209
  if (callbackUrl.username || callbackUrl.password || callbackUrl.hash || callbackUrl.search) {
210
- throw new Error("OIDC callbackUrl must be fixed and have no credentials, query, or fragment");
210
+ throw new Error("SSO callbackUrl must be fixed and have no credentials, query, or fragment");
211
211
  }
212
212
  const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(callbackUrl.hostname);
213
213
  if (callbackUrl.protocol !== "https:" && !(callbackUrl.protocol === "http:" && loopback)) {
214
- throw new Error("OIDC requires HTTPS except on loopback");
214
+ throw new Error("SSO requires HTTPS except on loopback");
215
215
  }
216
216
  if (callbackUrl.protocol === "https:" && !opts.secureCookies) {
217
- throw new Error("OIDC HTTPS callbacks require Secure cookies");
217
+ throw new Error("SSO HTTPS callbacks require Secure cookies");
218
218
  }
219
219
  }
220
220
  const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE;
@@ -257,7 +257,7 @@ function createTangleSsoHandlers(opts) {
257
257
  async start(request) {
258
258
  if (request.method !== "GET") return new Response(null, { status: 405, headers: { Allow: "GET" } });
259
259
  const url = new URL(request.url);
260
- if (opts.protocol === "oidc" && url.origin !== callbackUrl.origin) {
260
+ if ((opts.protocol === "oidc" || opts.protocol === "identity") && url.origin !== callbackUrl.origin) {
261
261
  return loginErrorRedirect("tangle_origin_mismatch");
262
262
  }
263
263
  const redirectPath = sanitizeRedirectPath(url.searchParams.get("redirect"), defaultRedirectPath);
@@ -286,7 +286,7 @@ function createTangleSsoHandlers(opts) {
286
286
  async callback(request) {
287
287
  if (request.method !== "GET") return new Response(null, { status: 405, headers: { Allow: "GET" } });
288
288
  const url = new URL(request.url);
289
- if (opts.protocol === "oidc" && (url.origin !== callbackUrl.origin || url.pathname !== callbackUrl.pathname)) {
289
+ if ((opts.protocol === "oidc" || opts.protocol === "identity") && (url.origin !== callbackUrl.origin || url.pathname !== callbackUrl.pathname)) {
290
290
  return loginErrorRedirect("tangle_origin_mismatch");
291
291
  }
292
292
  const code = url.searchParams.get("code");
@@ -313,6 +313,12 @@ function createTangleSsoHandlers(opts) {
313
313
  throw new Error("OIDC requires offline_access and a finite access-token lifetime");
314
314
  }
315
315
  exchanged = { user: result.user, emailVerified: result.user.emailVerified };
316
+ } else if (opts.protocol === "identity") {
317
+ const identity = await opts.auth.exchange(code);
318
+ if (identity.kind !== "identity" || "apiKey" in identity || "keyId" in identity || !identity.user || typeof identity.user.id !== "string" || !identity.user.id.trim() || typeof identity.user.email !== "string" || !identity.user.email.trim()) {
319
+ throw new Error("Identity exchange returned an invalid result");
320
+ }
321
+ exchanged = identity;
316
322
  } else {
317
323
  legacyExchange = await opts.auth.exchange(code);
318
324
  exchanged = legacyExchange;
@@ -373,6 +379,16 @@ function createTangleSsoHandlers(opts) {
373
379
  committed = true;
374
380
  return response;
375
381
  }
382
+ if (opts.protocol === "identity") {
383
+ const headers2 = new Headers();
384
+ headers2.append("Set-Cookie", clearCookieHeader(stateCookieOpts));
385
+ const sessionCookies2 = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies });
386
+ for (const cookie of sessionCookies2) headers2.append("Set-Cookie", cookie);
387
+ const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers2);
388
+ await opts.store.saveTangleLink(link);
389
+ committed = true;
390
+ return response;
391
+ }
376
392
  await opts.store.saveTangleLink({ ...link, apiKey: legacyExchange.apiKey, planTier: legacyExchange.plan?.tier ?? null });
377
393
  const headers = new Headers();
378
394
  headers.append("Set-Cookie", clearCookieHeader(stateCookieOpts));
@@ -380,11 +396,11 @@ function createTangleSsoHandlers(opts) {
380
396
  for (const cookie of sessionCookies) headers.append("Set-Cookie", cookie);
381
397
  return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers);
382
398
  } catch (error) {
383
- if (opts.protocol !== "oidc") throw error;
399
+ if (opts.protocol !== "oidc" && opts.protocol !== "identity") throw error;
384
400
  log("[tangle-sso] local session persistence failed");
385
401
  return loginErrorRedirect("tangle_session_failed");
386
402
  } finally {
387
- if (opts.protocol === "oidc" && !committed) {
403
+ if ((opts.protocol === "oidc" || opts.protocol === "identity") && !committed) {
388
404
  if (createdSession) {
389
405
  try {
390
406
  await opts.store.deleteSession({ sessionToken: createdSession });
@@ -392,7 +408,7 @@ function createTangleSsoHandlers(opts) {
392
408
  log("[tangle-sso] unpublished session cleanup failed");
393
409
  }
394
410
  }
395
- if (oidcTokens) {
411
+ if (opts.protocol === "oidc" && oidcTokens) {
396
412
  try {
397
413
  if (oidcTokens.refreshToken) await opts.auth.revoke(oidcTokens.refreshToken, "refresh_token");
398
414
  } catch {
@@ -480,4 +496,4 @@ export {
480
496
  createAdminGuard,
481
497
  assertBillableBalance
482
498
  };
483
- //# sourceMappingURL=chunk-MVIIH2EQ.js.map
499
+ //# sourceMappingURL=chunk-MUABCTFT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/platform/sso.ts","../src/platform/guards.ts"],"sourcesContent":["/**\n * Tangle SSO for agent apps. OIDC callers use the registered runtime client\n * with S256 PKCE. Legacy API-key callers remain explicit during migration.\n * OIDC never falls back to cross-site exchange. Account matching and native\n * session cookie minting remain shared.\n */\n\nimport { clearCookieHeader, readCookieValue, serializeCookie } from '../web/index'\n\nconst DEFAULT_STATE_TTL_SECONDS = 600\nconst DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7\nconst DEFAULT_REDIRECT_PATH = '/app'\nconst DEFAULT_LOGIN_PATH = '/login'\nconst DEFAULT_SESSION_COOKIE = 'better-auth.session_token'\n\n// ── Signed state ────────────────────────────────────────────────────────────\n\n/** Define configuration options for managing SSO state including secret, lifetime, and clock injection */\nexport interface SsoStateConfig {\n /** HMAC-SHA256 secret (e.g. the app's auth secret). */\n secret: string\n /** State lifetime in ms. Default 600 000. */\n ttlMs?: number\n /** Injectable clock (ms since epoch). Default Date.now. */\n now?: () => number\n}\n\nfunction randomHex(bytes: number): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nasync function hmacBytes(secret: string, value: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value)))\n}\n\nasync function hmacHex(secret: string, value: string): Promise<string> {\n return Array.from(await hmacBytes(secret, value), (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n\n/** Mint a `<randomHex32>.<timestamp36>.<hmacHex>` state value. The timestamp\n * is inside the signed payload, so expiry survives cookie-attribute tampering. */\nexport async function createSignedSsoState(config: SsoStateConfig): Promise<string> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const now = config.now ?? Date.now\n const payload = `${randomHex(16)}.${now().toString(36)}`\n return `${payload}.${await hmacHex(config.secret, payload)}`\n}\n\n/** Verify the MAC (constant-time) and the signed TTL. */\nexport async function verifySignedSsoState(state: string, config: SsoStateConfig): Promise<boolean> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const parts = state.split('.')\n if (parts.length !== 3) return false\n const [random, timestamp, mac] = parts\n if (!random || !timestamp || !mac || !/^[0-9a-z]+$/.test(timestamp)) return false\n const expected = await hmacHex(config.secret, `${random}.${timestamp}`)\n if (!constantTimeEqual(mac, expected)) return false\n const mintedAt = parseInt(timestamp, 36)\n if (!Number.isFinite(mintedAt)) return false\n const now = config.now ?? Date.now\n const ttlMs = config.ttlMs ?? DEFAULT_STATE_TTL_SECONDS * 1000\n const age = now() - mintedAt\n return age >= 0 && age <= ttlMs\n}\n\n// ── Seams ───────────────────────────────────────────────────────────────────\n\n/** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */\nexport interface TangleSsoExchangeResult {\n apiKey: string\n /** The platform's canonical verified-email proof. */\n emailVerified: boolean\n user: { id: string; email: string; name?: string | null }\n plan?: { tier: string } | null\n}\n\n/** Structural mirror of the platform auth wire client — any object with these\n * two methods satisfies it without this module importing the concrete class. */\nexport interface TangleSsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string): Promise<TangleSsoExchangeResult>\n}\n\n/** First-party login exchanges identity without minting a Platform API key. */\nexport interface TangleIdentitySsoExchangeResult {\n kind: 'identity'\n emailVerified: boolean\n user: TangleSsoExchangeResult['user']\n apiKey?: never\n keyId?: never\n}\n\nexport interface TangleIdentitySsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string): Promise<TangleIdentitySsoExchangeResult>\n}\n\n/** Token fields the callback persists from the runtime's OIDC exchange. */\nexport interface TangleOidcSsoTokens {\n accessToken: string\n tokenType: string\n expiresIn?: number\n refreshToken?: string\n idToken?: string\n scope?: string\n}\n\n/** Verified identity returned by the runtime's OIDC userinfo lookup. */\nexport interface TangleOidcSsoUser {\n id: string\n email: string\n emailVerified: true\n name?: string\n}\n\n/** The caller needs only the OIDC operations used by this callback. Keeping\n * this structural prevents legacy consumers from needing the optional\n * Runtime peer just to load the SSO declarations. */\nexport interface TangleOidcSsoAuthClient {\n authorizeUrl(options: {\n state: string\n codeChallenge: string\n nonce?: string\n prompt?: 'login' | 'consent' | 'none'\n loginHint?: string\n }): string\n exchange(code: string, codeVerifier: string): Promise<{\n tokens: TangleOidcSsoTokens\n user: TangleOidcSsoUser\n }>\n revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void>\n}\n\n/** Local account shape required by the verified Tangle SSO account policy.\n * Consumers map their user/link tables into this shape before resolving an\n * exchange. */\nexport interface TangleSsoLocalAccount {\n userId: string\n email: string\n emailVerified: boolean\n platformUserId: string | null\n}\n\n/** Reasons a local account lookup must stop instead of linking an exchange. */\nexport type TangleSsoAccountConflictReason =\n | 'ambiguous-platform-id'\n | 'platform-id-email-conflict'\n | 'unverified-email'\n | 'email-platform-id-conflict'\n | 'ambiguous-email'\n | 'invalid-platform-id'\n | 'invalid-email'\n | 'platform-match-mismatch'\n | 'email-match-mismatch'\n\n/** Result of applying the verified Tangle SSO account matching policy. */\nexport type TangleSsoAccountResolution =\n | { kind: 'existing'; userId: string; matchedBy: 'platform-id' | 'verified-email' }\n | { kind: 'create' }\n | { kind: 'reject'; reason: TangleSsoAccountConflictReason }\n\n/** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the\n * platform's stable identity. */\nexport interface TangleSsoAccountResolutionInput {\n email: string\n platformMatches: readonly TangleSsoLocalAccount[]\n emailMatches: readonly TangleSsoLocalAccount[]\n platformUserId: string\n}\n\n/** Normalize an email for comparisons and persistence queries. */\nexport function normalizeTangleSsoEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nfunction normalizedPlatformUserId(value: string | null | undefined): string | null {\n const normalized = value?.trim()\n return normalized ? normalized : null\n}\n\n/**\n * Resolve a platform exchange against all local rows found by stable platform\n * identity and normalized email.\n *\n * A stable platform id takes precedence over email. Every ambiguous or\n * contradictory result rejects, and email fallback requires a verified local\n * email that is not already bound to another platform id. The resolver is\n * pure so each consumer can query its own schema, including schemas without\n * unique indexes, then rerun it after an insert race.\n */\nexport function resolveTangleSsoAccount(\n input: TangleSsoAccountResolutionInput,\n): TangleSsoAccountResolution {\n const email = normalizeTangleSsoEmail(input.email)\n const platformUserId = normalizedPlatformUserId(input.platformUserId)\n if (!email) return { kind: 'reject', reason: 'invalid-email' }\n if (!platformUserId) return { kind: 'reject', reason: 'invalid-platform-id' }\n\n if (input.platformMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-platform-id' }\n }\n if (input.emailMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-email' }\n }\n\n if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {\n return { kind: 'reject', reason: 'platform-match-mismatch' }\n }\n if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {\n return { kind: 'reject', reason: 'email-match-mismatch' }\n }\n\n const platformMatch = input.platformMatches[0]\n const emailMatch = input.emailMatches[0]\n\n if (platformMatch) {\n if (emailMatch && emailMatch.userId !== platformMatch.userId) {\n return { kind: 'reject', reason: 'platform-id-email-conflict' }\n }\n return { kind: 'existing', userId: platformMatch.userId, matchedBy: 'platform-id' }\n }\n\n if (!emailMatch) return { kind: 'create' }\n const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId)\n if (existingPlatformUserId === platformUserId) {\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'platform-id' }\n }\n if (emailMatch.emailVerified !== true) {\n return { kind: 'reject', reason: 'unverified-email' }\n }\n\n if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {\n return { kind: 'reject', reason: 'email-platform-id-conflict' }\n }\n\n // The platform proof is already verified. A verified local email row is safe\n // to link when it is unbound or already carries this same platform id.\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'verified-email' }\n}\n\n/** Thrown by an account store when the verified SSO policy rejects a link. */\nexport class TangleSsoAccountConflictError extends Error {\n readonly reason: TangleSsoAccountConflictReason\n\n constructor(reason: TangleSsoAccountConflictReason, message = 'Tangle SSO account linking was rejected') {\n super(message)\n this.name = 'TangleSsoAccountConflictError'\n this.reason = reason\n }\n}\n\n/** Thrown by `upsertUserByEmail` when the app-local user row cannot be\n * created; the callback handler maps it to `?error=tangle_user_create_failed`.\n * Any other store error propagates. */\nexport class TangleSsoUserCreateError extends Error {\n constructor(message = 'Failed to create local user for Tangle SSO') {\n super(message)\n this.name = 'TangleSsoUserCreateError'\n }\n}\n\n/**\n * Account persistence seam. Covers both storage styles in use: link-table\n * apps (a per-user platform-link row) and session-column apps (the key on the\n * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,\n * and each app persists with the key it needs. `createSession` runs first so\n * the token is always available to `saveTangleLink`.\n */\nexport interface TangleSsoAccountStore {\n /** Resolve the local account before any user, session, or link write.\n * Implementations must query every candidate by stable platform id and\n * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent\n * fail-closed policy). The callback rejects `kind: 'reject'` results before\n * it calls `upsertUserByEmail`. */\n resolveAccount(input: { email: string; platformUserId: string }): Promise<TangleSsoAccountResolution>\n /** Find-or-create the app-local user selected by `resolveAccount`.\n * `tangleUserId` is the platform's stable user id — match on it first when\n * the app stores it (emails are mutable on the platform; the id is not),\n * falling back to email for first-time logins. */\n upsertUserByEmail(input: {\n email: string\n name: string | null\n tangleUserId: string\n resolution: Exclude<TangleSsoAccountResolution, { kind: 'reject' }>\n }): Promise<{ userId: string }>\n /** Create an app session row; returns the session-cookie token value. */\n createSession(input: {\n userId: string\n expiresAt: Date\n ipAddress: string | null\n userAgent: string | null\n }): Promise<{ token: string }>\n /** Persist the platform link (API key + platform identity). */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n apiKey: string\n planTier: string | null\n }): Promise<void>\n}\n\n/**\n * OIDC tokens are server-side credentials, not API keys. Implementations must\n * encrypt them at rest and bind them to the exact local session. Refresh and\n * disconnect must serialize updates to the same grant.\n */\nexport interface TangleOidcSsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** Atomically bind the grant to this exact session. If persistence partially\n * fails, `deleteSession` must remove every row and credential for it. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n tokens: TangleOidcSsoTokens\n accessTokenExpiresAt: Date\n }): Promise<void>\n /** Remove an unpublished local session if callback persistence fails.\n * This must also remove any grant rows or credentials partially written by\n * a rejected `saveTangleLink`, keyed by this exact session token. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n/** Persist only the stable identity link for a first-party app. */\nexport interface TangleIdentitySsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** `upsertUserByEmail` may create the local user, but must not publish a new\n * platform link or update an existing user's verified-email state. The\n * callback has not yet created a session or prepared its cookie. */\n upsertUserByEmail: TangleSsoAccountStore['upsertUserByEmail']\n /** Commit the identity link and any verified-email update atomically. Do not\n * reject after committing. If the store can partially write before throwing,\n * `deleteSession` must remove only writes made for this session token. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n }): Promise<void>\n /** Remove an unpublished session and any link or verification writes that a\n * rejected `saveTangleLink` made for its exact token. Preserve preexisting\n * links and other sessions. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n// ── Session cookie ──────────────────────────────────────────────────────────\n\n/** Successful-login context handed to the `setSessionCookie` seam. */\nexport interface TangleSsoSessionCookieArgs {\n /** Session token returned by `store.createSession`. */\n token: string\n /** Session expiry (now + `sessionTtlSeconds`). */\n expiresAt: Date\n /** Mirrors `sessionTtlSeconds` after defaulting. */\n ttlSeconds: number\n /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */\n secure: boolean\n}\n\n/**\n * Sign a session token to better-call's signed-cookie contract — the value\n * better-auth's `getSignedCookie` verifies: `<token>.<signature>` where the\n * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as\n * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call\n * rejects any other length or suffix, so url-safe/unpadded variants read back\n * as a null session). The joined value is percent-encoded once at cookie\n * serialization, matching better-call's `serializeSignedCookie` byte-exactly.\n */\nexport async function signSessionCookieValue(token: string, secret: string): Promise<string> {\n if (!secret) throw new Error('signSessionCookieValue requires a non-empty secret')\n const sig = await hmacBytes(secret, token)\n let bin = ''\n for (const byte of sig) bin += String.fromCharCode(byte)\n return `${token}.${btoa(bin)}`\n}\n\n/** Structural slice of a `betterAuth()` instance — only what cookie minting\n * reads. No better-auth import: the signing contract is implemented by\n * `signSessionCookieValue`, byte-compatible with better-auth's own\n * `makeSignature`. */\nexport interface BetterAuthSessionCookieSource {\n $context: PromiseLike<{\n secret: string\n authCookies: {\n sessionToken: {\n /** Final cookie name — better-auth decides the `__Secure-` prefix\n * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */\n name: string\n attributes: {\n secure?: boolean\n sameSite?: string\n path?: string\n httpOnly?: boolean\n domain?: string\n }\n }\n }\n }>\n}\n\n/** Define options to customize warning behavior for shadowed cookie names in authentication sessions */\nexport interface BetterAuthSessionCookieMinterOptions {\n /** Receives the shadowed-cookie-name warning (see below). Default\n * console.warn. */\n warn?: (message: string) => void\n}\n\n/**\n * Canonical `setSessionCookie` wiring for better-auth apps: mint the session\n * Set-Cookie exactly as better-auth's own login flows do — name + attributes\n * from `auth.$context.authCookies.sessionToken` (better-auth stays\n * authoritative over prefix/name/attributes) and the value signed to\n * better-call's `getSignedCookie` contract. A raw unprefixed\n * `better-auth.session_token` left by an earlier login is explicitly expired\n * so it cannot shadow the real cookie.\n *\n * Warns when the app's session cookie still has better-auth's DEFAULT name:\n * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie\n * under that exact name, and equal-path cookies are sent oldest-first — the\n * platform's cookie is always older (the user signs in there before the app's\n * callback runs), so the app reads the platform's token, fails its own\n * signature check, and every fresh login lands logged-out. Per-app\n * `advanced.cookiePrefix` is the fix.\n *\n * Throws on a domain-scoped session cookie for the same reason: a\n * `Domain=`-wide session cookie is exactly the shadowing footgun.\n */\nexport function createBetterAuthSessionCookieMinter(\n auth: BetterAuthSessionCookieSource,\n options: BetterAuthSessionCookieMinterOptions = {},\n): (args: TangleSsoSessionCookieArgs) => Promise<string[]> {\n const warn = options.warn ?? ((message: string) => console.warn(message))\n return async ({ token, ttlSeconds }) => {\n const ctx = await auth.$context\n if (!ctx.secret) {\n throw new Error('createBetterAuthSessionCookieMinter: auth context has no secret')\n }\n const { name, attributes } = ctx.authCookies.sessionToken\n if (attributes.domain) {\n throw new Error(\n `createBetterAuthSessionCookieMinter: refusing a domain-scoped session cookie (Domain=${attributes.domain}) — ` +\n 'a domain-wide session cookie shadows sibling apps that share the parent domain',\n )\n }\n if (name === DEFAULT_SESSION_COOKIE || name === `__Secure-${DEFAULT_SESSION_COOKIE}`) {\n warn(\n `[tangle-sso] session cookie is named \"${name}\" — better-auth's default. ` +\n 'The Tangle platform (id.tangle.tools) sets a Domain=.tangle.tools cookie under the same name, ' +\n \"and the platform's (older) cookie wins the Cookie-header order, so this app's sessions read back null. \" +\n \"Set a per-app prefix: betterAuth({ advanced: { cookiePrefix: '<app>' } }).\",\n )\n }\n const sameSite = typeof attributes.sameSite === 'string' ? attributes.sameSite : 'lax'\n const cookieOptions = {\n name,\n path: typeof attributes.path === 'string' ? attributes.path : '/',\n httpOnly: attributes.httpOnly !== false,\n sameSite: (sameSite.charAt(0).toUpperCase() + sameSite.slice(1)) as 'Lax' | 'Strict' | 'None',\n secure: Boolean(attributes.secure) || name.startsWith('__Secure-'),\n maxAgeSeconds: ttlSeconds,\n }\n const cookies = [serializeCookie(await signSessionCookieValue(token, ctx.secret), cookieOptions)]\n if (name !== DEFAULT_SESSION_COOKIE) {\n cookies.push(clearCookieHeader({ ...cookieOptions, name: DEFAULT_SESSION_COOKIE }))\n }\n return cookies\n }\n}\n\n// ── Handlers ────────────────────────────────────────────────────────────────\n\n/** Define configuration options for handling Tangle SSO authentication and session management */\nexport interface TangleSsoHandlerOptions {\n protocol?: 'legacy'\n auth: TangleSsoAuthClient\n store: TangleSsoAccountStore\n /** HMAC secret for the state cookie. */\n stateSecret: string\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n stateCookieName: string\n /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is\n * provided. The default path prepends `__Secure-` iff `secureCookies`. */\n sessionCookieName?: string\n /** Mint the host auth framework's own session cookie(s); return complete\n * Set-Cookie header values (the handler appends them verbatim and sets no\n * session cookie itself). Supply this when the framework should stay\n * authoritative over name/prefix/signing/attributes — e.g. better-auth:\n * `auth.$context.authCookies.sessionToken` + `makeSignature`. */\n setSessionCookie?: (\n args: TangleSsoSessionCookieArgs,\n ) => readonly string[] | Promise<readonly string[]>\n /** HMAC-SHA256 secret the host auth framework verifies session cookies with\n * (better-auth: its `secret`). Required when `setSessionCookie` is absent —\n * the default cookie is minted to better-call's signed contract via\n * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a\n * null session, so there is deliberately no fallback to `stateSecret`\n * (which is not guaranteed to be the auth secret). */\n sessionCookieSecret?: string\n /** Adds `Secure` to every cookie this module sets, and (default session\n * cookie only) the `__Secure-` name prefix. Must match the auth\n * framework's own secure-cookie decision (better-auth: https `baseURL` /\n * `advanced.useSecureCookies`), or it will look up a different cookie name\n * than the one set here. */\n secureCookies: boolean\n /** Default 604 800 (7 days). */\n sessionTtlSeconds?: number\n /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */\n stateTtlSeconds?: number\n /** Default '/app'. */\n defaultRedirectPath?: string\n /** Default '/login'. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n now?: () => number\n}\n\n/** Registered OIDC client and token store. The OIDC caller supplies the Runtime PKCE helper. */\nexport interface TangleOidcSsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'oidc'\n auth: TangleOidcSsoAuthClient\n store: TangleOidcSsoAccountStore\n /** Pass the Runtime's `createPkcePair` helper from the OIDC-only caller. */\n createPkcePair: () => Promise<{ verifier: string; challenge: string }>\n}\n\nexport interface TangleIdentitySsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'identity'\n auth: TangleIdentitySsoAuthClient\n store: TangleIdentitySsoAccountStore\n}\n\n/** Define handlers for SSO start and callback routes managing authentication flow and session cookies */\nexport interface TangleSsoHandlers {\n /** GET start route: mint + sign state, set the state cookie, 302 to the\n * platform authorize URL. `?redirect=` carries the post-login path. */\n start(request: Request): Promise<Response>\n /** GET callback route: verify state, exchange the code, upsert the user,\n * create the session, save the platform link, set the session cookie\n * (via the `setSessionCookie` seam, else signed to better-call's contract\n * with `sessionCookieSecret`), 302 to the saved redirect. Every failure\n * 302s to `loginPath?error=…` with the state cookie cleared. */\n callback(request: Request): Promise<Response>\n}\n\n/** Redirect targets must stay on this app's origin. A prefix check on `/`\n * and `//` is not enough: in the WHATWG parser a backslash is a path\n * separator for special schemes, so `/\\host` resolves to `https://host/`,\n * and tab or newline are stripped before parsing, so `/\\t/host` is `//host`.\n * Parse the value against a fixed origin and accept it only when that origin\n * survives; the result is the parser's normalised path, query and fragment.\n * Backslash and control characters are rejected up front as well, so the\n * decision does not rest on one parser's handling of them.\n *\n * The input staying on-origin is not enough either: dot-segment removal\n * turns `/.//host` or `/a/..//host` into the pathname `//host` with the\n * origin intact, and a browser resolves that Location off-origin. So the\n * emitted value must itself resolve on-origin and come back unchanged. */\nconst REDIRECT_REFERENCE_ORIGIN = 'https://redirect-reference.invalid'\n\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (!value || !value.startsWith('/') || /[\\\\\\u0000-\\u001f\\u007f]/.test(value)) return fallback\n const path = resolveOnReferenceOrigin(value)\n if (path === null || resolveOnReferenceOrigin(path) !== path) return fallback\n return path\n}\n\n/** `path + search + hash` of `value` resolved against the reference origin,\n * or null when the parse fails or lands on another origin. */\nfunction resolveOnReferenceOrigin(value: string): string | null {\n let url: URL\n try {\n url = new URL(value, REDIRECT_REFERENCE_ORIGIN)\n } catch {\n return null\n }\n if (url.origin !== REDIRECT_REFERENCE_ORIGIN) return null\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\n headers.set('Cache-Control', 'no-store')\n headers.set('Referrer-Policy', 'no-referrer')\n return new Response(null, { status: 302, headers })\n}\n\n/** Real client IP: `CF-Connecting-IP` behind Cloudflare, else the first\n * `x-forwarded-for` hop (the rest of the list is sender-controlled). */\nfunction clientIp(request: Request): string | null {\n return (\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n null\n )\n}\n\ninterface StateCookiePayload {\n s: string\n r: string\n v?: string\n m?: string\n}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw || raw.length > 4096) return null\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed === null || typeof parsed !== 'object') return null\n const { s, r, v, m } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r, ...(typeof v === 'string' ? { v } : {}), ...(typeof m === 'string' ? { m } : {}) }\n } catch { return null }\n}\n\nfunction stateBinding(payload: StateCookiePayload, callbackUrl: string): string {\n return JSON.stringify([payload.s, payload.r, payload.v ?? null, callbackUrl])\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(\n opts: TangleSsoHandlerOptions | TangleOidcSsoHandlerOptions | TangleIdentitySsoHandlerOptions,\n): TangleSsoHandlers {\n if (!opts.stateSecret) throw new Error('TangleSsoHandlerOptions.stateSecret is required')\n if (!opts.callbackUrl) throw new Error('TangleSsoHandlerOptions.callbackUrl is required')\n if (!opts.stateCookieName) throw new Error('TangleSsoHandlerOptions.stateCookieName is required')\n const callbackUrl = new URL(opts.callbackUrl)\n if (opts.protocol === 'oidc' || opts.protocol === 'identity') {\n if (opts.stateSecret.length < 32) throw new Error('SSO stateSecret must contain at least 32 characters')\n if (callbackUrl.username || callbackUrl.password || callbackUrl.hash || callbackUrl.search) {\n throw new Error('SSO callbackUrl must be fixed and have no credentials, query, or fragment')\n }\n const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(callbackUrl.hostname)\n if (callbackUrl.protocol !== 'https:' && !(callbackUrl.protocol === 'http:' && loopback)) {\n throw new Error('SSO requires HTTPS except on loopback')\n }\n if (callbackUrl.protocol === 'https:' && !opts.secureCookies) {\n throw new Error('SSO HTTPS callbacks require Secure cookies')\n }\n }\n const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE\n let mintSessionCookies: (args: TangleSsoSessionCookieArgs) => Promise<readonly string[]>\n if (opts.setSessionCookie) {\n const seam = opts.setSessionCookie\n mintSessionCookies = async (args) => await seam(args)\n } else if (opts.sessionCookieSecret) {\n const secret = opts.sessionCookieSecret\n mintSessionCookies = async ({ token, secure, ttlSeconds }) => [\n serializeCookie(await signSessionCookieValue(token, secret), {\n name: secure ? `__Secure-${sessionCookieName}` : sessionCookieName,\n secure,\n maxAgeSeconds: ttlSeconds,\n }),\n ]\n } else {\n throw new Error('TangleSsoHandlerOptions requires setSessionCookie or sessionCookieSecret')\n }\n const sessionTtlSeconds = opts.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS\n const stateTtlSeconds = opts.stateTtlSeconds ?? DEFAULT_STATE_TTL_SECONDS\n if (!Number.isFinite(stateTtlSeconds) || stateTtlSeconds <= 0\n || !Number.isFinite(sessionTtlSeconds) || sessionTtlSeconds <= 0) {\n throw new Error('SSO lifetimes must be positive and finite')\n }\n const defaultRedirectPath = sanitizeRedirectPath(opts.defaultRedirectPath ?? DEFAULT_REDIRECT_PATH, DEFAULT_REDIRECT_PATH)\n const loginPath = sanitizeRedirectPath(opts.loginPath ?? DEFAULT_LOGIN_PATH, DEFAULT_LOGIN_PATH)\n const log = opts.log ?? (() => {})\n const now = opts.now ?? Date.now\n const stateConfig: SsoStateConfig = { secret: opts.stateSecret, ttlMs: stateTtlSeconds * 1000, now }\n const stateCookieOpts = { name: opts.stateCookieName, secure: opts.secureCookies }\n function loginErrorRedirect(code: string): Response {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const target = new URL(loginPath, REDIRECT_REFERENCE_ORIGIN)\n target.searchParams.set('error', code)\n return redirectResponse(`${target.pathname}${target.search}${target.hash}`, headers)\n }\n return {\n async start(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity') && url.origin !== callbackUrl.origin) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const redirectPath = sanitizeRedirectPath(url.searchParams.get('redirect'), defaultRedirectPath)\n const state = await createSignedSsoState(stateConfig)\n const payload: StateCookiePayload = { s: state, r: redirectPath }\n let authorizationUrl: string\n if (opts.protocol === 'oidc') {\n const pkce = await opts.createPkcePair()\n payload.v = pkce.verifier\n payload.m = await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))\n authorizationUrl = opts.auth.authorizeUrl({ state, codeChallenge: pkce.challenge })\n if (new URL(authorizationUrl).searchParams.get('redirect_uri') !== opts.callbackUrl) {\n throw new Error('PlatformOidcClient.redirectUri must equal callbackUrl')\n }\n } else {\n authorizationUrl = opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl })\n }\n const cookie = serializeCookie(JSON.stringify(payload), {\n ...stateCookieOpts,\n maxAgeSeconds: stateTtlSeconds,\n })\n const headers = new Headers()\n headers.append('Set-Cookie', cookie)\n return redirectResponse(authorizationUrl, headers)\n },\n async callback(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity')\n && (url.origin !== callbackUrl.origin || url.pathname !== callbackUrl.pathname)) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const code = url.searchParams.get('code')\n const stateFromPlatform = url.searchParams.get('state')\n if (!code || !stateFromPlatform || url.searchParams.has('error')) return loginErrorRedirect('tangle_callback_missing')\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || !constantTimeEqual(payload.s, stateFromPlatform)) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n if (opts.protocol === 'oidc' && (!payload.v || !/^[A-Za-z0-9_-]{43,128}$/.test(payload.v)\n || !payload.m || !constantTimeEqual(payload.m, await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))))) {\n return loginErrorRedirect('tangle_state_mismatch')\n }\n const tokenRequestedAt = now()\n let oidcTokens: TangleOidcSsoTokens | undefined\n let legacyExchange: TangleSsoExchangeResult | undefined\n let createdSession: string | undefined\n let committed = false\n try {\n let exchanged: { user: TangleSsoExchangeResult['user']; emailVerified: boolean }\n try {\n if (opts.protocol === 'oidc') {\n const result = await opts.auth.exchange(code, payload.v!)\n oidcTokens = result.tokens\n if (!oidcTokens.refreshToken || !Number.isFinite(oidcTokens.expiresIn) || oidcTokens.expiresIn! <= 0) {\n throw new Error('OIDC requires offline_access and a finite access-token lifetime')\n }\n exchanged = { user: result.user, emailVerified: result.user.emailVerified }\n } else if (opts.protocol === 'identity') {\n const identity = await opts.auth.exchange(code)\n if (identity.kind !== 'identity' || 'apiKey' in identity || 'keyId' in identity\n || !identity.user || typeof identity.user.id !== 'string' || !identity.user.id.trim()\n || typeof identity.user.email !== 'string' || !identity.user.email.trim()) {\n throw new Error('Identity exchange returned an invalid result')\n }\n exchanged = identity\n } else {\n legacyExchange = await opts.auth.exchange(code)\n exchanged = legacyExchange\n }\n } catch {\n log('[tangle-sso] exchange failed')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n if (exchanged.emailVerified !== true) {\n log('[tangle-sso] exchange did not include a verified-email proof')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({ email, platformUserId: exchanged.user.id })\n if (resolution.kind === 'reject') throw new TangleSsoAccountConflictError(resolution.reason)\n ;({ userId } = await opts.store.upsertUserByEmail({\n email,\n name: exchanged.user.name ?? null,\n tangleUserId: exchanged.user.id,\n resolution,\n }))\n } catch (err) {\n if (err instanceof TangleSsoAccountConflictError) {\n log('[tangle-sso] account linking conflict', err.reason)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\n const expiresAt = new Date(now() + sessionTtlSeconds * 1000)\n const { token } = await opts.store.createSession({\n userId,\n expiresAt,\n ipAddress: clientIp(request),\n userAgent: request.headers.get('user-agent'),\n })\n createdSession = token\n const link = {\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n }\n if (opts.protocol === 'oidc') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink({\n ...link,\n tokens: oidcTokens!,\n accessTokenExpiresAt: new Date(tokenRequestedAt + oidcTokens!.expiresIn! * 1000),\n })\n committed = true\n return response\n }\n if (opts.protocol === 'identity') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink(link)\n committed = true\n return response\n }\n await opts.store.saveTangleLink({ ...link, apiKey: legacyExchange!.apiKey, planTier: legacyExchange!.plan?.tier ?? null })\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n } catch (error) {\n if (opts.protocol !== 'oidc' && opts.protocol !== 'identity') throw error\n log('[tangle-sso] local session persistence failed')\n return loginErrorRedirect('tangle_session_failed')\n } finally {\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity') && !committed) {\n if (createdSession) {\n try { await opts.store.deleteSession({ sessionToken: createdSession }) }\n catch { log('[tangle-sso] unpublished session cleanup failed') }\n }\n if (opts.protocol === 'oidc' && oidcTokens) {\n try { if (oidcTokens.refreshToken) await opts.auth.revoke(oidcTokens.refreshToken, 'refresh_token') }\n catch { log('[tangle-sso] rejected grant refresh revocation failed') }\n try { await opts.auth.revoke(oidcTokens.accessToken, 'access_token') }\n catch { log('[tangle-sso] rejected grant access revocation failed') }\n }\n }\n }\n },\n }\n}\n","/**\n * Request guards for agent-app routes: session auth (302 redirect for pages,\n * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to\n * non-admins), and the billable-balance gate (402 with a stable code).\n * Session resolution is a seam; thrown Responses follow the router convention\n * of surfacing a thrown Response as the route result.\n */\n\nimport { isTangleBillingEnforcementDisabled } from '../runtime/model'\n\n/** Define options for configuring authentication guard behavior and session retrieval */\nexport interface AuthGuardOptions<Session> {\n /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */\n getSession(request: Request): Promise<Session | null | undefined>\n /** Default '/login'. */\n loginPath?: string\n}\n\n/** Resolve user authentication and session requirements for page and API requests */\nexport interface AuthGuard<Session> {\n /** Page guard — throws a 302 redirect Response to `loginPath`. */\n requireUser(request: Request): Promise<Session>\n /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */\n requireApiUser(request: Request): Promise<Session>\n /** `apiResponse` selects the 401 JSON path over the redirect. */\n requireSession(request: Request, opts?: { apiResponse?: boolean }): Promise<Session>\n getOptionalSession(request: Request): Promise<Session | null>\n}\n\n/** Create an authentication guard that enforces session presence and handles unauthorized access responses */\nexport function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session> {\n const loginPath = opts.loginPath ?? '/login'\n\n async function requireSession(request: Request, o: { apiResponse?: boolean } = {}): Promise<Session> {\n const session = await opts.getSession(request)\n if (!session) {\n if (o.apiResponse) {\n throw Response.json({ error: 'Unauthorized', code: 'auth.unauthenticated' }, { status: 401 })\n }\n throw new Response(null, { status: 302, headers: { Location: loginPath } })\n }\n return session\n }\n\n return {\n requireSession,\n requireUser: (request) => requireSession(request),\n requireApiUser: (request) => requireSession(request, { apiResponse: true }),\n getOptionalSession: async (request) => (await opts.getSession(request)) ?? null,\n }\n}\n\n/** Resolve a value or an HTTP response indicating failure in a guarded operation */\nexport type GuardResolution<T> = { ok: true; value: T } | { ok: false; response: Response }\n\n/**\n * Adapt a guard that THROWS a Response (the quartet above — the router\n * convention) to the `{ok: true, value} | {ok: false, response}` resolution\n * shape the route factories take (`/chat-routes` `authorize`,\n * `/interactions` `resolveConnection`, `/chat-routes` upload `authorize`).\n * Every product wrote this try/catch by hand; it lives here once.\n */\nexport async function guardResolution<T>(run: () => Promise<T>): Promise<GuardResolution<T>> {\n try {\n return { ok: true, value: await run() }\n } catch (err) {\n if (err instanceof Response) return { ok: false, response: err }\n throw err\n }\n}\n\n/** Comma/whitespace separated → trimmed, lowercased, empties dropped. */\nexport function parseAdminEmails(raw: string | null | undefined): string[] {\n return (raw ?? '')\n .split(/[,\\s]+/)\n .map((e) => e.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** Define options to resolve user session and control access based on allowed admin emails */\nexport interface AdminGuardOptions<Session> {\n requireUser(request: Request): Promise<Session>\n emailOf(session: Session): string | null | undefined\n /** Resolved per request; an EMPTY allowlist refuses everyone. */\n allowedEmails(): string[]\n}\n\n/** Non-admins (and empty allowlists) get 404, keeping the route invisible —\n * better than a \"forbidden\" footprint that advertises its existence. */\nexport function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session> {\n return async (request) => {\n const session = await opts.requireUser(request)\n const allowed = opts.allowedEmails()\n if (allowed.length === 0) throw new Response('Not found', { status: 404 })\n const email = (opts.emailOf(session) ?? '').toLowerCase()\n if (!allowed.includes(email)) throw new Response('Not found', { status: 404 })\n return session\n }\n}\n\n/** Describe the billable balance state including overage permission and remaining USD balance */\nexport interface BillableBalanceState {\n overageAllowed: boolean\n remainingBalanceUsd: number\n}\n\n/** Define options to assert and customize billable balance enforcement behavior */\nexport interface AssertBillableBalanceOptions {\n env?: Record<string, string | undefined>\n /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),\n * fed to `isTangleBillingEnforcementDisabled`. */\n enforcementEnvVar?: string\n /** Default 'Add balance or upgrade your plan to invoke this agent.'. */\n errorMessage?: string\n /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */\n errorBody?: Record<string, unknown>\n}\n\n/**\n * Gate a billable turn: passes when enforcement is disabled (dev default),\n * the tier allows overage, or remaining balance is positive. Otherwise throws\n * a 402 Response with the stable `billing.balance_required` code so clients\n * can route to the billing screen.\n */\nexport function assertBillableBalance(state: BillableBalanceState, opts: AssertBillableBalanceOptions = {}): void {\n if (isTangleBillingEnforcementDisabled({ env: opts.env, enforcementEnvVar: opts.enforcementEnvVar })) return\n if (state.overageAllowed || state.remainingBalanceUsd > 0) return\n // errorBody first: the stable error/code contract always wins over caller extras.\n throw Response.json(\n {\n ...opts.errorBody,\n error: opts.errorMessage ?? 'Add balance or upgrade your plan to invoke this agent.',\n code: 'billing.balance_required',\n },\n { status: 402 },\n )\n}\n"],"mappings":"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","names":["headers","sessionCookies"]}
@@ -13,7 +13,7 @@ import {
13
13
  resolveTangleSsoAccount,
14
14
  signSessionCookieValue,
15
15
  verifySignedSsoState
16
- } from "../chunk-MVIIH2EQ.js";
16
+ } from "../chunk-MUABCTFT.js";
17
17
  import {
18
18
  resolveTangleDevOrUserKey,
19
19
  resolveTangleExecutionEnvironment
@@ -41,6 +41,21 @@ export interface TangleSsoAuthClient {
41
41
  }): string;
42
42
  exchange(code: string): Promise<TangleSsoExchangeResult>;
43
43
  }
44
+ /** First-party login exchanges identity without minting a Platform API key. */
45
+ export interface TangleIdentitySsoExchangeResult {
46
+ kind: 'identity';
47
+ emailVerified: boolean;
48
+ user: TangleSsoExchangeResult['user'];
49
+ apiKey?: never;
50
+ keyId?: never;
51
+ }
52
+ export interface TangleIdentitySsoAuthClient {
53
+ authorizeUrl(options: {
54
+ state: string;
55
+ redirectUri?: string;
56
+ }): string;
57
+ exchange(code: string): Promise<TangleIdentitySsoExchangeResult>;
58
+ }
44
59
  /** Token fields the callback persists from the runtime's OIDC exchange. */
45
60
  export interface TangleOidcSsoTokens {
46
61
  accessToken: string;
@@ -203,6 +218,29 @@ export interface TangleOidcSsoAccountStore extends Omit<TangleSsoAccountStore, '
203
218
  sessionToken: string;
204
219
  }): Promise<void>;
205
220
  }
221
+ /** Persist only the stable identity link for a first-party app. */
222
+ export interface TangleIdentitySsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {
223
+ /** `upsertUserByEmail` may create the local user, but must not publish a new
224
+ * platform link or update an existing user's verified-email state. The
225
+ * callback has not yet created a session or prepared its cookie. */
226
+ upsertUserByEmail: TangleSsoAccountStore['upsertUserByEmail'];
227
+ /** Commit the identity link and any verified-email update atomically. Do not
228
+ * reject after committing. If the store can partially write before throwing,
229
+ * `deleteSession` must remove only writes made for this session token. */
230
+ saveTangleLink(input: {
231
+ userId: string;
232
+ sessionToken: string;
233
+ tangleUserId: string;
234
+ email: string;
235
+ name: string | null;
236
+ }): Promise<void>;
237
+ /** Remove an unpublished session and any link or verification writes that a
238
+ * rejected `saveTangleLink` made for its exact token. Preserve preexisting
239
+ * links and other sessions. */
240
+ deleteSession(input: {
241
+ sessionToken: string;
242
+ }): Promise<void>;
243
+ }
206
244
  /** Successful-login context handed to the `setSessionCookie` seam. */
207
245
  export interface TangleSsoSessionCookieArgs {
208
246
  /** Session token returned by `store.createSession`. */
@@ -329,6 +367,11 @@ export interface TangleOidcSsoHandlerOptions extends Omit<TangleSsoHandlerOption
329
367
  challenge: string;
330
368
  }>;
331
369
  }
370
+ export interface TangleIdentitySsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {
371
+ protocol: 'identity';
372
+ auth: TangleIdentitySsoAuthClient;
373
+ store: TangleIdentitySsoAccountStore;
374
+ }
332
375
  /** Define handlers for SSO start and callback routes managing authentication flow and session cookies */
333
376
  export interface TangleSsoHandlers {
334
377
  /** GET start route: mint + sign state, set the state cookie, 302 to the
@@ -342,4 +385,4 @@ export interface TangleSsoHandlers {
342
385
  callback(request: Request): Promise<Response>;
343
386
  }
344
387
  /** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */
345
- export declare function createTangleSsoHandlers(opts: TangleSsoHandlerOptions | TangleOidcSsoHandlerOptions): TangleSsoHandlers;
388
+ export declare function createTangleSsoHandlers(opts: TangleSsoHandlerOptions | TangleOidcSsoHandlerOptions | TangleIdentitySsoHandlerOptions): TangleSsoHandlers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.49.37",
3
+ "version": "0.50.0",
4
4
  "packageManager": "pnpm@11.24.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [
@@ -624,12 +624,12 @@
624
624
  "peerDependencies": {
625
625
  "@firecrawl/pdf-inspector-wasm": ">=0.1.3",
626
626
  "@huggingface/transformers": ">=3",
627
- "@tangle-network/agent-eval": ">=0.199.0 <0.200.0",
628
- "@tangle-network/agent-integrations": ">=0.54.2 <0.58.0",
627
+ "@tangle-network/agent-eval": ">=0.199.0 <0.201.0",
628
+ "@tangle-network/agent-integrations": ">=0.54.2 <0.58.0 || >=0.59.0 <0.60.0",
629
629
  "@tangle-network/agent-interface": "^2.13.0",
630
630
  "@tangle-network/agent-knowledge": "^17.1.2",
631
631
  "@tangle-network/agent-profile-materialize": ">=0.20.1 <0.21.0",
632
- "@tangle-network/agent-runtime": ">=0.282.2 <0.283.0",
632
+ "@tangle-network/agent-runtime": ">=0.282.2 <0.284.0",
633
633
  "@tangle-network/brand": ">=1.6.0",
634
634
  "@tangle-network/hub-sdk": ">=0.19.3 <0.21.0",
635
635
  "@tangle-network/sandbox": ">=0.45.0 <0.48.0 || ^0.49.0 || ^0.50.0 || ^0.51.0 || ^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/platform/sso.ts","../src/platform/guards.ts"],"sourcesContent":["/**\n * Tangle SSO for agent apps. OIDC callers use the registered runtime client\n * with S256 PKCE. Legacy API-key callers remain explicit during migration.\n * OIDC never falls back to cross-site exchange. Account matching and native\n * session cookie minting remain shared.\n */\n\nimport { clearCookieHeader, readCookieValue, serializeCookie } from '../web/index'\n\nconst DEFAULT_STATE_TTL_SECONDS = 600\nconst DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7\nconst DEFAULT_REDIRECT_PATH = '/app'\nconst DEFAULT_LOGIN_PATH = '/login'\nconst DEFAULT_SESSION_COOKIE = 'better-auth.session_token'\n\n// ── Signed state ────────────────────────────────────────────────────────────\n\n/** Define configuration options for managing SSO state including secret, lifetime, and clock injection */\nexport interface SsoStateConfig {\n /** HMAC-SHA256 secret (e.g. the app's auth secret). */\n secret: string\n /** State lifetime in ms. Default 600 000. */\n ttlMs?: number\n /** Injectable clock (ms since epoch). Default Date.now. */\n now?: () => number\n}\n\nfunction randomHex(bytes: number): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nasync function hmacBytes(secret: string, value: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value)))\n}\n\nasync function hmacHex(secret: string, value: string): Promise<string> {\n return Array.from(await hmacBytes(secret, value), (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n\n/** Mint a `<randomHex32>.<timestamp36>.<hmacHex>` state value. The timestamp\n * is inside the signed payload, so expiry survives cookie-attribute tampering. */\nexport async function createSignedSsoState(config: SsoStateConfig): Promise<string> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const now = config.now ?? Date.now\n const payload = `${randomHex(16)}.${now().toString(36)}`\n return `${payload}.${await hmacHex(config.secret, payload)}`\n}\n\n/** Verify the MAC (constant-time) and the signed TTL. */\nexport async function verifySignedSsoState(state: string, config: SsoStateConfig): Promise<boolean> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const parts = state.split('.')\n if (parts.length !== 3) return false\n const [random, timestamp, mac] = parts\n if (!random || !timestamp || !mac || !/^[0-9a-z]+$/.test(timestamp)) return false\n const expected = await hmacHex(config.secret, `${random}.${timestamp}`)\n if (!constantTimeEqual(mac, expected)) return false\n const mintedAt = parseInt(timestamp, 36)\n if (!Number.isFinite(mintedAt)) return false\n const now = config.now ?? Date.now\n const ttlMs = config.ttlMs ?? DEFAULT_STATE_TTL_SECONDS * 1000\n const age = now() - mintedAt\n return age >= 0 && age <= ttlMs\n}\n\n// ── Seams ───────────────────────────────────────────────────────────────────\n\n/** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */\nexport interface TangleSsoExchangeResult {\n apiKey: string\n /** The platform's canonical verified-email proof. */\n emailVerified: boolean\n user: { id: string; email: string; name?: string | null }\n plan?: { tier: string } | null\n}\n\n/** Structural mirror of the platform auth wire client — any object with these\n * two methods satisfies it without this module importing the concrete class. */\nexport interface TangleSsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string): Promise<TangleSsoExchangeResult>\n}\n\n/** Token fields the callback persists from the runtime's OIDC exchange. */\nexport interface TangleOidcSsoTokens {\n accessToken: string\n tokenType: string\n expiresIn?: number\n refreshToken?: string\n idToken?: string\n scope?: string\n}\n\n/** Verified identity returned by the runtime's OIDC userinfo lookup. */\nexport interface TangleOidcSsoUser {\n id: string\n email: string\n emailVerified: true\n name?: string\n}\n\n/** The caller needs only the OIDC operations used by this callback. Keeping\n * this structural prevents legacy consumers from needing the optional\n * Runtime peer just to load the SSO declarations. */\nexport interface TangleOidcSsoAuthClient {\n authorizeUrl(options: {\n state: string\n codeChallenge: string\n nonce?: string\n prompt?: 'login' | 'consent' | 'none'\n loginHint?: string\n }): string\n exchange(code: string, codeVerifier: string): Promise<{\n tokens: TangleOidcSsoTokens\n user: TangleOidcSsoUser\n }>\n revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void>\n}\n\n/** Local account shape required by the verified Tangle SSO account policy.\n * Consumers map their user/link tables into this shape before resolving an\n * exchange. */\nexport interface TangleSsoLocalAccount {\n userId: string\n email: string\n emailVerified: boolean\n platformUserId: string | null\n}\n\n/** Reasons a local account lookup must stop instead of linking an exchange. */\nexport type TangleSsoAccountConflictReason =\n | 'ambiguous-platform-id'\n | 'platform-id-email-conflict'\n | 'unverified-email'\n | 'email-platform-id-conflict'\n | 'ambiguous-email'\n | 'invalid-platform-id'\n | 'invalid-email'\n | 'platform-match-mismatch'\n | 'email-match-mismatch'\n\n/** Result of applying the verified Tangle SSO account matching policy. */\nexport type TangleSsoAccountResolution =\n | { kind: 'existing'; userId: string; matchedBy: 'platform-id' | 'verified-email' }\n | { kind: 'create' }\n | { kind: 'reject'; reason: TangleSsoAccountConflictReason }\n\n/** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the\n * platform's stable identity. */\nexport interface TangleSsoAccountResolutionInput {\n email: string\n platformMatches: readonly TangleSsoLocalAccount[]\n emailMatches: readonly TangleSsoLocalAccount[]\n platformUserId: string\n}\n\n/** Normalize an email for comparisons and persistence queries. */\nexport function normalizeTangleSsoEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nfunction normalizedPlatformUserId(value: string | null | undefined): string | null {\n const normalized = value?.trim()\n return normalized ? normalized : null\n}\n\n/**\n * Resolve a platform exchange against all local rows found by stable platform\n * identity and normalized email.\n *\n * A stable platform id takes precedence over email. Every ambiguous or\n * contradictory result rejects, and email fallback requires a verified local\n * email that is not already bound to another platform id. The resolver is\n * pure so each consumer can query its own schema, including schemas without\n * unique indexes, then rerun it after an insert race.\n */\nexport function resolveTangleSsoAccount(\n input: TangleSsoAccountResolutionInput,\n): TangleSsoAccountResolution {\n const email = normalizeTangleSsoEmail(input.email)\n const platformUserId = normalizedPlatformUserId(input.platformUserId)\n if (!email) return { kind: 'reject', reason: 'invalid-email' }\n if (!platformUserId) return { kind: 'reject', reason: 'invalid-platform-id' }\n\n if (input.platformMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-platform-id' }\n }\n if (input.emailMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-email' }\n }\n\n if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {\n return { kind: 'reject', reason: 'platform-match-mismatch' }\n }\n if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {\n return { kind: 'reject', reason: 'email-match-mismatch' }\n }\n\n const platformMatch = input.platformMatches[0]\n const emailMatch = input.emailMatches[0]\n\n if (platformMatch) {\n if (emailMatch && emailMatch.userId !== platformMatch.userId) {\n return { kind: 'reject', reason: 'platform-id-email-conflict' }\n }\n return { kind: 'existing', userId: platformMatch.userId, matchedBy: 'platform-id' }\n }\n\n if (!emailMatch) return { kind: 'create' }\n const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId)\n if (existingPlatformUserId === platformUserId) {\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'platform-id' }\n }\n if (emailMatch.emailVerified !== true) {\n return { kind: 'reject', reason: 'unverified-email' }\n }\n\n if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {\n return { kind: 'reject', reason: 'email-platform-id-conflict' }\n }\n\n // The platform proof is already verified. A verified local email row is safe\n // to link when it is unbound or already carries this same platform id.\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'verified-email' }\n}\n\n/** Thrown by an account store when the verified SSO policy rejects a link. */\nexport class TangleSsoAccountConflictError extends Error {\n readonly reason: TangleSsoAccountConflictReason\n\n constructor(reason: TangleSsoAccountConflictReason, message = 'Tangle SSO account linking was rejected') {\n super(message)\n this.name = 'TangleSsoAccountConflictError'\n this.reason = reason\n }\n}\n\n/** Thrown by `upsertUserByEmail` when the app-local user row cannot be\n * created; the callback handler maps it to `?error=tangle_user_create_failed`.\n * Any other store error propagates. */\nexport class TangleSsoUserCreateError extends Error {\n constructor(message = 'Failed to create local user for Tangle SSO') {\n super(message)\n this.name = 'TangleSsoUserCreateError'\n }\n}\n\n/**\n * Account persistence seam. Covers both storage styles in use: link-table\n * apps (a per-user platform-link row) and session-column apps (the key on the\n * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,\n * and each app persists with the key it needs. `createSession` runs first so\n * the token is always available to `saveTangleLink`.\n */\nexport interface TangleSsoAccountStore {\n /** Resolve the local account before any user, session, or link write.\n * Implementations must query every candidate by stable platform id and\n * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent\n * fail-closed policy). The callback rejects `kind: 'reject'` results before\n * it calls `upsertUserByEmail`. */\n resolveAccount(input: { email: string; platformUserId: string }): Promise<TangleSsoAccountResolution>\n /** Find-or-create the app-local user selected by `resolveAccount`.\n * `tangleUserId` is the platform's stable user id — match on it first when\n * the app stores it (emails are mutable on the platform; the id is not),\n * falling back to email for first-time logins. */\n upsertUserByEmail(input: {\n email: string\n name: string | null\n tangleUserId: string\n resolution: Exclude<TangleSsoAccountResolution, { kind: 'reject' }>\n }): Promise<{ userId: string }>\n /** Create an app session row; returns the session-cookie token value. */\n createSession(input: {\n userId: string\n expiresAt: Date\n ipAddress: string | null\n userAgent: string | null\n }): Promise<{ token: string }>\n /** Persist the platform link (API key + platform identity). */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n apiKey: string\n planTier: string | null\n }): Promise<void>\n}\n\n/**\n * OIDC tokens are server-side credentials, not API keys. Implementations must\n * encrypt them at rest and bind them to the exact local session. Refresh and\n * disconnect must serialize updates to the same grant.\n */\nexport interface TangleOidcSsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** Atomically bind the grant to this exact session. If persistence partially\n * fails, `deleteSession` must remove every row and credential for it. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n tokens: TangleOidcSsoTokens\n accessTokenExpiresAt: Date\n }): Promise<void>\n /** Remove an unpublished local session if callback persistence fails.\n * This must also remove any grant rows or credentials partially written by\n * a rejected `saveTangleLink`, keyed by this exact session token. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n// ── Session cookie ──────────────────────────────────────────────────────────\n\n/** Successful-login context handed to the `setSessionCookie` seam. */\nexport interface TangleSsoSessionCookieArgs {\n /** Session token returned by `store.createSession`. */\n token: string\n /** Session expiry (now + `sessionTtlSeconds`). */\n expiresAt: Date\n /** Mirrors `sessionTtlSeconds` after defaulting. */\n ttlSeconds: number\n /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */\n secure: boolean\n}\n\n/**\n * Sign a session token to better-call's signed-cookie contract — the value\n * better-auth's `getSignedCookie` verifies: `<token>.<signature>` where the\n * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as\n * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call\n * rejects any other length or suffix, so url-safe/unpadded variants read back\n * as a null session). The joined value is percent-encoded once at cookie\n * serialization, matching better-call's `serializeSignedCookie` byte-exactly.\n */\nexport async function signSessionCookieValue(token: string, secret: string): Promise<string> {\n if (!secret) throw new Error('signSessionCookieValue requires a non-empty secret')\n const sig = await hmacBytes(secret, token)\n let bin = ''\n for (const byte of sig) bin += String.fromCharCode(byte)\n return `${token}.${btoa(bin)}`\n}\n\n/** Structural slice of a `betterAuth()` instance — only what cookie minting\n * reads. No better-auth import: the signing contract is implemented by\n * `signSessionCookieValue`, byte-compatible with better-auth's own\n * `makeSignature`. */\nexport interface BetterAuthSessionCookieSource {\n $context: PromiseLike<{\n secret: string\n authCookies: {\n sessionToken: {\n /** Final cookie name — better-auth decides the `__Secure-` prefix\n * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */\n name: string\n attributes: {\n secure?: boolean\n sameSite?: string\n path?: string\n httpOnly?: boolean\n domain?: string\n }\n }\n }\n }>\n}\n\n/** Define options to customize warning behavior for shadowed cookie names in authentication sessions */\nexport interface BetterAuthSessionCookieMinterOptions {\n /** Receives the shadowed-cookie-name warning (see below). Default\n * console.warn. */\n warn?: (message: string) => void\n}\n\n/**\n * Canonical `setSessionCookie` wiring for better-auth apps: mint the session\n * Set-Cookie exactly as better-auth's own login flows do — name + attributes\n * from `auth.$context.authCookies.sessionToken` (better-auth stays\n * authoritative over prefix/name/attributes) and the value signed to\n * better-call's `getSignedCookie` contract. A raw unprefixed\n * `better-auth.session_token` left by an earlier login is explicitly expired\n * so it cannot shadow the real cookie.\n *\n * Warns when the app's session cookie still has better-auth's DEFAULT name:\n * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie\n * under that exact name, and equal-path cookies are sent oldest-first — the\n * platform's cookie is always older (the user signs in there before the app's\n * callback runs), so the app reads the platform's token, fails its own\n * signature check, and every fresh login lands logged-out. Per-app\n * `advanced.cookiePrefix` is the fix.\n *\n * Throws on a domain-scoped session cookie for the same reason: a\n * `Domain=`-wide session cookie is exactly the shadowing footgun.\n */\nexport function createBetterAuthSessionCookieMinter(\n auth: BetterAuthSessionCookieSource,\n options: BetterAuthSessionCookieMinterOptions = {},\n): (args: TangleSsoSessionCookieArgs) => Promise<string[]> {\n const warn = options.warn ?? ((message: string) => console.warn(message))\n return async ({ token, ttlSeconds }) => {\n const ctx = await auth.$context\n if (!ctx.secret) {\n throw new Error('createBetterAuthSessionCookieMinter: auth context has no secret')\n }\n const { name, attributes } = ctx.authCookies.sessionToken\n if (attributes.domain) {\n throw new Error(\n `createBetterAuthSessionCookieMinter: refusing a domain-scoped session cookie (Domain=${attributes.domain}) — ` +\n 'a domain-wide session cookie shadows sibling apps that share the parent domain',\n )\n }\n if (name === DEFAULT_SESSION_COOKIE || name === `__Secure-${DEFAULT_SESSION_COOKIE}`) {\n warn(\n `[tangle-sso] session cookie is named \"${name}\" — better-auth's default. ` +\n 'The Tangle platform (id.tangle.tools) sets a Domain=.tangle.tools cookie under the same name, ' +\n \"and the platform's (older) cookie wins the Cookie-header order, so this app's sessions read back null. \" +\n \"Set a per-app prefix: betterAuth({ advanced: { cookiePrefix: '<app>' } }).\",\n )\n }\n const sameSite = typeof attributes.sameSite === 'string' ? attributes.sameSite : 'lax'\n const cookieOptions = {\n name,\n path: typeof attributes.path === 'string' ? attributes.path : '/',\n httpOnly: attributes.httpOnly !== false,\n sameSite: (sameSite.charAt(0).toUpperCase() + sameSite.slice(1)) as 'Lax' | 'Strict' | 'None',\n secure: Boolean(attributes.secure) || name.startsWith('__Secure-'),\n maxAgeSeconds: ttlSeconds,\n }\n const cookies = [serializeCookie(await signSessionCookieValue(token, ctx.secret), cookieOptions)]\n if (name !== DEFAULT_SESSION_COOKIE) {\n cookies.push(clearCookieHeader({ ...cookieOptions, name: DEFAULT_SESSION_COOKIE }))\n }\n return cookies\n }\n}\n\n// ── Handlers ────────────────────────────────────────────────────────────────\n\n/** Define configuration options for handling Tangle SSO authentication and session management */\nexport interface TangleSsoHandlerOptions {\n protocol?: 'legacy'\n auth: TangleSsoAuthClient\n store: TangleSsoAccountStore\n /** HMAC secret for the state cookie. */\n stateSecret: string\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n stateCookieName: string\n /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is\n * provided. The default path prepends `__Secure-` iff `secureCookies`. */\n sessionCookieName?: string\n /** Mint the host auth framework's own session cookie(s); return complete\n * Set-Cookie header values (the handler appends them verbatim and sets no\n * session cookie itself). Supply this when the framework should stay\n * authoritative over name/prefix/signing/attributes — e.g. better-auth:\n * `auth.$context.authCookies.sessionToken` + `makeSignature`. */\n setSessionCookie?: (\n args: TangleSsoSessionCookieArgs,\n ) => readonly string[] | Promise<readonly string[]>\n /** HMAC-SHA256 secret the host auth framework verifies session cookies with\n * (better-auth: its `secret`). Required when `setSessionCookie` is absent —\n * the default cookie is minted to better-call's signed contract via\n * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a\n * null session, so there is deliberately no fallback to `stateSecret`\n * (which is not guaranteed to be the auth secret). */\n sessionCookieSecret?: string\n /** Adds `Secure` to every cookie this module sets, and (default session\n * cookie only) the `__Secure-` name prefix. Must match the auth\n * framework's own secure-cookie decision (better-auth: https `baseURL` /\n * `advanced.useSecureCookies`), or it will look up a different cookie name\n * than the one set here. */\n secureCookies: boolean\n /** Default 604 800 (7 days). */\n sessionTtlSeconds?: number\n /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */\n stateTtlSeconds?: number\n /** Default '/app'. */\n defaultRedirectPath?: string\n /** Default '/login'. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n now?: () => number\n}\n\n/** Registered OIDC client and token store. The OIDC caller supplies the Runtime PKCE helper. */\nexport interface TangleOidcSsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'oidc'\n auth: TangleOidcSsoAuthClient\n store: TangleOidcSsoAccountStore\n /** Pass the Runtime's `createPkcePair` helper from the OIDC-only caller. */\n createPkcePair: () => Promise<{ verifier: string; challenge: string }>\n}\n\n/** Define handlers for SSO start and callback routes managing authentication flow and session cookies */\nexport interface TangleSsoHandlers {\n /** GET start route: mint + sign state, set the state cookie, 302 to the\n * platform authorize URL. `?redirect=` carries the post-login path. */\n start(request: Request): Promise<Response>\n /** GET callback route: verify state, exchange the code, upsert the user,\n * create the session, save the platform link, set the session cookie\n * (via the `setSessionCookie` seam, else signed to better-call's contract\n * with `sessionCookieSecret`), 302 to the saved redirect. Every failure\n * 302s to `loginPath?error=…` with the state cookie cleared. */\n callback(request: Request): Promise<Response>\n}\n\n/** Redirect targets must stay on this app's origin. A prefix check on `/`\n * and `//` is not enough: in the WHATWG parser a backslash is a path\n * separator for special schemes, so `/\\host` resolves to `https://host/`,\n * and tab or newline are stripped before parsing, so `/\\t/host` is `//host`.\n * Parse the value against a fixed origin and accept it only when that origin\n * survives; the result is the parser's normalised path, query and fragment.\n * Backslash and control characters are rejected up front as well, so the\n * decision does not rest on one parser's handling of them.\n *\n * The input staying on-origin is not enough either: dot-segment removal\n * turns `/.//host` or `/a/..//host` into the pathname `//host` with the\n * origin intact, and a browser resolves that Location off-origin. So the\n * emitted value must itself resolve on-origin and come back unchanged. */\nconst REDIRECT_REFERENCE_ORIGIN = 'https://redirect-reference.invalid'\n\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (!value || !value.startsWith('/') || /[\\\\\\u0000-\\u001f\\u007f]/.test(value)) return fallback\n const path = resolveOnReferenceOrigin(value)\n if (path === null || resolveOnReferenceOrigin(path) !== path) return fallback\n return path\n}\n\n/** `path + search + hash` of `value` resolved against the reference origin,\n * or null when the parse fails or lands on another origin. */\nfunction resolveOnReferenceOrigin(value: string): string | null {\n let url: URL\n try {\n url = new URL(value, REDIRECT_REFERENCE_ORIGIN)\n } catch {\n return null\n }\n if (url.origin !== REDIRECT_REFERENCE_ORIGIN) return null\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\n headers.set('Cache-Control', 'no-store')\n headers.set('Referrer-Policy', 'no-referrer')\n return new Response(null, { status: 302, headers })\n}\n\n/** Real client IP: `CF-Connecting-IP` behind Cloudflare, else the first\n * `x-forwarded-for` hop (the rest of the list is sender-controlled). */\nfunction clientIp(request: Request): string | null {\n return (\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n null\n )\n}\n\ninterface StateCookiePayload {\n s: string\n r: string\n v?: string\n m?: string\n}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw || raw.length > 4096) return null\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed === null || typeof parsed !== 'object') return null\n const { s, r, v, m } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r, ...(typeof v === 'string' ? { v } : {}), ...(typeof m === 'string' ? { m } : {}) }\n } catch { return null }\n}\n\nfunction stateBinding(payload: StateCookiePayload, callbackUrl: string): string {\n return JSON.stringify([payload.s, payload.r, payload.v ?? null, callbackUrl])\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(\n opts: TangleSsoHandlerOptions | TangleOidcSsoHandlerOptions,\n): TangleSsoHandlers {\n if (!opts.stateSecret) throw new Error('TangleSsoHandlerOptions.stateSecret is required')\n if (!opts.callbackUrl) throw new Error('TangleSsoHandlerOptions.callbackUrl is required')\n if (!opts.stateCookieName) throw new Error('TangleSsoHandlerOptions.stateCookieName is required')\n const callbackUrl = new URL(opts.callbackUrl)\n if (opts.protocol === 'oidc') {\n if (opts.stateSecret.length < 32) throw new Error('OIDC stateSecret must contain at least 32 characters')\n if (callbackUrl.username || callbackUrl.password || callbackUrl.hash || callbackUrl.search) {\n throw new Error('OIDC callbackUrl must be fixed and have no credentials, query, or fragment')\n }\n const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(callbackUrl.hostname)\n if (callbackUrl.protocol !== 'https:' && !(callbackUrl.protocol === 'http:' && loopback)) {\n throw new Error('OIDC requires HTTPS except on loopback')\n }\n if (callbackUrl.protocol === 'https:' && !opts.secureCookies) {\n throw new Error('OIDC HTTPS callbacks require Secure cookies')\n }\n }\n const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE\n let mintSessionCookies: (args: TangleSsoSessionCookieArgs) => Promise<readonly string[]>\n if (opts.setSessionCookie) {\n const seam = opts.setSessionCookie\n mintSessionCookies = async (args) => await seam(args)\n } else if (opts.sessionCookieSecret) {\n const secret = opts.sessionCookieSecret\n mintSessionCookies = async ({ token, secure, ttlSeconds }) => [\n serializeCookie(await signSessionCookieValue(token, secret), {\n name: secure ? `__Secure-${sessionCookieName}` : sessionCookieName,\n secure,\n maxAgeSeconds: ttlSeconds,\n }),\n ]\n } else {\n throw new Error('TangleSsoHandlerOptions requires setSessionCookie or sessionCookieSecret')\n }\n const sessionTtlSeconds = opts.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS\n const stateTtlSeconds = opts.stateTtlSeconds ?? DEFAULT_STATE_TTL_SECONDS\n if (!Number.isFinite(stateTtlSeconds) || stateTtlSeconds <= 0\n || !Number.isFinite(sessionTtlSeconds) || sessionTtlSeconds <= 0) {\n throw new Error('SSO lifetimes must be positive and finite')\n }\n const defaultRedirectPath = sanitizeRedirectPath(opts.defaultRedirectPath ?? DEFAULT_REDIRECT_PATH, DEFAULT_REDIRECT_PATH)\n const loginPath = sanitizeRedirectPath(opts.loginPath ?? DEFAULT_LOGIN_PATH, DEFAULT_LOGIN_PATH)\n const log = opts.log ?? (() => {})\n const now = opts.now ?? Date.now\n const stateConfig: SsoStateConfig = { secret: opts.stateSecret, ttlMs: stateTtlSeconds * 1000, now }\n const stateCookieOpts = { name: opts.stateCookieName, secure: opts.secureCookies }\n function loginErrorRedirect(code: string): Response {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const target = new URL(loginPath, REDIRECT_REFERENCE_ORIGIN)\n target.searchParams.set('error', code)\n return redirectResponse(`${target.pathname}${target.search}${target.hash}`, headers)\n }\n return {\n async start(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if (opts.protocol === 'oidc' && url.origin !== callbackUrl.origin) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const redirectPath = sanitizeRedirectPath(url.searchParams.get('redirect'), defaultRedirectPath)\n const state = await createSignedSsoState(stateConfig)\n const payload: StateCookiePayload = { s: state, r: redirectPath }\n let authorizationUrl: string\n if (opts.protocol === 'oidc') {\n const pkce = await opts.createPkcePair()\n payload.v = pkce.verifier\n payload.m = await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))\n authorizationUrl = opts.auth.authorizeUrl({ state, codeChallenge: pkce.challenge })\n if (new URL(authorizationUrl).searchParams.get('redirect_uri') !== opts.callbackUrl) {\n throw new Error('PlatformOidcClient.redirectUri must equal callbackUrl')\n }\n } else {\n authorizationUrl = opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl })\n }\n const cookie = serializeCookie(JSON.stringify(payload), {\n ...stateCookieOpts,\n maxAgeSeconds: stateTtlSeconds,\n })\n const headers = new Headers()\n headers.append('Set-Cookie', cookie)\n return redirectResponse(authorizationUrl, headers)\n },\n async callback(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if (opts.protocol === 'oidc'\n && (url.origin !== callbackUrl.origin || url.pathname !== callbackUrl.pathname)) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const code = url.searchParams.get('code')\n const stateFromPlatform = url.searchParams.get('state')\n if (!code || !stateFromPlatform || url.searchParams.has('error')) return loginErrorRedirect('tangle_callback_missing')\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || !constantTimeEqual(payload.s, stateFromPlatform)) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n if (opts.protocol === 'oidc' && (!payload.v || !/^[A-Za-z0-9_-]{43,128}$/.test(payload.v)\n || !payload.m || !constantTimeEqual(payload.m, await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))))) {\n return loginErrorRedirect('tangle_state_mismatch')\n }\n const tokenRequestedAt = now()\n let oidcTokens: TangleOidcSsoTokens | undefined\n let legacyExchange: TangleSsoExchangeResult | undefined\n let createdSession: string | undefined\n let committed = false\n try {\n let exchanged: { user: TangleSsoExchangeResult['user']; emailVerified: boolean }\n try {\n if (opts.protocol === 'oidc') {\n const result = await opts.auth.exchange(code, payload.v!)\n oidcTokens = result.tokens\n if (!oidcTokens.refreshToken || !Number.isFinite(oidcTokens.expiresIn) || oidcTokens.expiresIn! <= 0) {\n throw new Error('OIDC requires offline_access and a finite access-token lifetime')\n }\n exchanged = { user: result.user, emailVerified: result.user.emailVerified }\n } else {\n legacyExchange = await opts.auth.exchange(code)\n exchanged = legacyExchange\n }\n } catch {\n log('[tangle-sso] exchange failed')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n if (exchanged.emailVerified !== true) {\n log('[tangle-sso] exchange did not include a verified-email proof')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({ email, platformUserId: exchanged.user.id })\n if (resolution.kind === 'reject') throw new TangleSsoAccountConflictError(resolution.reason)\n ;({ userId } = await opts.store.upsertUserByEmail({\n email,\n name: exchanged.user.name ?? null,\n tangleUserId: exchanged.user.id,\n resolution,\n }))\n } catch (err) {\n if (err instanceof TangleSsoAccountConflictError) {\n log('[tangle-sso] account linking conflict', err.reason)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\n const expiresAt = new Date(now() + sessionTtlSeconds * 1000)\n const { token } = await opts.store.createSession({\n userId,\n expiresAt,\n ipAddress: clientIp(request),\n userAgent: request.headers.get('user-agent'),\n })\n createdSession = token\n const link = {\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n }\n if (opts.protocol === 'oidc') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink({\n ...link,\n tokens: oidcTokens!,\n accessTokenExpiresAt: new Date(tokenRequestedAt + oidcTokens!.expiresIn! * 1000),\n })\n committed = true\n return response\n }\n await opts.store.saveTangleLink({ ...link, apiKey: legacyExchange!.apiKey, planTier: legacyExchange!.plan?.tier ?? null })\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n } catch (error) {\n if (opts.protocol !== 'oidc') throw error\n log('[tangle-sso] local session persistence failed')\n return loginErrorRedirect('tangle_session_failed')\n } finally {\n if (opts.protocol === 'oidc' && !committed) {\n if (createdSession) {\n try { await opts.store.deleteSession({ sessionToken: createdSession }) }\n catch { log('[tangle-sso] unpublished session cleanup failed') }\n }\n if (oidcTokens) {\n try { if (oidcTokens.refreshToken) await opts.auth.revoke(oidcTokens.refreshToken, 'refresh_token') }\n catch { log('[tangle-sso] rejected grant refresh revocation failed') }\n try { await opts.auth.revoke(oidcTokens.accessToken, 'access_token') }\n catch { log('[tangle-sso] rejected grant access revocation failed') }\n }\n }\n }\n },\n }\n}\n","/**\n * Request guards for agent-app routes: session auth (302 redirect for pages,\n * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to\n * non-admins), and the billable-balance gate (402 with a stable code).\n * Session resolution is a seam; thrown Responses follow the router convention\n * of surfacing a thrown Response as the route result.\n */\n\nimport { isTangleBillingEnforcementDisabled } from '../runtime/model'\n\n/** Define options for configuring authentication guard behavior and session retrieval */\nexport interface AuthGuardOptions<Session> {\n /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */\n getSession(request: Request): Promise<Session | null | undefined>\n /** Default '/login'. */\n loginPath?: string\n}\n\n/** Resolve user authentication and session requirements for page and API requests */\nexport interface AuthGuard<Session> {\n /** Page guard — throws a 302 redirect Response to `loginPath`. */\n requireUser(request: Request): Promise<Session>\n /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */\n requireApiUser(request: Request): Promise<Session>\n /** `apiResponse` selects the 401 JSON path over the redirect. */\n requireSession(request: Request, opts?: { apiResponse?: boolean }): Promise<Session>\n getOptionalSession(request: Request): Promise<Session | null>\n}\n\n/** Create an authentication guard that enforces session presence and handles unauthorized access responses */\nexport function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session> {\n const loginPath = opts.loginPath ?? '/login'\n\n async function requireSession(request: Request, o: { apiResponse?: boolean } = {}): Promise<Session> {\n const session = await opts.getSession(request)\n if (!session) {\n if (o.apiResponse) {\n throw Response.json({ error: 'Unauthorized', code: 'auth.unauthenticated' }, { status: 401 })\n }\n throw new Response(null, { status: 302, headers: { Location: loginPath } })\n }\n return session\n }\n\n return {\n requireSession,\n requireUser: (request) => requireSession(request),\n requireApiUser: (request) => requireSession(request, { apiResponse: true }),\n getOptionalSession: async (request) => (await opts.getSession(request)) ?? null,\n }\n}\n\n/** Resolve a value or an HTTP response indicating failure in a guarded operation */\nexport type GuardResolution<T> = { ok: true; value: T } | { ok: false; response: Response }\n\n/**\n * Adapt a guard that THROWS a Response (the quartet above — the router\n * convention) to the `{ok: true, value} | {ok: false, response}` resolution\n * shape the route factories take (`/chat-routes` `authorize`,\n * `/interactions` `resolveConnection`, `/chat-routes` upload `authorize`).\n * Every product wrote this try/catch by hand; it lives here once.\n */\nexport async function guardResolution<T>(run: () => Promise<T>): Promise<GuardResolution<T>> {\n try {\n return { ok: true, value: await run() }\n } catch (err) {\n if (err instanceof Response) return { ok: false, response: err }\n throw err\n }\n}\n\n/** Comma/whitespace separated → trimmed, lowercased, empties dropped. */\nexport function parseAdminEmails(raw: string | null | undefined): string[] {\n return (raw ?? '')\n .split(/[,\\s]+/)\n .map((e) => e.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** Define options to resolve user session and control access based on allowed admin emails */\nexport interface AdminGuardOptions<Session> {\n requireUser(request: Request): Promise<Session>\n emailOf(session: Session): string | null | undefined\n /** Resolved per request; an EMPTY allowlist refuses everyone. */\n allowedEmails(): string[]\n}\n\n/** Non-admins (and empty allowlists) get 404, keeping the route invisible —\n * better than a \"forbidden\" footprint that advertises its existence. */\nexport function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session> {\n return async (request) => {\n const session = await opts.requireUser(request)\n const allowed = opts.allowedEmails()\n if (allowed.length === 0) throw new Response('Not found', { status: 404 })\n const email = (opts.emailOf(session) ?? '').toLowerCase()\n if (!allowed.includes(email)) throw new Response('Not found', { status: 404 })\n return session\n }\n}\n\n/** Describe the billable balance state including overage permission and remaining USD balance */\nexport interface BillableBalanceState {\n overageAllowed: boolean\n remainingBalanceUsd: number\n}\n\n/** Define options to assert and customize billable balance enforcement behavior */\nexport interface AssertBillableBalanceOptions {\n env?: Record<string, string | undefined>\n /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),\n * fed to `isTangleBillingEnforcementDisabled`. */\n enforcementEnvVar?: string\n /** Default 'Add balance or upgrade your plan to invoke this agent.'. */\n errorMessage?: string\n /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */\n errorBody?: Record<string, unknown>\n}\n\n/**\n * Gate a billable turn: passes when enforcement is disabled (dev default),\n * the tier allows overage, or remaining balance is positive. Otherwise throws\n * a 402 Response with the stable `billing.balance_required` code so clients\n * can route to the billing screen.\n */\nexport function assertBillableBalance(state: BillableBalanceState, opts: AssertBillableBalanceOptions = {}): void {\n if (isTangleBillingEnforcementDisabled({ env: opts.env, enforcementEnvVar: opts.enforcementEnvVar })) return\n if (state.overageAllowed || state.remainingBalanceUsd > 0) return\n // errorBody first: the stable error/code contract always wins over caller extras.\n throw Response.json(\n {\n ...opts.errorBody,\n error: opts.errorMessage ?? 'Add balance or upgrade your plan to invoke this agent.',\n code: 'billing.balance_required',\n },\n { status: 402 },\n )\n}\n"],"mappings":"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","names":["headers","sessionCookies"]}