@tangle-network/agent-app 0.43.39 → 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.
- package/dist/chat-routes/index.js +13 -13
- package/dist/{chunk-H4WCEZE3.js → chunk-5GWXCSLQ.js} +5 -5
- 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/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/package.json +6 -1
- package/dist/chunk-3II3AWHY.js.map +0 -1
- /package/dist/{chunk-H4WCEZE3.js.map → chunk-5GWXCSLQ.js.map} +0 -0
|
@@ -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-
|
|
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-
|
|
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-
|
|
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":"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","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-
|
|
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-
|
|
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":[]}
|
package/dist/sandbox/index.js
CHANGED
|
@@ -54,10 +54,11 @@ import {
|
|
|
54
54
|
verifySandboxTerminalToken,
|
|
55
55
|
verifyTerminalProxyToken,
|
|
56
56
|
writeProfileFilesToBox
|
|
57
|
-
} from "../chunk-
|
|
57
|
+
} from "../chunk-5GWXCSLQ.js";
|
|
58
58
|
import "../chunk-E7QYOOON.js";
|
|
59
|
-
import "../chunk-
|
|
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,
|
package/dist/tools/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
restrictTaxonomy,
|
|
7
7
|
verifyCapabilityToken,
|
|
8
8
|
verifyExpiringCapabilityToken
|
|
9
|
-
} from "../chunk-
|
|
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.
|
|
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":[]}
|
|
File without changes
|