@openparachute/vault 0.7.3-rc.13 → 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 (53) 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/auth-hub-jwt.test.ts +1 -118
  25. package/src/auth.ts +0 -64
  26. package/src/cli.ts +1 -5
  27. package/src/contract-errors.test.ts +1 -2
  28. package/src/mcp-http.ts +4 -33
  29. package/src/mcp-tools.ts +14 -61
  30. package/src/oauth-discovery.ts +0 -31
  31. package/src/onboarding-seed.test.ts +0 -64
  32. package/src/routes.ts +95 -92
  33. package/src/routing.test.ts +4 -229
  34. package/src/routing.ts +23 -152
  35. package/src/scopes.ts +0 -22
  36. package/src/server.ts +0 -7
  37. package/src/storage.test.ts +1 -200
  38. package/src/transcription-worker.test.ts +0 -151
  39. package/src/transcription-worker.ts +52 -113
  40. package/src/vault.test.ts +11 -48
  41. package/core/src/attachment/bytes-provider.ts +0 -65
  42. package/core/src/attachment/policy.test.ts +0 -66
  43. package/core/src/attachment/policy.ts +0 -131
  44. package/core/src/attachment/tickets.test.ts +0 -45
  45. package/core/src/attachment/tickets.ts +0 -117
  46. package/core/src/attachment-tickets-tool.test.ts +0 -286
  47. package/core/src/display-title.test.ts +0 -190
  48. package/core/src/lede.test.ts +0 -96
  49. package/core/src/search-title-boost.test.ts +0 -125
  50. package/src/attachment-bytes.ts +0 -68
  51. package/src/attachment-tickets.test.ts +0 -475
  52. package/src/attachment-tickets.ts +0 -340
  53. package/src/read-attachment.test.ts +0 -436
@@ -28,7 +28,7 @@ import { tmpdir } from "os";
28
28
  import { generateKeyPair, exportJWK, SignJWT } from "jose";
29
29
  import { writeVaultConfig, readVaultConfig } from "./config.ts";
30
30
  import { getVaultStore, clearVaultStoreCache } from "./vault-store.ts";
31
- import { authenticateVaultRequest, authenticateGlobalRequest, deriveVaultFromToken } from "./auth.ts";
31
+ import { authenticateVaultRequest, authenticateGlobalRequest } from "./auth.ts";
32
32
  import { resetJwksCache, resetRevocationCache } from "./hub-jwt.ts";
33
33
 
34
34
  interface Keypair {
@@ -705,120 +705,3 @@ describe("pvt_* DROP (vault#282 Stage 2 — unvalidatable)", () => {
705
705
  expect("error" in result).toBe(false);
706
706
  });
707
707
  });
