@openparachute/vault 0.7.3-rc.12 → 0.7.3-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +3 -5
  2. package/core/src/conformance.ts +1 -2
  3. package/core/src/content-range.test.ts +0 -127
  4. package/core/src/content-range.ts +0 -100
  5. package/core/src/contract-typed-index.test.ts +3 -4
  6. package/core/src/core.test.ts +4 -521
  7. package/core/src/expand.ts +3 -11
  8. package/core/src/indexed-fields.test.ts +3 -9
  9. package/core/src/indexed-fields.ts +1 -9
  10. package/core/src/mcp.ts +10 -601
  11. package/core/src/notes.ts +4 -201
  12. package/core/src/schema-defaults.ts +1 -85
  13. package/core/src/search-fts-v25.test.ts +1 -9
  14. package/core/src/search-query.test.ts +0 -42
  15. package/core/src/search-query.ts +0 -27
  16. package/core/src/seed-packs.test.ts +1 -117
  17. package/core/src/seed-packs.ts +1 -217
  18. package/core/src/store.ts +3 -59
  19. package/core/src/tag-schemas.ts +12 -27
  20. package/core/src/types.ts +1 -7
  21. package/core/src/vault-projection.ts +0 -55
  22. package/package.json +1 -1
  23. package/src/add-pack.test.ts +0 -32
  24. package/src/cli.ts +1 -5
  25. package/src/contract-errors.test.ts +1 -2
  26. package/src/mcp-http.ts +4 -33
  27. package/src/mcp-tools.ts +14 -61
  28. package/src/onboarding-seed.test.ts +0 -64
  29. package/src/routes.ts +95 -92
  30. package/src/routing.ts +0 -17
  31. package/src/server.ts +0 -7
  32. package/src/storage.test.ts +1 -200
  33. package/src/transcription-worker.test.ts +0 -151
  34. package/src/transcription-worker.ts +52 -113
  35. package/src/vault.test.ts +11 -48
  36. package/core/src/attachment/bytes-provider.ts +0 -65
  37. package/core/src/attachment/policy.test.ts +0 -66
  38. package/core/src/attachment/policy.ts +0 -131
  39. package/core/src/attachment/tickets.test.ts +0 -45
  40. package/core/src/attachment/tickets.ts +0 -117
  41. package/core/src/attachment-tickets-tool.test.ts +0 -286
  42. package/core/src/display-title.test.ts +0 -190
  43. package/core/src/lede.test.ts +0 -96
  44. package/core/src/search-title-boost.test.ts +0 -125
  45. package/src/attachment-bytes.ts +0 -68
  46. package/src/attachment-tickets.test.ts +0 -475
  47. package/src/attachment-tickets.ts +0 -340
  48. package/src/read-attachment.test.ts +0 -436
