@tangle-network/agent-app 0.60.170 → 0.60.172

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.
@@ -2,7 +2,7 @@ import {
2
2
  createAuthGuard,
3
3
  createBetterAuthSessionCookieMinter,
4
4
  createTangleSsoHandlers
5
- } from "../chunk-WN3UK23P.js";
5
+ } from "../chunk-OGLTKK72.js";
6
6
  import {
7
7
  noticeEmail
8
8
  } from "../chunk-WOTWQ2ST.js";
@@ -8,15 +8,15 @@ import "../chunk-FBVLEGEG.js";
8
8
  import "../chunk-CW6F6GDP.js";
9
9
  import "../chunk-ILZQAXJH.js";
10
10
  import "../chunk-E4AY62T2.js";
11
- import "../chunk-CCTQQ45L.js";
11
+ import "../chunk-CRHXK3QT.js";
12
12
  import "../chunk-Y6PRDUFJ.js";
13
13
  import "../chunk-GEYACSFW.js";
14
14
  import {
15
15
  ChatComposer
16
- } from "../chunk-ZEMO7S2G.js";
16
+ } from "../chunk-PZHZV6QY.js";
17
17
  import "../chunk-L7MB2LLM.js";
18
18
  import "../chunk-SJWIZT7B.js";
19
- import "../chunk-XSPUKVHF.js";
19
+ import "../chunk-KICTB6RK.js";
20
20
  import "../chunk-R5K5ML3Q.js";
21
21
  import {
22
22
  ModelPicker
@@ -27,7 +27,7 @@ import {
27
27
  } from "../chunk-LSUELMHY.js";
28
28
  import "../chunk-FQYAJYPW.js";
29
29
  import "../chunk-WTC6AOPL.js";
30
- import "../chunk-WVESWAJJ.js";
30
+ import "../chunk-MXNVSY6J.js";
31
31
  import "../chunk-B7CL7ENS.js";
32
32
  import "../chunk-PV33JZKV.js";
33
33
  import "../chunk-AMUXRAUO.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  ComposerModeControls,
3
3
  EntryComposer
4
- } from "../chunk-ZEMO7S2G.js";
4
+ } from "../chunk-PZHZV6QY.js";
5
5
  import "../chunk-L7MB2LLM.js";
6
6
  import "../chunk-SJWIZT7B.js";
7
- import "../chunk-XSPUKVHF.js";
7
+ import "../chunk-KICTB6RK.js";
8
8
  import "../chunk-R5K5ML3Q.js";
9
9
  import "../chunk-AA4FKEQW.js";
10
10
  import "../chunk-RASGKEAL.js";
11
11
  import "../chunk-WTC6AOPL.js";
12
- import "../chunk-WVESWAJJ.js";
12
+ import "../chunk-MXNVSY6J.js";
13
13
  import "../chunk-AMUXRAUO.js";
14
14
  import "../chunk-YHRWZZJN.js";
15
15
  import "../chunk-TXD5HXLE.js";
@@ -42,9 +42,9 @@ import {
42
42
  flattenHistory,
43
43
  readSandboxBinaryBytes,
44
44
  statSandboxFileSize
45
- } from "../chunk-4DZVSTET.js";
45
+ } from "../chunk-RCRC2JKP.js";
46
46
  import "../chunk-4LBCTFBP.js";
47
- import "../chunk-WVESWAJJ.js";
47
+ import "../chunk-MXNVSY6J.js";
48
48
  import "../chunk-LWSJK546.js";
49
49
  import "../chunk-74WXGIEK.js";
50
50
  import "../chunk-6A7MYOUI.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  HarnessPicker,
3
3
  filterAcceptedFiles
4
- } from "./chunk-XSPUKVHF.js";
4
+ } from "./chunk-KICTB6RK.js";
5
5
  import {
6
6
  EffortPicker,
7
7
  ModelPicker,
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  isModelCompatibleWithHarness,
15
15
  snapModelToHarness
16
- } from "./chunk-WVESWAJJ.js";
16
+ } from "./chunk-MXNVSY6J.js";
17
17
  import {
18
18
  isCommitSha,
19
19
  parseGitHubLocation,
@@ -2214,4 +2214,4 @@ export {
2214
2214
  AGENT_PROFILE_EDITOR_SECTIONS,
2215
2215
  AgentProfileEditor
2216
2216
  };
2217
- //# sourceMappingURL=chunk-CCTQQ45L.js.map
2217
+ //# sourceMappingURL=chunk-CRHXK3QT.js.map
@@ -15,7 +15,7 @@ import {
15
15
  import {
16
16
  snapHarnessToModel,
17
17
  snapModelToHarness
18
- } from "./chunk-WVESWAJJ.js";
18
+ } from "./chunk-MXNVSY6J.js";
19
19
 
20
20
  // src/web-react/composer-file-accept.ts
21
21
  function isAcceptedFileType(file, accept) {
@@ -576,4 +576,4 @@ export {
576
576
  HarnessPicker,
577
577
  AgentSessionControls
578
578
  };
579
- //# sourceMappingURL=chunk-XSPUKVHF.js.map
579
+ //# sourceMappingURL=chunk-KICTB6RK.js.map
@@ -83,6 +83,31 @@ function assertHarnessModelCompatible(harness, selection) {
83
83
  );
84
84
  }
85
85
  }
