@tangle-network/agent-app 0.47.24 → 0.47.25

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 (39) hide show
  1. package/dist/DesignCanvas-MWEYD6PR.js +11 -0
  2. package/dist/DesignCanvasEditor-UZ2S6BDD.js +14 -0
  3. package/dist/app-auth/index.js +2 -2
  4. package/dist/chat-routes/index.js +2 -2
  5. package/dist/{chunk-G4CPEJNN.js → chunk-AKMQFCDL.js} +2 -2
  6. package/dist/{chunk-TXM4SS46.js → chunk-CSMJM7YT.js} +2 -2
  7. package/dist/{chunk-3YUI7WNW.js → chunk-IAY2DMKW.js} +30 -3
  8. package/dist/chunk-IAY2DMKW.js.map +1 -0
  9. package/dist/{chunk-54KJ54SY.js → chunk-JAT6SOPK.js} +3 -3
  10. package/dist/{chunk-HUYTN7RI.js → chunk-JF423ASS.js} +5 -5
  11. package/dist/{chunk-WOZSOPUZ.js → chunk-KX2GA3L6.js} +3 -3
  12. package/dist/{chunk-42NBWR4U.js → chunk-SYWEMKZR.js} +2 -2
  13. package/dist/{chunk-QEPLXYOE.js → chunk-VHMZ3KUH.js} +2 -2
  14. package/dist/{chunk-A2XMTCER.js → chunk-WYHFHTSQ.js} +2 -2
  15. package/dist/design-canvas/index.js +2 -2
  16. package/dist/design-canvas-react/engine.js +4 -4
  17. package/dist/design-canvas-react/index.js +7 -7
  18. package/dist/design-canvas-react/lazy.js +1 -1
  19. package/dist/platform/index.js +2 -2
  20. package/dist/public-consultation/index.js +2 -2
  21. package/dist/sequences/index.js +1 -1
  22. package/dist/web/core.d.ts +84 -0
  23. package/dist/web/index.d.ts +3 -84
  24. package/dist/web/index.js +3 -1
  25. package/dist/web/message-groups.d.ts +21 -0
  26. package/package.json +1 -1
  27. package/dist/DesignCanvas-UPRRA4YG.js +0 -11
  28. package/dist/DesignCanvasEditor-GVJO5SMH.js +0 -14
  29. package/dist/chunk-3YUI7WNW.js.map +0 -1
  30. /package/dist/{DesignCanvas-UPRRA4YG.js.map → DesignCanvas-MWEYD6PR.js.map} +0 -0
  31. /package/dist/{DesignCanvasEditor-GVJO5SMH.js.map → DesignCanvasEditor-UZ2S6BDD.js.map} +0 -0
  32. /package/dist/{chunk-G4CPEJNN.js.map → chunk-AKMQFCDL.js.map} +0 -0
  33. /package/dist/{chunk-TXM4SS46.js.map → chunk-CSMJM7YT.js.map} +0 -0
  34. /package/dist/{chunk-54KJ54SY.js.map → chunk-JAT6SOPK.js.map} +0 -0
  35. /package/dist/{chunk-HUYTN7RI.js.map → chunk-JF423ASS.js.map} +0 -0
  36. /package/dist/{chunk-WOZSOPUZ.js.map → chunk-KX2GA3L6.js.map} +0 -0
  37. /package/dist/{chunk-42NBWR4U.js.map → chunk-SYWEMKZR.js.map} +0 -0
  38. /package/dist/{chunk-QEPLXYOE.js.map → chunk-VHMZ3KUH.js.map} +0 -0
  39. /package/dist/{chunk-A2XMTCER.js.map → chunk-WYHFHTSQ.js.map} +0 -0
@@ -0,0 +1,11 @@
1
+ import {
2
+ DesignCanvas
3
+ } from "./chunk-JAT6SOPK.js";
4
+ import "./chunk-AKMQFCDL.js";
5
+ import "./chunk-WYHFHTSQ.js";
6
+ import "./chunk-IAY2DMKW.js";
7
+ import "./chunk-TXD5HXLE.js";
8
+ export {
9
+ DesignCanvas
10
+ };
11
+ //# sourceMappingURL=DesignCanvas-MWEYD6PR.js.map
@@ -0,0 +1,14 @@
1
+ import {
2
+ DesignCanvasEditor
3
+ } from "./chunk-JF423ASS.js";
4
+ import "./chunk-JAT6SOPK.js";
5
+ import "./chunk-Q2QKEV6J.js";
6
+ import "./chunk-VHMZ3KUH.js";
7
+ import "./chunk-AKMQFCDL.js";
8
+ import "./chunk-WYHFHTSQ.js";
9
+ import "./chunk-IAY2DMKW.js";
10
+ import "./chunk-TXD5HXLE.js";
11
+ export {
12
+ DesignCanvasEditor
13
+ };
14
+ //# sourceMappingURL=DesignCanvasEditor-UZ2S6BDD.js.map
@@ -2,9 +2,9 @@ import {
2
2
  createAuthGuard,
3
3
  createBetterAuthSessionCookieMinter,
4
4
  createTangleSsoHandlers
5
- } from "../chunk-TXM4SS46.js";
5
+ } from "../chunk-CSMJM7YT.js";
6
6
  import "../chunk-JML7WKWU.js";
7
- import "../chunk-3YUI7WNW.js";
7
+ import "../chunk-IAY2DMKW.js";
8
8
  import "../chunk-TXD5HXLE.js";
9
9
 
10
10
  // src/app-auth/index.ts
@@ -61,9 +61,9 @@ import {
61
61
  rowIdOf,
62
62
  storeSupportsDraftPersistence,
63
63
  streamChatRouteAsSandboxEvents
64
- } from "../chunk-42NBWR4U.js";
64
+ } from "../chunk-SYWEMKZR.js";
65
65
  import "../chunk-ZSSCVT6H.js";
66
- import "../chunk-3YUI7WNW.js";
66
+ import "../chunk-IAY2DMKW.js";
67
67
  import {
68
68
  attachmentInputToPart,
69
69
  attachmentKindForMime,
@@ -3,7 +3,7 @@ import {
3
3
  applySceneOperations,
4
4
  requireElement,
5
5
  requirePage
6
- } from "./chunk-A2XMTCER.js";
6
+ } from "./chunk-WYHFHTSQ.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-G4CPEJNN.js.map
604
+ //# sourceMappingURL=chunk-AKMQFCDL.js.map
@@ -5,7 +5,7 @@ import {
5
5
  clearCookieHeader,
6
6
  readCookieValue,
7
7
  serializeCookie
8
- } from "./chunk-3YUI7WNW.js";
8
+ } from "./chunk-IAY2DMKW.js";
9
9
 
10
10
  // src/platform/sso.ts
11
11
  var DEFAULT_STATE_TTL_SECONDS = 600;
@@ -374,4 +374,4 @@ export {
374
374
  createAdminGuard,
375
375
  assertBillableBalance
376
376
  };
377
- //# sourceMappingURL=chunk-TXM4SS46.js.map
377
+ //# sourceMappingURL=chunk-CSMJM7YT.js.map
@@ -176,7 +176,7 @@ function withFreeRouteLimit(options, handler) {
176
176
  };
177
177
  }
178
178
 
