@tangle-network/agent-app 0.47.15 → 0.47.16

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 (74) hide show
  1. package/README.md +8 -0
  2. package/dist/DesignCanvas-UPRRA4YG.js +11 -0
  3. package/dist/DesignCanvasEditor-GVJO5SMH.js +14 -0
  4. package/dist/app-auth/index.js +3 -2
  5. package/dist/app-auth/index.js.map +1 -1
  6. package/dist/assistant/index.js +7 -6
  7. package/dist/assistant/index.js.map +1 -1
  8. package/dist/chat-react/index.js +4 -3
  9. package/dist/chat-routes/index.js +12 -6
  10. package/dist/chat-routes/index.js.map +1 -1
  11. package/dist/chat-routes/wire.d.ts +3 -3
  12. package/dist/chat-store/index.js +3 -2
  13. package/dist/chat-store/index.js.map +1 -1
  14. package/dist/{chunk-LYAYNWDS.js → chunk-3GVFJYLO.js} +4 -4
  15. package/dist/{chunk-F7VH4G5C.js → chunk-3YUI7WNW.js} +1 -1
  16. package/dist/{chunk-F7VH4G5C.js.map → chunk-3YUI7WNW.js.map} +1 -1
  17. package/dist/{chunk-IRYULX3R.js → chunk-4PZE7XAM.js} +2 -2
  18. package/dist/{chunk-7LJYCHXK.js → chunk-54KJ54SY.js} +3 -3
  19. package/dist/{chunk-DRB77FYL.js → chunk-A2XMTCER.js} +2 -2
  20. package/dist/{chunk-UB4LENR5.js → chunk-AXJV66PC.js} +2 -2
  21. package/dist/{chunk-HZVVO6VR.js → chunk-FQYAJYPW.js} +11 -4
  22. package/dist/chunk-FQYAJYPW.js.map +1 -0
  23. package/dist/{chunk-X5DQ5QOP.js → chunk-G4CPEJNN.js} +2 -2
  24. package/dist/{chunk-TTYJIMTR.js → chunk-H6R3VYAZ.js} +334 -164
  25. package/dist/chunk-H6R3VYAZ.js.map +1 -0
  26. package/dist/{chunk-33BN7P57.js → chunk-HUYTN7RI.js} +5 -5
  27. package/dist/{chunk-SUO2R2ET.js → chunk-NU7QSZBR.js} +2 -2
  28. package/dist/{chunk-OYTMILRK.js → chunk-QEPLXYOE.js} +2 -2
  29. package/dist/{chunk-FMEL7VJR.js → chunk-TNWQA5XJ.js} +2 -2
  30. package/dist/chunk-TXD5HXLE.js +14 -0
  31. package/dist/chunk-TXD5HXLE.js.map +1 -0
  32. package/dist/{chunk-UMV45ZDF.js → chunk-WOZSOPUZ.js} +3 -3
  33. package/dist/{chunk-DXHUQ5NO.js → chunk-X47R2IVO.js} +8 -1
  34. package/dist/chunk-X47R2IVO.js.map +1 -0
  35. package/dist/design-canvas/index.js +3 -2
  36. package/dist/design-canvas/index.js.map +1 -1
  37. package/dist/design-canvas-react/engine.js +5 -4
  38. package/dist/design-canvas-react/index.js +8 -7
  39. package/dist/design-canvas-react/index.js.map +1 -1
  40. package/dist/design-canvas-react/lazy.js +1 -1
  41. package/dist/platform/api-key-auth.d.ts +27 -0
  42. package/dist/platform/index.d.ts +1 -0
  43. package/dist/platform/index.js +47 -2
  44. package/dist/platform/index.js.map +1 -1
  45. package/dist/public-consultation/index.d.ts +1 -1
  46. package/dist/public-consultation/index.js +5 -4
  47. package/dist/public-consultation/index.js.map +1 -1
  48. package/dist/sequences/index.js +2 -1
  49. package/dist/sequences/index.js.map +1 -1
  50. package/dist/web/file-export.d.ts +6 -0
  51. package/dist/web/index.d.ts +1 -0
  52. package/dist/web/index.js +5 -1
  53. package/dist/web-react/api-access-panel.d.ts +33 -0
  54. package/dist/web-react/index.d.ts +1 -0
  55. package/dist/web-react/index.js +9 -6
  56. package/package.json +1 -1
  57. package/dist/DesignCanvas-JRBCAWHW.js +0 -10
  58. package/dist/DesignCanvasEditor-J2OLS22G.js +0 -13
  59. package/dist/chunk-DXHUQ5NO.js.map +0 -1
  60. package/dist/chunk-HZVVO6VR.js.map +0 -1
  61. package/dist/chunk-TTYJIMTR.js.map +0 -1
  62. /package/dist/{DesignCanvas-JRBCAWHW.js.map → DesignCanvas-UPRRA4YG.js.map} +0 -0
  63. /package/dist/{DesignCanvasEditor-J2OLS22G.js.map → DesignCanvasEditor-GVJO5SMH.js.map} +0 -0
  64. /package/dist/{chunk-LYAYNWDS.js.map → chunk-3GVFJYLO.js.map} +0 -0
  65. /package/dist/{chunk-IRYULX3R.js.map → chunk-4PZE7XAM.js.map} +0 -0
  66. /package/dist/{chunk-7LJYCHXK.js.map → chunk-54KJ54SY.js.map} +0 -0
  67. /package/dist/{chunk-DRB77FYL.js.map → chunk-A2XMTCER.js.map} +0 -0
  68. /package/dist/{chunk-UB4LENR5.js.map → chunk-AXJV66PC.js.map} +0 -0
  69. /package/dist/{chunk-X5DQ5QOP.js.map → chunk-G4CPEJNN.js.map} +0 -0
  70. /package/dist/{chunk-33BN7P57.js.map → chunk-HUYTN7RI.js.map} +0 -0
  71. /package/dist/{chunk-SUO2R2ET.js.map → chunk-NU7QSZBR.js.map} +0 -0
  72. /package/dist/{chunk-OYTMILRK.js.map → chunk-QEPLXYOE.js.map} +0 -0
  73. /package/dist/{chunk-FMEL7VJR.js.map → chunk-TNWQA5XJ.js.map} +0 -0
  74. /package/dist/{chunk-UMV45ZDF.js.map → chunk-WOZSOPUZ.js.map} +0 -0
@@ -1,9 +1,9 @@
1
1
  import type { ReasoningEffort } from '@tangle-network/agent-interface';