708
-
709
- // ---------------------------------------------------------------------------
710
- // deriveVaultFromToken — the derivation precedence for the canonical root
711
- // `/mcp` endpoint (U1). This function READS a validated token's claims to name
712
- // the target vault; it never authorizes (the router re-dispatches through the
713
- // full per-vault machinery). These cases isolate the three naming sources —
714
- // narrowed scope / `aud=vault.<name>` / single-element `vault_scope` — and the
715
- // fail-closed rules (agree → one name; disagree or none → `not_derivable`).
716
- // The end-to-end routing.test.ts covers the wired behavior; this pins the
717
- // precedence logic directly.
718
- // ---------------------------------------------------------------------------
719
- describe("deriveVaultFromToken — root /mcp vault derivation (U1)", () => {
720
- test("all three sources agree → that vault", async () => {
721
- const token = await signJwt(kp, {
722
- iss: fixture.origin,
723
- aud: "vault.journal",
724
- scope: "vault:journal:write",
725
- vaultScope: ["journal"],
726
- });
727
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ vaultName: "journal" });
728
- });
729
-
730
- test("narrowed scope alone names the vault (non-vault aud, no vault_scope)", async () => {
731
- const token = await signJwt(kp, {
732
- iss: fixture.origin,
733
- aud: "urn:opaque-resource",
734
- scope: "vault:journal:read",
735
- });
736
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ vaultName: "journal" });
737
- });
738
-
739
- test("aud=vault.<name> alone names the vault (broad scope names nothing)", async () => {
740
- const token = await signJwt(kp, {
741
- iss: fixture.origin,
742
- aud: "vault.journal",
743
- scope: "vault:read",
744
- });
745
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ vaultName: "journal" });
746
- });
747
-
748
- test("single-element vault_scope alone names the vault", async () => {
749
- const token = await signJwt(kp, {
750
- iss: fixture.origin,
751
- aud: "urn:opaque-resource",
752
- scope: "vault:read",
753
- vaultScope: ["journal"],
754
- });
755
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ vaultName: "journal" });
756
- });
757
-
758
- test("multi-element vault_scope is NOT a single name → not_derivable (nothing else names)", async () => {
759
- // Phase-2 multi-vault shape: a multi-element vault_scope doesn't name ONE
760
- // vault, so at the single-vault root it can't be the sole source.
761
- const token = await signJwt(kp, {
762
- iss: fixture.origin,
763
- aud: "urn:opaque-resource",
764
- scope: "vault:read",
765
- vaultScope: ["journal", "work"],
766
- });
767
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ error: "not_derivable" });
768
- });
769
-
770
- test("sources disagree (scope vs aud) → not_derivable, never a guess", async () => {
771
- const token = await signJwt(kp, {
772
- iss: fixture.origin,
773
- aud: "vault.work",
774
- scope: "vault:journal:write",
775
- });
776
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ error: "not_derivable" });
777
- });
778
-
779
- test("no source names a vault → not_derivable", async () => {
780
- const token = await signJwt(kp, {
781
- iss: fixture.origin,
782
- aud: "urn:opaque-resource",
783
- scope: "vault:read",
784
- });
785
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ error: "not_derivable" });
786
- });
787
-
788
- test("no bearer → no_bearer", async () => {
789
- const req = new Request("https://vault.test/mcp");
790
- expect(await deriveVaultFromToken(req)).toEqual({ error: "no_bearer" });
791
- });
792
-
793
- test("non-JWT bearer (operator / legacy shape) names no vault → not_derivable", async () => {
794
- // The operator VAULT_AUTH_TOKEN and legacy YAML keys are vault-agnostic —
795
- // they can't route the token-derived root endpoint (they keep working at
796
- // the per-vault URL).
797
- expect(await deriveVaultFromToken(bearer("opaque-operator-secret"))).toEqual({
798
- error: "not_derivable",
799
- });
800
- });
801
-
802
- test("expired JWT → not_derivable (validated with the full trust kernel)", async () => {
803
- const token = await signJwt(kp, {
804
- iss: fixture.origin,
805
- aud: "vault.journal",
806
- scope: "vault:journal:write",
807
- ttlSeconds: -10, // already expired
808
- });
809
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ error: "not_derivable" });
810
- });
811
-
812
- test("revoked JWT → not_derivable (revocation runs in derivation too)", async () => {
813
- const jti = "u1-derive-revoked";
814
- const token = await signJwt(kp, {
815
- iss: fixture.origin,
816
- aud: "vault.journal",
817
- scope: "vault:journal:write",
818
- jti,
819
- });
820
- fixture.setRevoked([jti]);
821
- resetRevocationCache();
822
- expect(await deriveVaultFromToken(bearer(token))).toEqual({ error: "not_derivable" });
823
- });
824
- });
package/src/auth.ts CHANGED
@@ -30,7 +30,6 @@ import {
30
30
  hasScope,
31
31
  hasScopeForVault,
32
32
  legacyPermissionToScopes,
33
- narrowedVaultNames,
34
33
  SCOPE_ADMIN,
35
34
  SCOPE_READ,
36
35
  SCOPE_WRITE,
@@ -713,66 +712,3 @@ export async function authenticateGlobalRequest(
713
712
  }
714
713
  return { error: Response.json({ error: "Unauthorized", message: "Invalid API key" }, { status: 401 }) };
715
714
  }
