@tangle-network/agent-app 0.43.38 → 0.43.40

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.
@@ -6,19 +6,6 @@ import {
6
6
  parseChatTurnParts,
7
7
  promptPartsByteSize
8
8
  } from "../chunk-5SV5PSU7.js";
9
- import {
10
- createInteractionAnswerRoute
11
- } from "../chunk-Y4QHNQ75.js";
12
- import {
13
- parseJsonObjectBody
14
- } from "../chunk-HFC4BTWJ.js";
15
- import {
16
- isRenderableInteractionKind,
17
- parseInteractionRequest
18
- } from "../chunk-4TXDD6P2.js";
19
- import {
20
- toChatMessageParts
21
- } from "../chunk-I2R2XT4M.js";
22
9
  import {
23
10
  asRecord,
24
11
  asString,
@@ -33,6 +20,19 @@ import {
33
20
  replayTurnEvents,
34
21
  resolveChatTurn
35
22
  } from "../chunk-XYWCGFII.js";
23
+ import {
24
+ createInteractionAnswerRoute
25
+ } from "../chunk-Y4QHNQ75.js";
26
+ import {
27
+ parseJsonObjectBody
28
+ } from "../chunk-HFC4BTWJ.js";
29
+ import {
30
+ isRenderableInteractionKind,
31
+ parseInteractionRequest
32
+ } from "../chunk-4TXDD6P2.js";
33
+ import {
34
+ toChatMessageParts
35
+ } from "../chunk-I2R2XT4M.js";
36
36
 
37
37
  // src/chat-routes/turn-routes.ts
38
38
  import { deriveExecutionId, handleChatTurn } from "@tangle-network/agent-runtime";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  assertHarnessModelCompatible
3
3
  } from "./chunk-E7QYOOON.js";
4
+ import {
5
+ buildAppToolMcpServer
6
+ } from "./chunk-7EVZUIHW.js";
4
7
  import {
5
8
  base64UrlDecodeText,
6
9
  base64UrlEncodeText,
7
10
  constantTimeEqual,
8
11
  hmacSha256Base64Url
9
- } from "./chunk-3II3AWHY.js";
10
- import {
11
- buildAppToolMcpServer
12
- } from "./chunk-7EVZUIHW.js";
12
+ } from "./chunk-S5SRJJQG.js";
13
13
  import {
14
14
  resolveTangleExecutionEnvironment,
15
15
  trimOrNull
@@ -1547,4 +1547,4 @@ export {
1547
1547
  isTerminalPromptEvent,
1548
1548
  detectInteractiveQuestion
1549
1549
  };
1550
- //# sourceMappingURL=chunk-H4WCEZE3.js.map
1550
+ //# sourceMappingURL=chunk-5GWXCSLQ.js.map
@@ -1,40 +1,17 @@
1
1
  import {
2
2
  authenticateToolRequest
3
3
  } from "./chunk-7EVZUIHW.js";
4
+ import {
5
+ base64UrlDecodeText,
6
+ base64UrlEncodeText,
7
+ constantTimeEqual,
8
+ hmacSha256Base64Url
9
+ } from "./chunk-S5SRJJQG.js";
4
10
  import {
5
11
  dispatchAppTool,
6
12
  outcomeStatus
7
13
  } from "./chunk-MFRCM32T.js";
8
14
 
9
- // src/crypto/web-token.ts
10
- function base64UrlEncode(bytes) {
11
- let bin = "";
12
- for (const b of bytes) bin += String.fromCharCode(b);
13
- return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
14
- }
15
- function base64UrlEncodeText(text) {
16
- return base64UrlEncode(new TextEncoder().encode(text));
17
- }
18
- function base64UrlDecodeText(value) {
19
- const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
20
- const bin = atob(padded);
21
- const bytes = new Uint8Array(bin.length);
22
- for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i);
23
- return new TextDecoder().decode(bytes);
24
- }
25
- async function hmacSha256Base64Url(message, secret) {
26
- const enc = new TextEncoder();
27
- const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
28
- const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
29
- return base64UrlEncode(new Uint8Array(sig));
30
- }
31
- function constantTimeEqual(a, b) {
32
- if (a.length !== b.length) return false;
33
- let diff = 0;
34
- for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
35
- return diff === 0;
36
- }
37
-
38
15
  // src/tools/capability.ts
