@tangle-network/agent-app 0.43.39 → 0.43.41
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.
- package/dist/assistant/index.d.ts +1 -1
- package/dist/assistant/index.js +2 -2
- package/dist/chat-routes/index.d.ts +2 -2
- package/dist/chat-routes/index.js +22 -14
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chunk-2EO7CPL3.js +191 -0
- package/dist/chunk-2EO7CPL3.js.map +1 -0
- package/dist/{chunk-H4WCEZE3.js → chunk-5GWXCSLQ.js} +5 -5
- package/dist/{chunk-D4HB72W2.js → chunk-KM766NN3.js} +159 -25
- package/dist/chunk-KM766NN3.js.map +1 -0
- package/dist/{chunk-3II3AWHY.js → chunk-PJC4NXPA.js} +7 -34
- package/dist/chunk-PJC4NXPA.js.map +1 -0
- package/dist/chunk-S5SRJJQG.js +36 -0
- package/dist/chunk-S5SRJJQG.js.map +1 -0
- package/dist/chunk-UMSOSUEU.js +124 -0
- package/dist/chunk-UMSOSUEU.js.map +1 -0
- package/dist/design-canvas-react/index.js +4 -4
- package/dist/file-index-Bw_IQE_G.d.ts +194 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +43 -28
- package/dist/object-store/index.d.ts +202 -0
- package/dist/object-store/index.js +18 -0
- package/dist/object-store/index.js.map +1 -0
- package/dist/sandbox/index.js +3 -2
- package/dist/teams-react/index.js +3 -3
- package/dist/tools/index.js +2 -1
- package/dist/web-react/index.d.ts +94 -2
- package/dist/web-react/index.js +19 -3
- package/package.json +6 -1
- package/dist/chunk-3II3AWHY.js.map +0 -1
- package/dist/chunk-5SV5PSU7.js +0 -80
- package/dist/chunk-5SV5PSU7.js.map +0 -1
- package/dist/chunk-D4HB72W2.js.map +0 -1
- package/dist/wire-BaUF66AS.d.ts +0 -61
- /package/dist/{chunk-H4WCEZE3.js.map → chunk-5GWXCSLQ.js.map} +0 -0
|
@@ -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-
|
|
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,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract between the chat client (composer + `streamChatTurn`) and the
|
|
3
|
+
* assembled server vertical (`createChatTurnRoutes`). Import-free on purpose:
|
|
4
|
+
* `/web-react` re-exports these types into browser bundles, so nothing here may
|
|
5
|
+
* reach a Node builtin or an engine package.
|
|
6
|
+
*
|
|
7
|
+
* The part shape mirrors the sandbox SDK's `PromptInputPart` structurally
|
|
8
|
+
* (text | image | file with filename/mediaType/url/path/content) — derived
|
|
9
|
+
* here, not imported, so the client bundle never touches the SDK.
|
|
10
|
+
*/
|
|
11
|
+
interface ChatTurnTextPartInput {
|
|
12
|
+
type: 'text';
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
/** A non-text prompt part the upload route hands back and the client echoes
|
|
16
|
+
* on send. `url` carries an inline `data:` URI for small files; `path` is a
|
|
17
|
+
* sandbox workspace reference for large ones (the >1 MiB gateway body cap
|
|
18
|
+
* makes the two-step upload mandatory). */
|
|
19
|
+
interface ChatTurnFilePartInput {
|
|
20
|
+
type: 'image' | 'file';
|
|
21
|
+
filename?: string;
|
|
22
|
+
mediaType?: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
path?: string;
|
|
25
|
+
content?: string;
|
|
26
|
+
}
|
|
27
|
+
type ChatTurnPartInput = ChatTurnTextPartInput | ChatTurnFilePartInput;
|
|
28
|
+
/** POST body for the turn route. `content` may be empty when `parts` carry the
|
|
29
|
+
* message (an image-only send). Product routing fields (workspaceId etc.) ride
|
|
30
|
+
* alongside and are read by the product's `authorize` seam. */
|
|
31
|
+
interface ChatTurnRequestPayload {
|
|
32
|
+
threadId: string;
|
|
33
|
+
content?: string;
|
|
34
|
+
/** Non-text parts from the upload route, echoed back verbatim. */
|
|
35
|
+
parts?: ChatTurnFilePartInput[];
|
|
36
|
+
model?: string;
|
|
37
|
+
effort?: 'auto' | 'low' | 'medium' | 'high';
|
|
38
|
+
harness?: string;
|
|
39
|
+
/** Client-generated idempotency key for the logical turn (retry-safe). */
|
|
40
|
+
turnId?: string;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
/** `fetch` init for the turn route — the one place the client wire shape is
|
|
44
|
+
* serialized, so composer glue and products never drift from the server's
|
|
45
|
+
* parser. */
|
|
46
|
+
declare function chatTurnRequestInit(payload: ChatTurnRequestPayload): RequestInit;
|
|
47
|
+
declare const INLINE_PARTS_MAX_BYTES = 950000;
|
|
48
|
+
declare class ChatTurnInputError extends Error {
|
|
49
|
+
readonly status: number;
|
|
50
|
+
readonly code: string;
|
|
51
|
+
constructor(message: string, status?: number, code?: string);
|
|
52
|
+
}
|
|
53
|
+
declare function promptPartsByteSize(parts: ChatTurnPartInput[]): number;
|
|
54
|
+
/** Throws `ChatTurnInputError` (413) when the parts' inline payload would blow
|
|
55
|
+
* the gateway cap. Path-ref parts are tiny by construction and always pass. */
|
|
56
|
+
declare function assertPromptPartsWithinCap(parts: ChatTurnPartInput[], maxBytes?: number): void;
|
|
57
|
+
/** A file mention resolved from the composer's `@`-picker: the
|
|
58
|
+
* workspace-relative path plus enough metadata to build a prompt part and
|
|
59
|
+
* pointer text. `path` is the canonical identity — the mention pill's
|
|
60
|
+
* `MentionItem.id` for the file kind (`/web-react`'s `useFileMentions`). */
|
|
61
|
+
interface FileMention {
|
|
62
|
+
path: string;
|
|
63
|
+
name: string;
|
|
64
|
+
size?: number;
|
|
65
|
+
}
|
|
66
|
+
/** The `image/*` mime for a mention path by extension, or `undefined` for
|
|
67
|
+
* anything not in the known image set (dispatched as `type: 'file'`). */
|
|
68
|
+
declare function mediaTypeForMentionPath(path: string): string | undefined;
|
|
69
|
+
interface FileMentionsToPartsOptions {
|
|
70
|
+
/** Resolve a mention's workspace-relative path to the absolute path the
|
|
71
|
+
* dispatched part should carry (e.g. a host prefixing the in-box vault
|
|
72
|
+
* root). Default: identity — the path travels unchanged. */
|
|
73
|
+
resolvePath?: (path: string) => string;
|
|
74
|
+
}
|
|
75
|
+
/** Maps resolved file mentions to path-only `ChatTurnFilePartInput`s —
|
|
76
|
+
* `image` vs `file` by extension, and always a `path`, never a `url` (the
|
|
77
|
+
* url/path XOR invariant: a mention is a sandbox path reference, never
|
|
78
|
+
* inline bytes). */
|
|
79
|
+
declare function fileMentionsToParts(mentions: readonly FileMention[], opts?: FileMentionsToPartsOptions): ChatTurnFilePartInput[];
|
|
80
|
+
/** The agent-facing pointer block appended to the dispatched prompt — never
|
|
81
|
+
* persisted in message `content`. Empty array → `''` so callers can append
|
|
82
|
+
* unconditionally. This is the sole producer of that text: the current
|
|
83
|
+
* turn's dispatch and any history projection built from the same mention
|
|
84
|
+
* list both route through here, so the two can't drift apart. */
|
|
85
|
+
declare function buildMentionPromptBlock(mentions: readonly Pick<FileMention, 'name' | 'path'>[]): string;
|
|
86
|
+
/** Validates the untyped `parts` array off the wire. Returns the typed parts
|
|
87
|
+
* or throws `ChatTurnInputError` (400) naming the offending entry. */
|
|
88
|
+
declare function parseChatTurnParts(raw: unknown): ChatTurnFilePartInput[];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `createSandboxFileIndexRoute` — server side of `@`-file-mentions
|
|
92
|
+
* (companion to sandbox-ui#184's composer mention primitive). Serves a flat,
|
|
93
|
+
* ignore-filtered listing of the workspace sandbox so `useFileMentions`
|
|
94
|
+
* (`/web-react`) can filter it client-side without a round trip per
|
|
95
|
+
* keystroke.
|
|
96
|
+
*
|
|
97
|
+
* Same seam style as `createUploadRoute`: `authorize({ request })` resolves a
|
|
98
|
+
* structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's
|
|
99
|
+
* `box.fs.tree`) — no SDK import here. `authorize` also carries the
|
|
100
|
+
* cold-box signal: a sandbox that isn't running yet answers `{ status:
|
|
101
|
+
* 'warming' }` directly, never provisions-and-waits inside this route.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's
|
|
105
|
+
* `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's
|
|
106
|
+
* omitted from the structural match. */
|
|
107
|
+
interface SandboxTreeFile {
|
|
108
|
+
path: string;
|
|
109
|
+
size: number;
|
|
110
|
+
}
|
|
111
|
+
/** Structural match of the sandbox SDK's `box.fs.tree` result shape
|
|
112
|
+
* (`FileTreeResult`). `stats.truncated` is the only stat this route reads;
|
|
113
|
+
* the rest ride through unread on the real SDK type. */
|
|
114
|
+
interface SandboxTreeResult {
|
|
115
|
+
root: string;
|
|
116
|
+
files: SandboxTreeFile[];
|
|
117
|
+
stats: {
|
|
118
|
+
truncated: boolean;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Structural match of the sandbox SDK's `box.fs` tree surface. */
|
|
122
|
+
interface SandboxFileTreeSource {
|
|
123
|
+
tree(path: string, options?: {
|
|
124
|
+
maxDepth?: number;
|
|
125
|
+
}): Promise<SandboxTreeResult>;
|
|
126
|
+
}
|
|
127
|
+
interface FileIndexReadyResponse {
|
|
128
|
+
status: 'ready';
|
|
129
|
+
/** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a
|
|
130
|
+
* client can hand a response entry straight to `fileMentionsToParts` /
|
|
131
|
+
* `buildMentionPromptBlock` without remapping. */
|
|
132
|
+
files: FileMention[];
|
|
133
|
+
/** True when either the underlying scan truncated (SDK-side cap) or this
|
|
134
|
+
* route's own `maxEntries` cap trimmed the filtered list. The client
|
|
135
|
+
* should show "showing first N files" rather than imply completeness. */
|
|
136
|
+
truncated: boolean;
|
|
137
|
+
generatedAt: string;
|
|
138
|
+
}
|
|
139
|
+
/** Cold-box answer: no provisioning happened, no files were scanned. The
|
|
140
|
+
* client shows a warming state and retries — this route never blocks on a
|
|
141
|
+
* box coming up. */
|
|
142
|
+
interface FileIndexWarmingResponse {
|
|
143
|
+
status: 'warming';
|
|
144
|
+
}
|
|
145
|
+
type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse;
|
|
146
|
+
/** Short-TTL cache seam so repeat popover opens in the same session don't
|
|
147
|
+
* re-scan the workspace. Host-provided (e.g. a KV binding); `key` is
|
|
148
|
+
* whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */
|
|
149
|
+
interface FileIndexCache {
|
|
150
|
+
get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null;
|
|
151
|
+
put(key: string, value: FileIndexReadyResponse, options?: {
|
|
152
|
+
ttlSeconds?: number;
|
|
153
|
+
}): Promise<void> | void;
|
|
154
|
+
}
|
|
155
|
+
type FileIndexAuthorization = {
|
|
156
|
+
status: 'ready';
|
|
157
|
+
/** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */
|
|
158
|
+
fs: SandboxFileTreeSource;
|
|
159
|
+
/** Workspace root to index (e.g. `/home/agent`). */
|
|
160
|
+
root: string;
|
|
161
|
+
/** Extra ignore segments for this request, merged with the route's
|
|
162
|
+
* defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */
|
|
163
|
+
ignore?: string[];
|
|
164
|
+
/** Opaque cache key for the optional cache seam. Omit to skip caching
|
|
165
|
+
* for this request (e.g. a workspace the host chooses not to cache). */
|
|
166
|
+
cacheKey?: string;
|
|
167
|
+
} | {
|
|
168
|
+
status: 'warming';
|
|
169
|
+
} | {
|
|
170
|
+
status: 'denied';
|
|
171
|
+
response: Response;
|
|
172
|
+
};
|
|
173
|
+
interface CreateSandboxFileIndexRouteOptions {
|
|
174
|
+
/** Authenticate the caller, resolve the sandbox `fs` handle, and signal a
|
|
175
|
+
* cold box — never provisions or waits. */
|
|
176
|
+
authorize(args: {
|
|
177
|
+
request: Request;
|
|
178
|
+
}): Promise<FileIndexAuthorization>;
|
|
179
|
+
/** Extra ignore segments beyond the route's defaults (node_modules, .git,
|
|
180
|
+
* dotfiles/dot-dirs, common build dirs). Matched as exact path-segment
|
|
181
|
+
* names, same rule as the defaults. */
|
|
182
|
+
ignore?: string[];
|
|
183
|
+
/** Passed to `fs.tree` as `options.maxDepth`. Default 12. */
|
|
184
|
+
maxDepth?: number;
|
|
185
|
+
/** Hard cap on entries returned after filtering. Default 5000. */
|
|
186
|
+
maxEntries?: number;
|
|
187
|
+
/** Optional host-provided cache seam. */
|
|
188
|
+
cache?: FileIndexCache;
|
|
189
|
+
/** Cache TTL in seconds when `cache` is set. Default 20. */
|
|
190
|
+
cacheTtlSeconds?: number;
|
|
191
|
+
}
|
|
192
|
+
declare function createSandboxFileIndexRoute(options: CreateSandboxFileIndexRouteOptions): (request: Request) => Promise<Response>;
|
|
193
|
+
|
|
194
|
+
export { type ChatTurnRequestPayload as C, type FileIndexAuthorization as F, INLINE_PARTS_MAX_BYTES as I, type SandboxFileTreeSource as S, type ChatTurnPartInput as a, type ChatTurnFilePartInput as b, ChatTurnInputError as c, type ChatTurnTextPartInput as d, type CreateSandboxFileIndexRouteOptions as e, type FileIndexCache as f, type FileIndexReadyResponse as g, type FileIndexResponse as h, type FileIndexWarmingResponse as i, type FileMention as j, type FileMentionsToPartsOptions as k, type SandboxTreeFile as l, type SandboxTreeResult as m, assertPromptPartsWithinCap as n, buildMentionPromptBlock as o, chatTurnRequestInit as p, createSandboxFileIndexRoute as q, fileMentionsToParts as r, mediaTypeForMentionPath as s, parseChatTurnParts as t, promptPartsByteSize as u };
|
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';
|