@openparachute/vault 0.4.9-rc.9 → 0.5.0-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 +51 -54
- package/core/src/core.test.ts +4 -1
- package/core/src/indexed-fields.test.ts +151 -0
- package/core/src/indexed-fields.ts +98 -0
- package/core/src/mcp.ts +66 -43
- package/core/src/notes.ts +26 -2
- package/core/src/portable-md.test.ts +52 -0
- package/core/src/portable-md.ts +48 -0
- package/core/src/schema.ts +87 -14
- package/core/src/store.ts +117 -0
- package/core/src/types.ts +28 -0
- package/package.json +2 -2
- package/src/auth-hub-jwt.test.ts +191 -11
- package/src/auth-status.ts +12 -5
- package/src/auth.test.ts +135 -219
- package/src/auth.ts +158 -107
- package/src/cli.ts +306 -224
- package/src/config.ts +12 -4
- package/src/export-watch.test.ts +23 -0
- package/src/export-watch.ts +14 -0
- package/src/git-preflight.test.ts +70 -0
- package/src/git-preflight.ts +68 -0
- package/src/hub-jwt.test.ts +27 -2
- package/src/hub-jwt.ts +10 -0
- package/src/init-summary.test.ts +4 -4
- package/src/init-summary.ts +36 -10
- package/src/mcp-config.test.ts +4 -2
- package/src/mcp-http.ts +24 -3
- package/src/mcp-install-interactive.test.ts +33 -71
- package/src/mcp-install-interactive.ts +23 -76
- package/src/mcp-install.test.ts +156 -55
- package/src/mcp-install.ts +109 -3
- package/src/mcp-tools.ts +249 -74
- package/src/mirror-config.test.ts +107 -0
- package/src/mirror-config.ts +275 -9
- package/src/mirror-credentials.test.ts +168 -17
- package/src/mirror-credentials.ts +155 -32
- package/src/mirror-deps.ts +25 -16
- package/src/mirror-import.test.ts +122 -16
- package/src/mirror-import.ts +50 -16
- package/src/mirror-manager.test.ts +51 -0
- package/src/mirror-manager.ts +116 -22
- package/src/mirror-per-vault.test.ts +519 -0
- package/src/mirror-registry.ts +91 -14
- package/src/mirror-routes.test.ts +81 -21
- package/src/mirror-routes.ts +90 -16
- package/src/routes.ts +39 -2
- package/src/routing.test.ts +203 -118
- package/src/routing.ts +46 -59
- package/src/scopes.test.ts +0 -86
- package/src/scopes.ts +9 -97
- package/src/server.ts +102 -34
- package/src/storage.test.ts +132 -7
- package/src/token-store.test.ts +88 -169
- package/src/token-store.ts +123 -249
- package/src/vault-create.test.ts +12 -4
- package/src/vault.test.ts +408 -103
- package/web/ui/dist/assets/index-DDRo6F4u.js +60 -0
- package/web/ui/dist/index.html +1 -1
- package/src/tokens-routes.test.ts +0 -727
- package/src/tokens-routes.ts +0 -392
- package/web/ui/dist/assets/index-Degr8snN.js +0 -60
package/core/src/types.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
2
|
+
import type { PrunedField } from "./indexed-fields.js";
|
|
2
3
|
|
|
3
4
|
// ---- Re-exports ----
|
|
4
5
|
|
|
5
6
|
export type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
7
|
+
export type { PrunedField } from "./indexed-fields.js";
|
|
6
8
|
|
|
7
9
|
// ---- Note ----
|
|
8
10
|
|
|
@@ -278,6 +280,24 @@ export interface Store {
|
|
|
278
280
|
deleteTagSchema(tag: string): Promise<boolean>;
|
|
279
281
|
getTagSchemaMap(): Promise<Record<string, { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }>>;
|
|
280
282
|
|
|
283
|
+
// Indexed-field lifecycle — generated columns + indexes on `notes` derived
|
|
284
|
+
// from tag-declared `indexed: true` fields. See core/src/indexed-fields.ts.
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Prune orphaned `indexed_fields` declarers — declarer tags with no `tags`
|
|
288
|
+
* row. Fields left with no live declarer are dropped wholesale; co-declared
|
|
289
|
+
* fields keep their column and lose only the dead declarers. `dryRun`
|
|
290
|
+
* (default true) returns the plan without mutating.
|
|
291
|
+
*/
|
|
292
|
+
pruneIndexedFields(opts?: { dryRun?: boolean }): Promise<PrunedField[]>;
|
|
293
|
+
/**
|
|
294
|
+
* Replay `declareField` for every `indexed: true` field across all current
|
|
295
|
+
* tag records, materializing the backing columns + indexes. Idempotent —
|
|
296
|
+
* used by the import path so a fresh import has the same columns a live
|
|
297
|
+
* vault would. Returns the count of (tag, field) declarations replayed.
|
|
298
|
+
*/
|
|
299
|
+
reconcileDeclaredIndexes(): Promise<number>;
|
|
300
|
+
|
|
281
301
|
// Tag records — full v14 identity row (description + fields + typed
|
|
282
302
|
// relationships + parent_names + timestamps). See
|
|
283
303
|
// parachute-patterns/patterns/tag-data-model.md.
|
|
@@ -322,6 +342,14 @@ export interface Store {
|
|
|
322
342
|
addAttachment(noteId: string, path: string, mimeType: string, metadata?: Record<string, unknown>): Promise<Attachment>;
|
|
323
343
|
getAttachments(noteId: string): Promise<Attachment[]>;
|
|
324
344
|
getAttachment(attachmentId: string): Promise<Attachment | null>;
|
|
345
|
+
/**
|
|
346
|
+
* Reverse-lookup attachment rows by their vault-internal relative `path`
|
|
347
|
+
* (`<date>/<filename>`). Returns every row sharing that path (a single
|
|
348
|
+
* on-disk asset can be referenced by >1 row). Used by the raw
|
|
349
|
+
* `/api/storage/<date>/<file>` serve path to map a requested file back to
|
|
350
|
+
* its owning note(s) for tag-scope enforcement.
|
|
351
|
+
*/
|
|
352
|
+
getAttachmentsByPath(path: string): Promise<Attachment[]>;
|
|
325
353
|
setAttachmentMetadata(attachmentId: string, metadata: Record<string, unknown>): Promise<void>;
|
|
326
354
|
deleteAttachment(noteId: string, attachmentId: string): Promise<{ deleted: boolean; path: string | null; orphaned: boolean }>;
|
|
327
355
|
listAttachmentsByTranscribeStatus(status: "pending" | "failed" | "done", limit?: number): Promise<Attachment[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openparachute/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-rc.2",
|
|
4
4
|
"description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
|
|
5
5
|
"module": "src/cli.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
30
|
-
"@openparachute/scope-guard": "^0.
|
|
30
|
+
"@openparachute/scope-guard": "^0.4.0-rc.2",
|
|
31
31
|
"jose": "^6.2.2",
|
|
32
32
|
"otpauth": "^9.5.0",
|
|
33
33
|
"qrcode-terminal": "^0.12.0"
|
package/src/auth-hub-jwt.test.ts
CHANGED
|
@@ -105,6 +105,13 @@ interface SignOpts {
|
|
|
105
105
|
* a hub-minted admin token; provide `["<name>"]` for a non-admin user.
|
|
106
106
|
*/
|
|
107
107
|
vaultScope?: string[];
|
|
108
|
+
/**
|
|
109
|
+
* `permissions` claim (auth-unification arc, C0). Undefined → omit
|
|
110
|
+
* entirely (today's hub-JWT shape → unscoped). Provide
|
|
111
|
+
* `{ scoped_tags: [...] }` for a tag-scoped token, or a deliberately
|
|
112
|
+
* malformed value to exercise the fail-closed path.
|
|
113
|
+
*/
|
|
114
|
+
permissions?: unknown;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
async function signJwt(kp: Keypair, opts: SignOpts): Promise<string> {
|
|
@@ -115,6 +122,7 @@ async function signJwt(kp: Keypair, opts: SignOpts): Promise<string> {
|
|
|
115
122
|
client_id: "test-client",
|
|
116
123
|
};
|
|
117
124
|
if (opts.vaultScope !== undefined) payload.vault_scope = opts.vaultScope;
|
|
125
|
+
if (opts.permissions !== undefined) payload.permissions = opts.permissions;
|
|
118
126
|
return await new SignJWT(payload)
|
|
119
127
|
.setProtectedHeader({ alg: "RS256", kid: kp.kid })
|
|
120
128
|
.setIssuer(opts.iss)
|
|
@@ -183,7 +191,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
183
191
|
const config = readVaultConfig("journal")!;
|
|
184
192
|
const store = getVaultStore("journal");
|
|
185
193
|
|
|
186
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
194
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
187
195
|
expect("error" in result).toBe(false);
|
|
188
196
|
if (!("error" in result)) {
|
|
189
197
|
expect(result.permission).toBe("full");
|
|
@@ -202,7 +210,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
202
210
|
const config = readVaultConfig("journal")!;
|
|
203
211
|
const store = getVaultStore("journal");
|
|
204
212
|
|
|
205
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
213
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
206
214
|
expect("error" in result).toBe(false);
|
|
207
215
|
if (!("error" in result)) expect(result.permission).toBe("read");
|
|
208
216
|
});
|
|
@@ -217,7 +225,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
217
225
|
const config = readVaultConfig("journal")!;
|
|
218
226
|
const store = getVaultStore("journal");
|
|
219
227
|
|
|
220
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
228
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
221
229
|
expect("error" in result).toBe(true);
|
|
222
230
|
if ("error" in result) {
|
|
223
231
|
expect(result.error.status).toBe(401);
|
|
@@ -287,7 +295,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
287
295
|
// this scenario, not a stderr inspection. Pattern carries to scribe/agent.
|
|
288
296
|
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
289
297
|
try {
|
|
290
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
298
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
291
299
|
expect("error" in result).toBe(true);
|
|
292
300
|
if ("error" in result) {
|
|
293
301
|
expect(result.error.status).toBe(401);
|
|
@@ -323,7 +331,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
323
331
|
const config = readVaultConfig("journal")!;
|
|
324
332
|
const store = getVaultStore("journal");
|
|
325
333
|
|
|
326
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
334
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
327
335
|
expect("error" in result).toBe(false);
|
|
328
336
|
if (!("error" in result)) {
|
|
329
337
|
expect(result.permission).toBe("full");
|
|
@@ -345,7 +353,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
345
353
|
const config = readVaultConfig("aaron")!;
|
|
346
354
|
const store = getVaultStore("aaron");
|
|
347
355
|
|
|
348
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
356
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
349
357
|
expect("error" in result).toBe(false);
|
|
350
358
|
if (!("error" in result)) {
|
|
351
359
|
expect(result.permission).toBe("full");
|
|
@@ -374,7 +382,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
374
382
|
const bobConfig = readVaultConfig("bob")!;
|
|
375
383
|
const bobStore = getVaultStore("bob");
|
|
376
384
|
|
|
377
|
-
const result = await authenticateVaultRequest(bearer(token), bobConfig
|
|
385
|
+
const result = await authenticateVaultRequest(bearer(token), bobConfig);
|
|
378
386
|
expect("error" in result).toBe(true);
|
|
379
387
|
if ("error" in result) {
|
|
380
388
|
expect(result.error.status).toBe(403);
|
|
@@ -418,7 +426,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
418
426
|
const config = readVaultConfig("work")!;
|
|
419
427
|
const store = getVaultStore("work");
|
|
420
428
|
|
|
421
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
429
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
422
430
|
expect("error" in result).toBe(false);
|
|
423
431
|
if (!("error" in result)) {
|
|
424
432
|
expect(result.permission).toBe("full");
|
|
@@ -444,7 +452,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
444
452
|
const config = readVaultConfig("legacy")!;
|
|
445
453
|
const store = getVaultStore("legacy");
|
|
446
454
|
|
|
447
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
455
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
448
456
|
expect("error" in result).toBe(false);
|
|
449
457
|
if (!("error" in result)) {
|
|
450
458
|
expect(result.permission).toBe("full");
|
|
@@ -469,7 +477,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
469
477
|
const config = readVaultConfig("aaron")!;
|
|
470
478
|
const store = getVaultStore("aaron");
|
|
471
479
|
|
|
472
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
480
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
473
481
|
expect("error" in result).toBe(true);
|
|
474
482
|
if ("error" in result) {
|
|
475
483
|
// 401, not 403 — broad-scope rejection takes precedence.
|
|
@@ -497,7 +505,7 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
497
505
|
|
|
498
506
|
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
499
507
|
try {
|
|
500
|
-
const result = await authenticateVaultRequest(bearer(token), config
|
|
508
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
501
509
|
expect("error" in result).toBe(true);
|
|
502
510
|
if ("error" in result) {
|
|
503
511
|
expect(result.error.status).toBe(401);
|
|
@@ -518,3 +526,175 @@ describe("authenticateVaultRequest — hub JWT integration", () => {
|
|
|
518
526
|
}
|
|
519
527
|
});
|
|
520
528
|
});
|
|
529
|
+
|
|
530
|
+
describe("authenticateVaultRequest — hub JWT tag-scoping (auth-unification C0)", () => {
|
|
531
|
+
// Helper: pull a successful AuthResult out of authenticateVaultRequest,
|
|
532
|
+
// failing the test loudly if auth returned an error Response.
|
|
533
|
+
async function authOk(
|
|
534
|
+
token: string,
|
|
535
|
+
vaultName: string,
|
|
536
|
+
): Promise<import("./auth.ts").AuthResult> {
|
|
537
|
+
const config = readVaultConfig(vaultName)!;
|
|
538
|
+
const store = getVaultStore(vaultName);
|
|
539
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
540
|
+
if ("error" in result) {
|
|
541
|
+
const body = await result.error.json();
|
|
542
|
+
throw new Error(`expected AuthResult, got ${result.error.status}: ${JSON.stringify(body)}`);
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
test("permissions.scoped_tags:[health] → AuthResult.scoped_tags=[health]; query-notes enforces (health visible, work hidden)", async () => {
|
|
548
|
+
seedVault("journal");
|
|
549
|
+
const store = getVaultStore("journal");
|
|
550
|
+
// Seed two notes: one tagged health, one tagged work.
|
|
551
|
+
await store.createNote("blood pressure log", { path: "h1", tags: ["health"] });
|
|
552
|
+
await store.createNote("quarterly OKRs", { path: "w1", tags: ["work"] });
|
|
553
|
+
|
|
554
|
+
const token = await signJwt(kp, {
|
|
555
|
+
iss: fixture.origin,
|
|
556
|
+
aud: "vault.journal",
|
|
557
|
+
scope: "vault:journal:read",
|
|
558
|
+
permissions: { scoped_tags: ["health"] },
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const auth = await authOk(token, "journal");
|
|
562
|
+
// The READ side: the allowlist is lifted off the validated token.
|
|
563
|
+
expect(auth.scoped_tags).toEqual(["health"]);
|
|
564
|
+
|
|
565
|
+
// End-to-end enforcement: the tag-scope-wrapped query-notes tool must
|
|
566
|
+
// hide the `work` note and surface the `health` note.
|
|
567
|
+
const { generateScopedMcpTools } = await import("./mcp-tools.ts");
|
|
568
|
+
const tools = generateScopedMcpTools("journal", auth);
|
|
569
|
+
const query = tools.find((t) => t.name === "query-notes")!;
|
|
570
|
+
const result = (await query.execute({})) as any;
|
|
571
|
+
const notes = Array.isArray(result) ? result : result.notes;
|
|
572
|
+
const paths = notes.map((n: any) => n.path).sort();
|
|
573
|
+
expect(paths).toEqual(["h1"]);
|
|
574
|
+
expect(paths).not.toContain("w1");
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("no permissions claim → scoped_tags=null (unscoped, full vault — regression: unchanged)", async () => {
|
|
578
|
+
seedVault("journal");
|
|
579
|
+
const store = getVaultStore("journal");
|
|
580
|
+
await store.createNote("a", { path: "h1", tags: ["health"] });
|
|
581
|
+
await store.createNote("b", { path: "w1", tags: ["work"] });
|
|
582
|
+
|
|
583
|
+
const token = await signJwt(kp, {
|
|
584
|
+
iss: fixture.origin,
|
|
585
|
+
aud: "vault.journal",
|
|
586
|
+
scope: "vault:journal:read",
|
|
587
|
+
// permissions intentionally omitted — today's hub-JWT shape
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const auth = await authOk(token, "journal");
|
|
591
|
+
expect(auth.scoped_tags).toBeNull();
|
|
592
|
+
|
|
593
|
+
const { generateScopedMcpTools } = await import("./mcp-tools.ts");
|
|
594
|
+
const tools = generateScopedMcpTools("journal", auth);
|
|
595
|
+
const query = tools.find((t) => t.name === "query-notes")!;
|
|
596
|
+
const result = (await query.execute({})) as any;
|
|
597
|
+
const notes = Array.isArray(result) ? result : result.notes;
|
|
598
|
+
const paths = notes.map((n: any) => n.path).sort();
|
|
599
|
+
// Unscoped: BOTH notes visible.
|
|
600
|
+
expect(paths).toEqual(["h1", "w1"]);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("permissions present but scoped_tags absent → scoped_tags=null (unscoped)", async () => {
|
|
604
|
+
seedVault("journal");
|
|
605
|
+
const token = await signJwt(kp, {
|
|
606
|
+
iss: fixture.origin,
|
|
607
|
+
aud: "vault.journal",
|
|
608
|
+
scope: "vault:journal:read",
|
|
609
|
+
permissions: { some_other_perm: true },
|
|
610
|
+
});
|
|
611
|
+
const auth = await authOk(token, "journal");
|
|
612
|
+
expect(auth.scoped_tags).toBeNull();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("permissions.scoped_tags:null → scoped_tags=null (explicit unscoped)", async () => {
|
|
616
|
+
seedVault("journal");
|
|
617
|
+
const token = await signJwt(kp, {
|
|
618
|
+
iss: fixture.origin,
|
|
619
|
+
aud: "vault.journal",
|
|
620
|
+
scope: "vault:journal:read",
|
|
621
|
+
permissions: { scoped_tags: null },
|
|
622
|
+
});
|
|
623
|
+
const auth = await authOk(token, "journal");
|
|
624
|
+
expect(auth.scoped_tags).toBeNull();
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ---- Fail-closed cases: present-but-malformed scoped_tags MUST 401, ----
|
|
628
|
+
// ---- never silently widen to full-vault. ----
|
|
629
|
+
for (const [label, badValue] of [
|
|
630
|
+
["a string", "health"],
|
|
631
|
+
["a number", 42],
|
|
632
|
+
["an object", { health: true }],
|
|
633
|
+
["an array with a non-string", ["health", 5]],
|
|
634
|
+
["an array with an empty string", ["health", ""]],
|
|
635
|
+
["an empty array", []],
|
|
636
|
+
] as Array<[string, unknown]>) {
|
|
637
|
+
test(`malformed scoped_tags (${label}) → 401 fail-closed (does NOT widen to full vault)`, async () => {
|
|
638
|
+
seedVault("journal");
|
|
639
|
+
const token = await signJwt(kp, {
|
|
640
|
+
iss: fixture.origin,
|
|
641
|
+
aud: "vault.journal",
|
|
642
|
+
scope: "vault:journal:read",
|
|
643
|
+
permissions: { scoped_tags: badValue },
|
|
644
|
+
});
|
|
645
|
+
const config = readVaultConfig("journal")!;
|
|
646
|
+
const store = getVaultStore("journal");
|
|
647
|
+
|
|
648
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
649
|
+
try {
|
|
650
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
651
|
+
// The whole request is rejected — NOT served with scoped_tags=null
|
|
652
|
+
// (full vault) or scoped_tags=[] (also full vault on the MCP path).
|
|
653
|
+
expect("error" in result).toBe(true);
|
|
654
|
+
if ("error" in result) {
|
|
655
|
+
expect(result.error.status).toBe(401);
|
|
656
|
+
const body = (await result.error.json()) as { error: string; message: string };
|
|
657
|
+
expect(body.error).toBe("Unauthorized");
|
|
658
|
+
expect(body.message).toContain("malformed tag-scope");
|
|
659
|
+
}
|
|
660
|
+
// Audit log carries the diagnostic.
|
|
661
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
662
|
+
} finally {
|
|
663
|
+
warnSpy.mockRestore();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
// pvt_* DROP (vault#282 Stage 2 — BREAKING). pvt_* tokens were the only
|
|
671
|
+
// non-JWT, non-YAML credential vault used to mint + validate. At 0.5.0 the
|
|
672
|
+
// mint + validation were removed entirely: a pvt_*-prefixed bearer is no
|
|
673
|
+
// longer JWT-shaped (skips authenticateHubJwt) and matches no surviving
|
|
674
|
+
// credential, so it 401s. The hub JWT — the migration target — keeps working.
|
|
675
|
+
// ---------------------------------------------------------------------------
|
|
676
|
+
|
|
677
|
+
describe("pvt_* DROP (vault#282 Stage 2 — unvalidatable)", () => {
|
|
678
|
+
test("a pvt_* bearer is 401-rejected on the per-vault hub-JWT surface", async () => {
|
|
679
|
+
seedVault("journal");
|
|
680
|
+
const config = readVaultConfig("journal")!;
|
|
681
|
+
const pvt = "pvt_deadbeefdeadbeefdeadbeefdeadbeef";
|
|
682
|
+
|
|
683
|
+
const result = await authenticateVaultRequest(bearer(pvt), config);
|
|
684
|
+
expect("error" in result).toBe(true);
|
|
685
|
+
if ("error" in result) expect(result.error.status).toBe(401);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("a real hub JWT still authenticates (migration target works)", async () => {
|
|
689
|
+
seedVault("journal");
|
|
690
|
+
const token = await signJwt(kp, {
|
|
691
|
+
iss: fixture.origin,
|
|
692
|
+
aud: "vault.journal",
|
|
693
|
+
scope: "vault:journal:write",
|
|
694
|
+
});
|
|
695
|
+
const config = readVaultConfig("journal")!;
|
|
696
|
+
|
|
697
|
+
const result = await authenticateVaultRequest(bearer(token), config);
|
|
698
|
+
expect("error" in result).toBe(false);
|
|
699
|
+
});
|
|
700
|
+
});
|
package/src/auth-status.ts
CHANGED
|
@@ -8,11 +8,18 @@
|
|
|
8
8
|
*
|
|
9
9
|
* What gets exposed:
|
|
10
10
|
* - `initialized` — at least one vault exists
|
|
11
|
-
* - `auth_modes` — accepted bearer formats (
|
|
11
|
+
* - `auth_modes` — accepted bearer formats. As of 0.5.0 (vault#282 Stage 2)
|
|
12
|
+
* vault is a pure hub resource-server: the only first-class user
|
|
13
|
+
* credential is a hub-issued JWT, so this is `["hub_jwt"]`. (The
|
|
14
|
+
* server-wide VAULT_AUTH_TOKEN operator bearer + legacy YAML api_keys
|
|
15
|
+
* still authenticate, but they're operator/legacy channels, not the
|
|
16
|
+
* advertised first-contact mode.)
|
|
12
17
|
* - `vaults` — list of `{ name, url }` for client-side dispatch
|
|
13
18
|
* - `hasOwnerPassword`, `hasTotp` — OAuth consent prerequisites
|
|
14
|
-
* - `hasTokens` — boolean | null.
|
|
15
|
-
*
|
|
19
|
+
* - `hasTokens` — boolean | null. Probes the vestigial `tokens` table for
|
|
20
|
+
* any leftover pre-0.5.0 rows (the table is kept inert as the YAML-import
|
|
21
|
+
* landing zone + a future-cosmetic-drop target). `null` ≈ "we couldn't
|
|
22
|
+
* read all DBs, don't trust this answer"; `true`/`false` are honest yes/no.
|
|
16
23
|
*
|
|
17
24
|
* What is deliberately NOT exposed: token counts, hashes, descriptions,
|
|
18
25
|
* timestamps, owner-password hash, totp secret, backup codes. The endpoint
|
|
@@ -26,7 +33,7 @@ import { listVaults, readGlobalConfig, vaultDbPath } from "./config.ts";
|
|
|
26
33
|
|
|
27
34
|
export interface AuthStatusResponse {
|
|
28
35
|
initialized: boolean;
|
|
29
|
-
auth_modes:
|
|
36
|
+
auth_modes: "hub_jwt"[];
|
|
30
37
|
vaults: { name: string; url: string }[];
|
|
31
38
|
hasOwnerPassword: boolean;
|
|
32
39
|
hasTotp: boolean;
|
|
@@ -77,7 +84,7 @@ export function buildAuthStatus(): AuthStatusResponse {
|
|
|
77
84
|
const vaultNames = listVaults();
|
|
78
85
|
return {
|
|
79
86
|
initialized: vaultNames.length > 0,
|
|
80
|
-
auth_modes: ["
|
|
87
|
+
auth_modes: ["hub_jwt"],
|
|
81
88
|
vaults: vaultNames.map((name) => ({ name, url: `/vault/${name}` })),
|
|
82
89
|
hasOwnerPassword: typeof globalConfig.owner_password_hash === "string"
|
|
83
90
|
&& globalConfig.owner_password_hash.length > 0,
|