@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.
- 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/auth-hub-jwt.test.ts +1 -118
- package/src/auth.ts +0 -64
- 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/oauth-discovery.ts +0 -31
- package/src/onboarding-seed.test.ts +0 -64
- package/src/routes.ts +95 -92
- package/src/routing.test.ts +4 -229
- package/src/routing.ts +23 -152
- package/src/scopes.ts +0 -22
- 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/auth-hub-jwt.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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);
|
package/src/oauth-discovery.ts
CHANGED
|
@@ -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.
|