2
2
  /**
3
3
  * Wire contract between the chat client (composer + `streamChatTurn`) and the
4
- * assembled server vertical (`createChatTurnRoutes`). Runtime-import-free on
5
- * purpose: `/web-react` re-exports these types into browser bundles, so nothing
6
- * here may reach a Node builtin or an engine package.
4
+ * assembled server vertical (`createChatTurnRoutes`). This module must remain
5
+ * browser-safe: `/web-react` re-exports these types into browser bundles, so
6
+ * imports must not reach a Node builtin or an engine package.
7
7
  *
8
8
  * The client part shape permits an absolute file path until the server converts
9
9
  * it to the URL required by the sandbox SDK. It is derived here, not imported,
@@ -19,9 +19,10 @@ import {
19
19
  mentionInputToPart,
20
20
  mentionPartsFromMessageParts,
21
21
  toChatMessageParts
22
- } from "../chunk-IRYULX3R.js";
22
+ } from "../chunk-4PZE7XAM.js";
23
23
  import "../chunk-ZVEEWGDK.js";
24
- import "../chunk-DXHUQ5NO.js";
24
+ import "../chunk-X47R2IVO.js";
25
+ import "../chunk-TXD5HXLE.js";
25
26
  import {
26
27
  attachmentPartKey
27
28
  } from "../chunk-ZMMIQOFI.js";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/chat-store/core.ts","../../src/chat-store/schema.ts","../../src/chat-store/store.ts"],"sourcesContent":["/**\n * Pure (drizzle-free) pieces of the chat store: thread-title derivation, the\n * bulk-delete bound, and the typed input error. Split from `./schema`/`./store`\n * so the root barrel can re-export them without dragging the optional\n * drizzle-orm peer into every root-entry consumer.\n */\n\n/** Bounds a single bulk-delete request's write set; product surfaces cap\n * thread lists at far fewer, so a larger batch is a malformed or hostile\n * request. (Lifted from legal's api.threads.bulk-delete route.) */\nexport const BULK_DELETE_MAX_THREADS = 200\n\n/** Invalid caller input (missing/oversized ids, empty title). Products map it\n * to a 400; anything else out of the store is a real failure. */\nexport class ChatStoreInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ChatStoreInputError'\n }\n}\n\n/** Thread titles come from the first message — keep the list scannable by\n * storing only its first non-empty line, capped at 80 chars, never the whole\n * multi-page prompt. (Lifted verbatim from legal's chat.new route.) */\nexport function threadTitleFromMessage(message: string): string {\n const firstLine = message.split('\\n').find((l) => l.trim().length > 0)?.trim() ?? ''\n if (!firstLine) return 'New Thread'\n return firstLine.length > 80 ? `${firstLine.slice(0, 79)}…` : firstLine\n}\n","/**\n * Drizzle schema factory for the chat thread/message tables — the same\n * injection pattern as `createTeamTables`: the product owns the workspace\n * table; the factory wires the thread FK into it so the whole graph lives in\n * one drizzle schema with real cascade semantics. Column names, types,\n * defaults, enums, and indexes mirror legal's and gtm's hand-rolled `thread`/\n * `message` tables so a product with those tables adopts the factory without\n * rewriting rows; `tablePrefix` covers products that namespace (tax's\n * `chat_messages` style).\n *\n * The core is the superset the three products agree on. Divergences dropped,\n * and why:\n * - `thread.status` ('active'|'archived', legal+gtm) — archive semantics\n * diverge (tax uses `archivedAt`); product-domain lifecycle → extra column.\n * - `thread.scopeKind`/`scopeKey`/`harness` (gtm) — artifact anchoring and\n * harness pinning are product-domain → extra columns.\n * - tax's `tax_sessions` session columns (`taxYear`, `projectRef`,\n * `agentSessionId`, `agentRuntime`, `agentHarness`, `profile`, `error`,\n * `userId`) — sandbox-session state, not chat state → extra columns.\n * - `message.toolInput`/`toolOutput` (legal+gtm) — duplicate of the tool\n * part's `state.input`/`state.output` inside `parts` (the shape `/stream`'s\n * `normalizePersistedPart` owns); keeping both invites drift.\n * - `message.vaultFiles` (legal+gtm) — vault is product-domain → extra column.\n * - tax's re-declared `turn_events`/`turn_status` DDL — deliberately NOT here;\n * `/stream`'s turn-buffer owns that DDL (`TURN_BUFFER_D1_SCHEMA_SQL`).\n *\n * Kept beyond the intersection: tax's per-message `model`/`inputTokens`/\n * `outputTokens`, extended to the full usage receipt the harness actually\n * reports in `step-finish` parts (`tokens {input, output, reasoning,\n * cache{read, write}}` + `cost`) — see `./parts`.\n *\n * `threadExtraColumns`/`messageExtraColumns` merge product columns into the\n * table definitions (the `/missions` opaque-extras pattern: the store writes\n * `extras` values verbatim in the SAME insert statement and never reads,\n * validates, or defaults them).\n *\n * SERVER-side module (D1/libsql/better-sqlite3 behind a worker or server\n * route) — but free of `node:` builtins on purpose: D1 workers have none.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'\nimport type {\n AnySQLiteColumn,\n AnySQLiteTable,\n SQLiteColumnBuilderBase,\n SQLiteTableExtraConfigValue,\n} from 'drizzle-orm/sqlite-core'\nimport type { ChatMessagePart } from './parts'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type ChatParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\n/** Define options to customize chat thread and message table creation including workspace and naming prefixes */\nexport interface CreateChatTablesOptions<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n> {\n /** The product's workspace table — threads reference `workspaceTable.id`\n * with cascade. Omitted: `workspace_id` stays a plain indexed text column\n * (products whose tenant table lives in another database). */\n workspaceTable?: ChatParentTable\n /** Prefixes table AND index names (`'chat_'` → `chat_thread`,\n * `idx_chat_thread_workspace`) for products that namespace chat tables in a\n * shared database. Default: unprefixed `thread`/`message` (legal/gtm row\n * compatibility). */\n tablePrefix?: string\n /** Product columns merged into the thread table (the `/missions` extras\n * pattern) — e.g. a `status` lifecycle enum or gtm's scope columns. */\n threadExtraColumns?: TThreadExtras\n /** Product columns merged into the message table — e.g. legal's\n * `vault_files`. */\n messageExtraColumns?: TMessageExtras\n /**\n * Product indexes appended to the thread table's own, given the built\n * columns (extras included) so a product can index what it added.\n *\n * Names are used VERBATIM — `tablePrefix` is not applied — because these\n * have to byte-match indexes the product's migrations already created.\n *\n * Without this a product with one extra index cannot adopt the factory at\n * all: it keeps a hand-rolled duplicate of the same physical table, and the\n * two drift on the next factory change.\n */\n threadExtraIndexes?: ChatExtraIndexes\n /** Product indexes appended to the message table's own. Same rules as\n * `threadExtraIndexes`. */\n messageExtraIndexes?: ChatExtraIndexes\n}\n\n/**\n * Builds product indexes from a table's columns.\n *\n * The column map is passed untyped because its shape depends on the product's\n * own extras; the RETURN is drizzle's own extra-config type, so the spread\n * below stays inside the union `sqliteTable` expects and the built table keeps\n * its inferred column types. Returning `unknown[]` here would force a cast on\n * the config array, which widens every column to the cross-dialect union and\n * breaks `db.select()` at every call site.\n */\nexport type ChatExtraIndexes = (\n columns: Record<string, AnySQLiteColumn>,\n) => SQLiteTableExtraConfigValue[]\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\n/** Build chat-related SQLite tables with customizable thread and message columns */\nexport function createChatTables<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n>(options: CreateChatTablesOptions<TThreadExtras, TMessageExtras> = {}) {\n const { workspaceTable, tablePrefix = '' } = options\n const threadExtras = options.threadExtraColumns ?? ({} as TThreadExtras)\n const messageExtras = options.messageExtraColumns ?? ({} as TMessageExtras)\n const extraIndexes = (\n build: ChatExtraIndexes | undefined,\n table: Record<string, AnySQLiteColumn>,\n ): SQLiteTableExtraConfigValue[] => build?.(table) ?? []\n\n const threads = sqliteTable(`${tablePrefix}thread`, {\n id: hexId(),\n workspaceId: workspaceTable\n ? text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' })\n : text('workspace_id').notNull(),\n title: text('title').notNull(),\n category: text('category'),\n isPinned: integer('is_pinned', { mode: 'boolean' }).notNull().default(false),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n ...threadExtras,\n }, (table) => [\n index(`idx_${tablePrefix}thread_workspace`).on(table.workspaceId),\n // Supports the store's list ordering (updatedAt desc within a workspace).\n index(`idx_${tablePrefix}thread_workspace_updated`).on(table.workspaceId, table.updatedAt),\n ...extraIndexes(options.threadExtraIndexes, table as unknown as Record<string, AnySQLiteColumn>),\n ])\n\n const messages = sqliteTable(`${tablePrefix}message`, {\n id: hexId(),\n threadId: text('thread_id').notNull().references(() => threads.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['user', 'assistant', 'system', 'tool'] }).notNull(),\n content: text('content').notNull(),\n parts: text('parts', { mode: 'json' }).$type<ChatMessagePart[]>().default([]),\n toolName: text('tool_name'),\n model: text('model'),\n requestedModel: text('requested_model'),\n servedModel: text('served_model'),\n servedProvider: text('served_provider'),\n servedSource: text('served_model_source'),\n // Usage receipt, flattened from the harness's `step-finish` shape\n // (`tokens {input, output, reasoning, cache{read, write}}` + `cost`).\n inputTokens: integer('input_tokens'),\n outputTokens: integer('output_tokens'),\n reasoningTokens: integer('reasoning_tokens'),\n cacheReadTokens: integer('cache_read_tokens'),\n cacheWriteTokens: integer('cache_write_tokens'),\n costUsd: real('cost_usd'),\n createdAt: createdAt(),\n ...messageExtras,\n }, (table) => [\n index(`idx_${tablePrefix}message_thread`).on(table.threadId),\n index(`idx_${tablePrefix}message_thread_created`).on(table.threadId, table.createdAt),\n ...extraIndexes(options.messageExtraIndexes, table as unknown as Record<string, AnySQLiteColumn>),\n ])\n\n return { threads, messages }\n}\n\n/**\n * The base (no-extras) table pair, pinned via an instantiation expression:\n * `ReturnType<typeof createChatTables>` on the bare generic substitutes the\n * extras params with their CONSTRAINT (`Record<string,\n * SQLiteColumnBuilderBase>`), stamping an index signature into the column map\n * that widens every concrete column to `unknown`/`notNull: false` — concrete\n * factory results then fail `extends ChatTables`. (`teams`' `createTeamTables`\n * is non-generic, so its plain `ReturnType` never hits this.)\n */\nexport type ChatTables = ReturnType<typeof createChatTables<{}, {}>>\n\n/** Resolve the selected fields of a chat thread row from the chat threads table */\nexport type ChatThreadRow = ChatTables['threads']['$inferSelect']\n/** Resolve the selected structure of a chat message row from the messages table */\nexport type ChatMessageRow = ChatTables['messages']['$inferSelect']\n/** Resolve the type for inserting a new chat thread row into the threads table */\nexport type NewChatThreadRow = ChatTables['threads']['$inferInsert']\n/** Resolve the type for inserting a new chat message row into the messages table */\nexport type NewChatMessageRow = ChatTables['messages']['$inferInsert']\n","/**\n * Typed CRUD over the tables from `createChatTables`. Works against any\n * SQLite drizzle driver (D1, libsql, better-sqlite3) — builders are awaited,\n * never `.run()`/`.all()`, so sync and async drivers behave identically.\n *\n * Access control is an injected seam, never an import: single-thread routes\n * check workspace access themselves (they know the thread), while\n * `bulkDeleteThreads` REQUIRES an `assertAccess` callback because one request\n * can span workspaces — it is called once per distinct workspace and any\n * throw rejects the whole request before a single delete runs (fail-closed;\n * legal's bulk-delete semantics).\n *\n * Deletes run messages-first in ONE `db.batch` round trip when the driver has\n * one (D1, libsql), so a partial failure never leaves orphaned rows behind a\n * deleted thread; drivers without `batch` (better-sqlite3) fall back to\n * sequential awaits in the same order.\n */\n\nimport { and, asc, desc, eq, gte, inArray, lt, sql, type SQL } from 'drizzle-orm'\nimport type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'\nimport {\n runSqliteStatements,\n type SqliteBatchDatabase,\n} from '../store'\nimport { BULK_DELETE_MAX_THREADS, ChatStoreInputError, threadTitleFromMessage } from './core'\nimport type { ChatMessagePart } from './parts'\nimport type { ChatMessageRow, ChatTables, ChatThreadRow, NewChatMessageRow, NewChatThreadRow } from './schema'\n\n/** Any SQLite drizzle database — `any` erases the driver-specific run-result\n * and schema generics so better-sqlite3, D1, and libsql handles all fit.\n * `batch` is structural: present on D1/libsql drizzle instances. */\nexport type ChatDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any> &\n SqliteBatchDatabase\n\n/** Product-injected access check. Throw to deny; the store never interprets\n * users or roles itself. */\nexport type WorkspaceAccessCheck = (workspaceId: string) => void | Promise<void>\n\n/** Define input parameters for listing threads within a workspace with pagination options */\nexport interface ListThreadsInput {\n workspaceId: string\n /** Clamped to 1..200; default 50 (legal's list route semantics). */\n limit?: number\n /** Clamped to >= 0; default 0. */\n offset?: number\n}\n\n/** Represent a paginated collection of chat threads with total count and pagination details */\nexport interface ListThreadsResult<TThread = ChatThreadRow> {\n threads: TThread[]\n total: number\n limit: number\n offset: number\n}\n\n/** Define input parameters required to create a new thread in a workspace */\nexport interface CreateThreadInput {\n /** Caller-assigned primary key. The agent gateway uses its validated\n * conversation id so API and browser turns share one durable thread. */\n id?: string\n workspaceId: string\n /** Title source when `title` is absent: first non-empty line, 80-char cap\n * (`threadTitleFromMessage`). */\n firstMessage?: string\n /** Explicit title; still normalized through `threadTitleFromMessage` so a\n * multi-page paste never becomes a sidebar entry. */\n title?: string\n category?: string | null\n isPinned?: boolean\n /** Opaque product-column values written verbatim in the SAME insert (the\n * `/missions` extras pattern). Never read, validated, or defaulted here. */\n extras?: Record<string, unknown>\n}\n\n/** Define input parameters for appending a message to a chat thread with optional metadata */\nexport interface AppendMessageInput {\n /** Caller-assigned primary key. Omitted, the column default assigns a random\n * hex id (today's behavior). Incremental assistant persistence passes a\n * DETERMINISTIC id derived from the turn's own identity, so a re-entered\n * turn (crashed worker, durable-driver retry) finds and updates the row a\n * previous attempt started instead of inserting a second one. */\n id?: string\n threadId: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[]\n toolName?: string | null\n model?: string | null\n requestedModel?: string | null\n servedModel?: string | null\n servedProvider?: string | null\n servedSource?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n /** Opaque product-column values written verbatim in the SAME insert. */\n extras?: Record<string, unknown>\n}\n\n/** Fields an existing message row may be patched with. Every field is\n * optional and only DEFINED fields are written, so a partial patch never\n * clears a column it does not mention. `threadId` and `role` are absent on\n * purpose: a message never moves thread or changes speaker.\n *\n * Exists for incremental assistant persistence — the streaming turn writes\n * the row once and then patches it as content accumulates, so the durable\n * transcript is at most one cadence interval behind the live stream. */\nexport interface UpdateMessageInput {\n content?: string\n parts?: ChatMessagePart[]\n toolName?: string | null\n model?: string | null\n requestedModel?: string | null\n servedModel?: string | null\n servedProvider?: string | null\n servedSource?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n /** Opaque product-column values written verbatim in the SAME update. */\n extras?: Record<string, unknown>\n}\n\n/** Define options to configure message listing with optional limit and offset parameters */\nexport interface ListMessagesOptions {\n limit?: number\n offset?: number\n}\n\n/** Define input for bulk deleting threads with access checks per workspace */\nexport interface BulkDeleteThreadsInput {\n ids: string[]\n /** Optional single-workspace fence for products whose route already resolved\n * one active workspace. Omitted preserves the multi-workspace legal route. */\n workspaceId?: string\n /** Called once per distinct workspace the ids touch, before ANY delete. */\n assertAccess: WorkspaceAccessCheck\n}\n\n/** Delete threads selected by their last activity time within one workspace.\n *\n * Products can add a lifecycle predicate such as `status = 'active'` through\n * `where`; the store still owns workspace scope, access ordering, and message\n * cleanup for every consumer. */\nexport interface BulkDeleteThreadsByUpdatedAtInput {\n workspaceId: string\n /** Exclusive upper bound: rows older than this instant. */\n updatedBefore?: Date\n /** Inclusive lower bound: rows updated at or after this instant. */\n updatedAfter?: Date\n where?: SQL\n /** Called before any delete for the requested workspace. */\n assertAccess: WorkspaceAccessCheck\n}\n\n/** Manage chat threads and messages with operations for listing, creating, updating, and deleting data */\nexport interface ChatStore<TThread = ChatThreadRow, TMessage = ChatMessageRow> {\n listThreads(input: ListThreadsInput): Promise<ListThreadsResult<TThread>>\n getThread(threadId: string): Promise<TThread | null>\n createThread(input: CreateThreadInput): Promise<TThread>\n renameThread(threadId: string, title: string): Promise<TThread | null>\n pinThread(threadId: string, isPinned: boolean): Promise<TThread | null>\n /** Messages + thread in one batch. Resolves false when the thread does not\n * exist. `assertAccess` (optional) receives the thread's workspaceId before\n * the delete — single-thread callers usually check access themselves. */\n deleteThread(threadId: string, options?: { assertAccess?: WorkspaceAccessCheck }): Promise<boolean>\n bulkDeleteThreads(input: BulkDeleteThreadsInput): Promise<{ deleted: number }>\n bulkDeleteThreadsByUpdatedAt(input: BulkDeleteThreadsByUpdatedAtInput): Promise<{ deleted: number }>\n /** Ordered oldest-first: `created_at`, then rowid (insertion order within a\n * same-second burst — a user+assistant pair lands in one epoch second). */\n listMessages(threadId: string, options?: ListMessagesOptions): Promise<TMessage[]>\n /** Inserts the message and bumps the thread's `updatedAt` in one batch so\n * workspace recency sorts stay truthful. */\n appendMessage(input: AppendMessageInput): Promise<TMessage>\n /** Patches an existing message and bumps its thread's `updatedAt` in the\n * same batch. Resolves `null` when the id does not exist. Only defined\n * patch fields are written. */\n updateMessage(id: string, patch: UpdateMessageInput): Promise<TMessage | null>\n /** Removes one message. Resolves false when the id does not exist. Used by\n * incremental persistence to retract a draft row for a turn that ended\n * producing nothing, so an empty assistant row is never left behind. */\n deleteMessage(id: string): Promise<boolean>\n}\n\nfunction clampLimit(limit: number | undefined, fallback: number, max: number): number {\n const value = Number.isFinite(limit) ? Math.trunc(limit as number) : fallback\n return Math.min(Math.max(value, 1), max)\n}\n\nfunction clampOffset(offset: number | undefined): number {\n const value = Number.isFinite(offset) ? Math.trunc(offset as number) : 0\n return Math.max(value, 0)\n}\n\n/** Create a chat store managing threads and messages based on the provided database and tables */\nexport function createChatStore<TTables extends ChatTables>(\n db: ChatDatabase,\n tables: TTables,\n): ChatStore<TTables['threads']['$inferSelect'], TTables['messages']['$inferSelect']> {\n type TThread = TTables['threads']['$inferSelect']\n type TMessage = TTables['messages']['$inferSelect']\n const threads = tables.threads as ChatTables['threads']\n const messages = tables.messages as ChatTables['messages']\n\n return {\n async listThreads(input) {\n const limit = clampLimit(input.limit, 50, 200)\n const offset = clampOffset(input.offset)\n const scope = eq(threads.workspaceId, input.workspaceId)\n const [list, [countRow]] = await Promise.all([\n db.select().from(threads).where(scope)\n // `id` tiebreak keeps pagination stable across same-second updates.\n .orderBy(desc(threads.updatedAt), asc(threads.id))\n .limit(limit)\n .offset(offset),\n db.select({ total: sql<number>`count(*)` }).from(threads).where(scope),\n ])\n return { threads: list as TThread[], total: countRow?.total ?? 0, limit, offset }\n },\n\n async getThread(threadId) {\n const [row] = await db.select().from(threads).where(eq(threads.id, threadId)).limit(1)\n return (row as TThread | undefined) ?? null\n },\n\n async createThread(input) {\n const title = threadTitleFromMessage(input.title ?? input.firstMessage ?? '')\n const values = {\n ...(input.id !== undefined ? { id: input.id } : {}),\n workspaceId: input.workspaceId,\n title,\n ...(input.category !== undefined ? { category: input.category } : {}),\n ...(input.isPinned !== undefined ? { isPinned: input.isPinned } : {}),\n ...(input.extras ?? {}),\n } as NewChatThreadRow\n const [row] = await db.insert(threads).values(values).returning()\n if (!row) throw new Error('thread insert returned no row')\n return row as TThread\n },\n\n async renameThread(threadId, title) {\n const trimmed = title.trim()\n if (!trimmed) throw new ChatStoreInputError('Missing title')\n const [row] = await db.update(threads)\n .set({ title: trimmed, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async pinThread(threadId, isPinned) {\n const [row] = await db.update(threads)\n .set({ isPinned, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async deleteThread(threadId, options) {\n const [existing] = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(eq(threads.id, threadId))\n .limit(1)\n if (!existing) return false\n if (options?.assertAccess) await options.assertAccess(existing.workspaceId)\n // Messages first so a partial failure never leaves orphaned rows behind\n // a deleted thread.\n await runSqliteStatements(db, [\n db.delete(messages).where(eq(messages.threadId, threadId)),\n db.delete(threads).where(eq(threads.id, threadId)),\n ])\n return true\n },\n\n async bulkDeleteThreads(input) {\n const { ids, workspaceId, assertAccess } = input\n if (typeof assertAccess !== 'function') throw new ChatStoreInputError('Missing assertAccess')\n if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === 'string' && id.length > 0)) {\n throw new ChatStoreInputError('Missing ids')\n }\n if (ids.length > BULK_DELETE_MAX_THREADS) {\n throw new ChatStoreInputError(`Too many ids (max ${BULK_DELETE_MAX_THREADS})`)\n }\n if (workspaceId !== undefined && !workspaceId.trim()) {\n throw new ChatStoreInputError('Invalid workspaceId')\n }\n\n const scope = workspaceId\n ? and(inArray(threads.id, ids), eq(threads.workspaceId, workspaceId))\n : inArray(threads.id, ids)\n const rows = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(scope)\n if (rows.length === 0) return { deleted: 0 }\n\n // Access is verified once per workspace the ids touch. Fail-closed: one\n // inaccessible workspace rejects the whole request before any delete.\n // Sorted so the check order (and therefore which denial surfaces) is\n // deterministic — row order follows random hex ids and varies per run.\n const workspaceIds = [...new Set(rows.map((row) => row.workspaceId))].sort()\n for (const workspaceId of workspaceIds) {\n await assertAccess(workspaceId)\n }\n\n const foundIds = rows.map((row) => row.id)\n await runSqliteStatements(db, [\n db.delete(messages).where(inArray(messages.threadId, foundIds)),\n db.delete(threads).where(inArray(threads.id, foundIds)),\n ])\n return { deleted: foundIds.length }\n },\n\n async bulkDeleteThreadsByUpdatedAt(input) {\n const { workspaceId, updatedBefore, updatedAfter, where, assertAccess } = input\n if (typeof assertAccess !== 'function') throw new ChatStoreInputError('Missing assertAccess')\n if (!workspaceId.trim()) throw new ChatStoreInputError('Missing workspaceId')\n if (updatedBefore === undefined && updatedAfter === undefined) {\n throw new ChatStoreInputError('Missing updatedAt boundary')\n }\n for (const boundary of [updatedBefore, updatedAfter]) {\n if (boundary !== undefined && Number.isNaN(boundary.getTime())) {\n throw new ChatStoreInputError('Invalid updatedAt boundary')\n }\n }\n\n const conditions = [eq(threads.workspaceId, workspaceId)]\n if (updatedBefore) conditions.push(lt(threads.updatedAt, updatedBefore))\n if (updatedAfter) conditions.push(gte(threads.updatedAt, updatedAfter))\n if (where) conditions.push(where)\n const scope = and(...conditions)\n const [countRow] = await db.select({ total: sql<number>`count(*)` })\n .from(threads)\n .where(scope)\n const deleted = Number(countRow?.total ?? 0)\n if (deleted === 0) return { deleted: 0 }\n\n await assertAccess(workspaceId)\n // Keep the age path unbounded without constructing a giant host-parameter\n // list; the subquery lets SQLite/D1 stream the matching thread ids.\n const matchingThreads = db.select({ id: threads.id }).from(threads).where(scope)\n await runSqliteStatements(db, [\n db.delete(messages).where(inArray(messages.threadId, matchingThreads)),\n db.delete(threads).where(scope),\n ])\n return { deleted }\n },\n\n async listMessages(threadId, options) {\n const query = db.select().from(messages)\n .where(eq(messages.threadId, threadId))\n .orderBy(asc(messages.createdAt), sql`rowid`)\n .$dynamic()\n if (options?.limit !== undefined) query.limit(clampLimit(options.limit, 1, 1000))\n if (options?.offset !== undefined) query.offset(clampOffset(options.offset))\n return await query as TMessage[]\n },\n\n async appendMessage(input) {\n const values = {\n ...(input.id !== undefined ? { id: input.id } : {}),\n threadId: input.threadId,\n role: input.role,\n content: input.content,\n ...(input.parts !== undefined ? { parts: input.parts } : {}),\n ...(input.toolName !== undefined ? { toolName: input.toolName } : {}),\n ...(input.model !== undefined ? { model: input.model } : {}),\n ...(input.requestedModel !== undefined ? { requestedModel: input.requestedModel } : {}),\n ...(input.servedModel !== undefined ? { servedModel: input.servedModel } : {}),\n ...(input.servedProvider !== undefined ? { servedProvider: input.servedProvider } : {}),\n ...(input.servedSource !== undefined ? { servedSource: input.servedSource } : {}),\n ...(input.inputTokens !== undefined ? { inputTokens: input.inputTokens } : {}),\n ...(input.outputTokens !== undefined ? { outputTokens: input.outputTokens } : {}),\n ...(input.reasoningTokens !== undefined ? { reasoningTokens: input.reasoningTokens } : {}),\n ...(input.cacheReadTokens !== undefined ? { cacheReadTokens: input.cacheReadTokens } : {}),\n ...(input.cacheWriteTokens !== undefined ? { cacheWriteTokens: input.cacheWriteTokens } : {}),\n ...(input.costUsd !== undefined ? { costUsd: input.costUsd } : {}),\n ...(input.extras ?? {}),\n } as NewChatMessageRow\n const [insertResult] = await runSqliteStatements(db, [\n db.insert(messages).values(values).returning(),\n db.update(threads).set({ updatedAt: new Date() }).where(eq(threads.id, input.threadId)),\n ])\n const row = (insertResult as TMessage[] | undefined)?.[0]\n if (!row) throw new Error('message insert returned no row')\n return row\n },\n\n async updateMessage(id, patch) {\n const values = {\n ...(patch.content !== undefined ? { content: patch.content } : {}),\n ...(patch.parts !== undefined ? { parts: patch.parts } : {}),\n ...(patch.toolName !== undefined ? { toolName: patch.toolName } : {}),\n ...(patch.model !== undefined ? { model: patch.model } : {}),\n ...(patch.requestedModel !== undefined ? { requestedModel: patch.requestedModel } : {}),\n ...(patch.servedModel !== undefined ? { servedModel: patch.servedModel } : {}),\n ...(patch.servedProvider !== undefined ? { servedProvider: patch.servedProvider } : {}),\n ...(patch.servedSource !== undefined ? { servedSource: patch.servedSource } : {}),\n ...(patch.inputTokens !== undefined ? { inputTokens: patch.inputTokens } : {}),\n ...(patch.outputTokens !== undefined ? { outputTokens: patch.outputTokens } : {}),\n ...(patch.reasoningTokens !== undefined ? { reasoningTokens: patch.reasoningTokens } : {}),\n ...(patch.cacheReadTokens !== undefined ? { cacheReadTokens: patch.cacheReadTokens } : {}),\n ...(patch.cacheWriteTokens !== undefined ? { cacheWriteTokens: patch.cacheWriteTokens } : {}),\n ...(patch.costUsd !== undefined ? { costUsd: patch.costUsd } : {}),\n ...(patch.extras ?? {}),\n }\n if (Object.keys(values).length === 0) {\n const [current] = await db.select().from(messages).where(eq(messages.id, id))\n return (current as TMessage | undefined) ?? null\n }\n // The thread bump reads the row's own `thread_id` rather than trusting a\n // caller-supplied one: a message never moves thread, so the subquery is\n // the authoritative source and one fewer parameter to get wrong.\n const [updateResult] = await runSqliteStatements(db, [\n db.update(messages).set(values).where(eq(messages.id, id)).returning(),\n db.update(threads).set({ updatedAt: new Date() })\n .where(eq(threads.id, sql`(select ${messages.threadId} from ${messages} where ${messages.id} = ${id})`)),\n ])\n return (updateResult as TMessage[] | undefined)?.[0] ?? null\n },\n\n async deleteMessage(id) {\n const deleted = await db.delete(messages).where(eq(messages.id, id)).returning()\n return (deleted as unknown[]).length > 0\n },\n }\n}\n"],"mappings":"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","names":["sql","sql","workspaceId"]}
1
+ {"version":3,"sources":["../../src/chat-store/core.ts","../../src/chat-store/schema.ts","../../src/chat-store/store.ts"],"sourcesContent":["/**\n * Pure (drizzle-free) pieces of the chat store: thread-title derivation, the\n * bulk-delete bound, and the typed input error. Split from `./schema`/`./store`\n * so the root barrel can re-export them without dragging the optional\n * drizzle-orm peer into every root-entry consumer.\n */\n\n/** Bounds a single bulk-delete request's write set; product surfaces cap\n * thread lists at far fewer, so a larger batch is a malformed or hostile\n * request. (Lifted from legal's api.threads.bulk-delete route.) */\nexport const BULK_DELETE_MAX_THREADS = 200\n\n/** Invalid caller input (missing/oversized ids, empty title). Products map it\n * to a 400; anything else out of the store is a real failure. */\nexport class ChatStoreInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ChatStoreInputError'\n }\n}\n\n/** Thread titles come from the first message — keep the list scannable by\n * storing only its first non-empty line, capped at 80 chars, never the whole\n * multi-page prompt. (Lifted verbatim from legal's chat.new route.) */\nexport function threadTitleFromMessage(message: string): string {\n const firstLine = message.split('\\n').find((l) => l.trim().length > 0)?.trim() ?? ''\n if (!firstLine) return 'New Thread'\n return firstLine.length > 80 ? `${firstLine.slice(0, 79)}…` : firstLine\n}\n","/**\n * Drizzle schema factory for the chat thread/message tables — the same\n * injection pattern as `createTeamTables`: the product owns the workspace\n * table; the factory wires the thread FK into it so the whole graph lives in\n * one drizzle schema with real cascade semantics. Column names, types,\n * defaults, enums, and indexes mirror legal's and gtm's hand-rolled `thread`/\n * `message` tables so a product with those tables adopts the factory without\n * rewriting rows; `tablePrefix` covers products that namespace (tax's\n * `chat_messages` style).\n *\n * The core is the superset the three products agree on. Divergences dropped,\n * and why:\n * - `thread.status` ('active'|'archived', legal+gtm) — archive semantics\n * diverge (tax uses `archivedAt`); product-domain lifecycle → extra column.\n * - `thread.scopeKind`/`scopeKey`/`harness` (gtm) — artifact anchoring and\n * harness pinning are product-domain → extra columns.\n * - tax's `tax_sessions` session columns (`taxYear`, `projectRef`,\n * `agentSessionId`, `agentRuntime`, `agentHarness`, `profile`, `error`,\n * `userId`) — sandbox-session state, not chat state → extra columns.\n * - `message.toolInput`/`toolOutput` (legal+gtm) — duplicate of the tool\n * part's `state.input`/`state.output` inside `parts` (the shape `/stream`'s\n * `normalizePersistedPart` owns); keeping both invites drift.\n * - `message.vaultFiles` (legal+gtm) — vault is product-domain → extra column.\n * - tax's re-declared `turn_events`/`turn_status` DDL — deliberately NOT here;\n * `/stream`'s turn-buffer owns that DDL (`TURN_BUFFER_D1_SCHEMA_SQL`).\n *\n * Kept beyond the intersection: tax's per-message `model`/`inputTokens`/\n * `outputTokens`, extended to the full usage receipt the harness actually\n * reports in `step-finish` parts (`tokens {input, output, reasoning,\n * cache{read, write}}` + `cost`) — see `./parts`.\n *\n * `threadExtraColumns`/`messageExtraColumns` merge product columns into the\n * table definitions (the `/missions` opaque-extras pattern: the store writes\n * `extras` values verbatim in the SAME insert statement and never reads,\n * validates, or defaults them).\n *\n * SERVER-side module (D1/libsql/better-sqlite3 behind a worker or server\n * route) — but free of `node:` builtins on purpose: D1 workers have none.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'\nimport type {\n AnySQLiteColumn,\n AnySQLiteTable,\n SQLiteColumnBuilderBase,\n SQLiteTableExtraConfigValue,\n} from 'drizzle-orm/sqlite-core'\nimport type { ChatMessagePart } from './parts'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type ChatParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\n/** Define options to customize chat thread and message table creation including workspace and naming prefixes */\nexport interface CreateChatTablesOptions<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n> {\n /** The product's workspace table — threads reference `workspaceTable.id`\n * with cascade. Omitted: `workspace_id` stays a plain indexed text column\n * (products whose tenant table lives in another database). */\n workspaceTable?: ChatParentTable\n /** Prefixes table AND index names (`'chat_'` → `chat_thread`,\n * `idx_chat_thread_workspace`) for products that namespace chat tables in a\n * shared database. Default: unprefixed `thread`/`message` (legal/gtm row\n * compatibility). */\n tablePrefix?: string\n /** Product columns merged into the thread table (the `/missions` extras\n * pattern) — e.g. a `status` lifecycle enum or gtm's scope columns. */\n threadExtraColumns?: TThreadExtras\n /** Product columns merged into the message table — e.g. legal's\n * `vault_files`. */\n messageExtraColumns?: TMessageExtras\n /**\n * Product indexes appended to the thread table's own, given the built\n * columns (extras included) so a product can index what it added.\n *\n * Names are used VERBATIM — `tablePrefix` is not applied — because these\n * have to byte-match indexes the product's migrations already created.\n *\n * Without this a product with one extra index cannot adopt the factory at\n * all: it keeps a hand-rolled duplicate of the same physical table, and the\n * two drift on the next factory change.\n */\n threadExtraIndexes?: ChatExtraIndexes\n /** Product indexes appended to the message table's own. Same rules as\n * `threadExtraIndexes`. */\n messageExtraIndexes?: ChatExtraIndexes\n}\n\n/**\n * Builds product indexes from a table's columns.\n *\n * The column map is passed untyped because its shape depends on the product's\n * own extras; the RETURN is drizzle's own extra-config type, so the spread\n * below stays inside the union `sqliteTable` expects and the built table keeps\n * its inferred column types. Returning `unknown[]` here would force a cast on\n * the config array, which widens every column to the cross-dialect union and\n * breaks `db.select()` at every call site.\n */\nexport type ChatExtraIndexes = (\n columns: Record<string, AnySQLiteColumn>,\n) => SQLiteTableExtraConfigValue[]\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\n/** Build chat-related SQLite tables with customizable thread and message columns */\nexport function createChatTables<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n>(options: CreateChatTablesOptions<TThreadExtras, TMessageExtras> = {}) {\n const { workspaceTable, tablePrefix = '' } = options\n const threadExtras = options.threadExtraColumns ?? ({} as TThreadExtras)\n const messageExtras = options.messageExtraColumns ?? ({} as TMessageExtras)\n const extraIndexes = (\n build: ChatExtraIndexes | undefined,\n table: Record<string, AnySQLiteColumn>,\n ): SQLiteTableExtraConfigValue[] => build?.(table) ?? []\n\n const threads = sqliteTable(`${tablePrefix}thread`, {\n id: hexId(),\n workspaceId: workspaceTable\n ? text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' })\n : text('workspace_id').notNull(),\n title: text('title').notNull(),\n category: text('category'),\n isPinned: integer('is_pinned', { mode: 'boolean' }).notNull().default(false),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n ...threadExtras,\n }, (table) => [\n index(`idx_${tablePrefix}thread_workspace`).on(table.workspaceId),\n // Supports the store's list ordering (updatedAt desc within a workspace).\n index(`idx_${tablePrefix}thread_workspace_updated`).on(table.workspaceId, table.updatedAt),\n ...extraIndexes(options.threadExtraIndexes, table as unknown as Record<string, AnySQLiteColumn>),\n ])\n\n const messages = sqliteTable(`${tablePrefix}message`, {\n id: hexId(),\n threadId: text('thread_id').notNull().references(() => threads.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['user', 'assistant', 'system', 'tool'] }).notNull(),\n content: text('content').notNull(),\n parts: text('parts', { mode: 'json' }).$type<ChatMessagePart[]>().default([]),\n toolName: text('tool_name'),\n model: text('model'),\n requestedModel: text('requested_model'),\n servedModel: text('served_model'),\n servedProvider: text('served_provider'),\n servedSource: text('served_model_source'),\n // Usage receipt, flattened from the harness's `step-finish` shape\n // (`tokens {input, output, reasoning, cache{read, write}}` + `cost`).\n inputTokens: integer('input_tokens'),\n outputTokens: integer('output_tokens'),\n reasoningTokens: integer('reasoning_tokens'),\n cacheReadTokens: integer('cache_read_tokens'),\n cacheWriteTokens: integer('cache_write_tokens'),\n costUsd: real('cost_usd'),\n createdAt: createdAt(),\n ...messageExtras,\n }, (table) => [\n index(`idx_${tablePrefix}message_thread`).on(table.threadId),\n index(`idx_${tablePrefix}message_thread_created`).on(table.threadId, table.createdAt),\n ...extraIndexes(options.messageExtraIndexes, table as unknown as Record<string, AnySQLiteColumn>),\n ])\n\n return { threads, messages }\n}\n\n/**\n * The base (no-extras) table pair, pinned via an instantiation expression:\n * `ReturnType<typeof createChatTables>` on the bare generic substitutes the\n * extras params with their CONSTRAINT (`Record<string,\n * SQLiteColumnBuilderBase>`), stamping an index signature into the column map\n * that widens every concrete column to `unknown`/`notNull: false` — concrete\n * factory results then fail `extends ChatTables`. (`teams`' `createTeamTables`\n * is non-generic, so its plain `ReturnType` never hits this.)\n */\nexport type ChatTables = ReturnType<typeof createChatTables<{}, {}>>\n\n/** Resolve the selected fields of a chat thread row from the chat threads table */\nexport type ChatThreadRow = ChatTables['threads']['$inferSelect']\n/** Resolve the selected structure of a chat message row from the messages table */\nexport type ChatMessageRow = ChatTables['messages']['$inferSelect']\n/** Resolve the type for inserting a new chat thread row into the threads table */\nexport type NewChatThreadRow = ChatTables['threads']['$inferInsert']\n/** Resolve the type for inserting a new chat message row into the messages table */\nexport type NewChatMessageRow = ChatTables['messages']['$inferInsert']\n","/**\n * Typed CRUD over the tables from `createChatTables`. Works against any\n * SQLite drizzle driver (D1, libsql, better-sqlite3) — builders are awaited,\n * never `.run()`/`.all()`, so sync and async drivers behave identically.\n *\n * Access control is an injected seam, never an import: single-thread routes\n * check workspace access themselves (they know the thread), while\n * `bulkDeleteThreads` REQUIRES an `assertAccess` callback because one request\n * can span workspaces — it is called once per distinct workspace and any\n * throw rejects the whole request before a single delete runs (fail-closed;\n * legal's bulk-delete semantics).\n *\n * Deletes run messages-first in ONE `db.batch` round trip when the driver has\n * one (D1, libsql), so a partial failure never leaves orphaned rows behind a\n * deleted thread; drivers without `batch` (better-sqlite3) fall back to\n * sequential awaits in the same order.\n */\n\nimport { and, asc, desc, eq, gte, inArray, lt, sql, type SQL } from 'drizzle-orm'\nimport type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'\nimport {\n runSqliteStatements,\n type SqliteBatchDatabase,\n} from '../store'\nimport { BULK_DELETE_MAX_THREADS, ChatStoreInputError, threadTitleFromMessage } from './core'\nimport type { ChatMessagePart } from './parts'\nimport type { ChatMessageRow, ChatTables, ChatThreadRow, NewChatMessageRow, NewChatThreadRow } from './schema'\n\n/** Any SQLite drizzle database — `any` erases the driver-specific run-result\n * and schema generics so better-sqlite3, D1, and libsql handles all fit.\n * `batch` is structural: present on D1/libsql drizzle instances. */\nexport type ChatDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any> &\n SqliteBatchDatabase\n\n/** Product-injected access check. Throw to deny; the store never interprets\n * users or roles itself. */\nexport type WorkspaceAccessCheck = (workspaceId: string) => void | Promise<void>\n\n/** Define input parameters for listing threads within a workspace with pagination options */\nexport interface ListThreadsInput {\n workspaceId: string\n /** Clamped to 1..200; default 50 (legal's list route semantics). */\n limit?: number\n /** Clamped to >= 0; default 0. */\n offset?: number\n}\n\n/** Represent a paginated collection of chat threads with total count and pagination details */\nexport interface ListThreadsResult<TThread = ChatThreadRow> {\n threads: TThread[]\n total: number\n limit: number\n offset: number\n}\n\n/** Define input parameters required to create a new thread in a workspace */\nexport interface CreateThreadInput {\n /** Caller-assigned primary key. The agent gateway uses its validated\n * conversation id so API and browser turns share one durable thread. */\n id?: string\n workspaceId: string\n /** Title source when `title` is absent: first non-empty line, 80-char cap\n * (`threadTitleFromMessage`). */\n firstMessage?: string\n /** Explicit title; still normalized through `threadTitleFromMessage` so a\n * multi-page paste never becomes a sidebar entry. */\n title?: string\n category?: string | null\n isPinned?: boolean\n /** Opaque product-column values written verbatim in the SAME insert (the\n * `/missions` extras pattern). Never read, validated, or defaulted here. */\n extras?: Record<string, unknown>\n}\n\n/** Define input parameters for appending a message to a chat thread with optional metadata */\nexport interface AppendMessageInput {\n /** Caller-assigned primary key. Omitted, the column default assigns a random\n * hex id (today's behavior). Incremental assistant persistence passes a\n * DETERMINISTIC id derived from the turn's own identity, so a re-entered\n * turn (crashed worker, durable-driver retry) finds and updates the row a\n * previous attempt started instead of inserting a second one. */\n id?: string\n threadId: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[]\n toolName?: string | null\n model?: string | null\n requestedModel?: string | null\n servedModel?: string | null\n servedProvider?: string | null\n servedSource?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n /** Opaque product-column values written verbatim in the SAME insert. */\n extras?: Record<string, unknown>\n}\n\n/** Fields an existing message row may be patched with. Every field is\n * optional and only DEFINED fields are written, so a partial patch never\n * clears a column it does not mention. `threadId` and `role` are absent on\n * purpose: a message never moves thread or changes speaker.\n *\n * Exists for incremental assistant persistence — the streaming turn writes\n * the row once and then patches it as content accumulates, so the durable\n * transcript is at most one cadence interval behind the live stream. */\nexport interface UpdateMessageInput {\n content?: string\n parts?: ChatMessagePart[]\n toolName?: string | null\n model?: string | null\n requestedModel?: string | null\n servedModel?: string | null\n servedProvider?: string | null\n servedSource?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n /** Opaque product-column values written verbatim in the SAME update. */\n extras?: Record<string, unknown>\n}\n\n/** Define options to configure message listing with optional limit and offset parameters */\nexport interface ListMessagesOptions {\n limit?: number\n offset?: number\n}\n\n/** Define input for bulk deleting threads with access checks per workspace */\nexport interface BulkDeleteThreadsInput {\n ids: string[]\n /** Optional single-workspace fence for products whose route already resolved\n * one active workspace. Omitted preserves the multi-workspace legal route. */\n workspaceId?: string\n /** Called once per distinct workspace the ids touch, before ANY delete. */\n assertAccess: WorkspaceAccessCheck\n}\n\n/** Delete threads selected by their last activity time within one workspace.\n *\n * Products can add a lifecycle predicate such as `status = 'active'` through\n * `where`; the store still owns workspace scope, access ordering, and message\n * cleanup for every consumer. */\nexport interface BulkDeleteThreadsByUpdatedAtInput {\n workspaceId: string\n /** Exclusive upper bound: rows older than this instant. */\n updatedBefore?: Date\n /** Inclusive lower bound: rows updated at or after this instant. */\n updatedAfter?: Date\n where?: SQL\n /** Called before any delete for the requested workspace. */\n assertAccess: WorkspaceAccessCheck\n}\n\n/** Manage chat threads and messages with operations for listing, creating, updating, and deleting data */\nexport interface ChatStore<TThread = ChatThreadRow, TMessage = ChatMessageRow> {\n listThreads(input: ListThreadsInput): Promise<ListThreadsResult<TThread>>\n getThread(threadId: string): Promise<TThread | null>\n createThread(input: CreateThreadInput): Promise<TThread>\n renameThread(threadId: string, title: string): Promise<TThread | null>\n pinThread(threadId: string, isPinned: boolean): Promise<TThread | null>\n /** Messages + thread in one batch. Resolves false when the thread does not\n * exist. `assertAccess` (optional) receives the thread's workspaceId before\n * the delete — single-thread callers usually check access themselves. */\n deleteThread(threadId: string, options?: { assertAccess?: WorkspaceAccessCheck }): Promise<boolean>\n bulkDeleteThreads(input: BulkDeleteThreadsInput): Promise<{ deleted: number }>\n bulkDeleteThreadsByUpdatedAt(input: BulkDeleteThreadsByUpdatedAtInput): Promise<{ deleted: number }>\n /** Ordered oldest-first: `created_at`, then rowid (insertion order within a\n * same-second burst — a user+assistant pair lands in one epoch second). */\n listMessages(threadId: string, options?: ListMessagesOptions): Promise<TMessage[]>\n /** Inserts the message and bumps the thread's `updatedAt` in one batch so\n * workspace recency sorts stay truthful. */\n appendMessage(input: AppendMessageInput): Promise<TMessage>\n /** Patches an existing message and bumps its thread's `updatedAt` in the\n * same batch. Resolves `null` when the id does not exist. Only defined\n * patch fields are written. */\n updateMessage(id: string, patch: UpdateMessageInput): Promise<TMessage | null>\n /** Removes one message. Resolves false when the id does not exist. Used by\n * incremental persistence to retract a draft row for a turn that ended\n * producing nothing, so an empty assistant row is never left behind. */\n deleteMessage(id: string): Promise<boolean>\n}\n\nfunction clampLimit(limit: number | undefined, fallback: number, max: number): number {\n const value = Number.isFinite(limit) ? Math.trunc(limit as number) : fallback\n return Math.min(Math.max(value, 1), max)\n}\n\nfunction clampOffset(offset: number | undefined): number {\n const value = Number.isFinite(offset) ? Math.trunc(offset as number) : 0\n return Math.max(value, 0)\n}\n\n/** Create a chat store managing threads and messages based on the provided database and tables */\nexport function createChatStore<TTables extends ChatTables>(\n db: ChatDatabase,\n tables: TTables,\n): ChatStore<TTables['threads']['$inferSelect'], TTables['messages']['$inferSelect']> {\n type TThread = TTables['threads']['$inferSelect']\n type TMessage = TTables['messages']['$inferSelect']\n const threads = tables.threads as ChatTables['threads']\n const messages = tables.messages as ChatTables['messages']\n\n return {\n async listThreads(input) {\n const limit = clampLimit(input.limit, 50, 200)\n const offset = clampOffset(input.offset)\n const scope = eq(threads.workspaceId, input.workspaceId)\n const [list, [countRow]] = await Promise.all([\n db.select().from(threads).where(scope)\n // `id` tiebreak keeps pagination stable across same-second updates.\n .orderBy(desc(threads.updatedAt), asc(threads.id))\n .limit(limit)\n .offset(offset),\n db.select({ total: sql<number>`count(*)` }).from(threads).where(scope),\n ])\n return { threads: list as TThread[], total: countRow?.total ?? 0, limit, offset }\n },\n\n async getThread(threadId) {\n const [row] = await db.select().from(threads).where(eq(threads.id, threadId)).limit(1)\n return (row as TThread | undefined) ?? null\n },\n\n async createThread(input) {\n const title = threadTitleFromMessage(input.title ?? input.firstMessage ?? '')\n const values = {\n ...(input.id !== undefined ? { id: input.id } : {}),\n workspaceId: input.workspaceId,\n title,\n ...(input.category !== undefined ? { category: input.category } : {}),\n ...(input.isPinned !== undefined ? { isPinned: input.isPinned } : {}),\n ...(input.extras ?? {}),\n } as NewChatThreadRow\n const [row] = await db.insert(threads).values(values).returning()\n if (!row) throw new Error('thread insert returned no row')\n return row as TThread\n },\n\n async renameThread(threadId, title) {\n const trimmed = title.trim()\n if (!trimmed) throw new ChatStoreInputError('Missing title')\n const [row] = await db.update(threads)\n .set({ title: trimmed, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async pinThread(threadId, isPinned) {\n const [row] = await db.update(threads)\n .set({ isPinned, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async deleteThread(threadId, options) {\n const [existing] = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(eq(threads.id, threadId))\n .limit(1)\n if (!existing) return false\n if (options?.assertAccess) await options.assertAccess(existing.workspaceId)\n // Messages first so a partial failure never leaves orphaned rows behind\n // a deleted thread.\n await runSqliteStatements(db, [\n db.delete(messages).where(eq(messages.threadId, threadId)),\n db.delete(threads).where(eq(threads.id, threadId)),\n ])\n return true\n },\n\n async bulkDeleteThreads(input) {\n const { ids, workspaceId, assertAccess } = input\n if (typeof assertAccess !== 'function') throw new ChatStoreInputError('Missing assertAccess')\n if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === 'string' && id.length > 0)) {\n throw new ChatStoreInputError('Missing ids')\n }\n if (ids.length > BULK_DELETE_MAX_THREADS) {\n throw new ChatStoreInputError(`Too many ids (max ${BULK_DELETE_MAX_THREADS})`)\n }\n if (workspaceId !== undefined && !workspaceId.trim()) {\n throw new ChatStoreInputError('Invalid workspaceId')\n }\n\n const scope = workspaceId\n ? and(inArray(threads.id, ids), eq(threads.workspaceId, workspaceId))\n : inArray(threads.id, ids)\n const rows = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(scope)\n if (rows.length === 0) return { deleted: 0 }\n\n // Access is verified once per workspace the ids touch. Fail-closed: one\n // inaccessible workspace rejects the whole request before any delete.\n // Sorted so the check order (and therefore which denial surfaces) is\n // deterministic — row order follows random hex ids and varies per run.\n const workspaceIds = [...new Set(rows.map((row) => row.workspaceId))].sort()\n for (const workspaceId of workspaceIds) {\n await assertAccess(workspaceId)\n }\n\n const foundIds = rows.map((row) => row.id)\n await runSqliteStatements(db, [\n db.delete(messages).where(inArray(messages.threadId, foundIds)),\n db.delete(threads).where(inArray(threads.id, foundIds)),\n ])\n return { deleted: foundIds.length }\n },\n\n async bulkDeleteThreadsByUpdatedAt(input) {\n const { workspaceId, updatedBefore, updatedAfter, where, assertAccess } = input\n if (typeof assertAccess !== 'function') throw new ChatStoreInputError('Missing assertAccess')\n if (!workspaceId.trim()) throw new ChatStoreInputError('Missing workspaceId')\n if (updatedBefore === undefined && updatedAfter === undefined) {\n throw new ChatStoreInputError('Missing updatedAt boundary')\n }\n for (const boundary of [updatedBefore, updatedAfter]) {\n if (boundary !== undefined && Number.isNaN(boundary.getTime())) {\n throw new ChatStoreInputError('Invalid updatedAt boundary')\n }\n }\n\n const conditions = [eq(threads.workspaceId, workspaceId)]\n if (updatedBefore) conditions.push(lt(threads.updatedAt, updatedBefore))\n if (updatedAfter) conditions.push(gte(threads.updatedAt, updatedAfter))\n if (where) conditions.push(where)\n const scope = and(...conditions)\n const [countRow] = await db.select({ total: sql<number>`count(*)` })\n .from(threads)\n .where(scope)\n const deleted = Number(countRow?.total ?? 0)\n if (deleted === 0) return { deleted: 0 }\n\n await assertAccess(workspaceId)\n // Keep the age path unbounded without constructing a giant host-parameter\n // list; the subquery lets SQLite/D1 stream the matching thread ids.\n const matchingThreads = db.select({ id: threads.id }).from(threads).where(scope)\n await runSqliteStatements(db, [\n db.delete(messages).where(inArray(messages.threadId, matchingThreads)),\n db.delete(threads).where(scope),\n ])\n return { deleted }\n },\n\n async listMessages(threadId, options) {\n const query = db.select().from(messages)\n .where(eq(messages.threadId, threadId))\n .orderBy(asc(messages.createdAt), sql`rowid`)\n .$dynamic()\n if (options?.limit !== undefined) query.limit(clampLimit(options.limit, 1, 1000))\n if (options?.offset !== undefined) query.offset(clampOffset(options.offset))\n return await query as TMessage[]\n },\n\n async appendMessage(input) {\n const values = {\n ...(input.id !== undefined ? { id: input.id } : {}),\n threadId: input.threadId,\n role: input.role,\n content: input.content,\n ...(input.parts !== undefined ? { parts: input.parts } : {}),\n ...(input.toolName !== undefined ? { toolName: input.toolName } : {}),\n ...(input.model !== undefined ? { model: input.model } : {}),\n ...(input.requestedModel !== undefined ? { requestedModel: input.requestedModel } : {}),\n ...(input.servedModel !== undefined ? { servedModel: input.servedModel } : {}),\n ...(input.servedProvider !== undefined ? { servedProvider: input.servedProvider } : {}),\n ...(input.servedSource !== undefined ? { servedSource: input.servedSource } : {}),\n ...(input.inputTokens !== undefined ? { inputTokens: input.inputTokens } : {}),\n ...(input.outputTokens !== undefined ? { outputTokens: input.outputTokens } : {}),\n ...(input.reasoningTokens !== undefined ? { reasoningTokens: input.reasoningTokens } : {}),\n ...(input.cacheReadTokens !== undefined ? { cacheReadTokens: input.cacheReadTokens } : {}),\n ...(input.cacheWriteTokens !== undefined ? { cacheWriteTokens: input.cacheWriteTokens } : {}),\n ...(input.costUsd !== undefined ? { costUsd: input.costUsd } : {}),\n ...(input.extras ?? {}),\n } as NewChatMessageRow\n const [insertResult] = await runSqliteStatements(db, [\n db.insert(messages).values(values).returning(),\n db.update(threads).set({ updatedAt: new Date() }).where(eq(threads.id, input.threadId)),\n ])\n const row = (insertResult as TMessage[] | undefined)?.[0]\n if (!row) throw new Error('message insert returned no row')\n return row\n },\n\n async updateMessage(id, patch) {\n const values = {\n ...(patch.content !== undefined ? { content: patch.content } : {}),\n ...(patch.parts !== undefined ? { parts: patch.parts } : {}),\n ...(patch.toolName !== undefined ? { toolName: patch.toolName } : {}),\n ...(patch.model !== undefined ? { model: patch.model } : {}),\n ...(patch.requestedModel !== undefined ? { requestedModel: patch.requestedModel } : {}),\n ...(patch.servedModel !== undefined ? { servedModel: patch.servedModel } : {}),\n ...(patch.servedProvider !== undefined ? { servedProvider: patch.servedProvider } : {}),\n ...(patch.servedSource !== undefined ? { servedSource: patch.servedSource } : {}),\n ...(patch.inputTokens !== undefined ? { inputTokens: patch.inputTokens } : {}),\n ...(patch.outputTokens !== undefined ? { outputTokens: patch.outputTokens } : {}),\n ...(patch.reasoningTokens !== undefined ? { reasoningTokens: patch.reasoningTokens } : {}),\n ...(patch.cacheReadTokens !== undefined ? { cacheReadTokens: patch.cacheReadTokens } : {}),\n ...(patch.cacheWriteTokens !== undefined ? { cacheWriteTokens: patch.cacheWriteTokens } : {}),\n ...(patch.costUsd !== undefined ? { costUsd: patch.costUsd } : {}),\n ...(patch.extras ?? {}),\n }\n if (Object.keys(values).length === 0) {\n const [current] = await db.select().from(messages).where(eq(messages.id, id))\n return (current as TMessage | undefined) ?? null\n }\n // The thread bump reads the row's own `thread_id` rather than trusting a\n // caller-supplied one: a message never moves thread, so the subquery is\n // the authoritative source and one fewer parameter to get wrong.\n const [updateResult] = await runSqliteStatements(db, [\n db.update(messages).set(values).where(eq(messages.id, id)).returning(),\n db.update(threads).set({ updatedAt: new Date() })\n .where(eq(threads.id, sql`(select ${messages.threadId} from ${messages} where ${messages.id} = ${id})`)),\n ])\n return (updateResult as TMessage[] | undefined)?.[0] ?? null\n },\n\n async deleteMessage(id) {\n const deleted = await db.delete(messages).where(eq(messages.id, id)).returning()\n return (deleted as unknown[]).length > 0\n },\n }\n}\n"],"mappings":"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","names":["sql","sql","workspaceId"]}
@@ -3,18 +3,18 @@ import {
3
3
  } from "./chunk-ZSSCVT6H.js";
4
4
  import {
5
5
  parseJsonObjectBody
6
- } from "./chunk-F7VH4G5C.js";
6
+ } from "./chunk-3YUI7WNW.js";
7
7
  import {
8
8
  mentionInputToPart,
9
9
  toChatMessageParts
10
- } from "./chunk-IRYULX3R.js";
10
+ } from "./chunk-4PZE7XAM.js";
11
11
  import {
12
12
  ChatTurnInputError,
13
13
  assertPromptPartsWithinCap,
14
14
  chatTurnRequestInit,
15
15
  parseChatTurnParts,
16
16
  parseFileMentions
17
- } from "./chunk-DXHUQ5NO.js";
17
+ } from "./chunk-X47R2IVO.js";
18
18
  import {
19
19
  normalizeClientTurnId,
20
20
  resolveChatTurn
@@ -1046,4 +1046,4 @@ export {
1046
1046
  createChatTurnRoutes,
1047
1047
  streamChatRouteAsSandboxEvents
1048
1048
  };
1049
- //# sourceMappingURL=chunk-LYAYNWDS.js.map
1049
+ //# sourceMappingURL=chunk-3GVFJYLO.js.map
@@ -280,4 +280,4 @@ export {
280
280
  addSecurityHeaders,
281
281
  assertMediaUrl
282
282
  };
283
- //# sourceMappingURL=chunk-F7VH4G5C.js.map
283
+ //# sourceMappingURL=chunk-3YUI7WNW.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/web/rate-limit.ts","../src/web/free-route-limit.ts","../src/web/index.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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/web/rate-limit.ts","../src/web/free-route-limit.ts","../src/web/index.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"],"mappings":"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","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-ZVEEWGDK.js";
4
4
  import {
5
5
  mentionKindForPath
6
- } from "./chunk-DXHUQ5NO.js";
6
+ } from "./chunk-X47R2IVO.js";
7
7
  import {
8
8
  persistedPartToInteraction
9
9
  } from "./chunk-M3K2HVQD.js";