716
-
717
- /**
718
- * Outcome of deriving a target vault from a request's bearer at the canonical
719
- * root `/mcp` endpoint (U1). `vaultName` on success; a coarse failure `error`
720
- * otherwise. Both failure reasons map to the SAME 401 + root-PRM challenge at
721
- * the router — the distinction exists for logging/tests, never leaks to the
722
- * client. `no_bearer` = no credential presented; `not_derivable` = a credential
723
- * that names no single vault (non-JWT operator/legacy bearer, invalid /
724
- * expired / revoked JWT, or a JWT whose scope / `aud` / `vault_scope` sources
725
- * name zero or conflicting vaults).
726
- */
727
- export type VaultDerivation =
728
- | { vaultName: string }
729
- | { error: "no_bearer" | "not_derivable" };
730
-
731
- /**
732
- * Derive the target vault from a request's bearer token WITHOUT authorizing the
733
- * request. The caller re-dispatches the derived name through the full per-vault
734
- * auth machinery (`authenticateVaultRequest`), which re-validates the token
735
- * WITH the audience pin — derive-then-redispatch, so a bad derivation FAILS the
736
- * inner check rather than bypassing it (defense in depth). This function only
737
- * reads the claims well enough to name the vault; it is never the authorization
738
- * gate.
739
- *
740
- * Only hub-issued JWTs name a vault. The operator `VAULT_AUTH_TOKEN` and legacy
741
- * YAML keys are vault-agnostic (they name no resource — see scopes.ts), so they
742
- * return `not_derivable` here and keep working at the URL-addressed
743
- * `/vault/<name>/*` surface. The JWT is validated with the SAME scope-guard
744
- * trust kernel the per-vault path uses (signature, `iss` pin, `jti` +
745
- * revocation, expiry) but WITHOUT `expectedAudience` — at the root we don't yet
746
- * know which audience to expect; that pin is re-applied by the re-dispatch.
747
- *
748
- * From the validated claims, three independent sources can name a vault:
749
- * 1. a narrowed `vault:<name>:<verb>` scope,
750
- * 2. an `aud` of the form `vault.<name>`,
751
- * 3. a single-element `vault_scope` claim.
752
- * On a hub-minted token these AGREE. We collect every name any source provides
753
- * and require EXACTLY ONE distinct name: zero (nothing named a vault) and
754
- * two-or-more (the sources disagree) both fail closed with `not_derivable`. We
755
- * never pick a winner from a precedence order — an ambiguous or unnamed token
756
- * gets the discovery challenge, not a silent guess.
757
- */
758
- export async function deriveVaultFromToken(req: Request): Promise<VaultDerivation> {
759
- const key = extractApiKey(req);
760
- if (!key) return { error: "no_bearer" };
761
- if (!looksLikeJwt(key)) return { error: "not_derivable" };
762
- let claims;
763
- try {
764
- // No `expectedAudience`: the per-vault trust kernel minus the aud pin
765
- // (which the re-dispatch re-applies). A bad signature / iss / expiry /
766
- // revoked jti throws here → not_derivable → the standard 401 challenge.
767
- claims = await validateHubJwt(key, {});
768
- } catch {
769
- return { error: "not_derivable" };
770
- }
771
- const named = new Set<string>();
772
- for (const name of narrowedVaultNames(claims.scopes)) named.add(name);
773
- const audMatch = claims.aud?.match(/^vault\.(.+)$/);
774
- if (audMatch) named.add(audMatch[1]!);
775
- if (claims.vaultScope.length === 1) named.add(claims.vaultScope[0]!);
776
- if (named.size !== 1) return { error: "not_derivable" };
777
- return { vaultName: [...named][0]! };
778
- }
package/src/cli.ts CHANGED
@@ -4842,11 +4842,7 @@ async function cmdAddPack(args: string[]) {
4842
4842
  console.log(` = note ${path} (already exists — left untouched)`);
4843
4843
  }
