@tangle-network/agent-app 0.48.2 → 0.48.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/DesignCanvas-YLVDCX6H.js +11 -0
  2. package/dist/DesignCanvasEditor-4QWY2LHM.js +14 -0
  3. package/dist/app-auth/index.js +2 -2
  4. package/dist/chat-routes/completed-sandbox-turn.d.ts +3 -3
  5. package/dist/chat-routes/detached-turn.d.ts +2 -2
  6. package/dist/chat-routes/draft-persistence.d.ts +1 -1
  7. package/dist/chat-routes/index.js +33 -37
  8. package/dist/chat-routes/index.js.map +1 -1
  9. package/dist/{chunk-VHMZ3KUH.js → chunk-2WVBGQ2Y.js} +2 -2
  10. package/dist/chunk-3NM4GCAY.js +170 -0
  11. package/dist/chunk-3NM4GCAY.js.map +1 -0
  12. package/dist/{chunk-WYHFHTSQ.js → chunk-BWQPVS7D.js} +2 -2
  13. package/dist/{chunk-IAY2DMKW.js → chunk-EA4UVS4T.js} +61 -2
  14. package/dist/chunk-EA4UVS4T.js.map +1 -0
  15. package/dist/{chunk-AKMQFCDL.js → chunk-FEVVF6JJ.js} +2 -2
  16. package/dist/{chunk-JF423ASS.js → chunk-G2B23BZ6.js} +5 -5
  17. package/dist/{chunk-JAT6SOPK.js → chunk-KWN5PGXM.js} +3 -3
  18. package/dist/{chunk-SYWEMKZR.js → chunk-MFLUDFD3.js} +3 -3
  19. package/dist/chunk-MFLUDFD3.js.map +1 -0
  20. package/dist/{chunk-CSMJM7YT.js → chunk-TWTLQWES.js} +2 -2
  21. package/dist/{chunk-KX2GA3L6.js → chunk-WAMJSZYB.js} +3 -3
  22. package/dist/design-canvas/index.js +2 -2
  23. package/dist/design-canvas-react/engine.js +4 -4
  24. package/dist/design-canvas-react/index.js +7 -7
  25. package/dist/design-canvas-react/lazy.js +1 -1
  26. package/dist/platform/index.js +2 -2
  27. package/dist/preset-cloudflare/detached-turn-workflow.d.ts +6 -20
  28. package/dist/preset-cloudflare/index.js +13 -10
  29. package/dist/preset-cloudflare/index.js.map +1 -1
  30. package/dist/public-consultation/index.js +3 -3
  31. package/dist/sequences/index.js +1 -1
  32. package/dist/stream/index.d.ts +1 -0
  33. package/dist/stream/index.js +7 -1
  34. package/dist/stream/turn-observation.d.ts +32 -0
  35. package/dist/web/api-key-fetch.d.ts +12 -0
  36. package/dist/web/index.d.ts +1 -0
  37. package/dist/web/index.js +3 -1
  38. package/package.json +1 -1
  39. package/dist/DesignCanvas-MWEYD6PR.js +0 -11
  40. package/dist/DesignCanvasEditor-UZ2S6BDD.js +0 -14
  41. package/dist/chunk-IAY2DMKW.js.map +0 -1
  42. package/dist/chunk-JEZJ6HTF.js +0 -77
  43. package/dist/chunk-JEZJ6HTF.js.map +0 -1
  44. package/dist/chunk-SYWEMKZR.js.map +0 -1
  45. /package/dist/{DesignCanvas-MWEYD6PR.js.map → DesignCanvas-YLVDCX6H.js.map} +0 -0
  46. /package/dist/{DesignCanvasEditor-UZ2S6BDD.js.map → DesignCanvasEditor-4QWY2LHM.js.map} +0 -0
  47. /package/dist/{chunk-VHMZ3KUH.js.map → chunk-2WVBGQ2Y.js.map} +0 -0
  48. /package/dist/{chunk-WYHFHTSQ.js.map → chunk-BWQPVS7D.js.map} +0 -0
  49. /package/dist/{chunk-AKMQFCDL.js.map → chunk-FEVVF6JJ.js.map} +0 -0
  50. /package/dist/{chunk-JF423ASS.js.map → chunk-G2B23BZ6.js.map} +0 -0
  51. /package/dist/{chunk-JAT6SOPK.js.map → chunk-KWN5PGXM.js.map} +0 -0
  52. /package/dist/{chunk-CSMJM7YT.js.map → chunk-TWTLQWES.js.map} +0 -0
  53. /package/dist/{chunk-KX2GA3L6.js.map → chunk-WAMJSZYB.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/preset-cloudflare/workflow-instance.ts","../../src/preset-cloudflare/detached-turn-workflow.ts","../../src/preset-cloudflare/headers.ts","../../src/preset-cloudflare/index.ts"],"sourcesContent":["/**\n * The structural part of a Cloudflare Workflow instance used to prove that a\n * failed create call actually left an addressable durable instance behind.\n */\nexport interface CloudflareWorkflowInstanceLike<TStatus = unknown> {\n status(): Promise<TStatus>\n}\n\n/** The structural binding implemented by Cloudflare's `Workflow<TParams>`. */\nexport interface CloudflareWorkflowBindingLike<\n TParams,\n TStatus = unknown,\n TInstance extends CloudflareWorkflowInstanceLike<TStatus> =\n CloudflareWorkflowInstanceLike<TStatus>,\n> {\n create(input: { id: string; params: TParams }): Promise<TInstance>\n get(id: string): Promise<TInstance>\n}\n\nexport interface EnsureCloudflareWorkflowInstanceResult<\n TStatus,\n TInstance extends CloudflareWorkflowInstanceLike<TStatus>,\n> {\n instance: TInstance\n /** False only when create failed but the same id was proven readable. */\n created: boolean\n /** Present on reuse because reading status is the proof the instance exists. */\n status?: TStatus\n}\n\n/**\n * Create one durable Workflow instance, or prove an instance with the same id\n * already exists before accepting a create error as an idempotent retry.\n *\n * Cloudflare does not expose a stable duplicate-error class. Matching error\n * text can therefore swallow unrelated failures. This helper uses the durable\n * resource itself as proof: `get(id).status()` must succeed, otherwise the\n * original create error is rethrown.\n */\nexport async function ensureCloudflareWorkflowInstance<\n TParams,\n TStatus,\n TInstance extends CloudflareWorkflowInstanceLike<TStatus>,\n>(\n binding: CloudflareWorkflowBindingLike<TParams, TStatus, TInstance>,\n input: { id: string; params: TParams },\n): Promise<EnsureCloudflareWorkflowInstanceResult<TStatus, TInstance>> {\n try {\n return {\n instance: await binding.create(input),\n created: true,\n }\n } catch (createError) {\n try {\n const instance = await binding.get(input.id)\n const status = await instance.status()\n return { instance, created: false, status }\n } catch {\n throw createError\n }\n }\n}\n","import type { TurnDriveResult } from '@tangle-network/sandbox'\nimport type { Outcome } from '../sandbox/outcome'\n\n/** The stable identity a Workflow reuses on every retry of one turn. */\nexport interface DetachedTurnWorkflowIdentity {\n /** Sandbox session resume key. */\n sessionId: string\n /** Sandbox completed-turn idempotency key. */\n turnId: string\n}\n\n/** The part of Cloudflare's Workflow event needed by the tick. */\nexport interface CloudflareWorkflowEventLike<TPayload> {\n payload: TPayload\n}\n\n/** A duration accepted by Cloudflare Workflow `step.sleep`. */\nexport type CloudflareWorkflowSleepDuration =\n | `${number} ${\n | 'second'\n | 'seconds'\n | 'minute'\n | 'minutes'\n | 'hour'\n | 'hours'\n | 'day'\n | 'days'\n | 'week'\n | 'weeks'\n | 'month'\n | 'months'\n | 'year'\n | 'years'}`\n | number\n\n/** The durable Workflow operations used by the tick. */\nexport interface CloudflareWorkflowStepLike {\n do<T>(name: string, callback: (context: unknown) => Promise<T>): Promise<T>\n sleep(name: string, duration: CloudflareWorkflowSleepDuration): Promise<void>\n}\n\n/** The states returned by the Sandbox `driveTurn` primitive. */\nexport type DetachedTurnDriveState = TurnDriveResult['state']\n\n/** The terminal result passed to product settlement. */\nexport type DetachedTurnTerminalResult = Exclude<TurnDriveResult, { state: 'running' }>\n\n/** A drive call's retryable transport boundary. */\nexport type DetachedTurnDriveOutcome = Outcome<TurnDriveResult>\n\n/** Options for one durable detached-turn Workflow run. */\nexport interface DetachedTurnWorkflowTickOptions<\n TPayload extends DetachedTurnWorkflowIdentity,\n TSettled,\n> {\n /** The Workflow event. Its payload must keep the same session and turn ids. */\n event: CloudflareWorkflowEventLike<TPayload>\n /** Cloudflare's durable step runner. */\n step: CloudflareWorkflowStepLike\n /**\n * Run exactly one `driveSandboxTurn`/`box.driveTurn` pass.\n *\n * The callback runs inside one `step.do`. A transport failure is thrown from\n * that step so Cloudflare retries the pass with the same deterministic ids.\n */\n drive: (\n payload: TPayload,\n ) => Promise<DetachedTurnDriveOutcome>\n /**\n * Read the completed Sandbox cache/session records and persist the product's\n * deterministic assistant row. This callback MUST be idempotent because a\n * Worker can stop after the write and before the Workflow step commits.\n */\n settle: (payload: TPayload, result: DetachedTurnTerminalResult) => Promise<TSettled>\n /** Delay between one-tick drive passes. */\n pollDelay?: CloudflareWorkflowSleepDuration\n /** Stable prefix for the Workflow's drive and wait step names. */\n stepName?: string\n}\n\nfunction assertIdentity(payload: DetachedTurnWorkflowIdentity): void {\n if (!payload || typeof payload.sessionId !== 'string' || !payload.sessionId.trim()) {\n throw new Error('detached turn Workflow payload requires a non-empty sessionId')\n }\n if (typeof payload.turnId !== 'string' || !payload.turnId.trim()) {\n throw new Error('detached turn Workflow payload requires a non-empty turnId')\n }\n}\n\nfunction isKnownDriveState(state: unknown): state is DetachedTurnDriveState {\n return state === 'running'\n || state === 'completed'\n || state === 'failed'\n || state === 'awaiting_plan_decision'\n}\n\n/**\n * Drive one detached Sandbox turn from a Cloudflare Workflow.\n *\n * Each drive step makes one prompt admission/status pass and returns promptly.\n * A running pass sleeps durably before the next pass. The final settlement is\n * also a step, so replayed Workflow execution never holds a live stream open.\n */\nexport async function runDetachedTurnWorkflowTick<\n TPayload extends DetachedTurnWorkflowIdentity,\n TSettled,\n>(\n options: DetachedTurnWorkflowTickOptions<TPayload, TSettled>,\n): Promise<TSettled> {\n const payload = options.event?.payload\n assertIdentity(payload)\n const name = options.stepName ?? 'detached-turn'\n const delay = options.pollDelay ?? '5 seconds'\n\n let attempt = 0\n let terminalResult: DetachedTurnTerminalResult\n while (true) {\n const driveResult = await options.step.do(`${name}:drive:${attempt}`, async () => {\n const outcome = await options.drive(payload)\n if (!outcome.succeeded) throw outcome.error\n return outcome.value\n })\n const state = (driveResult as { state?: unknown } | null)?.state\n if (!isKnownDriveState(state)) {\n throw new Error(`detached turn drive returned unknown state: ${String(state)}`)\n }\n if (state !== 'running') {\n terminalResult = driveResult as DetachedTurnTerminalResult\n break\n }\n await options.step.sleep(`${name}:wait:${attempt}`, delay)\n attempt += 1\n }\n\n return options.step.do(`${name}:settle`, () => options.settle(payload, terminalResult))\n}\n","const HEADER_NAME = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/\n\n/** One path rule in Cloudflare's static-asset `_headers` file. */\nexport interface CloudflareHeadersRule {\n pattern: string\n headers: Readonly<Record<string, string>>\n}\n\nfunction assertSingleLine(value: string, label: string): void {\n if (value.length === 0 || /[\\r\\n]/.test(value)) {\n throw new Error(`${label} must be a non-empty single line`)\n }\n}\n\n/**\n * Render native Cloudflare static-asset header rules.\n *\n * Values are validated before serialization so a product setting cannot add a\n * second path rule or response header through a newline.\n */\nexport function renderCloudflareHeadersFile(\n rules: readonly CloudflareHeadersRule[],\n): string {\n if (rules.length === 0) {\n throw new Error('At least one Cloudflare header rule is required')\n }\n\n const renderedRules = rules.map(({ pattern, headers }) => {\n assertSingleLine(pattern, 'Cloudflare header pattern')\n if (!pattern.startsWith('/') || /\\s/.test(pattern)) {\n throw new Error('Cloudflare header patterns must start with / and contain no whitespace')\n }\n\n const entries = Object.entries(headers)\n if (entries.length === 0) {\n throw new Error(`Cloudflare header rule ${pattern} must contain at least one header`)\n }\n\n const renderedHeaders = entries.map(([name, value]) => {\n if (!HEADER_NAME.test(name)) {\n throw new Error(`Invalid HTTP header name: ${name}`)\n }\n assertSingleLine(value, `Cloudflare header ${name}`)\n return ` ${name}: ${value}`\n })\n\n return [pattern, ...renderedHeaders].join('\\n')\n })\n\n return `${renderedRules.join('\\n\\n')}\\n`\n}\n","/**\n * `@tangle-network/agent-app/preset-cloudflare` — the batteries-included default\n * stack.\n *\n * Every fleet agent runs the SAME backend: Cloudflare D1 (SQLite) through\n * Drizzle for state, a KV namespace as the artifact vault, AES-GCM field crypto\n * for PII, and per-workspace budget-capped model keys. The other agent-app\n * modules are pure SEAMS — `./tools` needs an `AppToolHandlers`, `./knowledge`\n * needs a `KnowledgeStateAccessor`, `./billing` needs a `WorkspaceKeyStore` +\n * `KeyCrypto`. This module is the ONE implementation of those seams against the\n * house stack, so a consumer that runs D1 + KV stands the whole shell up with\n * config + bindings and ZERO handler code.\n *\n * Layering:\n * - Drizzle is a PEER (the consumer installs `drizzle-orm`, never bundled). The\n * schema is therefore expressed two ways that need no import here: the plain\n * DDL ({@link PRESET_MIGRATION_SQL}) a consumer runs to create the tables, and\n * a {@link createPresetDrizzleSchema} factory that takes the consumer's\n * `drizzle-orm/sqlite-core` builder module and returns the typed tables. The\n * column names in {@link PRESET_TABLES} are the contract the handlers,\n * accessor, and DDL all agree on.\n * - D1 + KV are STRUCTURAL: {@link D1Like} (Cloudflare `D1Database` satisfies it)\n * and `KvLike` from `../web` (Cloudflare `KVNamespace` satisfies it). No\n * `@cloudflare/workers-types` dependency.\n * - Crypto/billing reuse `../crypto` + `../billing` exactly — this only wires\n * them to the D1 key table.\n */\n\nimport { createFieldCrypto } from '../crypto/index'\nimport {\n createWorkspaceKeyManager,\n type KeyCrypto,\n type KeyProvisioner,\n type WorkspaceKeyManager,\n type WorkspaceKeyRecord,\n type WorkspaceKeyStore,\n} from '../billing/index'\nimport type { KnowledgeStateAccessor } from '../knowledge/index'\nimport type {\n AddCitationArgs,\n AddCitationResult,\n AppToolContext,\n AppToolHandlers,\n RenderUiArgs,\n RenderUiResult,\n ScheduleFollowupArgs,\n ScheduleFollowupResult,\n SubmitProposalArgs,\n SubmitProposalResult,\n} from '../tools/index'\nimport type { KvLike } from '../web/index'\n\nexport {\n ensureCloudflareWorkflowInstance,\n type CloudflareWorkflowBindingLike,\n type CloudflareWorkflowInstanceLike,\n type EnsureCloudflareWorkflowInstanceResult,\n} from './workflow-instance'\n\nexport {\n runDetachedTurnWorkflowTick,\n type CloudflareWorkflowEventLike,\n type CloudflareWorkflowSleepDuration,\n type CloudflareWorkflowStepLike,\n type DetachedTurnDriveOutcome,\n type DetachedTurnDriveState,\n type DetachedTurnTerminalResult,\n type DetachedTurnWorkflowIdentity,\n type DetachedTurnWorkflowTickOptions,\n} from './detached-turn-workflow'\n\nexport {\n renderCloudflareHeadersFile,\n type CloudflareHeadersRule,\n} from './headers'\n\n// ---------------------------------------------------------------------------\n// D1 structural seam\n//\n// The minimal surface of Cloudflare's `D1Database` the handlers + accessor use.\n// `D1Database` satisfies it structurally, so the consumer passes `env.DB`\n// directly and tests pass an in-memory fake. We keep it to the\n// prepare/bind/first/run/all shape D1 already exposes — no Drizzle here, so the\n// default handlers run on a fresh Worker with only the D1 binding.\n// ---------------------------------------------------------------------------\n\n/** A prepared, bound D1 statement. */\nexport interface D1PreparedLike {\n bind(...values: unknown[]): D1PreparedLike\n first<T = Record<string, unknown>>(colName?: string): Promise<T | null>\n run(): Promise<unknown>\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>\n}\n\n/** The D1 surface the preset needs. Cloudflare `D1Database` satisfies it. */\nexport interface D1Like {\n prepare(query: string): D1PreparedLike\n}\n\n// ---------------------------------------------------------------------------\n// Default schema\n//\n// The four tables the default handlers + accessor read/write. Column names are\n// the single source of truth shared by the DDL, the Drizzle factory, the\n// handlers, and the accessor. Every table is workspace-scoped on `workspace_id`\n// (the accessor's default `where` column) so the knowledge `count` rule and the\n// tool writes agree without per-consumer wiring.\n//\n// Deliberately NOT here: chat thread/message tables. `/chat-store` is the\n// single thread-schema owner (`createChatTables` + `createChatStore`); the\n// preset once declared an unconsumed `threads` DDL, removed so two schemas\n// can't drift.\n// ---------------------------------------------------------------------------\n\n/** The preset table + column names — the contract the DDL, Drizzle schema,\n * handlers, and accessor share. Exposed so a consumer can reference a column\n * without a string literal. */\nexport const PRESET_TABLES = {\n proposals: {\n name: 'proposals',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n threadId: 'thread_id',\n type: 'type',\n title: 'title',\n description: 'description',\n status: 'status',\n createdBy: 'created_by',\n createdAt: 'created_at',\n },\n },\n knowledge: {\n name: 'knowledge',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n path: 'path',\n kind: 'kind',\n label: 'label',\n content: 'content',\n createdAt: 'created_at',\n },\n },\n deadlines: {\n name: 'deadlines',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n threadId: 'thread_id',\n title: 'title',\n dueDate: 'due_date',\n priority: 'priority',\n status: 'status',\n createdAt: 'created_at',\n },\n },\n workspaceKeys: {\n name: 'workspace_keys',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n keyId: 'key_id',\n keyEncrypted: 'key_encrypted',\n budgetUsd: 'budget_usd',\n expiresAt: 'expires_at',\n revokedAt: 'revoked_at',\n createdAt: 'created_at',\n },\n },\n} as const\n\n/**\n * Plain DDL for the preset schema — run by a consumer to create the tables with\n * ZERO drizzle (`for (const sql of PRESET_MIGRATION_SQL) await db.prepare(sql).run()`,\n * or paste into a `.sql` migration). One statement per table so D1's\n * single-statement `prepare` accepts each. Matches {@link PRESET_TABLES} exactly.\n */\nexport const PRESET_MIGRATION_SQL: readonly string[] = [\n `CREATE TABLE IF NOT EXISTS proposals (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n thread_id TEXT,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n created_by TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS knowledge (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n path TEXT NOT NULL,\n kind TEXT NOT NULL,\n label TEXT,\n content TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS deadlines (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n thread_id TEXT,\n title TEXT NOT NULL,\n due_date TEXT NOT NULL,\n priority TEXT,\n status TEXT NOT NULL DEFAULT 'scheduled',\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS workspace_keys (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n key_id TEXT NOT NULL,\n key_encrypted TEXT NOT NULL,\n budget_usd REAL NOT NULL,\n expires_at INTEGER NOT NULL,\n revoked_at INTEGER,\n created_at INTEGER NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS idx_proposals_ws ON proposals (workspace_id, status)`,\n `CREATE INDEX IF NOT EXISTS idx_deadlines_ws ON deadlines (workspace_id, status)`,\n `CREATE INDEX IF NOT EXISTS idx_knowledge_ws ON knowledge (workspace_id)`,\n `CREATE INDEX IF NOT EXISTS idx_workspace_keys_ws ON workspace_keys (workspace_id, revoked_at)`,\n]\n\n/** A chainable column builder — every modifier returns the builder so calls\n * like `.notNull().default('pending')` typecheck. The concrete drizzle builders\n * satisfy this structurally. */\nexport interface DrizzleColumnLike {\n primaryKey: () => DrizzleColumnLike\n notNull: () => DrizzleColumnLike\n default: (v: unknown) => DrizzleColumnLike\n}\n\n/** The shape of a `drizzle-orm/sqlite-core` module — the few builders the\n * preset schema uses. The consumer passes the real module; agent-app never\n * imports it (it stays a peer). */\nexport interface DrizzleSqliteCoreLike {\n sqliteTable: (name: string, columns: Record<string, DrizzleColumnLike>) => unknown\n text: (name?: string) => DrizzleColumnLike\n integer: (name?: string, config?: unknown) => DrizzleColumnLike\n real: (name?: string) => DrizzleColumnLike\n}\n\n/**\n * Build the typed Drizzle schema for the preset, given the consumer's\n * `drizzle-orm/sqlite-core` module. Returns one table object per\n * {@link PRESET_TABLES} entry — pass to `drizzle(db, { schema })` for typed\n * queries, or to drizzle-kit for migration generation. agent-app never imports\n * drizzle; the builder module is the seam.\n *\n * ```ts\n * import * as d from 'drizzle-orm/sqlite-core'\n * const schema = createPresetDrizzleSchema(d)\n * ```\n */\nexport function createPresetDrizzleSchema(d: DrizzleSqliteCoreLike) {\n const { sqliteTable, text, integer, real } = d\n const C = PRESET_TABLES\n return {\n proposals: sqliteTable(C.proposals.name, {\n id: text(C.proposals.columns.id).primaryKey(),\n workspaceId: text(C.proposals.columns.workspaceId).notNull(),\n threadId: text(C.proposals.columns.threadId),\n type: text(C.proposals.columns.type).notNull(),\n title: text(C.proposals.columns.title).notNull(),\n description: text(C.proposals.columns.description),\n status: text(C.proposals.columns.status).notNull().default('pending'),\n createdBy: text(C.proposals.columns.createdBy),\n createdAt: integer(C.proposals.columns.createdAt).notNull(),\n }),\n knowledge: sqliteTable(C.knowledge.name, {\n id: text(C.knowledge.columns.id).primaryKey(),\n workspaceId: text(C.knowledge.columns.workspaceId).notNull(),\n path: text(C.knowledge.columns.path).notNull(),\n kind: text(C.knowledge.columns.kind).notNull(),\n label: text(C.knowledge.columns.label),\n content: text(C.knowledge.columns.content),\n createdAt: integer(C.knowledge.columns.createdAt).notNull(),\n }),\n deadlines: sqliteTable(C.deadlines.name, {\n id: text(C.deadlines.columns.id).primaryKey(),\n workspaceId: text(C.deadlines.columns.workspaceId).notNull(),\n threadId: text(C.deadlines.columns.threadId),\n title: text(C.deadlines.columns.title).notNull(),\n dueDate: text(C.deadlines.columns.dueDate).notNull(),\n priority: text(C.deadlines.columns.priority),\n status: text(C.deadlines.columns.status).notNull().default('scheduled'),\n createdAt: integer(C.deadlines.columns.createdAt).notNull(),\n }),\n workspaceKeys: sqliteTable(C.workspaceKeys.name, {\n id: text(C.workspaceKeys.columns.id).primaryKey(),\n workspaceId: text(C.workspaceKeys.columns.workspaceId).notNull(),\n keyId: text(C.workspaceKeys.columns.keyId).notNull(),\n keyEncrypted: text(C.workspaceKeys.columns.keyEncrypted).notNull(),\n budgetUsd: real(C.workspaceKeys.columns.budgetUsd).notNull(),\n expiresAt: integer(C.workspaceKeys.columns.expiresAt).notNull(),\n revokedAt: integer(C.workspaceKeys.columns.revokedAt),\n createdAt: integer(C.workspaceKeys.columns.createdAt).notNull(),\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Default AppToolHandlers over D1 + KV\n// ---------------------------------------------------------------------------\n\n/** The KV-backed vault. `KvLike` (from `../web`) is the structural KV contract;\n * Cloudflare `KVNamespace` satisfies it. Artifacts are stored under their path. */\nexport type VaultKv = KvLike\n\n/** Define configuration options for handling preset tools including database, vault, and optional utilities */\nexport interface PresetToolHandlerOptions {\n /** The D1 database (Cloudflare `D1Database` satisfies {@link D1Like}). */\n db: D1Like\n /** The KV namespace used as the artifact vault. */\n vault: VaultKv\n /** Id generator. Default `crypto.randomUUID`. Injectable for deterministic tests. */\n newId?: () => string\n /** Clock (epoch ms). Default `Date.now`. Injectable for deterministic tests. */\n now?: () => number\n /** Vault path prefix for `render_ui` artifacts. Default `'ui'`. */\n uiPathPrefix?: string\n /** Vault path prefix for `add_citation` artifacts. Default `'citations'`. */\n citationPathPrefix?: string\n}\n\nfunction slug(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 64) || 'item'\n}\n\n/**\n * The default {@link AppToolHandlers} for the house stack:\n * - `submit_proposal` → insert a `proposals` row (`status='pending'`), deduped\n * on (workspace, title) so a retried turn doesn't double-queue.\n * - `schedule_followup` → insert a `deadlines` row, deduped on (workspace, title, due_date).\n * - `render_ui` → write the schema JSON as a `ui/<thread>/<slug>.json`\n * vault artifact AND a `knowledge` row pointing at it.\n * - `add_citation` → write the quote as a `citations/<slug>.json` artifact AND\n * a `knowledge` row.\n *\n * Returns the EXACT persisted content from `render_ui` (per the seam contract) so\n * a completion oracle sees real bytes. Pure seam wiring: a consumer that runs\n * D1 + KV gets all four tools with no handler code.\n */\nexport function createPresetToolHandlers(opts: PresetToolHandlerOptions): AppToolHandlers {\n const { db, vault } = opts\n const newId = opts.newId ?? (() => crypto.randomUUID())\n const now = opts.now ?? (() => Date.now())\n const uiPrefix = opts.uiPathPrefix ?? 'ui'\n const citationPrefix = opts.citationPathPrefix ?? 'citations'\n const P = PRESET_TABLES.proposals\n const D = PRESET_TABLES.deadlines\n const K = PRESET_TABLES.knowledge\n\n async function persistArtifact(path: string, body: string): Promise<void> {\n await vault.put(path, body)\n }\n\n async function insertKnowledge(workspaceId: string, path: string, kind: string, label: string | null, content: string): Promise<void> {\n await db\n .prepare(\n `INSERT INTO ${K.name} (${K.columns.id}, ${K.columns.workspaceId}, ${K.columns.path}, ${K.columns.kind}, ${K.columns.label}, ${K.columns.content}, ${K.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(newId(), workspaceId, path, kind, label, content, now())\n .run()\n }\n\n return {\n async submitProposal(args: SubmitProposalArgs, ctx: AppToolContext): Promise<SubmitProposalResult> {\n const existing = await db\n .prepare(`SELECT ${P.columns.id} AS id FROM ${P.name} WHERE ${P.columns.workspaceId} = ? AND ${P.columns.title} = ? LIMIT 1`)\n .bind(ctx.workspaceId, args.title)\n .first<{ id: string }>()\n if (existing) return { proposalId: existing.id, deduped: true }\n\n const id = newId()\n await db\n .prepare(\n `INSERT INTO ${P.name} (${P.columns.id}, ${P.columns.workspaceId}, ${P.columns.threadId}, ${P.columns.type}, ${P.columns.title}, ${P.columns.description}, ${P.columns.status}, ${P.columns.createdBy}, ${P.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,\n )\n .bind(id, ctx.workspaceId, ctx.threadId, args.type, args.title, args.description ?? null, ctx.userId, now())\n .run()\n return { proposalId: id, deduped: false }\n },\n\n async scheduleFollowup(args: ScheduleFollowupArgs, ctx: AppToolContext): Promise<ScheduleFollowupResult> {\n const existing = await db\n .prepare(\n `SELECT ${D.columns.id} AS id, ${D.columns.dueDate} AS dueDate FROM ${D.name} WHERE ${D.columns.workspaceId} = ? AND ${D.columns.title} = ? AND ${D.columns.dueDate} = ? LIMIT 1`,\n )\n .bind(ctx.workspaceId, args.title, args.dueDate)\n .first<{ id: string; dueDate: string }>()\n if (existing) return { id: existing.id, dueDate: existing.dueDate, deduped: true }\n\n const id = newId()\n await db\n .prepare(\n `INSERT INTO ${D.name} (${D.columns.id}, ${D.columns.workspaceId}, ${D.columns.threadId}, ${D.columns.title}, ${D.columns.dueDate}, ${D.columns.priority}, ${D.columns.status}, ${D.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'scheduled', ?)`,\n )\n .bind(id, ctx.workspaceId, ctx.threadId, args.title, args.dueDate, args.priority ?? null, now())\n .run()\n return { id, dueDate: args.dueDate, deduped: false }\n },\n\n async renderUi(args: RenderUiArgs, ctx: AppToolContext): Promise<RenderUiResult> {\n const content = JSON.stringify(args.schema)\n const path = `${uiPrefix}/${ctx.threadId ?? 'global'}/${slug(args.title)}.json`\n await persistArtifact(path, content)\n await insertKnowledge(ctx.workspaceId, path, 'ui', args.title, content)\n return { path, content }\n },\n\n async addCitation(args: AddCitationArgs, ctx: AppToolContext): Promise<AddCitationResult> {\n const citationId = newId()\n const path = `${citationPrefix}/${slug(args.label ?? args.path)}-${citationId.slice(0, 8)}.json`\n const body = JSON.stringify({ sourcePath: args.path, quote: args.quote, label: args.label ?? null })\n await persistArtifact(path, body)\n await insertKnowledge(ctx.workspaceId, path, 'citation', args.label ?? null, body)\n return { citationId, path }\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// D1-backed KnowledgeStateAccessor\n// ---------------------------------------------------------------------------\n\n/** Define options for accessing preset knowledge scoped to a specific workspace and configuration */\nexport interface PresetKnowledgeAccessorOptions {\n db: D1Like\n /** The active workspace — every `count` is scoped to it. */\n workspaceId: string\n /** Workspace config the `satisfiedBy: { config }` rules read. A resolved\n * object (dot-path lookup), or a function the accessor calls per path. */\n config: Record<string, unknown> | ((path: string) => unknown)\n /** The default workspace fk column a `count` rule scopes on when its rule\n * omits `where`. Default `'workspace_id'` (the preset schema convention). */\n defaultWhereColumn?: string\n}\n\nfunction readDotPath(obj: Record<string, unknown>, path: string): unknown {\n let cur: unknown = obj\n for (const part of path.split('.')) {\n if (cur == null || typeof cur !== 'object') return undefined\n cur = (cur as Record<string, unknown>)[part]\n }\n return cur\n}\n\n/**\n * The {@link KnowledgeStateAccessor} over the preset D1 schema — the seam that\n * lets the declarative `satisfiedBy` rules resolve with ZERO consumer code:\n * - `config(path)` reads the supplied workspace config by dot-path.\n * - `count({ table, where, statusIn })` runs `SELECT count(*)` scoped to the\n * active workspace (the rule's `where` column, default `workspace_id`),\n * optionally filtered to `statusIn` via a parameterized `IN (...)`.\n *\n * Identifiers (table/column) are validated against a safe pattern before\n * interpolation — they originate from the product's own config, never model\n * input, but we fail loud rather than build a malformed/injectable query.\n */\nexport function createD1KnowledgeStateAccessor(opts: PresetKnowledgeAccessorOptions): KnowledgeStateAccessor {\n const { db, workspaceId } = opts\n const defaultWhere = opts.defaultWhereColumn ?? 'workspace_id'\n const configFn = typeof opts.config === 'function' ? opts.config : (path: string) => readDotPath(opts.config as Record<string, unknown>, path)\n\n const isIdentifier = (s: string) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s)\n\n return {\n config: configFn,\n async count(query) {\n if (!isIdentifier(query.table)) throw new Error(`unsafe table identifier: ${query.table}`)\n const whereCol = query.where ?? defaultWhere\n if (!isIdentifier(whereCol)) throw new Error(`unsafe where identifier: ${whereCol}`)\n\n let sql = `SELECT count(*) AS n FROM ${query.table} WHERE ${whereCol} = ?`\n const binds: unknown[] = [workspaceId]\n if (query.statusIn && query.statusIn.length > 0) {\n sql += ` AND status IN (${query.statusIn.map(() => '?').join(', ')})`\n binds.push(...query.statusIn)\n }\n const row = await db.prepare(sql).bind(...binds).first<{ n: number }>()\n return row?.n ?? 0\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Crypto + per-workspace key wiring\n// ---------------------------------------------------------------------------\n\n/** Build the {@link KeyCrypto} the billing key store uses — AES-256-GCM field\n * crypto bound to the product's 64-char-hex `ENCRYPTION_KEY` (or a resolver).\n * This is the concrete impl behind the `../billing` `KeyCrypto` seam. */\nexport function createPresetFieldCrypto(key: string | (() => string)): KeyCrypto {\n return createFieldCrypto(key)\n}\n\n/**\n * The {@link WorkspaceKeyStore} over the preset `workspace_keys` table — the\n * persistence seam the per-workspace key manager needs. \"Active\" = a row with a\n * null `revoked_at`. Pure D1 wiring; no key minting (that's the provisioner).\n */\nexport function createPresetWorkspaceKeyStore(db: D1Like): WorkspaceKeyStore {\n const W = PRESET_TABLES.workspaceKeys\n return {\n async getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null> {\n const row = await db\n .prepare(\n `SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId, ${W.columns.keyEncrypted} AS keyEncrypted, ${W.columns.budgetUsd} AS budgetUsd, ${W.columns.expiresAt} AS expiresAt FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL ORDER BY ${W.columns.createdAt} DESC LIMIT 1`,\n )\n .bind(workspaceId)\n .first<{ id: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: number | null }>()\n if (!row) return null\n return {\n id: row.id,\n keyId: row.keyId,\n keyEncrypted: row.keyEncrypted,\n budgetUsd: row.budgetUsd,\n expiresAt: row.expiresAt == null ? null : new Date(row.expiresAt),\n }\n },\n async listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>> {\n const res = await db\n .prepare(`SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL`)\n .bind(workspaceId)\n .all<{ id: string; keyId: string }>()\n return res.results\n },\n async insert(record): Promise<void> {\n await db\n .prepare(\n `INSERT INTO ${W.name} (${W.columns.id}, ${W.columns.workspaceId}, ${W.columns.keyId}, ${W.columns.keyEncrypted}, ${W.columns.budgetUsd}, ${W.columns.expiresAt}, ${W.columns.revokedAt}, ${W.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, NULL, ?)`,\n )\n .bind(crypto.randomUUID(), record.workspaceId, record.keyId, record.keyEncrypted, record.budgetUsd, record.expiresAt.getTime(), Date.now())\n .run()\n },\n async markRevoked(id: string, now: Date): Promise<void> {\n await db\n .prepare(`UPDATE ${W.name} SET ${W.columns.revokedAt} = ? WHERE ${W.columns.id} = ?`)\n .bind(now.getTime(), id)\n .run()\n },\n }\n}\n\n/** Define preset billing options including database, provisioner, encryption key, budget, and optional settings */\nexport interface PresetBillingOptions {\n db: D1Like\n /** The key provisioner (`@tangle-network/tcloud`'s client satisfies it structurally). */\n provisioner: KeyProvisioner\n /** Field-crypto key (64-char hex) or resolver — encrypts the minted key at rest. */\n encryptionKey: string | (() => string)\n /** Default monthly USD allowance when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\n/**\n * Stand up the per-workspace budget-capped {@link WorkspaceKeyManager} on the\n * house stack: the preset `workspace_keys` D1 store + AES-GCM field crypto +\n * the consumer's tcloud provisioner. The mint/rotate/rollover/usage LOGIC lives\n * in `../billing`; this only binds it to the preset table + crypto.\n */\nexport function createPresetWorkspaceKeyManager(opts: PresetBillingOptions): WorkspaceKeyManager {\n return createWorkspaceKeyManager({\n provisioner: opts.provisioner,\n store: createPresetWorkspaceKeyStore(opts.db),\n crypto: createPresetFieldCrypto(opts.encryptionKey),\n defaultBudgetUsd: opts.defaultBudgetUsd,\n now: opts.now,\n product: opts.product,\n })\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/preset-cloudflare/workflow-instance.ts","../../src/preset-cloudflare/detached-turn-workflow.ts","../../src/preset-cloudflare/headers.ts","../../src/preset-cloudflare/index.ts"],"sourcesContent":["/**\n * The structural part of a Cloudflare Workflow instance used to prove that a\n * failed create call actually left an addressable durable instance behind.\n */\nexport interface CloudflareWorkflowInstanceLike<TStatus = unknown> {\n status(): Promise<TStatus>\n}\n\n/** The structural binding implemented by Cloudflare's `Workflow<TParams>`. */\nexport interface CloudflareWorkflowBindingLike<\n TParams,\n TStatus = unknown,\n TInstance extends CloudflareWorkflowInstanceLike<TStatus> =\n CloudflareWorkflowInstanceLike<TStatus>,\n> {\n create(input: { id: string; params: TParams }): Promise<TInstance>\n get(id: string): Promise<TInstance>\n}\n\nexport interface EnsureCloudflareWorkflowInstanceResult<\n TStatus,\n TInstance extends CloudflareWorkflowInstanceLike<TStatus>,\n> {\n instance: TInstance\n /** False only when create failed but the same id was proven readable. */\n created: boolean\n /** Present on reuse because reading status is the proof the instance exists. */\n status?: TStatus\n}\n\n/**\n * Create one durable Workflow instance, or prove an instance with the same id\n * already exists before accepting a create error as an idempotent retry.\n *\n * Cloudflare does not expose a stable duplicate-error class. Matching error\n * text can therefore swallow unrelated failures. This helper uses the durable\n * resource itself as proof: `get(id).status()` must succeed, otherwise the\n * original create error is rethrown.\n */\nexport async function ensureCloudflareWorkflowInstance<\n TParams,\n TStatus,\n TInstance extends CloudflareWorkflowInstanceLike<TStatus>,\n>(\n binding: CloudflareWorkflowBindingLike<TParams, TStatus, TInstance>,\n input: { id: string; params: TParams },\n): Promise<EnsureCloudflareWorkflowInstanceResult<TStatus, TInstance>> {\n try {\n return {\n instance: await binding.create(input),\n created: true,\n }\n } catch (createError) {\n try {\n const instance = await binding.get(input.id)\n const status = await instance.status()\n return { instance, created: false, status }\n } catch {\n throw createError\n }\n }\n}\n","import type { TurnDriveResult } from '@tangle-network/sandbox'\nimport type { Outcome } from '../sandbox/outcome'\n\n/** The stable identity a Workflow reuses on every retry of one turn. */\nexport interface DetachedTurnWorkflowIdentity {\n /** Sandbox session resume key. */\n sessionId: string\n /** Sandbox completed-turn idempotency key. */\n turnId: string\n}\n\n/** The part of Cloudflare's Workflow event needed by the tick. */\nexport interface CloudflareWorkflowEventLike<TPayload> {\n payload: TPayload\n}\n\n/** A duration accepted by Cloudflare Workflow `step.sleep`. */\nexport type CloudflareWorkflowSleepDuration =\n | `${number} ${\n | 'second'\n | 'seconds'\n | 'minute'\n | 'minutes'\n | 'hour'\n | 'hours'\n | 'day'\n | 'days'\n | 'week'\n | 'weeks'\n | 'month'\n | 'months'\n | 'year'\n | 'years'}`\n | number\n\n/** The durable Workflow operations used by the tick. */\nexport interface CloudflareWorkflowStepLike {\n do<T>(name: string, callback: (context: unknown) => Promise<T>): Promise<T>\n sleep(name: string, duration: CloudflareWorkflowSleepDuration): Promise<void>\n}\n\n/** The states returned by the Sandbox `driveTurn` primitive. */\nexport type DetachedTurnDriveState = TurnDriveResult['state']\n\n/** The terminal result passed to product settlement. */\nexport type DetachedTurnTerminalResult = Exclude<TurnDriveResult, { state: 'running' }>\n\n/** A drive call's retryable transport boundary. */\nexport type DetachedTurnDriveOutcome = Outcome<TurnDriveResult>\n\n/** Options for one durable detached-turn Workflow run. */\nexport interface DetachedTurnWorkflowTickOptions<\n TPayload extends DetachedTurnWorkflowIdentity,\n TSettled,\n> {\n event: CloudflareWorkflowEventLike<TPayload>\n step: CloudflareWorkflowStepLike\n /** One SDK drive pass. Rejected results must not enter the Workflow cache. */\n drive: (payload: TPayload) => Promise<DetachedTurnDriveOutcome>\n /** Must be idempotent: the Worker can stop after the write but before commit. */\n settle: (payload: TPayload, result: DetachedTurnTerminalResult) => Promise<TSettled>\n pollDelay?: CloudflareWorkflowSleepDuration\n stepName?: string\n}\n\nfunction assertIdentity(payload: DetachedTurnWorkflowIdentity): void {\n if (!payload || typeof payload.sessionId !== 'string' || !payload.sessionId.trim()) {\n throw new Error('detached turn Workflow payload requires a non-empty sessionId')\n }\n if (typeof payload.turnId !== 'string' || !payload.turnId.trim()) {\n throw new Error('detached turn Workflow payload requires a non-empty turnId')\n }\n}\n\nfunction isKnownDriveState(state: unknown): state is DetachedTurnDriveState {\n return state === 'running'\n || state === 'completed'\n || state === 'failed'\n || state === 'awaiting_plan_decision'\n}\n\nfunction checkedDriveResult(value: TurnDriveResult): TurnDriveResult {\n const state = (value as { state?: unknown } | null)?.state\n if (!isKnownDriveState(state)) {\n throw new Error(`detached turn drive returned unknown state: ${String(state)}`)\n }\n return value\n}\n\n/**\n * Drive a detached SDK turn with durable steps, not an HTTP waitUntil lifetime.\n * Stable step names let an evicted Workflow resume without repeating committed\n * passes. Validation belongs inside step.do so a transient malformed response\n * is retried rather than committed permanently as a poisoned checkpoint.\n */\nexport async function runDetachedTurnWorkflowTick<\n TPayload extends DetachedTurnWorkflowIdentity,\n TSettled,\n>(\n options: DetachedTurnWorkflowTickOptions<TPayload, TSettled>,\n): Promise<TSettled> {\n assertIdentity(options.event?.payload)\n // Never allow a callback to change the admission identity for later passes.\n const payload = Object.freeze({ ...options.event.payload }) as TPayload\n const name = options.stepName ?? 'detached-turn'\n const delay = options.pollDelay ?? '5 seconds'\n let attempt = 0\n let terminalResult: DetachedTurnTerminalResult\n while (true) {\n const driveResult = checkedDriveResult(await options.step.do(`${name}:drive:${attempt}`, async () => {\n const outcome = await options.drive(payload)\n if (!outcome.succeeded) throw outcome.error\n return checkedDriveResult(outcome.value)\n }))\n // Validate replayed values as well, including checkpoints from older code.\n if (driveResult.state !== 'running') {\n terminalResult = driveResult as DetachedTurnTerminalResult\n break\n }\n await options.step.sleep(`${name}:wait:${attempt}`, delay)\n attempt += 1\n }\n return options.step.do(`${name}:settle`, () => options.settle(payload, terminalResult))\n}\n","const HEADER_NAME = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/\n\n/** One path rule in Cloudflare's static-asset `_headers` file. */\nexport interface CloudflareHeadersRule {\n pattern: string\n headers: Readonly<Record<string, string>>\n}\n\nfunction assertSingleLine(value: string, label: string): void {\n if (value.length === 0 || /[\\r\\n]/.test(value)) {\n throw new Error(`${label} must be a non-empty single line`)\n }\n}\n\n/**\n * Render native Cloudflare static-asset header rules.\n *\n * Values are validated before serialization so a product setting cannot add a\n * second path rule or response header through a newline.\n */\nexport function renderCloudflareHeadersFile(\n rules: readonly CloudflareHeadersRule[],\n): string {\n if (rules.length === 0) {\n throw new Error('At least one Cloudflare header rule is required')\n }\n\n const renderedRules = rules.map(({ pattern, headers }) => {\n assertSingleLine(pattern, 'Cloudflare header pattern')\n if (!pattern.startsWith('/') || /\\s/.test(pattern)) {\n throw new Error('Cloudflare header patterns must start with / and contain no whitespace')\n }\n\n const entries = Object.entries(headers)\n if (entries.length === 0) {\n throw new Error(`Cloudflare header rule ${pattern} must contain at least one header`)\n }\n\n const renderedHeaders = entries.map(([name, value]) => {\n if (!HEADER_NAME.test(name)) {\n throw new Error(`Invalid HTTP header name: ${name}`)\n }\n assertSingleLine(value, `Cloudflare header ${name}`)\n return ` ${name}: ${value}`\n })\n\n return [pattern, ...renderedHeaders].join('\\n')\n })\n\n return `${renderedRules.join('\\n\\n')}\\n`\n}\n","/**\n * `@tangle-network/agent-app/preset-cloudflare` — the batteries-included default\n * stack.\n *\n * Every fleet agent runs the SAME backend: Cloudflare D1 (SQLite) through\n * Drizzle for state, a KV namespace as the artifact vault, AES-GCM field crypto\n * for PII, and per-workspace budget-capped model keys. The other agent-app\n * modules are pure SEAMS — `./tools` needs an `AppToolHandlers`, `./knowledge`\n * needs a `KnowledgeStateAccessor`, `./billing` needs a `WorkspaceKeyStore` +\n * `KeyCrypto`. This module is the ONE implementation of those seams against the\n * house stack, so a consumer that runs D1 + KV stands the whole shell up with\n * config + bindings and ZERO handler code.\n *\n * Layering:\n * - Drizzle is a PEER (the consumer installs `drizzle-orm`, never bundled). The\n * schema is therefore expressed two ways that need no import here: the plain\n * DDL ({@link PRESET_MIGRATION_SQL}) a consumer runs to create the tables, and\n * a {@link createPresetDrizzleSchema} factory that takes the consumer's\n * `drizzle-orm/sqlite-core` builder module and returns the typed tables. The\n * column names in {@link PRESET_TABLES} are the contract the handlers,\n * accessor, and DDL all agree on.\n * - D1 + KV are STRUCTURAL: {@link D1Like} (Cloudflare `D1Database` satisfies it)\n * and `KvLike` from `../web` (Cloudflare `KVNamespace` satisfies it). No\n * `@cloudflare/workers-types` dependency.\n * - Crypto/billing reuse `../crypto` + `../billing` exactly — this only wires\n * them to the D1 key table.\n */\n\nimport { createFieldCrypto } from '../crypto/index'\nimport {\n createWorkspaceKeyManager,\n type KeyCrypto,\n type KeyProvisioner,\n type WorkspaceKeyManager,\n type WorkspaceKeyRecord,\n type WorkspaceKeyStore,\n} from '../billing/index'\nimport type { KnowledgeStateAccessor } from '../knowledge/index'\nimport type {\n AddCitationArgs,\n AddCitationResult,\n AppToolContext,\n AppToolHandlers,\n RenderUiArgs,\n RenderUiResult,\n ScheduleFollowupArgs,\n ScheduleFollowupResult,\n SubmitProposalArgs,\n SubmitProposalResult,\n} from '../tools/index'\nimport type { KvLike } from '../web/index'\n\nexport {\n ensureCloudflareWorkflowInstance,\n type CloudflareWorkflowBindingLike,\n type CloudflareWorkflowInstanceLike,\n type EnsureCloudflareWorkflowInstanceResult,\n} from './workflow-instance'\n\nexport {\n runDetachedTurnWorkflowTick,\n type CloudflareWorkflowEventLike,\n type CloudflareWorkflowSleepDuration,\n type CloudflareWorkflowStepLike,\n type DetachedTurnDriveOutcome,\n type DetachedTurnDriveState,\n type DetachedTurnTerminalResult,\n type DetachedTurnWorkflowIdentity,\n type DetachedTurnWorkflowTickOptions,\n} from './detached-turn-workflow'\n\nexport {\n renderCloudflareHeadersFile,\n type CloudflareHeadersRule,\n} from './headers'\n\n// ---------------------------------------------------------------------------\n// D1 structural seam\n//\n// The minimal surface of Cloudflare's `D1Database` the handlers + accessor use.\n// `D1Database` satisfies it structurally, so the consumer passes `env.DB`\n// directly and tests pass an in-memory fake. We keep it to the\n// prepare/bind/first/run/all shape D1 already exposes — no Drizzle here, so the\n// default handlers run on a fresh Worker with only the D1 binding.\n// ---------------------------------------------------------------------------\n\n/** A prepared, bound D1 statement. */\nexport interface D1PreparedLike {\n bind(...values: unknown[]): D1PreparedLike\n first<T = Record<string, unknown>>(colName?: string): Promise<T | null>\n run(): Promise<unknown>\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>\n}\n\n/** The D1 surface the preset needs. Cloudflare `D1Database` satisfies it. */\nexport interface D1Like {\n prepare(query: string): D1PreparedLike\n}\n\n// ---------------------------------------------------------------------------\n// Default schema\n//\n// The four tables the default handlers + accessor read/write. Column names are\n// the single source of truth shared by the DDL, the Drizzle factory, the\n// handlers, and the accessor. Every table is workspace-scoped on `workspace_id`\n// (the accessor's default `where` column) so the knowledge `count` rule and the\n// tool writes agree without per-consumer wiring.\n//\n// Deliberately NOT here: chat thread/message tables. `/chat-store` is the\n// single thread-schema owner (`createChatTables` + `createChatStore`); the\n// preset once declared an unconsumed `threads` DDL, removed so two schemas\n// can't drift.\n// ---------------------------------------------------------------------------\n\n/** The preset table + column names — the contract the DDL, Drizzle schema,\n * handlers, and accessor share. Exposed so a consumer can reference a column\n * without a string literal. */\nexport const PRESET_TABLES = {\n proposals: {\n name: 'proposals',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n threadId: 'thread_id',\n type: 'type',\n title: 'title',\n description: 'description',\n status: 'status',\n createdBy: 'created_by',\n createdAt: 'created_at',\n },\n },\n knowledge: {\n name: 'knowledge',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n path: 'path',\n kind: 'kind',\n label: 'label',\n content: 'content',\n createdAt: 'created_at',\n },\n },\n deadlines: {\n name: 'deadlines',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n threadId: 'thread_id',\n title: 'title',\n dueDate: 'due_date',\n priority: 'priority',\n status: 'status',\n createdAt: 'created_at',\n },\n },\n workspaceKeys: {\n name: 'workspace_keys',\n columns: {\n id: 'id',\n workspaceId: 'workspace_id',\n keyId: 'key_id',\n keyEncrypted: 'key_encrypted',\n budgetUsd: 'budget_usd',\n expiresAt: 'expires_at',\n revokedAt: 'revoked_at',\n createdAt: 'created_at',\n },\n },\n} as const\n\n/**\n * Plain DDL for the preset schema — run by a consumer to create the tables with\n * ZERO drizzle (`for (const sql of PRESET_MIGRATION_SQL) await db.prepare(sql).run()`,\n * or paste into a `.sql` migration). One statement per table so D1's\n * single-statement `prepare` accepts each. Matches {@link PRESET_TABLES} exactly.\n */\nexport const PRESET_MIGRATION_SQL: readonly string[] = [\n `CREATE TABLE IF NOT EXISTS proposals (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n thread_id TEXT,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n created_by TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS knowledge (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n path TEXT NOT NULL,\n kind TEXT NOT NULL,\n label TEXT,\n content TEXT,\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS deadlines (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n thread_id TEXT,\n title TEXT NOT NULL,\n due_date TEXT NOT NULL,\n priority TEXT,\n status TEXT NOT NULL DEFAULT 'scheduled',\n created_at INTEGER NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS workspace_keys (\n id TEXT PRIMARY KEY,\n workspace_id TEXT NOT NULL,\n key_id TEXT NOT NULL,\n key_encrypted TEXT NOT NULL,\n budget_usd REAL NOT NULL,\n expires_at INTEGER NOT NULL,\n revoked_at INTEGER,\n created_at INTEGER NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS idx_proposals_ws ON proposals (workspace_id, status)`,\n `CREATE INDEX IF NOT EXISTS idx_deadlines_ws ON deadlines (workspace_id, status)`,\n `CREATE INDEX IF NOT EXISTS idx_knowledge_ws ON knowledge (workspace_id)`,\n `CREATE INDEX IF NOT EXISTS idx_workspace_keys_ws ON workspace_keys (workspace_id, revoked_at)`,\n]\n\n/** A chainable column builder — every modifier returns the builder so calls\n * like `.notNull().default('pending')` typecheck. The concrete drizzle builders\n * satisfy this structurally. */\nexport interface DrizzleColumnLike {\n primaryKey: () => DrizzleColumnLike\n notNull: () => DrizzleColumnLike\n default: (v: unknown) => DrizzleColumnLike\n}\n\n/** The shape of a `drizzle-orm/sqlite-core` module — the few builders the\n * preset schema uses. The consumer passes the real module; agent-app never\n * imports it (it stays a peer). */\nexport interface DrizzleSqliteCoreLike {\n sqliteTable: (name: string, columns: Record<string, DrizzleColumnLike>) => unknown\n text: (name?: string) => DrizzleColumnLike\n integer: (name?: string, config?: unknown) => DrizzleColumnLike\n real: (name?: string) => DrizzleColumnLike\n}\n\n/**\n * Build the typed Drizzle schema for the preset, given the consumer's\n * `drizzle-orm/sqlite-core` module. Returns one table object per\n * {@link PRESET_TABLES} entry — pass to `drizzle(db, { schema })` for typed\n * queries, or to drizzle-kit for migration generation. agent-app never imports\n * drizzle; the builder module is the seam.\n *\n * ```ts\n * import * as d from 'drizzle-orm/sqlite-core'\n * const schema = createPresetDrizzleSchema(d)\n * ```\n */\nexport function createPresetDrizzleSchema(d: DrizzleSqliteCoreLike) {\n const { sqliteTable, text, integer, real } = d\n const C = PRESET_TABLES\n return {\n proposals: sqliteTable(C.proposals.name, {\n id: text(C.proposals.columns.id).primaryKey(),\n workspaceId: text(C.proposals.columns.workspaceId).notNull(),\n threadId: text(C.proposals.columns.threadId),\n type: text(C.proposals.columns.type).notNull(),\n title: text(C.proposals.columns.title).notNull(),\n description: text(C.proposals.columns.description),\n status: text(C.proposals.columns.status).notNull().default('pending'),\n createdBy: text(C.proposals.columns.createdBy),\n createdAt: integer(C.proposals.columns.createdAt).notNull(),\n }),\n knowledge: sqliteTable(C.knowledge.name, {\n id: text(C.knowledge.columns.id).primaryKey(),\n workspaceId: text(C.knowledge.columns.workspaceId).notNull(),\n path: text(C.knowledge.columns.path).notNull(),\n kind: text(C.knowledge.columns.kind).notNull(),\n label: text(C.knowledge.columns.label),\n content: text(C.knowledge.columns.content),\n createdAt: integer(C.knowledge.columns.createdAt).notNull(),\n }),\n deadlines: sqliteTable(C.deadlines.name, {\n id: text(C.deadlines.columns.id).primaryKey(),\n workspaceId: text(C.deadlines.columns.workspaceId).notNull(),\n threadId: text(C.deadlines.columns.threadId),\n title: text(C.deadlines.columns.title).notNull(),\n dueDate: text(C.deadlines.columns.dueDate).notNull(),\n priority: text(C.deadlines.columns.priority),\n status: text(C.deadlines.columns.status).notNull().default('scheduled'),\n createdAt: integer(C.deadlines.columns.createdAt).notNull(),\n }),\n workspaceKeys: sqliteTable(C.workspaceKeys.name, {\n id: text(C.workspaceKeys.columns.id).primaryKey(),\n workspaceId: text(C.workspaceKeys.columns.workspaceId).notNull(),\n keyId: text(C.workspaceKeys.columns.keyId).notNull(),\n keyEncrypted: text(C.workspaceKeys.columns.keyEncrypted).notNull(),\n budgetUsd: real(C.workspaceKeys.columns.budgetUsd).notNull(),\n expiresAt: integer(C.workspaceKeys.columns.expiresAt).notNull(),\n revokedAt: integer(C.workspaceKeys.columns.revokedAt),\n createdAt: integer(C.workspaceKeys.columns.createdAt).notNull(),\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Default AppToolHandlers over D1 + KV\n// ---------------------------------------------------------------------------\n\n/** The KV-backed vault. `KvLike` (from `../web`) is the structural KV contract;\n * Cloudflare `KVNamespace` satisfies it. Artifacts are stored under their path. */\nexport type VaultKv = KvLike\n\n/** Define configuration options for handling preset tools including database, vault, and optional utilities */\nexport interface PresetToolHandlerOptions {\n /** The D1 database (Cloudflare `D1Database` satisfies {@link D1Like}). */\n db: D1Like\n /** The KV namespace used as the artifact vault. */\n vault: VaultKv\n /** Id generator. Default `crypto.randomUUID`. Injectable for deterministic tests. */\n newId?: () => string\n /** Clock (epoch ms). Default `Date.now`. Injectable for deterministic tests. */\n now?: () => number\n /** Vault path prefix for `render_ui` artifacts. Default `'ui'`. */\n uiPathPrefix?: string\n /** Vault path prefix for `add_citation` artifacts. Default `'citations'`. */\n citationPathPrefix?: string\n}\n\nfunction slug(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 64) || 'item'\n}\n\n/**\n * The default {@link AppToolHandlers} for the house stack:\n * - `submit_proposal` → insert a `proposals` row (`status='pending'`), deduped\n * on (workspace, title) so a retried turn doesn't double-queue.\n * - `schedule_followup` → insert a `deadlines` row, deduped on (workspace, title, due_date).\n * - `render_ui` → write the schema JSON as a `ui/<thread>/<slug>.json`\n * vault artifact AND a `knowledge` row pointing at it.\n * - `add_citation` → write the quote as a `citations/<slug>.json` artifact AND\n * a `knowledge` row.\n *\n * Returns the EXACT persisted content from `render_ui` (per the seam contract) so\n * a completion oracle sees real bytes. Pure seam wiring: a consumer that runs\n * D1 + KV gets all four tools with no handler code.\n */\nexport function createPresetToolHandlers(opts: PresetToolHandlerOptions): AppToolHandlers {\n const { db, vault } = opts\n const newId = opts.newId ?? (() => crypto.randomUUID())\n const now = opts.now ?? (() => Date.now())\n const uiPrefix = opts.uiPathPrefix ?? 'ui'\n const citationPrefix = opts.citationPathPrefix ?? 'citations'\n const P = PRESET_TABLES.proposals\n const D = PRESET_TABLES.deadlines\n const K = PRESET_TABLES.knowledge\n\n async function persistArtifact(path: string, body: string): Promise<void> {\n await vault.put(path, body)\n }\n\n async function insertKnowledge(workspaceId: string, path: string, kind: string, label: string | null, content: string): Promise<void> {\n await db\n .prepare(\n `INSERT INTO ${K.name} (${K.columns.id}, ${K.columns.workspaceId}, ${K.columns.path}, ${K.columns.kind}, ${K.columns.label}, ${K.columns.content}, ${K.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(newId(), workspaceId, path, kind, label, content, now())\n .run()\n }\n\n return {\n async submitProposal(args: SubmitProposalArgs, ctx: AppToolContext): Promise<SubmitProposalResult> {\n const existing = await db\n .prepare(`SELECT ${P.columns.id} AS id FROM ${P.name} WHERE ${P.columns.workspaceId} = ? AND ${P.columns.title} = ? LIMIT 1`)\n .bind(ctx.workspaceId, args.title)\n .first<{ id: string }>()\n if (existing) return { proposalId: existing.id, deduped: true }\n\n const id = newId()\n await db\n .prepare(\n `INSERT INTO ${P.name} (${P.columns.id}, ${P.columns.workspaceId}, ${P.columns.threadId}, ${P.columns.type}, ${P.columns.title}, ${P.columns.description}, ${P.columns.status}, ${P.columns.createdBy}, ${P.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,\n )\n .bind(id, ctx.workspaceId, ctx.threadId, args.type, args.title, args.description ?? null, ctx.userId, now())\n .run()\n return { proposalId: id, deduped: false }\n },\n\n async scheduleFollowup(args: ScheduleFollowupArgs, ctx: AppToolContext): Promise<ScheduleFollowupResult> {\n const existing = await db\n .prepare(\n `SELECT ${D.columns.id} AS id, ${D.columns.dueDate} AS dueDate FROM ${D.name} WHERE ${D.columns.workspaceId} = ? AND ${D.columns.title} = ? AND ${D.columns.dueDate} = ? LIMIT 1`,\n )\n .bind(ctx.workspaceId, args.title, args.dueDate)\n .first<{ id: string; dueDate: string }>()\n if (existing) return { id: existing.id, dueDate: existing.dueDate, deduped: true }\n\n const id = newId()\n await db\n .prepare(\n `INSERT INTO ${D.name} (${D.columns.id}, ${D.columns.workspaceId}, ${D.columns.threadId}, ${D.columns.title}, ${D.columns.dueDate}, ${D.columns.priority}, ${D.columns.status}, ${D.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, 'scheduled', ?)`,\n )\n .bind(id, ctx.workspaceId, ctx.threadId, args.title, args.dueDate, args.priority ?? null, now())\n .run()\n return { id, dueDate: args.dueDate, deduped: false }\n },\n\n async renderUi(args: RenderUiArgs, ctx: AppToolContext): Promise<RenderUiResult> {\n const content = JSON.stringify(args.schema)\n const path = `${uiPrefix}/${ctx.threadId ?? 'global'}/${slug(args.title)}.json`\n await persistArtifact(path, content)\n await insertKnowledge(ctx.workspaceId, path, 'ui', args.title, content)\n return { path, content }\n },\n\n async addCitation(args: AddCitationArgs, ctx: AppToolContext): Promise<AddCitationResult> {\n const citationId = newId()\n const path = `${citationPrefix}/${slug(args.label ?? args.path)}-${citationId.slice(0, 8)}.json`\n const body = JSON.stringify({ sourcePath: args.path, quote: args.quote, label: args.label ?? null })\n await persistArtifact(path, body)\n await insertKnowledge(ctx.workspaceId, path, 'citation', args.label ?? null, body)\n return { citationId, path }\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// D1-backed KnowledgeStateAccessor\n// ---------------------------------------------------------------------------\n\n/** Define options for accessing preset knowledge scoped to a specific workspace and configuration */\nexport interface PresetKnowledgeAccessorOptions {\n db: D1Like\n /** The active workspace — every `count` is scoped to it. */\n workspaceId: string\n /** Workspace config the `satisfiedBy: { config }` rules read. A resolved\n * object (dot-path lookup), or a function the accessor calls per path. */\n config: Record<string, unknown> | ((path: string) => unknown)\n /** The default workspace fk column a `count` rule scopes on when its rule\n * omits `where`. Default `'workspace_id'` (the preset schema convention). */\n defaultWhereColumn?: string\n}\n\nfunction readDotPath(obj: Record<string, unknown>, path: string): unknown {\n let cur: unknown = obj\n for (const part of path.split('.')) {\n if (cur == null || typeof cur !== 'object') return undefined\n cur = (cur as Record<string, unknown>)[part]\n }\n return cur\n}\n\n/**\n * The {@link KnowledgeStateAccessor} over the preset D1 schema — the seam that\n * lets the declarative `satisfiedBy` rules resolve with ZERO consumer code:\n * - `config(path)` reads the supplied workspace config by dot-path.\n * - `count({ table, where, statusIn })` runs `SELECT count(*)` scoped to the\n * active workspace (the rule's `where` column, default `workspace_id`),\n * optionally filtered to `statusIn` via a parameterized `IN (...)`.\n *\n * Identifiers (table/column) are validated against a safe pattern before\n * interpolation — they originate from the product's own config, never model\n * input, but we fail loud rather than build a malformed/injectable query.\n */\nexport function createD1KnowledgeStateAccessor(opts: PresetKnowledgeAccessorOptions): KnowledgeStateAccessor {\n const { db, workspaceId } = opts\n const defaultWhere = opts.defaultWhereColumn ?? 'workspace_id'\n const configFn = typeof opts.config === 'function' ? opts.config : (path: string) => readDotPath(opts.config as Record<string, unknown>, path)\n\n const isIdentifier = (s: string) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s)\n\n return {\n config: configFn,\n async count(query) {\n if (!isIdentifier(query.table)) throw new Error(`unsafe table identifier: ${query.table}`)\n const whereCol = query.where ?? defaultWhere\n if (!isIdentifier(whereCol)) throw new Error(`unsafe where identifier: ${whereCol}`)\n\n let sql = `SELECT count(*) AS n FROM ${query.table} WHERE ${whereCol} = ?`\n const binds: unknown[] = [workspaceId]\n if (query.statusIn && query.statusIn.length > 0) {\n sql += ` AND status IN (${query.statusIn.map(() => '?').join(', ')})`\n binds.push(...query.statusIn)\n }\n const row = await db.prepare(sql).bind(...binds).first<{ n: number }>()\n return row?.n ?? 0\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Crypto + per-workspace key wiring\n// ---------------------------------------------------------------------------\n\n/** Build the {@link KeyCrypto} the billing key store uses — AES-256-GCM field\n * crypto bound to the product's 64-char-hex `ENCRYPTION_KEY` (or a resolver).\n * This is the concrete impl behind the `../billing` `KeyCrypto` seam. */\nexport function createPresetFieldCrypto(key: string | (() => string)): KeyCrypto {\n return createFieldCrypto(key)\n}\n\n/**\n * The {@link WorkspaceKeyStore} over the preset `workspace_keys` table — the\n * persistence seam the per-workspace key manager needs. \"Active\" = a row with a\n * null `revoked_at`. Pure D1 wiring; no key minting (that's the provisioner).\n */\nexport function createPresetWorkspaceKeyStore(db: D1Like): WorkspaceKeyStore {\n const W = PRESET_TABLES.workspaceKeys\n return {\n async getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null> {\n const row = await db\n .prepare(\n `SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId, ${W.columns.keyEncrypted} AS keyEncrypted, ${W.columns.budgetUsd} AS budgetUsd, ${W.columns.expiresAt} AS expiresAt FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL ORDER BY ${W.columns.createdAt} DESC LIMIT 1`,\n )\n .bind(workspaceId)\n .first<{ id: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: number | null }>()\n if (!row) return null\n return {\n id: row.id,\n keyId: row.keyId,\n keyEncrypted: row.keyEncrypted,\n budgetUsd: row.budgetUsd,\n expiresAt: row.expiresAt == null ? null : new Date(row.expiresAt),\n }\n },\n async listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>> {\n const res = await db\n .prepare(`SELECT ${W.columns.id} AS id, ${W.columns.keyId} AS keyId FROM ${W.name} WHERE ${W.columns.workspaceId} = ? AND ${W.columns.revokedAt} IS NULL`)\n .bind(workspaceId)\n .all<{ id: string; keyId: string }>()\n return res.results\n },\n async insert(record): Promise<void> {\n await db\n .prepare(\n `INSERT INTO ${W.name} (${W.columns.id}, ${W.columns.workspaceId}, ${W.columns.keyId}, ${W.columns.keyEncrypted}, ${W.columns.budgetUsd}, ${W.columns.expiresAt}, ${W.columns.revokedAt}, ${W.columns.createdAt}) VALUES (?, ?, ?, ?, ?, ?, NULL, ?)`,\n )\n .bind(crypto.randomUUID(), record.workspaceId, record.keyId, record.keyEncrypted, record.budgetUsd, record.expiresAt.getTime(), Date.now())\n .run()\n },\n async markRevoked(id: string, now: Date): Promise<void> {\n await db\n .prepare(`UPDATE ${W.name} SET ${W.columns.revokedAt} = ? WHERE ${W.columns.id} = ?`)\n .bind(now.getTime(), id)\n .run()\n },\n }\n}\n\n/** Define preset billing options including database, provisioner, encryption key, budget, and optional settings */\nexport interface PresetBillingOptions {\n db: D1Like\n /** The key provisioner (`@tangle-network/tcloud`'s client satisfies it structurally). */\n provisioner: KeyProvisioner\n /** Field-crypto key (64-char hex) or resolver — encrypts the minted key at rest. */\n encryptionKey: string | (() => string)\n /** Default monthly USD allowance when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\n/**\n * Stand up the per-workspace budget-capped {@link WorkspaceKeyManager} on the\n * house stack: the preset `workspace_keys` D1 store + AES-GCM field crypto +\n * the consumer's tcloud provisioner. The mint/rotate/rollover/usage LOGIC lives\n * in `../billing`; this only binds it to the preset table + crypto.\n */\nexport function createPresetWorkspaceKeyManager(opts: PresetBillingOptions): WorkspaceKeyManager {\n return createWorkspaceKeyManager({\n provisioner: opts.provisioner,\n store: createPresetWorkspaceKeyStore(opts.db),\n crypto: createPresetFieldCrypto(opts.encryptionKey),\n defaultBudgetUsd: opts.defaultBudgetUsd,\n now: opts.now,\n product: opts.product,\n })\n}\n"],"mappings":"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","names":[]}
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  createChatTurnRoutes,
3
3
  streamChatRouteAsSandboxEvents
4
- } from "../chunk-SYWEMKZR.js";
4
+ } from "../chunk-MFLUDFD3.js";
5
5
  import "../chunk-ZSSCVT6H.js";