86
+ var HARNESS_CAPABILITIES = {
87
+ "claude-code": { subagents: true, mcp: "native", toolControls: "runtime", skillsDir: ".claude/skills" },
88
+ nanoclaw: { subagents: true, mcp: "native", toolControls: "runtime", skillsDir: ".claude/skills" },
89
+ opencode: { subagents: true, mcp: "config-file", toolControls: "runtime", skillsDir: ".opencode/skills" },
90
+ codex: { subagents: true, mcp: "config-file", toolControls: "runtime", skillsDir: ".codex/skills" },
91
+ "kimi-code": { subagents: false, mcp: "config-file", toolControls: "runtime", skillsDir: ".kimi/skills" },
92
+ pi: { subagents: false, mcp: "extension", toolControls: "startup", skillsDir: ".pi/skills" },
93
+ prime: { subagents: true, mcp: "native", toolControls: "runtime", skillsDir: ".prime/agent/skills" },
94
+ hermes: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
95
+ openclaw: { subagents: false, mcp: "config-file", toolControls: "runtime", skillsDir: "skills" },
96
+ // Harnesses the platform placement map does not bridge yet: conservative
97
+ // facts — assert nothing they have not proven. Each upgrades to its real
98
+ // facts when the materializer grows support for it.
99
+ acp: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
100
+ amp: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
101
+ "factory-droids": { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
102
+ forge: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
103
+ cursor: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
104
+ "cli-base": { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null }
105
+ };
106
+ function harnessCapabilities(harness) {
107
+ const caps = HARNESS_CAPABILITIES[harness];
108
+ if (!caps) throw new Error(`harnessCapabilities: no capability facts for "${harness}"`);
109
+ return caps;
110
+ }
86
111
 
87
112
  export {
88
113
  modelProvider,
@@ -95,6 +120,7 @@ export {
95
120
  isModelCompatibleWithHarness,
96
121
  snapModelToHarness,
97
122
  snapHarnessToModel,
98
- assertHarnessModelCompatible
123
+ assertHarnessModelCompatible,
124
+ harnessCapabilities
99
125
  };
100
- //# sourceMappingURL=chunk-WVESWAJJ.js.map
126
+ //# sourceMappingURL=chunk-MXNVSY6J.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/harness/index.ts"],"sourcesContent":["/**\n * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.\n *\n * A \"harness\" is the coding-agent CLI a sandbox drives (opencode / codex /\n * claude-code / …). The shell governs WHICH harness a chat session uses and\n * enforces that a session is LOCKED to the harness it started with — the model\n * may change mid-session, the harness may not (swapping it mid-session would\n * orphan the session's running agent state). Every product otherwise hand-rolls\n * this and hard-codes a single harness; this is the one place the rule lives.\n *\n * Substrate-free: the harness list is derived from `@tangle-network/agent-interface`'s canonical\n * harness enum and mirrors the sandbox SDK's `BackendType` (no sandbox dependency). The consumer\n * owns storage — which harness a workspace defaults to, which one a session locked — and maps the\n * resolved value onto the SDK's `backend.type`.\n *\n * Harness↔model COMPATIBILITY (which models a harness can run, snapping) is NOT defined here — it\n * comes from `@tangle-network/agent-interface`, the single source of truth shared with the\n * sandbox-ui pickers and the cli-bridge backends. This module owns the harness TAXONOMY + the\n * session lock, and its server assertion also checks resolved transport-provider evidence.\n */\n\nimport {\n harnessProviders,\n harnessSupportsModel,\n harnessTypeSchema,\n modelProvider,\n preferredHarnessForModel,\n snapHarnessToModel as aiSnapHarnessToModel,\n snapModelToHarness as aiSnapModelToHarness,\n type HarnessType,\n} from '@tangle-network/agent-interface'\n\n/**\n * Canonical harnesses that are NOT sandbox backends. Each value in `KNOWN_HARNESSES` is dispatched\n * as `backend.type`, so a harness the platform ships no provider adapter for must stay out —\n * offering it would resolve a session onto a runner that cannot start.\n */\nconst NON_BACKEND_HARNESSES = ['gemini'] as const satisfies readonly HarnessType[]\n\nconst nonBackendHarnesses = new Set<string>(NON_BACKEND_HARNESSES)\n\n/** A coding-agent backend this shell can dispatch a session onto. */\nexport type Harness = Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]>\n\n/**\n * The known coding-agent backends: the canonical harness set minus what has no provider adapter,\n * Derived from `harnessTypeSchema.options` so a harness added upstream reaches this shell without\n * an edit here, and kept structural so this module still needs no sandbox dependency.\n */\nexport const KNOWN_HARNESSES: readonly Harness[] = [\n ...harnessTypeSchema.options.filter(\n (harness): harness is Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]> =>\n !nonBackendHarnesses.has(harness),\n ),\n]\n\n/** Define the default harness to use for code execution and testing environments */\nexport const DEFAULT_HARNESS: Harness = 'opencode'\n\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\n/** Determine if a value is a recognized harness string identifier */\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\n/** Resolve input options to determine the appropriate session harness to use */\nexport interface ResolveSessionHarnessInput {\n /** The harness already locked to this session (recorded at its first turn). */\n sessionHarness?: unknown\n /** The harness requested now — a new session's choice, or a turn's attempt to switch. */\n requested?: unknown\n /** The workspace's default harness, used only when starting a fresh session. */\n workspaceDefault?: unknown\n /** Final fallback when nothing else resolves (default `opencode`). */\n fallback?: Harness\n}\n\n/** Represent resolved session state including harness, lock status, and swap attempt flag */\nexport interface ResolvedSessionHarness {\n /** The harness to actually run — the locked one when the session already has it. */\n harness: Harness\n /** True when the session already had a locked harness (this turn did not pick it). */\n locked: boolean\n /** True when `requested` differs from the locked harness — a forbidden mid-session\n * swap the caller should reject or warn on. The lock always wins regardless. */\n swapAttempted: boolean\n}\n\n/**\n * Resolve the harness for a turn, enforcing the session lock.\n *\n * - **Session already started** (`sessionHarness` is a known harness): that harness\n * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller\n * can reject the swap. The model is a separate per-turn concern and is unaffected.\n * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller\n * persists the result as the session's lock for every subsequent turn.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n const fallback = input.fallback ?? DEFAULT_HARNESS\n if (isHarness(input.sessionHarness)) {\n const locked = input.sessionHarness\n const swapAttempted = isHarness(input.requested) && input.requested !== locked\n return { harness: locked, locked: true, swapAttempted }\n }\n const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback))\n return { harness, locked: false, swapAttempted: false }\n}\n\n/**\n * An app's harness configuration, read from its deploy configuration (for example the env vars\n * `AGENT_HARNESS` and `AGENT_HARNESS_WORKSPACES`), so no product hard-codes `backend.type`.\n */\nexport interface HarnessConfig {\n /** The app default for new sessions. Unset or blank means {@link DEFAULT_HARNESS}. */\n default?: string\n /** Per-workspace choices: a JSON object mapping a workspace id to a harness. */\n workspaces?: string\n}\n\nfunction configHarness(value: unknown, where: string): Harness {\n if (!isHarness(value)) throw new Error(`${where} names an unknown harness: ${String(value)}`)\n return value\n}\n\n/**\n * The harness a workspace's new sessions start on: its own choice, else the app default, else\n * {@link DEFAULT_HARNESS}. Pass the result to {@link resolveSessionHarness} as `workspaceDefault`\n * so a session that already ran keeps its harness. A value naming an unknown harness throws:\n * a configuration typo must fail loudly, never run a different harness than the one configured.\n */\nexport function configuredHarness(config: HarnessConfig, workspaceId?: string): Harness {\n const appDefault = config.default?.trim()\n ? configHarness(config.default.trim(), 'The harness default') : DEFAULT_HARNESS\n const raw = config.workspaces?.trim()\n if (!raw) return appDefault\n let choices: unknown\n try {\n choices = JSON.parse(raw)\n } catch {\n throw new Error('The per-workspace harness configuration is not JSON')\n }\n if (!choices || typeof choices !== 'object' || Array.isArray(choices)) {\n throw new Error('The per-workspace harness configuration must map workspace ids to harnesses')\n }\n // Every entry is checked, not only the caller's, so one bad entry fails every workspace's\n // first turn after the deploy instead of hiding until that workspace sends a message.\n const parsed = new Map(Object.entries(choices).map(([id, value]) =>\n [id, configHarness(value, `The harness for workspace ${id}`)]))\n return (workspaceId === undefined ? undefined : parsed.get(workspaceId)) ?? appDefault\n}\n\n/**\n * Harness ↔ model compatibility + snapping — delegated to `@tangle-network/agent-interface`.\n *\n * `Harness` is the dispatchable subset of `HarnessType`: this shell drops only canonical harnesses\n * for which the platform has no backend. Compatibility therefore comes entirely from Interface;\n * this package keeps no second harness taxonomy.\n */\n\nexport { modelProvider }\n\n/** A resolved model and the transport provider that will execute it. */\nexport interface HarnessModelSelection {\n model: string\n provider?: string\n}\n\n/** Provider-less ids (sentinels like \"default\", or a session's own config) are\n * compatible everywhere — every harness honors its own configuration. */\nexport function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean {\n return harnessSupportsModel(harness, modelId)\n}\n\n/** Keep `modelId` when the harness can run it; else the harness's best compatible\n * catalog id (preferred patterns in order, highest version). When nothing in the\n * catalog fits, return the original so the caller sees the incompatibility. */\nexport function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string {\n return aiSnapModelToHarness(harness, modelId, canonicalIds)\n}\n\n/** Keep the harness when it can run `modelId`; else the model's native harness\n * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to opencode. */\nexport function snapHarnessToModel(harness: Harness, modelId: string): Harness {\n const snapped = aiSnapHarnessToModel(harness, modelId)\n if (!isHarness(snapped)) {\n throw new Error(\n `Harness \"${harness}\" snapped to \"${snapped}\" for model \"${modelId}\", which this platform has no backend for.`,\n )\n }\n return snapped\n}\n\n/** Fail-loud server guard: throw when a harness is asked to run a model it can't.\n * A resolved model tuple supplies provider evidence when its id is unqualified.\n * Call before dispatching a sandbox turn so a bypassed UI cannot reach the\n * sidecar with an incompatible or unproven pair. */\nexport function assertHarnessModelCompatible(\n harness: Harness,\n selection: string | HarnessModelSelection,\n): void {\n const modelId = typeof selection === 'string' ? selection : selection.model\n const transportProvider = typeof selection === 'string' ? undefined : selection.provider\n const providers = harnessProviders(harness)\n const provider = modelProvider(modelId) ?? transportProvider ?? null\n // An omitted/default model is deliberately left to the harness. Once a\n // caller names a model, however, a vendor-locked harness must have provider\n // evidence. Treating a router provider as compatible with every native CLI\n // is how a bare Gemini model reached Claude Code and produced \"Not logged in\".\n const isDefaultSentinel = modelId.trim() === '' || modelId.trim() === 'default'\n const compatible = isDefaultSentinel\n || providers === null\n || (provider !== null && providers.includes(provider))\n if (!compatible) {\n const native = preferredHarnessForModel(modelId)\n const providerDescription = provider ?? 'unqualified'\n throw new Error(\n `Harness \"${harness}\" cannot run model \"${modelId}\" (provider \"${providerDescription}\"). ` +\n `Use ${native ?? 'a router-backed harness (opencode)'} or an allowed model.`,\n )\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/harness/index.ts"],"sourcesContent":["/**\n * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.\n *\n * A \"harness\" is the coding-agent CLI a sandbox drives (opencode / codex /\n * claude-code / …). The shell governs WHICH harness a chat session uses and\n * enforces that a session is LOCKED to the harness it started with — the model\n * may change mid-session, the harness may not (swapping it mid-session would\n * orphan the session's running agent state). Every product otherwise hand-rolls\n * this and hard-codes a single harness; this is the one place the rule lives.\n *\n * Substrate-free: the harness list is derived from `@tangle-network/agent-interface`'s canonical\n * harness enum and mirrors the sandbox SDK's `BackendType` (no sandbox dependency). The consumer\n * owns storage — which harness a workspace defaults to, which one a session locked — and maps the\n * resolved value onto the SDK's `backend.type`.\n *\n * Harness↔model COMPATIBILITY (which models a harness can run, snapping) is NOT defined here — it\n * comes from `@tangle-network/agent-interface`, the single source of truth shared with the\n * sandbox-ui pickers and the cli-bridge backends. This module owns the harness TAXONOMY + the\n * session lock, and its server assertion also checks resolved transport-provider evidence.\n */\n\nimport {\n harnessProviders,\n harnessSupportsModel,\n harnessTypeSchema,\n modelProvider,\n preferredHarnessForModel,\n snapHarnessToModel as aiSnapHarnessToModel,\n snapModelToHarness as aiSnapModelToHarness,\n type HarnessType,\n} from '@tangle-network/agent-interface'\n\n/**\n * Canonical harnesses that are NOT sandbox backends. Each value in `KNOWN_HARNESSES` is dispatched\n * as `backend.type`, so a harness the platform ships no provider adapter for must stay out —\n * offering it would resolve a session onto a runner that cannot start.\n */\nconst NON_BACKEND_HARNESSES = ['gemini'] as const satisfies readonly HarnessType[]\n\nconst nonBackendHarnesses = new Set<string>(NON_BACKEND_HARNESSES)\n\n/** A coding-agent backend this shell can dispatch a session onto. */\nexport type Harness = Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]>\n\n/**\n * The known coding-agent backends: the canonical harness set minus what has no provider adapter,\n * Derived from `harnessTypeSchema.options` so a harness added upstream reaches this shell without\n * an edit here, and kept structural so this module still needs no sandbox dependency.\n */\nexport const KNOWN_HARNESSES: readonly Harness[] = [\n ...harnessTypeSchema.options.filter(\n (harness): harness is Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]> =>\n !nonBackendHarnesses.has(harness),\n ),\n]\n\n/** Define the default harness to use for code execution and testing environments */\nexport const DEFAULT_HARNESS: Harness = 'opencode'\n\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\n/** Determine if a value is a recognized harness string identifier */\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\n/** Resolve input options to determine the appropriate session harness to use */\nexport interface ResolveSessionHarnessInput {\n /** The harness already locked to this session (recorded at its first turn). */\n sessionHarness?: unknown\n /** The harness requested now — a new session's choice, or a turn's attempt to switch. */\n requested?: unknown\n /** The workspace's default harness, used only when starting a fresh session. */\n workspaceDefault?: unknown\n /** Final fallback when nothing else resolves (default `opencode`). */\n fallback?: Harness\n}\n\n/** Represent resolved session state including harness, lock status, and swap attempt flag */\nexport interface ResolvedSessionHarness {\n /** The harness to actually run — the locked one when the session already has it. */\n harness: Harness\n /** True when the session already had a locked harness (this turn did not pick it). */\n locked: boolean\n /** True when `requested` differs from the locked harness — a forbidden mid-session\n * swap the caller should reject or warn on. The lock always wins regardless. */\n swapAttempted: boolean\n}\n\n/**\n * Resolve the harness for a turn, enforcing the session lock.\n *\n * - **Session already started** (`sessionHarness` is a known harness): that harness\n * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller\n * can reject the swap. The model is a separate per-turn concern and is unaffected.\n * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller\n * persists the result as the session's lock for every subsequent turn.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n const fallback = input.fallback ?? DEFAULT_HARNESS\n if (isHarness(input.sessionHarness)) {\n const locked = input.sessionHarness\n const swapAttempted = isHarness(input.requested) && input.requested !== locked\n return { harness: locked, locked: true, swapAttempted }\n }\n const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback))\n return { harness, locked: false, swapAttempted: false }\n}\n\n/**\n * An app's harness configuration, read from its deploy configuration (for example the env vars\n * `AGENT_HARNESS` and `AGENT_HARNESS_WORKSPACES`), so no product hard-codes `backend.type`.\n */\nexport interface HarnessConfig {\n /** The app default for new sessions. Unset or blank means {@link DEFAULT_HARNESS}. */\n default?: string\n /** Per-workspace choices: a JSON object mapping a workspace id to a harness. */\n workspaces?: string\n}\n\nfunction configHarness(value: unknown, where: string): Harness {\n if (!isHarness(value)) throw new Error(`${where} names an unknown harness: ${String(value)}`)\n return value\n}\n\n/**\n * The harness a workspace's new sessions start on: its own choice, else the app default, else\n * {@link DEFAULT_HARNESS}. Pass the result to {@link resolveSessionHarness} as `workspaceDefault`\n * so a session that already ran keeps its harness. A value naming an unknown harness throws:\n * a configuration typo must fail loudly, never run a different harness than the one configured.\n */\nexport function configuredHarness(config: HarnessConfig, workspaceId?: string): Harness {\n const appDefault = config.default?.trim()\n ? configHarness(config.default.trim(), 'The harness default') : DEFAULT_HARNESS\n const raw = config.workspaces?.trim()\n if (!raw) return appDefault\n let choices: unknown\n try {\n choices = JSON.parse(raw)\n } catch {\n throw new Error('The per-workspace harness configuration is not JSON')\n }\n if (!choices || typeof choices !== 'object' || Array.isArray(choices)) {\n throw new Error('The per-workspace harness configuration must map workspace ids to harnesses')\n }\n // Every entry is checked, not only the caller's, so one bad entry fails every workspace's\n // first turn after the deploy instead of hiding until that workspace sends a message.\n const parsed = new Map(Object.entries(choices).map(([id, value]) =>\n [id, configHarness(value, `The harness for workspace ${id}`)]))\n return (workspaceId === undefined ? undefined : parsed.get(workspaceId)) ?? appDefault\n}\n\n/**\n * Harness ↔ model compatibility + snapping — delegated to `@tangle-network/agent-interface`.\n *\n * `Harness` is the dispatchable subset of `HarnessType`: this shell drops only canonical harnesses\n * for which the platform has no backend. Compatibility therefore comes entirely from Interface;\n * this package keeps no second harness taxonomy.\n */\n\nexport { modelProvider }\n\n/** A resolved model and the transport provider that will execute it. */\nexport interface HarnessModelSelection {\n model: string\n provider?: string\n}\n\n/** Provider-less ids (sentinels like \"default\", or a session's own config) are\n * compatible everywhere — every harness honors its own configuration. */\nexport function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean {\n return harnessSupportsModel(harness, modelId)\n}\n\n/** Keep `modelId` when the harness can run it; else the harness's best compatible\n * catalog id (preferred patterns in order, highest version). When nothing in the\n * catalog fits, return the original so the caller sees the incompatibility. */\nexport function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string {\n return aiSnapModelToHarness(harness, modelId, canonicalIds)\n}\n\n/** Keep the harness when it can run `modelId`; else the model's native harness\n * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to opencode. */\nexport function snapHarnessToModel(harness: Harness, modelId: string): Harness {\n const snapped = aiSnapHarnessToModel(harness, modelId)\n if (!isHarness(snapped)) {\n throw new Error(\n `Harness \"${harness}\" snapped to \"${snapped}\" for model \"${modelId}\", which this platform has no backend for.`,\n )\n }\n return snapped\n}\n\n/** Fail-loud server guard: throw when a harness is asked to run a model it can't.\n * A resolved model tuple supplies provider evidence when its id is unqualified.\n * Call before dispatching a sandbox turn so a bypassed UI cannot reach the\n * sidecar with an incompatible or unproven pair. */\nexport function assertHarnessModelCompatible(\n harness: Harness,\n selection: string | HarnessModelSelection,\n): void {\n const modelId = typeof selection === 'string' ? selection : selection.model\n const transportProvider = typeof selection === 'string' ? undefined : selection.provider\n const providers = harnessProviders(harness)\n const provider = modelProvider(modelId) ?? transportProvider ?? null\n // An omitted/default model is deliberately left to the harness. Once a\n // caller names a model, however, a vendor-locked harness must have provider\n // evidence. Treating a router provider as compatible with every native CLI\n // is how a bare Gemini model reached Claude Code and produced \"Not logged in\".\n const isDefaultSentinel = modelId.trim() === '' || modelId.trim() === 'default'\n const compatible = isDefaultSentinel\n || providers === null\n || (provider !== null && providers.includes(provider))\n if (!compatible) {\n const native = preferredHarnessForModel(modelId)\n const providerDescription = provider ?? 'unqualified'\n throw new Error(\n `Harness \"${harness}\" cannot run model \"${modelId}\" (provider \"${providerDescription}\"). ` +\n `Use ${native ?? 'a router-backed harness (opencode)'} or an allowed model.`,\n )\n }\n}\n\n// ── Harness capabilities ───────────────────────────────────────────────────\n//\n// The one place a product learns WHAT a harness can express, before it\n// assembles a profile — so incompatible content is never added in the first\n// place instead of being dropped (or refused) after the fact. Facts here are\n// sourced from the platform materializer's behavior (the per-harness subagent\n// lowering, the MCP config channel, the startup control validation) and the\n// placement map; when those change, this table changes with them.\n\n/** What a harness can express, for profile assembly. */\nexport interface HarnessCapabilities {\n /** Lower `profile.subagents` into native cwd files (e.g. `.claude/agents/`).\n * False: a profile carrying subagents is refused at materialization —\n * builders must not add them. */\n subagents: boolean\n /** How `profile.mcp` reaches the harness. */\n mcp: 'native' | 'config-file' | 'extension' | 'none'\n /** When tool controls are validated: 'startup' means an allowlist naming\n * MCP-carried verbs is checked before MCP tools register (spawn-time\n * validation refuses unknown names); 'runtime' tolerates them. */\n toolControls: 'startup' | 'runtime'\n /** cwd-relative directory `resources.skills` materializes into, or null\n * when the harness has no cwd skill primitive (falls back to inline). */\n skillsDir: string | null\n}\n\nconst HARNESS_CAPABILITIES: Record<Harness, HarnessCapabilities> = {\n 'claude-code': { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.claude/skills' },\n nanoclaw: { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.claude/skills' },\n opencode: { subagents: true, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.opencode/skills' },\n codex: { subagents: true, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.codex/skills' },\n 'kimi-code': { subagents: false, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.kimi/skills' },\n pi: { subagents: false, mcp: 'extension', toolControls: 'startup', skillsDir: '.pi/skills' },\n prime: { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.prime/agent/skills' },\n hermes: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n openclaw: { subagents: false, mcp: 'config-file', toolControls: 'runtime', skillsDir: 'skills' },\n // Harnesses the platform placement map does not bridge yet: conservative\n // facts — assert nothing they have not proven. Each upgrades to its real\n // facts when the materializer grows support for it.\n acp: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n amp: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n 'factory-droids': { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n forge: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n cursor: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n 'cli-base': { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n}\n\n/** The capability facts for `harness`. Every harness in KNOWN_HARNESSES has\n * an entry; the map is exhaustive by construction (tests assert parity). */\nexport function harnessCapabilities(harness: Harness): HarnessCapabilities {\n const caps = HARNESS_CAPABILITIES[harness]\n if (!caps) throw new Error(`harnessCapabilities: no capability facts for \"${harness}\"`)\n return caps\n}\n"],"mappings":"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","names":[]}
@@ -259,6 +259,8 @@ function createTangleSsoHandlers(opts) {
259
259
  return loginErrorRedirect("tangle_origin_mismatch");
260
260
  }
261
261
  const redirectPath = sanitizeRedirectPath(url.searchParams.get("redirect"), defaultRedirectPath);
262
+ const promptParam = url.searchParams.get("prompt");
263
+ const prompt = promptParam === "login" || promptParam === "select_account" ? promptParam : void 0;
262
264
  const state = await createSignedSsoState(stateConfig);
263
265
  const payload = { s: state, r: redirectPath };
264
266
  let authorizationUrl;
@@ -271,7 +273,7 @@ function createTangleSsoHandlers(opts) {
271
273
  throw new Error("PlatformOidcClient.redirectUri must equal callbackUrl");
272
274
  }
273
275
  } else {
274
- authorizationUrl = opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl });
276
+ authorizationUrl = opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl, prompt });
275
277
  }