39
16
  async function createCapabilityToken(userId, opts) {
40
17
  const secret = opts.secret?.trim();
@@ -141,10 +118,6 @@ async function handleAppToolRequest(request, opts) {
141
118
  }
142
119
 
143
120
  export {
144
- base64UrlEncodeText,
145
- base64UrlDecodeText,
146
- hmacSha256Base64Url,
147
- constantTimeEqual,
148
121
  createCapabilityToken,
149
122
  verifyCapabilityToken,
150
123
  createExpiringCapabilityToken,
@@ -153,4 +126,4 @@ export {
153
126
  restrictTaxonomy,
154
127
  handleAppToolRequest
155
128
  };
156
- //# sourceMappingURL=chunk-3II3AWHY.js.map
129
+ //# sourceMappingURL=chunk-PJC4NXPA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools/capability.ts","../src/tools/gating.ts","../src/tools/http.ts"],"sourcesContent":["/**\n * Per-user capability token — the sandbox→app auth primitive behind the\n * `verifyToken` seam in {@link authenticateToolRequest}.\n *\n * An app-agent runs inside the sandbox and reaches the host app back over HTTP\n * (the app tools, the integration-invoke bridge). The route must act AS the\n * connecting user without trusting any model-supplied identity, so the turn\n * mints a short HMAC token bound to the user id and bakes it into the per-turn\n * MCP server header; the route verifies it to recover the user.\n *\n * `HMAC-SHA256(secret, \"user:<userId>\")`, base64url, with an app-chosen prefix.\n * The token encodes no scopes — the hub's policy engine authorizes per action.\n * Fail-closed: with no secret, no token is minted (the caller MUST omit the MCP\n * server rather than fake an authorized call). WebCrypto only — runs on\n * Workers, Node, and the browser with no Node `crypto` dependency.\n */\n\nimport {\n base64UrlDecodeText,\n base64UrlEncodeText,\n constantTimeEqual,\n hmacSha256Base64Url,\n} from '../crypto/web-token'\n\nexport interface CapabilityTokenOptions {\n /** Shared HMAC secret. When absent, mint returns undefined / verify returns false. */\n secret?: string\n /** Token prefix (namespaces the credential; lets verify reject foreign tokens\n * cheaply). Default `cap_`. */\n prefix?: string\n}\n\n/** Mint a capability token for `userId`, or `undefined` when no secret is\n * configured (fail-closed — the caller omits the MCP server rather than fake it). */\nexport async function createCapabilityToken(userId: string, opts: CapabilityTokenOptions): Promise<string | undefined> {\n const secret = opts.secret?.trim()\n if (!secret) return undefined\n const prefix = opts.prefix ?? 'cap_'\n return `${prefix}${await sign(userId, secret)}`\n}\n\n/** Verify a capability token against `userId`. Returns false (never throws) for\n * an unconfigured secret, a wrong prefix, a malformed token, or a mismatch. */\nexport async function verifyCapabilityToken(userId: string, token: string, opts: CapabilityTokenOptions): Promise<boolean> {\n const secret = opts.secret?.trim()\n const prefix = opts.prefix ?? 'cap_'\n if (!secret || !token.startsWith(prefix)) return false\n const expected = `${prefix}${await sign(userId, secret)}`\n return constantTimeEqual(token, expected)\n}\n\nexport interface ExpiringCapabilityTokenOptions extends CapabilityTokenOptions {\n /** Token lifetime. Expired tokens verify false regardless of signature. */\n expiresInMs: number\n /** Clock injection for tests; defaults to Date.now. */\n now?: () => number\n}\n\n/**\n * Mint an EXPIRING capability token: `<prefix><base64url(payload)>.<sig>` where\n * the payload carries `{ sub, exp, n }` (subject, epoch-ms expiry, random\n * nonce) and the signature is HMAC-SHA256 over the encoded payload. Use this\n * for user-initiated scoped channels (e.g. a per-sequence MCP endpoint) where\n * a captured token must not stay valid past its window; the bare\n * {@link createCapabilityToken} remains for turn-scoped tool bridges whose\n * mint+verify happen inside one request cycle. Fail-closed like the bare\n * variant: no secret → no token.\n */\nexport async function createExpiringCapabilityToken(subject: string, opts: ExpiringCapabilityTokenOptions): Promise<string | undefined> {\n const secret = opts.secret?.trim()\n if (!secret) return undefined\n if (!Number.isFinite(opts.expiresInMs) || opts.expiresInMs <= 0) throw new Error('expiresInMs must be a positive number')\n const prefix = opts.prefix ?? 'cap_'\n const now = opts.now ?? Date.now\n const payload = base64UrlEncodeText(JSON.stringify({ sub: subject, exp: now() + opts.expiresInMs, n: crypto.randomUUID() }))\n return `${prefix}${payload}.${await hmacSha256Base64Url(payload, secret)}`\n}\n\n/** Verify an expiring token against `subject`: prefix, payload integrity,\n * subject match, and expiry all checked; returns false (never throws) on any\n * failure including a malformed payload. */\nexport async function verifyExpiringCapabilityToken(subject: string, token: string, opts: CapabilityTokenOptions & { now?: () => number }): Promise<boolean> {\n const secret = opts.secret?.trim()\n const prefix = opts.prefix ?? 'cap_'\n if (!secret || !token.startsWith(prefix)) return false\n const body = token.slice(prefix.length)\n const dot = body.lastIndexOf('.')\n if (dot <= 0 || dot === body.length - 1) return false\n const payload = body.slice(0, dot)\n const sig = body.slice(dot + 1)\n if (!constantTimeEqual(sig, await hmacSha256Base64Url(payload, secret))) return false\n let parsed: { sub?: unknown; exp?: unknown }\n try {\n parsed = JSON.parse(base64UrlDecodeText(payload)) as { sub?: unknown; exp?: unknown }\n } catch {\n return false\n }\n if (parsed.sub !== subject) return false\n if (typeof parsed.exp !== 'number') return false\n const now = opts.now ?? Date.now\n return parsed.exp > now()\n}\n\nasync function sign(userId: string, secret: string): Promise<string> {\n return hmacSha256Base64Url(`user:${userId}`, secret)\n}\n","/**\n * Capability gating — compose an agent session's tool surface from a\n * product-defined capability registry.\n *\n * Products that let users pick what an agent can do (a \"Studio\" agent with the\n * full build toolset vs a clean \"Assistant\" with none) all need the same\n * mechanics: a registry of named capabilities, each unlocking proposal types\n * and/or named tool groups, resolved against the product's\n * {@link AppToolTaxonomy} into the concrete tools to expose. The mechanics are\n * generic and live here; the capability VOCABULARY (ids, labels, which\n * proposal types, which tool groups) is the product's.\n */\n\nimport type { AppToolTaxonomy } from './types'\n\n/** One toggleable tool group in a product's capability registry. */\nexport interface ToolCapability {\n id: string\n label: string\n description: string\n /** Proposal types this capability unlocks (intersected with the taxonomy,\n * so a capability can never widen the product's proposal surface). */\n proposalTypes?: readonly string[]\n /** Unlocks every taxonomy proposal type beyond the base set — the domain's\n * specialized long tail (risk_assessment, vuln_report, …). */\n domainActions?: boolean\n /** Named product tool groups (e.g. 'sandbox', 'integrations') this\n * capability unlocks. The vocabulary is the product's; the resolver only\n * unions them. */\n toolGroups?: readonly string[]\n}\n\nexport interface ResolveToolCapabilitiesOptions {\n taxonomy: AppToolTaxonomy\n /** The product's full capability registry. */\n capabilities: readonly ToolCapability[]\n /** Enabled capability ids. `undefined` means full access (legacy callers\n * that don't send a capability set); an explicit `[]` means a pure chat\n * agent with no tools. Unknown ids are ignored. */\n enabled: readonly string[] | undefined\n /** The shared base proposal types `domainActions` excludes. Defaults to\n * every type some capability names explicitly via `proposalTypes` — i.e.\n * \"domain actions\" are the taxonomy types no capability claims. */\n baseProposalTypes?: readonly string[]\n}\n\nexport interface ResolvedToolCapabilities {\n /** Proposal types to keep — feed to {@link restrictTaxonomy}. */\n proposalTypes: string[]\n /** Product tool groups to expose (deduped union across enabled caps). */\n toolGroups: string[]\n}\n\n/**\n * Resolve an enabled capability-id set against a taxonomy into the concrete\n * tool surface. Fail-closed: only types present in the taxonomy survive, and\n * an empty `enabled` set yields no tools at all.\n */\nexport function resolveToolCapabilities(\n opts: ResolveToolCapabilitiesOptions,\n): ResolvedToolCapabilities {\n const { taxonomy, capabilities, enabled } = opts\n if (enabled === undefined) {\n return {\n proposalTypes: [...taxonomy.proposalTypes],\n toolGroups: [...new Set(capabilities.flatMap((c) => c.toolGroups ?? []))],\n }\n }\n const base = new Set(\n opts.baseProposalTypes ?? capabilities.flatMap((c) => c.proposalTypes ?? []),\n )\n const domainTypes = taxonomy.proposalTypes.filter((t) => !base.has(t))\n const byId = new Map(capabilities.map((c) => [c.id, c]))\n\n const proposalTypes = new Set<string>()\n const toolGroups = new Set<string>()\n for (const id of enabled) {\n const cap = byId.get(id)\n if (!cap) continue\n for (const t of cap.proposalTypes ?? []) {\n if (taxonomy.proposalTypes.includes(t)) proposalTypes.add(t)\n }\n if (cap.domainActions) for (const t of domainTypes) proposalTypes.add(t)\n for (const g of cap.toolGroups ?? []) toolGroups.add(g)\n }\n return { proposalTypes: [...proposalTypes], toolGroups: [...toolGroups] }\n}\n\n/**\n * Restrict a taxonomy to a subset of proposal types, intersecting the\n * regulated subset too — the regulated label survives restriction, so a\n * narrowed agent can never launder a regulated type into an unregulated one.\n */\nexport function restrictTaxonomy(\n taxonomy: AppToolTaxonomy,\n allowed: readonly string[],\n): AppToolTaxonomy {\n const allow = new Set(allowed)\n return {\n proposalTypes: taxonomy.proposalTypes.filter((t) => allow.has(t)),\n regulatedTypes: taxonomy.regulatedTypes.filter((t) => allow.has(t)),\n }\n}\n","import { authenticateToolRequest, type ToolHeaderNames } from './auth'\nimport { dispatchAppTool, outcomeStatus, type DispatchOptions } from './dispatch'\nimport type { AppToolName } from './openai'\nimport type { AppToolDefinition } from './registry'\n\nexport interface HandleToolRequestOptions extends DispatchOptions {\n /** Which app tool this route serves — a built-in name or a product-registered\n * {@link AppToolDefinition} (auto-added to `customTools` for dispatch). */\n tool: AppToolName | AppToolDefinition\n /** Verify the bearer capability token belongs to the header user. */\n verifyToken: (userId: string, bearer: string) => Promise<boolean>\n headerNames?: ToolHeaderNames\n /** Optional success-message builder for a friendlier tool result. */\n message?: (result: unknown) => string\n}\n\n/**\n * Handle one app-tool HTTP request end to end — the sandbox MCP path. The\n * agent's per-turn HTTP MCP server POSTs here; this authenticates (header user\n * + capability token), reads the args (MCP-alias tolerant), dispatches to the\n * product handler, and returns a JSON Response. A product's route file becomes\n * a one-liner: `export const action = ({ request }) => handleAppToolRequest(request, cfg)`.\n */\nexport async function handleAppToolRequest(request: Request, opts: HandleToolRequestOptions): Promise<Response> {\n if (request.method !== 'POST') return Response.json({ error: 'Method not allowed' }, { status: 405 })\n\n const auth = await authenticateToolRequest(request, { verifyToken: opts.verifyToken, headerNames: opts.headerNames })\n if (!auth.ok) return auth.response\n\n let body: { args?: Record<string, unknown>; arguments?: Record<string, unknown> } & Record<string, unknown>\n try {\n body = (await request.json()) as typeof body\n } catch {\n return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n const args = (body.args ?? body.arguments ?? body) as Record<string, unknown>\n\n // A custom tool passed as `tool` is registered for this dispatch so the route\n // file stays a one-liner (no separate customTools wiring needed).\n const toolName = typeof opts.tool === 'string' ? opts.tool : opts.tool.name\n const customTools =\n typeof opts.tool === 'string' ? opts.customTools : [...(opts.customTools ?? []), opts.tool]\n const outcome = await dispatchAppTool(toolName, args, auth.ctx, { ...opts, customTools })\n if (!outcome.ok) {\n return Response.json({ error: outcome.code, message: outcome.message }, { status: outcomeStatus(outcome) })\n }\n const payload = outcome.result as Record<string, unknown>\n return Response.json({ ok: true, ...payload, ...(opts.message ? { message: opts.message(outcome.result) } : {}) })\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkCA,eAAsB,sBAAsB,QAAgB,MAA2D;AACrH,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,KAAK,UAAU;AAC9B,SAAO,GAAG,MAAM,GAAG,MAAM,KAAK,QAAQ,MAAM,CAAC;AAC/C;AAIA,eAAsB,sBAAsB,QAAgB,OAAe,MAAgD;AACzH,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,QAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,UAAU,CAAC,MAAM,WAAW,MAAM,EAAG,QAAO;AACjD,QAAM,WAAW,GAAG,MAAM,GAAG,MAAM,KAAK,QAAQ,MAAM,CAAC;AACvD,SAAO,kBAAkB,OAAO,QAAQ;AAC1C;AAmBA,eAAsB,8BAA8B,SAAiB,MAAmE;AACtI,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,CAAC,OAAO,SAAS,KAAK,WAAW,KAAK,KAAK,eAAe,EAAG,OAAM,IAAI,MAAM,uCAAuC;AACxH,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,QAAM,UAAU,oBAAoB,KAAK,UAAU,EAAE,KAAK,SAAS,KAAK,IAAI,IAAI,KAAK,aAAa,GAAG,OAAO,WAAW,EAAE,CAAC,CAAC;AAC3H,SAAO,GAAG,MAAM,GAAG,OAAO,IAAI,MAAM,oBAAoB,SAAS,MAAM,CAAC;AAC1E;AAKA,eAAsB,8BAA8B,SAAiB,OAAe,MAAyE;AAC3J,QAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,QAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,UAAU,CAAC,MAAM,WAAW,MAAM,EAAG,QAAO;AACjD,QAAM,OAAO,MAAM,MAAM,OAAO,MAAM;AACtC,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,MAAI,OAAO,KAAK,QAAQ,KAAK,SAAS,EAAG,QAAO;AAChD,QAAM,UAAU,KAAK,MAAM,GAAG,GAAG;AACjC,QAAM,MAAM,KAAK,MAAM,MAAM,CAAC;AAC9B,MAAI,CAAC,kBAAkB,KAAK,MAAM,oBAAoB,SAAS,MAAM,CAAC,EAAG,QAAO;AAChF,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,oBAAoB,OAAO,CAAC;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,QAAS,QAAO;AACnC,MAAI,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC3C,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,SAAO,OAAO,MAAM,IAAI;AAC1B;AAEA,eAAe,KAAK,QAAgB,QAAiC;AACnE,SAAO,oBAAoB,QAAQ,MAAM,IAAI,MAAM;AACrD;;;AC/CO,SAAS,wBACd,MAC0B;AAC1B,QAAM,EAAE,UAAU,cAAc,QAAQ,IAAI;AAC5C,MAAI,YAAY,QAAW;AACzB,WAAO;AAAA,MACL,eAAe,CAAC,GAAG,SAAS,aAAa;AAAA,MACzC,YAAY,CAAC,GAAG,IAAI,IAAI,aAAa,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;AAAA,IAC1E;AAAA,EACF;AACA,QAAM,OAAO,IAAI;AAAA,IACf,KAAK,qBAAqB,aAAa,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAAA,EAC7E;AACA,QAAM,cAAc,SAAS,cAAc,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;AACrE,QAAM,OAAO,IAAI,IAAI,aAAa,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAEvD,QAAM,gBAAgB,oBAAI,IAAY;AACtC,QAAM,aAAa,oBAAI,IAAY;AACnC,aAAW,MAAM,SAAS;AACxB,UAAM,MAAM,KAAK,IAAI,EAAE;AACvB,QAAI,CAAC,IAAK;AACV,eAAW,KAAK,IAAI,iBAAiB,CAAC,GAAG;AACvC,UAAI,SAAS,cAAc,SAAS,CAAC,EAAG,eAAc,IAAI,CAAC;AAAA,IAC7D;AACA,QAAI,IAAI,cAAe,YAAW,KAAK,YAAa,eAAc,IAAI,CAAC;AACvE,eAAW,KAAK,IAAI,cAAc,CAAC,EAAG,YAAW,IAAI,CAAC;AAAA,EACxD;AACA,SAAO,EAAE,eAAe,CAAC,GAAG,aAAa,GAAG,YAAY,CAAC,GAAG,UAAU,EAAE;AAC1E;AAOO,SAAS,iBACd,UACA,SACiB;AACjB,QAAM,QAAQ,IAAI,IAAI,OAAO;AAC7B,SAAO;AAAA,IACL,eAAe,SAAS,cAAc,OAAO,CAAC,MAAM,MAAM,IAAI,CAAC,CAAC;AAAA,IAChE,gBAAgB,SAAS,eAAe,OAAO,CAAC,MAAM,MAAM,IAAI,CAAC,CAAC;AAAA,EACpE;AACF;;;AC/EA,eAAsB,qBAAqB,SAAkB,MAAmD;AAC9G,MAAI,QAAQ,WAAW,OAAQ,QAAO,SAAS,KAAK,EAAE,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAEpG,QAAM,OAAO,MAAM,wBAAwB,SAAS,EAAE,aAAa,KAAK,aAAa,aAAa,KAAK,YAAY,CAAC;AACpH,MAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAE1B,MAAI;AACJ,MAAI;AACF,WAAQ,MAAM,QAAQ,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO,SAAS,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjE;AACA,QAAM,OAAQ,KAAK,QAAQ,KAAK,aAAa;AAI7C,QAAM,WAAW,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,KAAK,KAAK;AACvE,QAAM,cACJ,OAAO,KAAK,SAAS,WAAW,KAAK,cAAc,CAAC,GAAI,KAAK,eAAe,CAAC,GAAI,KAAK,IAAI;AAC5F,QAAM,UAAU,MAAM,gBAAgB,UAAU,MAAM,KAAK,KAAK,EAAE,GAAG,MAAM,YAAY,CAAC;AACxF,MAAI,CAAC,QAAQ,IAAI;AACf,WAAO,SAAS,KAAK,EAAE,OAAO,QAAQ,MAAM,SAAS,QAAQ,QAAQ,GAAG,EAAE,QAAQ,cAAc,OAAO,EAAE,CAAC;AAAA,EAC5G;AACA,QAAM,UAAU,QAAQ;AACxB,SAAO,SAAS,KAAK,EAAE,IAAI,MAAM,GAAG,SAAS,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,QAAQ,MAAM,EAAE,IAAI,CAAC,EAAG,CAAC;AACnH;","names":[]}
@@ -0,0 +1,36 @@
1
+ // src/crypto/web-token.ts
2
+ function base64UrlEncode(bytes) {
3
+ let bin = "";
4
+ for (const b of bytes) bin += String.fromCharCode(b);
5
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
+ }
7
+ function base64UrlEncodeText(text) {
8
+ return base64UrlEncode(new TextEncoder().encode(text));
9
+ }
10
+ function base64UrlDecodeText(value) {
11
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
12
+ const bin = atob(padded);
13
+ const bytes = new Uint8Array(bin.length);
14
+ for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i);
15
+ return new TextDecoder().decode(bytes);
16
+ }
17
+ async function hmacSha256Base64Url(message, secret) {
18
+ const enc = new TextEncoder();
19
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
20
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
21
+ return base64UrlEncode(new Uint8Array(sig));
22
+ }
23
+ function constantTimeEqual(a, b) {
24
+ if (a.length !== b.length) return false;
25
+ let diff = 0;
26
+ for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
27
+ return diff === 0;
28
+ }
29
+
30
+ export {
31
+ base64UrlEncodeText,
32
+ base64UrlDecodeText,
33
+ hmacSha256Base64Url,
34
+ constantTimeEqual
35
+ };
36
+ //# sourceMappingURL=chunk-S5SRJJQG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/crypto/web-token.ts"],"sourcesContent":["/**\n * Dependency-free WebCrypto primitives for HMAC-signed, base64url-encoded\n * tokens — base64url encode/decode, HMAC-SHA256, and a constant-time compare.\n * Runs on Cloudflare Workers, Node, and the browser with no Node `crypto`\n * dependency. Shared by the sandbox terminal-proxy token, the WS-upgrade token\n * parser, and the app-tool capability token so the logic lives in one place\n * rather than three near-identical private copies.\n *\n * Internal leaf: not exported from the `/crypto` barrel (that subpath is the\n * AES-GCM field-crypto surface); imported directly by the modules that need it.\n */\n\n/** base64url-encode raw bytes (RFC 4648 §5, no padding). */\nexport function base64UrlEncode(bytes: Uint8Array): string {\n let bin = ''\n for (const b of bytes) bin += String.fromCharCode(b)\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** base64url-encode a UTF-8 string. */\nexport function base64UrlEncodeText(text: string): string {\n return base64UrlEncode(new TextEncoder().encode(text))\n}\n\n/** Decode a base64url string back to its UTF-8 text. Re-pads before `atob` so\n * unpadded input decodes correctly regardless of the runtime's leniency. */\nexport function base64UrlDecodeText(value: string): string {\n const padded = value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=')\n const bin = atob(padded)\n const bytes = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i)\n return new TextDecoder().decode(bytes)\n}\n\n/** HMAC-SHA256 `message` under `secret`, returned base64url-encoded. */\nexport async function hmacSha256Base64Url(message: string, secret: string): Promise<string> {\n const enc = new TextEncoder()\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message))\n return base64UrlEncode(new Uint8Array(sig))\n}\n\n/** Length-independent-leak-free compare of two same-charset strings. */\nexport function constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n"],"mappings":";AAaO,SAAS,gBAAgB,OAA2B;AACzD,MAAI,MAAM;AACV,aAAW,KAAK,MAAO,QAAO,OAAO,aAAa,CAAC;AACnD,SAAO,KAAK,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC5E;AAGO,SAAS,oBAAoB,MAAsB;AACxD,SAAO,gBAAgB,IAAI,YAAY,EAAE,OAAO,IAAI,CAAC;AACvD;AAIO,SAAS,oBAAoB,OAAuB;AACzD,QAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,EAAE,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC,IAAI,GAAG,GAAG;AACtG,QAAM,MAAM,KAAK,MAAM;AACvB,QAAM,QAAQ,IAAI,WAAW,IAAI,MAAM;AACvC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,EAAG,OAAM,CAAC,IAAI,IAAI,WAAW,CAAC;AACnE,SAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AACvC;AAGA,eAAsB,oBAAoB,SAAiB,QAAiC;AAC1F,QAAM,MAAM,IAAI,YAAY;AAC5B,QAAM,MAAM,MAAM,OAAO,OAAO,UAAU,OAAO,IAAI,OAAO,MAAM,GAAG,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;AACvH,QAAM,MAAM,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,OAAO,CAAC;AACrE,SAAO,gBAAgB,IAAI,WAAW,GAAG,CAAC;AAC5C;AAGO,SAAS,kBAAkB,GAAW,GAAoB;AAC/D,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,EAAG,SAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;AAC9E,SAAO,SAAS;AAClB;","names":[]}
@@ -0,0 +1,124 @@
1
+ import {
2
+ constantTimeEqual,
3
+ hmacSha256Base64Url
4
+ } from "./chunk-S5SRJJQG.js";
5
+
6
+ // src/object-store/index.ts
7
+ function createR2ObjectStore({ bucket }) {
8
+ return {
9
+ async put(key, body, opts) {
10
+ const options = opts?.contentType ? { httpMetadata: { contentType: opts.contentType } } : void 0;
11
+ await bucket.put(key, body, options);
12
+ },
13
+ async get(key) {
14
+ const obj = await bucket.get(key);
15
+ if (!obj) return null;
16
+ return {
17
+ stream: () => obj.body,
18
+ size: obj.size,
19
+ contentType: obj.httpMetadata?.contentType
20
+ };
21
+ },
22
+ async head(key) {
23
+ const obj = await bucket.head(key);
24
+ if (!obj) return null;
25
+ return { size: obj.size, contentType: obj.httpMetadata?.contentType };
26
+ },
27
+ async delete(key) {
28
+ await bucket.delete(key);
29
+ }
30
+ };
31
+ }
32
+ function assertSafeKeySegment(s) {
33
+ if (s.length === 0) throw new Error("object-store: empty key segment");
34
+ if (s.includes("..")) throw new Error(`object-store: unsafe key segment (contains "..") \u2014 ${s}`);
35
+ if (s.startsWith("/")) throw new Error(`object-store: unsafe key segment (leading "/") \u2014 ${s}`);
36
+ if (s.includes("\\")) throw new Error(`object-store: unsafe key segment (backslash) \u2014 ${s}`);
37
+ return s;
38
+ }
39
+ function sanitizeFilename(filename) {
40
+ const leaf = filename.split(/[/\\]/).pop() ?? "";
41
+ const cleaned = leaf.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+/, "");
42
+ return cleaned.length > 0 ? cleaned : "file";
43
+ }
44
+ function objectKey({ operatorId, customerId, uploadId, filename }) {
45
+ const operator = assertSafeKeySegment(operatorId);
46
+ const customer = customerId == null ? "_unattributed" : assertSafeKeySegment(customerId);
47
+ const upload = assertSafeKeySegment(uploadId);
48
+ return `${operator}/${customer}/${upload}-${sanitizeFilename(filename)}`;
49
+ }
50
+ function canonicalizeObjectKey(raw) {
51
+ let decoded;
52
+ try {
53
+ decoded = decodeURIComponent(raw);
54
+ } catch {
55
+ throw new Error("object-store: malformed key encoding");
56
+ }
57
+ for (const segment of decoded.split("/")) assertSafeKeySegment(segment);
58
+ return decoded;
59
+ }
60
+ function signingMessage(canonicalKey, exp) {
61
+ return JSON.stringify({ v: 1, exp, key: canonicalKey });
62
+ }
63
+ async function signObjectUrl({ key, exp, secret }) {
64
+ if (!secret) throw new Error("object-store: signObjectUrl requires a non-empty secret (fail-closed)");
65
+ const canonical = canonicalizeObjectKey(key);
66
+ const sig = await hmacSha256Base64Url(signingMessage(canonical, exp), secret);
67
+ const params = new URLSearchParams({ key: canonical, exp: String(exp), sig });
68
+ return `?${params.toString()}`;
69
+ }
70
+ async function verifyObjectUrl(request, { secret }) {
71
+ if (!secret) return { ok: false };
72
+ const url = new URL(request.url);
73
+ const rawKey = url.searchParams.get("key");
74
+ const expRaw = url.searchParams.get("exp");
75
+ const sig = url.searchParams.get("sig");
76
+ if (!rawKey || !expRaw || !sig) return { ok: false };
77
+ let key;
78
+ try {
79
+ key = canonicalizeObjectKey(rawKey);
80
+ } catch {
81
+ return { ok: false };
82
+ }
83
+ const exp = Number(expRaw);
84
+ if (!Number.isFinite(exp)) return { ok: false };
85
+ const expected = await hmacSha256Base64Url(signingMessage(key, exp), secret);
86
+ if (!constantTimeEqual(expected, sig)) return { ok: false };
87
+ if (Date.now() > exp) return { ok: false };
88
+ return { ok: true, key };
89
+ }
90
+ function createProxiedArtifactRoute({
91
+ store,
92
+ secret
93
+ }) {
94
+ return async (request) => {
95
+ const rawKey = new URL(request.url).searchParams.get("key");
96
+ if (!rawKey) return new Response("Missing key", { status: 400 });
97
+ try {
98
+ canonicalizeObjectKey(rawKey);
99
+ } catch {
100
+ return new Response("Malformed key", { status: 400 });
101
+ }
102
+ const verified = await verifyObjectUrl(request, { secret });
103
+ if (!verified.ok) return new Response("Forbidden", { status: 403 });
104
+ const obj = await store.get(verified.key);
105
+ if (!obj) return new Response("Not found", { status: 404 });
106
+ const headers = {
107
+ "Content-Type": "application/octet-stream",
108
+ "Content-Disposition": "attachment",
109
+ "X-Content-Type-Options": "nosniff"
110
+ };
111
+ if (Number.isFinite(obj.size) && obj.size >= 0) headers["Content-Length"] = String(obj.size);
112
+ return new Response(obj.stream(), { status: 200, headers });
113
+ };
114
+ }
115
+
116
+ export {
117
+ createR2ObjectStore,
118
+ assertSafeKeySegment,
119
+ objectKey,
120
+ signObjectUrl,
121
+ verifyObjectUrl,
122
+ createProxiedArtifactRoute
123
+ };
124
+ //# sourceMappingURL=chunk-UMSOSUEU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/object-store/index.ts"],"sourcesContent":["/**\n * `/object-store` — a portable, secure object store for durable large\n * attachments (uploaded PDFs, images, exports) that are too big to ride a chat\n * turn body or live in KV.\n *\n * SECURITY POSTURE (this module guards a legal privilege wall). Every object key\n * carries an operator segment and a customer segment, and access is by a\n * short-lived HMAC-signed URL whose signature covers the EXACT key (operator +\n * customer + upload id + filename) AND the expiry. A tampered key, a swapped\n * customer segment, or a replayed-after-expiry URL all fail the signature or the\n * expiry check and are refused. The signing secret is a PARAMETER on every call\n * — this module reads NOTHING global (no `process.env`, no ambient config); if a\n * product forgets to bind the secret, {@link verifyObjectUrl} fails closed and\n * {@link signObjectUrl} throws rather than minting an unsigned URL.\n *\n * PURE MECHANISM behind two seams: an {@link ObjectStore} port (the R2 impl maps\n * 1:1 to a bucket held behind the structural {@link R2LikeBucket} — so\n * `@cloudflare/workers-types` never leaks into this package's public `.d.ts`)\n * and the signing `secret`. Reads and writes STREAM: {@link createR2ObjectStore}\n * returns the R2 object's `.body` stream directly and never calls\n * `.text()`/`.arrayBuffer()`, so a 15 MB PDF flows through the worker without\n * buffering the whole file into the isolate's heap.\n *\n * RECONCILIATION with the sandbox `storage` seam (`../sandbox` →\n * `SandboxRuntimeConfig.storage`): that is BYOS3/R2 *snapshot* storage — it\n * checkpoints and restores a sandbox box's filesystem for durable session\n * execution. This is a DIFFERENT concern: a content-addressed store for\n * user-facing attachments gated by a signed-URL privilege wall. They are not\n * interchangeable and must not be merged: one persists agent runtime state, the\n * other serves per-operator/per-customer documents to browsers. Do not route\n * artifact downloads through the snapshot bucket, or vice versa.\n */\n\nimport { constantTimeEqual, hmacSha256Base64Url } from '../crypto/web-token'\n\n// ── The store port + R2 impl ────────────────────────────────────────────────\n\n/** Options for a single {@link ObjectStore.put}. Both fields are optional; a\n * store that needs a fixed content length (e.g. R2 with a `ReadableStream`\n * body) uses `contentLength` when present. */\nexport interface PutObjectOptions {\n /** MIME type recorded with the object (returned by `get`/`head`). Advisory:\n * the proxied download route always serves `application/octet-stream`. */\n contentType?: string\n /** Byte length of `body`, when known. Some backends require it for a\n * streamed body; the R2 impl passes a known length through when supplied. */\n contentLength?: number\n}\n\n/** A retrieved object. `stream()` is the ONLY way to read the bytes — there is\n * deliberately no `text()`/`arrayBuffer()`, so a large object never buffers\n * into the isolate heap. */\nexport interface ObjectBody {\n /** The object's bytes as a `ReadableStream` (backed by R2's `.body`). */\n stream(): ReadableStream\n /** Size in bytes. */\n size: number\n /** Recorded MIME type, if any. */\n contentType?: string\n}\n\n/**\n * Portable object-store port. `get`/`head` return `null` on a miss and NEVER\n * throw for a missing key (a miss is a normal control-flow outcome, not an\n * error); `delete` is idempotent.\n */\nexport interface ObjectStore {\n put(key: string, body: ReadableStream | Uint8Array, opts?: PutObjectOptions): Promise<void>\n /** `null` on a miss — never throws for a missing key. */\n get(key: string): Promise<ObjectBody | null>\n /** `null` on a miss — never throws for a missing key. */\n head(key: string): Promise<{ size: number; contentType?: string } | null>\n delete(key: string): Promise<void>\n}\n\n/** Head/metadata shape of a stored object (structural match of R2's `R2Object`). */\nexport interface R2LikeObjectHead {\n size: number\n httpMetadata?: { contentType?: string }\n}\n\n/** Body shape of a retrieved object (structural match of R2's `R2ObjectBody`).\n * `body` is the streamed content — the impl reads this, never `.text()`. */\nexport interface R2LikeObjectBody extends R2LikeObjectHead {\n body: ReadableStream\n}\n\n/**\n * The minimal slice of Cloudflare's `R2Bucket` this module calls. A real\n * `R2Bucket` satisfies it structurally, so the consumer passes its binding\n * without this package ever importing `@cloudflare/workers-types` (which would\n * otherwise leak into the public `.d.ts`).\n */\nexport interface R2LikeBucket {\n put(\n key: string,\n value: ReadableStream | Uint8Array | ArrayBuffer,\n options?: { httpMetadata?: { contentType?: string } },\n ): Promise<unknown>\n get(key: string): Promise<R2LikeObjectBody | null>\n head(key: string): Promise<R2LikeObjectHead | null>\n delete(key: string): Promise<void>\n}\n\n/**\n * Map the {@link ObjectStore} port onto an R2 bucket 1:1. `get` STREAMS: it\n * returns the R2 object's `.body` behind `ObjectBody.stream()` and never calls\n * `.text()`/`.arrayBuffer()`, so a large file flows through the worker rather\n * than buffering into the isolate.\n */\nexport function createR2ObjectStore({ bucket }: { bucket: R2LikeBucket }): ObjectStore {\n return {\n async put(key, body, opts) {\n const options = opts?.contentType ? { httpMetadata: { contentType: opts.contentType } } : undefined\n await bucket.put(key, body, options)\n },\n async get(key) {\n const obj = await bucket.get(key)\n if (!obj) return null\n return {\n stream: () => obj.body,\n size: obj.size,\n contentType: obj.httpMetadata?.contentType,\n }\n },\n async head(key) {\n const obj = await bucket.head(key)\n if (!obj) return null\n return { size: obj.size, contentType: obj.httpMetadata?.contentType }\n },\n async delete(key) {\n await bucket.delete(key)\n },\n }\n}\n\n// ── Key construction + safety ───────────────────────────────────────────────\n\n/**\n * Assert a single object-key path SEGMENT (an operator id, customer id, or\n * upload id) is safe to interpolate into a key, and return it. Throws on the\n * traversal / injection shapes: `..` anywhere, a leading `/`, a backslash, or an\n * empty segment. Consumers should call this on caller-supplied identifiers\n * BEFORE any `get`/`put` so an attacker-controlled id can never widen the key\n * beyond its own operator+customer prefix.\n */\nexport function assertSafeKeySegment(s: string): string {\n if (s.length === 0) throw new Error('object-store: empty key segment')\n if (s.includes('..')) throw new Error(`object-store: unsafe key segment (contains \"..\") — ${s}`)\n if (s.startsWith('/')) throw new Error(`object-store: unsafe key segment (leading \"/\") — ${s}`)\n if (s.includes('\\\\')) throw new Error(`object-store: unsafe key segment (backslash) — ${s}`)\n return s\n}\n\n/** Strip a filename down to a safe leaf: drop any path, keep only\n * `[A-Za-z0-9._-]`, and remove leading dots so a `..`/`.hidden`/dot-only name\n * can neither traverse nor vanish. Never returns an empty string. */\nfunction sanitizeFilename(filename: string): string {\n const leaf = filename.split(/[/\\\\]/).pop() ?? ''\n const cleaned = leaf.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^\\.+/, '')\n return cleaned.length > 0 ? cleaned : 'file'\n}\n\n/** Inputs to {@link objectKey}. `customerId` is optional — an unattributed\n * upload lands under the reserved `_unattributed` customer segment. */\nexport interface ObjectKeyParts {\n operatorId: string\n /** Optional — omitted/undefined groups the upload under `_unattributed`. */\n customerId?: string\n uploadId: string\n filename: string\n}\n\n/**\n * Build the canonical object key: `operator/customer/upload-filename`. The\n * operator, customer, and upload segments are asserted safe (throwing on\n * traversal); the filename is sanitized to a safe leaf. The customer segment\n * falls back to `_unattributed` when no customer is attributed. Because a signed\n * URL binds the EXACT key, the operator and customer segments are part of the\n * privilege wall — a caller cannot later swap them without breaking the\n * signature.\n */\nexport function objectKey({ operatorId, customerId, uploadId, filename }: ObjectKeyParts): string {\n const operator = assertSafeKeySegment(operatorId)\n const customer = customerId == null ? '_unattributed' : assertSafeKeySegment(customerId)\n const upload = assertSafeKeySegment(uploadId)\n return `${operator}/${customer}/${upload}-${sanitizeFilename(filename)}`\n}\n\n/**\n * Decode a key ONCE and assert every `/`-delimited segment is safe. Slash- and\n * percent-encoded forms of the same key canonicalize identically (`a/b` and\n * `a%2Fb` both → `a/b`), so a signature binds the key's MEANING, not its\n * on-the-wire spelling. Throws on malformed percent-encoding or an unsafe\n * segment. Our own keys never contain `%`, so the single decode is idempotent.\n */\nfunction canonicalizeObjectKey(raw: string): string {\n let decoded: string\n try {\n decoded = decodeURIComponent(raw)\n } catch {\n throw new Error('object-store: malformed key encoding')\n }\n for (const segment of decoded.split('/')) assertSafeKeySegment(segment)\n return decoded\n}\n\n// ── Signed URLs ─────────────────────────────────────────────────────────────\n\n/** The exact string the signature covers: a versioned JSON of the canonical key\n * and the expiry. JSON escaping makes it delimiter-injection-proof (a key\n * cannot forge the `exp` field). Both sign and verify build it identically. */\nfunction signingMessage(canonicalKey: string, exp: number): string {\n return JSON.stringify({ v: 1, exp, key: canonicalKey })\n}\n\n/** Arguments to {@link signObjectUrl}. */\nexport interface SignObjectUrlArgs {\n /** The exact object key to authorize (from {@link objectKey}). */\n key: string\n /** Absolute expiry in epoch MILLISECONDS (e.g. `Date.now() + 5 * 60_000` for a\n * ~5-minute TTL — the recommended default; keep it short). */\n exp: number\n /** HMAC signing secret. Must be non-empty — signing fails closed otherwise. */\n secret: string\n}\n\n/**\n * Mint a signed query string (`?key=…&exp=…&sig=…`) authorizing a download of\n * `key` until `exp`. The product appends it to its artifact route path\n * (`` `/artifacts${await signObjectUrl(...)}` ``); {@link createProxiedArtifactRoute}\n * / {@link verifyObjectUrl} read it straight off the request URL, so the route's\n * own mount path never matters.\n *\n * FAIL-CLOSED: throws if `secret` is empty (never mints an unsigned URL) and if\n * `key` is unsafe (traversal). Async because the HMAC primitive is WebCrypto.\n *\n * TTL: `exp` is caller-supplied on purpose (the caller knows the sensitivity);\n * keep it SHORT — ~5 minutes is the recommended default for a privilege-walled\n * document.\n */\nexport async function signObjectUrl({ key, exp, secret }: SignObjectUrlArgs): Promise<string> {\n if (!secret) throw new Error('object-store: signObjectUrl requires a non-empty secret (fail-closed)')\n const canonical = canonicalizeObjectKey(key)\n const sig = await hmacSha256Base64Url(signingMessage(canonical, exp), secret)\n const params = new URLSearchParams({ key: canonical, exp: String(exp), sig })\n return `?${params.toString()}`\n}\n\n/** Result of {@link verifyObjectUrl}: the canonical key on success, nothing\n * distinguishing on failure (so a rejection leaks no detail). */\nexport type VerifyObjectUrlResult = { ok: true; key: string } | { ok: false }\n\n/**\n * Verify a signed download request. Reads `key`/`exp`/`sig` off the request URL,\n * canonicalizes the key identically to the signer, recomputes the HMAC and\n * constant-time-compares it, and checks the expiry. Returns the canonical key on\n * success.\n *\n * FAIL-CLOSED everywhere: an empty `secret`, a missing/ malformed parameter, a\n * non-finite expiry, a signature mismatch, or an expired URL all return\n * `{ ok: false }` — and the mismatch path uses a constant-time compare so a\n * near-correct signature is not distinguishable by timing. Async because the\n * HMAC primitive is WebCrypto.\n */\nexport async function verifyObjectUrl(\n request: Request,\n { secret }: { secret: string },\n): Promise<VerifyObjectUrlResult> {\n if (!secret) return { ok: false }\n const url = new URL(request.url)\n const rawKey = url.searchParams.get('key')\n const expRaw = url.searchParams.get('exp')\n const sig = url.searchParams.get('sig')\n if (!rawKey || !expRaw || !sig) return { ok: false }\n\n let key: string\n try {\n key = canonicalizeObjectKey(rawKey)\n } catch {\n return { ok: false }\n }\n\n const exp = Number(expRaw)\n if (!Number.isFinite(exp)) return { ok: false }\n\n const expected = await hmacSha256Base64Url(signingMessage(key, exp), secret)\n if (!constantTimeEqual(expected, sig)) return { ok: false }\n // Signature is valid; enforce expiry last so the constant-time compare always\n // runs (an expired-but-otherwise-valid URL is refused just the same).\n if (Date.now() > exp) return { ok: false }\n return { ok: true, key }\n}\n\n// ── Proxied download route ──────────────────────────────────────────────────\n\n/**\n * Build a download handler that verifies a signed request and STREAMS the object\n * back with a conservative, non-executable content type. Status contract:\n *\n * - `400` — the `key` is missing or malformed (bad encoding / traversal). This\n * is an unauthenticated client error and leaks NOTHING about object existence.\n * - `403` — the signature is missing, wrong, or expired.\n * - `404` — the signature was valid but no object exists at that key (existence\n * is only ever revealed to a validly-signed request).\n * - `200` — streams the bytes with `Content-Disposition: attachment`,\n * `Content-Type: application/octet-stream`, and `X-Content-Type-Options:\n * nosniff`, so the worker never serves active/inline content.\n */\nexport function createProxiedArtifactRoute({\n store,\n secret,\n}: {\n store: ObjectStore\n secret: string\n}): (request: Request) => Promise<Response> {\n return async (request) => {\n // 1. Malformed/missing key → 400, decided BEFORE auth so it leaks no\n // existence signal (a malformed key can never carry a valid signature).\n const rawKey = new URL(request.url).searchParams.get('key')\n if (!rawKey) return new Response('Missing key', { status: 400 })\n try {\n canonicalizeObjectKey(rawKey)\n } catch {\n return new Response('Malformed key', { status: 400 })\n }\n\n // 2. Bad / expired signature → 403.\n const verified = await verifyObjectUrl(request, { secret })\n if (!verified.ok) return new Response('Forbidden', { status: 403 })\n\n // 3. Fetch by the VERIFIED canonical key. Miss → 404 (only reachable behind\n // a valid signature). Hit → stream with a conservative content type.\n const obj = await store.get(verified.key)\n if (!obj) return new Response('Not found', { status: 404 })\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/octet-stream',\n 'Content-Disposition': 'attachment',\n 'X-Content-Type-Options': 'nosniff',\n }\n if (Number.isFinite(obj.size) && obj.size >= 0) headers['Content-Length'] = String(obj.size)\n return new Response(obj.stream(), { status: 200, headers })\n }\n}\n"],"mappings":"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","names":[]}
@@ -42,6 +42,10 @@ import {
42
42
  formatRulerLabel,
43
43
  selectTickStep
44
44
  } from "../chunk-H5FM75MR.js";
