@tangle-network/agent-app 0.43.49 → 0.43.51

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.
@@ -0,0 +1,239 @@
1
+ import {
2
+ formatBytes
3
+ } from "./chunk-JGYOYY5D.js";
4
+
5
+ // src/chat-routes/binary-sniff.ts
6
+ function bytesStartWith(bytes, offset, signature) {
7
+ if (bytes.length < offset + signature.length) return false;
8
+ for (let i = 0; i < signature.length; i++) {
9
+ if (bytes[offset + i] !== signature[i]) return false;
10
+ }
11
+ return true;
12
+ }
13
+ function asciiAt(bytes, offset, text) {
14
+ if (bytes.length < offset + text.length) return false;
15
+ for (let i = 0; i < text.length; i++) {
16
+ if (bytes[offset + i] !== text.charCodeAt(i)) return false;
17
+ }
18
+ return true;
19
+ }
20
+ function sniffRiff(bytes) {
21
+ if (!asciiAt(bytes, 0, "RIFF")) return null;
22
+ if (asciiAt(bytes, 8, "WEBP")) return "image/webp";
23
+ if (asciiAt(bytes, 8, "WAVE")) return "audio/wav";
24
+ return null;
25
+ }
26
+ function sniffFtyp(bytes) {
27
+ if (!asciiAt(bytes, 4, "ftyp")) return null;
28
+ if (asciiAt(bytes, 8, "qt ")) return "video/quicktime";
29
+ if (asciiAt(bytes, 8, "avif") || asciiAt(bytes, 8, "avis")) return "image/avif";
30
+ if (asciiAt(bytes, 8, "heic") || asciiAt(bytes, 8, "heix") || asciiAt(bytes, 8, "hevc") || asciiAt(bytes, 8, "hevx")) return "image/heic";
31
+ if (asciiAt(bytes, 8, "mif1") || asciiAt(bytes, 8, "msf1")) return "image/heif";
32
+ return "video/mp4";
33
+ }
34
+ function sniffBmp(bytes) {
35
+ return asciiAt(bytes, 0, "BM") && bytes.length >= 10 && bytes[6] === 0 && bytes[7] === 0 && bytes[8] === 0 && bytes[9] === 0;
36
+ }
37
+ function sniffId3(bytes) {
38
+ return asciiAt(bytes, 0, "ID3") && bytes.length >= 10 && bytes[3] < 16 && bytes[6] < 128 && bytes[7] < 128 && bytes[8] < 128 && bytes[9] < 128;
39
+ }
40
+ function sniffMagicBytes(bytes) {
41
+ if (bytesStartWith(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10])) return "image/png";
42
+ if (bytesStartWith(bytes, 0, [255, 216, 255])) return "image/jpeg";
43
+ if (asciiAt(bytes, 0, "GIF87a") || asciiAt(bytes, 0, "GIF89a")) return "image/gif";
44
+ if (sniffBmp(bytes)) return "image/bmp";
45
+ if (bytesStartWith(bytes, 0, [73, 73, 42, 0])) return "image/tiff";
46
+ if (bytesStartWith(bytes, 0, [77, 77, 0, 42])) return "image/tiff";
47
+ if (bytesStartWith(bytes, 0, [0, 0, 1, 0])) return "image/x-icon";
48
+ if (asciiAt(bytes, 0, "%PDF-")) return "application/pdf";
49
+ if (bytesStartWith(bytes, 0, [80, 75, 3, 4])) return "application/zip";
50
+ if (bytesStartWith(bytes, 0, [31, 139])) return "application/gzip";
51
+ if (sniffId3(bytes) || bytesStartWith(bytes, 0, [255, 251])) return "audio/mpeg";
52
+ if (asciiAt(bytes, 0, "OggS")) return "audio/ogg";
53
+ const riff = sniffRiff(bytes);
54
+ if (riff) return riff;
55
+ const ftyp = sniffFtyp(bytes);
56
+ if (ftyp) return ftyp;
57
+ return null;
58
+ }
59
+ function sniffSvgText(decoded) {
60
+ let text = decoded;
61
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
62
+ text = text.trimStart();
63
+ if (/^<svg[\s>/]/.test(text)) return true;
64
+ if (!text.startsWith("<?xml")) return false;
65
+ return /<svg[\s>/]/.test(text.slice(0, 1024));
66
+ }
67
+ function sniffBinary(bytes) {
68
+ const mime = sniffMagicBytes(bytes);
69
+ if (mime) return { binary: true, mime };
70
+ if (bytes.includes(0)) return { binary: true, mime: null };
71
+ let decoded;
72
+ try {
73
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
74
+ } catch {
75
+ return { binary: true, mime: null };
76
+ }
77
+ if (sniffSvgText(decoded)) return { binary: true, mime: "image/svg+xml" };
78
+ return { binary: false, mime: null };
79
+ }
80
+
81
+ // src/chat-routes/attachment-validation.ts
82
+ var MAX_BINARY_ATTACHMENT_BYTES = 10 * 1024 * 1024;
83
+ var MAX_TEXT_ATTACHMENT_BYTES = 950 * 1024;
84
+ var ATTACHMENT_MAX_COUNT = 10;
85
+ var MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024;
86
+ var ATTACHMENT_ACCEPT = "image/*,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html";
87
+ var ALLOWED_ATTACHMENT_SNIFFED_MIMES = /* @__PURE__ */ new Set([
88
+ "image/png",
89
+ "image/jpeg",
90
+ "image/gif",
91
+ "image/bmp",
92
+ "image/tiff",
93
+ "image/x-icon",
94
+ "image/webp",
95
+ "image/svg+xml",
96
+ "image/avif",
97
+ "image/heic",
98
+ "image/heif",
99
+ "application/pdf"
100
+ ]);
101
+ var EXTENSION_IMPLIES_SNIFFED_MIME = {
102
+ png: "image/png",
103
+ jpg: "image/jpeg",
104
+ jpeg: "image/jpeg",
105
+ gif: "image/gif",
106
+ bmp: "image/bmp",
107
+ tif: "image/tiff",
108
+ tiff: "image/tiff",
109
+ ico: "image/x-icon",
110
+ webp: "image/webp",
111
+ svg: "image/svg+xml",
112
+ pdf: "application/pdf"
113
+ };
114
+ function checkAttachmentType(fileName, sniff, allowed = ALLOWED_ATTACHMENT_SNIFFED_MIMES) {
115
+ if (sniff.binary === false) return { succeeded: true };
116
+ const extension = fileName.split(".").pop()?.toLowerCase() ?? "";
117
+ const impliedMime = EXTENSION_IMPLIES_SNIFFED_MIME[extension];
118
+ if (impliedMime && sniff.mime && sniff.mime !== impliedMime) {
119
+ return {
120
+ succeeded: false,
121
+ code: "attachment_type_mismatch",
122
+ message: `${fileName} has a .${extension} extension, but its content is ${sniff.mime}`
123
+ };
124
+ }
125
+ if (!sniff.mime || !allowed.has(sniff.mime)) {
126
+ return {
127
+ succeeded: false,
128
+ code: "attachment_type_not_allowed",
129
+ message: sniff.mime ? `${fileName}'s content (${sniff.mime}) is not an allowed attachment type` : `${fileName}'s content is not a recognized attachment type`
130
+ };
131
+ }
132
+ return { succeeded: true };
133
+ }
134
+ function sanitizeAttachmentFileName(name) {
135
+ const sanitized = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
136
+ return sanitized || "file";
137
+ }
138
+ function attachmentSizeErrorMessage(name, actualBytes, limitBytes) {
139
+ return `${name} is ${formatBytes(actualBytes)}; attachments are limited to ${formatBytes(limitBytes)}`;
140
+ }
141
+ function attachmentTotalSizeErrorMessage(totalBytes, limitBytes) {
142
+ return `Attachments total ${formatBytes(totalBytes)}; each message is limited to ${formatBytes(limitBytes)}`;
143
+ }
144
+
145
+ // src/chat-routes/file-index.ts
146
+ var DEFAULT_IGNORE_SEGMENTS = [
147
+ "node_modules",
148
+ "dist",
149
+ "build",
150
+ "out",
151
+ "coverage",
152
+ "target",
153
+ "__pycache__",
154
+ "venv"
155
+ ];
156
+ function isIgnored(relPath, ignoreSegments) {
157
+ for (const segment of relPath.split("/")) {
158
+ if (!segment) continue;
159
+ if (segment.startsWith(".")) return true;
160
+ if (ignoreSegments.has(segment)) return true;
161
+ }
162
+ return false;
163
+ }
164
+ function relativeTo(root, path) {
165
+ const prefix = root.endsWith("/") ? root : `${root}/`;
166
+ if (path.startsWith(prefix)) return path.slice(prefix.length);
167
+ if (path === root) return "";
168
+ return path;
169
+ }
170
+ function basename(path) {
171
+ const segments = path.split("/").filter(Boolean);
172
+ return segments[segments.length - 1] ?? path;
173
+ }
174
+ function isMissingRootError(err, root) {
175
+ if (!(err instanceof Error)) return false;
176
+ if (err.code !== "VALIDATION_ERROR") return false;
177
+ return /ENOENT/.test(err.message) && /no such file or directory/.test(err.message) && new RegExp(`\\blstat '${escapeRegExp(root)}'`).test(err.message);
178
+ }
179
+ function escapeRegExp(value) {
180
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
181
+ }
182
+ function createSandboxFileIndexRoute(options) {
183
+ const maxDepth = options.maxDepth ?? 12;
184
+ const maxEntries = options.maxEntries ?? 5e3;
185
+ const cacheTtlSeconds = options.cacheTtlSeconds ?? 20;
186
+ const staticIgnore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE_SEGMENTS, ...options.ignore ?? []]);
187
+ return async function fileIndex(request) {
188
+ const auth = await options.authorize({ request });
189
+ if (auth.status === "denied") return auth.response;
190
+ if (auth.status === "warming") {
191
+ return Response.json({ status: "warming" });
192
+ }
193
+ const cache = options.cache;
194
+ if (cache && auth.cacheKey) {
195
+ const cached = await cache.get(auth.cacheKey);
196
+ if (cached) return Response.json(cached);
197
+ }
198
+ const ignoreSegments = auth.ignore?.length ? /* @__PURE__ */ new Set([...staticIgnore, ...auth.ignore]) : staticIgnore;
199
+ let scan;
200
+ try {
201
+ scan = await auth.fs.tree(auth.root, { maxDepth });
202
+ } catch (err) {
203
+ if (!isMissingRootError(err, auth.root)) throw err;
204
+ return Response.json({ status: "warming" });
205
+ }
206
+ const filtered = scan.files.filter((f) => !isIgnored(relativeTo(scan.root, f.path), ignoreSegments));
207
+ const truncated = scan.stats.truncated || filtered.length > maxEntries;
208
+ const files = filtered.slice(0, maxEntries).map((f) => {
209
+ const path = relativeTo(scan.root, f.path);
210
+ const entry = { path, name: basename(path) };
211
+ if (typeof f.size === "number") entry.size = f.size;
212
+ return entry;
213
+ });
214
+ const body = {
215
+ status: "ready",
216
+ files,
217
+ truncated,
218
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
219
+ };
220
+ if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds });
221
+ return Response.json(body);
222
+ };
223
+ }
224
+
225
+ export {
226
+ sniffBinary,
227
+ MAX_BINARY_ATTACHMENT_BYTES,
228
+ MAX_TEXT_ATTACHMENT_BYTES,
229
+ ATTACHMENT_MAX_COUNT,
230
+ MAX_ATTACHMENT_TOTAL_BYTES,
231
+ ATTACHMENT_ACCEPT,
232
+ ALLOWED_ATTACHMENT_SNIFFED_MIMES,
233
+ checkAttachmentType,
234
+ sanitizeAttachmentFileName,
235
+ attachmentSizeErrorMessage,
236
+ attachmentTotalSizeErrorMessage,
237
+ createSandboxFileIndexRoute
238
+ };
239
+ //# sourceMappingURL=chunk-3EKOSBYL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-routes/binary-sniff.ts","../src/chat-routes/attachment-validation.ts","../src/chat-routes/file-index.ts"],"sourcesContent":["/**\n * Content-based binary/text classification, shared by the attachment upload\n * route (server) and the composer's client-side pre-validation (browser) —\n * both sides must agree on what counts as binary before a byte ever leaves\n * the client. Extension-based allowlists lie (a renamed `.docx`, a PNG saved\n * as `.txt`), so classification reads the actual bytes: a magic-byte table\n * for common binary formats first, then a UTF-8 decode attempt for\n * everything else.\n *\n * Lifted near-verbatim from gtm-agent's `src/lib/binary-sniff.ts` (the\n * source PRs hardened this against real corruption/gate bugs: gtm#584,\n * gtm#592). Import-free by design — `/web-react` re-exports `/chat-routes`\n * modules into browser bundles (`tests/browser-safe-subpaths.test.ts` walks\n * the graph), so nothing here may reach a Node builtin or an engine package.\n */\n\nexport interface SniffResult {\n binary: boolean\n mime: string | null\n}\n\nfunction bytesStartWith(bytes: Uint8Array, offset: number, signature: number[]): boolean {\n if (bytes.length < offset + signature.length) return false\n for (let i = 0; i < signature.length; i++) {\n if (bytes[offset + i] !== signature[i]) return false\n }\n return true\n}\n\nfunction asciiAt(bytes: Uint8Array, offset: number, text: string): boolean {\n if (bytes.length < offset + text.length) return false\n for (let i = 0; i < text.length; i++) {\n if (bytes[offset + i] !== text.charCodeAt(i)) return false\n }\n return true\n}\n\n/** RIFF containers (WebP, WAV) share the `RIFF....<TYPE>` header; the type\n * tag at byte offset 8 distinguishes them. */\nfunction sniffRiff(bytes: Uint8Array): string | null {\n if (!asciiAt(bytes, 0, 'RIFF')) return null\n if (asciiAt(bytes, 8, 'WEBP')) return 'image/webp'\n if (asciiAt(bytes, 8, 'WAVE')) return 'audio/wav'\n return null\n}\n\n/** MP4/MOV/AVIF/HEIC containers share an `ftyp` box at byte offset 4; the\n * major brand at offset 8 tells them apart within the shared ISO-BMFF\n * family. Brands outside this table (mp4, m4a, m4v, etc) fall back to\n * `video/mp4`, the family's most common member. */\nfunction sniffFtyp(bytes: Uint8Array): string | null {\n if (!asciiAt(bytes, 4, 'ftyp')) return null\n if (asciiAt(bytes, 8, 'qt ')) return 'video/quicktime'\n if (asciiAt(bytes, 8, 'avif') || asciiAt(bytes, 8, 'avis')) return 'image/avif'\n if (asciiAt(bytes, 8, 'heic') || asciiAt(bytes, 8, 'heix') || asciiAt(bytes, 8, 'hevc') || asciiAt(bytes, 8, 'hevx')) return 'image/heic'\n if (asciiAt(bytes, 8, 'mif1') || asciiAt(bytes, 8, 'msf1')) return 'image/heif'\n return 'video/mp4'\n}\n\n/** `BM` alone matches ordinary prose (\"BMW…\"), so require the BMP header's\n * reserved bytes (offsets 6-9), which the format mandates to be zero. */\nfunction sniffBmp(bytes: Uint8Array): boolean {\n return asciiAt(bytes, 0, 'BM')\n && bytes.length >= 10\n && bytes[6] === 0 && bytes[7] === 0 && bytes[8] === 0 && bytes[9] === 0\n}\n\n/** `ID3` alone matches ordinary prose (\"ID3 tags…\"), so require the ID3v2\n * header shape: a plausible version byte and sync-safe size bytes. */\nfunction sniffId3(bytes: Uint8Array): boolean {\n return asciiAt(bytes, 0, 'ID3')\n && bytes.length >= 10\n && bytes[3]! < 0x10\n && bytes[6]! < 0x80 && bytes[7]! < 0x80 && bytes[8]! < 0x80 && bytes[9]! < 0x80\n}\n\nfunction sniffMagicBytes(bytes: Uint8Array): string | null {\n if (bytesStartWith(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return 'image/png'\n if (bytesStartWith(bytes, 0, [0xff, 0xd8, 0xff])) return 'image/jpeg'\n if (asciiAt(bytes, 0, 'GIF87a') || asciiAt(bytes, 0, 'GIF89a')) return 'image/gif'\n if (sniffBmp(bytes)) return 'image/bmp'\n if (bytesStartWith(bytes, 0, [0x49, 0x49, 0x2a, 0x00])) return 'image/tiff' // little-endian\n if (bytesStartWith(bytes, 0, [0x4d, 0x4d, 0x00, 0x2a])) return 'image/tiff' // big-endian\n if (bytesStartWith(bytes, 0, [0x00, 0x00, 0x01, 0x00])) return 'image/x-icon'\n if (asciiAt(bytes, 0, '%PDF-')) return 'application/pdf'\n // OOXML (.docx/.xlsx/.pptx) is a zip archive; the container format is all\n // that matters here, so no attempt is made to distinguish the payload.\n if (bytesStartWith(bytes, 0, [0x50, 0x4b, 0x03, 0x04])) return 'application/zip'\n if (bytesStartWith(bytes, 0, [0x1f, 0x8b])) return 'application/gzip'\n if (sniffId3(bytes) || bytesStartWith(bytes, 0, [0xff, 0xfb])) return 'audio/mpeg'\n if (asciiAt(bytes, 0, 'OggS')) return 'audio/ogg'\n\n const riff = sniffRiff(bytes)\n if (riff) return riff\n\n const ftyp = sniffFtyp(bytes)\n if (ftyp) return ftyp\n\n return null\n}\n\n/** SVG is valid UTF-8 but must round-trip byte-identical (image tools read\n * it from the box as a file), so it is classified binary. Conservative\n * match: the document's first element is `<svg`, or an `<?xml` prolog is\n * followed by an `<svg` element within the first ~1KB (comments/doctype may\n * sit between). Plain XML without an svg root, or prose that merely\n * mentions \"<svg\", stays text. */\nfunction sniffSvgText(decoded: string): boolean {\n let text = decoded\n if (text.charCodeAt(0) === 0xfeff) text = text.slice(1)\n text = text.trimStart()\n if (/^<svg[\\s>/]/.test(text)) return true\n if (!text.startsWith('<?xml')) return false\n return /<svg[\\s>/]/.test(text.slice(0, 1024))\n}\n\n/** Decide whether uploaded bytes are binary or text, and identify the mime\n * type when it can be determined from content. Magic bytes are checked\n * first; anything unmatched falls back to a fatal UTF-8 decode. A NUL byte\n * or a decode failure means binary. Valid UTF-8 that is an SVG document is\n * binary (byte-identity matters for image tooling). Content that matches\n * nothing and does not decode as text is binary with an unknown mime —\n * extension-based guessing happens at the call site, not here. */\nexport function sniffBinary(bytes: Uint8Array): SniffResult {\n const mime = sniffMagicBytes(bytes)\n if (mime) return { binary: true, mime }\n\n if (bytes.includes(0x00)) return { binary: true, mime: null }\n\n let decoded: string\n try {\n decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n } catch {\n return { binary: true, mime: null }\n }\n if (sniffSvgText(decoded)) return { binary: true, mime: 'image/svg+xml' }\n return { binary: false, mime: null }\n}\n","/**\n * Shared attachment validation core — constants, type-gate, and filename\n * sanitization used by BOTH the (server) attachment upload route and the\n * (browser) composer's client-side pre-validation, so a rejection never\n * differs depending on which side classified the bytes first.\n *\n * ≈ gtm-agent's `src/lib/attachment-limits.ts`, minus what agent-app already\n * has (`ATTACHMENT_MAX_COUNT`/`MAX_ATTACHMENT_TOTAL_BYTES`/\n * `attachmentTotalSizeErrorMessage` lived in `./resolve-attachments` and are\n * re-homed here so the whole validation vocabulary — count cap, size caps,\n * and type gate — has one address). Import-free besides `./wire`\n * (`formatBytes`) and `./binary-sniff` (`SniffResult`): `/web-react`\n * re-exports `/chat-routes` modules into browser bundles\n * (`tests/browser-safe-subpaths.test.ts` walks the graph), so nothing here\n * may reach a Node builtin or an engine package.\n */\n\nimport { formatBytes } from './wire'\nimport type { SniffResult } from './binary-sniff'\n\n/** Ceiling on a binary attachment's raw (pre-encoding) byte size. */\nexport const MAX_BINARY_ATTACHMENT_BYTES = 10 * 1024 * 1024\n\n/** Ceiling on a text attachment's raw byte size. Text hydrates through\n * inline prompt parts, a separate path that remains proxy-capped (see\n * `INLINE_PARTS_MAX_BYTES` in `./wire`). */\nexport const MAX_TEXT_ATTACHMENT_BYTES = 950 * 1024\n\n/** Most files a single request may carry: the composer staging cap, the\n * upload route's per-request cap, and the chat body's `attachments` cap. */\nexport const ATTACHMENT_MAX_COUNT = 10\n\n/** Aggregate raw-byte ceiling across one message's attachments. */\nexport const MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024\n\n/**\n * Accept list for the composer file picker + type validation, same grammar as\n * the native `<input accept>` attribute. Images plus the text/doc types a\n * product's store actually reads.\n */\nexport const ATTACHMENT_ACCEPT =\n 'image/*,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html'\n\n/** Sniffed-mime counterpart of `ATTACHMENT_ACCEPT`: the binary formats\n * `sniffBinary` can identify from magic bytes among the accepted types.\n * Values must match `sniffBinary`'s output strings verbatim, or every\n * upload of that format fails the type gate. */\nexport const ALLOWED_ATTACHMENT_SNIFFED_MIMES: ReadonlySet<string> = new Set([\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/bmp',\n 'image/tiff',\n 'image/x-icon',\n 'image/webp',\n 'image/svg+xml',\n 'image/avif',\n 'image/heic',\n 'image/heif',\n 'application/pdf',\n])\n\n/** Extensions whose magic-byte family is unambiguous, mapped to the mime\n * `sniffBinary` emits for genuine content of that format. Keep in sync with\n * `ATTACHMENT_ACCEPT`: an extension only belongs here if its format has a\n * detectable magic-byte signature. Text extensions (.txt/.md/.csv/.json/\n * .yaml/.yml/.html) are deliberately absent — they have no magic bytes to\n * compare against, so they ride the plain UTF-8 gate instead. AVIF/HEIC/HEIF\n * extensions are also deliberately absent: the ISO-BMFF brand-to-extension\n * mapping in that family isn't one-to-one (a legitimate `.heic` can carry a\n * `mif1` brand), so an extension-implies-mime entry would reject genuine\n * files. They still ride the allowlist gate below, which is what catches an\n * mp4 renamed `.avif` (its content sniffs `video/mp4`, not an allowed mime). */\nconst EXTENSION_IMPLIES_SNIFFED_MIME: Readonly<Record<string, string>> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n bmp: 'image/bmp',\n tif: 'image/tiff',\n tiff: 'image/tiff',\n ico: 'image/x-icon',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n pdf: 'application/pdf',\n}\n\nexport type AttachmentTypeCheckResult =\n | { succeeded: true }\n | { succeeded: false; code: 'attachment_type_mismatch' | 'attachment_type_not_allowed'; message: string }\n\n/**\n * Cross-check a filename's extension against its sniffed content.\n *\n * Text content (`sniff.binary === false`) always passes here — it has no\n * magic bytes to compare, so it rides the existing UTF-8 gate instead. For\n * binary content: an extension with an unambiguous magic-byte family (e.g.\n * `.pdf`) must match the sniffed mime, or the upload is a mismatch (a\n * renamed file). Otherwise the sniffed mime must be one of `allowed`\n * (default {@link ALLOWED_ATTACHMENT_SNIFFED_MIMES}), or the upload is\n * rejected outright. The `allowed` param feeds a route's override seam (a\n * product accepting a narrower or wider set than the default).\n */\nexport function checkAttachmentType(\n fileName: string,\n sniff: SniffResult,\n allowed: ReadonlySet<string> = ALLOWED_ATTACHMENT_SNIFFED_MIMES,\n): AttachmentTypeCheckResult {\n if (sniff.binary === false) return { succeeded: true }\n\n const extension = fileName.split('.').pop()?.toLowerCase() ?? ''\n const impliedMime = EXTENSION_IMPLIES_SNIFFED_MIME[extension]\n if (impliedMime && sniff.mime && sniff.mime !== impliedMime) {\n return {\n succeeded: false,\n code: 'attachment_type_mismatch',\n message: `${fileName} has a .${extension} extension, but its content is ${sniff.mime}`,\n }\n }\n\n if (!sniff.mime || !allowed.has(sniff.mime)) {\n return {\n succeeded: false,\n code: 'attachment_type_not_allowed',\n message: sniff.mime\n ? `${fileName}'s content (${sniff.mime}) is not an allowed attachment type`\n : `${fileName}'s content is not a recognized attachment type`,\n }\n }\n\n return { succeeded: true }\n}\n\n/**\n * Rewrite a filename into the store-path charset (`A-Za-z0-9._-` per\n * segment) — attachment paths double as store keys, sandbox file paths, and\n * in-message path references, none of which tolerate spaces or punctuation.\n * Runs of unsupported characters collapse to one `-`; leading dots/dashes are\n * stripped so the name can't read as a hidden segment. The original name is\n * preserved separately (the returned `ChatAttachmentInput.name`), so\n * sanitization loses nothing.\n */\nexport function sanitizeAttachmentFileName(name: string): string {\n const sanitized = name\n .trim()\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/^[.-]+/, '')\n return sanitized || 'file'\n}\n\n/** Human-readable error naming both the actual size and the limit that was\n * exceeded. Shared so the server route and the composer pre-check report\n * the same message shape. */\nexport function attachmentSizeErrorMessage(name: string, actualBytes: number, limitBytes: number): string {\n return `${name} is ${formatBytes(actualBytes)}; attachments are limited to ${formatBytes(limitBytes)}`\n}\n\n/** Human-readable error for a chat message whose combined attachments exceed\n * the aggregate raw-byte ceiling. */\nexport function attachmentTotalSizeErrorMessage(totalBytes: number, limitBytes: number): string {\n return `Attachments total ${formatBytes(totalBytes)}; each message is limited to ${formatBytes(limitBytes)}`\n}\n","/**\n * `createSandboxFileIndexRoute` — server side of `@`-file-mentions\n * (companion to sandbox-ui#184's composer mention primitive). Serves a flat,\n * ignore-filtered listing of the workspace sandbox so `useFileMentions`\n * (`/web-react`) can filter it client-side without a round trip per\n * keystroke.\n *\n * Same seam style as `createUploadRoute`: `authorize({ request })` resolves a\n * structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's\n * `box.fs.tree`) — no SDK import here. `authorize` also carries the\n * cold-box signal: a sandbox that isn't running yet answers `{ status:\n * 'warming' }` directly, never provisions-and-waits inside this route.\n *\n * A box can also be running with its workspace root not yet materialised, which\n * `authorize` cannot see; the route recognises that one signal off `fs.tree`\n * and answers `warming` too, so every consumer gets the retry-and-wait state\n * instead of a 500. Every other `tree()` failure propagates.\n */\n\nimport type { FileMention } from './wire'\n\n/** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's\n * `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's\n * omitted from the structural match. */\nexport interface SandboxTreeFile {\n path: string\n size: number\n}\n\n/** Structural match of the sandbox SDK's `box.fs.tree` result shape\n * (`FileTreeResult`). `stats.truncated` is the only stat this route reads;\n * the rest ride through unread on the real SDK type. */\nexport interface SandboxTreeResult {\n root: string\n files: SandboxTreeFile[]\n stats: { truncated: boolean }\n}\n\n/** Structural match of the sandbox SDK's `box.fs` tree surface. */\nexport interface SandboxFileTreeSource {\n tree(path: string, options?: { maxDepth?: number }): Promise<SandboxTreeResult>\n}\n\nexport interface FileIndexReadyResponse {\n status: 'ready'\n /** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a\n * client can hand a response entry straight to `fileMentionsToParts` /\n * `buildMentionPromptBlock` without remapping. */\n files: FileMention[]\n /** True when either the underlying scan truncated (SDK-side cap) or this\n * route's own `maxEntries` cap trimmed the filtered list. The client\n * should show \"showing first N files\" rather than imply completeness. */\n truncated: boolean\n generatedAt: string\n}\n\n/** Cold-box answer: no provisioning happened, no files were scanned. The\n * client shows a warming state and retries — this route never blocks on a\n * box coming up. Two situations produce it: `authorize` reporting a box that\n * is not running, and a running box whose workspace root does not exist yet\n * (see `isMissingRootError`). */\nexport interface FileIndexWarmingResponse {\n status: 'warming'\n}\n\nexport type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse\n\n/** Short-TTL cache seam so repeat popover opens in the same session don't\n * re-scan the workspace. Host-provided (e.g. a KV binding); `key` is\n * whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */\nexport interface FileIndexCache {\n get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null\n put(key: string, value: FileIndexReadyResponse, options?: { ttlSeconds?: number }): Promise<void> | void\n}\n\nexport type FileIndexAuthorization =\n | {\n status: 'ready'\n /** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */\n fs: SandboxFileTreeSource\n /** Workspace root to index (e.g. `/home/agent`). */\n root: string\n /** Extra ignore segments for this request, merged with the route's\n * defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */\n ignore?: string[]\n /** Opaque cache key for the optional cache seam. Omit to skip caching\n * for this request (e.g. a workspace the host chooses not to cache). */\n cacheKey?: string\n }\n | { status: 'warming' }\n | { status: 'denied'; response: Response }\n\nexport interface CreateSandboxFileIndexRouteOptions {\n /** Authenticate the caller, resolve the sandbox `fs` handle, and signal a\n * cold box — never provisions or waits. */\n authorize(args: { request: Request }): Promise<FileIndexAuthorization>\n /** Extra ignore segments beyond the route's defaults (node_modules, .git,\n * dotfiles/dot-dirs, common build dirs). Matched as exact path-segment\n * names, same rule as the defaults. */\n ignore?: string[]\n /** Passed to `fs.tree` as `options.maxDepth`. Default 12. */\n maxDepth?: number\n /** Hard cap on entries returned after filtering. Default 5000. */\n maxEntries?: number\n /** Optional host-provided cache seam. */\n cache?: FileIndexCache\n /** Cache TTL in seconds when `cache` is set. Default 20. */\n cacheTtlSeconds?: number\n}\n\n/** Segment names ignored anywhere in a path, beyond the generic dotfile rule\n * below. Intentionally small and language/framework-agnostic — callers\n * extend it via `ignore` for anything domain-specific (e.g. a vault's\n * `uploads` dir). */\nconst DEFAULT_IGNORE_SEGMENTS = [\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n 'target',\n '__pycache__',\n 'venv',\n]\n\n/** A path segment starting with `.` (`.git`, `.env`, `.next`, `.cache`, …) is\n * always ignored — this single rule covers most dot-prefixed VCS/tooling\n * dirs and dotfiles without enumerating them. */\nfunction isIgnored(relPath: string, ignoreSegments: ReadonlySet<string>): boolean {\n for (const segment of relPath.split('/')) {\n if (!segment) continue\n if (segment.startsWith('.')) return true\n if (ignoreSegments.has(segment)) return true\n }\n return false\n}\n\n/** Strips the tree result's echoed `root` prefix so entries are always\n * workspace-relative, whichever convention the structural `fs.tree` uses\n * (root-relative already, or root-prefixed). */\nfunction relativeTo(root: string, path: string): string {\n const prefix = root.endsWith('/') ? root : `${root}/`\n if (path.startsWith(prefix)) return path.slice(prefix.length)\n if (path === root) return ''\n return path\n}\n\nfunction basename(path: string): string {\n const segments = path.split('/').filter(Boolean)\n return segments[segments.length - 1] ?? path\n}\n\n/**\n * A box can answer `running` before it has materialised the workspace root —\n * `authorize` has already committed to `ready` by then, so `fs.tree` is the\n * first thing to notice, and it rejects with the sandbox SDK's\n * `ValidationError` wrapping a box-side `ENOENT … lstat` on the root. That is\n * the SAME \"not usable yet\" state `authorize` collapses onto `warming` for an\n * absent or stopped box, just discovered one step later, so it gets the same\n * answer instead of escaping as a 500.\n *\n * Matched STRUCTURALLY, not with `instanceof`: importing the SDK's error class\n * would make `@tangle-network/sandbox` a hard dependency of a route factory\n * whose entire `fs` seam is structural (`SandboxFileTreeSource`), and would\n * break any host feeding it a non-SDK handle.\n *\n * Deliberately narrow — the error code, `ENOENT`, the ENOENT message text, AND\n * the failing syscall's own operand all have to line up. A permission error, a\n * timeout, an auth failure, or an ENOENT on some other path inside the tree is\n * a real failure and still surfaces.\n *\n * The operand is matched as the quoted `lstat '<root>'` clause rather than by\n * substring, because the root is a PREFIX of everything under it: a plain\n * `includes(root)` would also swallow an ENOENT on `<root>/gone/x.md`, and on\n * a prefix sibling like `/home/agent-old/...`.\n */\nfunction isMissingRootError(err: unknown, root: string): boolean {\n if (!(err instanceof Error)) return false\n if ((err as { code?: unknown }).code !== 'VALIDATION_ERROR') return false\n return (\n /ENOENT/.test(err.message) &&\n /no such file or directory/.test(err.message) &&\n new RegExp(`\\\\blstat '${escapeRegExp(root)}'`).test(err.message)\n )\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nexport function createSandboxFileIndexRoute(\n options: CreateSandboxFileIndexRouteOptions,\n): (request: Request) => Promise<Response> {\n const maxDepth = options.maxDepth ?? 12\n const maxEntries = options.maxEntries ?? 5000\n const cacheTtlSeconds = options.cacheTtlSeconds ?? 20\n const staticIgnore = new Set([...DEFAULT_IGNORE_SEGMENTS, ...(options.ignore ?? [])])\n\n return async function fileIndex(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (auth.status === 'denied') return auth.response\n if (auth.status === 'warming') {\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n\n const cache = options.cache\n if (cache && auth.cacheKey) {\n const cached = await cache.get(auth.cacheKey)\n if (cached) return Response.json(cached)\n }\n\n const ignoreSegments = auth.ignore?.length\n ? new Set([...staticIgnore, ...auth.ignore])\n : staticIgnore\n\n let scan: SandboxTreeResult\n try {\n scan = await auth.fs.tree(auth.root, { maxDepth })\n } catch (err) {\n if (!isMissingRootError(err, auth.root)) throw err\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n const filtered = scan.files.filter((f) => !isIgnored(relativeTo(scan.root, f.path), ignoreSegments))\n const truncated = scan.stats.truncated || filtered.length > maxEntries\n const files: FileMention[] = filtered.slice(0, maxEntries).map((f) => {\n const path = relativeTo(scan.root, f.path)\n const entry: FileMention = { path, name: basename(path) }\n if (typeof f.size === 'number') entry.size = f.size\n return entry\n })\n\n const body: FileIndexReadyResponse = {\n status: 'ready',\n files,\n truncated,\n generatedAt: new Date().toISOString(),\n }\n\n if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds })\n\n return Response.json(body)\n }\n}\n"],"mappings":"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","names":[]}