276
278
  const cookie = serializeCookie(JSON.stringify(payload), {
277
279
  ...stateCookieOpts,
@@ -289,6 +291,12 @@ function createTangleSsoHandlers(opts) {
289
291
  }
290
292
  const code = url.searchParams.get("code");
291
293
  const stateFromPlatform = url.searchParams.get("state");
294
+ if (url.searchParams.get("error") === "signed_out") {
295
+ const signedOutPayload = parseStateCookiePayload(readCookieValue(request.headers.get("cookie"), opts.stateCookieName));
296
+ const headers = new Headers();
297
+ headers.append("Set-Cookie", serializeCookie("", { ...stateCookieOpts, maxAgeSeconds: 0 }));
298
+ return redirectResponse(sanitizeRedirectPath(signedOutPayload?.r ?? null, defaultRedirectPath), headers);
299
+ }
292
300
  if (!code || !stateFromPlatform || url.searchParams.has("error")) return loginErrorRedirect("tangle_callback_missing");
293
301
  const payload = parseStateCookiePayload(readCookieValue(request.headers.get("cookie"), opts.stateCookieName));
294
302
  if (!payload || !constantTimeEqual(payload.s, stateFromPlatform)) return loginErrorRedirect("tangle_state_mismatch");
@@ -494,4 +502,4 @@ export {
494
502
  createAdminGuard,
495
503
  assertBillableBalance
496
504
  };
497
- //# sourceMappingURL=chunk-WN3UK23P.js.map
505
+ //# sourceMappingURL=chunk-OGLTKK72.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/platform/sso.ts","../src/platform/guards.ts"],"sourcesContent":["/**\n * Tangle SSO for agent apps. OIDC callers use the registered runtime client\n * with S256 PKCE. Legacy API-key callers remain explicit during migration.\n * OIDC never falls back to cross-site exchange. Account matching and native\n * session cookie minting remain shared.\n */\n\nimport { clearCookieHeader, readCookieValue, serializeCookie } from '../web/index'\n\nconst DEFAULT_STATE_TTL_SECONDS = 600\nconst DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7\nconst DEFAULT_REDIRECT_PATH = '/app'\nconst DEFAULT_LOGIN_PATH = '/login'\nconst DEFAULT_SESSION_COOKIE = 'better-auth.session_token'\n\n// ── Signed state ────────────────────────────────────────────────────────────\n\n/** Define configuration options for managing SSO state including secret, lifetime, and clock injection */\nexport interface SsoStateConfig {\n /** HMAC-SHA256 secret (e.g. the app's auth secret). */\n secret: string\n /** State lifetime in ms. Default 600 000. */\n ttlMs?: number\n /** Injectable clock (ms since epoch). Default Date.now. */\n now?: () => number\n}\n\nfunction randomHex(bytes: number): string {\n const buf = new Uint8Array(bytes)\n crypto.getRandomValues(buf)\n return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nasync function hmacBytes(secret: string, value: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value)))\n}\n\nasync function hmacHex(secret: string, value: string): Promise<string> {\n return Array.from(await hmacBytes(secret, value), (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n\n/** Mint a `<randomHex32>.<timestamp36>.<hmacHex>` state value. The timestamp\n * is inside the signed payload, so expiry survives cookie-attribute tampering. */\nexport async function createSignedSsoState(config: SsoStateConfig): Promise<string> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const now = config.now ?? Date.now\n const payload = `${randomHex(16)}.${now().toString(36)}`\n return `${payload}.${await hmacHex(config.secret, payload)}`\n}\n\n/** Verify the MAC (constant-time) and the signed TTL. */\nexport async function verifySignedSsoState(state: string, config: SsoStateConfig): Promise<boolean> {\n if (!config.secret) throw new Error('SsoStateConfig.secret is required')\n const parts = state.split('.')\n if (parts.length !== 3) return false\n const [random, timestamp, mac] = parts\n if (!random || !timestamp || !mac || !/^[0-9a-z]+$/.test(timestamp)) return false\n const expected = await hmacHex(config.secret, `${random}.${timestamp}`)\n if (!constantTimeEqual(mac, expected)) return false\n const mintedAt = parseInt(timestamp, 36)\n if (!Number.isFinite(mintedAt)) return false\n const now = config.now ?? Date.now\n const ttlMs = config.ttlMs ?? DEFAULT_STATE_TTL_SECONDS * 1000\n const age = now() - mintedAt\n return age >= 0 && age <= ttlMs\n}\n\n// ── Seams ───────────────────────────────────────────────────────────────────\n\n/** Describe the result of exchanging SSO credentials including API key, user info, and optional plan details */\nexport interface TangleSsoExchangeResult {\n apiKey: string\n /** The platform's canonical verified-email proof. */\n emailVerified: boolean\n user: { id: string; email: string; name?: string | null }\n plan?: { tier: string } | null\n}\n\n/** Structural mirror of the platform auth wire client — any object with these\n * two methods satisfies it without this module importing the concrete class. */\nexport interface TangleSsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string; prompt?: 'login' | 'select_account' }): string\n exchange(code: string): Promise<TangleSsoExchangeResult>\n}\n\n/** First-party login exchanges identity without minting a Platform API key. */\nexport interface TangleIdentitySsoExchangeResult {\n kind: 'identity'\n emailVerified: boolean\n user: TangleSsoExchangeResult['user']\n apiKey?: never\n keyId?: never\n}\n\nexport interface TangleIdentitySsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string; prompt?: 'login' | 'select_account' }): string\n exchange(code: string): Promise<TangleIdentitySsoExchangeResult>\n}\n\n/** Token fields the callback persists from the runtime's OIDC exchange. */\nexport interface TangleOidcSsoTokens {\n accessToken: string\n tokenType: string\n expiresIn?: number\n refreshToken?: string\n idToken?: string\n scope?: string\n}\n\n/** Verified identity returned by the runtime's OIDC userinfo lookup. */\nexport interface TangleOidcSsoUser {\n id: string\n email: string\n emailVerified: true\n name?: string\n}\n\n/** The caller needs only the OIDC operations used by this callback. Keeping\n * this structural prevents legacy consumers from needing the optional\n * Runtime peer just to load the SSO declarations. */\nexport interface TangleOidcSsoAuthClient {\n authorizeUrl(options: {\n state: string\n codeChallenge: string\n nonce?: string\n prompt?: 'login' | 'consent' | 'none'\n loginHint?: string\n }): string\n exchange(code: string, codeVerifier: string): Promise<{\n tokens: TangleOidcSsoTokens\n user: TangleOidcSsoUser\n }>\n revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void>\n}\n\n/** Local account shape required by the verified Tangle SSO account policy.\n * Consumers map their user/link tables into this shape before resolving an\n * exchange. */\nexport interface TangleSsoLocalAccount {\n userId: string\n email: string\n emailVerified: boolean\n platformUserId: string | null\n}\n\n/** Reasons a local account lookup must stop instead of linking an exchange. */\nexport type TangleSsoAccountConflictReason =\n | 'ambiguous-platform-id'\n | 'platform-id-email-conflict'\n | 'unverified-email'\n | 'email-platform-id-conflict'\n | 'ambiguous-email'\n | 'invalid-platform-id'\n | 'invalid-email'\n | 'platform-match-mismatch'\n | 'email-match-mismatch'\n\n/** Result of applying the verified Tangle SSO account matching policy. */\nexport type TangleSsoAccountResolution =\n | { kind: 'existing'; userId: string; matchedBy: 'platform-id' | 'verified-email' }\n | { kind: 'create' }\n | { kind: 'reject'; reason: TangleSsoAccountConflictReason }\n\n/** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the\n * platform's stable identity. */\nexport interface TangleSsoAccountResolutionInput {\n email: string\n platformMatches: readonly TangleSsoLocalAccount[]\n emailMatches: readonly TangleSsoLocalAccount[]\n platformUserId: string\n}\n\n/** Normalize an email for comparisons and persistence queries. */\nexport function normalizeTangleSsoEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nfunction normalizedPlatformUserId(value: string | null | undefined): string | null {\n const normalized = value?.trim()\n return normalized ? normalized : null\n}\n\n/**\n * Resolve a platform exchange against all local rows found by stable platform\n * identity and normalized email.\n *\n * A stable platform id takes precedence over email. Every ambiguous or\n * contradictory result rejects, and email fallback requires a verified local\n * email that is not already bound to another platform id. The resolver is\n * pure so each consumer can query its own schema, including schemas without\n * unique indexes, then rerun it after an insert race.\n */\nexport function resolveTangleSsoAccount(\n input: TangleSsoAccountResolutionInput,\n): TangleSsoAccountResolution {\n const email = normalizeTangleSsoEmail(input.email)\n const platformUserId = normalizedPlatformUserId(input.platformUserId)\n if (!email) return { kind: 'reject', reason: 'invalid-email' }\n if (!platformUserId) return { kind: 'reject', reason: 'invalid-platform-id' }\n\n if (input.platformMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-platform-id' }\n }\n if (input.emailMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-email' }\n }\n\n if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {\n return { kind: 'reject', reason: 'platform-match-mismatch' }\n }\n if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {\n return { kind: 'reject', reason: 'email-match-mismatch' }\n }\n\n const platformMatch = input.platformMatches[0]\n const emailMatch = input.emailMatches[0]\n\n if (platformMatch) {\n if (emailMatch && emailMatch.userId !== platformMatch.userId) {\n return { kind: 'reject', reason: 'platform-id-email-conflict' }\n }\n return { kind: 'existing', userId: platformMatch.userId, matchedBy: 'platform-id' }\n }\n\n if (!emailMatch) return { kind: 'create' }\n const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId)\n if (existingPlatformUserId === platformUserId) {\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'platform-id' }\n }\n if (emailMatch.emailVerified !== true) {\n return { kind: 'reject', reason: 'unverified-email' }\n }\n\n if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {\n return { kind: 'reject', reason: 'email-platform-id-conflict' }\n }\n\n // The platform proof is already verified. A verified local email row is safe\n // to link when it is unbound or already carries this same platform id.\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'verified-email' }\n}\n\n/** Thrown by an account store when the verified SSO policy rejects a link. */\nexport class TangleSsoAccountConflictError extends Error {\n readonly reason: TangleSsoAccountConflictReason\n\n constructor(reason: TangleSsoAccountConflictReason, message = 'Tangle SSO account linking was rejected') {\n super(message)\n this.name = 'TangleSsoAccountConflictError'\n this.reason = reason\n }\n}\n\n/** Thrown by `upsertUserByEmail` when the app-local user row cannot be\n * created; the callback handler maps it to `?error=tangle_user_create_failed`.\n * Any other store error propagates. */\nexport class TangleSsoUserCreateError extends Error {\n constructor(message = 'Failed to create local user for Tangle SSO') {\n super(message)\n this.name = 'TangleSsoUserCreateError'\n }\n}\n\n/**\n * Account persistence seam. Covers both storage styles in use: link-table\n * apps (a per-user platform-link row) and session-column apps (the key on the\n * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,\n * and each app persists with the key it needs. `createSession` runs first so\n * the token is always available to `saveTangleLink`.\n */\nexport interface TangleSsoAccountStore {\n /** Resolve the local account before any user, session, or link write.\n * Implementations must query every candidate by stable platform id and\n * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent\n * fail-closed policy). The callback rejects `kind: 'reject'` results before\n * it calls `upsertUserByEmail`. */\n resolveAccount(input: { email: string; platformUserId: string }): Promise<TangleSsoAccountResolution>\n /** Find-or-create the app-local user selected by `resolveAccount`.\n * `tangleUserId` is the platform's stable user id — match on it first when\n * the app stores it (emails are mutable on the platform; the id is not),\n * falling back to email for first-time logins. */\n upsertUserByEmail(input: {\n email: string\n name: string | null\n tangleUserId: string\n resolution: Exclude<TangleSsoAccountResolution, { kind: 'reject' }>\n }): Promise<{ userId: string }>\n /** Create an app session row; returns the session-cookie token value. */\n createSession(input: {\n userId: string\n expiresAt: Date\n ipAddress: string | null\n userAgent: string | null\n }): Promise<{ token: string }>\n /** Persist the platform link (API key + platform identity). */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n apiKey: string\n planTier: string | null\n }): Promise<void>\n}\n\n/**\n * OIDC tokens are server-side credentials, not API keys. Implementations must\n * encrypt them at rest and bind them to the exact local session. Refresh and\n * disconnect must serialize updates to the same grant.\n */\nexport interface TangleOidcSsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** Atomically bind the grant to this exact session. If persistence partially\n * fails, `deleteSession` must remove every row and credential for it. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n tokens: TangleOidcSsoTokens\n accessTokenExpiresAt: Date\n }): Promise<void>\n /** Remove an unpublished local session if callback persistence fails.\n * This must also remove any grant rows or credentials partially written by\n * a rejected `saveTangleLink`, keyed by this exact session token. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n/** Persist only the stable identity link for a first-party app. */\nexport interface TangleIdentitySsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** `upsertUserByEmail` may create the local user, but must not publish a new\n * platform link or update an existing user's verified-email state. The\n * callback has not yet created a session or prepared its cookie. */\n upsertUserByEmail: TangleSsoAccountStore['upsertUserByEmail']\n /** Commit the identity link and any verified-email update atomically. Do not\n * reject after committing. If the store can partially write before throwing,\n * `deleteSession` must remove only writes made for this session token. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n }): Promise<void>\n /** Remove an unpublished session and any link or verification writes that a\n * rejected `saveTangleLink` made for its exact token. Preserve preexisting\n * links and other sessions. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n// ── Session cookie ──────────────────────────────────────────────────────────\n\n/** Successful-login context handed to the `setSessionCookie` seam. */\nexport interface TangleSsoSessionCookieArgs {\n /** Session token returned by `store.createSession`. */\n token: string\n /** Session expiry (now + `sessionTtlSeconds`). */\n expiresAt: Date\n /** Mirrors `sessionTtlSeconds` after defaulting. */\n ttlSeconds: number\n /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */\n secure: boolean\n}\n\n/**\n * Sign a session token to better-call's signed-cookie contract — the value\n * better-auth's `getSignedCookie` verifies: `<token>.<signature>` where the\n * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as\n * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call\n * rejects any other length or suffix, so url-safe/unpadded variants read back\n * as a null session). The joined value is percent-encoded once at cookie\n * serialization, matching better-call's `serializeSignedCookie` byte-exactly.\n */\nexport async function signSessionCookieValue(token: string, secret: string): Promise<string> {\n if (!secret) throw new Error('signSessionCookieValue requires a non-empty secret')\n const sig = await hmacBytes(secret, token)\n let bin = ''\n for (const byte of sig) bin += String.fromCharCode(byte)\n return `${token}.${btoa(bin)}`\n}\n\n/** Structural slice of a `betterAuth()` instance — only what cookie minting\n * reads. No better-auth import: the signing contract is implemented by\n * `signSessionCookieValue`, byte-compatible with better-auth's own\n * `makeSignature`. */\nexport interface BetterAuthSessionCookieSource {\n $context: PromiseLike<{\n secret: string\n authCookies: {\n sessionToken: {\n /** Final cookie name — better-auth decides the `__Secure-` prefix\n * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */\n name: string\n attributes: {\n secure?: boolean\n sameSite?: string\n path?: string\n httpOnly?: boolean\n domain?: string\n }\n }\n }\n }>\n}\n\n/** Define options to customize warning behavior for shadowed cookie names in authentication sessions */\nexport interface BetterAuthSessionCookieMinterOptions {\n /** Receives the shadowed-cookie-name warning (see below). Default\n * console.warn. */\n warn?: (message: string) => void\n}\n\n/**\n * Canonical `setSessionCookie` wiring for better-auth apps: mint the session\n * Set-Cookie exactly as better-auth's own login flows do — name + attributes\n * from `auth.$context.authCookies.sessionToken` (better-auth stays\n * authoritative over prefix/name/attributes) and the value signed to\n * better-call's `getSignedCookie` contract. A raw unprefixed\n * `better-auth.session_token` left by an earlier login is explicitly expired\n * so it cannot shadow the real cookie.\n *\n * Warns when the app's session cookie still has better-auth's DEFAULT name:\n * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie\n * under that exact name, and equal-path cookies are sent oldest-first — the\n * platform's cookie is always older (the user signs in there before the app's\n * callback runs), so the app reads the platform's token, fails its own\n * signature check, and every fresh login lands logged-out. Per-app\n * `advanced.cookiePrefix` is the fix.\n *\n * Throws on a domain-scoped session cookie for the same reason: a\n * `Domain=`-wide session cookie is exactly the shadowing footgun.\n */\nexport function createBetterAuthSessionCookieMinter(\n auth: BetterAuthSessionCookieSource,\n options: BetterAuthSessionCookieMinterOptions = {},\n): (args: TangleSsoSessionCookieArgs) => Promise<string[]> {\n const warn = options.warn ?? ((message: string) => console.warn(message))\n return async ({ token, ttlSeconds }) => {\n const ctx = await auth.$context\n if (!ctx.secret) {\n throw new Error('createBetterAuthSessionCookieMinter: auth context has no secret')\n }\n const { name, attributes } = ctx.authCookies.sessionToken\n if (attributes.domain) {\n throw new Error(\n `createBetterAuthSessionCookieMinter: refusing a domain-scoped session cookie (Domain=${attributes.domain}) — ` +\n 'a domain-wide session cookie shadows sibling apps that share the parent domain',\n )\n }\n if (name === DEFAULT_SESSION_COOKIE || name === `__Secure-${DEFAULT_SESSION_COOKIE}`) {\n warn(\n `[tangle-sso] session cookie is named \"${name}\" — better-auth's default. ` +\n 'The Tangle platform (id.tangle.tools) sets a Domain=.tangle.tools cookie under the same name, ' +\n \"and the platform's (older) cookie wins the Cookie-header order, so this app's sessions read back null. \" +\n \"Set a per-app prefix: betterAuth({ advanced: { cookiePrefix: '<app>' } }).\",\n )\n }\n const sameSite = typeof attributes.sameSite === 'string' ? attributes.sameSite : 'lax'\n const cookieOptions = {\n name,\n path: typeof attributes.path === 'string' ? attributes.path : '/',\n httpOnly: attributes.httpOnly !== false,\n sameSite: (sameSite.charAt(0).toUpperCase() + sameSite.slice(1)) as 'Lax' | 'Strict' | 'None',\n secure: Boolean(attributes.secure) || name.startsWith('__Secure-'),\n maxAgeSeconds: ttlSeconds,\n }\n const cookies = [serializeCookie(await signSessionCookieValue(token, ctx.secret), cookieOptions)]\n if (name !== DEFAULT_SESSION_COOKIE) {\n cookies.push(clearCookieHeader({ ...cookieOptions, name: DEFAULT_SESSION_COOKIE }))\n }\n return cookies\n }\n}\n\n// ── Handlers ────────────────────────────────────────────────────────────────\n\n/** Define configuration options for handling Tangle SSO authentication and session management */\nexport interface TangleSsoHandlerOptions {\n protocol?: 'legacy'\n auth: TangleSsoAuthClient\n store: TangleSsoAccountStore\n /** HMAC secret for the state cookie. */\n stateSecret: string\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n stateCookieName: string\n /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is\n * provided. The default path prepends `__Secure-` iff `secureCookies`. */\n sessionCookieName?: string\n /** Mint the host auth framework's own session cookie(s); return complete\n * Set-Cookie header values (the handler appends them verbatim and sets no\n * session cookie itself). Supply this when the framework should stay\n * authoritative over name/prefix/signing/attributes — e.g. better-auth:\n * `auth.$context.authCookies.sessionToken` + `makeSignature`. */\n setSessionCookie?: (\n args: TangleSsoSessionCookieArgs,\n ) => readonly string[] | Promise<readonly string[]>\n /** HMAC-SHA256 secret the host auth framework verifies session cookies with\n * (better-auth: its `secret`). Required when `setSessionCookie` is absent —\n * the default cookie is minted to better-call's signed contract via\n * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a\n * null session, so there is deliberately no fallback to `stateSecret`\n * (which is not guaranteed to be the auth secret). */\n sessionCookieSecret?: string\n /** Adds `Secure` to every cookie this module sets, and (default session\n * cookie only) the `__Secure-` name prefix. Must match the auth\n * framework's own secure-cookie decision (better-auth: https `baseURL` /\n * `advanced.useSecureCookies`), or it will look up a different cookie name\n * than the one set here. */\n secureCookies: boolean\n /** Default 604 800 (7 days). */\n sessionTtlSeconds?: number\n /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */\n stateTtlSeconds?: number\n /** Default '/app'. */\n defaultRedirectPath?: string\n /** Default '/login'. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n now?: () => number\n}\n\n/** Registered OIDC client and token store. The OIDC caller supplies the Runtime PKCE helper. */\nexport interface TangleOidcSsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'oidc'\n auth: TangleOidcSsoAuthClient\n store: TangleOidcSsoAccountStore\n /** Pass the Runtime's `createPkcePair` helper from the OIDC-only caller. */\n createPkcePair: () => Promise<{ verifier: string; challenge: string }>\n}\n\nexport interface TangleIdentitySsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'identity'\n auth: TangleIdentitySsoAuthClient\n store: TangleIdentitySsoAccountStore\n}\n\n/** Define handlers for SSO start and callback routes managing authentication flow and session cookies */\nexport interface TangleSsoHandlers {\n /** GET start route: mint + sign state, set the state cookie, 302 to the\n * platform authorize URL. `?redirect=` carries the post-login path. */\n start(request: Request): Promise<Response>\n /** GET callback route: verify state, exchange the code, upsert the user,\n * create the session, save the platform link, set the session cookie\n * (via the `setSessionCookie` seam, else signed to better-call's contract\n * with `sessionCookieSecret`), 302 to the saved redirect. Every failure\n * 302s to `loginPath?error=…` with the state cookie cleared. */\n callback(request: Request): Promise<Response>\n}\n\n/** Redirect targets must stay on this app's origin. A prefix check on `/`\n * and `//` is not enough: in the WHATWG parser a backslash is a path\n * separator for special schemes, so `/\\host` resolves to `https://host/`,\n * and tab or newline are stripped before parsing, so `/\\t/host` is `//host`.\n * Parse the value against a fixed origin and accept it only when that origin\n * survives; the result is the parser's normalised path, query and fragment.\n * Backslash and control characters are rejected up front as well, so the\n * decision does not rest on one parser's handling of them.\n *\n * The input staying on-origin is not enough either: dot-segment removal\n * turns `/.//host` or `/a/..//host` into the pathname `//host` with the\n * origin intact, and a browser resolves that Location off-origin. So the\n * emitted value must itself resolve on-origin and come back unchanged. */\nconst REDIRECT_REFERENCE_ORIGIN = 'https://redirect-reference.invalid'\n\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (!value || !value.startsWith('/') || /[\\\\\\u0000-\\u001f\\u007f]/.test(value)) return fallback\n const path = resolveOnReferenceOrigin(value)\n if (path === null || resolveOnReferenceOrigin(path) !== path) return fallback\n return path\n}\n\n/** `path + search + hash` of `value` resolved against the reference origin,\n * or null when the parse fails or lands on another origin. */\nfunction resolveOnReferenceOrigin(value: string): string | null {\n let url: URL\n try {\n url = new URL(value, REDIRECT_REFERENCE_ORIGIN)\n } catch {\n return null\n }\n if (url.origin !== REDIRECT_REFERENCE_ORIGIN) return null\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\n headers.set('Cache-Control', 'no-store')\n headers.set('Referrer-Policy', 'no-referrer')\n return new Response(null, { status: 302, headers })\n}\n\n/** Real client IP: `CF-Connecting-IP` behind Cloudflare, else the first\n * `x-forwarded-for` hop (the rest of the list is sender-controlled). */\nfunction clientIp(request: Request): string | null {\n return (\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n null\n )\n}\n\ninterface StateCookiePayload {\n s: string\n r: string\n v?: string\n m?: string\n}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw || raw.length > 4096) return null\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed === null || typeof parsed !== 'object') return null\n const { s, r, v, m } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r, ...(typeof v === 'string' ? { v } : {}), ...(typeof m === 'string' ? { m } : {}) }\n } catch { return null }\n}\n\nfunction stateBinding(payload: StateCookiePayload, callbackUrl: string): string {\n return JSON.stringify([payload.s, payload.r, payload.v ?? null, callbackUrl])\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(\n opts: TangleSsoHandlerOptions | TangleOidcSsoHandlerOptions | TangleIdentitySsoHandlerOptions,\n): TangleSsoHandlers {\n if (!opts.stateSecret) throw new Error('TangleSsoHandlerOptions.stateSecret is required')\n if (!opts.callbackUrl) throw new Error('TangleSsoHandlerOptions.callbackUrl is required')\n if (!opts.stateCookieName) throw new Error('TangleSsoHandlerOptions.stateCookieName is required')\n const callbackUrl = new URL(opts.callbackUrl)\n // Startup hardening applies to EVERY protocol. The checks were previously\n // gated on 'oidc' | 'identity', which left 'legacy' (the default when\n // `protocol` is omitted — the configuration every product shipped before\n // the typed variants existed) accepting a short stateSecret, a non-HTTPS\n // callback, credentials/query/fragment in the callback URL, and https with\n // non-Secure cookies (issue #749). The same configuration rules protect\n // every flow; only the runtime origin checks remain protocol-specific.\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 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 // `?prompt=` passes through to the platform authorize URL so an app can\n // offer `login` (fresh primary auth — the \"switch account\" semantic)\n // and `select_account` (the platform account picker). One-shot on the\n // platform side; it is deliberately not persisted into the state\n // cookie because the callback never replays it.\n const promptParam = url.searchParams.get('prompt')\n const prompt = promptParam === 'login' || promptParam === 'select_account' ? promptParam : undefined\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, prompt })\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 // The platform's RP-initiated end-session returns `error=signed_out`:\n // the person signed out of Tangle on purpose and lands back here. That\n // is the app's sign-in surface, not a failed handshake — route there\n // quietly (still clearing the one-shot state cookie).\n if (url.searchParams.get('error') === 'signed_out') {\n const signedOutPayload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n const headers = new Headers()\n headers.append('Set-Cookie', serializeCookie('', { ...stateCookieOpts, maxAgeSeconds: 0 }))\n return redirectResponse(sanitizeRedirectPath(signedOutPayload?.r ?? null, defaultRedirectPath), headers)\n }\n if (!code || !stateFromPlatform || url.searchParams.has('error')) return loginErrorRedirect('tangle_callback_missing')\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || !constantTimeEqual(payload.s, stateFromPlatform)) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n if (opts.protocol === 'oidc' && (!payload.v || !/^[A-Za-z0-9_-]{43,128}$/.test(payload.v)\n || !payload.m || !constantTimeEqual(payload.m, await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))))) {\n return loginErrorRedirect('tangle_state_mismatch')\n }\n const tokenRequestedAt = now()\n let oidcTokens: TangleOidcSsoTokens | undefined\n let legacyExchange: TangleSsoExchangeResult | undefined\n let createdSession: string | undefined\n let committed = false\n try {\n let exchanged: { user: TangleSsoExchangeResult['user']; emailVerified: boolean }\n try {\n if (opts.protocol === 'oidc') {\n const result = await opts.auth.exchange(code, payload.v!)\n oidcTokens = result.tokens\n if (!oidcTokens.refreshToken || !Number.isFinite(oidcTokens.expiresIn) || oidcTokens.expiresIn! <= 0) {\n throw new Error('OIDC requires offline_access and a finite access-token lifetime')\n }\n exchanged = { user: result.user, emailVerified: result.user.emailVerified }\n } else if (opts.protocol === 'identity') {\n const identity = await opts.auth.exchange(code)\n if (identity.kind !== 'identity' || 'apiKey' in identity || 'keyId' in identity\n || !identity.user || typeof identity.user.id !== 'string' || !identity.user.id.trim()\n || typeof identity.user.email !== 'string' || !identity.user.email.trim()) {\n throw new Error('Identity exchange returned an invalid result')\n }\n exchanged = identity\n } else {\n legacyExchange = await opts.auth.exchange(code)\n exchanged = legacyExchange\n }\n } catch {\n log('[tangle-sso] exchange failed')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n if (exchanged.emailVerified !== true) {\n log('[tangle-sso] exchange did not include a verified-email proof')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({ email, platformUserId: exchanged.user.id })\n if (resolution.kind === 'reject') throw new TangleSsoAccountConflictError(resolution.reason)\n ;({ userId } = await opts.store.upsertUserByEmail({\n email,\n name: exchanged.user.name ?? null,\n tangleUserId: exchanged.user.id,\n resolution,\n }))\n } catch (err) {\n if (err instanceof TangleSsoAccountConflictError) {\n log('[tangle-sso] account linking conflict', err.reason)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\n const expiresAt = new Date(now() + sessionTtlSeconds * 1000)\n const { token } = await opts.store.createSession({\n userId,\n expiresAt,\n ipAddress: clientIp(request),\n userAgent: request.headers.get('user-agent'),\n })\n createdSession = token\n const link = {\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n }\n if (opts.protocol === 'oidc') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink({\n ...link,\n tokens: oidcTokens!,\n accessTokenExpiresAt: new Date(tokenRequestedAt + oidcTokens!.expiresIn! * 1000),\n })\n committed = true\n return response\n }\n if (opts.protocol === 'identity') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink(link)\n committed = true\n return response\n }\n await opts.store.saveTangleLink({ ...link, apiKey: legacyExchange!.apiKey, planTier: legacyExchange!.plan?.tier ?? null })\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n } catch (error) {\n if (opts.protocol !== 'oidc' && opts.protocol !== 'identity') throw error\n log('[tangle-sso] local session persistence failed')\n return loginErrorRedirect('tangle_session_failed')\n } finally {\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity') && !committed) {\n if (createdSession) {\n try { await opts.store.deleteSession({ sessionToken: createdSession }) }\n catch { log('[tangle-sso] unpublished session cleanup failed') }\n }\n if (opts.protocol === 'oidc' && oidcTokens) {\n try { if (oidcTokens.refreshToken) await opts.auth.revoke(oidcTokens.refreshToken, 'refresh_token') }\n catch { log('[tangle-sso] rejected grant refresh revocation failed') }\n try { await opts.auth.revoke(oidcTokens.accessToken, 'access_token') }\n catch { log('[tangle-sso] rejected grant access revocation failed') }\n }\n }\n }\n },\n }\n}\n","/**\n * Request guards for agent-app routes: session auth (302 redirect for pages,\n * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to\n * non-admins), and the billable-balance gate (402 with a stable code).\n * Session resolution is a seam; thrown Responses follow the router convention\n * of surfacing a thrown Response as the route result.\n */\n\nimport { isTangleBillingEnforcementDisabled } from '../runtime/model'\n\n/** Define options for configuring authentication guard behavior and session retrieval */\nexport interface AuthGuardOptions<Session> {\n /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */\n getSession(request: Request): Promise<Session | null | undefined>\n /** Default '/login'. */\n loginPath?: string\n}\n\n/** Resolve user authentication and session requirements for page and API requests */\nexport interface AuthGuard<Session> {\n /** Page guard — throws a 302 redirect Response to `loginPath`. */\n requireUser(request: Request): Promise<Session>\n /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */\n requireApiUser(request: Request): Promise<Session>\n /** `apiResponse` selects the 401 JSON path over the redirect. */\n requireSession(request: Request, opts?: { apiResponse?: boolean }): Promise<Session>\n getOptionalSession(request: Request): Promise<Session | null>\n}\n\n/** Create an authentication guard that enforces session presence and handles unauthorized access responses */\nexport function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session> {\n const loginPath = opts.loginPath ?? '/login'\n\n async function requireSession(request: Request, o: { apiResponse?: boolean } = {}): Promise<Session> {\n const session = await opts.getSession(request)\n if (!session) {\n if (o.apiResponse) {\n throw Response.json({ error: 'Unauthorized', code: 'auth.unauthenticated' }, { status: 401 })\n }\n throw new Response(null, { status: 302, headers: { Location: loginPath } })\n }\n return session\n }\n\n return {\n requireSession,\n requireUser: (request) => requireSession(request),\n requireApiUser: (request) => requireSession(request, { apiResponse: true }),\n getOptionalSession: async (request) => (await opts.getSession(request)) ?? null,\n }\n}\n\n/** Resolve a value or an HTTP response indicating failure in a guarded operation */\nexport type GuardResolution<T> = { ok: true; value: T } | { ok: false; response: Response }\n\n/**\n * Adapt a guard that THROWS a Response (the quartet above — the router\n * convention) to the `{ok: true, value} | {ok: false, response}` resolution\n * shape the route factories take (`/chat-routes` `authorize`,\n * `/interactions` `resolveConnection`, `/chat-routes` upload `authorize`).\n * Every product wrote this try/catch by hand; it lives here once.\n */\nexport async function guardResolution<T>(run: () => Promise<T>): Promise<GuardResolution<T>> {\n try {\n return { ok: true, value: await run() }\n } catch (err) {\n if (err instanceof Response) return { ok: false, response: err }\n throw err\n }\n}\n\n/** Comma/whitespace separated → trimmed, lowercased, empties dropped. */\nexport function parseAdminEmails(raw: string | null | undefined): string[] {\n return (raw ?? '')\n .split(/[,\\s]+/)\n .map((e) => e.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** Define options to resolve user session and control access based on allowed admin emails */\nexport interface AdminGuardOptions<Session> {\n requireUser(request: Request): Promise<Session>\n emailOf(session: Session): string | null | undefined\n /** Resolved per request; an EMPTY allowlist refuses everyone. */\n allowedEmails(): string[]\n}\n\n/** Non-admins (and empty allowlists) get 404, keeping the route invisible —\n * better than a \"forbidden\" footprint that advertises its existence. */\nexport function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session> {\n return async (request) => {\n const session = await opts.requireUser(request)\n const allowed = opts.allowedEmails()\n if (allowed.length === 0) throw new Response('Not found', { status: 404 })\n const email = (opts.emailOf(session) ?? '').toLowerCase()\n if (!allowed.includes(email)) throw new Response('Not found', { status: 404 })\n return session\n }\n}\n\n/** Describe the billable balance state including overage permission and remaining USD balance */\nexport interface BillableBalanceState {\n overageAllowed: boolean\n remainingBalanceUsd: number\n}\n\n/** Define options to assert and customize billable balance enforcement behavior */\nexport interface AssertBillableBalanceOptions {\n env?: Record<string, string | undefined>\n /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),\n * fed to `isTangleBillingEnforcementDisabled`. */\n enforcementEnvVar?: string\n /** Default 'Add balance or upgrade your plan to invoke this agent.'. */\n errorMessage?: string\n /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */\n errorBody?: Record<string, unknown>\n}\n\n/**\n * Gate a billable turn: passes when enforcement is disabled (dev default),\n * the tier allows overage, or remaining balance is positive. Otherwise throws\n * a 402 Response with the stable `billing.balance_required` code so clients\n * can route to the billing screen.\n */\nexport function assertBillableBalance(state: BillableBalanceState, opts: AssertBillableBalanceOptions = {}): void {\n if (isTangleBillingEnforcementDisabled({ env: opts.env, enforcementEnvVar: opts.enforcementEnvVar })) return\n if (state.overageAllowed || state.remainingBalanceUsd > 0) return\n // errorBody first: the stable error/code contract always wins over caller extras.\n throw Response.json(\n {\n ...opts.errorBody,\n error: opts.errorMessage ?? 'Add balance or upgrade your plan to invoke this agent.',\n code: 'billing.balance_required',\n },\n { status: 402 },\n )\n}\n"],"mappings":"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","names":["headers","sessionCookies"]}
@@ -10,7 +10,7 @@ import {
10
10
  filterAcceptedFiles,
11
11
  isAcceptedFileType,
12
12
  renamePastedImages
13
- } from "./chunk-XSPUKVHF.js";
13
+ } from "./chunk-KICTB6RK.js";
14
14
  import {
15
15
  OVERLAY_SHADOW,
16
16
  POPOVER_OPTION_FOCUS,
@@ -1552,4 +1552,4 @@ export {
1552
1552
  ComposerModeControls,
1553
1553
  EntryComposer
1554
1554
  };
1555
- //# sourceMappingURL=chunk-ZEMO7S2G.js.map
1555
+ //# sourceMappingURL=chunk-PZHZV6QY.js.map
@@ -13,7 +13,7 @@ import {
13
13
  DEFAULT_HARNESS,
14
14
  assertHarnessModelCompatible,
15
15
  isHarness
16
- } from "./chunk-WVESWAJJ.js";
16
+ } from "./chunk-MXNVSY6J.js";
17
17
  import {
18
18
  assertProfilePromptWithinBudget,
19
19
  fingerprintAgentProfile
@@ -3343,4 +3343,4 @@ export {
3343
3343
  isTerminalPromptEvent,
3344
3344
  detectInteractiveQuestion
3345
3345
  };
3346
- //# sourceMappingURL=chunk-4DZVSTET.js.map
3346
+ //# sourceMappingURL=chunk-RCRC2JKP.js.map
@@ -115,3 +115,22 @@ export declare function snapHarnessToModel(harness: Harness, modelId: string): H
115
115
  * Call before dispatching a sandbox turn so a bypassed UI cannot reach the
116
116
  * sidecar with an incompatible or unproven pair. */
117
117
  export declare function assertHarnessModelCompatible(harness: Harness, selection: string | HarnessModelSelection): void;
118
+ /** What a harness can express, for profile assembly. */
119
+ export interface HarnessCapabilities {
120
+ /** Lower `profile.subagents` into native cwd files (e.g. `.claude/agents/`).
121
+ * False: a profile carrying subagents is refused at materialization —
122
+ * builders must not add them. */
123
+ subagents: boolean;
124
+ /** How `profile.mcp` reaches the harness. */
125
+ mcp: 'native' | 'config-file' | 'extension' | 'none';
126
+ /** When tool controls are validated: 'startup' means an allowlist naming
127
+ * MCP-carried verbs is checked before MCP tools register (spawn-time
128
+ * validation refuses unknown names); 'runtime' tolerates them. */
129
+ toolControls: 'startup' | 'runtime';
130
+ /** cwd-relative directory `resources.skills` materializes into, or null
131
+ * when the harness has no cwd skill primitive (falls back to inline). */
132
+ skillsDir: string | null;
133
+ }
134
+ /** The capability facts for `harness`. Every harness in KNOWN_HARNESSES has
135
+ * an entry; the map is exhaustive by construction (tests assert parity). */
136
+ export declare function harnessCapabilities(harness: Harness): HarnessCapabilities;
@@ -4,19 +4,21 @@ import {
4
4
  assertHarnessModelCompatible,
5
5
  coerceHarness,
6
6
  configuredHarness,
7
+ harnessCapabilities,
7
8
  isHarness,
8
9
  isModelCompatibleWithHarness,
9
10
  modelProvider,
10
11
  resolveSessionHarness,
11
12
  snapHarnessToModel,
12
13
  snapModelToHarness
13
- } from "../chunk-WVESWAJJ.js";
14
+ } from "../chunk-MXNVSY6J.js";
14
15
  export {
15
16
  DEFAULT_HARNESS,
16
17
  KNOWN_HARNESSES,
17
18
  assertHarnessModelCompatible,
18
19
  coerceHarness,
19
20
  configuredHarness,
21
+ harnessCapabilities,
20
22
  isHarness,
21
23
  isModelCompatibleWithHarness,
22
24
  modelProvider,
@@ -21,7 +21,7 @@ import {
21
21
  resolveTangleSsoAccount,
22
22
  signSessionCookieValue,
23
23
  verifySignedSsoState
24
- } from "../chunk-WN3UK23P.js";
24
+ } from "../chunk-OGLTKK72.js";
25
25
  import {
26
26
  resolveTangleExecutionEnvironment
27
27
  } from "../chunk-D6UJEOZQ.js";
@@ -38,6 +38,7 @@ export interface TangleSsoAuthClient {
38
38
  authorizeUrl(options: {
39
39
  state: string;
40
40
  redirectUri?: string;
41
+ prompt?: 'login' | 'select_account';
41
42
  }): string;
42
43
  exchange(code: string): Promise<TangleSsoExchangeResult>;
43
44
  }
@@ -53,6 +54,7 @@ export interface TangleIdentitySsoAuthClient {
53
54
  authorizeUrl(options: {
54
55
  state: string;
55
56
  redirectUri?: string;
57
+ prompt?: 'login' | 'select_account';
56
58
  }): string;
57
59
  exchange(code: string): Promise<TangleIdentitySsoExchangeResult>;
58
60
  }
@@ -97,7 +97,7 @@ import {
97
97
  warmBinOf,
98
98
  warmWeekdayOf,
99
99
  writeProfileFilesToBox
100
- } from "../chunk-4DZVSTET.js";
100
+ } from "../chunk-RCRC2JKP.js";
101
101
  import {
102
102
  EGRESS_PROXY_RECOVERY_PHASE,
103
103
  EGRESS_PROXY_RECOVERY_REQUIRED,
@@ -136,7 +136,7 @@ import {
136
136
  workspaceSandboxRecoveryMessage,
137
137
  workspaceSandboxRecoveryRecommendedActions
138
138
  } from "../chunk-4LBCTFBP.js";
139
- import "../chunk-WVESWAJJ.js";
139
+ import "../chunk-MXNVSY6J.js";
140
140
  import "../chunk-LWSJK546.js";
141
141
  import "../chunk-74WXGIEK.js";
142
142
  import "../chunk-6A7MYOUI.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KNOWN_HARNESSES
3
- } from "../chunk-WVESWAJJ.js";
3
+ } from "../chunk-MXNVSY6J.js";
4
4
  import {
5
5
  composeSkills,
6
6
  renderInlineSkills
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  AGENT_PROFILE_EDITOR_SECTIONS,
3
3
  AgentProfileEditor
4
- } from "../chunk-CCTQQ45L.js";
5
- import "../chunk-XSPUKVHF.js";
4
+ } from "../chunk-CRHXK3QT.js";
5
+ import "../chunk-KICTB6RK.js";
6
6
  import "../chunk-R5K5ML3Q.js";
