@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.
Files changed (62) hide show
  1. package/README.md +51 -54
  2. package/core/src/core.test.ts +4 -1
  3. package/core/src/indexed-fields.test.ts +151 -0
  4. package/core/src/indexed-fields.ts +98 -0
  5. package/core/src/mcp.ts +66 -43
  6. package/core/src/notes.ts +26 -2
  7. package/core/src/portable-md.test.ts +52 -0
  8. package/core/src/portable-md.ts +48 -0
  9. package/core/src/schema.ts +87 -14
  10. package/core/src/store.ts +117 -0
  11. package/core/src/types.ts +28 -0
  12. package/package.json +2 -2
  13. package/src/auth-hub-jwt.test.ts +191 -11
  14. package/src/auth-status.ts +12 -5
  15. package/src/auth.test.ts +135 -219
  16. package/src/auth.ts +158 -107
  17. package/src/cli.ts +306 -224
  18. package/src/config.ts +12 -4
  19. package/src/export-watch.test.ts +23 -0
  20. package/src/export-watch.ts +14 -0
  21. package/src/git-preflight.test.ts +70 -0
  22. package/src/git-preflight.ts +68 -0
  23. package/src/hub-jwt.test.ts +27 -2
  24. package/src/hub-jwt.ts +10 -0
  25. package/src/init-summary.test.ts +4 -4
  26. package/src/init-summary.ts +36 -10
  27. package/src/mcp-config.test.ts +4 -2
  28. package/src/mcp-http.ts +24 -3
  29. package/src/mcp-install-interactive.test.ts +33 -71
  30. package/src/mcp-install-interactive.ts +23 -76
  31. package/src/mcp-install.test.ts +156 -55
  32. package/src/mcp-install.ts +109 -3
  33. package/src/mcp-tools.ts +249 -74
  34. package/src/mirror-config.test.ts +107 -0
  35. package/src/mirror-config.ts +275 -9
  36. package/src/mirror-credentials.test.ts +168 -17
  37. package/src/mirror-credentials.ts +155 -32
  38. package/src/mirror-deps.ts +25 -16
  39. package/src/mirror-import.test.ts +122 -16
  40. package/src/mirror-import.ts +50 -16
  41. package/src/mirror-manager.test.ts +51 -0
  42. package/src/mirror-manager.ts +116 -22
  43. package/src/mirror-per-vault.test.ts +519 -0
  44. package/src/mirror-registry.ts +91 -14
  45. package/src/mirror-routes.test.ts +81 -21
  46. package/src/mirror-routes.ts +90 -16
  47. package/src/routes.ts +39 -2
  48. package/src/routing.test.ts +203 -118
  49. package/src/routing.ts +46 -59
  50. package/src/scopes.test.ts +0 -86
  51. package/src/scopes.ts +9 -97
  52. package/src/server.ts +102 -34
  53. package/src/storage.test.ts +132 -7
  54. package/src/token-store.test.ts +88 -169
  55. package/src/token-store.ts +123 -249
  56. package/src/vault-create.test.ts +12 -4
  57. package/src/vault.test.ts +408 -103
  58. package/web/ui/dist/assets/index-DDRo6F4u.js +60 -0
  59. package/web/ui/dist/index.html +1 -1
  60. package/src/tokens-routes.test.ts +0 -727
  61. package/src/tokens-routes.ts +0 -392
  62. package/web/ui/dist/assets/index-Degr8snN.js +0 -60
package/src/auth.test.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  /**
2
- * Auth invariants — per-vault routing with strict isolation.
2
+ * Auth invariants — vault as a pure hub resource-server (vault#282 Stage 2).
3
3
  *
4
- * Every HTTP path that touches a vault lives under `/vault/<name>/...`, so
5
- * a token minted for vault A must authenticate at vault A endpoints and
6
- * must not authenticate at vault B endpoints. The global auth path still
7
- * exists for cross-vault listings (`/vaults`) and scans every vault's DB.
4
+ * The `pvt_*` opaque vault-DB token was dropped at 0.5.0: vault no longer
5
+ * mints or validates it. The surviving auth surfaces tested here are:
6
+ * - VAULT_AUTH_TOKEN the server-wide operator bearer.
7
+ * - Legacy YAML api_keys (vault.yaml / config.yaml) hashed keys.
8
+ * - The fail-closed guarantee: a `pvt_`-prefixed bearer is now 401-rejected
9
+ * on both the per-vault and global auth surfaces (it's unvalidatable).
8
10
  *
9
- * These tests isolate `PARACHUTE_HOME` so they don't touch the user's real
10
- * config. Each test builds 1-2 vaults from scratch.
11
+ * Hub-JWT auth + its per-vault audience isolation is covered end-to-end in
12
+ * `auth-hub-jwt.test.ts`. These tests isolate `PARACHUTE_HOME` so they don't
13
+ * touch the user's real config.
11
14
  */