179
- // src/web/index.ts
179
+ // src/web/core.ts
180
180
  async function parseJsonObjectBody(request) {
181
181
  let raw;
182
182
  try {
@@ -262,6 +262,32 @@ function assertMediaUrl(url, what = "media url") {
262
262
  throw new Error(`${what} must be http(s) or a rooted /api/ path (${shown})`);
263
263
  }
264
264
 
265
+ // src/web/message-groups.ts
266
+ function groupConversationMessages(items = []) {
267
+ let speaker = null;
268
+ let conversation;
269
+ let groupId;
270
+ return items.map((item, index) => {
271
+ if (item.conversationId !== void 0 && item.conversationId !== conversation) {
272
+ conversation = item.conversationId;
273
+ speaker = null;
274
+ groupId = void 0;
275
+ }
276
+ const role = item.kind === "thinking" ? "assistant" : !item.kind || item.kind === "message" ? item.role : void 0;
277
+ if (role === "user") {
278
+ speaker = null;
279
+ groupId = void 0;
280
+ return { ...item, isContinuation: false };
281
+ }
282
+ if (role !== "assistant") return item;
283
+ const current = item.speakerId ?? "assistant";
284
+ const isContinuation = speaker === current;
285
+ if (!isContinuation) groupId = String(item.id ?? `assistant-${index}`);
286
+ speaker = current;
287
+ return { ...item, isContinuation, groupId };
288
+ });
289
+ }
290
+
265
291
  export {
266
292
  checkRateLimit,
267
293
  FREE_ROUTE_BUDGETS,
@@ -278,6 +304,7 @@ export {
278
304
  readCookieValue,
279
305
  STANDARD_SECURITY_HEADERS,
280
306
  addSecurityHeaders,
281
- assertMediaUrl
307
+ assertMediaUrl,
308
+ groupConversationMessages
282
309
  };
283
- //# sourceMappingURL=chunk-3YUI7WNW.js.map
310
+ //# sourceMappingURL=chunk-IAY2DMKW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/rate-limit.ts","../src/web/free-route-limit.ts","../src/web/core.ts","../src/web/message-groups.ts"],"sourcesContent":["/**\n * The KV-backed sliding-window rate limiter. Lives in its own leaf so policy\n * layers (`./free-route-limit`) can build on it without importing the web\n * barrel and creating an import cycle. Re-exported from `./index`, which is the\n * published surface.\n */\n\n/** Minimal KV contract (Cloudflare `KVNamespace` satisfies it structurally). */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\n/** Describe the outcome of a rate limit check including allowance, remaining count, and reset time */\nexport interface RateLimitResult {\n allowed: boolean\n remaining: number\n resetAt: number\n}\n\n/** KV-backed sliding-window rate limit. Stores recent timestamps per key,\n * prunes the window, allows until `limit` is hit.\n *\n * Read-modify-write is best-effort, NOT atomic: KV has no compare-and-swap, so\n * two requests racing on the same key can each read the same pre-state and both\n * write — a concurrent burst can momentarily admit up to one extra request per\n * racing writer. This is acceptable for coarse abuse limiting; it is NOT a hard\n * quota gate.\n *\n * Fail-CLOSED on unreadable state: corrupt/non-array KV (a poisoned or\n * truncated value) is treated as a full window, so a tampered key cannot reset\n * the count and bypass the limiter. A bare `JSON.parse` here would throw and\n * abort the request handler, silently disabling the limit (fail-open). */\nexport async function checkRateLimit(kv: KvLike, key: string, limit: number, windowSeconds: number): Promise<RateLimitResult> {\n const now = Math.floor(Date.now() / 1000)\n const windowStart = now - windowSeconds\n const kvKey = `rl:${key}`\n const raw = await kv.get(kvKey)\n const parsed = parseRateLimitState(raw)\n if (parsed === POISONED_STATE) {\n // Unreadable state (parse threw or value is not a JSON array): deny rather\n // than reset the window to empty. A poisoned key must not become a bypass.\n return { allowed: false, remaining: 0, resetAt: now + windowSeconds }\n }\n const valid = parsed.filter((t) => t > windowStart)\n if (valid.length >= limit) return { allowed: false, remaining: 0, resetAt: (valid[0] ?? now) + windowSeconds }\n valid.push(now)\n await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 })\n return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds }\n}\n\n/** Sentinel returned by `parseRateLimitState` when the stored value cannot be\n * read as a timestamp array — distinct from an empty window so the limiter can\n * fail closed instead of treating corruption as a fresh window. */\nconst POISONED_STATE = Symbol('rate-limit-poisoned-state')\n\n/** Parse stored rate-limit state into a timestamp array. Absent state is a\n * fresh (empty) window. A value that fails to parse, or parses to a non-array,\n * returns `POISONED_STATE`; numeric junk inside a valid array is dropped. */\nfunction parseRateLimitState(raw: string | null): number[] | typeof POISONED_STATE {\n if (raw === null) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return POISONED_STATE\n }\n if (!Array.isArray(parsed)) return POISONED_STATE\n return parsed.filter((t): t is number => typeof t === 'number' && Number.isFinite(t))\n}\n","/**\n * Rate-limit policy for FREE routes: authenticated, unmetered, compute-bearing\n * endpoints.\n *\n * Billing meters the chat turn. Everything deterministic a product adds beside\n * it — a planning recompute fired on every slider drag, a redline diff, a\n * deadline computation — runs unmetered, so nothing bounds how much worker CPU\n * one authenticated caller can spend. `checkRateLimit` is the primitive; this\n * is the policy around it, so a route declares a cost class instead of each one\n * inventing a key format, a budget, and a denial response.\n *\n * Three rules this enforces that a hand-rolled call site usually does not:\n *\n * 1. **Identity is required.** A missing subject is denied, never pooled into a\n * shared bucket and never quietly keyed by IP — a shared bucket makes one\n * heavy user throttle everyone, and an IP key makes one NAT do the same.\n * 2. **A limiter that throws denies.** KV `get`/`put` failures are transport\n * failures; treating them as \"allowed\" converts a KV outage into an\n * unbounded-compute hole, which is exactly when the limiter matters most.\n * 3. **The denial is a typed, correctable outcome** — reason, HTTP status and\n * `Retry-After` — so the caller answers the client rather than failing\n * opaquely or, worse, continuing.\n *\n * ```ts\n * export const action = withFreeRouteLimit(\n * {\n * route: 'tax.plan.recompute',\n * costClass: 'interactive',\n * kv: ({ context }) => context.cloudflare.env.RATE_LIMIT_KV,\n * identify: async ({ request }) => {\n * const session = await requireSession(request)\n * return { subject: session.userId, workspace: session.workspaceId }\n * },\n * },\n * async ({ request }) => Response.json(recompute(await request.json())),\n * )\n * ```\n */\n\nimport { checkRateLimit, type KvLike, type RateLimitResult } from './rate-limit'\n\n/** Requests allowed per identity inside a sliding window. */\nexport interface RateLimitBudget {\n limit: number\n windowSeconds: number\n}\n\n/**\n * How expensive one call is, which is what picks the default budget.\n *\n * - `interactive` — a control the user drags or types into, recomputed per\n * tick. Sized for sustained two-per-second interaction, so a real slider\n * never trips it and a script still stops.\n * - `compute` — a derivation the user triggers deliberately (recalculate a\n * plan, diff a document, compute a deadline set). The default.\n * - `heavy` — whole-document or whole-matter work measured in seconds of CPU.\n */\nexport type FreeRouteClass = 'interactive' | 'compute' | 'heavy'\n\n/** Default budget per cost class. A product overrides with an explicit\n * `budget` when it has measured its own route. */\nexport const FREE_ROUTE_BUDGETS: Readonly<Record<FreeRouteClass, RateLimitBudget>> = Object.freeze({\n interactive: Object.freeze({ limit: 120, windowSeconds: 60 }),\n compute: Object.freeze({ limit: 30, windowSeconds: 60 }),\n heavy: Object.freeze({ limit: 10, windowSeconds: 300 }),\n})\n\n/** A workspace's shared ceiling defaults to this many times one member's, so a\n * multi-seat tenant is bounded without a single active member tripping it. */\nexport const WORKSPACE_BUDGET_MULTIPLIER = 5\n\n/** Why a call was refused. Every value is a REFUSAL — there is no reason code\n * that lets the handler run. */\nexport type FreeRouteDenialReason =\n /** No authenticated subject. Not correctable by waiting. */\n | 'unidentified'\n /** The window is full for this subject or workspace. */\n | 'rate-limited'\n /** The limiter itself failed (KV read/write threw). Denied, not passed. */\n | 'limiter-unavailable'\n\n/** Which window refused the call. `null` when no window was consulted. */\nexport type FreeRouteDimension = 'subject' | 'workspace' | null\n\n/**\n * A correctable refusal: the caller maps `status` + `retryAfterSeconds` onto a\n * response (see {@link freeRouteLimitResponse}) so the client learns what to do\n * next. Carrying the limiter's own failure in `cause` keeps a KV outage\n * diagnosable instead of collapsing into a generic 429.\n */\nexport class FreeRouteLimitError extends Error {\n readonly reason: FreeRouteDenialReason\n readonly status: 401 | 429 | 503\n readonly retryAfterSeconds: number\n readonly dimension: FreeRouteDimension\n readonly route: string\n\n constructor(init: {\n reason: FreeRouteDenialReason\n status: 401 | 429 | 503\n retryAfterSeconds: number\n dimension: FreeRouteDimension\n route: string\n message: string\n cause?: unknown\n }) {\n super(init.message, init.cause === undefined ? undefined : { cause: init.cause })\n this.name = 'FreeRouteLimitError'\n this.reason = init.reason\n this.status = init.status\n this.retryAfterSeconds = init.retryAfterSeconds\n this.dimension = init.dimension\n this.route = init.route\n }\n}\n\n/** What remains of the binding window after an allowed call. */\nexport interface FreeRouteAllowance {\n /** Remaining calls in whichever window is tightest. */\n remaining: number\n /** Unix seconds at which that window resets. */\n resetAt: number\n /** The budget that window enforces. */\n budget: RateLimitBudget\n /** Which window is tightest — `subject` unless a workspace ceiling binds. */\n dimension: Exclude<FreeRouteDimension, null>\n}\n\n/** Typed outcome at the route boundary: an allowance, or a refusal that says\n * how to correct it. There is no third state and no silent pass. */\nexport type FreeRouteLimitOutcome =\n | { succeeded: true; value: FreeRouteAllowance }\n | { succeeded: false; error: FreeRouteLimitError }\n\nexport interface FreeRouteLimitInput {\n kv: KvLike\n /** Stable route name. Part of the key, so two routes never share a window. */\n route: string\n /** The authenticated subject (user id). Absent or blank ⇒ denied. */\n subject: string | null | undefined\n /** Tenant the call belongs to. When present, a second, wider window bounds\n * the workspace as a whole. */\n workspace?: string | null\n /** Cost class picking the default budget. Ignored when `budget` is given.\n * Default `'compute'`. */\n costClass?: FreeRouteClass\n budget?: RateLimitBudget\n /** Workspace ceiling. Defaults to the subject budget's limit multiplied by\n * {@link WORKSPACE_BUDGET_MULTIPLIER} over the same window. */\n workspaceBudget?: RateLimitBudget\n}\n\n/** Key components are percent-encoded so a route name or subject containing the\n * separator cannot be crafted to collide with another caller's window. */\nfunction limitKey(route: string, dimension: string, id: string): string {\n return `free:${encodeURIComponent(route)}:${dimension}:${encodeURIComponent(id)}`\n}\n\nfunction assertBudget(budget: RateLimitBudget, label: string): void {\n const valid =\n Number.isInteger(budget.limit) &&\n budget.limit >= 1 &&\n Number.isInteger(budget.windowSeconds) &&\n budget.windowSeconds >= 1\n if (!valid) {\n throw new Error(\n `${label} must be positive integers (got limit=${budget.limit}, windowSeconds=${budget.windowSeconds})`,\n )\n }\n}\n\nfunction secondsUntil(resetAt: number): number {\n return Math.max(1, resetAt - Math.floor(Date.now() / 1000))\n}\n\n/**\n * Run one window. A throw from KV becomes a `limiter-unavailable` refusal\n * rather than propagating — the caller must be able to answer 503 with a\n * `Retry-After` instead of the handler running or the route 500ing opaquely.\n */\nasync function runWindow(\n kv: KvLike,\n route: string,\n dimension: Exclude<FreeRouteDimension, null>,\n id: string,\n budget: RateLimitBudget,\n): Promise<{ succeeded: true; value: RateLimitResult } | { succeeded: false; error: FreeRouteLimitError }> {\n try {\n const value = await checkRateLimit(kv, limitKey(route, dimension, id), budget.limit, budget.windowSeconds)\n return { succeeded: true, value }\n } catch (cause) {\n return {\n succeeded: false,\n error: new FreeRouteLimitError({\n reason: 'limiter-unavailable',\n status: 503,\n retryAfterSeconds: Math.min(budget.windowSeconds, 30),\n dimension,\n route,\n message: `Rate limiter unavailable for route '${route}' — the request is refused rather than admitted unmetered`,\n cause,\n }),\n }\n }\n}\n\n/**\n * Decide whether one authenticated, unmetered call may run.\n *\n * Checks the subject's window first, then the workspace ceiling when a\n * workspace is supplied; the tighter of the two is what the allowance reports.\n * Every failure path returns a refusal — none returns an allowance.\n */\nexport async function checkFreeRouteLimit(input: FreeRouteLimitInput): Promise<FreeRouteLimitOutcome> {\n const budget = input.budget ?? FREE_ROUTE_BUDGETS[input.costClass ?? 'compute']\n assertBudget(budget, `Free-route budget for '${input.route}'`)\n const workspaceBudget =\n input.workspaceBudget ?? { limit: budget.limit * WORKSPACE_BUDGET_MULTIPLIER, windowSeconds: budget.windowSeconds }\n if (input.workspace) assertBudget(workspaceBudget, `Free-route workspace budget for '${input.route}'`)\n\n const subject = input.subject?.trim()\n if (!subject) {\n return {\n succeeded: false,\n error: new FreeRouteLimitError({\n reason: 'unidentified',\n status: 401,\n // Not correctable by waiting: the caller must authenticate.\n retryAfterSeconds: 0,\n dimension: null,\n route: input.route,\n message: `Route '${input.route}' is authenticated-only — an unidentified caller cannot be rate limited, so it is refused`,\n }),\n }\n }\n\n const subjectWindow = await runWindow(input.kv, input.route, 'subject', subject, budget)\n if (!subjectWindow.succeeded) return subjectWindow\n if (!subjectWindow.value.allowed) {\n return { succeeded: false, error: rateLimited(input.route, 'subject', subjectWindow.value) }\n }\n\n let tightest: { result: RateLimitResult; budget: RateLimitBudget; dimension: Exclude<FreeRouteDimension, null> } = {\n result: subjectWindow.value,\n budget,\n dimension: 'subject',\n }\n\n if (input.workspace) {\n const workspaceWindow = await runWindow(input.kv, input.route, 'workspace', input.workspace, workspaceBudget)\n if (!workspaceWindow.succeeded) return workspaceWindow\n if (!workspaceWindow.value.allowed) {\n return { succeeded: false, error: rateLimited(input.route, 'workspace', workspaceWindow.value) }\n }\n if (workspaceWindow.value.remaining < tightest.result.remaining) {\n tightest = { result: workspaceWindow.value, budget: workspaceBudget, dimension: 'workspace' }\n }\n }\n\n return {\n succeeded: true,\n value: {\n remaining: tightest.result.remaining,\n resetAt: tightest.result.resetAt,\n budget: tightest.budget,\n dimension: tightest.dimension,\n },\n }\n}\n\nfunction rateLimited(\n route: string,\n dimension: Exclude<FreeRouteDimension, null>,\n result: RateLimitResult,\n): FreeRouteLimitError {\n const retryAfterSeconds = secondsUntil(result.resetAt)\n return new FreeRouteLimitError({\n reason: 'rate-limited',\n status: 429,\n retryAfterSeconds,\n dimension,\n route,\n message: `Route '${route}' rate limit reached for this ${dimension}; retry in ${retryAfterSeconds}s`,\n })\n}\n\nexport interface FreeRouteLimitResponseOptions {\n /** Extra response headers (a product's security headers, CORS). */\n headers?: Record<string, string>\n /** Client-facing message. Defaults to the error's own message. */\n message?: string\n}\n\n/** The refusal as an HTTP response: the error's status, a JSON body carrying\n * the machine-readable `reason`, and `Retry-After` when waiting can fix it. */\nexport function freeRouteLimitResponse(\n error: FreeRouteLimitError,\n options: FreeRouteLimitResponseOptions = {},\n): Response {\n const headers = new Headers(options.headers)\n headers.set('Content-Type', 'application/json')\n if (error.retryAfterSeconds > 0) headers.set('Retry-After', String(error.retryAfterSeconds))\n return new Response(\n JSON.stringify({\n error: options.message ?? error.message,\n reason: error.reason,\n retryAfterSeconds: error.retryAfterSeconds,\n }),\n { status: error.status, headers },\n )\n}\n\n/** The authenticated caller behind one request. */\nexport interface FreeRouteIdentity {\n subject: string\n workspace?: string | null\n}\n\nexport interface WithFreeRouteLimitOptions<TArgs> {\n /** Stable route name; part of the key. */\n route: string\n costClass?: FreeRouteClass\n budget?: RateLimitBudget\n workspaceBudget?: RateLimitBudget\n /** The KV binding for this request. */\n kv: (args: TArgs) => KvLike\n /**\n * Resolve the authenticated caller. Return `null` for an unauthenticated\n * request — the wrapper then refuses with 401 and the handler never runs.\n *\n * A THROW here propagates: an auth lookup that failed is not the same as an\n * absent session, and the wrapper will not invent a status for it. The\n * handler still does not run, so the failure stays closed.\n */\n identify: (args: TArgs) => Promise<FreeRouteIdentity | null> | FreeRouteIdentity | null\n /** Build the refusal response. Defaults to {@link freeRouteLimitResponse}. */\n onDenied?: (error: FreeRouteLimitError, args: TArgs) => Response | Promise<Response>\n}\n\n/**\n * Wrap an authenticated, unmetered, compute-bearing route handler in its\n * budget. The limit is applied BEFORE the handler is entered — including\n * before the request body is read — so a refused call costs a KV read, not the\n * compute the route exists to do.\n *\n * The handler's own response is returned untouched: this never rewrites status,\n * body or headers on the success path, so it cannot break a streaming or\n * pre-built response.\n *\n * `TArgs` is the router's handler-argument object (react-router's\n * `ActionFunctionArgs`, or any single-object shape a product uses).\n */\nexport function withFreeRouteLimit<TArgs>(\n options: WithFreeRouteLimitOptions<TArgs>,\n handler: (args: TArgs) => Response | Promise<Response>,\n): (args: TArgs) => Promise<Response> {\n return async (args: TArgs): Promise<Response> => {\n const identity = await options.identify(args)\n const outcome = await checkFreeRouteLimit({\n kv: options.kv(args),\n route: options.route,\n subject: identity?.subject,\n workspace: identity?.workspace,\n costClass: options.costClass,\n budget: options.budget,\n workspaceBudget: options.workspaceBudget,\n })\n if (!outcome.succeeded) {\n return options.onDenied ? await options.onDenied(outcome.error, args) : freeRouteLimitResponse(outcome.error)\n }\n return await handler(args)\n }\n}\n","/**\n * Web-boundary utilities every agent app's routes hand-roll: JSON body parsing\n * + narrowing, request-context extraction (real client IP behind Cloudflare),\n * a KV-backed sliding-window rate limiter, the free-route budget policy built\n * on it, and security response headers. Pure mechanism — no DB, no domain. The\n * KV is a structural interface so this needs no `@cloudflare/workers-types`\n * dependency.\n */\n\nexport * from './rate-limit'\nexport * from './free-route-limit'\n\nexport type JsonObject = Record<string, unknown>\n\n/** Parse + object-narrow a Request body. `[body, null]` on success, `[null,\n * errorResponse]` on a non-object body (callers `if (err) return err`). */\nexport async function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]> {\n let raw: unknown\n try {\n raw = await request.json()\n } catch {\n return [null, Response.json({ error: 'Invalid JSON body' }, { status: 400 })]\n }\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n return [null, Response.json({ error: 'Body must be a JSON object' }, { status: 400 })]\n }\n return [raw as JsonObject, null]\n}\n\n/** Narrow one required string field, 400 if missing/empty. */\nexport function requireString(body: JsonObject, field: string): string | Response {\n const v = body[field]\n if (typeof v !== 'string' || v.length === 0) {\n return Response.json({ error: `Missing or non-string field: ${field}` }, { status: 400 })\n }\n return v\n}\n\n/** Define the context of a request including IP address, user agent, timestamp, and request ID */\nexport interface RequestContext {\n ipAddress: string\n userAgent: string\n timestamp: string\n requestId: string\n}\n\n/** Extract request context for audit trails. Uses `CF-Connecting-IP` for the\n * real client IP behind Cloudflare. */\nexport function extractRequestContext(request: Request): RequestContext {\n const ipAddress =\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() ??\n '0.0.0.0'\n return {\n ipAddress,\n userAgent: request.headers.get('User-Agent') ?? '',\n timestamp: new Date().toISOString(),\n requestId: crypto.randomUUID(),\n }\n}\n\n/** Define options for configuring cookie attributes and behavior */\nexport interface CookieOptions {\n name: string\n /** Default '/'. */\n path?: string\n /** Default true. */\n httpOnly?: boolean\n /** Adds the `Secure` attribute. Default false. */\n secure?: boolean\n /** Default 'Lax'. */\n sameSite?: 'Lax' | 'Strict' | 'None'\n maxAgeSeconds?: number\n}\n\n/** Serialize a Set-Cookie header value: `name=encodeURIComponent(value)` plus\n * attributes in Path / HttpOnly / SameSite / Max-Age / Secure order.\n * Throws on `SameSite=None` without `secure` — browsers silently drop that\n * combination, which would otherwise fail invisibly. */\nexport function serializeCookie(value: string, opts: CookieOptions): string {\n if (opts.sameSite === 'None' && !opts.secure) {\n throw new Error('SameSite=None cookies require secure: true (browsers reject them otherwise)')\n }\n const parts = [`${opts.name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? '/'}`]\n if (opts.httpOnly !== false) parts.push('HttpOnly')\n parts.push(`SameSite=${opts.sameSite ?? 'Lax'}`)\n if (opts.maxAgeSeconds !== undefined) parts.push(`Max-Age=${opts.maxAgeSeconds}`)\n if (opts.secure) parts.push('Secure')\n return parts.join('; ')\n}\n\n/** Set-Cookie header value that deletes the cookie (empty value, Max-Age=0). */\nexport function clearCookieHeader(opts: Omit<CookieOptions, 'maxAgeSeconds'>): string {\n return serializeCookie('', { ...opts, maxAgeSeconds: 0 })\n}\n\n/** Read + decode one cookie from a Cookie request header; null when absent. */\nexport function readCookieValue(cookieHeader: string | null, name: string): string | null {\n if (!cookieHeader) return null\n for (const part of cookieHeader.split(/;\\s*/)) {\n const [cookieName, ...rest] = part.split('=')\n if (cookieName === name) {\n try {\n return decodeURIComponent(rest.join('='))\n } catch {\n return null\n }\n }\n }\n return null\n}\n\n/** Define options for configuring security-related HTTP headers including disclaimers and retention labels */\nexport interface SecurityHeaderOptions {\n /** Product disclaimer (e.g. \"AI-powered tool. Not legal advice.\"). Omitted if absent. */\n disclaimer?: string\n /** Data-retention label (e.g. \"7-years\"). Omitted if absent. */\n retention?: string\n /** Extra headers to set. */\n extra?: Record<string, string>\n}\n\n/** Canonical generic response headers used by {@link addSecurityHeaders}.\n * Exported so static-asset hosts can apply the same policy without copying\n * values that silently drift from Worker/API responses. */\nexport const STANDARD_SECURITY_HEADERS = Object.freeze({\n 'Strict-Transport-Security':\n 'max-age=31536000; includeSubDomains; preload',\n 'X-Content-Type-Options': 'nosniff',\n 'X-Frame-Options': 'SAMEORIGIN',\n 'Referrer-Policy': 'same-origin',\n 'X-XSS-Protection': '1; mode=block',\n} as const)\n\n/** Set standard security headers on a response (HSTS, nosniff, frame-options,\n * referrer-policy, XSS) + optional product disclaimer/retention. The security\n * set is generic; the disclaimer/retention are the product's. */\nexport function addSecurityHeaders(response: Response, opts: SecurityHeaderOptions = {}): Response {\n for (const [name, value] of Object.entries(STANDARD_SECURITY_HEADERS)) {\n response.headers.set(name, value)\n }\n if (opts.disclaimer) response.headers.set('X-AI-Disclaimer', opts.disclaimer)\n if (opts.retention) response.headers.set('X-Data-Retention', opts.retention)\n for (const [k, v] of Object.entries(opts.extra ?? {})) response.headers.set(k, v)\n return response\n}\n\n/** Local-sandbox / inline schemes a stored media reference must never use.\n * Reachable from neither a browser nor the product worker, and a `file:`/`data:`\n * url is the tell of an agent substituting local ffmpeg output for a real\n * provider artifact. `blob:` and `javascript:` are inert/active client schemes\n * with no server reachability. */\nconst REJECTED_MEDIA_SCHEMES = ['file:', 'data:', 'blob:', 'javascript:', 'vbscript:'] as const\n\n/**\n * Canonical media-reference boundary shared by every surface that persists a\n * media url (sequences clips, design-canvas image/video src). The ONE rule:\n * remote `http(s)` or a rooted `/api/` path are allowed; everything else is\n * rejected, with a named reason for known-bad local/inline schemes so the\n * thrown message is actionable for an LLM planner. The url is trimmed before\n * the scheme check so leading whitespace cannot smuggle a rejected scheme past\n * a naive `startsWith`.\n *\n * @param what - noun for the error message (e.g. 'media url', 'src').\n */\nexport function assertMediaUrl(url: string, what = 'media url'): void {\n const trimmed = url.trim()\n if (/^https?:\\/\\//i.test(trimmed)) return\n if (trimmed.startsWith('/api/')) return\n const shown = trimmed.length > 96 ? `${trimmed.slice(0, 96)}…` : trimmed\n const lower = trimmed.toLowerCase()\n if (\n REJECTED_MEDIA_SCHEMES.some((scheme) => lower.startsWith(scheme)) ||\n lower.startsWith('/tmp/') ||\n lower.startsWith('/home/')\n ) {\n throw new Error(`${what} must reference a provider http(s) URL or a rooted /api/ path, not a local sandbox file (${shown})`)\n }\n throw new Error(`${what} must be http(s) or a rooted /api/ path (${shown})`)\n}\n\nexport { isWorkspaceFileExportable } from './file-export'\n","/** Renderer-neutral attribution. Not a transcript merger or an execution state machine. */\nexport interface ConversationGroupItem {\n id?: string | number\n kind?: string\n role?: string\n /** A different assistant/persona must never inherit the previous speaker's label. */\n speakerId?: string\n /** Distinct threads must not be grouped if their rows share a viewport. */\n conversationId?: string\n}\nexport type GroupedConversationItem<T> = T & { isContinuation?: boolean; groupId?: string }\n\n/**\n * Annotate assistant rows until an actual user message, speaker or conversation change.\n * Tool/progress notices do not interrupt the group. The first visible assistant is\n * always labeled, including when a renderer has paged earlier history away.\n * Pass display order; stored content, IDs, timestamps and input objects are untouched.\n */\nexport function groupConversationMessages<T extends ConversationGroupItem>(items: readonly T[] = []): GroupedConversationItem<T>[] {\n let speaker: string | null = null\n let conversation: string | undefined\n let groupId: string | undefined\n return items.map((item, index) => {\n if (item.conversationId !== undefined && item.conversationId !== conversation) {\n conversation = item.conversationId\n speaker = null\n groupId = undefined\n }\n const role = item.kind === 'thinking' ? 'assistant' : (!item.kind || item.kind === 'message' ? item.role : undefined)\n if (role === 'user') { speaker = null; groupId = undefined; return { ...item, isContinuation: false } }\n if (role !== 'assistant') return item\n const current = item.speakerId ?? 'assistant'\n const isContinuation = speaker === current\n if (!isContinuation) groupId = String(item.id ?? `assistant-${index}`)\n speaker = current\n return { ...item, isContinuation, groupId }\n })\n}\n"],"mappings":"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","names":[]}
@@ -13,13 +13,13 @@ import {
13
13
  setPageGuidesCommand,
14
14
  setPagePropsCommand,
15
15
  ungroupElementCommand
16
- } from "./chunk-G4CPEJNN.js";
16
+ } from "./chunk-AKMQFCDL.js";
17
17
  import {
18
18
  SIZE_PRESETS,
19
19
  findElement,
20
20
  matchPreset,
21
21
  requirePage
22
- } from "./chunk-A2XMTCER.js";
22
+ } from "./chunk-WYHFHTSQ.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-54KJ54SY.js.map
2264
+ //# sourceMappingURL=chunk-JAT6SOPK.js.map
@@ -4,7 +4,7 @@ import {
4
4
  DesignCanvas,
5
5
  PlusGlyph,
6
6
  ShapesGlyph
7
- } from "./chunk-54KJ54SY.js";
7
+ } from "./chunk-JAT6SOPK.js";
8
8
  import {
9
9
  lightTheme
10
10
  } from "./chunk-Q2QKEV6J.js";