4844
4844
  for (const tag of result.tags) {
4845
- if (result.preservedTagDescriptions.includes(tag)) {
4846
- console.log(` ~ tag ${tag} (kept user description)`);
4847
- } else {
4848
- console.log(` ~ tag ${tag} (upserted)`);
4849
- }
4845
+ console.log(` ~ tag ${tag} (upserted)`);
4850
4846
  }
4851
4847
  if (result.seededNotes.length === 0 && result.tags.length === 0) {
4852
4848
  console.log(" Nothing to add — everything was already in place.");
@@ -315,7 +315,7 @@ describe("contract: error taxonomy — #554 (flipped from todo)", () => {
315
315
  expect(record?.fields ?? null).toBeFalsy();
316
316
  });
317
317
 
318
- // Positive control — every one of the recognized types (indexable or
318
+ // Positive control — every one of the seven recognized types (indexable or
319
319
  // not) is accepted without complaint.
320
320
  it("REST PUT /api/tags/:name accepts every recognized field type", async () => {
321
321
  const req = new Request("http://localhost/api/tags/widget", {
@@ -329,7 +329,6 @@ describe("contract: error taxonomy — #554 (flipped from todo)", () => {
329
329
  e: { type: "array" },
330
330
  f: { type: "object" },
331
331
  g: { type: "reference" },
332
- h: { type: "date" },
333
332
  },
334
333
  }),
335
334
  });
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);
@@ -27,7 +27,6 @@
27
27
  */
28
28
 
29
29
  import { getHubOrigin } from "./hub-jwt.ts";
30
- import { SCOPE_READ, SCOPE_WRITE } from "./scopes.ts";
31
30
 
32
31
  /**
33
32
  * OAuth scopes vault publishes through discovery, RESOURCE-NARROWED to the
@@ -88,36 +87,6 @@ export function handleProtectedResource(req: Request, vaultName: string): Respon
88
87
  });
89
88
  }
90
89
 
91
- /**
92
- * Protected Resource Metadata (RFC 9728) for the CANONICAL ROOT `/mcp`
93
- * endpoint (U1). Same document shape as `handleProtectedResource`, with two
94
- * deliberate differences that follow from the root being vault-AGNOSTIC (the
95
- * vault is derived from the token, not the URL):
96
- *
97
- * - `resource` is the origin-root `<base>/mcp`, not a `/vault/<name>/mcp`.
98
- * - `scopes_supported` advertises the UN-NARROWED forms `vault:read` /
99
- * `vault:write` (there's no vault name to narrow to here). A spec-following
100
- * client reads these and requests the broad forms; the hub's consent picker
101
- * narrows the grant to a chosen vault at authorization time (that path
102
- * already exists hub-side), minting a token stamped with a
103
- * `vault:<name>:<verb>` scope + `aud=vault.<name>` — exactly what the root
104
- * endpoint derives the target vault from. `admin` is intentionally omitted:
105
- * the interactive connect flow grants read/write; admin is an operator
106
- * concern, not something the consent picker offers.
107
- *
108
- * Served at the RFC 9728 §3.1 path-insertion location for the root resource:
109
- * `/.well-known/oauth-protected-resource/mcp`.
110
- */
111
- export function handleRootProtectedResource(req: Request): Response {
112
- const base = getBaseUrl(req);
113
- return Response.json({
114
- resource: `${base}/mcp`,
115
- authorization_servers: [getHubOrigin()],
116
- scopes_supported: [SCOPE_READ, SCOPE_WRITE],
117
- bearer_methods_supported: ["header"],
118
- });
119
- }
120
-
121
90
  /**
122
91
  * OAuth 2.0 Authorization Server Metadata (RFC 8414).
123
92
  *
@@ -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.