package/src/mcp-http.ts CHANGED
@@ -109,7 +109,7 @@ export async function handleScopedMcp(
109
109
  const instruction = await getServerInstruction(vaultName, auth);
110
110
  return handleMcp(
111
111
  req,
112
- () => generateScopedMcpTools(vaultName, auth, callerBearer ?? null, req),
112
+ () => generateScopedMcpTools(vaultName, auth, callerBearer ?? null),
113
113
  `parachute-vault/${vaultName}`,
114
114
  vaultName,
115
115
  auth,
@@ -180,15 +180,9 @@ export async function handleMcp(
180
180
  }
181
181
  try {
182
182
  const result = await tool.execute((args ?? {}) as Record<string, unknown>);
183
- // The one wrapper change (attachments-for-agents design, Wave 2):
184
- // `resultContent`, when a tool defines it, decides the MCP content
185
- // blocks instead of the universal single-text-block default —
186
- // `read-attachment`'s image branch is the only current user (needs a
187
- // REAL {type:"image"} block alongside the row-JSON text block).
188
- const content = tool.resultContent
189
- ? tool.resultContent(result)
190
- : [{ type: "text" as const, text: JSON.stringify(result, null, 2) }];
191
- return { content };
183
+ return {
184
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
185
+ };
192
186
  } catch (err) {
193
187
  // vault#555 fix 6 — never re-wrap an already-formed McpError. Passing
194
188
  // the SAME instance straight through is strictly correct (no
@@ -235,14 +229,6 @@ export async function handleMcp(
235
229
  tag?: string;
236
230
  cycle?: unknown;
237
231
  referencing_tags?: unknown;
238
- /** Attachment-tickets design (§2c "errors as JIT docs") — a short, imperative next-step, distinct from the older free-form `hint`. */
239
- how_to?: string;
240
- /** `read-attachment` (Wave 2) refusals — `image_too_large` / `unsupported_attachment_type` carry the attachment's actual byte size. */
241
- size?: number;
242
- /** `read-attachment` `image_too_large` — the 4 MiB cap it exceeded. */
243
- max_bytes?: number;
244
- /** `read-attachment` `unsupported_attachment_type` — the mime type that couldn't be read. */
245
- mime_type?: string;
246
232
  };
247
233
  // Honest-queries validation errors (vault#550) — `limit`/`offset`/date
248
234
  // values that are structurally invalid rather than merely "no
@@ -413,21 +399,6 @@ export async function handleMcp(
413
399
  error_type: e.error_type,
414
400
  field: e.field,
415
401
  hint: e.hint,
416
- // Forwarded when present (undefined keys are fine — JSON-RPC
417
- // `data` just omits them) so a `structuredError()` call site that
418
- // stamps extra fields — `limit`/`got`/`extension` (size/type
419
- // refusals) or `how_to` (attachment-tickets' JIT-docs field, §2c)
420
- // — isn't silently truncated down to error_type/field/hint by
421
- // this backstop.
422
- ...(e.limit !== undefined ? { limit: e.limit } : {}),
423
- ...(e.got !== undefined ? { got: e.got } : {}),
424
- ...(e.extension !== undefined ? { extension: e.extension } : {}),
425
- ...(e.how_to !== undefined ? { how_to: e.how_to } : {}),
426
- // read-attachment (Wave 2) refusal fields — same forward-when-present
427
- // discipline as the ticket fields just above.
428
- ...(e.size !== undefined ? { size: e.size } : {}),
429
- ...(e.max_bytes !== undefined ? { max_bytes: e.max_bytes } : {}),
430
- ...(e.mime_type !== undefined ? { mime_type: e.mime_type } : {}),
431
402
  });
432
403
  }
433
404
  return {
package/src/mcp-tools.ts CHANGED
@@ -43,9 +43,6 @@ import {
43
43
  import { chooseHubOrigin, mintHubJwt, revokeHubJwt } from "./mcp-install.ts";
44
44
  import { looksLikeJwt } from "./hub-jwt.ts";
45
45
  import { readGlobalConfig, DEFAULT_PORT } from "./config.ts";
46
- import { getBaseUrl } from "./oauth-discovery.ts";
47
- import { getSharedAttachmentTicketProvider } from "./attachment-tickets.ts";
48
- import { createFsAttachmentBytesProvider } from "./attachment-bytes.ts";
49
46
 
50
47
  /**
51
48
  * Filter a vault projection to entries an in-scope tag contributes to.
@@ -113,12 +110,6 @@ export async function getServerInstruction(
113
110
  description: config?.description ?? null,
114
111
  projection,
115
112
  coordinates: resolveVaultCoordinates(),
116
- // Bun always wires an in-process AttachmentTicketProvider AND a fresh
117
- // fs-backed AttachmentBytesProvider per session (see
118
- // `generateScopedMcpTools` below) — the connect-time brief can
119
- // unconditionally teach both the ticket tools and read-attachment on
120
- // this door.
121
- attachments: { ticketsEnabled: true, readEnabled: true },
122
113
  });
123
114
  }
124
115
 
@@ -152,16 +143,6 @@ export function generateScopedMcpTools(
152
143
  vaultName: string,
153
144
  auth?: AuthResult,
154
145
  callerBearer?: string | null,
155
- /**
156
- * The incoming MCP request, when available (omitted by the many
157
- * test-only call sites that construct tools without a live request).
158
- * Threaded through ONLY so the attachment-ticket tools can resolve a
159
- * request-accurate `urlBase` (honoring `X-Forwarded-Host`/proto, same as
160
- * `getBaseUrl`) — falls back to `resolveVaultCoordinates()`'s configured/
161
- * expose-state origin when omitted, so ticket tools are still present
162
- * (just less precisely origined) in that case.
163
- */
164
- req?: Request,
165
146
  ): McpToolDef[] {
166
147
  const store = getVaultStore(vaultName);
167
148
 
@@ -244,48 +225,20 @@ export function generateScopedMcpTools(
244
225
  ? (info) => logStrictBypass(info)
245
226
  : undefined;
246
227
 
247
- // Attachment tickets (Wave 1): bun always wires the in-process provider
248
- // (see src/attachment-tickets.ts) — unlike the tag-scope predicates
249
- // above, this isn't conditional on `scoped` because the tools should be
250
- // listed for every session, scoped or not. Tag-scope confidentiality is
251
- // still enforced (`noteVisible`, awaited inline inside the ticket tools'
252
- // own async execute see its doc comment in generateMcpTools for why
253
- // this doesn't need the shared allowedHolder machinery the OTHER
254
- // predicates above do).
255
- const ticketNoteVisible = scoped
256
- ? async (note: Note) => {
257
- const allowed = await expandTokenTagScope(store, rawTags);
258
- return noteWithinTagScope(note, allowed, rawTags);
259
- }
260
- : undefined;
261
- const ticketUrlBase = req
262
- ? `${getBaseUrl(req).replace(/\/$/, "")}/vault/${vaultName}`
263
- : `${resolveVaultCoordinates().hubOrigin.replace(/\/$/, "")}/vault/${vaultName}`;
264
-
265
- const tools = generateMcpTools(store, {
266
- ...(expandVisibility ? { expandVisibility } : {}),
267
- ...(nearTraversable ? { nearTraversable } : {}),
268
- ...(ifExistsVisible ? { ifExistsVisible } : {}),
269
- ...(aggregateVisibility ? { aggregateVisibility } : {}),
270
- ...(writeContext ? { writeContext } : {}),
271
- ...(strictBypass ? { strictBypass } : {}),
272
- ...(onStrictBypass ? { onStrictBypass } : {}),
273
- attachmentTickets: {
274
- provider: getSharedAttachmentTicketProvider(),
275
- vaultName,
276
- urlBase: ticketUrlBase,
277
- ...(ticketNoteVisible ? { noteVisible: ticketNoteVisible } : {}),
278
- },
279
- // Attachment bytes (Wave 2 model lane): bun always wires a fresh fs
280
- // provider per session — cheap (stateless), unlike the ticket
281
- // provider's process-wide shared Map. Same `ticketNoteVisible`
282
- // tag-scope predicate as the ticket seam above (identical contract:
283
- // "is the owning note in scope").
284
- attachmentBytes: {
285
- provider: createFsAttachmentBytesProvider(vaultName),
286
- ...(ticketNoteVisible ? { noteVisible: ticketNoteVisible } : {}),
287
- },
288
- });
228
+ const tools = generateMcpTools(
229
+ store,
230
+ expandVisibility || nearTraversable || ifExistsVisible || aggregateVisibility || writeContext || strictBypass
231
+ ? {
232
+ ...(expandVisibility ? { expandVisibility } : {}),
233
+ ...(nearTraversable ? { nearTraversable } : {}),
234
+ ...(ifExistsVisible ? { ifExistsVisible } : {}),
235
+ ...(aggregateVisibility ? { aggregateVisibility } : {}),
236
+ ...(writeContext ? { writeContext } : {}),
237
+ ...(strictBypass ? { strictBypass } : {}),
238
+ ...(onStrictBypass ? { onStrictBypass } : {}),
239
+ }
240
+ : undefined,
241
+ );
289
242
 
290
243
  overrideVaultInfo(tools, vaultName, auth);
291
244
  applyTagDependencyGuards(tools, vaultName);
@@ -23,17 +23,14 @@ import { BunStore } from "./vault-store.ts";
23
23
  import { seedOnboardingNotes } from "./onboarding-seed.ts";
24
24
  import {
25
25
  applySeedPack,
26
- ARCHIVED_TAG,
27
26
  CAPTURE_ANYTHING_PATH,
28
27
  CONNECT_AI_PATH,
29
28
  GETTING_STARTED_PATH,
30
29
  NOTES_REQUIRED_TAGS,
31
- STARTER_ONTOLOGY_PACK,
32
30
  SURFACE_STARTER_PACK,
33
31
  SURFACE_STARTER_PATH,
34
32
  TAGS_GRAPH_PATH,
35
33
  WELCOME_PATH,
36
- welcomePack,
37
34
  YOURS_TO_KEEP_PATH,
38
35
  } from "../core/src/seed-packs.ts";
39
36
  import {
@@ -293,67 +290,6 @@ describe("applySeedPack — surface-starter via add-pack", () => {
293
290
  });
294
291
  });
295
292
 
296
- describe("applySeedPack — tag description preservation on re-apply (Aaron-ratified 2026-07-17)", () => {
297
- test("fresh apply: a brand-new tag gets the pack's description, reported as touched not preserved", async () => {
298
- const result = await applySeedPack(store, STARTER_ONTOLOGY_PACK);
299
- expect(result.tags).toContain(ARCHIVED_TAG.name);
300
- expect(result.preservedTagDescriptions).toEqual([]);
301
-
302
- const record = await store.getTagRecord(ARCHIVED_TAG.name);
303
- expect(record!.description).toBe(ARCHIVED_TAG.description);
304
- });
305
-
306
- test("re-apply unmodified: description is still the pack's text — upserted, not preserved", async () => {
307
- await applySeedPack(store, STARTER_ONTOLOGY_PACK);
308
- const rerun = await applySeedPack(store, STARTER_ONTOLOGY_PACK);
309
- expect(rerun.preservedTagDescriptions).toEqual([]);
310
-
311
- const record = await store.getTagRecord(ARCHIVED_TAG.name);
312
- expect(record!.description).toBe(ARCHIVED_TAG.description);
313
- });
314
-
315
- test("re-apply after a user edit: the hand-tuned description survives and is reported preserved", async () => {
316
- await applySeedPack(store, STARTER_ONTOLOGY_PACK);
317
- const edited = "My own house rules for archiving — nothing like the default.";
318
- await store.upsertTagRecord(ARCHIVED_TAG.name, { description: edited });
319
-
320
- const rerun = await applySeedPack(store, STARTER_ONTOLOGY_PACK);
321
- expect(rerun.preservedTagDescriptions).toContain(ARCHIVED_TAG.name);
322
- // Still listed in `tags` — the tag itself was touched (fields/parent_names
323
- // upsert unconditionally); only the description write was skipped.
324
- expect(rerun.tags).toContain(ARCHIVED_TAG.name);
325
-
326
- const record = await store.getTagRecord(ARCHIVED_TAG.name);
327
- expect(record!.description).toBe(edited);
328
- });
329
-
330
- test("capture byte-identity anchor (NOTES_REQUIRED_TAGS) holds under either apply order", async () => {
331
- // welcome, then starter-ontology.
332
- await applySeedPack(store, welcomePack());
333
- expect((await store.getTagRecord("capture"))!.description).toBe(
334
- NOTES_REQUIRED_TAGS[0]!.description,
335
- );
336
- const afterOntology = await applySeedPack(store, STARTER_ONTOLOGY_PACK);
337
- expect(afterOntology.preservedTagDescriptions).not.toContain("capture");
338
- expect((await store.getTagRecord("capture"))!.description).toBe(
339
- NOTES_REQUIRED_TAGS[0]!.description,
340
- );
341
- });
342
-
343
- test("capture byte-identity anchor holds in the reverse order too", async () => {
344
- // starter-ontology, then welcome.
345
- await applySeedPack(store, STARTER_ONTOLOGY_PACK);
346
- expect((await store.getTagRecord("capture"))!.description).toBe(
347
- NOTES_REQUIRED_TAGS[0]!.description,
348
- );
349
- const afterWelcome = await applySeedPack(store, welcomePack());
350
- expect(afterWelcome.preservedTagDescriptions).not.toContain("capture");
351
- expect((await store.getTagRecord("capture"))!.description).toBe(
352
- NOTES_REQUIRED_TAGS[0]!.description,
353
- );
354
- });
355
- });
356
-
357
293
  describe("vault-info / projection pointer (A2)", () => {
358
294
  test("projection carries getting_started when the note exists", async () => {
359
295
  // Absent before seeding → no pointer.
package/src/routes.ts CHANGED
@@ -42,10 +42,6 @@ import {
42
42
  type ContentRange,
43
43
  } from "../core/src/content-range.ts";
44
44
  import { attachValidationStatus, enforceStrictWrite, applySchemaDefaults } from "../core/src/mcp.ts";
45
- import {
46
- BLOCKED_ATTACHMENT_EXTENSIONS,
47
- ATTACHMENT_MIME_TYPES,
48
- } from "../core/src/attachment/policy.ts";
49
45
  import type { ValidationWarning } from "../core/src/schema-defaults.ts";
50
46
  import { logStrictBypass } from "./scopes.ts";
51
47
  import * as linkOps from "../core/src/links.ts";
@@ -143,7 +139,7 @@ import {
143
139
  type ExpandMode,
144
140
  } from "../core/src/expand.ts";
145
141
  import { join, extname, normalize } from "path";
146
- import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
142
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
147
143
  import { assetsDir, readGlobalConfig, readVaultConfig } from "./config.ts";
148
144
  import { shouldAutoTranscribe } from "./auto-transcribe.ts";
149
145
  // usage.ts imports `assetsDir` from config.ts (neutral ground), so this import
@@ -4444,64 +4440,97 @@ export const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; // 100MB
4444
4440
  */
4445
4441
  export const MAX_REQUEST_BODY_BYTES = MAX_UPLOAD_BYTES + 20 * 1024 * 1024; // 120MB
4446
4442
 
4447
- // Storage upload policy (extension blocklist + MIME lookup) now lives in
4448
- // core/src/attachment/policy.tsthe canonical source shared with the
4449
- // attachment-ticket mint/spend path (vault attachment-tickets design,
4450
- // "shared BLOCKED_EXTENSIONS"), so a blocked extension can never diverge
4451
- // between REST and tickets. See that module's doc comment for the full
4452
- // same-origin-XSS rationale and the MIME curation invariant.
4453
- const BLOCKED_EXTENSIONS = BLOCKED_ATTACHMENT_EXTENSIONS;
4454
- const MIME_TYPES = ATTACHMENT_MIME_TYPES;
4455
-
4456
- /**
4457
- * Parse a single-range `Range: bytes=a-b` header (RFC 7233 §2.1 — single
4458
- * range only, attachments-for-agents design D9, the REST twin of MCP's
4459
- * `content_offset`). Returns `null` — this function's own contract is
4460
- * "serve the full response, unranged" — for a missing header, a
4461
- * MALFORMED value, an unrecognized unit, a multi-range list
4462
- * (`bytes=0-10,20-30` ignored, not an error, per D9), or a range this
4463
- * file can't satisfy (a `start` past EOF). `start`/`end` are both
4464
- * INCLUSIVE byte offsets; `end` is clamped to `total - 1` when the
4465
- * request left it open (`bytes=500-`) or asked past EOF.
4466
- *
4467
- * IMPORTANT this `null` contract is NOT the last word for an
4468
- * unsatisfiable (syntactically-valid but out-of-bounds) range on a real
4469
- * `Bun.serve()` deployment. Live-verified against an actual socket (not
4470
- * the in-process `handleStorage()` call the test suite uses): when the
4471
- * response body is a `Bun.file()` — which `handleStorage`'s full-response
4472
- * branch below always hands back — Bun's OWN runtime transparently
4473
- * reinterprets the incoming request's `Range` header a second time and,
4474
- * for an out-of-bounds range, overrides our 200 with a native
4475
- * **416 Range Not Satisfiable**, regardless of what this function or
4476
- * `handleStorage` returned. That's RFC 7233-correct and is being KEPT,
4477
- * not fought — so in practice, `null` from an out-of-bounds `start`
4478
- * still results in a 200 from `handleStorage`'s own logic, but the byte
4479
- * that actually reaches a real client for that specific case is a 416
4480
- * courtesy of Bun itself. MALFORMED and multi-range headers are NOT
4481
- * range-shaped at all, so Bun's native layer doesn't touch them — those
4482
- * two cases genuinely serve the full 200, in-process harness and real
4483
- * socket alike.
4484
- */
4485
- export function parseByteRangeHeader(header: string | null, total: number): { start: number; end: number } | null {
4486
- if (!header || total <= 0) return null;
4487
- const match = header.match(/^bytes=(\d*)-(\d*)$/);
4488
- if (!match) return null; // malformed, unrecognized unit, or a multi-range list
4489
- const [, startRaw, endRaw] = match;
4490
- if (startRaw === "" && endRaw === "") return null;
4491
-
4492
- if (startRaw === "") {
4493
- // Suffix range: last N bytes (`bytes=-500`).
4494
- const suffixLength = Number(endRaw);
4495
- if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0) return null;
4496
- return { start: Math.max(0, total - suffixLength), end: total - 1 };
4497
- }
4498
-
4499
- const start = Number(startRaw);
4500
- if (!Number.isSafeInteger(start) || start < 0 || start >= total) return null;
4501
- const end = endRaw === "" ? total - 1 : Math.min(Number(endRaw), total - 1);
4502
- if (!Number.isSafeInteger(end) || end < start) return null;
4503
- return { start, end };
4504
- }
4443
+ // Storage upload policy: DENY-LIST (vault#517). A knowledge vault stores
4444
+ // arbitrary files ebooks, office docs, datasets, archives, binaries — so we
4445
+ // accept ANY upload EXCEPT the handful of types a browser can execute as
4446
+ // active content in our origin when served back from /storage/. (The prior
4447
+ // allowlist rejected the long tail: .epub/.csv/.zip/… all came back "File type
4448
+ // not allowed".)
4449
+ //
4450
+ // BLOCKED same-origin-XSS / active-content set:
4451
+ // .html/.htm/.xhtml/.shtml/.xht HTML — embeds <script>
4452
+ // .svg XML image — embeds <script>
4453
+ // .xml can carry XSLT / be parsed as XHTML
4454
+ // .js/.mjs/.cjs JavaScript
4455
+ // .css style-injection / UI-redress vector
4456
+ //
4457
+ // Two independent guards keep every STORED file inert when served:
4458
+ // 1. Only the curated MIME_TYPES below map to a real (always passive) type;
4459
+ // every other extension serves as application/octet-stream a download,
4460
+ // never rendered.
4461
+ // 2. The GET byte-serve response pins `X-Content-Type-Options: nosniff`, so
4462
+ // a browser can't sniff an octet-stream body into an executable type.
4463
+ // The blocklist is belt-and-suspenders on top of those: even if a future MIME
4464
+ // entry or an upstream proxy weakened (1) or (2), these extensions still never
4465
+ // land on disk. If a future use case needs SVG, sanitize on read (strip
4466
+ // <script>/<foreignObject>) and revisit.
4467
+ const BLOCKED_EXTENSIONS = new Set([
4468
+ ".html", ".htm", ".xhtml", ".shtml", ".xht",
4469
+ ".svg",
4470
+ ".xml",
4471
+ ".js", ".mjs", ".cjs",
4472
+ ".css",
4473
+ ]);
4474
+
4475
+ // Explicit MIME types for the commonly-previewed formats. Anything accepted
4476
+ // but absent here serves as application/octet-stream a download, never
4477
+ // rendered (e.g. .pages/.key/.numbers/.azw3/.exe/arbitrary binaries). None of
4478
+ // these map to an active type (text/html, image/svg+xml), so a served asset
4479
+ // can't execute script; `nosniff` on the GET response makes that ironclad.
4480
+ //
4481
+ // INVARIANT: never add an entry that maps to a browser-active type
4482
+ // text/html, image/svg+xml, application/xhtml+xml, text/javascript,
4483
+ // application/wasm, text/css. Doing so re-enables same-origin execution for
4484
+ // that extension (and would mean it must also join BLOCKED_EXTENSIONS).
4485
+ const MIME_TYPES: Record<string, string> = {
4486
+ // Audio
4487
+ ".wav": "audio/wav",
4488
+ ".mp3": "audio/mpeg",
4489
+ ".m4a": "audio/mp4",
4490
+ ".ogg": "audio/ogg",
4491
+ ".oga": "audio/ogg",
4492
+ ".opus": "audio/opus",
4493
+ ".aac": "audio/aac",
4494
+ ".flac": "audio/flac",
4495
+ ".webm": "audio/webm",
4496
+ // Image
4497
+ ".png": "image/png",
4498
+ ".jpg": "image/jpeg",
4499
+ ".jpeg": "image/jpeg",
4500
+ ".gif": "image/gif",
4501
+ ".webp": "image/webp",
4502
+ ".bmp": "image/bmp",
4503
+ ".tiff": "image/tiff",
4504
+ ".tif": "image/tiff",
4505
+ ".heic": "image/heic",
4506
+ ".heif": "image/heif",
4507
+ ".avif": "image/avif",
4508
+ // Video
4509
+ ".mp4": "video/mp4",
4510
+ ".m4v": "video/x-m4v",
4511
+ ".mov": "video/quicktime",
4512
+ // Documents / ebooks / data
4513
+ ".pdf": "application/pdf",
4514
+ ".epub": "application/epub+zip",
4515
+ ".mobi": "application/x-mobipocket-ebook",
4516
+ ".txt": "text/plain; charset=utf-8",
4517
+ ".md": "text/markdown; charset=utf-8",
4518
+ ".markdown": "text/markdown; charset=utf-8",
4519
+ ".rtf": "application/rtf",
4520
+ ".csv": "text/csv; charset=utf-8",
4521
+ ".tsv": "text/tab-separated-values; charset=utf-8",
4522
+ ".json": "application/json; charset=utf-8",
4523
+ ".doc": "application/msword",
4524
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
4525
+ ".ppt": "application/vnd.ms-powerpoint",
4526
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
4527
+ ".xls": "application/vnd.ms-excel",
4528
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
4529
+ ".odt": "application/vnd.oasis.opendocument.text",
4530
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
4531
+ ".odp": "application/vnd.oasis.opendocument.presentation",
4532
+ ".zip": "application/zip",
4533
+ };
4505
4534
 
4506
4535
  export async function handleStorage(
4507
4536
  req: Request,
@@ -4665,38 +4694,12 @@ export async function handleStorage(
4665
4694
  const stat = statSync(filePath);
4666
4695
  const ext = extname(filePath).toLowerCase();
4667
4696
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
4668
- const total = stat.size;
4669
-
4670
- // vault attachments-for-agents design (D9) — the REST twin of MCP's
4671
- // `content_offset`. `Bun.file(filePath)` resolves lazily: `.slice()`
4672
- // creates a bounded view and only the requested bytes are actually read
4673
- // on `.arrayBuffer()`/streaming, replacing the prior whole-file
4674
- // `readFileSync` (a standing memory smell on a large attachment, e.g. a
4675
- // 90 MB video) on BOTH the ranged and full-file paths below.
4676
- const bunFile = Bun.file(filePath);
4677
- const range = parseByteRangeHeader(req.headers.get("range"), total);
4678
-
4679
- if (range) {
4680
- const { start, end } = range; // inclusive
4681
- return new Response(bunFile.slice(start, end + 1), {
4682
- status: 206,
4683
- headers: {
4684
- "Content-Type": contentType,
4685
- "Content-Length": String(end - start + 1),
4686
- "Content-Range": `bytes ${start}-${end}/${total}`,
4687
- "Accept-Ranges": "bytes",
4688
- // Defense-in-depth: never let a browser MIME-sniff a stored asset
4689
- // into an active type — see the full-response branch below.
4690
- "X-Content-Type-Options": "nosniff",
4691
- },
4692
- });
4693
- }
4697
+ const fileBuffer = readFileSync(filePath);
4694
4698
 
4695
- return new Response(bunFile, {
4699
+ return new Response(fileBuffer, {
4696
4700
  headers: {
4697
4701
  "Content-Type": contentType,
4698
- "Content-Length": String(total),
4699
- "Accept-Ranges": "bytes",
4702
+ "Content-Length": String(stat.size),
4700
4703
  // Defense-in-depth: never let a browser MIME-sniff a stored asset into
4701
4704
  // an active type (e.g. an octet-stream body sniffed as text/html).
4702
4705
  // Combined with the upload blocklist (no .svg/.html) this closes the
package/src/routing.ts CHANGED
@@ -103,7 +103,6 @@ import {
103
103
  } from "./mirror-routes.ts";
104
104
  import { getMirrorManager } from "./mirror-registry.ts";
105
105
  import { buildUsageReport } from "./usage.ts";
106
- import { handleTicketSpend } from "./attachment-tickets.ts";
107
106
 
108
107
  /**
109
108
  * Decorate a 401 response from the MCP endpoint with the RFC 9728 challenge
@@ -491,22 +490,6 @@ export async function route(
491
490
  return handleAuthorizationServer(req, vaultName);
492
491
  }
493
492
 
494
- // Attachment ticket spend (attachment-tickets design, Wave 1) — no auth
495
- // beyond the ticket itself; the ticket IS the credential (single-use,
496
- // short TTL, scoped to exactly one upload slot or one attachment's
497
- // bytes). Deliberately placed BEFORE `authenticateVaultRequest` below —
498
- // a bearer-less runtime (a bare `curl`) must be able to spend a ticket
499
- // without ever holding this vault's API key. The path segment
500
- // (`/tickets/<id>`) is outside the authed `/api` tree and outside
501
- // `/mcp` on purpose — see `request-attachment-upload`/`-download`
502
- // (core/src/mcp.ts) for where tickets are minted.
503
- const vaultTicketMatch = subpath.match(/^\/tickets\/([^/]+)$/);
504
- if (vaultTicketMatch) {
505
- const ticketId = decodeURIComponent(vaultTicketMatch[1]!);
506
- const store = getVaultStore(vaultName);
507
- return handleTicketSpend(req, ticketId, vaultName, store);
508
- }
509
-
510
493
  // ---------------------------------------------------------------------
511
494
  // Authenticated surface
512
495
  // ---------------------------------------------------------------------
package/src/server.ts CHANGED
@@ -21,7 +21,6 @@ import { migrateVaultKeys } from "./token-store.ts";
21
21
  import { resolveFirstBootVaultName, reservedNameSquatWarnings } from "./vault-name.ts";
22
22
  import { getVaultStore, getVaultNameForStore, getSharedEmbeddingProvider } from "./vault-store.ts";
23
23
  import { EmbeddingWorker, registerEmbeddingHook } from "./embedding-worker.ts";
24
- import { startAttachmentTicketSweep, stopAttachmentTicketSweep } from "./attachment-tickets.ts";
25
24
  import { seedOnboardingNotesBestEffort } from "./onboarding-seed.ts";
26
25
  import { defaultHookRegistry } from "../core/src/hooks.ts";
27
26
  import { registerTriggers } from "./triggers.ts";
@@ -238,11 +237,6 @@ const embeddingWorker = new EmbeddingWorker({
238
237
  registerEmbeddingHook(defaultHookRegistry, embeddingWorker, (store) => getVaultNameForStore(store as never));
239
238
  embeddingWorker.start();
240
239
 
241
- // Attachment-ticket sweep (vault#612) — drops expired-unspent tickets from
242
- // the in-process store so an abandoned mint (an agent that never curls)
243
- // doesn't sit in memory forever. See src/attachment-tickets.ts.
244
- startAttachmentTicketSweep();
245
-
246
240
  if (process.env.VAULT_AUTH_TOKEN?.trim()) {
247
241
  console.log("[auth] VAULT_AUTH_TOKEN set — server-wide operator bearer active");
248
242
  }
@@ -625,7 +619,6 @@ async function shutdown(signal: string): Promise<void> {
625
619
  // Then drain hooks + stop the transcription/embedding workers in
626
620
  // parallel.
627
621
  embeddingWorker.stop();
628
- stopAttachmentTicketSweep();
629
622
  await Promise.all([
630
623
  defaultHookRegistry.drain(),
631
624
  transcriptionWorker?.stop() ?? Promise.resolve(),