6
- import "../chunk-IAY2DMKW.js";
6
+ import "../chunk-EA4UVS4T.js";
7
7
  import "../chunk-4PZE7XAM.js";
8
8
  import "../chunk-ZVEEWGDK.js";
9
9
  import "../chunk-X47R2IVO.js";
10
10
  import "../chunk-TXD5HXLE.js";
11
- import "../chunk-JEZJ6HTF.js";
11
+ import "../chunk-3NM4GCAY.js";
12
12
  import "../chunk-ZMMIQOFI.js";
13
13
  import "../chunk-M3K2HVQD.js";
14
14
  import "../chunk-YJMCRXQQ.js";
@@ -18,7 +18,7 @@ import {
18
18
  } from "../chunk-6A7MYOUI.js";
19
19
  import {
20
20
  assertMediaUrl
21
- } from "../chunk-IAY2DMKW.js";
21
+ } from "../chunk-EA4UVS4T.js";
22
22
  import "../chunk-TXD5HXLE.js";
23
23
 
24
24
  // src/sequences/operations.ts
@@ -8,3 +8,4 @@
8
8
  export * from './stream-normalizer';
9
9
  export * from './turn-identity';
10
10
  export * from './turn-buffer';
11
+ export * from './turn-observation';
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  buildUserTextParts,
3
+ consumeTurnStream,
3
4
  messageHasTurnId,
