@tangle-network/agent-app 0.47.20 → 0.47.22

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.
@@ -57,6 +57,59 @@ async function verifySignedSsoState(state, config) {
57
57
  const ttlMs = config.ttlMs ?? DEFAULT_STATE_TTL_SECONDS * 1e3;
58
58
  return now() - mintedAt <= ttlMs;
59
59
  }
60
+ function normalizeTangleSsoEmail(email) {
61
+ return email.trim().toLowerCase();
62
+ }
63
+ function normalizedPlatformUserId(value) {
64
+ const normalized = value?.trim();
65
+ return normalized ? normalized : null;
66
+ }
67
+ function resolveTangleSsoAccount(input) {
68
+ const email = normalizeTangleSsoEmail(input.email);
69
+ const platformUserId = normalizedPlatformUserId(input.platformUserId);
70
+ if (!email) return { kind: "reject", reason: "invalid-email" };
71
+ if (!platformUserId) return { kind: "reject", reason: "invalid-platform-id" };
72
+ if (input.platformMatches.length > 1) {
73
+ return { kind: "reject", reason: "ambiguous-platform-id" };
74
+ }
75
+ if (input.emailMatches.length > 1) {
76
+ return { kind: "reject", reason: "ambiguous-email" };
77
+ }
78
+ if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {
79
+ return { kind: "reject", reason: "platform-match-mismatch" };
80
+ }
81
+ if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {
82
+ return { kind: "reject", reason: "email-match-mismatch" };
83
+ }
84
+ const platformMatch = input.platformMatches[0];
85
+ const emailMatch = input.emailMatches[0];
86
+ if (platformMatch) {
87
+ if (emailMatch && emailMatch.userId !== platformMatch.userId) {
88
+ return { kind: "reject", reason: "platform-id-email-conflict" };
89
+ }
90
+ return { kind: "existing", userId: platformMatch.userId, matchedBy: "platform-id" };
91
+ }
92
+ if (!emailMatch) return { kind: "create" };
93
+ const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId);
94
+ if (existingPlatformUserId === platformUserId) {
95
+ return { kind: "existing", userId: emailMatch.userId, matchedBy: "platform-id" };
96
+ }
97
+ if (emailMatch.emailVerified !== true) {
98
+ return { kind: "reject", reason: "unverified-email" };
99
+ }
100
+ if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {
101
+ return { kind: "reject", reason: "email-platform-id-conflict" };
102
+ }
103
+ return { kind: "existing", userId: emailMatch.userId, matchedBy: "verified-email" };
104
+ }
105
+ var TangleSsoAccountConflictError = class extends Error {
106
+ reason;
107
+ constructor(reason, message = "Tangle SSO account linking was rejected") {
108
+ super(message);
109
+ this.name = "TangleSsoAccountConflictError";
110
+ this.reason = reason;
111
+ }
112
+ };
60
113
  var TangleSsoUserCreateError = class extends Error {
61
114
  constructor(message = "Failed to create local user for Tangle SSO") {
62
115
  super(message);
@@ -192,15 +245,32 @@ function createTangleSsoHandlers(opts) {
192
245
  log("[tangle-sso] exchange failed", err);
193
246
  return loginErrorRedirect("tangle_exchange_failed");
194
247
  }
248
+ if (exchanged.emailVerified !== true) {
249
+ log("[tangle-sso] exchange did not include a verified-email proof");
250
+ return loginErrorRedirect("tangle_exchange_failed");
251
+ }
195
252
  let userId;
196
253
  try {
254
+ const email = normalizeTangleSsoEmail(exchanged.user.email);
255
+ const resolution = await opts.store.resolveAccount({
256
+ email,
257
+ platformUserId: exchanged.user.id
258
+ });
259
+ if (resolution.kind === "reject") {
260
+ throw new TangleSsoAccountConflictError(resolution.reason);
261
+ }
197
262
  ;
198
263
  ({ userId } = await opts.store.upsertUserByEmail({
199
- email: exchanged.user.email,
264
+ email,
200
265
  name: exchanged.user.name ?? null,
201
- tangleUserId: exchanged.user.id
266
+ tangleUserId: exchanged.user.id,
267
+ resolution
202
268
  }));
203
269
  } catch (err) {
270
+ if (err instanceof TangleSsoAccountConflictError) {
271
+ log("[tangle-sso] account linking conflict", err);
272
+ return loginErrorRedirect("tangle_account_conflict");
273
+ }
204
274
  if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect("tangle_user_create_failed");
205
275
  throw err;
206
276
  }
@@ -215,7 +285,7 @@ function createTangleSsoHandlers(opts) {
215
285
  userId,
216
286
  sessionToken: token,
217
287
  tangleUserId: exchanged.user.id,
218
- email: exchanged.user.email,
288
+ email: normalizeTangleSsoEmail(exchanged.user.email),
219
289
  name: exchanged.user.name ?? null,
220
290
  apiKey: exchanged.apiKey,
221
291
  planTier: exchanged.plan?.tier ?? null
@@ -291,6 +361,9 @@ function assertBillableBalance(state, opts = {}) {
291
361
  export {
292
362
  createSignedSsoState,
293
363
  verifySignedSsoState,
364
+ normalizeTangleSsoEmail,
365
+ resolveTangleSsoAccount,
366
+ TangleSsoAccountConflictError,
294
367
  TangleSsoUserCreateError,
295
368
  signSessionCookieValue,
296
369
  createBetterAuthSessionCookieMinter,
@@ -301,4 +374,4 @@ export {
301
374
  createAdminGuard,
302
375
  assertBillableBalance
303
376
  };
304
- //# sourceMappingURL=chunk-AXJV66PC.js.map
377
+ //# sourceMappingURL=chunk-TXM4SS46.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/platform/sso.ts","../src/platform/guards.ts"],"sourcesContent":["/**\n * Cross-site Tangle SSO for agent apps: signed-state CSRF cookies plus the\n * full start/callback orchestration against the platform's /cross-site\n * bridge. The platform wire client and account persistence are structural\n * seams (`TangleSsoAuthClient` / `TangleSsoAccountStore`), so this module\n * never imports agent-runtime, an auth framework, or a database driver.\n * WebCrypto only — runs in workerd without node compatibility flags.\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) 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 return now() - mintedAt <= 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/** 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// ── 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 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/** 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/** Accept only same-origin absolute paths (rejects `//host` protocol-relative URLs). */\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (value && value.startsWith('/') && !value.startsWith('//')) return value\n return fallback\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\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}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw) 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 } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r }\n } catch {\n return null\n }\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(opts: TangleSsoHandlerOptions): 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\n const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE\n\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(\n 'TangleSsoHandlerOptions requires setSessionCookie or sessionCookieSecret: ' +\n 'better-auth only accepts HMAC-signed (and, on https, __Secure--prefixed) session cookies, ' +\n 'so an unsigned default would mint sessions that read back null',\n )\n }\n const sessionTtlSeconds = opts.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS\n const stateTtlSeconds = opts.stateTtlSeconds ?? DEFAULT_STATE_TTL_SECONDS\n const defaultRedirectPath = opts.defaultRedirectPath ?? DEFAULT_REDIRECT_PATH\n const loginPath = opts.loginPath ?? 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\n const stateCookieOpts = { name: opts.stateCookieName, secure: opts.secureCookies }\n\n function loginErrorRedirect(code: string): Response {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n return redirectResponse(`${loginPath}?error=${code}`, headers)\n }\n\n return {\n async start(request) {\n const url = new URL(request.url)\n const redirectPath = sanitizeRedirectPath(url.searchParams.get('redirect'), defaultRedirectPath)\n const state = await createSignedSsoState(stateConfig)\n const cookie = serializeCookie(JSON.stringify({ s: state, r: redirectPath }), {\n ...stateCookieOpts,\n maxAgeSeconds: stateTtlSeconds,\n })\n const headers = new Headers()\n headers.append('Set-Cookie', cookie)\n return redirectResponse(opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl }), headers)\n },\n\n async callback(request) {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const stateFromPlatform = url.searchParams.get('state')\n if (!code || !stateFromPlatform) return loginErrorRedirect('tangle_callback_missing')\n\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || payload.s !== stateFromPlatform) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n\n let exchanged: TangleSsoExchangeResult\n try {\n exchanged = await opts.auth.exchange(code)\n } catch (err) {\n log('[tangle-sso] exchange failed', err)\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\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({\n email,\n platformUserId: exchanged.user.id,\n })\n if (resolution.kind === 'reject') {\n throw new TangleSsoAccountConflictError(resolution.reason)\n }\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)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\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\n await opts.store.saveTangleLink({\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n apiKey: exchanged.apiKey,\n planTier: exchanged.plan?.tier ?? null,\n })\n\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({\n token,\n expiresAt,\n ttlSeconds: sessionTtlSeconds,\n secure: opts.secureCookies,\n })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\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":[]}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ TangleSsoAccountConflictError,
2
3
  TangleSsoUserCreateError,
3
4
  assertBillableBalance,
4
5
  createAdminGuard,
@@ -7,10 +8,12 @@ import {
7
8
  createSignedSsoState,
8
9
  createTangleSsoHandlers,
9
10
  guardResolution,
11
+ normalizeTangleSsoEmail,
10
12
  parseAdminEmails,
13
+ resolveTangleSsoAccount,
11
14
  signSessionCookieValue,
12
15
  verifySignedSsoState
13
- } from "../chunk-AXJV66PC.js";
16
+ } from "../chunk-TXM4SS46.js";
14
17
  import {
15
18
  resolveTangleDevOrUserKey,
16
19
  resolveTangleExecutionEnvironment
@@ -363,6 +366,7 @@ export {
363
366
  FREE_TIER_SPEND_CAP_USD,
364
367
  PlatformBillingHttpError,
365
368
  TangleBearerMissingError,
369
+ TangleSsoAccountConflictError,
366
370
  TangleSsoUserCreateError,
367
371
  assertBillableBalance,
368
372
  createAdminGuard,
@@ -382,8 +386,10 @@ export {
382
386
  isSeatBillingEnabled,
383
387
  isTangleBearerMissingError,
384
388
  normalizeTanglePlanTier,
389
+ normalizeTangleSsoEmail,
385
390
  parseAdminEmails,
386
391
  readTangleTierState,
392
+ resolveTangleSsoAccount,
387
393
  resolveUserTangleHubBearer,
388
394
  resolveUserTangleHubBearerForUser,
389
395
  seatCheckoutUrl,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/platform/hub.ts","../../src/platform/billing.ts","../../src/platform/api-key-auth.ts"],"sourcesContent":["/**\n * Integrations-hub proxy routes: the app-side surface that forwards an\n * authenticated user's requests to the platform's `/v1/integrations/*` API\n * using their stored platform key. Auth, key lookup, and the wire client are\n * structural seams (`HubProxyContext`); error detection is by name + shape so\n * it survives bundlers duplicating module instances.\n */\n\nimport {\n resolveTangleDevOrUserKey,\n type TangleExecutionEnvironment,\n type TangleExecutionKeySource,\n} from '../runtime/model'\n\n/** Hub bearer provenance mirrors the execution-key source union. */\nexport type TangleHubBearerSource = TangleExecutionKeySource\n\n/** Represent a resolved bearer token with its associated TangleHub bearer source */\nexport interface ResolvedTangleHubBearer {\n bearer: string\n source: TangleHubBearerSource\n}\n\n/** Resolve options required to obtain a user's TangleHub bearer token including environment and API key retrieval */\nexport interface ResolveUserTangleHubBearerOptions {\n userId: string\n /** Deployment context. Only local development may use env credentials. */\n environment?: TangleExecutionEnvironment\n /** Env to read for the local-development bearer. */\n env?: Record<string, string | undefined>\n /** App-owned lookup for the caller's linked platform API key. */\n getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Resolve options for retrieving a TangleHub bearer token for a specified user */\nexport interface ResolveUserTangleHubBearerForUserOptions<UserId = string> {\n userId: UserId\n environment?: TangleExecutionEnvironment\n env?: Record<string, string | undefined>\n getUserApiKey: (userId: UserId) => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Represent missing Tangle platform link error for a specified user ID */\nexport class TangleBearerMissingError extends Error {\n constructor(readonly userId: string) {\n super(`No Tangle platform link for user ${userId}`)\n this.name = 'TangleBearerMissingError'\n }\n}\n\n/**\n * Resolve the Tangle bearer used by the integration hub proxy.\n *\n * Local development may use a server env key so apps can exercise the hub\n * without completing cross-site SSO. Deployed contexts must use the caller's\n * linked platform key; this keeps integration ownership aligned with the user.\n */\nexport async function resolveUserTangleHubBearer(\n opts: ResolveUserTangleHubBearerOptions,\n): Promise<ResolvedTangleHubBearer> {\n const resolved = await resolveTangleDevOrUserKey({\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: opts.getUserApiKey,\n })\n if (resolved) return { bearer: resolved.apiKey, source: resolved.source }\n\n throw new TangleBearerMissingError(opts.userId)\n}\n\n/** Resolve the TangleHub bearer token for a specified user based on provided options */\nexport async function resolveUserTangleHubBearerForUser<UserId = string>(\n opts: ResolveUserTangleHubBearerForUserOptions<UserId>,\n): Promise<ResolvedTangleHubBearer> {\n return resolveUserTangleHubBearer({\n userId: String(opts.userId),\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: () => opts.getUserApiKey(opts.userId),\n })\n}\n\n/** Structural guard (name + userId shape) — robust when the error class is\n * constructed in a different module instance than the one checking it. */\nexport function isTangleBearerMissingError(error: unknown): error is TangleBearerMissingError {\n return (\n error instanceof Error &&\n error.name === 'TangleBearerMissingError' &&\n typeof (error as { userId?: unknown }).userId === 'string'\n )\n}\n\n/** Structural detection of the platform hub wire error (name + numeric status). */\nexport function isPlatformHubErrorLike(error: unknown): error is Error & { status: number; code?: string } {\n return (\n error instanceof Error &&\n error.name === 'PlatformHubError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Structural subset of the platform hub wire client — extra methods are fine. */\nexport interface HubClientLike {\n catalog(): Promise<unknown>\n listConnections(): Promise<unknown>\n revokeConnection(connectionId: string): Promise<unknown>\n startAuth(input: {\n providerId: string\n connectorId: string\n returnUrl: string\n requestedScopes?: string[]\n }): Promise<{ authorizationUrl: string; state: string }>\n listHealthchecks(): Promise<unknown>\n}\n\n/** Define methods to require user ID, get bearer token, and create a hub client bound to the bearer */\nexport interface HubProxyContext {\n /** Resolve the authenticated user id. Throw the app's own auth Response /\n * redirect to reject — it propagates untouched. */\n requireUserId(request: Request): Promise<string>\n /** The user's platform bearer; throw `TangleBearerMissingError` when unlinked. */\n getBearer(userId: string): Promise<string>\n /** A hub client bound to the bearer. */\n createHubClient(bearer: string): HubClientLike\n}\n\n/** Define arguments for configuring a proxy route with request and optional parameters */\nexport interface HubProxyRouteArgs {\n request: Request\n params?: Record<string, string | undefined>\n}\n\n/** Define routes for hub proxy handling catalog, connections, healthchecks, and authorization actions */\nexport interface HubProxyRoutes {\n /** GET → `{ catalog }`. */\n catalog(args: HubProxyRouteArgs): Promise<Response>\n /** GET → `{ connections }`. */\n connections(args: HubProxyRouteArgs): Promise<Response>\n /** DELETE → the platform revocation result verbatim; 405 otherwise. */\n connectionDelete(args: { request: Request; params: { connectionId: string } }): Promise<Response>\n /** GET → `{ healthchecks }`. */\n healthchecks(args: HubProxyRouteArgs): Promise<Response>\n /** POST `{ providerId, connectorId, returnUrl, requestedScopes? }` →\n * `{ authorizationUrl, state }`; 405 non-POST; 400 on bad JSON / missing fields. */\n authStart(args: HubProxyRouteArgs): Promise<Response>\n}\n\ninterface StartAuthBody {\n providerId?: string\n connectorId?: string\n returnUrl?: string\n requestedScopes?: string[]\n}\n\n/** Resolve hub proxy routes with authentication and error handling based on the given context */\nexport function createHubProxyRoutes(ctx: HubProxyContext): HubProxyRoutes {\n /** Auth runs OUTSIDE the proxy try/catch so the app's auth throw (redirect\n * Response etc.) is never swallowed; bearer + platform errors are mapped. */\n async function proxy(request: Request, call: (hub: HubClientLike) => Promise<Response>): Promise<Response> {\n const userId = await ctx.requireUserId(request)\n try {\n const bearer = await ctx.getBearer(userId)\n return await call(ctx.createHubClient(bearer))\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n }\n\n return {\n catalog: ({ request }) => proxy(request, async (hub) => Response.json({ catalog: await hub.catalog() })),\n\n connections: ({ request }) =>\n proxy(request, async (hub) => Response.json({ connections: await hub.listConnections() })),\n\n connectionDelete: async ({ request, params }) => {\n if (request.method !== 'DELETE') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n return proxy(request, async (hub) => Response.json(await hub.revokeConnection(params.connectionId)))\n },\n\n healthchecks: ({ request }) =>\n proxy(request, async (hub) => Response.json({ healthchecks: await hub.listHealthchecks() })),\n\n authStart: async ({ request }) => {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const userId = await ctx.requireUserId(request)\n let body: StartAuthBody\n try {\n body = (await request.json()) as StartAuthBody\n } catch {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body.providerId || !body.connectorId || !body.returnUrl) {\n return Response.json({ error: 'providerId, connectorId, and returnUrl are required' }, { status: 400 })\n }\n try {\n const bearer = await ctx.getBearer(userId)\n const result = await ctx.createHubClient(bearer).startAuth({\n providerId: body.providerId,\n connectorId: body.connectorId,\n returnUrl: body.returnUrl,\n requestedScopes: body.requestedScopes,\n })\n return Response.json({ authorizationUrl: result.authorizationUrl, state: result.state })\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n },\n }\n}\n","/**\n * Platform billing HTTP transport + tier state for apps on the shared\n * Tangle balance model (id.tangle.tools). Reads authenticate as the user via\n * their per-user platform key (the platform resolves the caller from the\n * key; service or impersonation headers on read routes are rejected). The\n * deduct write authenticates as the product service (`Bearer <serviceToken>`\n * + `X-Service-Name`) and names the target user in the body. Also provides a\n * fetch-backed implementation of the `/billing` module's\n * `PlatformBillingClient` seam (type-only import — no runtime coupling).\n */\n\nimport type { PlatformBillingClient, PlatformIdentity } from '../billing/index'\nimport { resolveTangleExecutionEnvironment } from '../runtime/model'\n\n/** Define available subscription tiers for the TanglePlan service */\nexport type TanglePlanTier = 'free' | 'pro' | 'enterprise'\n\n/** 'pro' | 'enterprise' pass through; anything else (null, unknown) → 'free'. */\nexport function normalizeTanglePlanTier(plan: string | null | undefined): TanglePlanTier {\n return plan === 'pro' || plan === 'enterprise' ? plan : 'free'\n}\n\n/** Represent platform billing HTTP errors with status code and detailed message */\nexport class PlatformBillingHttpError extends Error {\n constructor(\n readonly status: number,\n detail: string,\n ) {\n super(`Platform request failed (${status}): ${detail}`)\n this.name = 'PlatformBillingHttpError'\n }\n}\n\n/** Structural guard (name + numeric status) — robust across module instances. */\nexport function isPlatformBillingHttpError(error: unknown): error is PlatformBillingHttpError {\n return (\n error instanceof Error &&\n error.name === 'PlatformBillingHttpError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Define HTTP options for platform billing including base URL, service token, product slug, fetch implementation, and timeout */\nexport interface PlatformBillingHttpOptions {\n /** Platform root, e.g. https://id.tangle.tools (trailing slashes stripped). */\n baseUrl: string\n /** Used only by `deduct()`; resolved lazily so reads never require it.\n * Throws at call time when empty. */\n serviceToken: string | (() => string)\n /** Product slug — the `X-Service-Name` header and the deduct `product` field. */\n productSlug: string\n fetchImpl?: typeof fetch\n /** Default 10 000. */\n timeoutMs?: number\n}\n\n/** Describe subscription tier and status information for a platform user */\nexport interface PlatformSubscriptionInfo {\n tier: TanglePlanTier\n status: string | null\n}\n\n/** Describe the platform balance and lifetime spending with an optional update timestamp */\nexport interface PlatformBalanceSnapshot {\n balance: number\n lifetimeSpent: number\n updatedAt?: string\n}\n\n/** Describe a product's usage and spending metrics on the platform */\nexport interface PlatformUsageProductRow {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Lifecycle of a per-product seat subscription, mirroring the Stripe states\n * the platform persists. 'none' = the user has never held this seat. */\nexport type SeatStatus = 'none' | 'active' | 'trialing' | 'past_due' | 'canceled'\n\n/** Price and included shared-wallet credit for one seat billing period. */\nexport interface ProductSeatOfferPeriod {\n priceCents: number\n includedCreditsCents: number\n}\n\n/** Commercial terms returned by the platform's product catalog. Products use\n * this exact object for display instead of duplicating prices in UI copy. */\nexport interface ProductSeatOffer {\n currency: 'usd'\n interval: 'month'\n recurring: ProductSeatOfferPeriod\n introductory: ProductSeatOfferPeriod | null\n}\n\n/**\n * Per-product entitlement snapshot from the platform — the single read that\n * tells a product whether to show its workspace or the seat paywall. Shape\n * matches `GET /v1/billing/product-entitlement?product=<id>`.\n *\n * `hasSeat` is computed platform-side from the raw seat row.\n * It is true for an active or trialing seat whose period has not lapsed.\n * `onFreeTier` remains in the wire shape for compatibility, but this client\n * always returns false and never uses it for access.\n */\nexport interface ProductEntitlement {\n seatStatus: SeatStatus\n /** ISO timestamp the active seat's paid period runs until; null when none. */\n currentPeriodEnd: string | null\n /** Cumulative inference spend across the whole suite, in dollars. */\n lifetimeSpentUsd: number\n hasSeat: boolean\n /** Platform access decision, including funded balance and identity policy. */\n paidAccess?: boolean\n onFreeTier: boolean\n /** Present when the platform exposes catalog-backed commercial terms. */\n offer?: ProductSeatOffer\n}\n\nfunction productSeatOffer(value: unknown): ProductSeatOffer | undefined {\n if (!value || typeof value !== 'object') return undefined\n const candidate = value as Record<string, unknown>\n if (candidate.currency !== 'usd' || candidate.interval !== 'month') return undefined\n\n const period = (input: unknown, allowZeroPrice: boolean): ProductSeatOfferPeriod | undefined => {\n if (!input || typeof input !== 'object') return undefined\n const data = input as Record<string, unknown>\n const priceCents = data.priceCents\n const includedCreditsCents = data.includedCreditsCents\n if (\n typeof priceCents !== 'number' ||\n !Number.isSafeInteger(priceCents) ||\n (allowZeroPrice ? priceCents < 0 : priceCents <= 0) ||\n typeof includedCreditsCents !== 'number' ||\n !Number.isSafeInteger(includedCreditsCents) ||\n includedCreditsCents < 0\n ) {\n return undefined\n }\n return { priceCents, includedCreditsCents }\n }\n\n const recurring = period(candidate.recurring, false)\n const introductory =\n candidate.introductory === null\n ? null\n : period(candidate.introductory, true)\n if (!recurring || introductory === undefined) return undefined\n return {\n currency: 'usd',\n interval: 'month',\n recurring,\n introductory,\n }\n}\n\n/** Define methods to interact with platform billing endpoints using user or service authentication */\nexport interface PlatformBillingHttp {\n /** GET /v1/plans/current (user bearer). */\n getSubscription(userApiKey: string): Promise<PlatformSubscriptionInfo>\n /** GET /v1/billing/balance (user bearer). */\n getBalance(userApiKey: string): Promise<PlatformBalanceSnapshot>\n /** GET /v1/billing/usage (user bearer). */\n getUsageByProduct(userApiKey: string): Promise<PlatformUsageProductRow[]>\n /** GET /v1/billing/product-entitlement?product=<id> (user bearer). */\n getProductEntitlement(userApiKey: string, productId: string): Promise<ProductEntitlement>\n /** POST /v1/billing/deduct (service token). */\n deduct(input: {\n platformUserId: string\n amountUsd: number\n type: string\n description: string\n referenceId: string\n }): Promise<void>\n /** Absolute URL of the platform's billing-management surface. */\n billingUrl(): string\n /** Absolute URL of the catalog-backed seat checkout for `productId`. */\n seatCheckoutUrl(productId: string): string\n}\n\n/** Create a PlatformBillingHttp instance configured with given options and default behaviors */\nexport function createPlatformBillingHttp(opts: PlatformBillingHttpOptions): PlatformBillingHttp {\n const baseUrl = opts.baseUrl.replace(/\\/+$/, '')\n if (!baseUrl) throw new Error('PlatformBillingHttpOptions.baseUrl is required')\n if (!opts.productSlug) throw new Error('PlatformBillingHttpOptions.productSlug is required')\n const fetchImpl = opts.fetchImpl ?? fetch\n const timeoutMs = opts.timeoutMs ?? 10_000\n\n function resolveServiceToken(): string {\n const token = typeof opts.serviceToken === 'function' ? opts.serviceToken() : opts.serviceToken\n if (!token) throw new Error('A platform service token is required for deduct')\n return token\n }\n\n async function request<T>(path: string, init: RequestInit, headers: Headers): Promise<T> {\n const res = await fetchImpl(`${baseUrl}${path}`, {\n ...init,\n headers,\n signal: AbortSignal.timeout(timeoutMs),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { error?: { message?: string } } | null\n throw new PlatformBillingHttpError(res.status, body?.error?.message ?? res.statusText)\n }\n return res.json() as Promise<T>\n }\n\n function userRead<T>(userApiKey: string, path: string): Promise<T> {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${userApiKey}`)\n return request<T>(path, {}, headers)\n }\n\n return {\n async getSubscription(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { subscription?: { plan?: string | null; status?: string | null } | null }\n }>(userApiKey, '/v1/plans/current')\n const sub = body.data?.subscription ?? null\n return { tier: normalizeTanglePlanTier(sub?.plan), status: sub?.status ?? null }\n },\n\n async getBalance(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { balance?: number; lifetimeSpent?: number; updatedAt?: string }\n }>(userApiKey, '/v1/billing/balance')\n return {\n balance: body.data?.balance ?? 0,\n lifetimeSpent: body.data?.lifetimeSpent ?? 0,\n updatedAt: body.data?.updatedAt,\n }\n },\n\n async getUsageByProduct(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: Array<{ product?: string | null; totalSpent?: number; count?: number }>\n }>(userApiKey, '/v1/billing/usage')\n return (body.data ?? []).map((row) => ({\n product: row.product ?? null,\n totalSpent: row.totalSpent ?? 0,\n count: row.count ?? 0,\n }))\n },\n\n async getProductEntitlement(userApiKey, productId) {\n const slug = encodeURIComponent(productId)\n const body = await userRead<{\n success: boolean\n data?: {\n seatStatus?: SeatStatus | null\n currentPeriodEnd?: string | null\n lifetimeSpentUsd?: number | null\n hasSeat?: boolean | null\n paidAccess?: unknown\n onFreeTier?: boolean | null\n offer?: unknown\n }\n }>(userApiKey, `/v1/billing/product-entitlement?product=${slug}`)\n const data = body.data ?? {}\n const hasSeat = data.hasSeat === true\n const offer = productSeatOffer(data.offer)\n return {\n seatStatus: data.seatStatus ?? 'none',\n currentPeriodEnd: data.currentPeriodEnd ?? null,\n lifetimeSpentUsd: data.lifetimeSpentUsd ?? 0,\n hasSeat,\n ...('paidAccess' in data ? { paidAccess: data.paidAccess === true } : {}),\n // Product-funded free access is retired. Ignore stale server signals.\n onFreeTier: false,\n ...(offer ? { offer } : {}),\n }\n },\n\n async deduct(input) {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${resolveServiceToken()}`)\n headers.set('X-Service-Name', opts.productSlug)\n headers.set('Content-Type', 'application/json')\n await request('/v1/billing/deduct', {\n method: 'POST',\n body: JSON.stringify({\n userId: input.platformUserId,\n amount: input.amountUsd,\n type: input.type,\n product: opts.productSlug,\n description: input.description,\n referenceId: input.referenceId,\n }),\n }, headers)\n },\n\n billingUrl() {\n return `${baseUrl}/app/billing`\n },\n\n seatCheckoutUrl(productId) {\n return seatCheckoutUrl(baseUrl, productId)\n },\n }\n}\n\n/**\n * Platform Stripe checkout URL for a product's catalog-backed seat. The\n * platform resolves the product-specific price and introductory offer from the\n * `product` query param. Mirrors the `billingUrl()` shape — a deterministic\n * platform-rooted URL with no client-side pricing decisions.\n */\nexport function seatCheckoutUrl(baseUrl: string, productId: string): string {\n const root = baseUrl.replace(/\\/+$/, '')\n return `${root}/app/billing/seat/checkout?product=${encodeURIComponent(productId)}`\n}\n\n// ── Tier policy + composed state ────────────────────────────────────────────\n\n/** Define policy settings for concurrency and overage allowance in a tangle tier */\nexport interface TangleTierPolicy {\n concurrency: number\n overageAllowed: boolean\n}\n\n/** Define default concurrency and overage policies for each TanglePlanTier level */\nexport const DEFAULT_TANGLE_TIER_POLICY: Record<TanglePlanTier, TangleTierPolicy> = {\n free: { concurrency: 1, overageAllowed: false },\n pro: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n enterprise: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n}\n\n/** Describe the state of a Tangle plan tier including subscription, balance, spending, and concurrency details */\nexport interface TangleTierState {\n tier: TanglePlanTier\n subscriptionStatus: string | null\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * Read subscription + balance and project them onto the tier policy. A\n * null/absent key fails CLOSED (free tier, zero balance) — a billable run is\n * never started against an unknown balance. Platform errors throw; callers\n * on the billable path choose their posture explicitly.\n */\nexport async function readTangleTierState(\n http: PlatformBillingHttp,\n userApiKey: string | null | undefined,\n policy: Record<TanglePlanTier, TangleTierPolicy> = DEFAULT_TANGLE_TIER_POLICY,\n): Promise<TangleTierState> {\n if (!userApiKey) {\n return {\n tier: 'free',\n subscriptionStatus: null,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n ...policy.free,\n }\n }\n const [subscription, balance] = await Promise.all([\n http.getSubscription(userApiKey),\n http.getBalance(userApiKey),\n ])\n return {\n tier: subscription.tier,\n subscriptionStatus: subscription.status,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n ...policy[subscription.tier],\n }\n}\n\n// ── Per-product seat entitlement ────────────────────────────────────────────\n\n/** Product-funded free inference spend is disabled. */\nexport const FREE_TIER_SPEND_CAP_USD = 0\n\n/**\n * Default name of the per-app feature flag that controls seat billing.\n * Explicit values override the environment default.\n */\nexport const DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR = 'SEAT_BILLING_ENABLED'\n\n/** Define options to configure seat billing flag environment variables and override flag name */\nexport interface SeatBillingFlagOptions {\n env?: Record<string, string | undefined>\n /** Override the flag name; default {@link DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR}. */\n flagEnvVar?: string\n}\n\n/**\n * An explicit flag controls seat billing in every environment.\n * Without a flag, known development and test environments default to OFF.\n * Deployed and unknown environments default to ON.\n */\nexport function isSeatBillingEnabled(opts: SeatBillingFlagOptions = {}): boolean {\n const env =\n opts.env ??\n (typeof process !== 'undefined' ? (process.env as Record<string, string | undefined>) : undefined)\n if (!env) return true\n const flag = env[opts.flagEnvVar ?? DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR]?.trim().toLowerCase()\n if (flag) return !['false', '0', 'off', 'disabled', 'no'].includes(flag)\n const environment = resolveTangleExecutionEnvironment(env)\n return environment !== 'development' && environment !== 'test'\n}\n\n/**\n * Read a user's entitlement for one product.\n * An absent key or disabled flag returns no access.\n * Transport failures propagate so the product can return a retryable error.\n *\n * @param flag — pass {@link isSeatBillingEnabled} (or your own boolean) so the\n * product owns when the gate engages. When false, no network call is made.\n */\nexport async function getProductEntitlement(\n http: Pick<PlatformBillingHttp, 'getProductEntitlement'>,\n userApiKey: string | null | undefined,\n productId: string,\n flag = true,\n): Promise<ProductEntitlement> {\n if (!flag) return unavailableEntitlement()\n if (!userApiKey) return unavailableEntitlement()\n return http.getProductEntitlement(userApiKey, productId)\n}\n\nfunction unavailableEntitlement(): ProductEntitlement {\n return {\n seatStatus: 'none',\n currentPeriodEnd: null,\n lifetimeSpentUsd: 0,\n hasSeat: false,\n onFreeTier: false,\n }\n}\n\n/** Honor platform access policy; older endpoints provide only seat authority. */\nexport function isProductEntitled(ent: ProductEntitlement): boolean {\n return ent.paidAccess === undefined ? ent.hasSeat === true : ent.paidAccess === true\n}\n\n// ── Bridge onto the /billing seam ───────────────────────────────────────────\n\n/** Define a contract for resolving platform identities based on user identifiers */\nexport interface PlatformIdentityStore {\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n}\n\n/** Concrete fetch-backed `PlatformBillingClient<TanglePlanTier>` for\n * `createPlatformBalanceManager` (from `/billing`). */\nexport function createTanglePlatformBillingClient(\n http: PlatformBillingHttp,\n identity: PlatformIdentityStore,\n): PlatformBillingClient<TanglePlanTier> {\n return {\n resolveIdentity: (userId) => identity.resolveIdentity(userId),\n getPlan: async (apiKey) => (await http.getSubscription(apiKey)).tier,\n getBalance: async (apiKey) => {\n const snapshot = await http.getBalance(apiKey)\n return { balance: snapshot.balance, lifetimeSpent: snapshot.lifetimeSpent }\n },\n getUsageByProduct: (apiKey) => http.getUsageByProduct(apiKey),\n deduct: (input) => http.deduct(input),\n }\n}\n","/** Verified key fields required to enter a product's private API routes. */\nexport interface RequestApiKey {\n keyId: string\n ownerId: string\n scopes: readonly string[]\n /** Unix epoch milliseconds. Private API credentials must expire. */\n expiresAt: number\n}\n\nexport interface ApiKeyRequestAuthOptions<Key extends RequestApiKey, Identity> {\n /** Verify against the existing key store, including revocation and spending limits. */\n verify(authorization: string): Promise<Key | null>\n /** Return one scope or all required scopes; null or an empty list denies the route. */\n requiredScope(request: Request): string | readonly string[] | null\n /** Load the owner from product storage; retain normal workspace and tenant authorization. */\n resolveIdentity(key: Key): Promise<Identity | null>\n /** Atomically enforce the existing key's request quotas before admitting the request. */\n claimRequest(key: Key, requestId: string): Promise<{ allowed: boolean; retryAfterSeconds?: number }>\n}\n\n/**\n * Adapt existing Bearer keys to private product routes without issuing sessions.\n * Only absent authorization returns null; supplied invalid credentials never\n * fall through to cookie authentication. Callers retain their ordinary RBAC.\n */\nexport function createApiKeyRequestAuth<Key extends RequestApiKey, Identity>(\n options: ApiKeyRequestAuthOptions<Key, Identity>,\n): (request: Request) => Promise<Identity | null> {\n return async (request) => {\n const authorization = request.headers.get('Authorization')\n if (authorization === null) return null\n const bearer = /^Bearer +(\\S+)$/i.exec(authorization)\n if (!bearer) {\n throw denied(401, 'api_key.invalid', 'Invalid API key')\n }\n const requiredScope = options.requiredScope(request)\n const requiredScopes = typeof requiredScope === 'string' ? [requiredScope] : Array.from(requiredScope ?? [])\n if (!requiredScopes?.length || !requiredScopes.every(scope => typeof scope === 'string' && scope.trim())) {\n throw denied(403, 'api_key.route_denied', 'API key access is not enabled for this route')\n }\n const key = await options.verify(`Bearer ${bearer[1]}`)\n if (!key || !key.keyId?.trim() || !key.ownerId?.trim()) {\n throw denied(401, 'api_key.invalid', 'Invalid API key')\n }\n if (!Number.isSafeInteger(key.expiresAt) || key.expiresAt <= Date.now()) {\n throw denied(401, 'api_key.expired', 'An unexpired API key with a finite expiry is required')\n }\n if (!Array.isArray(key.scopes) || !requiredScopes.every(scope => key.scopes.includes(scope))) {\n throw denied(403, 'api_key.insufficient_scope', `API key requires scopes: ${requiredScopes.join(', ')}`)\n }\n const identity = await options.resolveIdentity(key)\n if (!identity) throw denied(401, 'api_key.invalid_owner', 'API key owner is unavailable')\n const claim = await options.claimRequest(key, crypto.randomUUID())\n if (claim.allowed !== true) {\n const headers = new Headers()\n if (Number.isFinite(claim.retryAfterSeconds) && claim.retryAfterSeconds! > 0) {\n headers.set('Retry-After', String(Math.ceil(claim.retryAfterSeconds!)))\n }\n throw denied(429, 'api_key.request_limit_exceeded', 'API key request limit exceeded', headers)\n }\n if (key.expiresAt <= Date.now()) {\n throw denied(401, 'api_key.expired', 'API key expired during authorization')\n }\n return identity\n }\n}\n\nfunction denied(status: number, code: string, error: string, headers?: Headers): Response {\n return Response.json({ error, code }, { status, headers })\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/platform/hub.ts","../../src/platform/billing.ts","../../src/platform/api-key-auth.ts"],"sourcesContent":["/**\n * Integrations-hub proxy routes: the app-side surface that forwards an\n * authenticated user's requests to the platform's `/v1/integrations/*` API\n * using their stored platform key. Auth, key lookup, and the wire client are\n * structural seams (`HubProxyContext`); error detection is by name + shape so\n * it survives bundlers duplicating module instances.\n */\n\nimport {\n resolveTangleDevOrUserKey,\n type TangleExecutionEnvironment,\n type TangleExecutionKeySource,\n} from '../runtime/model'\n\n/** Hub bearer provenance mirrors the execution-key source union. */\nexport type TangleHubBearerSource = TangleExecutionKeySource\n\n/** Represent a resolved bearer token with its associated TangleHub bearer source */\nexport interface ResolvedTangleHubBearer {\n bearer: string\n source: TangleHubBearerSource\n}\n\n/** Resolve options required to obtain a user's TangleHub bearer token including environment and API key retrieval */\nexport interface ResolveUserTangleHubBearerOptions {\n userId: string\n /** Deployment context. Only local development may use env credentials. */\n environment?: TangleExecutionEnvironment\n /** Env to read for the local-development bearer. */\n env?: Record<string, string | undefined>\n /** App-owned lookup for the caller's linked platform API key. */\n getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Resolve options for retrieving a TangleHub bearer token for a specified user */\nexport interface ResolveUserTangleHubBearerForUserOptions<UserId = string> {\n userId: UserId\n environment?: TangleExecutionEnvironment\n env?: Record<string, string | undefined>\n getUserApiKey: (userId: UserId) => string | null | undefined | Promise<string | null | undefined>\n}\n\n/** Represent missing Tangle platform link error for a specified user ID */\nexport class TangleBearerMissingError extends Error {\n constructor(readonly userId: string) {\n super(`No Tangle platform link for user ${userId}`)\n this.name = 'TangleBearerMissingError'\n }\n}\n\n/**\n * Resolve the Tangle bearer used by the integration hub proxy.\n *\n * Local development may use a server env key so apps can exercise the hub\n * without completing cross-site SSO. Deployed contexts must use the caller's\n * linked platform key; this keeps integration ownership aligned with the user.\n */\nexport async function resolveUserTangleHubBearer(\n opts: ResolveUserTangleHubBearerOptions,\n): Promise<ResolvedTangleHubBearer> {\n const resolved = await resolveTangleDevOrUserKey({\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: opts.getUserApiKey,\n })\n if (resolved) return { bearer: resolved.apiKey, source: resolved.source }\n\n throw new TangleBearerMissingError(opts.userId)\n}\n\n/** Resolve the TangleHub bearer token for a specified user based on provided options */\nexport async function resolveUserTangleHubBearerForUser<UserId = string>(\n opts: ResolveUserTangleHubBearerForUserOptions<UserId>,\n): Promise<ResolvedTangleHubBearer> {\n return resolveUserTangleHubBearer({\n userId: String(opts.userId),\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: () => opts.getUserApiKey(opts.userId),\n })\n}\n\n/** Structural guard (name + userId shape) — robust when the error class is\n * constructed in a different module instance than the one checking it. */\nexport function isTangleBearerMissingError(error: unknown): error is TangleBearerMissingError {\n return (\n error instanceof Error &&\n error.name === 'TangleBearerMissingError' &&\n typeof (error as { userId?: unknown }).userId === 'string'\n )\n}\n\n/** Structural detection of the platform hub wire error (name + numeric status). */\nexport function isPlatformHubErrorLike(error: unknown): error is Error & { status: number; code?: string } {\n return (\n error instanceof Error &&\n error.name === 'PlatformHubError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Structural subset of the platform hub wire client — extra methods are fine. */\nexport interface HubClientLike {\n catalog(): Promise<unknown>\n listConnections(): Promise<unknown>\n revokeConnection(connectionId: string): Promise<unknown>\n startAuth(input: {\n providerId: string\n connectorId: string\n returnUrl: string\n requestedScopes?: string[]\n }): Promise<{ authorizationUrl: string; state: string }>\n listHealthchecks(): Promise<unknown>\n}\n\n/** Define methods to require user ID, get bearer token, and create a hub client bound to the bearer */\nexport interface HubProxyContext {\n /** Resolve the authenticated user id. Throw the app's own auth Response /\n * redirect to reject — it propagates untouched. */\n requireUserId(request: Request): Promise<string>\n /** The user's platform bearer; throw `TangleBearerMissingError` when unlinked. */\n getBearer(userId: string): Promise<string>\n /** A hub client bound to the bearer. */\n createHubClient(bearer: string): HubClientLike\n}\n\n/** Define arguments for configuring a proxy route with request and optional parameters */\nexport interface HubProxyRouteArgs {\n request: Request\n params?: Record<string, string | undefined>\n}\n\n/** Define routes for hub proxy handling catalog, connections, healthchecks, and authorization actions */\nexport interface HubProxyRoutes {\n /** GET → `{ catalog }`. */\n catalog(args: HubProxyRouteArgs): Promise<Response>\n /** GET → `{ connections }`. */\n connections(args: HubProxyRouteArgs): Promise<Response>\n /** DELETE → the platform revocation result verbatim; 405 otherwise. */\n connectionDelete(args: { request: Request; params: { connectionId: string } }): Promise<Response>\n /** GET → `{ healthchecks }`. */\n healthchecks(args: HubProxyRouteArgs): Promise<Response>\n /** POST `{ providerId, connectorId, returnUrl, requestedScopes? }` →\n * `{ authorizationUrl, state }`; 405 non-POST; 400 on bad JSON / missing fields. */\n authStart(args: HubProxyRouteArgs): Promise<Response>\n}\n\ninterface StartAuthBody {\n providerId?: string\n connectorId?: string\n returnUrl?: string\n requestedScopes?: string[]\n}\n\n/** Resolve hub proxy routes with authentication and error handling based on the given context */\nexport function createHubProxyRoutes(ctx: HubProxyContext): HubProxyRoutes {\n /** Auth runs OUTSIDE the proxy try/catch so the app's auth throw (redirect\n * Response etc.) is never swallowed; bearer + platform errors are mapped. */\n async function proxy(request: Request, call: (hub: HubClientLike) => Promise<Response>): Promise<Response> {\n const userId = await ctx.requireUserId(request)\n try {\n const bearer = await ctx.getBearer(userId)\n return await call(ctx.createHubClient(bearer))\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n }\n\n return {\n catalog: ({ request }) => proxy(request, async (hub) => Response.json({ catalog: await hub.catalog() })),\n\n connections: ({ request }) =>\n proxy(request, async (hub) => Response.json({ connections: await hub.listConnections() })),\n\n connectionDelete: async ({ request, params }) => {\n if (request.method !== 'DELETE') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n return proxy(request, async (hub) => Response.json(await hub.revokeConnection(params.connectionId)))\n },\n\n healthchecks: ({ request }) =>\n proxy(request, async (hub) => Response.json({ healthchecks: await hub.listHealthchecks() })),\n\n authStart: async ({ request }) => {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const userId = await ctx.requireUserId(request)\n let body: StartAuthBody\n try {\n body = (await request.json()) as StartAuthBody\n } catch {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n if (!body.providerId || !body.connectorId || !body.returnUrl) {\n return Response.json({ error: 'providerId, connectorId, and returnUrl are required' }, { status: 400 })\n }\n try {\n const bearer = await ctx.getBearer(userId)\n const result = await ctx.createHubClient(bearer).startAuth({\n providerId: body.providerId,\n connectorId: body.connectorId,\n returnUrl: body.returnUrl,\n requestedScopes: body.requestedScopes,\n })\n return Response.json({ authorizationUrl: result.authorizationUrl, state: result.state })\n } catch (err) {\n if (isTangleBearerMissingError(err)) {\n return Response.json({ error: 'tangle_link_required' }, { status: 412 })\n }\n if (isPlatformHubErrorLike(err)) {\n return Response.json({ error: err.message, code: err.code }, { status: err.status })\n }\n throw err\n }\n },\n }\n}\n","/**\n * Platform billing HTTP transport + tier state for apps on the shared\n * Tangle balance model (id.tangle.tools). Reads authenticate as the user via\n * their per-user platform key (the platform resolves the caller from the\n * key; service or impersonation headers on read routes are rejected). The\n * deduct write authenticates as the product service (`Bearer <serviceToken>`\n * + `X-Service-Name`) and names the target user in the body. Also provides a\n * fetch-backed implementation of the `/billing` module's\n * `PlatformBillingClient` seam (type-only import — no runtime coupling).\n */\n\nimport type { PlatformBillingClient, PlatformIdentity } from '../billing/index'\nimport { resolveTangleExecutionEnvironment } from '../runtime/model'\n\n/** Define available subscription tiers for the TanglePlan service */\nexport type TanglePlanTier = 'free' | 'pro' | 'enterprise'\n\n/** 'pro' | 'enterprise' pass through; anything else (null, unknown) → 'free'. */\nexport function normalizeTanglePlanTier(plan: string | null | undefined): TanglePlanTier {\n return plan === 'pro' || plan === 'enterprise' ? plan : 'free'\n}\n\n/** Represent platform billing HTTP errors with status code and detailed message */\nexport class PlatformBillingHttpError extends Error {\n constructor(\n readonly status: number,\n detail: string,\n ) {\n super(`Platform request failed (${status}): ${detail}`)\n this.name = 'PlatformBillingHttpError'\n }\n}\n\n/** Structural guard (name + numeric status) — robust across module instances. */\nexport function isPlatformBillingHttpError(error: unknown): error is PlatformBillingHttpError {\n return (\n error instanceof Error &&\n error.name === 'PlatformBillingHttpError' &&\n typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\n/** Define HTTP options for platform billing including base URL, service token, product slug, fetch implementation, and timeout */\nexport interface PlatformBillingHttpOptions {\n /** Platform root, e.g. https://id.tangle.tools (trailing slashes stripped). */\n baseUrl: string\n /** Used only by `deduct()`; resolved lazily so reads never require it.\n * Throws at call time when empty. */\n serviceToken: string | (() => string)\n /** Product slug — the `X-Service-Name` header and the deduct `product` field. */\n productSlug: string\n fetchImpl?: typeof fetch\n /** Default 10 000. */\n timeoutMs?: number\n}\n\n/** Describe subscription tier and status information for a platform user */\nexport interface PlatformSubscriptionInfo {\n tier: TanglePlanTier\n status: string | null\n}\n\n/** Describe the platform balance and lifetime spending with an optional update timestamp */\nexport interface PlatformBalanceSnapshot {\n balance: number\n lifetimeSpent: number\n updatedAt?: string\n}\n\n/** Describe a product's usage and spending metrics on the platform */\nexport interface PlatformUsageProductRow {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Lifecycle of a per-product seat subscription, mirroring the Stripe states\n * the platform persists. 'none' = the user has never held this seat. */\nexport type SeatStatus = 'none' | 'active' | 'trialing' | 'past_due' | 'canceled'\n\n/** Price and included shared-wallet credit for one seat billing period. */\nexport interface ProductSeatOfferPeriod {\n priceCents: number\n includedCreditsCents: number\n}\n\n/** Commercial terms returned by the platform's product catalog. Products use\n * this exact object for display instead of duplicating prices in UI copy. */\nexport interface ProductSeatOffer {\n currency: 'usd'\n interval: 'month'\n recurring: ProductSeatOfferPeriod\n introductory: ProductSeatOfferPeriod | null\n}\n\n/**\n * Per-product entitlement snapshot from the platform — the single read that\n * tells a product whether to show its workspace or the seat paywall. Shape\n * matches `GET /v1/billing/product-entitlement?product=<id>`.\n *\n * `hasSeat` is computed platform-side from the raw seat row.\n * It is true for an active or trialing seat whose period has not lapsed.\n * `onFreeTier` remains in the wire shape for compatibility, but this client\n * always returns false and never uses it for access.\n */\nexport interface ProductEntitlement {\n seatStatus: SeatStatus\n /** ISO timestamp the active seat's paid period runs until; null when none. */\n currentPeriodEnd: string | null\n /** Cumulative inference spend across the whole suite, in dollars. */\n lifetimeSpentUsd: number\n hasSeat: boolean\n /** Platform access decision, including funded balance and identity policy. */\n paidAccess?: boolean\n onFreeTier: boolean\n /** Present when the platform exposes catalog-backed commercial terms. */\n offer?: ProductSeatOffer\n}\n\nfunction productSeatOffer(value: unknown): ProductSeatOffer | undefined {\n if (!value || typeof value !== 'object') return undefined\n const candidate = value as Record<string, unknown>\n if (candidate.currency !== 'usd' || candidate.interval !== 'month') return undefined\n\n const period = (input: unknown, allowZeroPrice: boolean): ProductSeatOfferPeriod | undefined => {\n if (!input || typeof input !== 'object') return undefined\n const data = input as Record<string, unknown>\n const priceCents = data.priceCents\n const includedCreditsCents = data.includedCreditsCents\n if (\n typeof priceCents !== 'number' ||\n !Number.isSafeInteger(priceCents) ||\n (allowZeroPrice ? priceCents < 0 : priceCents <= 0) ||\n typeof includedCreditsCents !== 'number' ||\n !Number.isSafeInteger(includedCreditsCents) ||\n includedCreditsCents < 0\n ) {\n return undefined\n }\n return { priceCents, includedCreditsCents }\n }\n\n const recurring = period(candidate.recurring, false)\n const introductory =\n candidate.introductory === null\n ? null\n : period(candidate.introductory, true)\n if (!recurring || introductory === undefined) return undefined\n return {\n currency: 'usd',\n interval: 'month',\n recurring,\n introductory,\n }\n}\n\n/** Define methods to interact with platform billing endpoints using user or service authentication */\nexport interface PlatformBillingHttp {\n /** GET /v1/plans/current (user bearer). */\n getSubscription(userApiKey: string): Promise<PlatformSubscriptionInfo>\n /** GET /v1/billing/balance (user bearer). */\n getBalance(userApiKey: string): Promise<PlatformBalanceSnapshot>\n /** GET /v1/billing/usage (user bearer). */\n getUsageByProduct(userApiKey: string): Promise<PlatformUsageProductRow[]>\n /** GET /v1/billing/product-entitlement?product=<id> (user bearer). */\n getProductEntitlement(userApiKey: string, productId: string): Promise<ProductEntitlement>\n /** POST /v1/billing/deduct (service token). */\n deduct(input: {\n platformUserId: string\n amountUsd: number\n type: string\n description: string\n referenceId: string\n }): Promise<void>\n /** Absolute URL of the platform's billing-management surface. */\n billingUrl(): string\n /** Absolute URL of the catalog-backed seat checkout for `productId`. */\n seatCheckoutUrl(productId: string): string\n}\n\n/** Create a PlatformBillingHttp instance configured with given options and default behaviors */\nexport function createPlatformBillingHttp(opts: PlatformBillingHttpOptions): PlatformBillingHttp {\n const baseUrl = opts.baseUrl.replace(/\\/+$/, '')\n if (!baseUrl) throw new Error('PlatformBillingHttpOptions.baseUrl is required')\n if (!opts.productSlug) throw new Error('PlatformBillingHttpOptions.productSlug is required')\n const fetchImpl = opts.fetchImpl ?? fetch\n const timeoutMs = opts.timeoutMs ?? 10_000\n\n function resolveServiceToken(): string {\n const token = typeof opts.serviceToken === 'function' ? opts.serviceToken() : opts.serviceToken\n if (!token) throw new Error('A platform service token is required for deduct')\n return token\n }\n\n async function request<T>(path: string, init: RequestInit, headers: Headers): Promise<T> {\n const res = await fetchImpl(`${baseUrl}${path}`, {\n ...init,\n headers,\n signal: AbortSignal.timeout(timeoutMs),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { error?: { message?: string } } | null\n throw new PlatformBillingHttpError(res.status, body?.error?.message ?? res.statusText)\n }\n return res.json() as Promise<T>\n }\n\n function userRead<T>(userApiKey: string, path: string): Promise<T> {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${userApiKey}`)\n return request<T>(path, {}, headers)\n }\n\n return {\n async getSubscription(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { subscription?: { plan?: string | null; status?: string | null } | null }\n }>(userApiKey, '/v1/plans/current')\n const sub = body.data?.subscription ?? null\n return { tier: normalizeTanglePlanTier(sub?.plan), status: sub?.status ?? null }\n },\n\n async getBalance(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: { balance?: number; lifetimeSpent?: number; updatedAt?: string }\n }>(userApiKey, '/v1/billing/balance')\n return {\n balance: body.data?.balance ?? 0,\n lifetimeSpent: body.data?.lifetimeSpent ?? 0,\n updatedAt: body.data?.updatedAt,\n }\n },\n\n async getUsageByProduct(userApiKey) {\n const body = await userRead<{\n success: boolean\n data?: Array<{ product?: string | null; totalSpent?: number; count?: number }>\n }>(userApiKey, '/v1/billing/usage')\n return (body.data ?? []).map((row) => ({\n product: row.product ?? null,\n totalSpent: row.totalSpent ?? 0,\n count: row.count ?? 0,\n }))\n },\n\n async getProductEntitlement(userApiKey, productId) {\n const slug = encodeURIComponent(productId)\n const body = await userRead<{\n success: boolean\n data?: {\n seatStatus?: SeatStatus | null\n currentPeriodEnd?: string | null\n lifetimeSpentUsd?: number | null\n hasSeat?: boolean | null\n paidAccess?: unknown\n onFreeTier?: boolean | null\n offer?: unknown\n }\n }>(userApiKey, `/v1/billing/product-entitlement?product=${slug}`)\n const data = body.data ?? {}\n const hasSeat = data.hasSeat === true\n const offer = productSeatOffer(data.offer)\n return {\n seatStatus: data.seatStatus ?? 'none',\n currentPeriodEnd: data.currentPeriodEnd ?? null,\n lifetimeSpentUsd: data.lifetimeSpentUsd ?? 0,\n hasSeat,\n ...('paidAccess' in data ? { paidAccess: data.paidAccess === true } : {}),\n // Product-funded free access is retired. Ignore stale server signals.\n onFreeTier: false,\n ...(offer ? { offer } : {}),\n }\n },\n\n async deduct(input) {\n const headers = new Headers()\n headers.set('Authorization', `Bearer ${resolveServiceToken()}`)\n headers.set('X-Service-Name', opts.productSlug)\n headers.set('Content-Type', 'application/json')\n await request('/v1/billing/deduct', {\n method: 'POST',\n body: JSON.stringify({\n userId: input.platformUserId,\n amount: input.amountUsd,\n type: input.type,\n product: opts.productSlug,\n description: input.description,\n referenceId: input.referenceId,\n }),\n }, headers)\n },\n\n billingUrl() {\n return `${baseUrl}/app/billing`\n },\n\n seatCheckoutUrl(productId) {\n return seatCheckoutUrl(baseUrl, productId)\n },\n }\n}\n\n/**\n * Platform Stripe checkout URL for a product's catalog-backed seat. The\n * platform resolves the product-specific price and introductory offer from the\n * `product` query param. Mirrors the `billingUrl()` shape — a deterministic\n * platform-rooted URL with no client-side pricing decisions.\n */\nexport function seatCheckoutUrl(baseUrl: string, productId: string): string {\n const root = baseUrl.replace(/\\/+$/, '')\n return `${root}/app/billing/seat/checkout?product=${encodeURIComponent(productId)}`\n}\n\n// ── Tier policy + composed state ────────────────────────────────────────────\n\n/** Define policy settings for concurrency and overage allowance in a tangle tier */\nexport interface TangleTierPolicy {\n concurrency: number\n overageAllowed: boolean\n}\n\n/** Define default concurrency and overage policies for each TanglePlanTier level */\nexport const DEFAULT_TANGLE_TIER_POLICY: Record<TanglePlanTier, TangleTierPolicy> = {\n free: { concurrency: 1, overageAllowed: false },\n pro: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n enterprise: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },\n}\n\n/** Describe the state of a Tangle plan tier including subscription, balance, spending, and concurrency details */\nexport interface TangleTierState {\n tier: TanglePlanTier\n subscriptionStatus: string | null\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * Read subscription + balance and project them onto the tier policy. A\n * null/absent key fails CLOSED (free tier, zero balance) — a billable run is\n * never started against an unknown balance. Platform errors throw; callers\n * on the billable path choose their posture explicitly.\n */\nexport async function readTangleTierState(\n http: PlatformBillingHttp,\n userApiKey: string | null | undefined,\n policy: Record<TanglePlanTier, TangleTierPolicy> = DEFAULT_TANGLE_TIER_POLICY,\n): Promise<TangleTierState> {\n if (!userApiKey) {\n return {\n tier: 'free',\n subscriptionStatus: null,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n ...policy.free,\n }\n }\n const [subscription, balance] = await Promise.all([\n http.getSubscription(userApiKey),\n http.getBalance(userApiKey),\n ])\n return {\n tier: subscription.tier,\n subscriptionStatus: subscription.status,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n ...policy[subscription.tier],\n }\n}\n\n// ── Per-product seat entitlement ────────────────────────────────────────────\n\n/** Product-funded free inference spend is disabled. */\nexport const FREE_TIER_SPEND_CAP_USD = 0\n\n/**\n * Default name of the per-app feature flag that controls seat billing.\n * Explicit values override the environment default.\n */\nexport const DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR = 'SEAT_BILLING_ENABLED'\n\n/** Define options to configure seat billing flag environment variables and override flag name */\nexport interface SeatBillingFlagOptions {\n env?: Record<string, string | undefined>\n /** Override the flag name; default {@link DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR}. */\n flagEnvVar?: string\n}\n\n/**\n * An explicit flag controls seat billing in every environment.\n * Without a flag, known development and test environments default to OFF.\n * Deployed and unknown environments default to ON.\n */\nexport function isSeatBillingEnabled(opts: SeatBillingFlagOptions = {}): boolean {\n const env =\n opts.env ??\n (typeof process !== 'undefined' ? (process.env as Record<string, string | undefined>) : undefined)\n if (!env) return true\n const flag = env[opts.flagEnvVar ?? DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR]?.trim().toLowerCase()\n if (flag) return !['false', '0', 'off', 'disabled', 'no'].includes(flag)\n const environment = resolveTangleExecutionEnvironment(env)\n return environment !== 'development' && environment !== 'test'\n}\n\n/**\n * Read a user's entitlement for one product.\n * An absent key or disabled flag returns no access.\n * Transport failures propagate so the product can return a retryable error.\n *\n * @param flag — pass {@link isSeatBillingEnabled} (or your own boolean) so the\n * product owns when the gate engages. When false, no network call is made.\n */\nexport async function getProductEntitlement(\n http: Pick<PlatformBillingHttp, 'getProductEntitlement'>,\n userApiKey: string | null | undefined,\n productId: string,\n flag = true,\n): Promise<ProductEntitlement> {\n if (!flag) return unavailableEntitlement()\n if (!userApiKey) return unavailableEntitlement()\n return http.getProductEntitlement(userApiKey, productId)\n}\n\nfunction unavailableEntitlement(): ProductEntitlement {\n return {\n seatStatus: 'none',\n currentPeriodEnd: null,\n lifetimeSpentUsd: 0,\n hasSeat: false,\n onFreeTier: false,\n }\n}\n\n/** Honor platform access policy; older endpoints provide only seat authority. */\nexport function isProductEntitled(ent: ProductEntitlement): boolean {\n return ent.paidAccess === undefined ? ent.hasSeat === true : ent.paidAccess === true\n}\n\n// ── Bridge onto the /billing seam ───────────────────────────────────────────\n\n/** Define a contract for resolving platform identities based on user identifiers */\nexport interface PlatformIdentityStore {\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n}\n\n/** Concrete fetch-backed `PlatformBillingClient<TanglePlanTier>` for\n * `createPlatformBalanceManager` (from `/billing`). */\nexport function createTanglePlatformBillingClient(\n http: PlatformBillingHttp,\n identity: PlatformIdentityStore,\n): PlatformBillingClient<TanglePlanTier> {\n return {\n resolveIdentity: (userId) => identity.resolveIdentity(userId),\n getPlan: async (apiKey) => (await http.getSubscription(apiKey)).tier,\n getBalance: async (apiKey) => {\n const snapshot = await http.getBalance(apiKey)\n return { balance: snapshot.balance, lifetimeSpent: snapshot.lifetimeSpent }\n },\n getUsageByProduct: (apiKey) => http.getUsageByProduct(apiKey),\n deduct: (input) => http.deduct(input),\n }\n}\n","/** Verified key fields required to enter a product's private API routes. */\nexport interface RequestApiKey {\n keyId: string\n ownerId: string\n scopes: readonly string[]\n /** Unix epoch milliseconds. Private API credentials must expire. */\n expiresAt: number\n}\n\nexport interface ApiKeyRequestAuthOptions<Key extends RequestApiKey, Identity> {\n /** Verify against the existing key store, including revocation and spending limits. */\n verify(authorization: string): Promise<Key | null>\n /** Return one scope or all required scopes; null or an empty list denies the route. */\n requiredScope(request: Request): string | readonly string[] | null\n /** Load the owner from product storage; retain normal workspace and tenant authorization. */\n resolveIdentity(key: Key): Promise<Identity | null>\n /** Atomically enforce the existing key's request quotas before admitting the request. */\n claimRequest(key: Key, requestId: string): Promise<{ allowed: boolean; retryAfterSeconds?: number }>\n}\n\n/**\n * Adapt existing Bearer keys to private product routes without issuing sessions.\n * Only absent authorization returns null; supplied invalid credentials never\n * fall through to cookie authentication. Callers retain their ordinary RBAC.\n */\nexport function createApiKeyRequestAuth<Key extends RequestApiKey, Identity>(\n options: ApiKeyRequestAuthOptions<Key, Identity>,\n): (request: Request) => Promise<Identity | null> {\n return async (request) => {\n const authorization = request.headers.get('Authorization')\n if (authorization === null) return null\n const bearer = /^Bearer +(\\S+)$/i.exec(authorization)\n if (!bearer) {\n throw denied(401, 'api_key.invalid', 'Invalid API key')\n }\n const requiredScope = options.requiredScope(request)\n const requiredScopes = typeof requiredScope === 'string' ? [requiredScope] : Array.from(requiredScope ?? [])\n if (!requiredScopes?.length || !requiredScopes.every(scope => typeof scope === 'string' && scope.trim())) {\n throw denied(403, 'api_key.route_denied', 'API key access is not enabled for this route')\n }\n const key = await options.verify(`Bearer ${bearer[1]}`)\n if (!key || !key.keyId?.trim() || !key.ownerId?.trim()) {\n throw denied(401, 'api_key.invalid', 'Invalid API key')\n }\n if (!Number.isSafeInteger(key.expiresAt) || key.expiresAt <= Date.now()) {\n throw denied(401, 'api_key.expired', 'An unexpired API key with a finite expiry is required')\n }\n if (!Array.isArray(key.scopes) || !requiredScopes.every(scope => key.scopes.includes(scope))) {\n throw denied(403, 'api_key.insufficient_scope', `API key requires scopes: ${requiredScopes.join(', ')}`)\n }\n const identity = await options.resolveIdentity(key)\n if (!identity) throw denied(401, 'api_key.invalid_owner', 'API key owner is unavailable')\n const claim = await options.claimRequest(key, crypto.randomUUID())\n if (claim.allowed !== true) {\n const headers = new Headers()\n if (Number.isFinite(claim.retryAfterSeconds) && claim.retryAfterSeconds! > 0) {\n headers.set('Retry-After', String(Math.ceil(claim.retryAfterSeconds!)))\n }\n throw denied(429, 'api_key.request_limit_exceeded', 'API key request limit exceeded', headers)\n }\n if (key.expiresAt <= Date.now()) {\n throw denied(401, 'api_key.expired', 'API key expired during authorization')\n }\n return identity\n }\n}\n\nfunction denied(status: number, code: string, error: string, headers?: Headers): Response {\n return Response.json({ error, code }, { status, headers })\n}\n"],"mappings":"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","names":[]}
@@ -23,6 +23,8 @@ export declare function verifySignedSsoState(state: string, config: SsoStateConf
23
23
  /** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */
24
24
  export interface TangleSsoExchangeResult {
25
25
  apiKey: string;
26
+ /** The platform's canonical verified-email proof. */
27
+ emailVerified: boolean;
26
28
  user: {
27
29
  id: string;
28
30
  email: string;
@@ -41,6 +43,54 @@ export interface TangleSsoAuthClient {
41
43
  }): string;
42
44
  exchange(code: string): Promise<TangleSsoExchangeResult>;
43
45
  }
46
+ /** Local account shape required by the verified Tangle SSO account policy.
47
+ * Consumers map their user/link tables into this shape before resolving an
48
+ * exchange. */
49
+ export interface TangleSsoLocalAccount {
50
+ userId: string;
51
+ email: string;
52
+ emailVerified: boolean;
53
+ platformUserId: string | null;
54
+ }
55
+ /** Reasons a local account lookup must stop instead of linking an exchange. */
56
+ export type TangleSsoAccountConflictReason = 'ambiguous-platform-id' | 'platform-id-email-conflict' | 'unverified-email' | 'email-platform-id-conflict' | 'ambiguous-email' | 'invalid-platform-id' | 'invalid-email' | 'platform-match-mismatch' | 'email-match-mismatch';
57
+ /** Result of applying the verified Tangle SSO account matching policy. */
58
+ export type TangleSsoAccountResolution = {
59
+ kind: 'existing';
60
+ userId: string;
61
+ matchedBy: 'platform-id' | 'verified-email';
62
+ } | {
63
+ kind: 'create';
64
+ } | {
65
+ kind: 'reject';
66
+ reason: TangleSsoAccountConflictReason;
67
+ };
68
+ /** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the
69
+ * platform's stable identity. */
70
+ export interface TangleSsoAccountResolutionInput {
71
+ email: string;
72
+ platformMatches: readonly TangleSsoLocalAccount[];
73
+ emailMatches: readonly TangleSsoLocalAccount[];
74
+ platformUserId: string;
75
+ }
76
+ /** Normalize an email for comparisons and persistence queries. */
77
+ export declare function normalizeTangleSsoEmail(email: string): string;
78
+ /**
79
+ * Resolve a platform exchange against all local rows found by stable platform
80
+ * identity and normalized email.
81
+ *
82
+ * A stable platform id takes precedence over email. Every ambiguous or
83
+ * contradictory result rejects, and email fallback requires a verified local
84
+ * email that is not already bound to another platform id. The resolver is
85
+ * pure so each consumer can query its own schema, including schemas without
86
+ * unique indexes, then rerun it after an insert race.
87
+ */
88
+ export declare function resolveTangleSsoAccount(input: TangleSsoAccountResolutionInput): TangleSsoAccountResolution;
89
+ /** Thrown by an account store when the verified SSO policy rejects a link. */
90
+ export declare class TangleSsoAccountConflictError extends Error {
91
+ readonly reason: TangleSsoAccountConflictReason;
92
+ constructor(reason: TangleSsoAccountConflictReason, message?: string);
93
+ }
44
94
  /** Thrown by `upsertUserByEmail` when the app-local user row cannot be
45
95
  * created; the callback handler maps it to `?error=tangle_user_create_failed`.
46
96
  * Any other store error propagates. */
@@ -55,14 +105,26 @@ export declare class TangleSsoUserCreateError extends Error {
55
105
  * the token is always available to `saveTangleLink`.
56
106
  */
57
107
  export interface TangleSsoAccountStore {
58
- /** Find-or-create the app-local user. `tangleUserId` is the platform's
59
- * stable user id — match on it first when the app stores it (emails are
60
- * mutable on the platform; the id is not), falling back to email for
61
- * first-time logins. */
108
+ /** Resolve the local account before any user, session, or link write.
109
+ * Implementations must query every candidate by stable platform id and
110
+ * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent
111
+ * fail-closed policy). The callback rejects `kind: 'reject'` results before
112
+ * it calls `upsertUserByEmail`. */
113
+ resolveAccount(input: {
114
+ email: string;
115
+ platformUserId: string;
116
+ }): Promise<TangleSsoAccountResolution>;
117
+ /** Find-or-create the app-local user selected by `resolveAccount`.
118
+ * `tangleUserId` is the platform's stable user id — match on it first when
119
+ * the app stores it (emails are mutable on the platform; the id is not),
120
+ * falling back to email for first-time logins. */
62
121
  upsertUserByEmail(input: {
63
122
  email: string;
64
123
  name: string | null;
65
124
  tangleUserId: string;
125
+ resolution: Exclude<TangleSsoAccountResolution, {
126
+ kind: 'reject';
127
+ }>;
66
128
  }): Promise<{
67
129
  userId: string;
68
130
  }>;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChatTurnRoutes,
3
3
  streamChatRouteAsSandboxEvents
4
- } from "../chunk-3GVFJYLO.js";
4
+ } from "../chunk-42NBWR4U.js";
5
5
  import "../chunk-ZSSCVT6H.js";
6
6
  import "../chunk-3YUI7WNW.js";
7
7
  import "../chunk-4PZE7XAM.js";
@@ -12,7 +12,7 @@ import "../chunk-JEZJ6HTF.js";
12
12
  import "../chunk-ZMMIQOFI.js";
13
13
  import "../chunk-M3K2HVQD.js";
14
14
  import "../chunk-YJMCRXQQ.js";
15
- import "../chunk-ES3I3UGP.js";
15
+ import "../chunk-N4RR4WAI.js";
16
16
 
17
17
  // src/public-consultation/index.ts
18
18
  import { searchKnowledge } from "@tangle-network/agent-knowledge";
@@ -99,7 +99,7 @@ import {
99
99
  workspaceSandboxRecoveryMessage,
100
100
  workspaceSandboxRecoveryRecommendedActions,
101
101
  writeProfileFilesToBox
102
- } from "../chunk-XR5ZXMYL.js";
102
+ } from "../chunk-PMAQSFJS.js";
103
103
  import "../chunk-MCYJON3F.js";
104
104
  import "../chunk-LWSJK546.js";
105
105
  import "../chunk-P5PIAQVS.js";
@@ -42,7 +42,7 @@ import {
42
42
  pumpBufferedTurn,
43
43
  replayTurnEvents,
44
44
  stampReplaySeq
45
- } from "../chunk-ES3I3UGP.js";
45
+ } from "../chunk-N4RR4WAI.js";
46
46
  export {
47
47
  DEFAULT_RUNNING_TURN_LEASE_MS,
48
48
  DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS,
@@ -200,7 +200,6 @@ export declare const TURN_STATUS_SCOPE_MIGRATION_SQL = "ALTER TABLE turn_status
200
200
  /** For deployments that already have the turn tables, add the index used by
201
201
  * terminal-turn retention without rerunning the table migration. */
202
202
  export declare const TURN_STATUS_RETENTION_MIGRATION_SQL = "CREATE INDEX IF NOT EXISTS idx_turn_status_retention ON turn_status (status, updatedAt);";
203
- /** Resolve a TurnEventStore that appends and reads turn events using a D1-like database interface */
204
203
  export declare function createD1TurnEventStore(db: D1LikeForTurns, options?: TurnEventStoreOptions): TurnEventStore;
205
204
  /** In-memory store for tests and keyless local dev. */
206
205
  export declare function createMemoryTurnEventStore(options?: TurnEventStoreOptions): TurnEventStore;