7
7
  import "../chunk-AA4FKEQW.js";
8
8
  import "../chunk-RASGKEAL.js";
9
- import "../chunk-WVESWAJJ.js";
9
+ import "../chunk-MXNVSY6J.js";
10
10
  import "../chunk-B7CL7ENS.js";
11
11
  import "../chunk-PV33JZKV.js";
12
12
  import "../chunk-AMUXRAUO.js";
@@ -166,7 +166,7 @@ import {
166
166
  AGENT_PROFILE_EDITOR_SECTIONS,
167
167
  AgentProfileEditor,
168
168
  AgentProfileRegistryError
169
- } from "../chunk-CCTQQ45L.js";
169
+ } from "../chunk-CRHXK3QT.js";
170
170
  import {
171
171
  EvidenceLineageTable,
172
172
  ExceptionList,
@@ -195,7 +195,7 @@ import {
195
195
  preloadWhenIdle,
196
196
  useComposerAttachments,
197
197
  useDictation
198
- } from "../chunk-ZEMO7S2G.js";
198
+ } from "../chunk-PZHZV6QY.js";
199
199
  import "../chunk-L7MB2LLM.js";
200
200
  import "../chunk-SJWIZT7B.js";
201
201
  import {
@@ -207,7 +207,7 @@ import {
207
207
  filterAcceptedFiles,
208
208
  isAcceptedFileType,
209
209
  renamePastedImages
210
- } from "../chunk-XSPUKVHF.js";
210
+ } from "../chunk-KICTB6RK.js";
211
211
  import "../chunk-R5K5ML3Q.js";
