@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/src/auth.test.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth invariants —
|
|
2
|
+
* Auth invariants — vault as a pure hub resource-server (vault#282 Stage 2).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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("
|
|
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
|
|
173
|
-
expect("error" in
|
|
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("
|
|
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
|
|
195
|
-
expect("error" in
|
|
196
|
-
if ("error" in
|
|
197
|
-
expect(
|
|
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("
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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("
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
229
|
-
expect("error" in
|
|
230
|
-
if (
|
|
231
|
-
expect(
|
|
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
|
-
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
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
|
|
349
|
-
//
|
|
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
|
|
359
|
-
const w = await authenticateVaultRequest(bearer(TOKEN), workConfig
|
|
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
|
|
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
|
|
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
|
-
|
|
422
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
});
|