@@ -155,4 +155,4 @@ export {
155
155
  buildAttachmentPromptBlock,
156
156
  historyContentWithAttachments
157
157
  };
158
- //# sourceMappingURL=chunk-IRYULX3R.js.map
158
+ //# sourceMappingURL=chunk-4PZE7XAM.js.map
@@ -13,13 +13,13 @@ import {
13
13
  setPageGuidesCommand,
14
14
  setPagePropsCommand,
15
15
  ungroupElementCommand
16
- } from "./chunk-X5DQ5QOP.js";
16
+ } from "./chunk-G4CPEJNN.js";
17
17
  import {
18
18
  SIZE_PRESETS,
19
19
  findElement,
20
20
  matchPreset,
21
21
  requirePage
22
- } from "./chunk-DRB77FYL.js";
22
+ } from "./chunk-A2XMTCER.js";
23
23
 
24
24
  // src/design-canvas-react/components/DesignCanvas.tsx
25
25
  import { useCallback as useCallback2, useEffect as useEffect3, useMemo, useRef as useRef5, useState as useState5, useSyncExternalStore } from "react";
@@ -2261,4 +2261,4 @@ export {
2261
2261
  ExportControl,
2262
2262
  DesignCanvas
2263
2263
  };
2264
- //# sourceMappingURL=chunk-7LJYCHXK.js.map
2264
+ //# sourceMappingURL=chunk-54KJ54SY.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  assertMediaUrl
3
- } from "./chunk-F7VH4G5C.js";
3
+ } from "./chunk-3YUI7WNW.js";
4
4
 