@@ -20,7 +20,7 @@ import {
20
20
  marqueeSelect,
21
21
  resolveExportParams,
22
22
  resolveNodeCachePixelRatio
23
- } from "./chunk-QEPLXYOE.js";
23
+ } from "./chunk-VHMZ3KUH.js";
24
24
  import {
25
25
  addElementCommand,
26
26
  createSceneCommandStack,
@@ -29,13 +29,13 @@ import {
29
29
  multiSetAttrsCommand,
30
30
  setAttrsCommand,
31
31
  ungroupElementCommand
32
- } from "./chunk-G4CPEJNN.js";
32
+ } from "./chunk-AKMQFCDL.js";
33
33
  import {
34
34
  SCENE_SCHEMA_VERSION,
35
35
  boundsIntersect,
36
36
  elementAabb,
37
37
  findElement
38
- } from "./chunk-A2XMTCER.js";
38
+ } from "./chunk-WYHFHTSQ.js";
39
39
 
40
40
  // src/design-canvas-react/components/Workspace.tsx
41
41
  import {
@@ -1948,4 +1948,4 @@ export {
1948
1948
  Workspace,
1949
1949
  DesignCanvasEditor
1950
1950
  };
1951
- //# sourceMappingURL=chunk-HUYTN7RI.js.map
1951
+ //# sourceMappingURL=chunk-JF423ASS.js.map
@@ -1,14 +1,14 @@
1
1
  // src/design-canvas-react/lazy.tsx
2
2
  import { lazy } from "react";
3
3
  var DesignCanvasLazy = lazy(
4
- () => import("./DesignCanvasEditor-GVJO5SMH.js").then((m) => ({ default: m.DesignCanvasEditor }))
4
+ () => import("./DesignCanvasEditor-UZ2S6BDD.js").then((m) => ({ default: m.DesignCanvasEditor }))
5
5
  );
6
6
  var DesignCanvasChromeLazy = lazy(
7
- () => import("./DesignCanvas-UPRRA4YG.js").then((m) => ({ default: m.DesignCanvas }))
7
+ () => import("./DesignCanvas-MWEYD6PR.js").then((m) => ({ default: m.DesignCanvas }))
8
8
  );
9
9
 
10
10
  export {
11
11
  DesignCanvasLazy,
12
12
  DesignCanvasChromeLazy
13
13
  };
14
- //# sourceMappingURL=chunk-WOZSOPUZ.js.map
14
+ //# sourceMappingURL=chunk-KX2GA3L6.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-ZSSCVT6H.js";
4
4
  import {
5
5
  parseJsonObjectBody
6
- } from "./chunk-3YUI7WNW.js";
6
+ } from "./chunk-IAY2DMKW.js";
7
7
  import {
8
8
  mentionInputToPart,
9
9
  toChatMessageParts
@@ -1046,4 +1046,4 @@ export {
1046
1046
  createChatTurnRoutes,
1047
1047
  streamChatRouteAsSandboxEvents
1048
1048
  };
1049
- //# sourceMappingURL=chunk-42NBWR4U.js.map
1049
+ //# sourceMappingURL=chunk-SYWEMKZR.js.map
@@ -4,7 +4,7 @@ import {
4
4
  boundsIntersect,
5
5
  elementAabb,
6
6
  scaleForPreset
7
- } from "./chunk-A2XMTCER.js";
7
+ } from "./chunk-WYHFHTSQ.js";
8
8
 