4
5
  normalizeClientTurnId,
6
+ observeTurnEvent,
7
+ parseTurnObservation,
5
8
  resolveChatTurn
6
- } from "../chunk-JEZJ6HTF.js";
9
+ } from "../chunk-3NM4GCAY.js";
7
10
  import {
8
11
  MISSING_TOOL_TERMINAL_ERROR,
9
12
  MISSING_TOOL_TERMINAL_REASON,
@@ -58,6 +61,7 @@ export {
58
61
  coalesceChatStreamEvents,
59
62
  coalesceDeltas,
60
63
  collapseRedundantTextParts,
64
+ consumeTurnStream,
61
65
  createBufferedTurnTap,
62
66
  createD1TurnEventStore,
63
67
  createMemoryTurnEventStore,
@@ -72,6 +76,8 @@ export {
72
76
  normalizePersistedPart,
73
77
  normalizeTime,
74
78
  normalizeToolEvent,
79
+ observeTurnEvent,
80
+ parseTurnObservation,
75
81
  pumpBufferedTurn,
76
82
  replayTurnEvents,
77
83
  resolveChatTurn,
@@ -0,0 +1,32 @@
1
+ /** Read-side checkpoints for native chat NDJSON. This never submits or cancels work. */
2
+ export interface TurnObservation {
3
+ schema: 'turn-observation-v1';
4
+ streamId: string | null;
5
+ /** Replay ordinals only. Unsequenced live frames cannot advance this cursor. */
6
+ lastSeq: number;
7
+ outcome: 'completed' | 'failed' | null;
8
+ replayStatus: string | null;
9
+ eventCount: number;
10
+ }
11
+ export declare function parseTurnObservation(value?: unknown): TurnObservation;
12
+ export interface TurnObservationUpdate {
13
+ checkpoint: TurnObservation;
14
+ /** False only for an already-checkpointed replay ordinal. */
15
+ accepted: boolean;
16
+ event: Record<string, unknown>;
17
+ }
18
+ /** Reduce an observed frame. Ordered replay gaps are errors, not silent lost work. */
19
+ export declare function observeTurnEvent(previous: TurnObservation, raw: unknown): TurnObservationUpdate;
20
+ export interface ConsumeTurnStreamOptions {
21
+ checkpoint?: TurnObservation;
22
+ /** Commit evidence and its checkpoint durably before acknowledging this frame. */
23
+ commit(update: TurnObservationUpdate): Promise<void>;
24
+ /** Bound untrusted frame memory, not total run duration or total transcript size. */
25
+ maxFrameBytes?: number;
26
+ }
27
+ /**
28
+ * Consume one viewing connection. EOF without a terminal event stays incomplete.
29
+ * A disconnect/abort propagates; the last committed checkpoint remains resumable.
30
+ * The caller owns network observation windows separately from execution deadlines.
31
+ */
32
+ export declare function consumeTurnStream(body: AsyncIterable<Uint8Array>, options: ConsumeTurnStreamOptions): Promise<TurnObservation>;
@@ -0,0 +1,12 @@
1
+ /** A credential-bearing client for one application origin, not a web fetch tool. */
2
+ export interface ApiKeyFetchOptions {
3
+ origin: string;
4
+ /** Resolve from trusted client secret storage; never from an agent's arguments. */
5
+ getApiKey(): string | Promise<string>;
6
+ fetchImpl?: typeof fetch;
7
+ /** Explicit local-development opt-in; never permits remote plaintext origins. */
8
+ allowHttpLoopback?: boolean;
9
+ }
10
+ export type ApiKeyFetch = (path: string, init?: RequestInit) => Promise<Response>;
11
+ /** No redirects, cookie fallback, hidden retries, or model-selected credential destination. */
12
+ export declare function createApiKeyFetch(options: ApiKeyFetchOptions): ApiKeyFetch;
@@ -1,3 +1,4 @@
1
1
  /** Browser-safe application-boundary helpers. No React or execution-engine peers. */
2
2
  export * from './core';
3
3
  export * from './message-groups';
4
+ export * from './api-key-fetch';
package/dist/web/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  checkFreeRouteLimit,
9
9
  checkRateLimit,
10
10
  clearCookieHeader,
11
+ createApiKeyFetch,
11
12
  extractRequestContext,
12
13
  freeRouteLimitResponse,
13
14
  groupConversationMessages,
@@ -16,7 +17,7 @@ import {
16
17
  requireString,
17
18
  serializeCookie,
18
19
  withFreeRouteLimit
19
- } from "../chunk-IAY2DMKW.js";
20
+ } from "../chunk-EA4UVS4T.js";
20
21
  import {
21
22
  isWorkspaceFileExportable
22
23
  } from "../chunk-TXD5HXLE.js";
@@ -30,6 +31,7 @@ export {
30
31
  checkFreeRouteLimit,
31
32
  checkRateLimit,
32
33
  clearCookieHeader,
34
+ createApiKeyFetch,
33
35
  extractRequestContext,
34
36
  freeRouteLimitResponse,
35
37
  groupConversationMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.48.2",
3
+ "version": "0.48.3",
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,11 +0,0 @@
1
- import {
2
- DesignCanvas
3
- } from "./chunk-JAT6SOPK.js";
4
- import "./chunk-AKMQFCDL.js";
5
- import "./chunk-WYHFHTSQ.js";
6
- import "./chunk-IAY2DMKW.js";
7
- import "./chunk-TXD5HXLE.js";
8
- export {
9
- DesignCanvas
10
- };
11
- //# sourceMappingURL=DesignCanvas-MWEYD6PR.js.map
@@ -1,14 +0,0 @@
1
- import {
2
- DesignCanvasEditor
3
- } from "./chunk-JF423ASS.js";
4
- import "./chunk-JAT6SOPK.js";
5
- import "./chunk-Q2QKEV6J.js";
6
- import "./chunk-VHMZ3KUH.js";
7
- import "./chunk-AKMQFCDL.js";
8
- import "./chunk-WYHFHTSQ.js";
9
- import "./chunk-IAY2DMKW.js";
10
- import "./chunk-TXD5HXLE.js";
11
- export {
12
- DesignCanvasEditor
13
- };
14
- //# sourceMappingURL=DesignCanvasEditor-UZ2S6BDD.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/web/rate-limit.ts","../src/web/free-route-limit.ts","../src/web/core.ts","../src/web/message-groups.ts"],"sourcesContent":["/**\n * The KV-backed sliding-window rate limiter. Lives in its own leaf so policy\n * layers (`./free-route-limit`) can build on it without importing the web\n * barrel and creating an import cycle. Re-exported from `./index`, which is the\n * published surface.\n */\n\n/** Minimal KV contract (Cloudflare `KVNamespace` satisfies it structurally). */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\n/** Describe the outcome of a rate limit check including allowance, remaining count, and reset time */\nexport interface RateLimitResult {\n allowed: boolean\n remaining: number\n resetAt: number\n}\n\n/** KV-backed sliding-window rate limit. Stores recent timestamps per key,\n * prunes the window, allows until `limit` is hit.\n *\n * Read-modify-write is best-effort, NOT atomic: KV has no compare-and-swap, so\n * two requests racing on the same key can each read the same pre-state and both\n * write — a concurrent burst can momentarily admit up to one extra request per\n * racing writer. This is acceptable for coarse abuse limiting; it is NOT a hard\n * quota gate.\n *\n * Fail-CLOSED on unreadable state: corrupt/non-array KV (a poisoned or\n * truncated value) is treated as a full window, so a tampered key cannot reset\n * the count and bypass the limiter. A bare `JSON.parse` here would throw and\n * abort the request handler, silently disabling the limit (fail-open). */\nexport async function checkRateLimit(kv: KvLike, key: string, limit: number, windowSeconds: number): Promise<RateLimitResult> {\n const now = Math.floor(Date.now() / 1000)\n const windowStart = now - windowSeconds\n const kvKey = `rl:${key}`\n const raw = await kv.get(kvKey)\n const parsed = parseRateLimitState(raw)\n if (parsed === POISONED_STATE) {\n // Unreadable state (parse threw or value is not a JSON array): deny rather\n // than reset the window to empty. A poisoned key must not become a bypass.\n return { allowed: false, remaining: 0, resetAt: now + windowSeconds }\n }\n const valid = parsed.filter((t) => t > windowStart)\n if (valid.length >= limit) return { allowed: false, remaining: 0, resetAt: (valid[0] ?? now) + windowSeconds }\n valid.push(now)\n await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 })\n return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds }\n}\n\n/** Sentinel returned by `parseRateLimitState` when the stored value cannot be\n * read as a timestamp array — distinct from an empty window so the limiter can\n * fail closed instead of treating corruption as a fresh window. */\nconst POISONED_STATE = Symbol('rate-limit-poisoned-state')\n\n/** Parse stored rate-limit state into a timestamp array. Absent state is a\n * fresh (empty) window. A value that fails to parse, or parses to a non-array,\n * returns `POISONED_STATE`; numeric junk inside a valid array is dropped. */\nfunction parseRateLimitState(raw: string | null): number[] | typeof POISONED_STATE {\n if (raw === null) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return POISONED_STATE\n }\n if (!Array.isArray(parsed)) return POISONED_STATE\n return parsed.filter((t): t is number => typeof t === 'number' && Number.isFinite(t))\n}\n","/**\n * Rate-limit policy for FREE routes: authenticated, unmetered, compute-bearing\n * endpoints.\n *\n * Billing meters the chat turn. Everything deterministic a product adds beside\n * it — a planning recompute fired on every slider drag, a redline diff, a\n * deadline computation — runs unmetered, so nothing bounds how much worker CPU\n * one authenticated caller can spend. `checkRateLimit` is the primitive; this\n * is the policy around it, so a route declares a cost class instead of each one\n * inventing a key format, a budget, and a denial response.\n *\n * Three rules this enforces that a hand-rolled call site usually does not:\n *\n * 1. **Identity is required.** A missing subject is denied, never pooled into a\n * shared bucket and never quietly keyed by IP — a shared bucket makes one\n * heavy user throttle everyone, and an IP key makes one NAT do the same.\n * 2. **A limiter that throws denies.** KV `get`/`put` failures are transport\n * failures; treating them as \"allowed\" converts a KV outage into an\n * unbounded-compute hole, which is exactly when the limiter matters most.\n * 3. **The denial is a typed, correctable outcome** — reason, HTTP status and\n * `Retry-After` — so the caller answers the client rather than failing\n * opaquely or, worse, continuing.\n *\n * ```ts\n * export const action = withFreeRouteLimit(\n * {\n * route: 'tax.plan.recompute',\n * costClass: 'interactive',\n * kv: ({ context }) => context.cloudflare.env.RATE_LIMIT_KV,\n * identify: async ({ request }) => {\n * const session = await requireSession(request)\n * return { subject: session.userId, workspace: session.workspaceId }\n * },\n * },\n * async ({ request }) => Response.json(recompute(await request.json())),\n * )\n * ```\n */\n\nimport { checkRateLimit, type KvLike, type RateLimitResult } from './rate-limit'\n\n/** Requests allowed per identity inside a sliding window. */\nexport interface RateLimitBudget {\n limit: number\n windowSeconds: number\n}\n\n/**\n * How expensive one call is, which is what picks the default budget.\n *\n * - `interactive` — a control the user drags or types into, recomputed per\n * tick. Sized for sustained two-per-second interaction, so a real slider\n * never trips it and a script still stops.\n * - `compute` — a derivation the user triggers deliberately (recalculate a\n * plan, diff a document, compute a deadline set). The default.\n * - `heavy` — whole-document or whole-matter work measured in seconds of CPU.\n */\nexport type FreeRouteClass = 'interactive' | 'compute' | 'heavy'\n\n/** Default budget per cost class. A product overrides with an explicit\n * `budget` when it has measured its own route. */\nexport const FREE_ROUTE_BUDGETS: Readonly<Record<FreeRouteClass, RateLimitBudget>> = Object.freeze({\n interactive: Object.freeze({ limit: 120, windowSeconds: 60 }),\n compute: Object.freeze({ limit: 30, windowSeconds: 60 }),\n heavy: Object.freeze({ limit: 10, windowSeconds: 300 }),\n})\n\n/** A workspace's shared ceiling defaults to this many times one member's, so a\n * multi-seat tenant is bounded without a single active member tripping it. */\nexport const WORKSPACE_BUDGET_MULTIPLIER = 5\n\n/** Why a call was refused. Every value is a REFUSAL — there is no reason code\n * that lets the handler run. */\nexport type FreeRouteDenialReason =\n /** No authenticated subject. Not correctable by waiting. */\n | 'unidentified'\n /** The window is full for this subject or workspace. */\n | 'rate-limited'\n /** The limiter itself failed (KV read/write threw). Denied, not passed. */\n | 'limiter-unavailable'\n\n/** Which window refused the call. `null` when no window was consulted. */\nexport type FreeRouteDimension = 'subject' | 'workspace' | null\n\n/**\n * A correctable refusal: the caller maps `status` + `retryAfterSeconds` onto a\n * response (see {@link freeRouteLimitResponse}) so the client learns what to do\n * next. Carrying the limiter's own failure in `cause` keeps a KV outage\n * diagnosable instead of collapsing into a generic 429.\n */\nexport class FreeRouteLimitError extends Error {\n readonly reason: FreeRouteDenialReason\n readonly status: 401 | 429 | 503\n readonly retryAfterSeconds: number\n readonly dimension: FreeRouteDimension\n readonly route: string\n\n constructor(init: {\n reason: FreeRouteDenialReason\n status: 401 | 429 | 503\n retryAfterSeconds: number\n dimension: FreeRouteDimension\n route: string\n message: string\n cause?: unknown\n }) {\n super(init.message, init.cause === undefined ? undefined : { cause: init.cause })\n this.name = 'FreeRouteLimitError'\n this.reason = init.reason\n this.status = init.status\n this.retryAfterSeconds = init.retryAfterSeconds\n this.dimension = init.dimension\n this.route = init.route\n }\n}\n\n/** What remains of the binding window after an allowed call. */\nexport interface FreeRouteAllowance {\n /** Remaining calls in whichever window is tightest. */\n remaining: number\n /** Unix seconds at which that window resets. */\n resetAt: number\n /** The budget that window enforces. */\n budget: RateLimitBudget\n /** Which window is tightest — `subject` unless a workspace ceiling binds. */\n dimension: Exclude<FreeRouteDimension, null>\n}\n\n/** Typed outcome at the route boundary: an allowance, or a refusal that says\n * how to correct it. There is no third state and no silent pass. */\nexport type FreeRouteLimitOutcome =\n | { succeeded: true; value: FreeRouteAllowance }\n | { succeeded: false; error: FreeRouteLimitError }\n\nexport interface FreeRouteLimitInput {\n kv: KvLike\n /** Stable route name. Part of the key, so two routes never share a window. */\n route: string\n /** The authenticated subject (user id). Absent or blank ⇒ denied. */\n subject: string | null | undefined\n /** Tenant the call belongs to. When present, a second, wider window bounds\n * the workspace as a whole. */\n workspace?: string | null\n /** Cost class picking the default budget. Ignored when `budget` is given.\n * Default `'compute'`. */\n costClass?: FreeRouteClass\n budget?: RateLimitBudget\n /** Workspace ceiling. Defaults to the subject budget's limit multiplied by\n * {@link WORKSPACE_BUDGET_MULTIPLIER} over the same window. */\n workspaceBudget?: RateLimitBudget\n}\n\n/** Key components are percent-encoded so a route name or subject containing the\n * separator cannot be crafted to collide with another caller's window. */\nfunction limitKey(route: string, dimension: string, id: string): string {\n return `free:${encodeURIComponent(route)}:${dimension}:${encodeURIComponent(id)}`\n}\n\nfunction assertBudget(budget: RateLimitBudget, label: string): void {\n const valid =\n Number.isInteger(budget.limit) &&\n budget.limit >= 1 &&\n Number.isInteger(budget.windowSeconds) &&\n budget.windowSeconds >= 1\n if (!valid) {\n throw new Error(\n `${label} must be positive integers (got limit=${budget.limit}, windowSeconds=${budget.windowSeconds})`,\n )\n }\n}\n\nfunction secondsUntil(resetAt: number): number {\n return Math.max(1, resetAt - Math.floor(Date.now() / 1000))\n}\n\n/**\n * Run one window. A throw from KV becomes a `limiter-unavailable` refusal\n * rather than propagating — the caller must be able to answer 503 with a\n * `Retry-After` instead of the handler running or the route 500ing opaquely.\n */\nasync function runWindow(\n kv: KvLike,\n route: string,\n dimension: Exclude<FreeRouteDimension, null>,\n id: string,\n budget: RateLimitBudget,\n): Promise<{ succeeded: true; value: RateLimitResult } | { succeeded: false; error: FreeRouteLimitError }> {\n try {\n const value = await checkRateLimit(kv, limitKey(route, dimension, id), budget.limit, budget.windowSeconds)\n return { succeeded: true, value }\n } catch (cause) {\n return {\n succeeded: false,\n error: new FreeRouteLimitError({\n reason: 'limiter-unavailable',\n status: 503,\n retryAfterSeconds: Math.min(budget.windowSeconds, 30),\n dimension,\n route,\n message: `Rate limiter unavailable for route '${route}' — the request is refused rather than admitted unmetered`,\n cause,\n }),\n }\n }\n}\n\n/**\n * Decide whether one authenticated, unmetered call may run.\n *\n * Checks the subject's window first, then the workspace ceiling when a\n * workspace is supplied; the tighter of the two is what the allowance reports.\n * Every failure path returns a refusal — none returns an allowance.\n */\nexport async function checkFreeRouteLimit(input: FreeRouteLimitInput): Promise<FreeRouteLimitOutcome> {\n const budget = input.budget ?? FREE_ROUTE_BUDGETS[input.costClass ?? 'compute']\n assertBudget(budget, `Free-route budget for '${input.route}'`)\n const workspaceBudget =\n input.workspaceBudget ?? { limit: budget.limit * WORKSPACE_BUDGET_MULTIPLIER, windowSeconds: budget.windowSeconds }\n if (input.workspace) assertBudget(workspaceBudget, `Free-route workspace budget for '${input.route}'`)\n\n const subject = input.subject?.trim()\n if (!subject) {\n return {\n succeeded: false,\n error: new FreeRouteLimitError({\n reason: 'unidentified',\n status: 401,\n // Not correctable by waiting: the caller must authenticate.\n retryAfterSeconds: 0,\n dimension: null,\n route: input.route,\n message: `Route '${input.route}' is authenticated-only — an unidentified caller cannot be rate limited, so it is refused`,\n }),\n }\n }\n\n const subjectWindow = await runWindow(input.kv, input.route, 'subject', subject, budget)\n if (!subjectWindow.succeeded) return subjectWindow\n if (!subjectWindow.value.allowed) {\n return { succeeded: false, error: rateLimited(input.route, 'subject', subjectWindow.value) }\n }\n\n let tightest: { result: RateLimitResult; budget: RateLimitBudget; dimension: Exclude<FreeRouteDimension, null> } = {\n result: subjectWindow.value,\n budget,\n dimension: 'subject',\n }\n\n if (input.workspace) {\n const workspaceWindow = await runWindow(input.kv, input.route, 'workspace', input.workspace, workspaceBudget)\n if (!workspaceWindow.succeeded) return workspaceWindow\n if (!workspaceWindow.value.allowed) {\n return { succeeded: false, error: rateLimited(input.route, 'workspace', workspaceWindow.value) }\n }\n if (workspaceWindow.value.remaining < tightest.result.remaining) {\n tightest = { result: workspaceWindow.value, budget: workspaceBudget, dimension: 'workspace' }\n }\n }\n\n return {\n succeeded: true,\n value: {\n remaining: tightest.result.remaining,\n resetAt: tightest.result.resetAt,\n budget: tightest.budget,\n dimension: tightest.dimension,\n },\n }\n}\n\nfunction rateLimited(\n route: string,\n dimension: Exclude<FreeRouteDimension, null>,\n result: RateLimitResult,\n): FreeRouteLimitError {\n const retryAfterSeconds = secondsUntil(result.resetAt)\n return new FreeRouteLimitError({\n reason: 'rate-limited',\n status: 429,\n retryAfterSeconds,\n dimension,\n route,\n message: `Route '${route}' rate limit reached for this ${dimension}; retry in ${retryAfterSeconds}s`,\n })\n}\n\nexport interface FreeRouteLimitResponseOptions {\n /** Extra response headers (a product's security headers, CORS). */\n headers?: Record<string, string>\n /** Client-facing message. Defaults to the error's own message. */\n message?: string\n}\n\n/** The refusal as an HTTP response: the error's status, a JSON body carrying\n * the machine-readable `reason`, and `Retry-After` when waiting can fix it. */\nexport function freeRouteLimitResponse(\n error: FreeRouteLimitError,\n options: FreeRouteLimitResponseOptions = {},\n): Response {\n const headers = new Headers(options.headers)\n headers.set('Content-Type', 'application/json')\n if (error.retryAfterSeconds > 0) headers.set('Retry-After', String(error.retryAfterSeconds))\n return new Response(\n JSON.stringify({\n error: options.message ?? error.message,\n reason: error.reason,\n retryAfterSeconds: error.retryAfterSeconds,\n }),\n { status: error.status, headers },\n )\n}\n\n/** The authenticated caller behind one request. */\nexport interface FreeRouteIdentity {\n subject: string\n workspace?: string | null\n}\n\nexport interface WithFreeRouteLimitOptions<TArgs> {\n /** Stable route name; part of the key. */\n route: string\n costClass?: FreeRouteClass\n budget?: RateLimitBudget\n workspaceBudget?: RateLimitBudget\n /** The KV binding for this request. */\n kv: (args: TArgs) => KvLike\n /**\n * Resolve the authenticated caller. Return `null` for an unauthenticated\n * request — the wrapper then refuses with 401 and the handler never runs.\n *\n * A THROW here propagates: an auth lookup that failed is not the same as an\n * absent session, and the wrapper will not invent a status for it. The\n * handler still does not run, so the failure stays closed.\n */\n identify: (args: TArgs) => Promise<FreeRouteIdentity | null> | FreeRouteIdentity | null\n /** Build the refusal response. Defaults to {@link freeRouteLimitResponse}. */\n onDenied?: (error: FreeRouteLimitError, args: TArgs) => Response | Promise<Response>\n}\n\n/**\n * Wrap an authenticated, unmetered, compute-bearing route handler in its\n * budget. The limit is applied BEFORE the handler is entered — including\n * before the request body is read — so a refused call costs a KV read, not the\n * compute the route exists to do.\n *\n * The handler's own response is returned untouched: this never rewrites status,\n * body or headers on the success path, so it cannot break a streaming or\n * pre-built response.\n *\n * `TArgs` is the router's handler-argument object (react-router's\n * `ActionFunctionArgs`, or any single-object shape a product uses).\n */\nexport function withFreeRouteLimit<TArgs>(\n options: WithFreeRouteLimitOptions<TArgs>,\n handler: (args: TArgs) => Response | Promise<Response>,\n): (args: TArgs) => Promise<Response> {\n return async (args: TArgs): Promise<Response> => {\n const identity = await options.identify(args)\n const outcome = await checkFreeRouteLimit({\n kv: options.kv(args),\n route: options.route,\n subject: identity?.subject,\n workspace: identity?.workspace,\n costClass: options.costClass,\n budget: options.budget,\n workspaceBudget: options.workspaceBudget,\n })\n if (!outcome.succeeded) {\n return options.onDenied ? await options.onDenied(outcome.error, args) : freeRouteLimitResponse(outcome.error)\n }\n return await handler(args)\n }\n}\n","/**\n * Web-boundary utilities every agent app's routes hand-roll: JSON body parsing\n * + narrowing, request-context extraction (real client IP behind Cloudflare),\n * a KV-backed sliding-window rate limiter, the free-route budget policy built\n * on it, and security response headers. Pure mechanism — no DB, no domain. The\n * KV is a structural interface so this needs no `@cloudflare/workers-types`\n * dependency.\n */\n\nexport * from './rate-limit'\nexport * from './free-route-limit'\n\nexport type JsonObject = Record<string, unknown>\n\n/** Parse + object-narrow a Request body. `[body, null]` on success, `[null,\n * errorResponse]` on a non-object body (callers `if (err) return err`). */\nexport async function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]> {\n let raw: unknown\n try {\n raw = await request.json()\n } catch {\n return [null, Response.json({ error: 'Invalid JSON body' }, { status: 400 })]\n }\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n return [null, Response.json({ error: 'Body must be a JSON object' }, { status: 400 })]\n }\n return [raw as JsonObject, null]\n}\n\n/** Narrow one required string field, 400 if missing/empty. */\nexport function requireString(body: JsonObject, field: string): string | Response {\n const v = body[field]\n if (typeof v !== 'string' || v.length === 0) {\n return Response.json({ error: `Missing or non-string field: ${field}` }, { status: 400 })\n }\n return v\n}\n\n/** Define the context of a request including IP address, user agent, timestamp, and request ID */\nexport interface RequestContext {\n ipAddress: string\n userAgent: string\n timestamp: string\n requestId: string\n}\n\n/** Extract request context for audit trails. Uses `CF-Connecting-IP` for the\n * real client IP behind Cloudflare. */\nexport function extractRequestContext(request: Request): RequestContext {\n const ipAddress =\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() ??\n '0.0.0.0'\n return {\n ipAddress,\n userAgent: request.headers.get('User-Agent') ?? '',\n timestamp: new Date().toISOString(),\n requestId: crypto.randomUUID(),\n }\n}\n\n/** Define options for configuring cookie attributes and behavior */\nexport interface CookieOptions {\n name: string\n /** Default '/'. */\n path?: string\n /** Default true. */\n httpOnly?: boolean\n /** Adds the `Secure` attribute. Default false. */\n secure?: boolean\n /** Default 'Lax'. */\n sameSite?: 'Lax' | 'Strict' | 'None'\n maxAgeSeconds?: number\n}\n\n/** Serialize a Set-Cookie header value: `name=encodeURIComponent(value)` plus\n * attributes in Path / HttpOnly / SameSite / Max-Age / Secure order.\n * Throws on `SameSite=None` without `secure` — browsers silently drop that\n * combination, which would otherwise fail invisibly. */\nexport function serializeCookie(value: string, opts: CookieOptions): string {\n if (opts.sameSite === 'None' && !opts.secure) {\n throw new Error('SameSite=None cookies require secure: true (browsers reject them otherwise)')\n }\n const parts = [`${opts.name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? '/'}`]\n if (opts.httpOnly !== false) parts.push('HttpOnly')\n parts.push(`SameSite=${opts.sameSite ?? 'Lax'}`)\n if (opts.maxAgeSeconds !== undefined) parts.push(`Max-Age=${opts.maxAgeSeconds}`)\n if (opts.secure) parts.push('Secure')\n return parts.join('; ')\n}\n\n/** Set-Cookie header value that deletes the cookie (empty value, Max-Age=0). */\nexport function clearCookieHeader(opts: Omit<CookieOptions, 'maxAgeSeconds'>): string {\n return serializeCookie('', { ...opts, maxAgeSeconds: 0 })\n}\n\n/** Read + decode one cookie from a Cookie request header; null when absent. */\nexport function readCookieValue(cookieHeader: string | null, name: string): string | null {\n if (!cookieHeader) return null\n for (const part of cookieHeader.split(/;\\s*/)) {\n const [cookieName, ...rest] = part.split('=')\n if (cookieName === name) {\n try {\n return decodeURIComponent(rest.join('='))\n } catch {\n return null\n }\n }\n }\n return null\n}\n\n/** Define options for configuring security-related HTTP headers including disclaimers and retention labels */\nexport interface SecurityHeaderOptions {\n /** Product disclaimer (e.g. \"AI-powered tool. Not legal advice.\"). Omitted if absent. */\n disclaimer?: string\n /** Data-retention label (e.g. \"7-years\"). Omitted if absent. */\n retention?: string\n /** Extra headers to set. */\n extra?: Record<string, string>\n}\n\n/** Canonical generic response headers used by {@link addSecurityHeaders}.\n * Exported so static-asset hosts can apply the same policy without copying\n * values that silently drift from Worker/API responses. */\nexport const STANDARD_SECURITY_HEADERS = Object.freeze({\n 'Strict-Transport-Security':\n 'max-age=31536000; includeSubDomains; preload',\n 'X-Content-Type-Options': 'nosniff',\n 'X-Frame-Options': 'SAMEORIGIN',\n 'Referrer-Policy': 'same-origin',\n 'X-XSS-Protection': '1; mode=block',\n} as const)\n\n/** Set standard security headers on a response (HSTS, nosniff, frame-options,\n * referrer-policy, XSS) + optional product disclaimer/retention. The security\n * set is generic; the disclaimer/retention are the product's. */\nexport function addSecurityHeaders(response: Response, opts: SecurityHeaderOptions = {}): Response {\n for (const [name, value] of Object.entries(STANDARD_SECURITY_HEADERS)) {\n response.headers.set(name, value)\n }\n if (opts.disclaimer) response.headers.set('X-AI-Disclaimer', opts.disclaimer)\n if (opts.retention) response.headers.set('X-Data-Retention', opts.retention)\n for (const [k, v] of Object.entries(opts.extra ?? {})) response.headers.set(k, v)\n return response\n}\n\n/** Local-sandbox / inline schemes a stored media reference must never use.\n * Reachable from neither a browser nor the product worker, and a `file:`/`data:`\n * url is the tell of an agent substituting local ffmpeg output for a real\n * provider artifact. `blob:` and `javascript:` are inert/active client schemes\n * with no server reachability. */\nconst REJECTED_MEDIA_SCHEMES = ['file:', 'data:', 'blob:', 'javascript:', 'vbscript:'] as const\n\n/**\n * Canonical media-reference boundary shared by every surface that persists a\n * media url (sequences clips, design-canvas image/video src). The ONE rule:\n * remote `http(s)` or a rooted `/api/` path are allowed; everything else is\n * rejected, with a named reason for known-bad local/inline schemes so the\n * thrown message is actionable for an LLM planner. The url is trimmed before\n * the scheme check so leading whitespace cannot smuggle a rejected scheme past\n * a naive `startsWith`.\n *\n * @param what - noun for the error message (e.g. 'media url', 'src').\n */\nexport function assertMediaUrl(url: string, what = 'media url'): void {\n const trimmed = url.trim()\n if (/^https?:\\/\\//i.test(trimmed)) return\n if (trimmed.startsWith('/api/')) return\n const shown = trimmed.length > 96 ? `${trimmed.slice(0, 96)}…` : trimmed\n const lower = trimmed.toLowerCase()\n if (\n REJECTED_MEDIA_SCHEMES.some((scheme) => lower.startsWith(scheme)) ||\n lower.startsWith('/tmp/') ||\n lower.startsWith('/home/')\n ) {\n throw new Error(`${what} must reference a provider http(s) URL or a rooted /api/ path, not a local sandbox file (${shown})`)\n }\n throw new Error(`${what} must be http(s) or a rooted /api/ path (${shown})`)\n}\n\nexport { isWorkspaceFileExportable } from './file-export'\n","/** Renderer-neutral attribution. Not a transcript merger or an execution state machine. */\nexport interface ConversationGroupItem {\n id?: string | number\n kind?: string\n role?: string\n /** A different assistant/persona must never inherit the previous speaker's label. */\n speakerId?: string\n /** Distinct threads must not be grouped if their rows share a viewport. */\n conversationId?: string\n}\nexport type GroupedConversationItem<T> = T & { isContinuation?: boolean; groupId?: string }\n\n/**\n * Annotate assistant rows until an actual user message, speaker or conversation change.\n * Tool/progress notices do not interrupt the group. The first visible assistant is\n * always labeled, including when a renderer has paged earlier history away.\n * Pass display order; stored content, IDs, timestamps and input objects are untouched.\n */\nexport function groupConversationMessages<T extends ConversationGroupItem>(items: readonly T[] = []): GroupedConversationItem<T>[] {\n let speaker: string | null = null\n let conversation: string | undefined\n let groupId: string | undefined\n return items.map((item, index) => {\n if (item.conversationId !== undefined && item.conversationId !== conversation) {\n conversation = item.conversationId\n speaker = null\n groupId = undefined\n }\n const role = item.kind === 'thinking' ? 'assistant' : (!item.kind || item.kind === 'message' ? item.role : undefined)\n if (role === 'user') { speaker = null; groupId = undefined; return { ...item, isContinuation: false } }\n if (role !== 'assistant') return item\n const current = item.speakerId ?? 'assistant'\n const isContinuation = speaker === current\n if (!isContinuation) groupId = String(item.id ?? `assistant-${index}`)\n speaker = current\n return { ...item, isContinuation, groupId }\n })\n}\n"],"mappings":"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","names":[]}
@@ -1,77 +0,0 @@
1
- // src/stream/turn-identity.ts
2
- function normalizeClientTurnId(value) {
3
- if (value === void 0 || value === null) return void 0;
4
- if (typeof value !== "string") throw new Error("turnId must be a string");
5
- const trimmed = value.trim();
6
- if (!trimmed) throw new Error("turnId must not be blank");
7
- if (trimmed.length > 160) throw new Error("turnId is too long");
8
- if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {
9
- throw new Error("turnId contains unsupported characters");
10
- }
11
- return trimmed;
12
- }
13
- function buildUserTextParts(text, turnId) {
14
- const part = { type: "text", text };
15
- if (turnId) part.turnId = turnId;
16
- return [part];
17
- }
18
- function messageHasTurnId(message, turnId) {
19
- for (const part of message.parts ?? []) {
20
- if (part && typeof part === "object" && String(part.turnId ?? "") === turnId) {
21
- return true;
22
- }
23
- }
24
- return false;
25
- }
26
- function resolveChatTurn(input) {
27
- const { existingMessages, userContent, turnId } = input;
28
- const reusableIndex = findReusableUserMessageIndex(
29
- existingMessages,
30
- userContent,
31
- turnId,
32
- input.hasRunningTurn === true
33
- );
34
- if (reusableIndex >= 0) {
35
- const reusedId = existingMessages[reusableIndex]?.id;
36
- return {
37
- turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),
38
- shouldInsertUserMessage: false,
39
- priorMessages: existingMessages.slice(0, reusableIndex),
40
- userParts: buildUserTextParts(userContent, turnId),
41
- ...typeof reusedId === "string" && reusedId ? { reusedUserMessageId: reusedId } : {}
42
- };
43
- }
44
- return {
45
- turnIndex: countUserMessages(existingMessages),
46
- shouldInsertUserMessage: true,
47
- priorMessages: existingMessages,
48
- userParts: buildUserTextParts(userContent, turnId)
49
- };
50
- }
51
- function findReusableUserMessageIndex(messages, userContent, turnId, hasRunningTurn) {
52
- if (turnId) {
53
- for (let index2 = messages.length - 1; index2 >= 0; index2 -= 1) {
54
- const message = messages[index2];
55
- if (message?.role === "user" && messageHasTurnId(message, turnId)) return index2;
56
- }
57
- return -1;
58
- }
59
- let index = messages.length - 1;
60
- if (hasRunningTurn) {
61
- while (index >= 0 && messages[index]?.role === "assistant") index -= 1;
62
- }
63
- const latest = index >= 0 ? messages[index] : void 0;
64
- if (latest?.role === "user" && latest.content === userContent) return index;
65
- return -1;
66
- }
67
- function countUserMessages(messages) {
68
- return messages.filter((message) => message.role === "user").length;
69
- }
70
-
71
- export {
72
- normalizeClientTurnId,
73
- buildUserTextParts,
74
- messageHasTurnId,
75
- resolveChatTurn
76
- };
77
- //# sourceMappingURL=chunk-JEZJ6HTF.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/stream/turn-identity.ts"],"sourcesContent":["import type { JsonRecord } from './stream-normalizer'\n\n/** Define the structure of a chat message stored for a specific conversation turn */\nexport interface PersistedChatMessageForTurn {\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts: Array<Record<string, unknown>> | null\n}\n\n/** Represent a chat turn with resolved user message insertion and prior message context */\nexport interface ResolvedChatTurn {\n turnIndex: number\n shouldInsertUserMessage: boolean\n priorMessages: PersistedChatMessageForTurn[]\n userParts: JsonRecord[]\n /** The id of the user row this turn REUSES (retry dedup), when one was\n * found. Absent on the insert path, where no row exists yet.\n *\n * The reused row is deliberately EXCLUDED from `priorMessages` (it is this\n * turn's own user message, not prior context), so this field is the only\n * way a caller can name it. */\n reusedUserMessageId?: string\n}\n\n/** Normalize and validate a client turn ID string ensuring it meets format and length requirements */\nexport function normalizeClientTurnId(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value !== 'string') throw new Error('turnId must be a string')\n const trimmed = value.trim()\n if (!trimmed) throw new Error('turnId must not be blank')\n if (trimmed.length > 160) throw new Error('turnId is too long')\n if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {\n throw new Error('turnId contains unsupported characters')\n }\n return trimmed\n}\n\n/** Build an array of text parts with optional turn ID for user input */\nexport function buildUserTextParts(text: string, turnId: string | undefined): JsonRecord[] {\n const part: JsonRecord = { type: 'text', text }\n if (turnId) part.turnId = turnId\n return [part]\n}\n\n/** Resolve whether a message contains any part with the specified turn ID */\nexport function messageHasTurnId(message: PersistedChatMessageForTurn, turnId: string): boolean {\n for (const part of message.parts ?? []) {\n if (part && typeof part === 'object' && String(part.turnId ?? '') === turnId) {\n return true\n }\n }\n return false\n}\n\n/** Resolve a chat turn by determining message reuse and constructing user message parts */\nexport function resolveChatTurn(input: {\n existingMessages: PersistedChatMessageForTurn[]\n userContent: string\n turnId?: string\n /** True when the thread has a turn still RUNNING in the turn-event buffer\n * (`turnStore.listRunning(threadId)`).\n *\n * Without incremental persistence the trailing row of a thread mid-turn is\n * always the user row, so the content fallback below could assume it. With\n * incremental persistence the assistant row lands seconds into the turn, so\n * a retry of that same turn finds an ASSISTANT row trailing and would\n * insert a duplicate user row.\n *\n * This flag is the discriminator that keeps both cases right, and it needs\n * no new state: an assistant row trailing a turn that is still running is\n * that turn's in-flight draft (walk past it — this is a retry), whereas an\n * assistant row trailing a SETTLED turn is a completed answer (stop — the\n * user genuinely repeated a message and deserves a new turn). */\n hasRunningTurn?: boolean\n}): ResolvedChatTurn {\n const { existingMessages, userContent, turnId } = input\n const reusableIndex = findReusableUserMessageIndex(\n existingMessages,\n userContent,\n turnId,\n input.hasRunningTurn === true,\n )\n if (reusableIndex >= 0) {\n // Guarded read: `id` is typed `string`, but these rows come from a product\n // store the shell does not validate, so an adapter that omits it must\n // surface as \"no id\" rather than as the string \"undefined\".\n const reusedId = existingMessages[reusableIndex]?.id\n return {\n turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),\n shouldInsertUserMessage: false,\n priorMessages: existingMessages.slice(0, reusableIndex),\n userParts: buildUserTextParts(userContent, turnId),\n ...(typeof reusedId === 'string' && reusedId ? { reusedUserMessageId: reusedId } : {}),\n }\n }\n\n return {\n turnIndex: countUserMessages(existingMessages),\n shouldInsertUserMessage: true,\n priorMessages: existingMessages,\n userParts: buildUserTextParts(userContent, turnId),\n }\n}\n\nfunction findReusableUserMessageIndex(\n messages: PersistedChatMessageForTurn[],\n userContent: string,\n turnId: string | undefined,\n hasRunningTurn: boolean,\n): number {\n if (turnId) {\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]\n if (message?.role === 'user' && messageHasTurnId(message, turnId)) return index\n }\n // A caller-supplied id is authoritative. A different id with identical\n // text is a deliberate new turn, while a transport retry reuses the same\n // id and takes the match above. Falling through to content dedup here lets\n // an unrelated abandoned running row swallow an intentional repeat.\n return -1\n }\n\n // Content fallback for a client that sends no turnId. Only the trailing rows\n // of a turn still RUNNING are walked past (they are that turn's incrementally\n // persisted assistant draft); a settled assistant row still ends the scan, so\n // a user who genuinely repeats a message gets a new turn exactly as before.\n let index = messages.length - 1\n if (hasRunningTurn) {\n while (index >= 0 && messages[index]?.role === 'assistant') index -= 1\n }\n const latest = index >= 0 ? messages[index] : undefined\n if (latest?.role === 'user' && latest.content === userContent) return index\n\n return -1\n}\n\nfunction countUserMessages(messages: PersistedChatMessageForTurn[]): number {\n return messages.filter((message) => message.role === 'user').length\n}\n"],"mappings":";AA0BO,SAAS,sBAAsB,OAAoC;AACxE,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,OAAM,IAAI,MAAM,yBAAyB;AACxE,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,0BAA0B;AACxD,MAAI,QAAQ,SAAS,IAAK,OAAM,IAAI,MAAM,oBAAoB;AAC9D,MAAI,CAAC,oBAAoB,KAAK,OAAO,GAAG;AACtC,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AACA,SAAO;AACT;AAGO,SAAS,mBAAmB,MAAc,QAA0C;AACzF,QAAM,OAAmB,EAAE,MAAM,QAAQ,KAAK;AAC9C,MAAI,OAAQ,MAAK,SAAS;AAC1B,SAAO,CAAC,IAAI;AACd;AAGO,SAAS,iBAAiB,SAAsC,QAAyB;AAC9F,aAAW,QAAQ,QAAQ,SAAS,CAAC,GAAG;AACtC,QAAI,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,UAAU,EAAE,MAAM,QAAQ;AAC5E,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,OAmBX;AACnB,QAAM,EAAE,kBAAkB,aAAa,OAAO,IAAI;AAClD,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,mBAAmB;AAAA,EAC3B;AACA,MAAI,iBAAiB,GAAG;AAItB,UAAM,WAAW,iBAAiB,aAAa,GAAG;AAClD,WAAO;AAAA,MACL,WAAW,kBAAkB,iBAAiB,MAAM,GAAG,aAAa,CAAC;AAAA,MACrE,yBAAyB;AAAA,MACzB,eAAe,iBAAiB,MAAM,GAAG,aAAa;AAAA,MACtD,WAAW,mBAAmB,aAAa,MAAM;AAAA,MACjD,GAAI,OAAO,aAAa,YAAY,WAAW,EAAE,qBAAqB,SAAS,IAAI,CAAC;AAAA,IACtF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,WAAW,kBAAkB,gBAAgB;AAAA,IAC7C,yBAAyB;AAAA,IACzB,eAAe;AAAA,IACf,WAAW,mBAAmB,aAAa,MAAM;AAAA,EACnD;AACF;AAEA,SAAS,6BACP,UACA,aACA,QACA,gBACQ;AACR,MAAI,QAAQ;AACV,aAASA,SAAQ,SAAS,SAAS,GAAGA,UAAS,GAAGA,UAAS,GAAG;AAC5D,YAAM,UAAU,SAASA,MAAK;AAC9B,UAAI,SAAS,SAAS,UAAU,iBAAiB,SAAS,MAAM,EAAG,QAAOA;AAAA,IAC5E;AAKA,WAAO;AAAA,EACT;AAMA,MAAI,QAAQ,SAAS,SAAS;AAC9B,MAAI,gBAAgB;AAClB,WAAO,SAAS,KAAK,SAAS,KAAK,GAAG,SAAS,YAAa,UAAS;AAAA,EACvE;AACA,QAAM,SAAS,SAAS,IAAI,SAAS,KAAK,IAAI;AAC9C,MAAI,QAAQ,SAAS,UAAU,OAAO,YAAY,YAAa,QAAO;AAEtE,SAAO;AACT;AAEA,SAAS,kBAAkB,UAAiD;AAC1E,SAAO,SAAS,OAAO,CAAC,YAAY,QAAQ,SAAS,MAAM,EAAE;AAC/D;","names":["index"]}