12
15
 
13
16
  import { describe, test, expect, beforeEach, afterEach } from "bun:test";
@@ -23,7 +26,6 @@ import {
23
26
  hashKey,
24
27
  } from "./config.ts";
25
28
  import { getVaultStore, clearVaultStoreCache } from "./vault-store.ts";
26
- import { generateToken, createToken } from "./token-store.ts";
27
29
  import { authenticateVaultRequest, authenticateGlobalRequest } from "./auth.ts";
28
30
 
29
31
  let tmpHome: string;
@@ -66,178 +68,100 @@ function seedVault(name: string, opts: { isDefault?: boolean } = {}): void {
66
68
  }
67
69
  }
68
70
 
69
- /** Mint a fresh OAuth-style token directly into the named vault's DB. */
70
- function mintTokenInVault(vaultName: string): string {
71
- const store = getVaultStore(vaultName);
72
- const { fullToken } = generateToken();
73
- createToken(store.db, fullToken, { label: "test", permission: "full" });
74
- return fullToken;
75
- }
76
-
77
71
  function bearer(token: string): Request {
78
72
  return new Request("https://vault.test/x", {
79
73
  headers: { Authorization: `Bearer ${token}` },
80
74
  });
81
75
  }
82
76
 
83
- describe("auth — per-vault routing", () => {
84
- test("token minted in a vault authenticates at its own /vault/<name>/* endpoints", async () => {
85
- seedVault("journal");
86
- const token = mintTokenInVault("journal");
87
- const journalConfig = readVaultConfig("journal")!;
88
- const journalStore = getVaultStore("journal");
89
-
90
- // /vault/journal/api/* and /vault/journal/mcp both funnel into
91
- // authenticateVaultRequest with journal's config + DB.
92
- const vaultAuth = await authenticateVaultRequest(bearer(token), journalConfig, journalStore.db);
93
- expect("error" in vaultAuth).toBe(false);
94
- if (!("error" in vaultAuth)) expect(vaultAuth.permission).toBe("full");
95
-
96
- // /vaults (global metadata listing) uses authenticateGlobalRequest which
97
- // scans every vault's DB. Since the token is in journal's DB, it must resolve.
98
- const global = await authenticateGlobalRequest(bearer(token));
99
- expect("error" in global).toBe(false);
100
- });
101
-
102
- // HTTP-level routing stand-in. Mirrors routing.ts: every vault-scoped path
103
- // matches `/vault/<name>/...`, we look the vault up, then authenticate the
104
- // request against that vault's DB.
105
- async function dispatchAuthFromPath(path: string, req: Request): Promise<{
106
- status: number;
107
- permission?: string;
108
- }> {
109
- const match = path.match(/^\/vault\/([^/]+)(\/.*)?$/);
110
- if (!match) return { status: 404 };
111
- const vaultName = match[1]!;
112
- const vaultConfig = readVaultConfig(vaultName);
113
- if (!vaultConfig) return { status: 404 };
114
- const store = getVaultStore(vaultName);
115
- const res = await authenticateVaultRequest(req, vaultConfig, store.db);
116
- if ("error" in res) return { status: res.error.status };
117
- return { status: 200, permission: res.permission };
118
- }
119
-
120
- test("routing: /vault/<name>/api/health accepts a token minted in that vault", async () => {
121
- seedVault("journal");
122
- const token = mintTokenInVault("journal");
123
-
124
- const result = await dispatchAuthFromPath("/vault/journal/api/health", bearer(token));
125
- expect(result.status).toBe(200);
126
- expect(result.permission).toBe("full");
127
- });
77
+ // ---------------------------------------------------------------------------
78
+ // pvt_* fail-closed regression (vault#282 Stage 2)
79
+ //
80
+ // The DROP's load-bearing safety property: a pvt_*-prefixed bearer is no
81
+ // longer validatable. It isn't JWT-shaped (skips authenticateHubJwt), matches
82
+ // no YAML key_hash, and doesn't match VAULT_AUTH_TOKEN — so it falls through
83
+ // to a 401 on BOTH the per-vault and global surfaces. This proves pvt_* can't
84
+ // authenticate post-DROP.
85
+ // ---------------------------------------------------------------------------
128
86
 
129
- test("routing: /vault/A/api/* rejects a token issued for vault B", async () => {
130
- // The privilege-escalation barrier: a valid token for vault A must not
131
- // authenticate at vault B's endpoint, even though the token is valid
132
- // for some vault. This is the point of per-vault DBs.
133
- seedVault("journal");
134
- seedVault("work");
135
- const workToken = mintTokenInVault("work");
87
+ describe("auth pvt_* tokens are unvalidatable (fail closed)", () => {
88
+ const PVT = "pvt_deadbeefdeadbeefdeadbeefdeadbeef";
136
89
 
137
- const crossVault = await dispatchAuthFromPath("/vault/journal/api/health", bearer(workToken));
138
- expect(crossVault.status).toBe(401);
139
- });
90
+ // The pointed message a pvt_*-shaped bearer gets (vs the generic "Invalid
91
+ // API key" a non-pvt_ bad token gets) — the prefix is the user-meaningful
92
+ // signal that the mechanism was dropped, not that the key was mistyped.
93
+ const PVT_MESSAGE =
94
+ "pvt_* tokens are no longer supported (vault 0.5.0). Re-add this vault via your hub to get an access token.";
140
95
 
141
- test("routing: /vault/<unknown> returns 404 (not 401)", async () => {
96
+ test("a pvt_* bearer is 401-rejected with the dropped-token message on the per-vault surface", async () => {
142
97
  seedVault("journal");
143
- const token = mintTokenInVault("journal");
144
- const result = await dispatchAuthFromPath("/vault/nonexistent/api/health", bearer(token));
145
- expect(result.status).toBe(404);
146
- });
147
- });
148
-
149
- describe("auth — cross-vault isolation", () => {
150
- test("token minted in a non-default vault authenticates via scoped and global paths", async () => {
151
- seedVault("journal", { isDefault: true });
152
- seedVault("work");
153
- const workToken = mintTokenInVault("work");
154
- const workConfig = readVaultConfig("work")!;
155
- const workStore = getVaultStore("work");
156
-
157
- const scoped = await authenticateVaultRequest(bearer(workToken), workConfig, workStore.db);
158
- expect("error" in scoped).toBe(false);
159
-
160
- // Global auth scans every vault, must find the token in work's DB.
161
- const global = await authenticateGlobalRequest(bearer(workToken));
162
- expect("error" in global).toBe(false);
163
- });
164
-
165
- test("a work-vault token does NOT authenticate against the journal vault", async () => {
166
- seedVault("journal", { isDefault: true });
167
- seedVault("work");
168
- const workToken = mintTokenInVault("work");
169
98
  const journalConfig = readVaultConfig("journal")!;
170
- const journalStore = getVaultStore("journal");
171
99
 
172
- const res = await authenticateVaultRequest(bearer(workToken), journalConfig, journalStore.db);
173
- expect("error" in res).toBe(true);
100
+ const result = await authenticateVaultRequest(bearer(PVT), journalConfig);
101
+ expect("error" in result).toBe(true);
102
+ if ("error" in result) {
103
+ expect(result.error.status).toBe(401);
104
+ const body = (await result.error.json()) as { error: string; message: string };
105
+ expect(body.error).toBe("Unauthorized");
106
+ expect(body.message).toBe(PVT_MESSAGE);
107
+ }
174
108
  });
175
109
 
176
- test("v16 vault_name binding rejects with 403 when the row is cross-vault", async () => {
177
- // Per-vault DB scoping is the first line of defense (a token only
178
- // resolves against the DB it was minted in). The v16 vault_name column
179
- // is defense-in-depth: if a token row somehow lives in vault A's DB
180
- // but its vault_name says "B" (e.g. an out-of-band copy, or a future
181
- // mistake in the mint path), the binding mismatch must reject. We
182
- // simulate that by minting into journal's DB with vault_name="work".
110
+ test("a pvt_* bearer is 401-rejected with the dropped-token message on the global (/vaults) surface", async () => {
183
111
  seedVault("journal", { isDefault: true });
184
112
  seedVault("work");
185
- const journalStore = getVaultStore("journal");
186
- const { fullToken } = generateToken();
187
- createToken(journalStore.db, fullToken, {
188
- label: "mis-bound",
189
- permission: "full",
190
- vault_name: "work",
191
- });
192
- const journalConfig = readVaultConfig("journal")!;
193
113
 
194
- const res = await authenticateVaultRequest(bearer(fullToken), journalConfig, journalStore.db);
195
- expect("error" in res).toBe(true);
196
- if ("error" in res) {
197
- expect(res.error.status).toBe(403);
114
+ const result = await authenticateGlobalRequest(bearer(PVT));
115
+ expect("error" in result).toBe(true);
116
+ if ("error" in result) {
117
+ expect(result.error.status).toBe(401);
118
+ const body = (await result.error.json()) as { error: string; message: string };
119
+ expect(body.error).toBe("Unauthorized");
120
+ expect(body.message).toBe(PVT_MESSAGE);
198
121
  }
199
122
  });
200
123
 
201
- test("v16 vault_name binding accepts when token is bound to the requested vault", async () => {
202
- seedVault("work");
203
- const workStore = getVaultStore("work");
204
- const { fullToken } = generateToken();
205
- createToken(workStore.db, fullToken, {
206
- label: "bound",
207
- permission: "full",
208
- vault_name: "work",
209
- });
210
- const workConfig = readVaultConfig("work")!;
211
-
212
- const res = await authenticateVaultRequest(bearer(fullToken), workConfig, workStore.db);
213
- expect("error" in res).toBe(false);
214
- if (!("error" in res)) {
215
- expect(res.vault_name).toBe("work");
124
+ test("a pvt_* bearer is rejected even when VAULT_AUTH_TOKEN is set (no fall-through accept)", async () => {
125
+ const prev = process.env.VAULT_AUTH_TOKEN;
126
+ process.env.VAULT_AUTH_TOKEN = "operator-bearer-not-the-pvt-token";
127
+ try {
128
+ seedVault("journal");
129
+ const journalConfig = readVaultConfig("journal")!;
130
+ const result = await authenticateVaultRequest(bearer(PVT), journalConfig);
131
+ expect("error" in result).toBe(true);
132
+ if ("error" in result) {
133
+ expect(result.error.status).toBe(401);
134
+ const body = (await result.error.json()) as { message: string };
135
+ expect(body.message).toBe(PVT_MESSAGE);
136
+ }
137
+ } finally {
138
+ if (prev === undefined) delete process.env.VAULT_AUTH_TOKEN;
139
+ else process.env.VAULT_AUTH_TOKEN = prev;
216
140
  }
217
141
  });
218
142
 
219
- test("legacy NULL-bound tokens still authenticate against any vault", async () => {
220
- // Backwards compatibility: pre-v16 tokens (and legacy YAML keys, and
221
- // hub JWTs) all carry vault_name = null. They keep working at any
222
- // vault — the migration is lenient by design.
223
- seedVault("work");
224
- const workToken = mintTokenInVault("work");
225
- const workConfig = readVaultConfig("work")!;
226
- const workStore = getVaultStore("work");
143
+ test("a non-pvt_ invalid bearer keeps the generic message (no behavior change)", async () => {
144
+ seedVault("journal");
145
+ const journalConfig = readVaultConfig("journal")!;
227
146
 
228
- const res = await authenticateVaultRequest(bearer(workToken), workConfig, workStore.db);
229
- expect("error" in res).toBe(false);
230
- if (!("error" in res)) {
231
- expect(res.vault_name).toBeNull();
147
+ const result = await authenticateVaultRequest(bearer("notavalidkey123"), journalConfig);
148
+ expect("error" in result).toBe(true);
149
+ if ("error" in result) {
150
+ expect(result.error.status).toBe(401);
151
+ const body = (await result.error.json()) as { message: string };
152
+ expect(body.message).toBe("Invalid API key");
232
153
  }
233
154
  });
234
- });
235
155
 
236
- // The "End-to-end OAuth flow" suite was retired alongside the standalone
237
- // OAuth issuer in workstream E (vault#366). Per-vault token coherence is
238
- // still pinned by the v16 binding tests above and by `tokens-routes.test.ts`
239
- // (mint-via-CLI → present at /vault/<name>/* surfaces); the OAuth handshake
240
- // itself has moved entirely to the hub.
156
+ test("/vault/<unknown> still 404s before auth (routing precedence unchanged)", async () => {
157
+ // HTTP-level routing stand-in mirroring routing.ts: an unknown vault is a
158
+ // 404 (the vault config lookup fails) before any bearer is evaluated.
159
+ seedVault("journal");
160
+ const match = "/vault/nonexistent/api/health".match(/^\/vault\/([^/]+)(\/.*)?$/);
161
+ const vaultName = match![1]!;
162
+ expect(readVaultConfig(vaultName)).toBeNull();
163
+ });
164
+ });
241
165
 
242
166
  // ---------------------------------------------------------------------------
243
167
  // Legacy YAML global keys — scope must round-trip through the parser
@@ -290,6 +214,32 @@ describe("auth — legacy global YAML keys honor declared scope", () => {
290
214
  expect(result.permission).toBe("full");
291
215
  }
292
216
  });
217
+
218
+ test("a per-vault YAML api_key authenticates at its vault and reports read scope", async () => {
219
+ // The surviving legacy per-vault path: vault.yaml api_keys. seedVault
220
+ // writes a `scope: write` key, so we mint a read one here explicitly.
221
+ const { fullKey, keyId } = generateApiKey();
222
+ writeVaultConfig({
223
+ name: "journal",
224
+ api_keys: [
225
+ {
226
+ id: keyId,
227
+ label: "reader",
228
+ scope: "read",
229
+ key_hash: hashKey(fullKey),
230
+ created_at: new Date().toISOString(),
231
+ },
232
+ ],
233
+ created_at: new Date().toISOString(),
234
+ });
235
+ const journalConfig = readVaultConfig("journal")!;
236
+
237
+ const result = await authenticateVaultRequest(bearer(fullKey), journalConfig);
238
+ expect("error" in result).toBe(false);
239
+ if (!("error" in result)) {
240
+ expect(result.permission).toBe("read");
241
+ }
242
+ });
293
243
  });
294
244
 
295
245
  // ---------------------------------------------------------------------------
@@ -303,11 +253,11 @@ describe("auth — legacy global YAML keys honor declared scope", () => {
303
253
  //
304
254
  // Semantic confirmed for the loopback/non-loopback split (auth gate is
305
255
  // orthogonal to socket-level loopback): when VAULT_AUTH_TOKEN is unset,
306
- // vault's existing token surface (per-vault DB tokens + hub JWTs + legacy
307
- // YAML keys) is the ONLY auth surface. The bind socket defaults to
308
- // 127.0.0.1 (`VAULT_BIND` in bind.ts), but no implicit loopback trust
309
- // exists at the auth layer — a request from 127.0.0.1 still has to
310
- // present a valid bearer. This matches docs/auth-model.md §1.
256
+ // vault's surviving token surface (hub JWTs + legacy YAML keys) is the ONLY
257
+ // auth surface. The bind socket defaults to 127.0.0.1 (`VAULT_BIND` in
258
+ // bind.ts), but no implicit loopback trust exists at the auth layer — a
259
+ // request from 127.0.0.1 still has to present a valid bearer. This matches
260
+ // docs/auth-model.md §1.
311
261
  // ---------------------------------------------------------------------------
312
262
 
313
263
  describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
@@ -327,13 +277,8 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
327
277
  process.env.VAULT_AUTH_TOKEN = TOKEN;
328
278
  seedVault("journal");
329
279
  const journalConfig = readVaultConfig("journal")!;
330
- const journalStore = getVaultStore("journal");
331
280
 
332
- const result = await authenticateVaultRequest(
333
- bearer(TOKEN),
334
- journalConfig,
335
- journalStore.db,
336
- );
281
+ const result = await authenticateVaultRequest(bearer(TOKEN), journalConfig);
337
282
 
338
283
  expect("error" in result).toBe(false);
339
284
  if (!("error" in result)) {
@@ -345,18 +290,16 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
345
290
  });
346
291
 
347
292
  test("env set + matching bearer authenticates against ANY vault on the server", async () => {
348
- // Server-wide → not tied to any one vault's DB. Same bearer works
349
- // for journal and work without minting a per-vault token in either.
293
+ // Server-wide → not tied to any one vault. Same bearer works for journal
294
+ // and work without any per-vault credential in either.
350
295
  process.env.VAULT_AUTH_TOKEN = TOKEN;
351
296
  seedVault("journal");
352
297
  seedVault("work");
353
298
  const journalConfig = readVaultConfig("journal")!;
354
- const journalStore = getVaultStore("journal");
355
299
  const workConfig = readVaultConfig("work")!;
356
- const workStore = getVaultStore("work");
357
300
 
358
- const j = await authenticateVaultRequest(bearer(TOKEN), journalConfig, journalStore.db);
359
- const w = await authenticateVaultRequest(bearer(TOKEN), workConfig, workStore.db);
301
+ const j = await authenticateVaultRequest(bearer(TOKEN), journalConfig);
302
+ const w = await authenticateVaultRequest(bearer(TOKEN), workConfig);
360
303
 
361
304
  expect("error" in j).toBe(false);
362
305
  expect("error" in w).toBe(false);
@@ -366,11 +309,10 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
366
309
  process.env.VAULT_AUTH_TOKEN = TOKEN;
367
310
  seedVault("journal");
368
311
  const journalConfig = readVaultConfig("journal")!;
369
- const journalStore = getVaultStore("journal");
370
312
 
371
313
  // No Authorization header at all.
372
314
  const noBearer = new Request("https://vault.test/x");
373
- const result = await authenticateVaultRequest(noBearer, journalConfig, journalStore.db);
315
+ const result = await authenticateVaultRequest(noBearer, journalConfig);
374
316
 
375
317
  expect("error" in result).toBe(true);
376
318
  if ("error" in result) {
@@ -382,13 +324,8 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
382
324
  process.env.VAULT_AUTH_TOKEN = TOKEN;
383
325
  seedVault("journal");
384
326
  const journalConfig = readVaultConfig("journal")!;
385
- const journalStore = getVaultStore("journal");
386
327
 
387
- const result = await authenticateVaultRequest(
388
- bearer("wrong-token-doesnotmatch"),
389
- journalConfig,
390
- journalStore.db,
391
- );
328
+ const result = await authenticateVaultRequest(bearer("wrong-token-doesnotmatch"), journalConfig);
392
329
 
393
330
  expect("error" in result).toBe(true);
394
331
  if ("error" in result) {
@@ -396,34 +333,25 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
396
333
  }
397
334
  });
398
335
 
399
- test("env set + bearer that matches a vault token still resolves (server-wide first, but per-vault unchanged)", async () => {
400
- // Per-vault tokens keep working even when the server-wide bearer is
401
- // set. The server-wide check is a fast-path lookup before token DB
402
- // resolution — a per-vault token doesn't match the env var so it
403
- // falls through to the existing path.
404
- process.env.VAULT_AUTH_TOKEN = TOKEN;
405
- seedVault("journal");
406
- const perVaultToken = mintTokenInVault("journal");
407
- const journalConfig = readVaultConfig("journal")!;
408
- const journalStore = getVaultStore("journal");
409
-
410
- const result = await authenticateVaultRequest(
411
- bearer(perVaultToken),
412
- journalConfig,
413
- journalStore.db,
414
- );
415
-
416
- expect("error" in result).toBe(false);
417
- });
418
-
419
- test("env unset + valid per-vault bearer → 200 (existing behavior preserved)", async () => {
336
+ test("env unset + valid per-vault YAML bearer 200 (existing behavior preserved)", async () => {
420
337
  delete process.env.VAULT_AUTH_TOKEN;
421
- seedVault("journal");
422
- const token = mintTokenInVault("journal");
338
+ const { fullKey, keyId } = generateApiKey();
339
+ writeVaultConfig({
340
+ name: "journal",
341
+ api_keys: [
342
+ {
343
+ id: keyId,
344
+ label: "bootstrap",
345
+ scope: "write",
346
+ key_hash: hashKey(fullKey),
347
+ created_at: new Date().toISOString(),
348
+ },
349
+ ],
350
+ created_at: new Date().toISOString(),
351
+ });
423
352
  const journalConfig = readVaultConfig("journal")!;
424
- const journalStore = getVaultStore("journal");
425
353
 
426
- const result = await authenticateVaultRequest(bearer(token), journalConfig, journalStore.db);
354
+ const result = await authenticateVaultRequest(bearer(fullKey), journalConfig);
427
355
  expect("error" in result).toBe(false);
428
356
  });
429
357
 
@@ -431,10 +359,9 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
431
359
  delete process.env.VAULT_AUTH_TOKEN;
432
360
  seedVault("journal");
433
361
  const journalConfig = readVaultConfig("journal")!;
434
- const journalStore = getVaultStore("journal");
435
362
 
436
363
  const noBearer = new Request("https://vault.test/x");
437
- const result = await authenticateVaultRequest(noBearer, journalConfig, journalStore.db);
364
+ const result = await authenticateVaultRequest(noBearer, journalConfig);
438
365
  expect("error" in result).toBe(true);
439
366
  if ("error" in result) {
440
367
  expect(result.error.status).toBe(401);
@@ -450,12 +377,11 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
450
377
  delete process.env.VAULT_AUTH_TOKEN;
451
378
  seedVault("journal");
452
379
  const journalConfig = readVaultConfig("journal")!;
453
- const journalStore = getVaultStore("journal");
454
380
 
455
381
  const remote = new Request("https://vault.test/x", {
456
382
  headers: { "X-Forwarded-For": "203.0.113.7" },
457
383
  });
458
- const result = await authenticateVaultRequest(remote, journalConfig, journalStore.db);
384
+ const result = await authenticateVaultRequest(remote, journalConfig);
459
385
  expect("error" in result).toBe(true);
460
386
  if ("error" in result) {
461
387
  expect(result.error.status).toBe(401);
@@ -466,15 +392,10 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
466
392
  process.env.VAULT_AUTH_TOKEN = " ";
467
393
  seedVault("journal");
468
394
  const journalConfig = readVaultConfig("journal")!;
469
- const journalStore = getVaultStore("journal");
470
395
 
471
396
  // An empty/whitespace VAULT_AUTH_TOKEN must NOT allow any bearer to
472
397
  // pass — the operator either commits to bearer auth or doesn't.
473
- const result = await authenticateVaultRequest(
474
- bearer(""),
475
- journalConfig,
476
- journalStore.db,
477
- );
398
+ const result = await authenticateVaultRequest(bearer(""), journalConfig);
478
399
  expect("error" in result).toBe(true);
479
400
  });
480
401
 
@@ -512,17 +433,12 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
512
433
  process.env.VAULT_AUTH_TOKEN = TOKEN;
513
434
  seedVault("journal");
514
435
  const journalConfig = readVaultConfig("journal")!;
515
- const journalStore = getVaultStore("journal");
516
436
 
517
437
  const nearMiss = TOKEN.slice(0, -1) + "x";
518
438
  expect(nearMiss).not.toBe(TOKEN);
519
439
  expect(nearMiss.length).toBe(TOKEN.length);
520
440
 
521
- const result = await authenticateVaultRequest(
522
- bearer(nearMiss),
523
- journalConfig,
524
- journalStore.db,
525
- );
441
+ const result = await authenticateVaultRequest(bearer(nearMiss), journalConfig);
526
442
  expect("error" in result).toBe(true);
527
443
  });
528
444
  });