@tangle-network/agent-app 0.49.38 → 0.50.1

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.
Files changed (69) hide show
  1. package/dist/DesignCanvas-H6PVRRLU.js +12 -0
  2. package/dist/DesignCanvasEditor-ZHKJIDOB.js +15 -0
  3. package/dist/app-auth/index.js +3 -2
  4. package/dist/app-auth/index.js.map +1 -1
  5. package/dist/assistant/index.js +7 -7
  6. package/dist/chat-routes/index.js +10 -9
  7. package/dist/chat-routes/index.js.map +1 -1
  8. package/dist/chat-store/index.js +4 -4
  9. package/dist/{chunk-EA4UVS4T.js → chunk-3NCPXNBZ.js} +35 -90
  10. package/dist/chunk-3NCPXNBZ.js.map +1 -0
  11. package/dist/{chunk-C2VHOSY3.js → chunk-5P6TB74H.js} +11 -6
  12. package/dist/chunk-5P6TB74H.js.map +1 -0
  13. package/dist/{chunk-WAMJSZYB.js → chunk-ACFM2GFL.js} +3 -3
  14. package/dist/{chunk-XTLYE2VO.js → chunk-FRVVHMZO.js} +10 -10
  15. package/dist/{chunk-BWQPVS7D.js → chunk-H6XQZFTT.js} +2 -2
  16. package/dist/{chunk-FEVVF6JJ.js → chunk-HQ7R6CGM.js} +2 -2
  17. package/dist/{chunk-MIFVOYZ7.js → chunk-KJEW23GR.js} +3 -3
  18. package/dist/{chunk-G2B23BZ6.js → chunk-KU5YAGLB.js} +5 -5
  19. package/dist/{chunk-ZMMIQOFI.js → chunk-MX55JP7D.js} +5 -5
  20. package/dist/{chunk-S3344RBY.js → chunk-RACQDXJ7.js} +4 -4
  21. package/dist/{chunk-2WVBGQ2Y.js → chunk-RRWC77YY.js} +2 -2
  22. package/dist/chunk-TZEEGDHS.js +89 -0
  23. package/dist/chunk-TZEEGDHS.js.map +1 -0
  24. package/dist/{chunk-KWN5PGXM.js → chunk-YJYPXMDL.js} +3 -3
  25. package/dist/design-canvas/index.js +3 -2
  26. package/dist/design-canvas/index.js.map +1 -1
  27. package/dist/design-canvas-react/engine.js +5 -4
  28. package/dist/design-canvas-react/index.js +11 -10
  29. package/dist/design-canvas-react/index.js.map +1 -1
  30. package/dist/design-canvas-react/lazy.js +1 -1
  31. package/dist/hosted-agent/application.d.ts +25 -0
  32. package/dist/hosted-agent/application.js +97 -0
  33. package/dist/hosted-agent/application.js.map +1 -0
  34. package/dist/hosted-agent/react/ApplicationLineSetup.d.ts +15 -0
  35. package/dist/hosted-agent/react/LineSetup.d.ts +1 -1
  36. package/dist/hosted-agent/react/contracts.d.ts +8 -1
  37. package/dist/hosted-agent/react/index.d.ts +1 -0
  38. package/dist/hosted-agent/react/index.js +90 -8
  39. package/dist/hosted-agent/react/index.js.map +1 -1
  40. package/dist/hosted-agent/react/lines.css +5 -0
  41. package/dist/hosted-agent/workspace-line.d.ts +9 -0
  42. package/dist/platform/index.js +3 -2
  43. package/dist/platform/index.js.map +1 -1
  44. package/dist/platform/sso.d.ts +18 -4
  45. package/dist/public-consultation/index.js +7 -6
  46. package/dist/public-consultation/index.js.map +1 -1
  47. package/dist/sequences/index.js +2 -1
  48. package/dist/sequences/index.js.map +1 -1
  49. package/dist/stream/index.js +2 -2
  50. package/dist/web/core.d.ts +3 -1
  51. package/dist/web/index.js +5 -3
  52. package/dist/web-react/index.js +20 -20
  53. package/package.json +15 -10
  54. package/dist/DesignCanvas-YLVDCX6H.js +0 -11
  55. package/dist/DesignCanvasEditor-4QWY2LHM.js +0 -14
  56. package/dist/chunk-C2VHOSY3.js.map +0 -1
  57. package/dist/chunk-EA4UVS4T.js.map +0 -1
  58. /package/dist/{DesignCanvas-YLVDCX6H.js.map → DesignCanvas-H6PVRRLU.js.map} +0 -0
  59. /package/dist/{DesignCanvasEditor-4QWY2LHM.js.map → DesignCanvasEditor-ZHKJIDOB.js.map} +0 -0
  60. /package/dist/{chunk-WAMJSZYB.js.map → chunk-ACFM2GFL.js.map} +0 -0
  61. /package/dist/{chunk-XTLYE2VO.js.map → chunk-FRVVHMZO.js.map} +0 -0
  62. /package/dist/{chunk-BWQPVS7D.js.map → chunk-H6XQZFTT.js.map} +0 -0
  63. /package/dist/{chunk-FEVVF6JJ.js.map → chunk-HQ7R6CGM.js.map} +0 -0
  64. /package/dist/{chunk-MIFVOYZ7.js.map → chunk-KJEW23GR.js.map} +0 -0
  65. /package/dist/{chunk-G2B23BZ6.js.map → chunk-KU5YAGLB.js.map} +0 -0
  66. /package/dist/{chunk-ZMMIQOFI.js.map → chunk-MX55JP7D.js.map} +0 -0
  67. /package/dist/{chunk-S3344RBY.js.map → chunk-RACQDXJ7.js.map} +0 -0
  68. /package/dist/{chunk-2WVBGQ2Y.js.map → chunk-RRWC77YY.js.map} +0 -0
  69. /package/dist/{chunk-KWN5PGXM.js.map → chunk-YJYPXMDL.js.map} +0 -0
@@ -28,9 +28,9 @@ import {
28
28
  terminalizeDanglingAssistantToolUpdates,
29
29
  terminalizeDanglingToolPart,
30
30
  terminalizeDanglingToolParts
31
- } from "../chunk-ZMMIQOFI.js";
32
- import "../chunk-M3K2HVQD.js";
31
+ } from "../chunk-MX55JP7D.js";
33
32
  import "../chunk-YJMCRXQQ.js";
33
+ import "../chunk-M3K2HVQD.js";
34
34
  import {
35
35
  DEFAULT_RUNNING_TURN_LEASE_MS,
36
36
  DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS,
@@ -11,7 +11,9 @@ export * from './free-route-limit';
11
11
  export type JsonObject = Record<string, unknown>;
12
12
  /** Parse + object-narrow a Request body. `[body, null]` on success, `[null,
13
13
  * errorResponse]` on a non-object body (callers `if (err) return err`). */
14
- export declare function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]>;
14
+ export declare function parseJsonObjectBody(request: Request, options?: {
15
+ maxBytes?: number;
16
+ }): Promise<[JsonObject, null] | [null, Response]>;
15
17
  /** Narrow one required string field, 400 if missing/empty. */
16
18
  export declare function requireString(body: JsonObject, field: string): string | Response;
17
19
  /** Define the context of a request including IP address, user agent, timestamp, and request ID */
package/dist/web/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ createApiKeyFetch,
3
+ groupConversationMessages
4
+ } from "../chunk-TZEEGDHS.js";
1
5
  import {
2
6
  FREE_ROUTE_BUDGETS,
3
7
  FreeRouteLimitError,
@@ -8,16 +12,14 @@ import {
8
12
  checkFreeRouteLimit,
9
13
  checkRateLimit,
10
14
  clearCookieHeader,
11
- createApiKeyFetch,
12
15
  extractRequestContext,
13
16
  freeRouteLimitResponse,
14
- groupConversationMessages,
15
17
  parseJsonObjectBody,
16
18
  readCookieValue,
17
19
  requireString,
18
20
  serializeCookie,
19
21
  withFreeRouteLimit
20
- } from "../chunk-EA4UVS4T.js";
22
+ } from "../chunk-3NCPXNBZ.js";
21
23
  import {
22
24
  isWorkspaceFileExportable
23
25
  } from "../chunk-TXD5HXLE.js";