45
+ import {
46
+ DesignCanvasChromeLazy,
47
+ DesignCanvasLazy
48
+ } from "../chunk-R4NOKZMD.js";
45
49
  import "../chunk-ZN5J47UX.js";
46
50
  import {
47
51
  DEFAULT_INSERT_TEMPLATES,
@@ -82,10 +86,6 @@ import {
82
86
  setPagePropsCommand,
83
87
  ungroupElementCommand
84
88
  } from "../chunk-XWJXZ6WE.js";
85
- import {
86
- DesignCanvasChromeLazy,
87
- DesignCanvasLazy
88
- } from "../chunk-R4NOKZMD.js";
89
89
  import "../chunk-47C6PJCW.js";
90
90
  import {
91
91
  assertSceneMediaSrc,
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { AgentAppConfig, AgentIdentityConfig, AgentIntegrationsConfig, AgentKnow
15
15
  export { D1Like, D1PreparedLike, DrizzleColumnLike, DrizzleSqliteCoreLike, PRESET_MIGRATION_SQL, PRESET_TABLES, PresetBillingOptions, PresetKnowledgeAccessorOptions, PresetToolHandlerOptions, VaultKv, createD1KnowledgeStateAccessor, createPresetDrizzleSchema, createPresetFieldCrypto, createPresetToolHandlers, createPresetWorkspaceKeyManager, createPresetWorkspaceKeyStore } from './preset-cloudflare/index.js';
16
16
  export { KeyCrypto, KeyProvisioner, PlanLimit, PlatformBalanceInfo, PlatformBalanceManager, PlatformBalanceManagerOptions, PlatformBillingClient, PlatformIdentity, PlatformProductUsage, SharedBillingState, TcloudKeyClient, WorkspaceKeyManager, WorkspaceKeyManagerOptions, WorkspaceKeyRecord, WorkspaceKeyStore, WorkspaceModelKeyUsage, createPlatformBalanceManager, createTcloudKeyProvisioner, createWorkspaceKeyManager } from './billing/index.js';
17
17
  export { HttpHeadProbeConfig, PreflightProbe, PreflightProbeResult, PreflightProbeVerdict, PreflightReport, RouterChatProbeConfig, SandboxAuthProbeConfig, formatPreflightReport, httpHeadProbe, routerChatProbe, runPreflight, sandboxAuthProbe } from './preflight/index.js';
18
+ export { ObjectBody, ObjectKeyParts, ObjectStore, PutObjectOptions, R2LikeBucket, R2LikeObjectBody, R2LikeObjectHead, SignObjectUrlArgs, VerifyObjectUrlResult, assertSafeKeySegment, createProxiedArtifactRoute, createR2ObjectStore, objectKey, signObjectUrl, verifyObjectUrl } from './object-store/index.js';
18
19
  export { B as BULK_DELETE_MAX_THREADS, C as ChatStoreInputError, t as threadTitleFromMessage } from './core-7qIM7svy.js';
19
20
  export { C as ChatFilePart, a as ChatImagePart, b as ChatInteractionPart, c as ChatMessagePart, d as ChatNoticePart, e as ChatPartTime, f as ChatReasoningPart, g as ChatStepFinishPart, h as ChatStepStartPart, i as ChatSubtaskPart, j as ChatTextPart, k as ChatToolPart, l as ChatToolState, m as ChatToolStatus, n as ChatUsageTokens, S as StorableHarnessPartKind, o as isChatInteractionPart, p as isChatStepFinishPart, q as isChatTextPart, r as isChatToolPart, t as toChatMessageParts } from './parts-BcbitSNp.js';
20
21
  export { DeriveKeyOptions, createFieldCrypto, decodeHexKey, decryptAesGcm, decryptBytes, decryptWithKey, deriveKey, encryptAesGcm, encryptBytes, encryptWithKey } from './crypto/index.js';
package/dist/index.js CHANGED
@@ -202,11 +202,45 @@ import {
202
202
  runPreflight,
203
203
  sandboxAuthProbe
204
204
  } from "./chunk-Q4TKVF3L.js";
205
+ import {
206
+ assertSafeKeySegment,
207
+ createProxiedArtifactRoute,
208
+ createR2ObjectStore,
209
+ objectKey,
210
+ signObjectUrl,
211
+ verifyObjectUrl
212
+ } from "./chunk-UMSOSUEU.js";
205
213
  import {
206
214
  BULK_DELETE_MAX_THREADS,
207
215
  ChatStoreInputError,
208
216
  threadTitleFromMessage
209
217
  } from "./chunk-5EQCITY3.js";
218
+ import {
219
+ TURN_EVENTS_MIGRATION_SQL,
220
+ TURN_STATUS_SCOPE_MIGRATION_SQL,
221
+ asRecord,
222
+ asString,
223
+ buildUserTextParts,
224
+ coalesceChatStreamEvents,
225
+ coalesceDeltas,
226
+ createBufferedTurnTap,
227
+ createD1TurnEventStore,
228
+ createMemoryTurnEventStore,
229
+ encodeEvent,
230
+ finalizeAssistantParts,
231
+ getPartKey,
232
+ mergePersistedPart,
233
+ messageHasTurnId,
234
+ normalizeClientTurnId,
235
+ normalizePersistedPart,
236
+ normalizeTime,
237
+ normalizeToolEvent,
238
+ pumpBufferedTurn,
239
+ replayTurnEvents,
240
+ resolveChatTurn,
241
+ resolveToolId,
242
+ resolveToolName
243
+ } from "./chunk-XYWCGFII.js";
210
244
  import {
211
245
  createInteractionAnswerRoute,
212
246
  listSessionInteractions,
@@ -266,32 +300,6 @@ import {
266
300
  encryptBytes,
267
301
  encryptWithKey
268
302
  } from "./chunk-TA5Q4I2K.js";
269
- import {
270
- TURN_EVENTS_MIGRATION_SQL,
271
- TURN_STATUS_SCOPE_MIGRATION_SQL,
272
- asRecord,
273
- asString,
274
- buildUserTextParts,
275
- coalesceChatStreamEvents,
276
- coalesceDeltas,
277
- createBufferedTurnTap,
278
- createD1TurnEventStore,
279
- createMemoryTurnEventStore,
280
- encodeEvent,
281
- finalizeAssistantParts,
282
- getPartKey,
283
- mergePersistedPart,
284
- messageHasTurnId,
285
- normalizeClientTurnId,
286
- normalizePersistedPart,
287
- normalizeTime,
288
- normalizeToolEvent,
289
- pumpBufferedTurn,
290
- replayTurnEvents,
291
- resolveChatTurn,
292
- resolveToolId,
293
- resolveToolName
294
- } from "./chunk-XYWCGFII.js";
295
303
  import {
296
304
  createKnowledgeLoop,
297
305
  createReviewerDecider,
@@ -353,7 +361,7 @@ import {
353
361
  verifySandboxTerminalToken,
354
362
  verifyTerminalProxyToken,
355
363
  writeProfileFilesToBox
356
- } from "./chunk-H4WCEZE3.js";
364
+ } from "./chunk-5GWXCSLQ.js";
357
365
  import {
358
366
  DEFAULT_HARNESS,
359
367
  KNOWN_HARNESSES,
@@ -374,7 +382,7 @@ import {
374
382
  restrictTaxonomy,
375
383
  verifyCapabilityToken,
376
384
  verifyExpiringCapabilityToken
377
- } from "./chunk-3II3AWHY.js";
385
+ } from "./chunk-PJC4NXPA.js";
378
386
  import {
379
387
  DEFAULT_APP_TOOL_PATHS,
380
388
  DEFAULT_HEADER_NAMES,
@@ -386,6 +394,7 @@ import {
386
394
  createMcpToolHandler,
387
395
  readToolArgs
388
396
  } from "./chunk-7EVZUIHW.js";
397
+ import "./chunk-S5SRJJQG.js";
389
398
  import {
390
399
  buildConsentUrl,
391
400
  createBrokerTokenProvider
@@ -523,6 +532,7 @@ export {
523
532
  assertMediaUrl,
524
533
  assertPositiveFinite,
525
534
  assertProvisionPayloadWithinCap,
535
+ assertSafeKeySegment,
526
536
  assertSceneMediaSrc,
527
537
  assertSequenceMediaUrl,
528
538
  attachReasoningEffort,
@@ -604,6 +614,8 @@ export {
604
614
  createPresetToolHandlers,
605
615
  createPresetWorkspaceKeyManager,
606
616
  createPresetWorkspaceKeyStore,
617
+ createProxiedArtifactRoute,
618
+ createR2ObjectStore,
607
619
  createReviewerDecider,
608
620
  createSandboxTerminalToken,
609
621
  createSequencesMcpHandler,
@@ -711,6 +723,7 @@ export {
711
723
  normalizeToolEvent,
712
724
  noticePart,
713
725
  noticePartKey,
726
+ objectKey,
714
727
  outcomeStatus,
715
728
  parseAssetSpec,
716
729
  parseInteractionCancel,
@@ -772,6 +785,7 @@ export {
772
785
  secondsToFrames,
773
786
  secretStoreFromClient,
774
787
  serializeCookie,
788
+ signObjectUrl,
775
789
  snapHarnessToModel,
776
790
  snapModelToHarness,
777
791
  snapshotFrame,
@@ -819,6 +833,7 @@ export {
819
833
  verifyCapabilityToken,
820
834
  verifyCompletion,
821
835
  verifyExpiringCapabilityToken,
836
+ verifyObjectUrl,
822
837
  verifySandboxTerminalToken,
823
838
  verifyTerminalProxyToken,
824
839
  volumeGateProposalId,
@@ -0,0 +1,202 @@
1
+ /**
2
+ * `/object-store` — a portable, secure object store for durable large
3
+ * attachments (uploaded PDFs, images, exports) that are too big to ride a chat
4
+ * turn body or live in KV.
5
+ *
6
+ * SECURITY POSTURE (this module guards a legal privilege wall). Every object key
7
+ * carries an operator segment and a customer segment, and access is by a
8
+ * short-lived HMAC-signed URL whose signature covers the EXACT key (operator +
9
+ * customer + upload id + filename) AND the expiry. A tampered key, a swapped
10
+ * customer segment, or a replayed-after-expiry URL all fail the signature or the
11
+ * expiry check and are refused. The signing secret is a PARAMETER on every call
12
+ * — this module reads NOTHING global (no `process.env`, no ambient config); if a
13
+ * product forgets to bind the secret, {@link verifyObjectUrl} fails closed and
14
+ * {@link signObjectUrl} throws rather than minting an unsigned URL.
15
+ *
16
+ * PURE MECHANISM behind two seams: an {@link ObjectStore} port (the R2 impl maps
17
+ * 1:1 to a bucket held behind the structural {@link R2LikeBucket} — so
18
+ * `@cloudflare/workers-types` never leaks into this package's public `.d.ts`)
19
+ * and the signing `secret`. Reads and writes STREAM: {@link createR2ObjectStore}
20
+ * returns the R2 object's `.body` stream directly and never calls
21
+ * `.text()`/`.arrayBuffer()`, so a 15 MB PDF flows through the worker without
22
+ * buffering the whole file into the isolate's heap.
23
+ *
24
+ * RECONCILIATION with the sandbox `storage` seam (`../sandbox` →
25
+ * `SandboxRuntimeConfig.storage`): that is BYOS3/R2 *snapshot* storage — it
26
+ * checkpoints and restores a sandbox box's filesystem for durable session
27
+ * execution. This is a DIFFERENT concern: a content-addressed store for
28
+ * user-facing attachments gated by a signed-URL privilege wall. They are not
29
+ * interchangeable and must not be merged: one persists agent runtime state, the
30
+ * other serves per-operator/per-customer documents to browsers. Do not route
31
+ * artifact downloads through the snapshot bucket, or vice versa.
32
+ */
33
+ /** Options for a single {@link ObjectStore.put}. Both fields are optional; a
34
+ * store that needs a fixed content length (e.g. R2 with a `ReadableStream`
35
+ * body) uses `contentLength` when present. */
36
+ interface PutObjectOptions {
37
+ /** MIME type recorded with the object (returned by `get`/`head`). Advisory:
38
+ * the proxied download route always serves `application/octet-stream`. */
39
+ contentType?: string;
40
+ /** Byte length of `body`, when known. Some backends require it for a
41
+ * streamed body; the R2 impl passes a known length through when supplied. */
42
+ contentLength?: number;
43
+ }
44
+ /** A retrieved object. `stream()` is the ONLY way to read the bytes — there is
45
+ * deliberately no `text()`/`arrayBuffer()`, so a large object never buffers
46
+ * into the isolate heap. */
47
+ interface ObjectBody {
48
+ /** The object's bytes as a `ReadableStream` (backed by R2's `.body`). */
49
+ stream(): ReadableStream;
50
+ /** Size in bytes. */
51
+ size: number;
52
+ /** Recorded MIME type, if any. */
53
+ contentType?: string;
54
+ }
55
+ /**
56
+ * Portable object-store port. `get`/`head` return `null` on a miss and NEVER
57
+ * throw for a missing key (a miss is a normal control-flow outcome, not an
58
+ * error); `delete` is idempotent.
59
+ */
60
+ interface ObjectStore {
61
+ put(key: string, body: ReadableStream | Uint8Array, opts?: PutObjectOptions): Promise<void>;
62
+ /** `null` on a miss — never throws for a missing key. */
63
+ get(key: string): Promise<ObjectBody | null>;
64
+ /** `null` on a miss — never throws for a missing key. */
65
+ head(key: string): Promise<{
66
+ size: number;
67
+ contentType?: string;
68
+ } | null>;
69
+ delete(key: string): Promise<void>;
70
+ }
71
+ /** Head/metadata shape of a stored object (structural match of R2's `R2Object`). */
72
+ interface R2LikeObjectHead {
73
+ size: number;
74
+ httpMetadata?: {
75
+ contentType?: string;
76
+ };
77
+ }
78
+ /** Body shape of a retrieved object (structural match of R2's `R2ObjectBody`).
79
+ * `body` is the streamed content — the impl reads this, never `.text()`. */
80
+ interface R2LikeObjectBody extends R2LikeObjectHead {
81
+ body: ReadableStream;
82
+ }
83
+ /**
84
+ * The minimal slice of Cloudflare's `R2Bucket` this module calls. A real
85
+ * `R2Bucket` satisfies it structurally, so the consumer passes its binding
86
+ * without this package ever importing `@cloudflare/workers-types` (which would
87
+ * otherwise leak into the public `.d.ts`).
88
+ */
89
+ interface R2LikeBucket {
90
+ put(key: string, value: ReadableStream | Uint8Array | ArrayBuffer, options?: {
91
+ httpMetadata?: {
92
+ contentType?: string;
93
+ };
94
+ }): Promise<unknown>;
95
+ get(key: string): Promise<R2LikeObjectBody | null>;
96
+ head(key: string): Promise<R2LikeObjectHead | null>;
97
+ delete(key: string): Promise<void>;
98
+ }
99
+ /**
100
+ * Map the {@link ObjectStore} port onto an R2 bucket 1:1. `get` STREAMS: it
101
+ * returns the R2 object's `.body` behind `ObjectBody.stream()` and never calls
102
+ * `.text()`/`.arrayBuffer()`, so a large file flows through the worker rather
103
+ * than buffering into the isolate.
104
+ */
105
+ declare function createR2ObjectStore({ bucket }: {
106
+ bucket: R2LikeBucket;
107
+ }): ObjectStore;
108
+ /**
109
+ * Assert a single object-key path SEGMENT (an operator id, customer id, or
110
+ * upload id) is safe to interpolate into a key, and return it. Throws on the
111
+ * traversal / injection shapes: `..` anywhere, a leading `/`, a backslash, or an
112
+ * empty segment. Consumers should call this on caller-supplied identifiers
113
+ * BEFORE any `get`/`put` so an attacker-controlled id can never widen the key
114
+ * beyond its own operator+customer prefix.
115
+ */
116
+ declare function assertSafeKeySegment(s: string): string;
117
+ /** Inputs to {@link objectKey}. `customerId` is optional — an unattributed
118
+ * upload lands under the reserved `_unattributed` customer segment. */
119
+ interface ObjectKeyParts {
120
+ operatorId: string;
121
+ /** Optional — omitted/undefined groups the upload under `_unattributed`. */
122
+ customerId?: string;
123
+ uploadId: string;
124
+ filename: string;
125
+ }
126
+ /**
127
+ * Build the canonical object key: `operator/customer/upload-filename`. The
128
+ * operator, customer, and upload segments are asserted safe (throwing on
129
+ * traversal); the filename is sanitized to a safe leaf. The customer segment
130
+ * falls back to `_unattributed` when no customer is attributed. Because a signed
131
+ * URL binds the EXACT key, the operator and customer segments are part of the
132
+ * privilege wall — a caller cannot later swap them without breaking the
133
+ * signature.
134
+ */
135
+ declare function objectKey({ operatorId, customerId, uploadId, filename }: ObjectKeyParts): string;
136
+ /** Arguments to {@link signObjectUrl}. */
137
+ interface SignObjectUrlArgs {
138
+ /** The exact object key to authorize (from {@link objectKey}). */
139
+ key: string;
140
+ /** Absolute expiry in epoch MILLISECONDS (e.g. `Date.now() + 5 * 60_000` for a
141
+ * ~5-minute TTL — the recommended default; keep it short). */
142
+ exp: number;
143
+ /** HMAC signing secret. Must be non-empty — signing fails closed otherwise. */
144
+ secret: string;
145
+ }
146
+ /**
147
+ * Mint a signed query string (`?key=…&exp=…&sig=…`) authorizing a download of
148
+ * `key` until `exp`. The product appends it to its artifact route path
149
+ * (`` `/artifacts${await signObjectUrl(...)}` ``); {@link createProxiedArtifactRoute}
150
+ * / {@link verifyObjectUrl} read it straight off the request URL, so the route's
151
+ * own mount path never matters.
152
+ *
153
+ * FAIL-CLOSED: throws if `secret` is empty (never mints an unsigned URL) and if
154
+ * `key` is unsafe (traversal). Async because the HMAC primitive is WebCrypto.
155
+ *
156
+ * TTL: `exp` is caller-supplied on purpose (the caller knows the sensitivity);
157
+ * keep it SHORT — ~5 minutes is the recommended default for a privilege-walled
158
+ * document.
159
+ */
160
+ declare function signObjectUrl({ key, exp, secret }: SignObjectUrlArgs): Promise<string>;
161
+ /** Result of {@link verifyObjectUrl}: the canonical key on success, nothing
162
+ * distinguishing on failure (so a rejection leaks no detail). */
163
+ type VerifyObjectUrlResult = {
164
+ ok: true;
165
+ key: string;
166
+ } | {
167
+ ok: false;
168
+ };
169
+ /**
170
+ * Verify a signed download request. Reads `key`/`exp`/`sig` off the request URL,
171
+ * canonicalizes the key identically to the signer, recomputes the HMAC and
172
+ * constant-time-compares it, and checks the expiry. Returns the canonical key on
173
+ * success.
174
+ *
175
+ * FAIL-CLOSED everywhere: an empty `secret`, a missing/ malformed parameter, a
176
+ * non-finite expiry, a signature mismatch, or an expired URL all return
177
+ * `{ ok: false }` — and the mismatch path uses a constant-time compare so a
178
+ * near-correct signature is not distinguishable by timing. Async because the
179
+ * HMAC primitive is WebCrypto.
180
+ */
181
+ declare function verifyObjectUrl(request: Request, { secret }: {
182
+ secret: string;
183
+ }): Promise<VerifyObjectUrlResult>;
184
+ /**
185
+ * Build a download handler that verifies a signed request and STREAMS the object
186
+ * back with a conservative, non-executable content type. Status contract:
187
+ *
188
+ * - `400` — the `key` is missing or malformed (bad encoding / traversal). This
189
+ * is an unauthenticated client error and leaks NOTHING about object existence.
190
+ * - `403` — the signature is missing, wrong, or expired.
191
+ * - `404` — the signature was valid but no object exists at that key (existence
192
+ * is only ever revealed to a validly-signed request).
193
+ * - `200` — streams the bytes with `Content-Disposition: attachment`,
194
+ * `Content-Type: application/octet-stream`, and `X-Content-Type-Options:
195
+ * nosniff`, so the worker never serves active/inline content.
196
+ */
197
+ declare function createProxiedArtifactRoute({ store, secret, }: {
198
+ store: ObjectStore;
199
+ secret: string;
200
+ }): (request: Request) => Promise<Response>;
201
+
202
+ export { type ObjectBody, type ObjectKeyParts, type ObjectStore, type PutObjectOptions, type R2LikeBucket, type R2LikeObjectBody, type R2LikeObjectHead, type SignObjectUrlArgs, type VerifyObjectUrlResult, assertSafeKeySegment, createProxiedArtifactRoute, createR2ObjectStore, objectKey, signObjectUrl, verifyObjectUrl };
@@ -0,0 +1,18 @@
1
+ import {
2
+ assertSafeKeySegment,
3
+ createProxiedArtifactRoute,
4
+ createR2ObjectStore,
5
+ objectKey,
6
+ signObjectUrl,
7
+ verifyObjectUrl
8
+ } from "../chunk-UMSOSUEU.js";
9
+ import "../chunk-S5SRJJQG.js";
10
+ export {
11
+ assertSafeKeySegment,
12
+ createProxiedArtifactRoute,
13
+ createR2ObjectStore,
14
+ objectKey,
15
+ signObjectUrl,
16
+ verifyObjectUrl
17
+ };
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -54,10 +54,11 @@ import {
54
54
  verifySandboxTerminalToken,
55
55
  verifyTerminalProxyToken,
56
56
  writeProfileFilesToBox
57
- } from "../chunk-H4WCEZE3.js";
57
+ } from "../chunk-5GWXCSLQ.js";
58
58
  import "../chunk-E7QYOOON.js";
59
- import "../chunk-3II3AWHY.js";
59
+ import "../chunk-PJC4NXPA.js";
60
60
  import "../chunk-7EVZUIHW.js";
61
+ import "../chunk-S5SRJJQG.js";
61
62
  import "../chunk-7W5XSTUF.js";
62
63
  import "../chunk-MFRCM32T.js";
63
64
  export {
@@ -1,12 +1,12 @@
1
+ import {
2
+ MembersPanel
3
+ } from "../chunk-GNL3MG5J.js";
1
4
  import {
2
5
  InvitationsPanel
3
6
  } from "../chunk-7C64WKAH.js";
4
7
  import {
5
8
  InviteAcceptPage
6
9
  } from "../chunk-VCPZ3HTN.js";
7
- import {
8
- MembersPanel
9
- } from "../chunk-GNL3MG5J.js";
10
10
  import "../chunk-63CE7FEZ.js";
11
11
  export {
12
12
  InvitationsPanel,
@@ -6,7 +6,7 @@ import {
6
6
  restrictTaxonomy,
7
7
  verifyCapabilityToken,
8
8
  verifyExpiringCapabilityToken
9
- } from "../chunk-3II3AWHY.js";
9
+ } from "../chunk-PJC4NXPA.js";
10
10
  import {
11
11
  DEFAULT_APP_TOOL_PATHS,
12
12
  DEFAULT_HEADER_NAMES,
@@ -18,6 +18,7 @@ import {
18
18
  createMcpToolHandler,
19
19
  readToolArgs
20
20
  } from "../chunk-7EVZUIHW.js";
21
+ import "../chunk-S5SRJJQG.js";
21
22
  import {
22
23
  APP_TOOL_NAMES,
23
24
  ToolInputError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.43.38",
3
+ "version": "0.43.40",
4
4
  "packageManager": "pnpm@10.33.4",
5
5
  "description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
6
6
  "keywords": [
@@ -147,6 +147,11 @@
147
147
  "import": "./dist/preflight/index.js",
148
148
  "default": "./dist/preflight/index.js"
149
149
  },
150
+ "./object-store": {
151
+ "types": "./dist/object-store/index.d.ts",
152
+ "import": "./dist/object-store/index.js",
153
+ "default": "./dist/object-store/index.js"
154
+ },
150
155
  "./chat-store": {
151
156
  "types": "./dist/chat-store/index.d.ts",
152
157
  "import": "./dist/chat-store/index.js",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/crypto/web-token.ts","../src/tools/capability.ts","../src/tools/gating.ts","../src/tools/http.ts"],"sourcesContent":["/**\n * Dependency-free WebCrypto primitives for HMAC-signed, base64url-encoded\n * tokens — base64url encode/decode, HMAC-SHA256, and a constant-time compare.\n * Runs on Cloudflare Workers, Node, and the browser with no Node `crypto`\n * dependency. Shared by the sandbox terminal-proxy token, the WS-upgrade token\n * parser, and the app-tool capability token so the logic lives in one place\n * rather than three near-identical private copies.\n *\n * Internal leaf: not exported from the `/crypto` barrel (that subpath is the\n * AES-GCM field-crypto surface); imported directly by the modules that need it.\n */\n\n/** base64url-encode raw bytes (RFC 4648 §5, no padding). */\nexport function base64UrlEncode(bytes: Uint8Array): string {\n let bin = ''\n for (const b of bytes) bin += String.fromCharCode(b)\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** base64url-encode a UTF-8 string. */\nexport function base64UrlEncodeText(text: string): string {\n return base64UrlEncode(new TextEncoder().encode(text))\n}\n\n/** Decode a base64url string back to its UTF-8 text. Re-pads before `atob` so\n * unpadded input decodes correctly regardless of the runtime's leniency. */\nexport function base64UrlDecodeText(value: string): string {\n const padded = value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=')\n const bin = atob(padded)\n const bytes = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i)\n return new TextDecoder().decode(bytes)\n}\n\n/** HMAC-SHA256 `message` under `secret`, returned base64url-encoded. */\nexport async function hmacSha256Base64Url(message: string, secret: string): Promise<string> {\n const enc = new TextEncoder()\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message))\n return base64UrlEncode(new Uint8Array(sig))\n}\n\n/** Length-independent-leak-free compare of two same-charset strings. */\nexport function constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n","/**\n * Per-user capability token — the sandbox→app auth primitive behind the\n * `verifyToken` seam in {@link authenticateToolRequest}.\n *\n * An app-agent runs inside the sandbox and reaches the host app back over HTTP\n * (the app tools, the integration-invoke bridge). The route must act AS the\n * connecting user without trusting any model-supplied identity, so the turn\n * mints a short HMAC token bound to the user id and bakes it into the per-turn\n * MCP server header; the route verifies it to recover the user.\n *\n * `HMAC-SHA256(secret, \"user:<userId>\")`, base64url, with an app-chosen prefix.\n * The token encodes no scopes — the hub's policy engine authorizes per action.\n * Fail-closed: with no secret, no token is minted (the caller MUST omit the MCP\n * server rather than fake an authorized call). WebCrypto only — runs on\n * Workers, Node, and the browser with no Node `crypto` dependency.\n */\n\nimport {\n base64UrlDecodeText,\n base64UrlEncodeText,\n constantTimeEqual,\n hmacSha256Base64Url,\n} from '../crypto/web-token'\n\nexport interface CapabilityTokenOptions {\n /** Shared HMAC secret. When absent, mint returns undefined / verify returns false. */\n secret?: string\n /** Token prefix (namespaces the credential; lets verify reject foreign tokens\n * cheaply). Default `cap_`. */\n prefix?: string\n}\n\n/** Mint a capability token for `userId`, or `undefined` when no secret is\n * configured (fail-closed — the caller omits the MCP server rather than fake it). */\nexport async function createCapabilityToken(userId: string, opts: CapabilityTokenOptions): Promise<string | undefined> {\n const secret = opts.secret?.trim()\n if (!secret) return undefined\n const prefix = opts.prefix ?? 'cap_'\n return `${prefix}${await sign(userId, secret)}`\n}\n\n/** Verify a capability token against `userId`. Returns false (never throws) for\n * an unconfigured secret, a wrong prefix, a malformed token, or a mismatch. */\nexport async function verifyCapabilityToken(userId: string, token: string, opts: CapabilityTokenOptions): Promise<boolean> {\n const secret = opts.secret?.trim()\n const prefix = opts.prefix ?? 'cap_'\n if (!secret || !token.startsWith(prefix)) return false\n const expected = `${prefix}${await sign(userId, secret)}`\n return constantTimeEqual(token, expected)\n}\n\nexport interface ExpiringCapabilityTokenOptions extends CapabilityTokenOptions {\n /** Token lifetime. Expired tokens verify false regardless of signature. */\n expiresInMs: number\n /** Clock injection for tests; defaults to Date.now. */\n now?: () => number\n}\n\n/**\n * Mint an EXPIRING capability token: `<prefix><base64url(payload)>.<sig>` where\n * the payload carries `{ sub, exp, n }` (subject, epoch-ms expiry, random\n * nonce) and the signature is HMAC-SHA256 over the encoded payload. Use this\n * for user-initiated scoped channels (e.g. a per-sequence MCP endpoint) where\n * a captured token must not stay valid past its window; the bare\n * {@link createCapabilityToken} remains for turn-scoped tool bridges whose\n * mint+verify happen inside one request cycle. Fail-closed like the bare\n * variant: no secret → no token.\n */\nexport async function createExpiringCapabilityToken(subject: string, opts: ExpiringCapabilityTokenOptions): Promise<string | undefined> {\n const secret = opts.secret?.trim()\n if (!secret) return undefined\n if (!Number.isFinite(opts.expiresInMs) || opts.expiresInMs <= 0) throw new Error('expiresInMs must be a positive number')\n const prefix = opts.prefix ?? 'cap_'\n const now = opts.now ?? Date.now\n const payload = base64UrlEncodeText(JSON.stringify({ sub: subject, exp: now() + opts.expiresInMs, n: crypto.randomUUID() }))\n return `${prefix}${payload}.${await hmacSha256Base64Url(payload, secret)}`\n}\n\n/** Verify an expiring token against `subject`: prefix, payload integrity,\n * subject match, and expiry all checked; returns false (never throws) on any\n * failure including a malformed payload. */\nexport async function verifyExpiringCapabilityToken(subject: string, token: string, opts: CapabilityTokenOptions & { now?: () => number }): Promise<boolean> {\n const secret = opts.secret?.trim()\n const prefix = opts.prefix ?? 'cap_'\n if (!secret || !token.startsWith(prefix)) return false\n const body = token.slice(prefix.length)\n const dot = body.lastIndexOf('.')\n if (dot <= 0 || dot === body.length - 1) return false\n const payload = body.slice(0, dot)\n const sig = body.slice(dot + 1)\n if (!constantTimeEqual(sig, await hmacSha256Base64Url(payload, secret))) return false\n let parsed: { sub?: unknown; exp?: unknown }\n try {\n parsed = JSON.parse(base64UrlDecodeText(payload)) as { sub?: unknown; exp?: unknown }\n } catch {\n return false\n }\n if (parsed.sub !== subject) return false\n if (typeof parsed.exp !== 'number') return false\n const now = opts.now ?? Date.now\n return parsed.exp > now()\n}\n\nasync function sign(userId: string, secret: string): Promise<string> {\n return hmacSha256Base64Url(`user:${userId}`, secret)\n}\n","/**\n * Capability gating — compose an agent session's tool surface from a\n * product-defined capability registry.\n *\n * Products that let users pick what an agent can do (a \"Studio\" agent with the\n * full build toolset vs a clean \"Assistant\" with none) all need the same\n * mechanics: a registry of named capabilities, each unlocking proposal types\n * and/or named tool groups, resolved against the product's\n * {@link AppToolTaxonomy} into the concrete tools to expose. The mechanics are\n * generic and live here; the capability VOCABULARY (ids, labels, which\n * proposal types, which tool groups) is the product's.\n */\n\nimport type { AppToolTaxonomy } from './types'\n\n/** One toggleable tool group in a product's capability registry. */\nexport interface ToolCapability {\n id: string\n label: string\n description: string\n /** Proposal types this capability unlocks (intersected with the taxonomy,\n * so a capability can never widen the product's proposal surface). */\n proposalTypes?: readonly string[]\n /** Unlocks every taxonomy proposal type beyond the base set — the domain's\n * specialized long tail (risk_assessment, vuln_report, …). */\n domainActions?: boolean\n /** Named product tool groups (e.g. 'sandbox', 'integrations') this\n * capability unlocks. The vocabulary is the product's; the resolver only\n * unions them. */\n toolGroups?: readonly string[]\n}\n\nexport interface ResolveToolCapabilitiesOptions {\n taxonomy: AppToolTaxonomy\n /** The product's full capability registry. */\n capabilities: readonly ToolCapability[]\n /** Enabled capability ids. `undefined` means full access (legacy callers\n * that don't send a capability set); an explicit `[]` means a pure chat\n * agent with no tools. Unknown ids are ignored. */\n enabled: readonly string[] | undefined\n /** The shared base proposal types `domainActions` excludes. Defaults to\n * every type some capability names explicitly via `proposalTypes` — i.e.\n * \"domain actions\" are the taxonomy types no capability claims. */\n baseProposalTypes?: readonly string[]\n}\n\nexport interface ResolvedToolCapabilities {\n /** Proposal types to keep — feed to {@link restrictTaxonomy}. */\n proposalTypes: string[]\n /** Product tool groups to expose (deduped union across enabled caps). */\n toolGroups: string[]\n}\n\n/**\n * Resolve an enabled capability-id set against a taxonomy into the concrete\n * tool surface. Fail-closed: only types present in the taxonomy survive, and\n * an empty `enabled` set yields no tools at all.\n */\nexport function resolveToolCapabilities(\n opts: ResolveToolCapabilitiesOptions,\n): ResolvedToolCapabilities {\n const { taxonomy, capabilities, enabled } = opts\n if (enabled === undefined) {\n return {\n proposalTypes: [...taxonomy.proposalTypes],\n toolGroups: [...new Set(capabilities.flatMap((c) => c.toolGroups ?? []))],\n }\n }\n const base = new Set(\n opts.baseProposalTypes ?? capabilities.flatMap((c) => c.proposalTypes ?? []),\n )\n const domainTypes = taxonomy.proposalTypes.filter((t) => !base.has(t))\n const byId = new Map(capabilities.map((c) => [c.id, c]))\n\n const proposalTypes = new Set<string>()\n const toolGroups = new Set<string>()\n for (const id of enabled) {\n const cap = byId.get(id)\n if (!cap) continue\n for (const t of cap.proposalTypes ?? []) {\n if (taxonomy.proposalTypes.includes(t)) proposalTypes.add(t)\n }\n if (cap.domainActions) for (const t of domainTypes) proposalTypes.add(t)\n for (const g of cap.toolGroups ?? []) toolGroups.add(g)\n }\n return { proposalTypes: [...proposalTypes], toolGroups: [...toolGroups] }\n}\n\n/**\n * Restrict a taxonomy to a subset of proposal types, intersecting the\n * regulated subset too — the regulated label survives restriction, so a\n * narrowed agent can never launder a regulated type into an unregulated one.\n */\nexport function restrictTaxonomy(\n taxonomy: AppToolTaxonomy,\n allowed: readonly string[],\n): AppToolTaxonomy {\n const allow = new Set(allowed)\n return {\n proposalTypes: taxonomy.proposalTypes.filter((t) => allow.has(t)),\n regulatedTypes: taxonomy.regulatedTypes.filter((t) => allow.has(t)),\n }\n}\n","import { authenticateToolRequest, type ToolHeaderNames } from './auth'\nimport { dispatchAppTool, outcomeStatus, type DispatchOptions } from './dispatch'\nimport type { AppToolName } from './openai'\nimport type { AppToolDefinition } from './registry'\n\nexport interface HandleToolRequestOptions extends DispatchOptions {\n /** Which app tool this route serves — a built-in name or a product-registered\n * {@link AppToolDefinition} (auto-added to `customTools` for dispatch). */\n tool: AppToolName | AppToolDefinition\n /** Verify the bearer capability token belongs to the header user. */\n verifyToken: (userId: string, bearer: string) => Promise<boolean>\n headerNames?: ToolHeaderNames\n /** Optional success-message builder for a friendlier tool result. */\n message?: (result: unknown) => string\n}\n\n/**\n * Handle one app-tool HTTP request end to end — the sandbox MCP path. The\n * agent's per-turn HTTP MCP server POSTs here; this authenticates (header user\n * + capability token), reads the args (MCP-alias tolerant), dispatches to the\n * product handler, and returns a JSON Response. A product's route file becomes\n * a one-liner: `export const action = ({ request }) => handleAppToolRequest(request, cfg)`.\n */\nexport async function handleAppToolRequest(request: Request, opts: HandleToolRequestOptions): Promise<Response> {\n if (request.method !== 'POST') return Response.json({ error: 'Method not allowed' }, { status: 405 })\n\n const auth = await authenticateToolRequest(request, { verifyToken: opts.verifyToken, headerNames: opts.headerNames })\n if (!auth.ok) return auth.response\n\n let body: { args?: Record<string, unknown>; arguments?: Record<string, unknown> } & Record<string, unknown>\n try {\n body = (await request.json()) as typeof body\n } catch {\n return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n const args = (body.args ?? body.arguments ?? body) as Record<string, unknown>\n\n // A custom tool passed as `tool` is registered for this dispatch so the route\n // file stays a one-liner (no separate customTools wiring needed).\n const toolName = typeof opts.tool === 'string' ? opts.tool : opts.tool.name\n const customTools =\n typeof opts.tool === 'string' ? opts.customTools : [...(opts.customTools ?? []), opts.tool]\n const outcome = await dispatchAppTool(toolName, args, auth.ctx, { ...opts, customTools })\n if (!outcome.ok) {\n return Response.json({ error: outcome.code, message: outcome.message }, { status: outcomeStatus(outcome) })\n }\n const payload = outcome.result as Record<string, unknown>\n return Response.json({ ok: true, ...payload, ...(opts.message ? { message: opts.message(outcome.result) } : {}) })\n}\n"],"mappings":"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","names":[]}