@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.
- package/README.md +3 -5
- package/core/src/conformance.ts +1 -2
- package/core/src/content-range.test.ts +0 -127
- package/core/src/content-range.ts +0 -100
- package/core/src/contract-typed-index.test.ts +3 -4
- package/core/src/core.test.ts +4 -521
- package/core/src/expand.ts +3 -11
- package/core/src/indexed-fields.test.ts +3 -9
- package/core/src/indexed-fields.ts +1 -9
- package/core/src/mcp.ts +10 -601
- package/core/src/notes.ts +4 -201
- package/core/src/schema-defaults.ts +1 -85
- package/core/src/search-fts-v25.test.ts +1 -9
- package/core/src/search-query.test.ts +0 -42
- package/core/src/search-query.ts +0 -27
- package/core/src/seed-packs.test.ts +1 -117
- package/core/src/seed-packs.ts +1 -217
- package/core/src/store.ts +3 -59
- package/core/src/tag-schemas.ts +12 -27
- package/core/src/types.ts +1 -7
- package/core/src/vault-projection.ts +0 -55
- package/package.json +1 -1
- package/src/add-pack.test.ts +0 -32
- package/src/cli.ts +1 -5
- package/src/contract-errors.test.ts +1 -2
- package/src/mcp-http.ts +4 -33
- package/src/mcp-tools.ts +14 -61
- package/src/onboarding-seed.test.ts +0 -64
- package/src/routes.ts +95 -92
- package/src/routing.ts +0 -17
- package/src/server.ts +0 -7
- package/src/storage.test.ts +1 -200
- package/src/transcription-worker.test.ts +0 -151
- package/src/transcription-worker.ts +52 -113
- package/src/vault.test.ts +11 -48
- package/core/src/attachment/bytes-provider.ts +0 -65
- package/core/src/attachment/policy.test.ts +0 -66
- package/core/src/attachment/policy.ts +0 -131
- package/core/src/attachment/tickets.test.ts +0 -45
- package/core/src/attachment/tickets.ts +0 -117
- package/core/src/attachment-tickets-tool.test.ts +0 -286
- package/core/src/display-title.test.ts +0 -190
- package/core/src/lede.test.ts +0 -96
- package/core/src/search-title-boost.test.ts +0 -125
- package/src/attachment-bytes.ts +0 -68
- package/src/attachment-tickets.test.ts +0 -475
- package/src/attachment-tickets.ts +0 -340
- 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
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 (
|
|
4448
|
-
//
|
|
4449
|
-
//
|
|
4450
|
-
//
|
|
4451
|
-
//
|
|
4452
|
-
//
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
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
|
|
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(
|
|
4699
|
+
return new Response(fileBuffer, {
|
|
4696
4700
|
headers: {
|
|
4697
4701
|
"Content-Type": contentType,
|
|
4698
|
-
"Content-Length": String(
|
|
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(),
|