9
9
  // src/design-canvas-react/engine/snap.ts
10
10
  var KIND_PRIORITY = {
@@ -447,4 +447,4 @@ export {
447
447
  buildInsertImageOp,
448
448
  DEFAULT_INSERT_TEMPLATES
449
449
  };
450
- //# sourceMappingURL=chunk-QEPLXYOE.js.map
450
+ //# sourceMappingURL=chunk-VHMZ3KUH.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  assertMediaUrl
3
- } from "./chunk-3YUI7WNW.js";
3
+ } from "./chunk-IAY2DMKW.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-A2XMTCER.js.map
1004
+ //# sourceMappingURL=chunk-WYHFHTSQ.js.map
@@ -31,12 +31,12 @@ import {
31
31
  validateSceneOperation,
32
32
  validateSceneOperations,
33
33
  validateSlotValue
34
- } from "../chunk-A2XMTCER.js";
34
+ } from "../chunk-WYHFHTSQ.js";
35
35
  import {
36
36
  buildScopedMcpServerEntry,
37
37
  createMcpToolHandler
38
38
  } from "../chunk-6A7MYOUI.js";
39
- import "../chunk-3YUI7WNW.js";
39
+ import "../chunk-IAY2DMKW.js";
40
40
  import "../chunk-TXD5HXLE.js";