212
212
  import {
213
213
  DEFAULT_EFFORT_LEVELS,
@@ -235,7 +235,7 @@ import "../chunk-FQYAJYPW.js";
235
235
  import {
236
236
  ATTACHMENT_ACCEPT
237
237
  } from "../chunk-WTC6AOPL.js";
238
- import "../chunk-WVESWAJJ.js";
238
+ import "../chunk-MXNVSY6J.js";
239
239
  import "../chunk-B7CL7ENS.js";
240
240
  import "../chunk-PV33JZKV.js";
241
241
  import "../chunk-AMUXRAUO.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.60.170",
3
+ "version": "0.60.172",
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": [
@@ -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 TangleIdentitySsoExchangeResult {\n kind: 'identity'\n emailVerified: boolean\n user: TangleSsoExchangeResult['user']\n apiKey?: never\n keyId?: never\n}\n\nexport interface TangleIdentitySsoAuthClient {\n authorizeUrl(options: { state: string; redirectUri?: string }): string\n exchange(code: string): Promise<TangleIdentitySsoExchangeResult>\n}\n\n/** Token fields the callback persists from the runtime's OIDC exchange. */\nexport interface TangleOidcSsoTokens {\n accessToken: string\n tokenType: string\n expiresIn?: number\n refreshToken?: string\n idToken?: string\n scope?: string\n}\n\n/** Verified identity returned by the runtime's OIDC userinfo lookup. */\nexport interface TangleOidcSsoUser {\n id: string\n email: string\n emailVerified: true\n name?: string\n}\n\n/** The caller needs only the OIDC operations used by this callback. Keeping\n * this structural prevents legacy consumers from needing the optional\n * Runtime peer just to load the SSO declarations. */\nexport interface TangleOidcSsoAuthClient {\n authorizeUrl(options: {\n state: string\n codeChallenge: string\n nonce?: string\n prompt?: 'login' | 'consent' | 'none'\n loginHint?: string\n }): string\n exchange(code: string, codeVerifier: string): Promise<{\n tokens: TangleOidcSsoTokens\n user: TangleOidcSsoUser\n }>\n revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void>\n}\n\n/** Local account shape required by the verified Tangle SSO account policy.\n * Consumers map their user/link tables into this shape before resolving an\n * exchange. */\nexport interface TangleSsoLocalAccount {\n userId: string\n email: string\n emailVerified: boolean\n platformUserId: string | null\n}\n\n/** Reasons a local account lookup must stop instead of linking an exchange. */\nexport type TangleSsoAccountConflictReason =\n | 'ambiguous-platform-id'\n | 'platform-id-email-conflict'\n | 'unverified-email'\n | 'email-platform-id-conflict'\n | 'ambiguous-email'\n | 'invalid-platform-id'\n | 'invalid-email'\n | 'platform-match-mismatch'\n | 'email-match-mismatch'\n\n/** Result of applying the verified Tangle SSO account matching policy. */\nexport type TangleSsoAccountResolution =\n | { kind: 'existing'; userId: string; matchedBy: 'platform-id' | 'verified-email' }\n | { kind: 'create' }\n | { kind: 'reject'; reason: TangleSsoAccountConflictReason }\n\n/** Inputs for {@link resolveTangleSsoAccount}. `platformUserId` is the\n * platform's stable identity. */\nexport interface TangleSsoAccountResolutionInput {\n email: string\n platformMatches: readonly TangleSsoLocalAccount[]\n emailMatches: readonly TangleSsoLocalAccount[]\n platformUserId: string\n}\n\n/** Normalize an email for comparisons and persistence queries. */\nexport function normalizeTangleSsoEmail(email: string): string {\n return email.trim().toLowerCase()\n}\n\nfunction normalizedPlatformUserId(value: string | null | undefined): string | null {\n const normalized = value?.trim()\n return normalized ? normalized : null\n}\n\n/**\n * Resolve a platform exchange against all local rows found by stable platform\n * identity and normalized email.\n *\n * A stable platform id takes precedence over email. Every ambiguous or\n * contradictory result rejects, and email fallback requires a verified local\n * email that is not already bound to another platform id. The resolver is\n * pure so each consumer can query its own schema, including schemas without\n * unique indexes, then rerun it after an insert race.\n */\nexport function resolveTangleSsoAccount(\n input: TangleSsoAccountResolutionInput,\n): TangleSsoAccountResolution {\n const email = normalizeTangleSsoEmail(input.email)\n const platformUserId = normalizedPlatformUserId(input.platformUserId)\n if (!email) return { kind: 'reject', reason: 'invalid-email' }\n if (!platformUserId) return { kind: 'reject', reason: 'invalid-platform-id' }\n\n if (input.platformMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-platform-id' }\n }\n if (input.emailMatches.length > 1) {\n return { kind: 'reject', reason: 'ambiguous-email' }\n }\n\n if (input.platformMatches.some((account) => normalizedPlatformUserId(account.platformUserId) !== platformUserId)) {\n return { kind: 'reject', reason: 'platform-match-mismatch' }\n }\n if (input.emailMatches.some((account) => normalizeTangleSsoEmail(account.email) !== email)) {\n return { kind: 'reject', reason: 'email-match-mismatch' }\n }\n\n const platformMatch = input.platformMatches[0]\n const emailMatch = input.emailMatches[0]\n\n if (platformMatch) {\n if (emailMatch && emailMatch.userId !== platformMatch.userId) {\n return { kind: 'reject', reason: 'platform-id-email-conflict' }\n }\n return { kind: 'existing', userId: platformMatch.userId, matchedBy: 'platform-id' }\n }\n\n if (!emailMatch) return { kind: 'create' }\n const existingPlatformUserId = normalizedPlatformUserId(emailMatch.platformUserId)\n if (existingPlatformUserId === platformUserId) {\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'platform-id' }\n }\n if (emailMatch.emailVerified !== true) {\n return { kind: 'reject', reason: 'unverified-email' }\n }\n\n if (existingPlatformUserId && existingPlatformUserId !== platformUserId) {\n return { kind: 'reject', reason: 'email-platform-id-conflict' }\n }\n\n // The platform proof is already verified. A verified local email row is safe\n // to link when it is unbound or already carries this same platform id.\n return { kind: 'existing', userId: emailMatch.userId, matchedBy: 'verified-email' }\n}\n\n/** Thrown by an account store when the verified SSO policy rejects a link. */\nexport class TangleSsoAccountConflictError extends Error {\n readonly reason: TangleSsoAccountConflictReason\n\n constructor(reason: TangleSsoAccountConflictReason, message = 'Tangle SSO account linking was rejected') {\n super(message)\n this.name = 'TangleSsoAccountConflictError'\n this.reason = reason\n }\n}\n\n/** Thrown by `upsertUserByEmail` when the app-local user row cannot be\n * created; the callback handler maps it to `?error=tangle_user_create_failed`.\n * Any other store error propagates. */\nexport class TangleSsoUserCreateError extends Error {\n constructor(message = 'Failed to create local user for Tangle SSO') {\n super(message)\n this.name = 'TangleSsoUserCreateError'\n }\n}\n\n/**\n * Account persistence seam. Covers both storage styles in use: link-table\n * apps (a per-user platform-link row) and session-column apps (the key on the\n * session row) — `saveTangleLink` receives both `userId` and `sessionToken`,\n * and each app persists with the key it needs. `createSession` runs first so\n * the token is always available to `saveTangleLink`.\n */\nexport interface TangleSsoAccountStore {\n /** Resolve the local account before any user, session, or link write.\n * Implementations must query every candidate by stable platform id and\n * normalized email, then apply `resolveTangleSsoAccount` (or an equivalent\n * fail-closed policy). The callback rejects `kind: 'reject'` results before\n * it calls `upsertUserByEmail`. */\n resolveAccount(input: { email: string; platformUserId: string }): Promise<TangleSsoAccountResolution>\n /** Find-or-create the app-local user selected by `resolveAccount`.\n * `tangleUserId` is the platform's stable user id — match on it first when\n * the app stores it (emails are mutable on the platform; the id is not),\n * falling back to email for first-time logins. */\n upsertUserByEmail(input: {\n email: string\n name: string | null\n tangleUserId: string\n resolution: Exclude<TangleSsoAccountResolution, { kind: 'reject' }>\n }): Promise<{ userId: string }>\n /** Create an app session row; returns the session-cookie token value. */\n createSession(input: {\n userId: string\n expiresAt: Date\n ipAddress: string | null\n userAgent: string | null\n }): Promise<{ token: string }>\n /** Persist the platform link (API key + platform identity). */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n apiKey: string\n planTier: string | null\n }): Promise<void>\n}\n\n/**\n * OIDC tokens are server-side credentials, not API keys. Implementations must\n * encrypt them at rest and bind them to the exact local session. Refresh and\n * disconnect must serialize updates to the same grant.\n */\nexport interface TangleOidcSsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** Atomically bind the grant to this exact session. If persistence partially\n * fails, `deleteSession` must remove every row and credential for it. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n tokens: TangleOidcSsoTokens\n accessTokenExpiresAt: Date\n }): Promise<void>\n /** Remove an unpublished local session if callback persistence fails.\n * This must also remove any grant rows or credentials partially written by\n * a rejected `saveTangleLink`, keyed by this exact session token. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n/** Persist only the stable identity link for a first-party app. */\nexport interface TangleIdentitySsoAccountStore extends Omit<TangleSsoAccountStore, 'saveTangleLink'> {\n /** `upsertUserByEmail` may create the local user, but must not publish a new\n * platform link or update an existing user's verified-email state. The\n * callback has not yet created a session or prepared its cookie. */\n upsertUserByEmail: TangleSsoAccountStore['upsertUserByEmail']\n /** Commit the identity link and any verified-email update atomically. Do not\n * reject after committing. If the store can partially write before throwing,\n * `deleteSession` must remove only writes made for this session token. */\n saveTangleLink(input: {\n userId: string\n sessionToken: string\n tangleUserId: string\n email: string\n name: string | null\n }): Promise<void>\n /** Remove an unpublished session and any link or verification writes that a\n * rejected `saveTangleLink` made for its exact token. Preserve preexisting\n * links and other sessions. */\n deleteSession(input: { sessionToken: string }): Promise<void>\n}\n\n// ── Session cookie ──────────────────────────────────────────────────────────\n\n/** Successful-login context handed to the `setSessionCookie` seam. */\nexport interface TangleSsoSessionCookieArgs {\n /** Session token returned by `store.createSession`. */\n token: string\n /** Session expiry (now + `sessionTtlSeconds`). */\n expiresAt: Date\n /** Mirrors `sessionTtlSeconds` after defaulting. */\n ttlSeconds: number\n /** Mirrors `TangleSsoHandlerOptions.secureCookies`. */\n secure: boolean\n}\n\n/**\n * Sign a session token to better-call's signed-cookie contract — the value\n * better-auth's `getSignedCookie` verifies: `<token>.<signature>` where the\n * signature is the raw HMAC-SHA256 of the token under `secret`, encoded as\n * STANDARD base64 WITH padding (32 bytes → 44 chars ending `=`; better-call\n * rejects any other length or suffix, so url-safe/unpadded variants read back\n * as a null session). The joined value is percent-encoded once at cookie\n * serialization, matching better-call's `serializeSignedCookie` byte-exactly.\n */\nexport async function signSessionCookieValue(token: string, secret: string): Promise<string> {\n if (!secret) throw new Error('signSessionCookieValue requires a non-empty secret')\n const sig = await hmacBytes(secret, token)\n let bin = ''\n for (const byte of sig) bin += String.fromCharCode(byte)\n return `${token}.${btoa(bin)}`\n}\n\n/** Structural slice of a `betterAuth()` instance — only what cookie minting\n * reads. No better-auth import: the signing contract is implemented by\n * `signSessionCookieValue`, byte-compatible with better-auth's own\n * `makeSignature`. */\nexport interface BetterAuthSessionCookieSource {\n $context: PromiseLike<{\n secret: string\n authCookies: {\n sessionToken: {\n /** Final cookie name — better-auth decides the `__Secure-` prefix\n * (and any `advanced.cookiePrefix`) once at `betterAuth()` init. */\n name: string\n attributes: {\n secure?: boolean\n sameSite?: string\n path?: string\n httpOnly?: boolean\n domain?: string\n }\n }\n }\n }>\n}\n\n/** Define options to customize warning behavior for shadowed cookie names in authentication sessions */\nexport interface BetterAuthSessionCookieMinterOptions {\n /** Receives the shadowed-cookie-name warning (see below). Default\n * console.warn. */\n warn?: (message: string) => void\n}\n\n/**\n * Canonical `setSessionCookie` wiring for better-auth apps: mint the session\n * Set-Cookie exactly as better-auth's own login flows do — name + attributes\n * from `auth.$context.authCookies.sessionToken` (better-auth stays\n * authoritative over prefix/name/attributes) and the value signed to\n * better-call's `getSignedCookie` contract. A raw unprefixed\n * `better-auth.session_token` left by an earlier login is explicitly expired\n * so it cannot shadow the real cookie.\n *\n * Warns when the app's session cookie still has better-auth's DEFAULT name:\n * the Tangle platform (id.tangle.tools) sets a `Domain=.tangle.tools` cookie\n * under that exact name, and equal-path cookies are sent oldest-first — the\n * platform's cookie is always older (the user signs in there before the app's\n * callback runs), so the app reads the platform's token, fails its own\n * signature check, and every fresh login lands logged-out. Per-app\n * `advanced.cookiePrefix` is the fix.\n *\n * Throws on a domain-scoped session cookie for the same reason: a\n * `Domain=`-wide session cookie is exactly the shadowing footgun.\n */\nexport function createBetterAuthSessionCookieMinter(\n auth: BetterAuthSessionCookieSource,\n options: BetterAuthSessionCookieMinterOptions = {},\n): (args: TangleSsoSessionCookieArgs) => Promise<string[]> {\n const warn = options.warn ?? ((message: string) => console.warn(message))\n return async ({ token, ttlSeconds }) => {\n const ctx = await auth.$context\n if (!ctx.secret) {\n throw new Error('createBetterAuthSessionCookieMinter: auth context has no secret')\n }\n const { name, attributes } = ctx.authCookies.sessionToken\n if (attributes.domain) {\n throw new Error(\n `createBetterAuthSessionCookieMinter: refusing a domain-scoped session cookie (Domain=${attributes.domain}) — ` +\n 'a domain-wide session cookie shadows sibling apps that share the parent domain',\n )\n }\n if (name === DEFAULT_SESSION_COOKIE || name === `__Secure-${DEFAULT_SESSION_COOKIE}`) {\n warn(\n `[tangle-sso] session cookie is named \"${name}\" — better-auth's default. ` +\n 'The Tangle platform (id.tangle.tools) sets a Domain=.tangle.tools cookie under the same name, ' +\n \"and the platform's (older) cookie wins the Cookie-header order, so this app's sessions read back null. \" +\n \"Set a per-app prefix: betterAuth({ advanced: { cookiePrefix: '<app>' } }).\",\n )\n }\n const sameSite = typeof attributes.sameSite === 'string' ? attributes.sameSite : 'lax'\n const cookieOptions = {\n name,\n path: typeof attributes.path === 'string' ? attributes.path : '/',\n httpOnly: attributes.httpOnly !== false,\n sameSite: (sameSite.charAt(0).toUpperCase() + sameSite.slice(1)) as 'Lax' | 'Strict' | 'None',\n secure: Boolean(attributes.secure) || name.startsWith('__Secure-'),\n maxAgeSeconds: ttlSeconds,\n }\n const cookies = [serializeCookie(await signSessionCookieValue(token, ctx.secret), cookieOptions)]\n if (name !== DEFAULT_SESSION_COOKIE) {\n cookies.push(clearCookieHeader({ ...cookieOptions, name: DEFAULT_SESSION_COOKIE }))\n }\n return cookies\n }\n}\n\n// ── Handlers ────────────────────────────────────────────────────────────────\n\n/** Define configuration options for handling Tangle SSO authentication and session management */\nexport interface TangleSsoHandlerOptions {\n protocol?: 'legacy'\n auth: TangleSsoAuthClient\n store: TangleSsoAccountStore\n /** HMAC secret for the state cookie. */\n stateSecret: string\n /** Absolute callback URL registered with the platform. */\n callbackUrl: string\n stateCookieName: string\n /** Default 'better-auth.session_token'. Ignored when `setSessionCookie` is\n * provided. The default path prepends `__Secure-` iff `secureCookies`. */\n sessionCookieName?: string\n /** Mint the host auth framework's own session cookie(s); return complete\n * Set-Cookie header values (the handler appends them verbatim and sets no\n * session cookie itself). Supply this when the framework should stay\n * authoritative over name/prefix/signing/attributes — e.g. better-auth:\n * `auth.$context.authCookies.sessionToken` + `makeSignature`. */\n setSessionCookie?: (\n args: TangleSsoSessionCookieArgs,\n ) => readonly string[] | Promise<readonly string[]>\n /** HMAC-SHA256 secret the host auth framework verifies session cookies with\n * (better-auth: its `secret`). Required when `setSessionCookie` is absent —\n * the default cookie is minted to better-call's signed contract via\n * `signSessionCookieValue`; an unsigned or mis-signed value reads back as a\n * null session, so there is deliberately no fallback to `stateSecret`\n * (which is not guaranteed to be the auth secret). */\n sessionCookieSecret?: string\n /** Adds `Secure` to every cookie this module sets, and (default session\n * cookie only) the `__Secure-` name prefix. Must match the auth\n * framework's own secure-cookie decision (better-auth: https `baseURL` /\n * `advanced.useSecureCookies`), or it will look up a different cookie name\n * than the one set here. */\n secureCookies: boolean\n /** Default 604 800 (7 days). */\n sessionTtlSeconds?: number\n /** Default 600. Applies to both the cookie Max-Age and the signed TTL. */\n stateTtlSeconds?: number\n /** Default '/app'. */\n defaultRedirectPath?: string\n /** Default '/login'. */\n loginPath?: string\n /** Failure log hook (e.g. console.error). Default no-op. */\n log?: (message: string, error?: unknown) => void\n now?: () => number\n}\n\n/** Registered OIDC client and token store. The OIDC caller supplies the Runtime PKCE helper. */\nexport interface TangleOidcSsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'oidc'\n auth: TangleOidcSsoAuthClient\n store: TangleOidcSsoAccountStore\n /** Pass the Runtime's `createPkcePair` helper from the OIDC-only caller. */\n createPkcePair: () => Promise<{ verifier: string; challenge: string }>\n}\n\nexport interface TangleIdentitySsoHandlerOptions extends Omit<TangleSsoHandlerOptions, 'protocol' | 'auth' | 'store'> {\n protocol: 'identity'\n auth: TangleIdentitySsoAuthClient\n store: TangleIdentitySsoAccountStore\n}\n\n/** Define handlers for SSO start and callback routes managing authentication flow and session cookies */\nexport interface TangleSsoHandlers {\n /** GET start route: mint + sign state, set the state cookie, 302 to the\n * platform authorize URL. `?redirect=` carries the post-login path. */\n start(request: Request): Promise<Response>\n /** GET callback route: verify state, exchange the code, upsert the user,\n * create the session, save the platform link, set the session cookie\n * (via the `setSessionCookie` seam, else signed to better-call's contract\n * with `sessionCookieSecret`), 302 to the saved redirect. Every failure\n * 302s to `loginPath?error=…` with the state cookie cleared. */\n callback(request: Request): Promise<Response>\n}\n\n/** Redirect targets must stay on this app's origin. A prefix check on `/`\n * and `//` is not enough: in the WHATWG parser a backslash is a path\n * separator for special schemes, so `/\\host` resolves to `https://host/`,\n * and tab or newline are stripped before parsing, so `/\\t/host` is `//host`.\n * Parse the value against a fixed origin and accept it only when that origin\n * survives; the result is the parser's normalised path, query and fragment.\n * Backslash and control characters are rejected up front as well, so the\n * decision does not rest on one parser's handling of them.\n *\n * The input staying on-origin is not enough either: dot-segment removal\n * turns `/.//host` or `/a/..//host` into the pathname `//host` with the\n * origin intact, and a browser resolves that Location off-origin. So the\n * emitted value must itself resolve on-origin and come back unchanged. */\nconst REDIRECT_REFERENCE_ORIGIN = 'https://redirect-reference.invalid'\n\nfunction sanitizeRedirectPath(value: string | null, fallback: string): string {\n if (!value || !value.startsWith('/') || /[\\\\\\u0000-\\u001f\\u007f]/.test(value)) return fallback\n const path = resolveOnReferenceOrigin(value)\n if (path === null || resolveOnReferenceOrigin(path) !== path) return fallback\n return path\n}\n\n/** `path + search + hash` of `value` resolved against the reference origin,\n * or null when the parse fails or lands on another origin. */\nfunction resolveOnReferenceOrigin(value: string): string | null {\n let url: URL\n try {\n url = new URL(value, REDIRECT_REFERENCE_ORIGIN)\n } catch {\n return null\n }\n if (url.origin !== REDIRECT_REFERENCE_ORIGIN) return null\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nfunction redirectResponse(location: string, headers = new Headers()): Response {\n headers.set('Location', location)\n headers.set('Cache-Control', 'no-store')\n headers.set('Referrer-Policy', 'no-referrer')\n return new Response(null, { status: 302, headers })\n}\n\n/** Real client IP: `CF-Connecting-IP` behind Cloudflare, else the first\n * `x-forwarded-for` hop (the rest of the list is sender-controlled). */\nfunction clientIp(request: Request): string | null {\n return (\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n null\n )\n}\n\ninterface StateCookiePayload {\n s: string\n r: string\n v?: string\n m?: string\n}\n\nfunction parseStateCookiePayload(raw: string | null): StateCookiePayload | null {\n if (!raw || raw.length > 4096) return null\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed === null || typeof parsed !== 'object') return null\n const { s, r, v, m } = parsed as Record<string, unknown>\n if (typeof s !== 'string' || typeof r !== 'string') return null\n return { s, r, ...(typeof v === 'string' ? { v } : {}), ...(typeof m === 'string' ? { m } : {}) }\n } catch { return null }\n}\n\nfunction stateBinding(payload: StateCookiePayload, callbackUrl: string): string {\n return JSON.stringify([payload.s, payload.r, payload.v ?? null, callbackUrl])\n}\n\n/** Create Tangle SSO handlers to manage authentication state, callbacks, and session cookies */\nexport function createTangleSsoHandlers(\n opts: TangleSsoHandlerOptions | TangleOidcSsoHandlerOptions | TangleIdentitySsoHandlerOptions,\n): TangleSsoHandlers {\n if (!opts.stateSecret) throw new Error('TangleSsoHandlerOptions.stateSecret is required')\n if (!opts.callbackUrl) throw new Error('TangleSsoHandlerOptions.callbackUrl is required')\n if (!opts.stateCookieName) throw new Error('TangleSsoHandlerOptions.stateCookieName is required')\n const callbackUrl = new URL(opts.callbackUrl)\n // Startup hardening applies to EVERY protocol. The checks were previously\n // gated on 'oidc' | 'identity', which left 'legacy' (the default when\n // `protocol` is omitted — the configuration every product shipped before\n // the typed variants existed) accepting a short stateSecret, a non-HTTPS\n // callback, credentials/query/fragment in the callback URL, and https with\n // non-Secure cookies (issue #749). The same configuration rules protect\n // every flow; only the runtime origin checks remain protocol-specific.\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 const sessionCookieName = opts.sessionCookieName ?? DEFAULT_SESSION_COOKIE\n let mintSessionCookies: (args: TangleSsoSessionCookieArgs) => Promise<readonly string[]>\n if (opts.setSessionCookie) {\n const seam = opts.setSessionCookie\n mintSessionCookies = async (args) => await seam(args)\n } else if (opts.sessionCookieSecret) {\n const secret = opts.sessionCookieSecret\n mintSessionCookies = async ({ token, secure, ttlSeconds }) => [\n serializeCookie(await signSessionCookieValue(token, secret), {\n name: secure ? `__Secure-${sessionCookieName}` : sessionCookieName,\n secure,\n maxAgeSeconds: ttlSeconds,\n }),\n ]\n } else {\n throw new Error('TangleSsoHandlerOptions requires setSessionCookie or sessionCookieSecret')\n }\n const sessionTtlSeconds = opts.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS\n const stateTtlSeconds = opts.stateTtlSeconds ?? DEFAULT_STATE_TTL_SECONDS\n if (!Number.isFinite(stateTtlSeconds) || stateTtlSeconds <= 0\n || !Number.isFinite(sessionTtlSeconds) || sessionTtlSeconds <= 0) {\n throw new Error('SSO lifetimes must be positive and finite')\n }\n const defaultRedirectPath = sanitizeRedirectPath(opts.defaultRedirectPath ?? DEFAULT_REDIRECT_PATH, DEFAULT_REDIRECT_PATH)\n const loginPath = sanitizeRedirectPath(opts.loginPath ?? DEFAULT_LOGIN_PATH, DEFAULT_LOGIN_PATH)\n const log = opts.log ?? (() => {})\n const now = opts.now ?? Date.now\n const stateConfig: SsoStateConfig = { secret: opts.stateSecret, ttlMs: stateTtlSeconds * 1000, now }\n const stateCookieOpts = { name: opts.stateCookieName, secure: opts.secureCookies }\n function loginErrorRedirect(code: string): Response {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const target = new URL(loginPath, REDIRECT_REFERENCE_ORIGIN)\n target.searchParams.set('error', code)\n return redirectResponse(`${target.pathname}${target.search}${target.hash}`, headers)\n }\n return {\n async start(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity') && url.origin !== callbackUrl.origin) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const redirectPath = sanitizeRedirectPath(url.searchParams.get('redirect'), defaultRedirectPath)\n const state = await createSignedSsoState(stateConfig)\n const payload: StateCookiePayload = { s: state, r: redirectPath }\n let authorizationUrl: string\n if (opts.protocol === 'oidc') {\n const pkce = await opts.createPkcePair()\n payload.v = pkce.verifier\n payload.m = await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))\n authorizationUrl = opts.auth.authorizeUrl({ state, codeChallenge: pkce.challenge })\n if (new URL(authorizationUrl).searchParams.get('redirect_uri') !== opts.callbackUrl) {\n throw new Error('PlatformOidcClient.redirectUri must equal callbackUrl')\n }\n } else {\n authorizationUrl = opts.auth.authorizeUrl({ state, redirectUri: opts.callbackUrl })\n }\n const cookie = serializeCookie(JSON.stringify(payload), {\n ...stateCookieOpts,\n maxAgeSeconds: stateTtlSeconds,\n })\n const headers = new Headers()\n headers.append('Set-Cookie', cookie)\n return redirectResponse(authorizationUrl, headers)\n },\n async callback(request) {\n if (request.method !== 'GET') return new Response(null, { status: 405, headers: { Allow: 'GET' } })\n const url = new URL(request.url)\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity')\n && (url.origin !== callbackUrl.origin || url.pathname !== callbackUrl.pathname)) {\n return loginErrorRedirect('tangle_origin_mismatch')\n }\n const code = url.searchParams.get('code')\n const stateFromPlatform = url.searchParams.get('state')\n if (!code || !stateFromPlatform || url.searchParams.has('error')) return loginErrorRedirect('tangle_callback_missing')\n const payload = parseStateCookiePayload(readCookieValue(request.headers.get('cookie'), opts.stateCookieName))\n if (!payload || !constantTimeEqual(payload.s, stateFromPlatform)) return loginErrorRedirect('tangle_state_mismatch')\n if (!(await verifySignedSsoState(payload.s, stateConfig))) return loginErrorRedirect('tangle_state_mismatch')\n if (opts.protocol === 'oidc' && (!payload.v || !/^[A-Za-z0-9_-]{43,128}$/.test(payload.v)\n || !payload.m || !constantTimeEqual(payload.m, await hmacHex(opts.stateSecret, stateBinding(payload, opts.callbackUrl))))) {\n return loginErrorRedirect('tangle_state_mismatch')\n }\n const tokenRequestedAt = now()\n let oidcTokens: TangleOidcSsoTokens | undefined\n let legacyExchange: TangleSsoExchangeResult | undefined\n let createdSession: string | undefined\n let committed = false\n try {\n let exchanged: { user: TangleSsoExchangeResult['user']; emailVerified: boolean }\n try {\n if (opts.protocol === 'oidc') {\n const result = await opts.auth.exchange(code, payload.v!)\n oidcTokens = result.tokens\n if (!oidcTokens.refreshToken || !Number.isFinite(oidcTokens.expiresIn) || oidcTokens.expiresIn! <= 0) {\n throw new Error('OIDC requires offline_access and a finite access-token lifetime')\n }\n exchanged = { user: result.user, emailVerified: result.user.emailVerified }\n } else if (opts.protocol === 'identity') {\n const identity = await opts.auth.exchange(code)\n if (identity.kind !== 'identity' || 'apiKey' in identity || 'keyId' in identity\n || !identity.user || typeof identity.user.id !== 'string' || !identity.user.id.trim()\n || typeof identity.user.email !== 'string' || !identity.user.email.trim()) {\n throw new Error('Identity exchange returned an invalid result')\n }\n exchanged = identity\n } else {\n legacyExchange = await opts.auth.exchange(code)\n exchanged = legacyExchange\n }\n } catch {\n log('[tangle-sso] exchange failed')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n if (exchanged.emailVerified !== true) {\n log('[tangle-sso] exchange did not include a verified-email proof')\n return loginErrorRedirect('tangle_exchange_failed')\n }\n let userId: string\n try {\n const email = normalizeTangleSsoEmail(exchanged.user.email)\n const resolution = await opts.store.resolveAccount({ email, platformUserId: exchanged.user.id })\n if (resolution.kind === 'reject') throw new TangleSsoAccountConflictError(resolution.reason)\n ;({ userId } = await opts.store.upsertUserByEmail({\n email,\n name: exchanged.user.name ?? null,\n tangleUserId: exchanged.user.id,\n resolution,\n }))\n } catch (err) {\n if (err instanceof TangleSsoAccountConflictError) {\n log('[tangle-sso] account linking conflict', err.reason)\n return loginErrorRedirect('tangle_account_conflict')\n }\n if (err instanceof TangleSsoUserCreateError) return loginErrorRedirect('tangle_user_create_failed')\n throw err\n }\n const expiresAt = new Date(now() + sessionTtlSeconds * 1000)\n const { token } = await opts.store.createSession({\n userId,\n expiresAt,\n ipAddress: clientIp(request),\n userAgent: request.headers.get('user-agent'),\n })\n createdSession = token\n const link = {\n userId,\n sessionToken: token,\n tangleUserId: exchanged.user.id,\n email: normalizeTangleSsoEmail(exchanged.user.email),\n name: exchanged.user.name ?? null,\n }\n if (opts.protocol === 'oidc') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink({\n ...link,\n tokens: oidcTokens!,\n accessTokenExpiresAt: new Date(tokenRequestedAt + oidcTokens!.expiresIn! * 1000),\n })\n committed = true\n return response\n }\n if (opts.protocol === 'identity') {\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n const response = redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n await opts.store.saveTangleLink(link)\n committed = true\n return response\n }\n await opts.store.saveTangleLink({ ...link, apiKey: legacyExchange!.apiKey, planTier: legacyExchange!.plan?.tier ?? null })\n const headers = new Headers()\n headers.append('Set-Cookie', clearCookieHeader(stateCookieOpts))\n const sessionCookies = await mintSessionCookies({ token, expiresAt, ttlSeconds: sessionTtlSeconds, secure: opts.secureCookies })\n for (const cookie of sessionCookies) headers.append('Set-Cookie', cookie)\n return redirectResponse(sanitizeRedirectPath(payload.r, defaultRedirectPath), headers)\n } catch (error) {\n if (opts.protocol !== 'oidc' && opts.protocol !== 'identity') throw error\n log('[tangle-sso] local session persistence failed')\n return loginErrorRedirect('tangle_session_failed')\n } finally {\n if ((opts.protocol === 'oidc' || opts.protocol === 'identity') && !committed) {\n if (createdSession) {\n try { await opts.store.deleteSession({ sessionToken: createdSession }) }\n catch { log('[tangle-sso] unpublished session cleanup failed') }\n }\n if (opts.protocol === 'oidc' && oidcTokens) {\n try { if (oidcTokens.refreshToken) await opts.auth.revoke(oidcTokens.refreshToken, 'refresh_token') }\n catch { log('[tangle-sso] rejected grant refresh revocation failed') }\n try { await opts.auth.revoke(oidcTokens.accessToken, 'access_token') }\n catch { log('[tangle-sso] rejected grant access revocation failed') }\n }\n }\n }\n },\n }\n}\n","/**\n * Request guards for agent-app routes: session auth (302 redirect for pages,\n * JSON 401 for APIs), admin allowlisting (404 — the route stays invisible to\n * non-admins), and the billable-balance gate (402 with a stable code).\n * Session resolution is a seam; thrown Responses follow the router convention\n * of surfacing a thrown Response as the route result.\n */\n\nimport { isTangleBillingEnforcementDisabled } from '../runtime/model'\n\n/** Define options for configuring authentication guard behavior and session retrieval */\nexport interface AuthGuardOptions<Session> {\n /** e.g. a better-auth `auth.api.getSession` wrapped by the app. */\n getSession(request: Request): Promise<Session | null | undefined>\n /** Default '/login'. */\n loginPath?: string\n}\n\n/** Resolve user authentication and session requirements for page and API requests */\nexport interface AuthGuard<Session> {\n /** Page guard — throws a 302 redirect Response to `loginPath`. */\n requireUser(request: Request): Promise<Session>\n /** API guard — throws JSON 401 `{ error: 'Unauthorized', code: 'auth.unauthenticated' }`. */\n requireApiUser(request: Request): Promise<Session>\n /** `apiResponse` selects the 401 JSON path over the redirect. */\n requireSession(request: Request, opts?: { apiResponse?: boolean }): Promise<Session>\n getOptionalSession(request: Request): Promise<Session | null>\n}\n\n/** Create an authentication guard that enforces session presence and handles unauthorized access responses */\nexport function createAuthGuard<Session>(opts: AuthGuardOptions<Session>): AuthGuard<Session> {\n const loginPath = opts.loginPath ?? '/login'\n\n async function requireSession(request: Request, o: { apiResponse?: boolean } = {}): Promise<Session> {\n const session = await opts.getSession(request)\n if (!session) {\n if (o.apiResponse) {\n throw Response.json({ error: 'Unauthorized', code: 'auth.unauthenticated' }, { status: 401 })\n }\n throw new Response(null, { status: 302, headers: { Location: loginPath } })\n }\n return session\n }\n\n return {\n requireSession,\n requireUser: (request) => requireSession(request),\n requireApiUser: (request) => requireSession(request, { apiResponse: true }),\n getOptionalSession: async (request) => (await opts.getSession(request)) ?? null,\n }\n}\n\n/** Resolve a value or an HTTP response indicating failure in a guarded operation */\nexport type GuardResolution<T> = { ok: true; value: T } | { ok: false; response: Response }\n\n/**\n * Adapt a guard that THROWS a Response (the quartet above — the router\n * convention) to the `{ok: true, value} | {ok: false, response}` resolution\n * shape the route factories take (`/chat-routes` `authorize`,\n * `/interactions` `resolveConnection`, `/chat-routes` upload `authorize`).\n * Every product wrote this try/catch by hand; it lives here once.\n */\nexport async function guardResolution<T>(run: () => Promise<T>): Promise<GuardResolution<T>> {\n try {\n return { ok: true, value: await run() }\n } catch (err) {\n if (err instanceof Response) return { ok: false, response: err }\n throw err\n }\n}\n\n/** Comma/whitespace separated → trimmed, lowercased, empties dropped. */\nexport function parseAdminEmails(raw: string | null | undefined): string[] {\n return (raw ?? '')\n .split(/[,\\s]+/)\n .map((e) => e.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** Define options to resolve user session and control access based on allowed admin emails */\nexport interface AdminGuardOptions<Session> {\n requireUser(request: Request): Promise<Session>\n emailOf(session: Session): string | null | undefined\n /** Resolved per request; an EMPTY allowlist refuses everyone. */\n allowedEmails(): string[]\n}\n\n/** Non-admins (and empty allowlists) get 404, keeping the route invisible —\n * better than a \"forbidden\" footprint that advertises its existence. */\nexport function createAdminGuard<Session>(opts: AdminGuardOptions<Session>): (request: Request) => Promise<Session> {\n return async (request) => {\n const session = await opts.requireUser(request)\n const allowed = opts.allowedEmails()\n if (allowed.length === 0) throw new Response('Not found', { status: 404 })\n const email = (opts.emailOf(session) ?? '').toLowerCase()\n if (!allowed.includes(email)) throw new Response('Not found', { status: 404 })\n return session\n }\n}\n\n/** Describe the billable balance state including overage permission and remaining USD balance */\nexport interface BillableBalanceState {\n overageAllowed: boolean\n remainingBalanceUsd: number\n}\n\n/** Define options to assert and customize billable balance enforcement behavior */\nexport interface AssertBillableBalanceOptions {\n env?: Record<string, string | undefined>\n /** App-specific enforcement override flag (e.g. 'GTM_BILLING_ENFORCEMENT'),\n * fed to `isTangleBillingEnforcementDisabled`. */\n enforcementEnvVar?: string\n /** Default 'Add balance or upgrade your plan to invoke this agent.'. */\n errorMessage?: string\n /** Merged into the 402 JSON body (e.g. `{ organizationId }`). */\n errorBody?: Record<string, unknown>\n}\n\n/**\n * Gate a billable turn: passes when enforcement is disabled (dev default),\n * the tier allows overage, or remaining balance is positive. Otherwise throws\n * a 402 Response with the stable `billing.balance_required` code so clients\n * can route to the billing screen.\n */\nexport function assertBillableBalance(state: BillableBalanceState, opts: AssertBillableBalanceOptions = {}): void {\n if (isTangleBillingEnforcementDisabled({ env: opts.env, enforcementEnvVar: opts.enforcementEnvVar })) return\n if (state.overageAllowed || state.remainingBalanceUsd > 0) return\n // errorBody first: the stable error/code contract always wins over caller extras.\n throw Response.json(\n {\n ...opts.errorBody,\n error: opts.errorMessage ?? 'Add balance or upgrade your plan to invoke this agent.',\n code: 'billing.balance_required',\n },\n { status: 402 },\n )\n}\n"],"mappings":"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","names":["headers","sessionCookies"]}