@@ -128,7 +128,10 @@ import {
128
128
  withoutRecordGridCreated,
129
129
  withoutRecordGridRemoved,
130
130
  withoutRecordGridUpdate
131
- } from "../chunk-XTLYE2VO.js";
131
+ } from "../chunk-FRVVHMZO.js";
132
+ import {
133
+ MENTION_PILL_CLASS
134
+ } from "../chunk-3HUFJ5LO.js";
132
135
  import {
133
136
  SessionHistoryPanel,
134
137
  formatSessionTimestamp,
@@ -136,10 +139,19 @@ import {
136
139
  useSessionActions,
137
140
  useSessionHistory
138
141
  } from "../chunk-F32T3DRJ.js";
139
- import {
140
- MENTION_PILL_CLASS
141
- } from "../chunk-3HUFJ5LO.js";
142
142
  import "../chunk-FBVLEGEG.js";
143
+ import {
144
+ EvidenceLineageTable,
145
+ ExceptionList,
146
+ ProvenanceStamp,
147
+ QualityCheckList,
148
+ ReviewQueuePanel,
149
+ WorkProductCard,
150
+ mergeReviewQueuePages,
151
+ reviewQueueStateLabel,
152
+ workProductPartsFromMessageParts,
153
+ workProductStatusLabel
154
+ } from "../chunk-ENLRJYVW.js";
143
155
  import {
144
156
  AgentSessionControls,
145
157
  ChatComposer,
@@ -179,18 +191,6 @@ import {
179
191
  useSandboxTerminalConnection
180
192
  } from "../chunk-BATKJP3P.js";
181
193
  import "../chunk-SJWIZT7B.js";
182
- import {
183
- EvidenceLineageTable,
184
- ExceptionList,
185
- ProvenanceStamp,
186
- QualityCheckList,
187
- ReviewQueuePanel,
188
- WorkProductCard,
189
- mergeReviewQueuePages,
190
- reviewQueueStateLabel,
191
- workProductPartsFromMessageParts,
192
- workProductStatusLabel
193
- } from "../chunk-ENLRJYVW.js";
194
194
  import {
195
195
  parseReviewQueueItem
196
196
  } from "../chunk-GEYACSFW.js";
@@ -208,7 +208,7 @@ import {
208
208
  isChatAttachmentPart,
209
209
  mentionInputToPart,
210
210
  mentionPartsFromMessageParts
211
- } from "../chunk-S3344RBY.js";
211
+ } from "../chunk-RACQDXJ7.js";
212
212
  import {
213
213
  DISPATCH_MAX_MEDIA_PARTS,
214
214
  DISPATCH_MAX_PARTS,
@@ -221,11 +221,12 @@ import {
221
221
  mediaTypeForMentionPath,
222
222
  mentionKindForPath
223
223
  } from "../chunk-X47R2IVO.js";
224
- import "../chunk-TXD5HXLE.js";
225
224
  import "../chunk-ZVEEWGDK.js";
225
+ import "../chunk-TXD5HXLE.js";
226
226
  import {
227
227
  attachmentPartKey
228
- } from "../chunk-ZMMIQOFI.js";
228
+ } from "../chunk-MX55JP7D.js";
229
+ import "../chunk-YJMCRXQQ.js";
229
230
  import {
230
231
  INTERACTION_CANCEL_EVENT,
231
232
  INTERACTION_EVENT,
@@ -251,7 +252,6 @@ import {
251
252
  questionInteractionContentSignature,
252
253
  stampInteractionAnswers
253
254
  } from "../chunk-M3K2HVQD.js";
254
- import "../chunk-YJMCRXQQ.js";
255
255
  export {
256
256
  ATTACHMENT_ACCEPT,
257
257
  AgentActivityPanel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.49.38",
3
+ "version": "0.50.1",
4
4
  "packageManager": "pnpm@11.24.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [
@@ -223,6 +223,11 @@
223
223
  "import": "./dist/hosted-agent/index.js",
224
224
  "default": "./dist/hosted-agent/index.js"
225
225
  },
226
+ "./hosted-agent/application": {
227
+ "types": "./dist/hosted-agent/application.d.ts",
228
+ "import": "./dist/hosted-agent/application.js",
229
+ "default": "./dist/hosted-agent/application.js"
230
+ },
226
231
  "./hosted-agent/react": {
227
232
  "types": "./dist/hosted-agent/react/index.d.ts",
228
233
  "import": "./dist/hosted-agent/react/index.js",
@@ -558,13 +563,13 @@
558
563
  "@tangle-network/agent-eval": "0.199.1",
559
564
  "@tangle-network/agent-gateway": "0.11.4",
560
565
  "@tangle-network/agent-integrations": "0.57.0",
561
- "@tangle-network/agent-interface": "2.13.0",
566
+ "@tangle-network/agent-interface": "2.14.0",
562
567
  "@tangle-network/agent-knowledge": "17.1.10",
563
568
  "@tangle-network/agent-profile-materialize": "0.20.1",
564
- "@tangle-network/agent-runtime": "0.282.2",
569
+ "@tangle-network/agent-runtime": "0.285.0",
565
570
  "@tangle-network/brand": "1.6.0",
566
- "@tangle-network/hub-sdk": "0.19.3",
567
- "@tangle-network/sandbox": "0.55.2",
571
+ "@tangle-network/hub-sdk": "0.21.0",
572
+ "@tangle-network/sandbox": "0.58.1",
568
573
  "@tangle-network/sandbox-ui": "0.114.0",
569
574
  "@tangle-network/ui": "11.10.0",
570
575
  "@testing-library/dom": "^10.4.1",
@@ -624,15 +629,15 @@
624
629
  "peerDependencies": {
625
630
  "@firecrawl/pdf-inspector-wasm": ">=0.1.3",
626
631
  "@huggingface/transformers": ">=3",
627
- "@tangle-network/agent-eval": ">=0.199.0 <0.200.0",
628
- "@tangle-network/agent-integrations": ">=0.54.2 <0.58.0",
632
+ "@tangle-network/agent-eval": ">=0.199.0 <0.201.0",
633
+ "@tangle-network/agent-integrations": ">=0.54.2 <0.58.0 || >=0.59.0 <0.60.0",
629
634
  "@tangle-network/agent-interface": "^2.13.0",
630
635
  "@tangle-network/agent-knowledge": "^17.1.2",
631
636
  "@tangle-network/agent-profile-materialize": ">=0.20.1 <0.21.0",
632
- "@tangle-network/agent-runtime": ">=0.282.2 <0.283.0",
637
+ "@tangle-network/agent-runtime": ">=0.282.2 <0.284.0 || >=0.285.0 <0.286.0",
633
638
  "@tangle-network/brand": ">=1.6.0",
634
- "@tangle-network/hub-sdk": ">=0.19.3 <0.21.0",
635
- "@tangle-network/sandbox": ">=0.45.0 <0.48.0 || ^0.49.0 || ^0.50.0 || ^0.51.0 || ^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0",
639
+ "@tangle-network/hub-sdk": ">=0.19.3 <0.21.0 || >=0.21.0 <0.22.0",
640
+ "@tangle-network/sandbox": ">=0.45.0 <0.48.0 || ^0.49.0 || ^0.50.0 || ^0.51.0 || ^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || >=0.58.1 <0.59.0",
636
641
  "@tangle-network/sandbox-ui": ">=0.113.3 <0.115.0",
637
642
  "@tangle-network/ui": ">=11.6.0 <12.0.0",
638
643
  "@tiptap/core": ">=3.28.0 <4.0.0",
@@ -1,11 +0,0 @@
1
- import {
2
- DesignCanvas
3
- } from "./chunk-KWN5PGXM.js";
4
- import "./chunk-FEVVF6JJ.js";
5
- import "./chunk-BWQPVS7D.js";
6
- import "./chunk-EA4UVS4T.js";
7
- import "./chunk-TXD5HXLE.js";
8
- export {
9
- DesignCanvas
10
- };
11
- //# sourceMappingURL=DesignCanvas-YLVDCX6H.js.map
@@ -1,14 +0,0 @@
1
- import {
2
- DesignCanvasEditor
3
- } from "./chunk-G2B23BZ6.js";
4
- import "./chunk-KWN5PGXM.js";
5
- import "./chunk-Q2QKEV6J.js";
6
- import "./chunk-2WVBGQ2Y.js";
7
- import "./chunk-FEVVF6JJ.js";
8
- import "./chunk-BWQPVS7D.js";
9
- import "./chunk-EA4UVS4T.js";
10
- import "./chunk-TXD5HXLE.js";
11
- export {
12
- DesignCanvasEditor
13
- };
14
- //# sourceMappingURL=DesignCanvasEditor-4QWY2LHM.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/platform/sso.ts","../src/platform/guards.ts"],"sourcesContent":["/**\n * Tangle SSO for agent apps. OIDC callers use the registered runtime client\n * with S256 PKCE. Legacy API-key callers remain explicit during migration.\n * OIDC never falls back to cross-site exchange. Account matching and native\n * session cookie minting remain shared.\n */\n\nimport { clearCookieHeader, readCookieValue, serializeCookie } from '../web/index'\n\nconst DEFAULT_STATE_TTL_SECONDS = 600\nconst DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7\nconst DEFAULT_REDIRECT_PATH = '/app'\nconst DEFAULT_LOGIN_PATH = '/login'\nconst DEFAULT_SESSION_COOKIE = 'better-auth.session_token'\n\n// ── Signed state ────────────────────────────────────────────────────────────\n\n/** Define configuration options for managing SSO state including secret, lifetime, and clock injection */\nexport interface SsoStateConfig {\n /** HMAC-SHA256 secret (e.g. the app's auth secret). */\n secret: string\n /** State lifetime in ms. Default 600 000. */\n ttlMs?: number\n /** Injectable clock (ms since epoch). Default Date.now. */\n now?: () => number\n}\n\nfunction randomHex(bytes: number): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nasync function hmacBytes(secret: string, value: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value)))\n}\n\nasync function hmacHex(secret: string, value: string): Promise<string> {\n return Array.from(await hmacBytes(secret, value), (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n\n/** Mint a `<randomHex32>.<timestamp36>.<hmacHex>` state value. The timestamp\n * is inside the signed payload, so expiry survives cookie-attribute tampering. */\nexport async function createSignedSsoState(config: SsoStateConfig): Promise<string> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const now = config.now ?? Date.now\n const payload = `${randomHex(16)}.${now().toString(36)}`\n return `${payload}.${await hmacHex(config.secret, payload)}`\n}\n\n/** Verify the MAC (constant-time) and the signed TTL. */\nexport async function verifySignedSsoState(state: string, config: SsoStateConfig): Promise<boolean> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const parts = state.split('.')\n if (parts.length !== 3) return false\n const [random, timestamp, mac] = parts\n if (!random || !timestamp || !mac || !/^[0-9a-z]+$/.test(timestamp)) return false\n const expected = await hmacHex(config.secret, `${random}.${timestamp}`)\n if (!constantTimeEqual(mac, expected)) return false\n const mintedAt = parseInt(timestamp, 36)\n if (!Number.isFinite(mintedAt)) return false\n const now = config.now ?? Date.now\n const ttlMs = config.ttlMs ?? DEFAULT_STATE_TTL_SECONDS * 1000\n const age = now() - mintedAt\n return age >= 0 && age <= ttlMs\n}\n\n// ── Seams ───────────────────────────────────────────────────────────────────\n\n/** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */\nexport interface TangleSsoExchangeResult {\n apiKey: string\n /** The platform's canonical verified-email proof. */\n emailVerified: boolean\n user: { id: string; email: string; name?: string | null }\n plan?: { tier: string } | null\n}\n\n/** Structural mirror of the platform auth wire client — any object with these\n * two methods satisfies it without this module importing the concrete class. */\nexport interface TangleSsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string): Promise<TangleSsoExchangeResult>\n}\n\n/** First-party login exchanges identity without minting a Platform API key. */\nexport interface TangleIdentitySsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string, redirectUri: string): Promise<{\n emailVerified: boolean\n user: TangleSsoExchangeResult['user']\n }>\n}\n\n/** Token fields the callback persists from the runtime's OIDC exchange. */\nexport interface TangleOidcSsoTokens {\n accessToken: string\n tokenType: string\n expiresIn?: number\n refreshToken?: string\n idToken?: string\n scope?: string\n}\n\n/** Verified identity returned by the runtime's OIDC userinfo lookup. */\nexport interface TangleOidcSsoUser {\n id: string\n email: string\n emailVerified: true\n name?: string\n}\n\n/** The caller needs only the OIDC operations used by this callback. Keeping\n * this structural prevents legacy consumers from needing the optional\n * Runtime peer just to load the SSO declarations. */\nexport interface TangleOidcSsoAuthClient {\n authorizeUrl(options: {\n state: string\n codeChallenge: string\n nonce?: string\n prompt?: 'login' | 'consent' | 'none'\n loginHint?: string\n }): string\n exchange(code: string, codeVerifier: string): Promise<{\n tokens: TangleOidcSsoTokens\n user: TangleOidcSsoUser\n }>\n revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void>\n}\n\n/** Local account shape required by the verified Tangle SSO account policy.\n * Consumers map their user/link tables into this shape before resolving an\n * exchange. */\nexport interface TangleSsoLocalAccount {\n userId: string\n email: string\n emailVerified: boolean\n platformUserId: string | null\n}\n\n/** Reasons a local account lookup must stop instead of linking an exchange. */\nexport type TangleSsoAccountConflictReason =\n | 'ambiguous-platform-id'\n | 'platform-id-email-conflict'\n | 'unverified-email'\n | 'email-platform-id-conflict'\n | 'ambiguous-email'\n | 'invalid-platform-id'\n | 'invalid-email'\n | 'platform-match-mismatch'\n | 'email-match-mismatch'\n\n/** Result of applying the verified Tangle SSO account matching policy. */\nexport type TangleSsoAccountResolution =\n | { kind: 'existing'; userId: string; matchedBy: 'platform-id' | 'verified-email' }\n | { kind: 'create' }\n | { kind: 'reject'; reason: TangleSsoAccountConflictReason }\n\n/** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the\n * platform's stable identity. */\nexport interface TangleSsoAccountResolutionInput {\n email: string\n platformMatches: readonly TangleSsoLocalAccount[]\n emailMatches: readonly TangleSsoLocalAccount[]\n platformUserId: string\n}\n\n/** Normalize an email for comparisons and persistence queries. */\nexport function normalizeTangleSsoEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nfunction normalizedPlatformUserId(value: string | null | undefined): string | null {\n const normalized = value?.trim()\n return normalized ? normalized : null\n}\n\n/**\n * Resolve a platform exchange against all local rows found by stable platform\n * identity and normalized email.\n *\n * A stable platform id takes precedence over email. Every ambiguous or\n * contradictory result rejects, and email fallback requires a verified local\n * email that is not already bound to another platform id. The resolver is\n * pure so each consumer can query its own schema, including schemas without\n * unique indexes, then rerun it after an insert race.\n */\nexport function resolveTangleSsoAccount(\n input: TangleSsoAccountResolutionInput,\n): TangleSsoAccountResolution {\n const email = normalizeTangleSsoEmail(input.email)\n const platformUserId = normalizedPlatformUserId(input.platformUserId)\n if (!email) return { kind: 'reject', reason: 'invalid-email' }\n if (!platformUserId) return { kind: 'reject', reason: 'invalid-platform-id' }\n\n if (input.platformMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-platform-id' }\n }\n if (input.emailMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-email' }\n }\n\n if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {\n return { kind: 'reject', reason: 'platform-match-mismatch' }\n }\n if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {\n return { kind: 'reject', reason: 'email-match-mismatch' }\n }\n\n const platformMatch = input.platformMatches[0]\n const emailMatch = input.emailMatches[0]\n\n if (platformMatch) {\n if (emailMatch && emailMatch.userId !== platformMatch.userId) {\n return { kind: 'reject', reason: 'platform-id-email-conflict' }\n }\n return { kind: 'existing', userId: platformMatch.userId, matchedBy: 'platform-id' }\n }\n\n if (!emailMatch) return { kind: 'create' }\n const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId)\n if (existingPlatformUserId === platformUserId) {\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'platform-id' }\n }\n if (emailMatch.emailVerified !== true) {\n return { kind: 'reject', reason: 'unverified-email' }\n }\n\n if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {\n return { kind: 'reject', reason: 'email-platform-id-conflict' }\n }\n\n // The platform proof is already verified. A verified local email row is safe\n // to link when it is unbound or already carries this same platform id.\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'verified-email' }\n}\n\n/** Thrown by an account store when the verified SSO policy rejects a link. */\nexport class TangleSsoAccountConflictError extends Error {\n readonly reason: TangleSsoAccountConflictReason\n\n constructor(reason: TangleSsoAccountConflictReason, message = 'Tangle SSO account linking was rejected') {\n super(message)\n this.name = 'TangleSsoAccountConflictError'\n this.reason = reason\n }\n}\n\n/** Thrown by `upsertUserByEmail` when the app-local user row cannot be\n * created; the callback handler maps it to `?error=tangle_user_create_failed`.\n * Any other store error propagates. */\nexport class TangleSsoUserCreateError extends Error {\n constructor(message = 'Failed to create local user for Tangle SSO') {\n super(message)\n this.name = 'TangleSsoUserCreateError'\n }\n}\n\n/**\n * Account persistence seam. Covers both storage styles in use: link-table\n * apps (a per-user platform-link row) and session-column apps (the key on the\n * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,\n * and each app persists with the key it needs. `createSession` runs first so\n * the token is always available to `saveTangleLink`.\n */\nexport interface TangleSsoAccountStore {\n /** Resolve the local account before any user, session, or link write.\n * Implementations must query every candidate by stable platform id and\n * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent\n * fail-closed policy). The callback rejects `kind: 'reject'` results before\n * it calls `upsertUserByEmail`. */\n resolveAccount(input: { email: string; platformUserId: string }): Promise<TangleSsoAccountResolution>\n /** Find-or-create the app-local user selected by `resolveAccount`.\n * `tangleUserId` is the platform's stable user id — match on it first when\n * the app stores it (emails are mutable on the platform; the id is not),\n * falling back to email for first-time logins. */\n upsertUserByEmail(input: {\n email: string\n name: string | null\n tangleUserId: string\n resolution: Exclude<TangleSsoAccountResolution, { kind: 'reject' }>\n }): Promise<{ userId: string }>\n /** Create an app session row; returns the session-cookie token value. */\n createSession(input: {\n userId: string\n expiresAt: Date\n ipAddress: string | null\n userAgent: string | null\n }): Promise<{ token: string }>\n /** Persist the platform link (API key + platform identity). */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n apiKey: string\n planTier: string | null\n }): Promise<void>\n}\n\n/**\n * OIDC tokens are server-side credentials, not API keys. Implementations must\n * encrypt them at rest and bind them to the exact local session. Refresh and\n * disconnect must serialize updates to the same grant.\n */\nexport interface TangleOidcSsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** Atomically bind the grant to this exact session. If persistence partially\n * fails, `deleteSession` must remove every row and credential for it. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n tokens: TangleOidcSsoTokens\n accessTokenExpiresAt: Date\n }): Promise<void>\n /** Remove an unpublished local session if callback persistence fails.\n * This must also remove any grant rows or credentials partially written by\n * a rejected `saveTangleLink`, keyed by this exact session token. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n/** Persist only the stable identity link for a first-party app. */\nexport interface TangleIdentitySsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n }): Promise<void>\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n// ── Session cookie ──────────────────────────────────────────────────────────\n\n/** Successful-login context handed to the `setSessionCookie` seam. */\nexport interface TangleSsoSessionCookieArgs {\n /** Session token returned by `store.createSession`. */\n token: string\n /** Session expiry (now + `sessionTtlSeconds`). */\n expiresAt: Date\n /** Mirrors `sessionTtlSeconds` after defaulting. */\n ttlSeconds: number\n /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */\n secure: boolean\n}\n\n/**\n * Sign a session token to better-call's signed-cookie contract — the value\n * better-auth's `getSignedCookie` verifies: `<token>.<signature>` where the\n * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as\n * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call\n * rejects any other length or suffix, so url-safe/unpadded variants read back\n * as a null session). The joined value is percent-encoded once at cookie\n * serialization, matching better-call's `serializeSignedCookie` byte-exactly.\n */\nexport async function signSessionCookieValue(token: string, secret: string): Promise<string> {\n if (!secret) throw new Error('signSessionCookieValue requires a non-empty secret')\n const sig = await hmacBytes(secret, token)\n let bin = ''\n for (const byte of sig) bin += String.fromCharCode(byte)\n return `${token}.${btoa(bin)}`\n}\n\n/** Structural slice of a `betterAuth()` instance — only what cookie minting\n * reads. No better-auth import: the signing contract is implemented by\n * `signSessionCookieValue`, byte-compatible with better-auth's own\n * `makeSignature`. */\nexport interface BetterAuthSessionCookieSource {\n $context: PromiseLike<{\n secret: string\n authCookies: {\n sessionToken: {\n /** Final cookie name — better-auth decides the `__Secure-` prefix\n * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */\n name: string\n attributes: {\n secure?: boolean\n sameSite?: string\n path?: string\n httpOnly?: boolean\n domain?: string\n }\n }\n }\n }>\n}\n\n/** Define options to customize warning behavior for shadowed cookie names in authentication sessions */\nexport interface BetterAuthSessionCookieMinterOptions {\n /** Receives the shadowed-cookie-name warning (see below). Default\n * console.warn. */\n warn?: (message: string) => void\n}\n\n/**\n * Canonical `setSessionCookie` wiring for better-auth apps: mint the session\n * Set-Cookie exactly as better-auth's own login flows do — name + attributes\n * from `auth.$context.authCookies.sessionToken` (better-auth stays\n * authoritative over prefix/name/attributes) and the value signed to\n * better-call's `getSignedCookie` contract. A raw unprefixed\n * `better-auth.session_token` left by an earlier login is explicitly expired\n * so it cannot shadow the real cookie.\n *\n * Warns when the app's session cookie still has better-auth's DEFAULT name:\n * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie\n * under that exact name, and equal-path cookies are sent oldest-first — the\n * platform's cookie is always older (the user signs in there before the app's\n * callback runs), so the app reads the platform's token, fails its own\n * signature check, and every fresh login lands logged-out. Per-app\n * `advanced.cookiePrefix` is the fix.\n *\n * Throws on a domain-scoped session cookie for the same reason: a\n * `Domain=`-wide session cookie is exactly the shadowing footgun.\n */\nexport function createBetterAuthSessionCookieMinter(\n auth: BetterAuthSessionCookieSource,\n options: BetterAuthSessionCookieMinterOptions = {},\n): (args: TangleSsoSessionCookieArgs) => Promise<string[]> {\n const warn = options.warn ?? ((message: string) => console.warn(message))\n return async ({ token, ttlSeconds }) => {\n const ctx = await auth.$context\n if (!ctx.secret) {\n throw new Error('createBetterAuthSessionCookieMinter: auth context has no secret')\n }\n const { name, attributes } = ctx.authCookies.sessionToken\n if (attributes.domain) {\n throw new Error(\n `createBetterAuthSessionCookieMinter: refusing a domain-scoped session cookie (Domain=${attributes.domain}) — ` +\n 'a domain-wide session cookie shadows sibling apps that share the parent domain',\n )\n }\n if (name === DEFAULT_SESSION_COOKIE || name === `__Secure-${DEFAULT_SESSION_COOKIE}`) {\n warn(\n `[tangle-sso] session cookie is named \"${name}\" — better-auth's default. ` +\n 'The Tangle platform (id.tangle.tools) sets a Domain=.tangle.tools cookie under the same name, ' +\n \"and the platform's (older) cookie wins the Cookie-header order, so this app's sessions read back null. \" +\n \"Set a per-app prefix: betterAuth({ advanced: { cookiePrefix: '<app>' } }).\",\n )\n }\n const sameSite = typeof attributes.sameSite === 'string' ? attributes.sameSite : 'lax'\n const cookieOptions = {\n name,\n path: typeof attributes.path === 'string' ? attributes.path : '/',\n httpOnly: attributes.httpOnly !== false,\n sameSite: (sameSite.charAt(0).toUpperCase() + sameSite.slice(1)) as 'Lax' | 'Strict' | 'None',\n secure: Boolean(attributes.secure) || name.startsWith('__Secure-'),\n maxAgeSeconds: ttlSeconds,\n }\n const cookies = [serializeCookie(await signSessionCookieValue(token, ctx.secret), cookieOptions)]\n if (name !== DEFAULT_SESSION_COOKIE) {\n cookies.push(clearCookieHeader({ ...cookieOptions, name: DEFAULT_SESSION_COOKIE }))\n }\n return cookies\n }\n}\n\n// ── Handlers ────────────────────────────────────────────────────────────────\n\n/** Define configuration options for handling Tangle SSO authentication and session management */\nexport interface TangleSsoHandlerOptions {\n protocol?: 'legacy'\n auth: TangleSsoAuthClient\n store: TangleSsoAccountStore\n /** HMAC secret for the state cookie. */\n stateSecret: string\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n stateCookieName: string\n /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is\n * provided. The default path prepends `__Secure-` iff `secureCookies`. */\n sessionCookieName?: string\n /** Mint the host auth framework's own session cookie(s); return complete\n * Set-Cookie header values (the handler appends them verbatim and sets no\n * session cookie itself). Supply this when the framework should stay\n * authoritative over name/prefix/signing/attributes — e.g. better-auth:\n * `auth.$context.authCookies.sessionToken` + `makeSignature`. */\n setSessionCookie?: (\n args: TangleSsoSessionCookieArgs,\n ) => readonly string[] | Promise<readonly string[]>\n /** HMAC-SHA256 secret the host auth framework verifies session cookies with\n * (better-auth: its `secret`). Required when `setSessionCookie` is absent —\n * the default cookie is minted to better-call's signed contract via\n * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a\n * null session, so there is deliberately no fallback to `stateSecret`\n * (which is not guaranteed to be the auth secret). */\n sessionCookieSecret?: string\n /** Adds `Secure` to every cookie this module sets, and (default session\n * cookie only) the `__Secure-` name prefix. Must match the auth\n * framework's own secure-cookie decision (better-auth: https `baseURL` /\n * `advanced.useSecureCookies`), or it will look up a different cookie name\n * than the one set here. */\n secureCookies: boolean\n /** Default 604 800 (7 days). */\n sessionTtlSeconds?: number\n /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */\n stateTtlSeconds?: number\n /** Default '/app'. */\n defaultRedirectPath?: string\n /** Default '/login'. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n now?: () => number\n}\n\n/** Registered OIDC client and token store. The OIDC caller supplies the Runtime PKCE helper. */\nexport interface TangleOidcSsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'oidc'\n auth: TangleOidcSsoAuthClient\n store: TangleOidcSsoAccountStore\n /** Pass the Runtime's `createPkcePair` helper from the OIDC-only caller. */\n createPkcePair: () => Promise<{ verifier: string; challenge: string }>\n}\n\nexport interface TangleIdentitySsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'identity'\n auth: TangleIdentitySsoAuthClient\n store: TangleIdentitySsoAccountStore\n}\n\n/** Define handlers for SSO start and callback routes managing authentication flow and session cookies */\nexport interface TangleSsoHandlers {\n /** GET start route: mint + sign state, set the state cookie, 302 to the\n * platform authorize URL. `?redirect=` carries the post-login path. */\n start(request: Request): Promise<Response>\n /** GET callback route: verify state, exchange the code, upsert the user,\n * create the session, save the platform link, set the session cookie\n * (via the `setSessionCookie` seam, else signed to better-call's contract\n * with `sessionCookieSecret`), 302 to the saved redirect. Every failure\n * 302s to `loginPath?error=…` with the state cookie cleared. */\n callback(request: Request): Promise<Response>\n}\n\n/** Redirect targets must stay on this app's origin. A prefix check on `/`\n * and `//` is not enough: in the WHATWG parser a backslash is a path\n * separator for special schemes, so `/\\host` resolves to `https://host/`,\n * and tab or newline are stripped before parsing, so `/\\t/host` is `//host`.\n * Parse the value against a fixed origin and accept it only when that origin\n * survives; the result is the parser's normalised path, query and fragment.\n * Backslash and control characters are rejected up front as well, so the\n * decision does not rest on one parser's handling of them.\n *\n * The input staying on-origin is not enough either: dot-segment removal\n * turns `/.//host` or `/a/..//host` into the pathname `//host` with the\n * origin intact, and a browser resolves that Location off-origin. So the\n * emitted value must itself resolve on-origin and come back unchanged. */\nconst REDIRECT_REFERENCE_ORIGIN = 'https://redirect-reference.invalid'\n\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (!value || !value.startsWith('/') || /[\\\\\\u0000-\\u001f\\u007f]/.test(value)) return fallback\n const path = resolveOnReferenceOrigin(value)\n if (path === null || resolveOnReferenceOrigin(path) !== path) return fallback\n return path\n}\n\n/** `path + search + hash` of `value` resolved against the reference origin,\n * or null when the parse fails or lands on another origin. */\nfunction resolveOnReferenceOrigin(value: string): string | null {\n let url: URL\n try {\n url = new URL(value, REDIRECT_REFERENCE_ORIGIN)\n } catch {\n return null\n }\n if (url.origin !== REDIRECT_REFERENCE_ORIGIN) return null\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\n headers.set('Cache-Control', 'no-store')\n headers.set('Referrer-Policy', 'no-referrer')\n return new Response(null, { status: 302, headers })\n}\n\n/** Real client IP: `CF-Connecting-IP` behind Cloudflare, else the first\n * `x-forwarded-for` hop (the rest of the list is sender-controlled). */\nfunction clientIp(request: Request): string | null {\n return (\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n null\n )\n}\n\ninterface StateCookiePayload {\n s: string\n r: string\n v?: string\n m?: string\n}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw || raw.length > 4096) return null\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed === null || typeof parsed !== 'object') return null\n const { s, r, v, m } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r, ...(typeof v === 'string' ? { v } : {}), ...(typeof m === 'string' ? { m } : {}) }\n } catch { return null }\n}\n\nfunction stateBinding(payload: StateCookiePayload, callbackUrl: string): string {\n return JSON.stringify([payload.s, payload.r, payload.v ?? null, callbackUrl])\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(\n opts: TangleSsoHandlerOptions | TangleOidcSsoHandlerOptions | TangleIdentitySsoHandlerOptions,\n): TangleSsoHandlers {\n if (!opts.stateSecret) throw new Error('TangleSsoHandlerOptions.stateSecret is required')\n if (!opts.callbackUrl) throw new Error('TangleSsoHandlerOptions.callbackUrl is required')\n if (!opts.stateCookieName) throw new Error('TangleSsoHandlerOptions.stateCookieName is required')\n const callbackUrl = new URL(opts.callbackUrl)\n if (opts.protocol === 'oidc' || opts.protocol === 'identity') {\n if (opts.stateSecret.length < 32) throw new Error('SSO stateSecret must contain at least 32 characters')\n if (callbackUrl.username || callbackUrl.password || callbackUrl.hash || callbackUrl.search) {\n throw new Error('SSO callbackUrl must be fixed and have no credentials, query, or fragment')\n }\n const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(callbackUrl.hostname)\n if (callbackUrl.protocol !== 'https:' && !(callbackUrl.protocol === 'http:' && loopback)) {\n throw new Error('SSO requires HTTPS except on loopback')\n }\n if (callbackUrl.protocol === 'https:' && !opts.secureCookies) {\n throw new Error('SSO HTTPS callbacks require Secure cookies')\n }\n }\n const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE\n let mintSessionCookies: (args: TangleSsoSessionCookieArgs) => Promise<readonly string[]>\n if (opts.setSessionCookie) {\n const seam = opts.setSessionCookie\n mintSessionCookies = async (args) => await seam(args)\n } else if (opts.sessionCookieSecret) {\n const secret = opts.sessionCookieSecret\n mintSessionCookies = async ({ token, secure, ttlSeconds }) => [\n serializeCookie(await signSessionCookieValue(token, secret), {\n name: secure ? `__Secure-${sessionCookieName}` : sessionCookieName,\n secure,\n maxAgeSeconds: ttlSeconds,\n }),\n ]\n } else {\n throw new Error('TangleSsoHandlerOptions requires setSessionCookie or sessionCookieSecret')\n }\n const sessionTtlSeconds = opts.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS\n const stateTtlSeconds = opts.stateTtlSeconds ?? DEFAULT_STATE_TTL_SECONDS\n if (!Number.isFinite(stateTtlSeconds) || stateTtlSeconds <= 0\n || !Number.isFinite(sessionTtlSeconds) || sessionTtlSeconds <= 0) {\n throw new Error('SSO lifetimes must be positive and finite')\n }\n const defaultRedirectPath = sanitizeRedirectPath(opts.defaultRedirectPath ?? DEFAULT_REDIRECT_PATH, DEFAULT_REDIRECT_PATH)\n const loginPath = sanitizeRedirectPath(opts.loginPath ?? DEFAULT_LOGIN_PATH, DEFAULT_LOGIN_PATH)\n const log = opts.log ?? (() => {})\n const now = opts.now ?? Date.now\n const stateConfig: SsoStateConfig = { secret: opts.stateSecret, ttlMs: stateTtlSeconds * 1000, now }\n const stateCookieOpts = { name: opts.stateCookieName, secure: opts.secureCookies }\n function loginErrorRedirect(code: string): Response {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const target = new URL(loginPath, REDIRECT_REFERENCE_ORIGIN)\n target.searchParams.set('error', code)\n return redirectResponse(`${target.pathname}${target.search}${target.hash}`, headers)\n }\n return {\n async start(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity') && url.origin !== callbackUrl.origin) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const redirectPath = sanitizeRedirectPath(url.searchParams.get('redirect'), defaultRedirectPath)\n const state = await createSignedSsoState(stateConfig)\n const payload: StateCookiePayload = { s: state, r: redirectPath }\n let authorizationUrl: string\n if (opts.protocol === 'oidc') {\n const pkce = await opts.createPkcePair()\n payload.v = pkce.verifier\n payload.m = await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))\n authorizationUrl = opts.auth.authorizeUrl({ state, codeChallenge: pkce.challenge })\n if (new URL(authorizationUrl).searchParams.get('redirect_uri') !== opts.callbackUrl) {\n throw new Error('PlatformOidcClient.redirectUri must equal callbackUrl')\n }\n } else {\n authorizationUrl = opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl })\n }\n const cookie = serializeCookie(JSON.stringify(payload), {\n ...stateCookieOpts,\n maxAgeSeconds: stateTtlSeconds,\n })\n const headers = new Headers()\n headers.append('Set-Cookie', cookie)\n return redirectResponse(authorizationUrl, headers)\n },\n async callback(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity')\n && (url.origin !== callbackUrl.origin || url.pathname !== callbackUrl.pathname)) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const code = url.searchParams.get('code')\n const stateFromPlatform = url.searchParams.get('state')\n if (!code || !stateFromPlatform || url.searchParams.has('error')) return loginErrorRedirect('tangle_callback_missing')\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || !constantTimeEqual(payload.s, stateFromPlatform)) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n if (opts.protocol === 'oidc' && (!payload.v || !/^[A-Za-z0-9_-]{43,128}$/.test(payload.v)\n || !payload.m || !constantTimeEqual(payload.m, await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))))) {\n return loginErrorRedirect('tangle_state_mismatch')\n }\n const tokenRequestedAt = now()\n let oidcTokens: TangleOidcSsoTokens | undefined\n let legacyExchange: TangleSsoExchangeResult | undefined\n let createdSession: string | undefined\n let committed = false\n try {\n let exchanged: { user: TangleSsoExchangeResult['user']; emailVerified: boolean }\n try {\n if (opts.protocol === 'oidc') {\n const result = await opts.auth.exchange(code, payload.v!)\n oidcTokens = result.tokens\n if (!oidcTokens.refreshToken || !Number.isFinite(oidcTokens.expiresIn) || oidcTokens.expiresIn! <= 0) {\n throw new Error('OIDC requires offline_access and a finite access-token lifetime')\n }\n exchanged = { user: result.user, emailVerified: result.user.emailVerified }\n } else if (opts.protocol === 'identity') {\n exchanged = await opts.auth.exchange(code, opts.callbackUrl)\n } else {\n legacyExchange = await opts.auth.exchange(code)\n exchanged = legacyExchange\n }\n } catch {\n log('[tangle-sso] exchange failed')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n if (exchanged.emailVerified !== true) {\n log('[tangle-sso] exchange did not include a verified-email proof')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({ email, platformUserId: exchanged.user.id })\n if (resolution.kind === 'reject') throw new TangleSsoAccountConflictError(resolution.reason)\n ;({ userId } = await opts.store.upsertUserByEmail({\n email,\n name: exchanged.user.name ?? null,\n tangleUserId: exchanged.user.id,\n resolution,\n }))\n } catch (err) {\n if (err instanceof TangleSsoAccountConflictError) {\n log('[tangle-sso] account linking conflict', err.reason)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\n const expiresAt = new Date(now() + sessionTtlSeconds * 1000)\n const { token } = await opts.store.createSession({\n userId,\n expiresAt,\n ipAddress: clientIp(request),\n userAgent: request.headers.get('user-agent'),\n })\n createdSession = token\n const link = {\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n }\n if (opts.protocol === 'oidc') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink({\n ...link,\n tokens: oidcTokens!,\n accessTokenExpiresAt: new Date(tokenRequestedAt + oidcTokens!.expiresIn! * 1000),\n })\n committed = true\n return response\n }\n if (opts.protocol === 'identity') {\n await opts.store.saveTangleLink(link)\n committed = true\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n }\n await opts.store.saveTangleLink({ ...link, apiKey: legacyExchange!.apiKey, planTier: legacyExchange!.plan?.tier ?? null })\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n } catch (error) {\n if (opts.protocol !== 'oidc' && opts.protocol !== 'identity') throw error\n log('[tangle-sso] local session persistence failed')\n return loginErrorRedirect('tangle_session_failed')\n } finally {\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity') && !committed) {\n if (createdSession) {\n try { await opts.store.deleteSession({ sessionToken: createdSession }) }\n catch { log('[tangle-sso] unpublished session cleanup failed') }\n }\n if (opts.protocol === 'oidc' && oidcTokens) {\n try { if (oidcTokens.refreshToken) await opts.auth.revoke(oidcTokens.refreshToken, 'refresh_token') }\n catch { log('[tangle-sso] rejected grant refresh revocation failed') }\n try { await opts.auth.revoke(oidcTokens.accessToken, 'access_token') }\n catch { log('[tangle-sso] rejected grant access revocation failed') }\n }\n }\n }\n },\n }\n}\n","/**\n * Request guards for agent-app routes: session auth (302 redirect for pages,\n * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to\n * non-admins), and the billable-balance gate (402 with a stable code).\n * Session resolution is a seam; thrown Responses follow the router convention\n * of surfacing a thrown Response as the route result.\n */\n\nimport { isTangleBillingEnforcementDisabled } from '../runtime/model'\n\n/** Define options for configuring authentication guard behavior and session retrieval */\nexport interface AuthGuardOptions<Session> {\n /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */\n getSession(request: Request): Promise<Session | null | undefined>\n /** Default '/login'. */\n loginPath?: string\n}\n\n/** Resolve user authentication and session requirements for page and API requests */\nexport interface AuthGuard<Session> {\n /** Page guard — throws a 302 redirect Response to `loginPath`. */\n requireUser(request: Request): Promise<Session>\n /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */\n requireApiUser(request: Request): Promise<Session>\n /** `apiResponse` selects the 401 JSON path over the redirect. */\n requireSession(request: Request, opts?: { apiResponse?: boolean }): Promise<Session>\n getOptionalSession(request: Request): Promise<Session | null>\n}\n\n/** Create an authentication guard that enforces session presence and handles unauthorized access responses */\nexport function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session> {\n const loginPath = opts.loginPath ?? '/login'\n\n async function requireSession(request: Request, o: { apiResponse?: boolean } = {}): Promise<Session> {\n const session = await opts.getSession(request)\n if (!session) {\n if (o.apiResponse) {\n throw Response.json({ error: 'Unauthorized', code: 'auth.unauthenticated' }, { status: 401 })\n }\n throw new Response(null, { status: 302, headers: { Location: loginPath } })\n }\n return session\n }\n\n return {\n requireSession,\n requireUser: (request) => requireSession(request),\n requireApiUser: (request) => requireSession(request, { apiResponse: true }),\n getOptionalSession: async (request) => (await opts.getSession(request)) ?? null,\n }\n}\n\n/** Resolve a value or an HTTP response indicating failure in a guarded operation */\nexport type GuardResolution<T> = { ok: true; value: T } | { ok: false; response: Response }\n\n/**\n * Adapt a guard that THROWS a Response (the quartet above — the router\n * convention) to the `{ok: true, value} | {ok: false, response}` resolution\n * shape the route factories take (`/chat-routes` `authorize`,\n * `/interactions` `resolveConnection`, `/chat-routes` upload `authorize`).\n * Every product wrote this try/catch by hand; it lives here once.\n */\nexport async function guardResolution<T>(run: () => Promise<T>): Promise<GuardResolution<T>> {\n try {\n return { ok: true, value: await run() }\n } catch (err) {\n if (err instanceof Response) return { ok: false, response: err }\n throw err\n }\n}\n\n/** Comma/whitespace separated → trimmed, lowercased, empties dropped. */\nexport function parseAdminEmails(raw: string | null | undefined): string[] {\n return (raw ?? '')\n .split(/[,\\s]+/)\n .map((e) => e.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** Define options to resolve user session and control access based on allowed admin emails */\nexport interface AdminGuardOptions<Session> {\n requireUser(request: Request): Promise<Session>\n emailOf(session: Session): string | null | undefined\n /** Resolved per request; an EMPTY allowlist refuses everyone. */\n allowedEmails(): string[]\n}\n\n/** Non-admins (and empty allowlists) get 404, keeping the route invisible —\n * better than a \"forbidden\" footprint that advertises its existence. */\nexport function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session> {\n return async (request) => {\n const session = await opts.requireUser(request)\n const allowed = opts.allowedEmails()\n if (allowed.length === 0) throw new Response('Not found', { status: 404 })\n const email = (opts.emailOf(session) ?? '').toLowerCase()\n if (!allowed.includes(email)) throw new Response('Not found', { status: 404 })\n return session\n }\n}\n\n/** Describe the billable balance state including overage permission and remaining USD balance */\nexport interface BillableBalanceState {\n overageAllowed: boolean\n remainingBalanceUsd: number\n}\n\n/** Define options to assert and customize billable balance enforcement behavior */\nexport interface AssertBillableBalanceOptions {\n env?: Record<string, string | undefined>\n /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),\n * fed to `isTangleBillingEnforcementDisabled`. */\n enforcementEnvVar?: string\n /** Default 'Add balance or upgrade your plan to invoke this agent.'. */\n errorMessage?: string\n /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */\n errorBody?: Record<string, unknown>\n}\n\n/**\n * Gate a billable turn: passes when enforcement is disabled (dev default),\n * the tier allows overage, or remaining balance is positive. Otherwise throws\n * a 402 Response with the stable `billing.balance_required` code so clients\n * can route to the billing screen.\n */\nexport function assertBillableBalance(state: BillableBalanceState, opts: AssertBillableBalanceOptions = {}): void {\n if (isTangleBillingEnforcementDisabled({ env: opts.env, enforcementEnvVar: opts.enforcementEnvVar })) return\n if (state.overageAllowed || state.remainingBalanceUsd > 0) return\n // errorBody first: the stable error/code contract always wins over caller extras.\n throw Response.json(\n {\n ...opts.errorBody,\n error: opts.errorMessage ?? 'Add balance or upgrade your plan to invoke this agent.',\n code: 'billing.balance_required',\n },\n { status: 402 },\n )\n}\n"],"mappings":"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","names":["headers","sessionCookies"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/web/rate-limit.ts","../src/web/free-route-limit.ts","../src/web/core.ts","../src/web/message-groups.ts","../src/web/api-key-fetch.ts"],"sourcesContent":["/**\n * The KV-backed sliding-window rate limiter. Lives in its own leaf so policy\n * layers (`./free-route-limit`) can build on it without importing the web\n * barrel and creating an import cycle. Re-exported from `./index`, which is the\n * published surface.\n */\n\n/** Minimal KV contract (Cloudflare `KVNamespace` satisfies it structurally). */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\n/** Describe the outcome of a rate limit check including allowance, remaining count, and reset time */\nexport interface RateLimitResult {\n allowed: boolean\n remaining: number\n resetAt: number\n}\n\n/** KV-backed sliding-window rate limit. Stores recent timestamps per key,\n * prunes the window, allows until `limit` is hit.\n *\n * Read-modify-write is best-effort, NOT atomic: KV has no compare-and-swap, so\n * two requests racing on the same key can each read the same pre-state and both\n * write — a concurrent burst can momentarily admit up to one extra request per\n * racing writer. This is acceptable for coarse abuse limiting; it is NOT a hard\n * quota gate.\n *\n * Fail-CLOSED on unreadable state: corrupt/non-array KV (a poisoned or\n * truncated value) is treated as a full window, so a tampered key cannot reset\n * the count and bypass the limiter. A bare `JSON.parse` here would throw and\n * abort the request handler, silently disabling the limit (fail-open). */\nexport async function checkRateLimit(kv: KvLike, key: string, limit: number, windowSeconds: number): Promise<RateLimitResult> {\n const now = Math.floor(Date.now() / 1000)\n const windowStart = now - windowSeconds\n const kvKey = `rl:${key}`\n const raw = await kv.get(kvKey)\n const parsed = parseRateLimitState(raw)\n if (parsed === POISONED_STATE) {\n // Unreadable state (parse threw or value is not a JSON array): deny rather\n // than reset the window to empty. A poisoned key must not become a bypass.\n return { allowed: false, remaining: 0, resetAt: now + windowSeconds }\n }\n const valid = parsed.filter((t) => t > windowStart)\n if (valid.length >= limit) return { allowed: false, remaining: 0, resetAt: (valid[0] ?? now) + windowSeconds }\n valid.push(now)\n await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 })\n return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds }\n}\n\n/** Sentinel returned by `parseRateLimitState` when the stored value cannot be\n * read as a timestamp array — distinct from an empty window so the limiter can\n * fail closed instead of treating corruption as a fresh window. */\nconst POISONED_STATE = Symbol('rate-limit-poisoned-state')\n\n/** Parse stored rate-limit state into a timestamp array. Absent state is a\n * fresh (empty) window. A value that fails to parse, or parses to a non-array,\n * returns `POISONED_STATE`; numeric junk inside a valid array is dropped. */\nfunction parseRateLimitState(raw: string | null): number[] | typeof POISONED_STATE {\n if (raw === null) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return POISONED_STATE\n }\n if (!Array.isArray(parsed)) return POISONED_STATE\n return parsed.filter((t): t is number => typeof t === 'number' && Number.isFinite(t))\n}\n","/**\n * Rate-limit policy for FREE routes: authenticated, unmetered, compute-bearing\n * endpoints.\n *\n * Billing meters the chat turn. Everything deterministic a product adds beside\n * it — a planning recompute fired on every slider drag, a redline diff, a\n * deadline computation — runs unmetered, so nothing bounds how much worker CPU\n * one authenticated caller can spend. `checkRateLimit` is the primitive; this\n * is the policy around it, so a route declares a cost class instead of each one\n * inventing a key format, a budget, and a denial response.\n *\n * Three rules this enforces that a hand-rolled call site usually does not:\n *\n * 1. **Identity is required.** A missing subject is denied, never pooled into a\n * shared bucket and never quietly keyed by IP — a shared bucket makes one\n * heavy user throttle everyone, and an IP key makes one NAT do the same.\n * 2. **A limiter that throws denies.** KV `get`/`put` failures are transport\n * failures; treating them as \"allowed\" converts a KV outage into an\n * unbounded-compute hole, which is exactly when the limiter matters most.\n * 3. **The denial is a typed, correctable outcome** — reason, HTTP status and\n * `Retry-After` — so the caller answers the client rather than failing\n * opaquely or, worse, continuing.\n *\n * ```ts\n * export const action = withFreeRouteLimit(\n * {\n * route: 'tax.plan.recompute',\n * costClass: 'interactive',\n * kv: ({ context }) => context.cloudflare.env.RATE_LIMIT_KV,\n * identify: async ({ request }) => {\n * const session = await requireSession(request)\n * return { subject: session.userId, workspace: session.workspaceId }\n * },\n * },\n * async ({ request }) => Response.json(recompute(await request.json())),\n * )\n * ```\n */\n\nimport { checkRateLimit, type KvLike, type RateLimitResult } from './rate-limit'\n\n/** Requests allowed per identity inside a sliding window. */\nexport interface RateLimitBudget {\n limit: number\n windowSeconds: number\n}\n\n/**\n * How expensive one call is, which is what picks the default budget.\n *\n * - `interactive` — a control the user drags or types into, recomputed per\n * tick. Sized for sustained two-per-second interaction, so a real slider\n * never trips it and a script still stops.\n * - `compute` — a derivation the user triggers deliberately (recalculate a\n * plan, diff a document, compute a deadline set). The default.\n * - `heavy` — whole-document or whole-matter work measured in seconds of CPU.\n */\nexport type FreeRouteClass = 'interactive' | 'compute' | 'heavy'\n\n/** Default budget per cost class. A product overrides with an explicit\n * `budget` when it has measured its own route. */\nexport const FREE_ROUTE_BUDGETS: Readonly<Record<FreeRouteClass, RateLimitBudget>> = Object.freeze({\n interactive: Object.freeze({ limit: 120, windowSeconds: 60 }),\n compute: Object.freeze({ limit: 30, windowSeconds: 60 }),\n heavy: Object.freeze({ limit: 10, windowSeconds: 300 }),\n})\n\n/** A workspace's shared ceiling defaults to this many times one member's, so a\n * multi-seat tenant is bounded without a single active member tripping it. */\nexport const WORKSPACE_BUDGET_MULTIPLIER = 5\n\n/** Why a call was refused. Every value is a REFUSAL — there is no reason code\n * that lets the handler run. */\nexport type FreeRouteDenialReason =\n /** No authenticated subject. Not correctable by waiting. */\n | 'unidentified'\n /** The window is full for this subject or workspace. */\n | 'rate-limited'\n /** The limiter itself failed (KV read/write threw). Denied, not passed. */\n | 'limiter-unavailable'\n\n/** Which window refused the call. `null` when no window was consulted. */\nexport type FreeRouteDimension = 'subject' | 'workspace' | null\n\n/**\n * A correctable refusal: the caller maps `status` + `retryAfterSeconds` onto a\n * response (see {@link freeRouteLimitResponse}) so the client learns what to do\n * next. Carrying the limiter's own failure in `cause` keeps a KV outage\n * diagnosable instead of collapsing into a generic 429.\n */\nexport class FreeRouteLimitError extends Error {\n readonly reason: FreeRouteDenialReason\n readonly status: 401 | 429 | 503\n readonly retryAfterSeconds: number\n readonly dimension: FreeRouteDimension\n readonly route: string\n\n constructor(init: {\n reason: FreeRouteDenialReason\n status: 401 | 429 | 503\n retryAfterSeconds: number\n dimension: FreeRouteDimension\n route: string\n message: string\n cause?: unknown\n }) {\n super(init.message, init.cause === undefined ? undefined : { cause: init.cause })\n this.name = 'FreeRouteLimitError'\n this.reason = init.reason\n this.status = init.status\n this.retryAfterSeconds = init.retryAfterSeconds\n this.dimension = init.dimension\n this.route = init.route\n }\n}\n\n/** What remains of the binding window after an allowed call. */\nexport interface FreeRouteAllowance {\n /** Remaining calls in whichever window is tightest. */\n remaining: number\n /** Unix seconds at which that window resets. */\n resetAt: number\n /** The budget that window enforces. */\n budget: RateLimitBudget\n /** Which window is tightest — `subject` unless a workspace ceiling binds. */\n dimension: Exclude<FreeRouteDimension, null>\n}\n\n/** Typed outcome at the route boundary: an allowance, or a refusal that says\n * how to correct it. There is no third state and no silent pass. */\nexport type FreeRouteLimitOutcome =\n | { succeeded: true; value: FreeRouteAllowance }\n | { succeeded: false; error: FreeRouteLimitError }\n\nexport interface FreeRouteLimitInput {\n kv: KvLike\n /** Stable route name. Part of the key, so two routes never share a window. */\n route: string\n /** The authenticated subject (user id). Absent or blank ⇒ denied. */\n subject: string | null | undefined\n /** Tenant the call belongs to. When present, a second, wider window bounds\n * the workspace as a whole. */\n workspace?: string | null\n /** Cost class picking the default budget. Ignored when `budget` is given.\n * Default `'compute'`. */\n costClass?: FreeRouteClass\n budget?: RateLimitBudget\n /** Workspace ceiling. Defaults to the subject budget's limit multiplied by\n * {@link WORKSPACE_BUDGET_MULTIPLIER} over the same window. */\n workspaceBudget?: RateLimitBudget\n}\n\n/** Key components are percent-encoded so a route name or subject containing the\n * separator cannot be crafted to collide with another caller's window. */\nfunction limitKey(route: string, dimension: string, id: string): string {\n return `free:${encodeURIComponent(route)}:${dimension}:${encodeURIComponent(id)}`\n}\n\nfunction assertBudget(budget: RateLimitBudget, label: string): void {\n const valid =\n Number.isInteger(budget.limit) &&\n budget.limit >= 1 &&\n Number.isInteger(budget.windowSeconds) &&\n budget.windowSeconds >= 1\n if (!valid) {\n throw new Error(\n `${label} must be positive integers (got limit=${budget.limit}, windowSeconds=${budget.windowSeconds})`,\n )\n }\n}\n\nfunction secondsUntil(resetAt: number): number {\n return Math.max(1, resetAt - Math.floor(Date.now() / 1000))\n}\n\n/**\n * Run one window. A throw from KV becomes a `limiter-unavailable` refusal\n * rather than propagating — the caller must be able to answer 503 with a\n * `Retry-After` instead of the handler running or the route 500ing opaquely.\n */\nasync function runWindow(\n kv: KvLike,\n route: string,\n dimension: Exclude<FreeRouteDimension, null>,\n id: string,\n budget: RateLimitBudget,\n): Promise<{ succeeded: true; value: RateLimitResult } | { succeeded: false; error: FreeRouteLimitError }> {\n try {\n const value = await checkRateLimit(kv, limitKey(route, dimension, id), budget.limit, budget.windowSeconds)\n return { succeeded: true, value }\n } catch (cause) {\n return {\n succeeded: false,\n error: new FreeRouteLimitError({\n reason: 'limiter-unavailable',\n status: 503,\n retryAfterSeconds: Math.min(budget.windowSeconds, 30),\n dimension,\n route,\n message: `Rate limiter unavailable for route '${route}' — the request is refused rather than admitted unmetered`,\n cause,\n }),\n }\n }\n}\n\n/**\n * Decide whether one authenticated, unmetered call may run.\n *\n * Checks the subject's window first, then the workspace ceiling when a\n * workspace is supplied; the tighter of the two is what the allowance reports.\n * Every failure path returns a refusal — none returns an allowance.\n */\nexport async function checkFreeRouteLimit(input: FreeRouteLimitInput): Promise<FreeRouteLimitOutcome> {\n const budget = input.budget ?? FREE_ROUTE_BUDGETS[input.costClass ?? 'compute']\n assertBudget(budget, `Free-route budget for '${input.route}'`)\n const workspaceBudget =\n input.workspaceBudget ?? { limit: budget.limit * WORKSPACE_BUDGET_MULTIPLIER, windowSeconds: budget.windowSeconds }\n if (input.workspace) assertBudget(workspaceBudget, `Free-route workspace budget for '${input.route}'`)\n\n const subject = input.subject?.trim()\n if (!subject) {\n return {\n succeeded: false,\n error: new FreeRouteLimitError({\n reason: 'unidentified',\n status: 401,\n // Not correctable by waiting: the caller must authenticate.\n retryAfterSeconds: 0,\n dimension: null,\n route: input.route,\n message: `Route '${input.route}' is authenticated-only — an unidentified caller cannot be rate limited, so it is refused`,\n }),\n }\n }\n\n const subjectWindow = await runWindow(input.kv, input.route, 'subject', subject, budget)\n if (!subjectWindow.succeeded) return subjectWindow\n if (!subjectWindow.value.allowed) {\n return { succeeded: false, error: rateLimited(input.route, 'subject', subjectWindow.value) }\n }\n\n let tightest: { result: RateLimitResult; budget: RateLimitBudget; dimension: Exclude<FreeRouteDimension, null> } = {\n result: subjectWindow.value,\n budget,\n dimension: 'subject',\n }\n\n if (input.workspace) {\n const workspaceWindow = await runWindow(input.kv, input.route, 'workspace', input.workspace, workspaceBudget)\n if (!workspaceWindow.succeeded) return workspaceWindow\n if (!workspaceWindow.value.allowed) {\n return { succeeded: false, error: rateLimited(input.route, 'workspace', workspaceWindow.value) }\n }\n if (workspaceWindow.value.remaining < tightest.result.remaining) {\n tightest = { result: workspaceWindow.value, budget: workspaceBudget, dimension: 'workspace' }\n }\n }\n\n return {\n succeeded: true,\n value: {\n remaining: tightest.result.remaining,\n resetAt: tightest.result.resetAt,\n budget: tightest.budget,\n dimension: tightest.dimension,\n },\n }\n}\n\nfunction rateLimited(\n route: string,\n dimension: Exclude<FreeRouteDimension, null>,\n result: RateLimitResult,\n): FreeRouteLimitError {\n const retryAfterSeconds = secondsUntil(result.resetAt)\n return new FreeRouteLimitError({\n reason: 'rate-limited',\n status: 429,\n retryAfterSeconds,\n dimension,\n route,\n message: `Route '${route}' rate limit reached for this ${dimension}; retry in ${retryAfterSeconds}s`,\n })\n}\n\nexport interface FreeRouteLimitResponseOptions {\n /** Extra response headers (a product's security headers, CORS). */\n headers?: Record<string, string>\n /** Client-facing message. Defaults to the error's own message. */\n message?: string\n}\n\n/** The refusal as an HTTP response: the error's status, a JSON body carrying\n * the machine-readable `reason`, and `Retry-After` when waiting can fix it. */\nexport function freeRouteLimitResponse(\n error: FreeRouteLimitError,\n options: FreeRouteLimitResponseOptions = {},\n): Response {\n const headers = new Headers(options.headers)\n headers.set('Content-Type', 'application/json')\n if (error.retryAfterSeconds > 0) headers.set('Retry-After', String(error.retryAfterSeconds))\n return new Response(\n JSON.stringify({\n error: options.message ?? error.message,\n reason: error.reason,\n retryAfterSeconds: error.retryAfterSeconds,\n }),\n { status: error.status, headers },\n )\n}\n\n/** The authenticated caller behind one request. */\nexport interface FreeRouteIdentity {\n subject: string\n workspace?: string | null\n}\n\nexport interface WithFreeRouteLimitOptions<TArgs> {\n /** Stable route name; part of the key. */\n route: string\n costClass?: FreeRouteClass\n budget?: RateLimitBudget\n workspaceBudget?: RateLimitBudget\n /** The KV binding for this request. */\n kv: (args: TArgs) => KvLike\n /**\n * Resolve the authenticated caller. Return `null` for an unauthenticated\n * request — the wrapper then refuses with 401 and the handler never runs.\n *\n * A THROW here propagates: an auth lookup that failed is not the same as an\n * absent session, and the wrapper will not invent a status for it. The\n * handler still does not run, so the failure stays closed.\n */\n identify: (args: TArgs) => Promise<FreeRouteIdentity | null> | FreeRouteIdentity | null\n /** Build the refusal response. Defaults to {@link freeRouteLimitResponse}. */\n onDenied?: (error: FreeRouteLimitError, args: TArgs) => Response | Promise<Response>\n}\n\n/**\n * Wrap an authenticated, unmetered, compute-bearing route handler in its\n * budget. The limit is applied BEFORE the handler is entered — including\n * before the request body is read — so a refused call costs a KV read, not the\n * compute the route exists to do.\n *\n * The handler's own response is returned untouched: this never rewrites status,\n * body or headers on the success path, so it cannot break a streaming or\n * pre-built response.\n *\n * `TArgs` is the router's handler-argument object (react-router's\n * `ActionFunctionArgs`, or any single-object shape a product uses).\n */\nexport function withFreeRouteLimit<TArgs>(\n options: WithFreeRouteLimitOptions<TArgs>,\n handler: (args: TArgs) => Response | Promise<Response>,\n): (args: TArgs) => Promise<Response> {\n return async (args: TArgs): Promise<Response> => {\n const identity = await options.identify(args)\n const outcome = await checkFreeRouteLimit({\n kv: options.kv(args),\n route: options.route,\n subject: identity?.subject,\n workspace: identity?.workspace,\n costClass: options.costClass,\n budget: options.budget,\n workspaceBudget: options.workspaceBudget,\n })\n if (!outcome.succeeded) {\n return options.onDenied ? await options.onDenied(outcome.error, args) : freeRouteLimitResponse(outcome.error)\n }\n return await handler(args)\n }\n}\n","/**\n * Web-boundary utilities every agent app's routes hand-roll: JSON body parsing\n * + narrowing, request-context extraction (real client IP behind Cloudflare),\n * a KV-backed sliding-window rate limiter, the free-route budget policy built\n * on it, and security response headers. Pure mechanism — no DB, no domain. The\n * KV is a structural interface so this needs no `@cloudflare/workers-types`\n * dependency.\n */\n\nexport * from './rate-limit'\nexport * from './free-route-limit'\n\nexport type JsonObject = Record<string, unknown>\n\n/** Parse + object-narrow a Request body. `[body, null]` on success, `[null,\n * errorResponse]` on a non-object body (callers `if (err) return err`). */\nexport async function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]> {\n let raw: unknown\n try {\n raw = await request.json()\n } catch {\n return [null, Response.json({ error: 'Invalid JSON body' }, { status: 400 })]\n }\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n return [null, Response.json({ error: 'Body must be a JSON object' }, { status: 400 })]\n }\n return [raw as JsonObject, null]\n}\n\n/** Narrow one required string field, 400 if missing/empty. */\nexport function requireString(body: JsonObject, field: string): string | Response {\n const v = body[field]\n if (typeof v !== 'string' || v.length === 0) {\n return Response.json({ error: `Missing or non-string field: ${field}` }, { status: 400 })\n }\n return v\n}\n\n/** Define the context of a request including IP address, user agent, timestamp, and request ID */\nexport interface RequestContext {\n ipAddress: string\n userAgent: string\n timestamp: string\n requestId: string\n}\n\n/** Extract request context for audit trails. Uses `CF-Connecting-IP` for the\n * real client IP behind Cloudflare. */\nexport function extractRequestContext(request: Request): RequestContext {\n const ipAddress =\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() ??\n '0.0.0.0'\n return {\n ipAddress,\n userAgent: request.headers.get('User-Agent') ?? '',\n timestamp: new Date().toISOString(),\n requestId: crypto.randomUUID(),\n }\n}\n\n/** Define options for configuring cookie attributes and behavior */\nexport interface CookieOptions {\n name: string\n /** Default '/'. */\n path?: string\n /** Default true. */\n httpOnly?: boolean\n /** Adds the `Secure` attribute. Default false. */\n secure?: boolean\n /** Default 'Lax'. */\n sameSite?: 'Lax' | 'Strict' | 'None'\n maxAgeSeconds?: number\n}\n\n/** Serialize a Set-Cookie header value: `name=encodeURIComponent(value)` plus\n * attributes in Path / HttpOnly / SameSite / Max-Age / Secure order.\n * Throws on `SameSite=None` without `secure` — browsers silently drop that\n * combination, which would otherwise fail invisibly. */\nexport function serializeCookie(value: string, opts: CookieOptions): string {\n if (opts.sameSite === 'None' && !opts.secure) {\n throw new Error('SameSite=None cookies require secure: true (browsers reject them otherwise)')\n }\n const parts = [`${opts.name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? '/'}`]\n if (opts.httpOnly !== false) parts.push('HttpOnly')\n parts.push(`SameSite=${opts.sameSite ?? 'Lax'}`)\n if (opts.maxAgeSeconds !== undefined) parts.push(`Max-Age=${opts.maxAgeSeconds}`)\n if (opts.secure) parts.push('Secure')\n return parts.join('; ')\n}\n\n/** Set-Cookie header value that deletes the cookie (empty value, Max-Age=0). */\nexport function clearCookieHeader(opts: Omit<CookieOptions, 'maxAgeSeconds'>): string {\n return serializeCookie('', { ...opts, maxAgeSeconds: 0 })\n}\n\n/** Read + decode one cookie from a Cookie request header; null when absent. */\nexport function readCookieValue(cookieHeader: string | null, name: string): string | null {\n if (!cookieHeader) return null\n for (const part of cookieHeader.split(/;\\s*/)) {\n const [cookieName, ...rest] = part.split('=')\n if (cookieName === name) {\n try {\n return decodeURIComponent(rest.join('='))\n } catch {\n return null\n }\n }\n }\n return null\n}\n\n/** Define options for configuring security-related HTTP headers including disclaimers and retention labels */\nexport interface SecurityHeaderOptions {\n /** Product disclaimer (e.g. \"AI-powered tool. Not legal advice.\"). Omitted if absent. */\n disclaimer?: string\n /** Data-retention label (e.g. \"7-years\"). Omitted if absent. */\n retention?: string\n /** Extra headers to set. */\n extra?: Record<string, string>\n}\n\n/** Canonical generic response headers used by {@link addSecurityHeaders}.\n * Exported so static-asset hosts can apply the same policy without copying\n * values that silently drift from Worker/API responses. */\nexport const STANDARD_SECURITY_HEADERS = Object.freeze({\n 'Strict-Transport-Security':\n 'max-age=31536000; includeSubDomains; preload',\n 'X-Content-Type-Options': 'nosniff',\n 'X-Frame-Options': 'SAMEORIGIN',\n 'Referrer-Policy': 'same-origin',\n 'X-XSS-Protection': '1; mode=block',\n} as const)\n\n/** Set standard security headers on a response (HSTS, nosniff, frame-options,\n * referrer-policy, XSS) + optional product disclaimer/retention. The security\n * set is generic; the disclaimer/retention are the product's. */\nexport function addSecurityHeaders(response: Response, opts: SecurityHeaderOptions = {}): Response {\n for (const [name, value] of Object.entries(STANDARD_SECURITY_HEADERS)) {\n response.headers.set(name, value)\n }\n if (opts.disclaimer) response.headers.set('X-AI-Disclaimer', opts.disclaimer)\n if (opts.retention) response.headers.set('X-Data-Retention', opts.retention)\n for (const [k, v] of Object.entries(opts.extra ?? {})) response.headers.set(k, v)\n return response\n}\n\n/** Local-sandbox / inline schemes a stored media reference must never use.\n * Reachable from neither a browser nor the product worker, and a `file:`/`data:`\n * url is the tell of an agent substituting local ffmpeg output for a real\n * provider artifact. `blob:` and `javascript:` are inert/active client schemes\n * with no server reachability. */\nconst REJECTED_MEDIA_SCHEMES = ['file:', 'data:', 'blob:', 'javascript:', 'vbscript:'] as const\n\n/**\n * Canonical media-reference boundary shared by every surface that persists a\n * media url (sequences clips, design-canvas image/video src). The ONE rule:\n * remote `http(s)` or a rooted `/api/` path are allowed; everything else is\n * rejected, with a named reason for known-bad local/inline schemes so the\n * thrown message is actionable for an LLM planner. The url is trimmed before\n * the scheme check so leading whitespace cannot smuggle a rejected scheme past\n * a naive `startsWith`.\n *\n * @param what - noun for the error message (e.g. 'media url', 'src').\n */\nexport function assertMediaUrl(url: string, what = 'media url'): void {\n const trimmed = url.trim()\n if (/^https?:\\/\\//i.test(trimmed)) return\n if (trimmed.startsWith('/api/')) return\n const shown = trimmed.length > 96 ? `${trimmed.slice(0, 96)}…` : trimmed\n const lower = trimmed.toLowerCase()\n if (\n REJECTED_MEDIA_SCHEMES.some((scheme) => lower.startsWith(scheme)) ||\n lower.startsWith('/tmp/') ||\n lower.startsWith('/home/')\n ) {\n throw new Error(`${what} must reference a provider http(s) URL or a rooted /api/ path, not a local sandbox file (${shown})`)\n }\n throw new Error(`${what} must be http(s) or a rooted /api/ path (${shown})`)\n}\n\nexport { isWorkspaceFileExportable } from './file-export'\n","/** Renderer-neutral attribution. Not a transcript merger or an execution state machine. */\nexport interface ConversationGroupItem {\n id?: string | number\n kind?: string\n role?: string\n /** A different assistant/persona must never inherit the previous speaker's label. */\n speakerId?: string\n /** Distinct threads must not be grouped if their rows share a viewport. */\n conversationId?: string\n}\nexport type GroupedConversationItem<T> = T & { isContinuation?: boolean; groupId?: string }\n\n/**\n * Annotate assistant rows until an actual user message, speaker or conversation change.\n * Tool/progress notices do not interrupt the group. The first visible assistant is\n * always labeled, including when a renderer has paged earlier history away.\n * Pass display order; stored content, IDs, timestamps and input objects are untouched.\n */\nexport function groupConversationMessages<T extends ConversationGroupItem>(items: readonly T[] = []): GroupedConversationItem<T>[] {\n let speaker: string | null = null\n let conversation: string | undefined\n let groupId: string | undefined\n return items.map((item, index) => {\n if (item.conversationId !== undefined && item.conversationId !== conversation) {\n conversation = item.conversationId\n speaker = null\n groupId = undefined\n }\n const role = item.kind === 'thinking' ? 'assistant' : (!item.kind || item.kind === 'message' ? item.role : undefined)\n if (role === 'user') { speaker = null; groupId = undefined; return { ...item, isContinuation: false } }\n if (role !== 'assistant') return item\n const current = item.speakerId ?? 'assistant'\n const isContinuation = speaker === current\n if (!isContinuation) groupId = String(item.id ?? `assistant-${index}`)\n speaker = current\n return { ...item, isContinuation, groupId }\n })\n}\n","/** A credential-bearing client for one application origin, not a web fetch tool. */\nexport interface ApiKeyFetchOptions {\n origin: string\n /** Resolve from trusted client secret storage; never from an agent's arguments. */\n getApiKey(): string | Promise<string>\n fetchImpl?: typeof fetch\n /** Explicit local-development opt-in; never permits remote plaintext origins. */\n allowHttpLoopback?: boolean\n}\n\nexport type ApiKeyFetch = (path: string, init?: RequestInit) => Promise<Response>\n\n/** No redirects, cookie fallback, hidden retries, or model-selected credential destination. */\nexport function createApiKeyFetch(options: ApiKeyFetchOptions): ApiKeyFetch {\n const origin = new URL(options.origin)\n const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname)\n if (origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash\n || (origin.protocol !== 'https:' && !(origin.protocol === 'http:' && loopback && options.allowHttpLoopback))) {\n throw new TypeError('API origin must be HTTPS without credentials, path, query, or fragment')\n }\n const fetchImpl = options.fetchImpl ?? globalThis.fetch\n if (typeof fetchImpl !== 'function') throw new TypeError('A fetch implementation is required')\n const getApiKey = options.getApiKey\n\n return async (path, init = {}) => {\n // Reject URL-parser repairs and authority-relative references before resolving secrets.\n if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//')\n || /[\\\\\\u0000-\\u0020\\u007f]/.test(path)) {\n throw new TypeError('API requests require a root-relative path without whitespace or backslashes')\n }\n const target = new URL(path, origin)\n if (target.origin !== origin.origin || target.username || target.password || target.hash) {\n throw new TypeError('API requests must stay on the configured origin without a fragment')\n }\n const headers = new Headers(init.headers)\n if (headers.has('authorization') || headers.has('cookie') || headers.has('proxy-authorization')) {\n throw new TypeError('Authentication headers must come from the configured secret resolver')\n }\n init.signal?.throwIfAborted()\n let apiKey: string\n try {\n apiKey = await getApiKey()\n } catch {\n throw new Error('API credential is unavailable')\n }\n if (typeof apiKey !== 'string' || !apiKey || /\\s|[^\\x21-\\x7e]/.test(apiKey)) {\n throw new TypeError('API credential must be a nonempty printable token without whitespace')\n }\n init.signal?.throwIfAborted()\n headers.set('Authorization', `Bearer ${apiKey}`)\n let response: Response\n try {\n response = await fetchImpl(target.href, {\n ...init, headers, credentials: 'omit', redirect: 'manual', referrerPolicy: 'no-referrer',\n })\n } catch {\n // A failed transport does not establish that an external action did not run.\n // Do not forward an adapter's diagnostic, which could include the bearer.\n init.signal?.throwIfAborted()\n throw new Error('API transport failed; inspect retained state before retrying a write')\n }\n if (response.type === 'opaqueredirect' || response.redirected\n || (response.status >= 300 && response.status < 400 && response.status !== 304)) {\n try { await response.body?.cancel() } catch { /* Preserve the redirect refusal. */ }\n throw new Error('API redirects are not allowed')\n }\n return response\n }\n}\n"],"mappings":"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","names":[]}