41
41
 
42
42
  // src/design-canvas/operations.ts
@@ -20,7 +20,7 @@ import {
20
20
  nudgeDelta,
21
21
  resolveExportParams,
22
22
  resolveNodeCachePixelRatio
23
- } from "../chunk-QEPLXYOE.js";
23
+ } from "../chunk-VHMZ3KUH.js";
24
24
  import {
25
25
  SCENE_COMMAND_HISTORY_LIMIT,
26
26
  addElementCommand,
@@ -39,12 +39,12 @@ import {
39
39
  setPageGuidesCommand,
40
40
  setPagePropsCommand,
41
41
  ungroupElementCommand
42
- } from "../chunk-G4CPEJNN.js";
42
+ } from "../chunk-AKMQFCDL.js";
43
43
  import {
44
44
  bleedAwareExportBounds,
45
45
  scaleForPreset
46
- } from "../chunk-A2XMTCER.js";
47
- import "../chunk-3YUI7WNW.js";
46
+ } from "../chunk-WYHFHTSQ.js";
47
+ import "../chunk-IAY2DMKW.js";
48
48
  import "../chunk-TXD5HXLE.js";
49
49
  export {
50
50
  DEFAULT_INSERT_TEMPLATES,
@@ -12,7 +12,7 @@ import {
12
12
  downloadDataUrl,
13
13
  exportDocumentJson,
14
14
  exportPageDataUrl
15
- } from "../chunk-HUYTN7RI.js";
15
+ } from "../chunk-JF423ASS.js";
16
16
  import {
17
17
  BTN,
18
18
  BTN_ACTIVE,
@@ -41,7 +41,7 @@ import {
41
41
  buildRulerTicks,
42
42
  formatRulerLabel,
43
43
  selectTickStep
44
- } from "../chunk-54KJ54SY.js";
44
+ } from "../chunk-JAT6SOPK.js";
45
45
  import "../chunk-Q2QKEV6J.js";