5
5
  // src/design-canvas/model.ts
6
6
  var SCENE_SCHEMA_VERSION = 1;
@@ -1001,4 +1001,4 @@ export {
1001
1001
  scalePageForChannelPreset,
1002
1002
  bleedAwareExportRect
1003
1003
  };
1004
- //# sourceMappingURL=chunk-DRB77FYL.js.map
1004
+ //# sourceMappingURL=chunk-A2XMTCER.js.map
@@ -5,7 +5,7 @@ import {
5
5
  clearCookieHeader,
6
6
  readCookieValue,
7
7
  serializeCookie
8
- } from "./chunk-F7VH4G5C.js";
8
+ } from "./chunk-3YUI7WNW.js";
9
9
 
10
10
  // src/platform/sso.ts
11
11
  var DEFAULT_STATE_TTL_SECONDS = 600;
@@ -301,4 +301,4 @@ export {
301
301
  createAdminGuard,
302
302
  assertBillableBalance
303
303
  };
304
- //# sourceMappingURL=chunk-UB4LENR5.js.map
304
+ //# sourceMappingURL=chunk-AXJV66PC.js.map
@@ -1,3 +1,7 @@
1
+ import {
2
+ isWorkspaceFileExportable
3
+ } from "./chunk-TXD5HXLE.js";
4
+
1
5
  // src/chat-routes/file-index.ts