46
46
  import "../chunk-ZN5J47UX.js";
47
47
  import {
@@ -65,7 +65,7 @@ import {
65
65
  nudgeDelta,
66
66
  resolveExportParams,
67
67
  resolveNodeCachePixelRatio
68
- } from "../chunk-QEPLXYOE.js";
68
+ } from "../chunk-VHMZ3KUH.js";
69
69
  import {
70
70
  SCENE_COMMAND_HISTORY_LIMIT,
71
71
  addElementCommand,
@@ -84,17 +84,17 @@ import {
84
84
  setPageGuidesCommand,
85
85
  setPagePropsCommand,
86
86
  ungroupElementCommand
87
- } from "../chunk-G4CPEJNN.js";
87
+ } from "../chunk-AKMQFCDL.js";
88
88
  import {
89
89
  assertSceneMediaSrc,
90
90
  bleedAwareExportBounds,
91
91
  scaleForPreset
92
- } from "../chunk-A2XMTCER.js";
92
+ } from "../chunk-WYHFHTSQ.js";
93
93
  import {
94
94
  DesignCanvasChromeLazy,
95
95
  DesignCanvasLazy
96
- } from "../chunk-WOZSOPUZ.js";
97
- import "../chunk-3YUI7WNW.js";
96
+ } from "../chunk-KX2GA3L6.js";
97
+ import "../chunk-IAY2DMKW.js";
98
98
  import "../chunk-TXD5HXLE.js";
99
99
 
100
100
  // src/design-canvas-react/components/CanvasInsertPanel.tsx
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DesignCanvasChromeLazy,
3
3
  DesignCanvasLazy
4
- } from "../chunk-WOZSOPUZ.js";
4
+ } from "../chunk-KX2GA3L6.js";
5
5
  export {
6
6
  DesignCanvasChromeLazy,
7
7
  DesignCanvasLazy
@@ -13,12 +13,12 @@ import {
13
13
  resolveTangleSsoAccount,
14
14
  signSessionCookieValue,
15
15
  verifySignedSsoState
16
- } from "../chunk-TXM4SS46.js";
16
+ } from "../chunk-CSMJM7YT.js";
17
17
  import {
18
18
  resolveTangleDevOrUserKey,
19
19
  resolveTangleExecutionEnvironment
20
20
  } from "../chunk-JML7WKWU.js";
21
- import "../chunk-3YUI7WNW.js";
21
+ import "../chunk-IAY2DMKW.js";
22
22
  import "../chunk-TXD5HXLE.js";
23
23
 
24
24
  // src/platform/hub.ts
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createChatTurnRoutes,
3
3
  streamChatRouteAsSandboxEvents
4
- } from "../chunk-42NBWR4U.js";
4
+ } from "../chunk-SYWEMKZR.js";
5
5
  import "../chunk-ZSSCVT6H.js";
6
- import "../chunk-3YUI7WNW.js";
6
+ import "../chunk-IAY2DMKW.js";
7
7
  import "../chunk-4PZE7XAM.js";
8
8
  import "../chunk-ZVEEWGDK.js";
9
9
  import "../chunk-X47R2IVO.js";
@@ -18,7 +18,7 @@ import {
18
18
  } from "../chunk-6A7MYOUI.js";
19
19
  import {
20
20
  assertMediaUrl
21
- } from "../chunk-3YUI7WNW.js";
21
+ } from "../chunk-IAY2DMKW.js";
22
22
  import "../chunk-TXD5HXLE.js";
23
23
 
24
24
  // src/sequences/operations.ts
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Web-boundary utilities every agent app's routes hand-roll: JSON body parsing
3
+ * + narrowing, request-context extraction (real client IP behind Cloudflare),
4
+ * a KV-backed sliding-window rate limiter, the free-route budget policy built
5
+ * on it, and security response headers. Pure mechanism — no DB, no domain. The
6
+ * KV is a structural interface so this needs no `@cloudflare/workers-types`
7
+ * dependency.
8
+ */
9
+ export * from './rate-limit';
10
+ export * from './free-route-limit';
11
+ export type JsonObject = Record<string, unknown>;
12
+ /** Parse + object-narrow a Request body. `[body, null]` on success, `[null,
13
+ * errorResponse]` on a non-object body (callers `if (err) return err`). */
14
+ export declare function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]>;
15
+ /** Narrow one required string field, 400 if missing/empty. */
16
+ export declare function requireString(body: JsonObject, field: string): string | Response;
17
+ /** Define the context of a request including IP address, user agent, timestamp, and request ID */
18
+ export interface RequestContext {
19
+ ipAddress: string;
20
+ userAgent: string;
21
+ timestamp: string;
22
+ requestId: string;
23
+ }
24
+ /** Extract request context for audit trails. Uses `CF-Connecting-IP` for the
25
+ * real client IP behind Cloudflare. */
26
+ export declare function extractRequestContext(request: Request): RequestContext;
27
+ /** Define options for configuring cookie attributes and behavior */
28
+ export interface CookieOptions {
29
+ name: string;
30
+ /** Default '/'. */
31
+ path?: string;
32
+ /** Default true. */
33
+ httpOnly?: boolean;
34
+ /** Adds the `Secure` attribute. Default false. */
35
+ secure?: boolean;
36
+ /** Default 'Lax'. */
37
+ sameSite?: 'Lax' | 'Strict' | 'None';
38
+ maxAgeSeconds?: number;
39
+ }
40
+ /** Serialize a Set-Cookie header value: `name=encodeURIComponent(value)` plus
41
+ * attributes in Path / HttpOnly / SameSite / Max-Age / Secure order.
42
+ * Throws on `SameSite=None` without `secure` — browsers silently drop that
43
+ * combination, which would otherwise fail invisibly. */
44
+ export declare function serializeCookie(value: string, opts: CookieOptions): string;
45
+ /** Set-Cookie header value that deletes the cookie (empty value, Max-Age=0). */
46
+ export declare function clearCookieHeader(opts: Omit<CookieOptions, 'maxAgeSeconds'>): string;
47
+ /** Read + decode one cookie from a Cookie request header; null when absent. */
48
+ export declare function readCookieValue(cookieHeader: string | null, name: string): string | null;
49
+ /** Define options for configuring security-related HTTP headers including disclaimers and retention labels */
50
+ export interface SecurityHeaderOptions {
51
+ /** Product disclaimer (e.g. "AI-powered tool. Not legal advice."). Omitted if absent. */
52
+ disclaimer?: string;
53
+ /** Data-retention label (e.g. "7-years"). Omitted if absent. */
54
+ retention?: string;
55
+ /** Extra headers to set. */
56
+ extra?: Record<string, string>;
57
+ }
58
+ /** Canonical generic response headers used by {@link addSecurityHeaders}.
59
+ * Exported so static-asset hosts can apply the same policy without copying
60
+ * values that silently drift from Worker/API responses. */
61
+ export declare const STANDARD_SECURITY_HEADERS: Readonly<{
62
+ readonly 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload';
63
+ readonly 'X-Content-Type-Options': 'nosniff';
64
+ readonly 'X-Frame-Options': 'SAMEORIGIN';
65
+ readonly 'Referrer-Policy': 'same-origin';
66
+ readonly 'X-XSS-Protection': '1; mode=block';
67
+ }>;
68
+ /** Set standard security headers on a response (HSTS, nosniff, frame-options,
69
+ * referrer-policy, XSS) + optional product disclaimer/retention. The security
70
+ * set is generic; the disclaimer/retention are the product's. */
71
+ export declare function addSecurityHeaders(response: Response, opts?: SecurityHeaderOptions): Response;
72
+ /**
73
+ * Canonical media-reference boundary shared by every surface that persists a
74
+ * media url (sequences clips, design-canvas image/video src). The ONE rule:
75
+ * remote `http(s)` or a rooted `/api/` path are allowed; everything else is
76
+ * rejected, with a named reason for known-bad local/inline schemes so the
77
+ * thrown message is actionable for an LLM planner. The url is trimmed before
78
+ * the scheme check so leading whitespace cannot smuggle a rejected scheme past
79
+ * a naive `startsWith`.
80
+ *
81
+ * @param what - noun for the error message (e.g. 'media url', 'src').
82
+ */
83
+ export declare function assertMediaUrl(url: string, what?: string): void;
84
+ export { isWorkspaceFileExportable } from './file-export';
@@ -1,84 +1,3 @@
1
- /**
2
- * Web-boundary utilities every agent app's routes hand-roll: JSON body parsing
3
- * + narrowing, request-context extraction (real client IP behind Cloudflare),
4
- * a KV-backed sliding-window rate limiter, the free-route budget policy built
5
- * on it, and security response headers. Pure mechanism — no DB, no domain. The
6
- * KV is a structural interface so this needs no `@cloudflare/workers-types`
7
- * dependency.
8
- */
9
- export * from './rate-limit';
10
- export * from './free-route-limit';
11
- export type JsonObject = Record<string, unknown>;
12
- /** Parse + object-narrow a Request body. `[body, null]` on success, `[null,
13
- * errorResponse]` on a non-object body (callers `if (err) return err`). */
14
- export declare function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]>;
15
- /** Narrow one required string field, 400 if missing/empty. */
16
- export declare function requireString(body: JsonObject, field: string): string | Response;
17
- /** Define the context of a request including IP address, user agent, timestamp, and request ID */
18
- export interface RequestContext {
19
- ipAddress: string;
20
- userAgent: string;
21
- timestamp: string;
22
- requestId: string;
23
- }
24
- /** Extract request context for audit trails. Uses `CF-Connecting-IP` for the
25
- * real client IP behind Cloudflare. */
26
- export declare function extractRequestContext(request: Request): RequestContext;
27
- /** Define options for configuring cookie attributes and behavior */
28
- export interface CookieOptions {
29
- name: string;
30
- /** Default '/'. */
31
- path?: string;
32
- /** Default true. */
33
- httpOnly?: boolean;
34
- /** Adds the `Secure` attribute. Default false. */
35
- secure?: boolean;
36
- /** Default 'Lax'. */
37
- sameSite?: 'Lax' | 'Strict' | 'None';
38
- maxAgeSeconds?: number;
39
- }
40
- /** Serialize a Set-Cookie header value: `name=encodeURIComponent(value)` plus
41
- * attributes in Path / HttpOnly / SameSite / Max-Age / Secure order.
42
- * Throws on `SameSite=None` without `secure` — browsers silently drop that
43
- * combination, which would otherwise fail invisibly. */
44
- export declare function serializeCookie(value: string, opts: CookieOptions): string;
45
- /** Set-Cookie header value that deletes the cookie (empty value, Max-Age=0). */
46
- export declare function clearCookieHeader(opts: Omit<CookieOptions, 'maxAgeSeconds'>): string;
47
- /** Read + decode one cookie from a Cookie request header; null when absent. */
48
- export declare function readCookieValue(cookieHeader: string | null, name: string): string | null;
49
- /** Define options for configuring security-related HTTP headers including disclaimers and retention labels */
50
- export interface SecurityHeaderOptions {
51
- /** Product disclaimer (e.g. "AI-powered tool. Not legal advice."). Omitted if absent. */
52
- disclaimer?: string;
53
- /** Data-retention label (e.g. "7-years"). Omitted if absent. */
54
- retention?: string;
55
- /** Extra headers to set. */
56
- extra?: Record<string, string>;
57
- }
58
- /** Canonical generic response headers used by {@link addSecurityHeaders}.
59
- * Exported so static-asset hosts can apply the same policy without copying
60
- * values that silently drift from Worker/API responses. */
61
- export declare const STANDARD_SECURITY_HEADERS: Readonly<{
62
- readonly 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload';
63
- readonly 'X-Content-Type-Options': 'nosniff';
64
- readonly 'X-Frame-Options': 'SAMEORIGIN';
65
- readonly 'Referrer-Policy': 'same-origin';
66
- readonly 'X-XSS-Protection': '1; mode=block';
67
- }>;
68
- /** Set standard security headers on a response (HSTS, nosniff, frame-options,
69
- * referrer-policy, XSS) + optional product disclaimer/retention. The security
70
- * set is generic; the disclaimer/retention are the product's. */
71
- export declare function addSecurityHeaders(response: Response, opts?: SecurityHeaderOptions): Response;
72
- /**
73
- * Canonical media-reference boundary shared by every surface that persists a
74
- * media url (sequences clips, design-canvas image/video src). The ONE rule:
75
- * remote `http(s)` or a rooted `/api/` path are allowed; everything else is
76
- * rejected, with a named reason for known-bad local/inline schemes so the
77
- * thrown message is actionable for an LLM planner. The url is trimmed before
78
- * the scheme check so leading whitespace cannot smuggle a rejected scheme past
79
- * a naive `startsWith`.
80
- *
81
- * @param what - noun for the error message (e.g. 'media url', 'src').
82
- */
83
- export declare function assertMediaUrl(url: string, what?: string): void;
84
- export { isWorkspaceFileExportable } from './file-export';
1
+ /** Browser-safe application-boundary helpers. No React or execution-engine peers. */
2
+ export * from './core';
3
+ export * from './message-groups';
package/dist/web/index.js CHANGED
@@ -10,12 +10,13 @@ import {
10
10
  clearCookieHeader,
11
11
  extractRequestContext,
12
12
  freeRouteLimitResponse,
13
+ groupConversationMessages,
13
14
  parseJsonObjectBody,
14
15
  readCookieValue,
15
16
  requireString,
16
17
  serializeCookie,
17
18
  withFreeRouteLimit
18
- } from "../chunk-3YUI7WNW.js";
19
+ } from "../chunk-IAY2DMKW.js";
19
20
  import {
20
21
  isWorkspaceFileExportable
21
22
  } from "../chunk-TXD5HXLE.js";