2
6
  var DEFAULT_IGNORE_SEGMENTS = [
3
7
  "node_modules",
@@ -10,7 +14,7 @@ var DEFAULT_IGNORE_SEGMENTS = [
10
14
  "venv"
11
15
  ];
12
16
  function isIgnored(relPath, ignoreSegments) {
13
- if (!relPath || relPath.startsWith("/") || relPath.includes("\\")) return true;
17
+ if (!isWorkspaceFileExportable(relPath)) return true;
14
18
  for (const segment of relPath.split("/")) {
15
19
  if (!segment) continue;
16
20
  if (segment.startsWith(".")) return true;
@@ -47,12 +51,15 @@ function createSandboxFileIndexRoute(options) {
47
51
  if (auth.status === "warming") {
48
52
  return Response.json({ status: "warming" });
49
53
  }
54
+ const ignoreSegments = auth.ignore?.length ? /* @__PURE__ */ new Set([...staticIgnore, ...auth.ignore]) : staticIgnore;
50
55
  const cache = options.cache;
51
56
  if (cache && auth.cacheKey) {
52
57
  const cached = await cache.get(auth.cacheKey);
53
- if (cached) return Response.json(cached);
58
+ if (cached) return Response.json({
59
+ ...cached,
60
+ files: cached.files.filter((file) => !isIgnored(file.path, ignoreSegments))
61
+ });
54
62
  }
55
- const ignoreSegments = auth.ignore?.length ? /* @__PURE__ */ new Set([...staticIgnore, ...auth.ignore]) : staticIgnore;
56
63
  let scan;
57
64
  try {
58
65
  scan = await auth.fs.tree(auth.root, { maxDepth });
@@ -82,4 +89,4 @@ function createSandboxFileIndexRoute(options) {
82
89
  export {
83
90
  createSandboxFileIndexRoute
84
91
  };
85
- //# sourceMappingURL=chunk-HZVVO6VR.js.map
92
+ //# sourceMappingURL=chunk-FQYAJYPW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-routes/file-index.ts"],"sourcesContent":["/**\n * `createSandboxFileIndexRoute` — server side of `@`-file-mentions\n * (companion to sandbox-ui#184's composer mention primitive). Serves a flat,\n * ignore-filtered listing of the workspace sandbox so `useFileMentions`\n * (`/web-react`) can filter it client-side without a round trip per\n * keystroke.\n *\n * Same seam style as `createUploadRoute`: `authorize({ request })` resolves a\n * structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's\n * `box.fs.tree`) — no SDK import here. `authorize` also carries the\n * cold-box signal: a sandbox that isn't running yet answers `{ status:\n * 'warming' }` directly, never provisions-and-waits inside this route.\n *\n * A box can also be running with its workspace root not yet materialised, which\n * `authorize` cannot see; the route recognises that one signal off `fs.tree`\n * and answers `warming` too, so every consumer gets the retry-and-wait state\n * instead of a 500. Every other `tree()` failure propagates.\n */\n\nimport type { FileMention } from './wire'\nimport { isWorkspaceFileExportable } from '../web/file-export'\n\n/** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's\n * `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's\n * omitted from the structural match. */\nexport interface SandboxTreeFile {\n path: string\n size: number\n}\n\n/** Structural match of the sandbox SDK's `box.fs.tree` result shape\n * (`FileTreeResult`). `stats.truncated` is the only stat this route reads;\n * the rest ride through unread on the real SDK type. */\nexport interface SandboxTreeResult {\n root: string\n files: SandboxTreeFile[]\n stats: { truncated: boolean }\n}\n\n/** Structural match of the sandbox SDK's `box.fs` tree surface. */\nexport interface SandboxFileTreeSource {\n tree(path: string, options?: { maxDepth?: number }): Promise<SandboxTreeResult>\n}\n\n/** Describe a ready file index response with workspace-relative entries and truncation status */\nexport interface FileIndexReadyResponse {\n status: 'ready'\n /** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a\n * client can hand a response entry straight to `fileMentionsToParts` /\n * `buildMentionPromptBlock` without remapping. */\n files: FileMention[]\n /** True when either the underlying scan truncated (SDK-side cap) or this\n * route's own `maxEntries` cap trimmed the filtered list. The client\n * should show \"showing first N files\" rather than imply completeness. */\n truncated: boolean\n generatedAt: string\n}\n\n/** Cold-box answer: no provisioning happened, no files were scanned. The\n * client shows a warming state and retries — this route never blocks on a\n * box coming up. Two situations produce it: `authorize` reporting a box that\n * is not running, and a running box whose workspace root does not exist yet\n * (see `isMissingRootError`). */\nexport interface FileIndexWarmingResponse {\n status: 'warming'\n}\n\n/** Resolve a response indicating the file index is either ready or warming up */\nexport type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse\n\n/** Short-TTL cache seam so repeat popover opens in the same session don't\n * re-scan the workspace. Host-provided (e.g. a KV binding); `key` is\n * whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */\nexport interface FileIndexCache {\n get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null\n put(key: string, value: FileIndexReadyResponse, options?: { ttlSeconds?: number }): Promise<void> | void\n}\n\n/** Define authorization details and parameters for indexing a file workspace with optional caching and ignore rules */\nexport type FileIndexAuthorization =\n | {\n status: 'ready'\n /** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */\n fs: SandboxFileTreeSource\n /** Workspace root to index (e.g. `/home/agent`). */\n root: string\n /** Extra ignore segments for this request, merged with the route's\n * defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */\n ignore?: string[]\n /** Opaque cache key for the optional cache seam. Omit to skip caching\n * for this request (e.g. a workspace the host chooses not to cache). */\n cacheKey?: string\n }\n | { status: 'warming' }\n | { status: 'denied'; response: Response }\n\n/** Define options to authorize and configure sandbox file index route behavior */\nexport interface CreateSandboxFileIndexRouteOptions {\n /** Authenticate the caller, resolve the sandbox `fs` handle, and signal a\n * cold box — never provisions or waits. */\n authorize(args: { request: Request }): Promise<FileIndexAuthorization>\n /** Extra ignore segments beyond the route's defaults (node_modules, .git,\n * dotfiles/dot-dirs, common build dirs). Matched as exact path-segment\n * names, same rule as the defaults. */\n ignore?: string[]\n /** Passed to `fs.tree` as `options.maxDepth`. Default 12. */\n maxDepth?: number\n /** Hard cap on entries returned after filtering. Default 5000. */\n maxEntries?: number\n /** Optional host-provided cache seam. */\n cache?: FileIndexCache\n /** Cache TTL in seconds when `cache` is set. Default 20. */\n cacheTtlSeconds?: number\n}\n\n/** Segment names ignored anywhere in a path, beyond the generic dotfile rule\n * below. Intentionally small and language/framework-agnostic — callers\n * extend it via `ignore` for anything domain-specific (e.g. a vault's\n * `uploads` dir). */\nconst DEFAULT_IGNORE_SEGMENTS = [\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n 'target',\n '__pycache__',\n 'venv',\n]\n\n/** A path segment starting with `.` (`.git`, `.env`, `.next`, `.cache`, …) is\n * always ignored — this single rule covers most dot-prefixed VCS/tooling\n * dirs and dotfiles without enumerating them. */\nfunction isIgnored(relPath: string, ignoreSegments: ReadonlySet<string>): boolean {\n if (!isWorkspaceFileExportable(relPath)) return true\n for (const segment of relPath.split('/')) {\n if (!segment) continue\n if (segment.startsWith('.')) return true\n if (ignoreSegments.has(segment)) return true\n }\n return false\n}\n\n/** Strips the tree result's echoed `root` prefix so entries are always\n * workspace-relative, whichever convention the structural `fs.tree` uses\n * (root-relative already, or root-prefixed). */\nfunction relativeTo(root: string, path: string): string {\n const prefix = root.endsWith('/') ? root : `${root}/`\n if (path.startsWith(prefix)) return path.slice(prefix.length)\n if (path === root) return ''\n return path\n}\n\nfunction basename(path: string): string {\n const segments = path.split('/').filter(Boolean)\n return segments[segments.length - 1] ?? path\n}\n\n/**\n * A box can answer `running` before it has materialised the workspace root —\n * `authorize` has already committed to `ready` by then, so `fs.tree` is the\n * first thing to notice, and it rejects with the sandbox SDK's\n * `ValidationError` wrapping a box-side `ENOENT … lstat` on the root. That is\n * the SAME \"not usable yet\" state `authorize` collapses onto `warming` for an\n * absent or stopped box, just discovered one step later, so it gets the same\n * answer instead of escaping as a 500.\n *\n * Matched STRUCTURALLY, not with `instanceof`: importing the SDK's error class\n * would make `@tangle-network/sandbox` a hard dependency of a route factory\n * whose entire `fs` seam is structural (`SandboxFileTreeSource`), and would\n * break any host feeding it a non-SDK handle.\n *\n * Deliberately narrow — the error code, `ENOENT`, the ENOENT message text, AND\n * the failing syscall's own operand all have to line up. A permission error, a\n * timeout, an auth failure, or an ENOENT on some other path inside the tree is\n * a real failure and still surfaces.\n *\n * The operand is matched as the quoted `lstat '<root>'` clause rather than by\n * substring, because the root is a PREFIX of everything under it: a plain\n * `includes(root)` would also swallow an ENOENT on `<root>/gone/x.md`, and on\n * a prefix sibling like `/home/agent-old/...`.\n */\nfunction isMissingRootError(err: unknown, root: string): boolean {\n if (!(err instanceof Error)) return false\n if ((err as { code?: unknown }).code !== 'VALIDATION_ERROR') return false\n return (\n /ENOENT/.test(err.message) &&\n /no such file or directory/.test(err.message) &&\n new RegExp(`\\\\blstat '${escapeRegExp(root)}'`).test(err.message)\n )\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Resolve a sandbox file index route with authorization, caching, and configurable depth and entries limits */\nexport function createSandboxFileIndexRoute(\n options: CreateSandboxFileIndexRouteOptions,\n): (request: Request) => Promise<Response> {\n const maxDepth = options.maxDepth ?? 12\n const maxEntries = options.maxEntries ?? 5000\n const cacheTtlSeconds = options.cacheTtlSeconds ?? 20\n const staticIgnore = new Set([...DEFAULT_IGNORE_SEGMENTS, ...(options.ignore ?? [])])\n\n return async function fileIndex(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (auth.status === 'denied') return auth.response\n if (auth.status === 'warming') {\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n\n const ignoreSegments = auth.ignore?.length\n ? new Set([...staticIgnore, ...auth.ignore])\n : staticIgnore\n const cache = options.cache\n if (cache && auth.cacheKey) {\n const cached = await cache.get(auth.cacheKey)\n if (cached) return Response.json({ ...cached,\n files: cached.files.filter(file => !isIgnored(file.path, ignoreSegments)),\n })\n }\n\n let scan: SandboxTreeResult\n try {\n scan = await auth.fs.tree(auth.root, { maxDepth })\n } catch (err) {\n if (!isMissingRootError(err, auth.root)) throw err\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n const filtered = scan.files.filter((f) => !isIgnored(relativeTo(auth.root, f.path), ignoreSegments))\n const truncated = scan.stats.truncated || filtered.length > maxEntries\n const files: FileMention[] = filtered.slice(0, maxEntries).map((f) => {\n const path = relativeTo(auth.root, f.path)\n const entry: FileMention = { path, name: basename(path) }\n if (typeof f.size === 'number') entry.size = f.size\n return entry\n })\n\n const body: FileIndexReadyResponse = {\n status: 'ready',\n files,\n truncated,\n generatedAt: new Date().toISOString(),\n }\n\n if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds })\n\n return Response.json(body)\n }\n}\n"],"mappings":"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","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  applySceneOperations,
4
4
  requireElement,
5
5
  requirePage
6
- } from "./chunk-DRB77FYL.js";
6
+ } from "./chunk-A2XMTCER.js";
7
7
 
8
8
  // src/design-canvas-react/engine/command-stack.ts
9
9
  var SCENE_COMMAND_HISTORY_LIMIT = 200;
@@ -601,4 +601,4 @@ export {
601
601
  bindSlotCommand,
602
602
  setDocumentTitleCommand
603
603
  };
604
- //# sourceMappingURL=chunk-X5DQ5QOP.js.map
604
+ //# sourceMappingURL=chunk-G4CPEJNN.js.map