@@ -31,6 +32,7 @@ export {
31
32
  clearCookieHeader,
32
33
  extractRequestContext,
33
34
  freeRouteLimitResponse,
35
+ groupConversationMessages,
34
36
  isWorkspaceFileExportable,
35
37
  parseJsonObjectBody,
36
38
  readCookieValue,
@@ -0,0 +1,21 @@
1
+ /** Renderer-neutral attribution. Not a transcript merger or an execution state machine. */
2
+ export interface ConversationGroupItem {
3
+ id?: string | number;
4
+ kind?: string;
5
+ role?: string;
6
+ /** A different assistant/persona must never inherit the previous speaker's label. */
7
+ speakerId?: string;
8
+ /** Distinct threads must not be grouped if their rows share a viewport. */
9
+ conversationId?: string;
10
+ }
11
+ export type GroupedConversationItem<T> = T & {
12
+ isContinuation?: boolean;
13
+ groupId?: string;
14
+ };
15
+ /**
16
+ * Annotate assistant rows until an actual user message, speaker or conversation change.
17
+ * Tool/progress notices do not interrupt the group. The first visible assistant is
18
+ * always labeled, including when a renderer has paged earlier history away.
19
+ * Pass display order; stored content, IDs, timestamps and input objects are untouched.
20
+ */
21
+ export declare function groupConversationMessages<T extends ConversationGroupItem>(items?: readonly T[]): GroupedConversationItem<T>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.47.24",
3
+ "version": "0.47.25",
4
4
  "packageManager": "pnpm@11.24.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [
@@ -1,11 +0,0 @@
1
- import {
2
- DesignCanvas
3
- } from "./chunk-54KJ54SY.js";
4
- import "./chunk-G4CPEJNN.js";
5
- import "./chunk-A2XMTCER.js";
6
- import "./chunk-3YUI7WNW.js";
7
- import "./chunk-TXD5HXLE.js";
8
- export {
9
- DesignCanvas
10
- };
11
- //# sourceMappingURL=DesignCanvas-UPRRA4YG.js.map
@@ -1,14 +0,0 @@
1
- import {
2
- DesignCanvasEditor
3
- } from "./chunk-HUYTN7RI.js";
4
- import "./chunk-54KJ54SY.js";
5
- import "./chunk-Q2QKEV6J.js";
6
- import "./chunk-QEPLXYOE.js";
7
- import "./chunk-G4CPEJNN.js";
8
- import "./chunk-A2XMTCER.js";
9
- import "./chunk-3YUI7WNW.js";
10
- import "./chunk-TXD5HXLE.js";
11
- export {
12
- DesignCanvasEditor
13
- };
14
- //# sourceMappingURL=DesignCanvasEditor-GVJO5SMH.js.map
@@ -1 +0,0 @@
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":[]}