@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.ts
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication and authorization for the vault server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* As of 0.5.0 vault is a PURE HUB RESOURCE-SERVER (vault#282 Stage 2). The
|
|
5
|
+
* opaque `pvt_*` vault-DB token was dropped — vault no longer mints or
|
|
6
|
+
* validates it. Three auth paths survive:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 1. Hub-issued JWT — the user-credential path. JWT-shaped bearers are
|
|
9
|
+
* validated against the hub's JWKS (`authenticateHubJwt` → hub-jwt.ts →
|
|
10
|
+
* scope-guard), audience-pinned to `vault.<name>`, scope-narrowed.
|
|
11
|
+
* 2. VAULT_AUTH_TOKEN — the server-wide operator bearer (env var,
|
|
12
|
+
* constant-time compare). Short-circuits both auth entry points.
|
|
13
|
+
* 3. Legacy YAML api_keys — hashed keys in vault.yaml / config.yaml. A
|
|
14
|
+
* separate deprecation axis from pvt_*; still validated here.
|
|
9
15
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* in the DB are normalized to "full" at read time.
|
|
16
|
+
* Permission levels remain "full" / "read"; legacy "admin"/"write" DB values
|
|
17
|
+
* normalize to "full" at read time.
|
|
13
18
|
*
|
|
14
|
-
* The unified /mcp endpoint
|
|
15
|
-
*
|
|
19
|
+
* The unified /mcp endpoint accepts only VAULT_AUTH_TOKEN + global config.yaml
|
|
20
|
+
* keys — hub JWTs are vault-bound (aud=vault.<name>) and have no single
|
|
21
|
+
* audience to strict-check on the cross-vault surface.
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
|
-
import { readGlobalConfig, writeVaultConfig, writeGlobalConfig, verifyKey
|
|
24
|
+
import { readGlobalConfig, writeVaultConfig, writeGlobalConfig, verifyKey } from "./config.ts";
|
|
19
25
|
import type { VaultConfig, StoredKey } from "./config.ts";
|
|
20
|
-
import { resolveToken } from "./token-store.ts";
|
|
21
26
|
import type { TokenPermission } from "./token-store.ts";
|
|
22
|
-
import type { Database } from "bun:sqlite";
|
|
23
27
|
import crypto from "node:crypto";
|
|
24
|
-
import { getVaultStore } from "./vault-store.ts";
|
|
25
28
|
import {
|
|
26
29
|
findBroadVaultScopes,
|
|
27
30
|
hasScope,
|
|
@@ -71,8 +74,8 @@ function constantTimeEquals(a: string, b: string): boolean {
|
|
|
71
74
|
*
|
|
72
75
|
* The operator-channel auth shape for non-loopback deploys (Render,
|
|
73
76
|
* sibling-container setups, vault#339). Hub uses this to call vault
|
|
74
|
-
* across a container boundary; end-user OAuth tokens
|
|
75
|
-
*
|
|
77
|
+
* across a container boundary; end-user OAuth tokens take the per-vault
|
|
78
|
+
* hub-JWT path below. See `docs/auth-model.md` §2.
|
|
76
79
|
*
|
|
77
80
|
* Scope set is broad (`vault:admin`) — the env-var bearer is an
|
|
78
81
|
* operator credential, not a user credential. Tag-scoping doesn't
|
|
@@ -127,12 +130,11 @@ export interface AuthResult {
|
|
|
127
130
|
*/
|
|
128
131
|
vault_name: string | null;
|
|
129
132
|
/**
|
|
130
|
-
* Session identifier
|
|
131
|
-
*
|
|
132
|
-
* `jti
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* scope to this session's mints. See vault#376.
|
|
133
|
+
* Session identifier. For hub JWTs this is the `jti` claim, when present.
|
|
134
|
+
* NULL for legacy YAML keys / server-wide env-var tokens / hub JWTs
|
|
135
|
+
* without a `jti`. Used by the manage-token MCP tool to stamp child
|
|
136
|
+
* mints with `parent_jti` so list/revoke can scope to this session's
|
|
137
|
+
* mints. See vault#376.
|
|
136
138
|
*/
|
|
137
139
|
caller_jti: string | null;
|
|
138
140
|
}
|
|
@@ -225,21 +227,19 @@ function validateKey(keys: StoredKey[], providedKey: string): StoredKey | null {
|
|
|
225
227
|
/**
|
|
226
228
|
* Authenticate for a specific vault.
|
|
227
229
|
*
|
|
228
|
-
* Token shape decides the path
|
|
230
|
+
* Token shape decides the path (vault#282 Stage 2 — vault is a pure hub
|
|
231
|
+
* resource-server, the `pvt_*` vault-DB lookup is GONE):
|
|
232
|
+
* - VAULT_AUTH_TOKEN match → server-wide operator bearer (checked first).
|
|
229
233
|
* - JWT-shaped (`eyJ…`) → validate against the hub's JWKS. JWT-shaped tokens
|
|
230
|
-
* commit to JWT validation; we don't fall through
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* Dual-validate window: both paths are live during this release cycle so
|
|
236
|
-
* existing `pvt_*` callers continue to work. A follow-up issue retires the
|
|
237
|
-
* legacy path.
|
|
234
|
+
* commit to JWT validation; we don't fall through on failure, since a
|
|
235
|
+
* malformed JWT was never going to be a valid local credential anyway.
|
|
236
|
+
* - Anything else → legacy YAML api_keys (vault.yaml, then config.yaml).
|
|
237
|
+
* A `pvt_*`-prefixed bearer is not JWT-shaped and matches no `key_hash`,
|
|
238
|
+
* so it falls through to the 401 — pvt_* is unvalidatable post-DROP.
|
|
238
239
|
*/
|
|
239
240
|
export async function authenticateVaultRequest(
|
|
240
241
|
req: Request,
|
|
241
242
|
vaultConfig: VaultConfig,
|
|
242
|
-
vaultDb?: Database,
|
|
243
243
|
): Promise<{ error: Response } | AuthResult> {
|
|
244
244
|
const key = extractApiKey(req);
|
|
245
245
|
if (!key) {
|
|
@@ -250,8 +250,8 @@ export async function authenticateVaultRequest(
|
|
|
250
250
|
// a matching bearer authenticates as full/admin against any vault. This
|
|
251
251
|
// is the cross-container path for Render / sibling-service deployments
|
|
252
252
|
// where hub talks to vault over HTTP. Checked first so it short-circuits
|
|
253
|
-
//
|
|
254
|
-
//
|
|
253
|
+
// JWT validation — the operator token is a credential the operator opts
|
|
254
|
+
// into, not one we'd ever fall through.
|
|
255
255
|
const serverWide = tryServerWideAuth(key);
|
|
256
256
|
if (serverWide !== null) return serverWide;
|
|
257
257
|
|
|
@@ -269,46 +269,6 @@ export async function authenticateVaultRequest(
|
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
// Try vault's token DB first
|
|
273
|
-
if (vaultDb) {
|
|
274
|
-
try {
|
|
275
|
-
const resolved = resolveToken(vaultDb, key);
|
|
276
|
-
if (resolved) {
|
|
277
|
-
// Per-vault binding (v16): tokens minted via /vault/<name>/tokens
|
|
278
|
-
// carry vault_name = <name>. Reject if presented at a different
|
|
279
|
-
// vault. NULL = legacy / server-wide; accept anywhere. The DB
|
|
280
|
-
// lookup itself already filters by per-vault DB scoping (resolve
|
|
281
|
-
// only succeeds if the token row lives in this vault's DB), so
|
|
282
|
-
// a mismatch here would only happen if a token row was copied
|
|
283
|
-
// across vault DBs out-of-band — defense-in-depth.
|
|
284
|
-
if (resolved.vault_name !== null && resolved.vault_name !== vaultConfig.name) {
|
|
285
|
-
return {
|
|
286
|
-
error: Response.json(
|
|
287
|
-
{
|
|
288
|
-
error: "Unauthorized",
|
|
289
|
-
message: `token is bound to vault '${resolved.vault_name}'; cannot be used against vault '${vaultConfig.name}'`,
|
|
290
|
-
},
|
|
291
|
-
{ status: 403 },
|
|
292
|
-
),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
if (resolved.legacyDerived) {
|
|
296
|
-
warnLegacyOnce(`vault-token:${vaultConfig.name ?? ""}`, "vault token without scopes column");
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
permission: resolved.permission,
|
|
300
|
-
scopes: resolved.scopes,
|
|
301
|
-
legacyDerived: resolved.legacyDerived,
|
|
302
|
-
scoped_tags: resolved.scoped_tags,
|
|
303
|
-
vault_name: resolved.vault_name,
|
|
304
|
-
caller_jti: resolved.jti,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
} catch {
|
|
308
|
-
// Token table might not exist yet — fall through to legacy auth
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
272
|
// Legacy: check per-vault keys from vault.yaml
|
|
313
273
|
const vaultKey = validateKey(vaultConfig.api_keys, key);
|
|
314
274
|
if (vaultKey) {
|
|
@@ -328,9 +288,102 @@ export async function authenticateVaultRequest(
|
|
|
328
288
|
}
|
|
329
289
|
}
|
|
330
290
|
|
|
291
|
+
if (looksLikeDroppedPvtToken(key)) {
|
|
292
|
+
return { error: droppedPvtTokenResponse() };
|
|
293
|
+
}
|
|
331
294
|
return { error: Response.json({ error: "Unauthorized", message: "Invalid API key" }, { status: 401 }) };
|
|
332
295
|
}
|
|
333
296
|
|
|
297
|
+
/**
|
|
298
|
+
* A bearer of the dropped `pvt_*` shape (vault#282 Stage 2 — vault no longer
|
|
299
|
+
* mints or validates the per-vault opaque token). Detected by prefix so a
|
|
300
|
+
* caller still presenting one gets a pointed 401 instead of the generic
|
|
301
|
+
* "Invalid API key" — the prefix is the user-meaningful signal, and a real
|
|
302
|
+
* hub JWT / `pvk_` / `VAULT_AUTH_TOKEN` never starts with `pvt_`.
|
|
303
|
+
*/
|
|
304
|
+
function looksLikeDroppedPvtToken(key: string): boolean {
|
|
305
|
+
return key.startsWith("pvt_");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function droppedPvtTokenResponse(): Response {
|
|
309
|
+
return Response.json(
|
|
310
|
+
{
|
|
311
|
+
error: "Unauthorized",
|
|
312
|
+
message:
|
|
313
|
+
"pvt_* tokens are no longer supported (vault 0.5.0). Re-add this vault via your hub to get an access token.",
|
|
314
|
+
},
|
|
315
|
+
{ status: 401 },
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Sentinel thrown when a hub JWT carries a `permissions.scoped_tags` claim
|
|
321
|
+
* that is present but malformed (not an array of non-empty strings). See
|
|
322
|
+
* `parseScopedTagsFromPermissions` for why this MUST fail closed.
|
|
323
|
+
*/
|
|
324
|
+
class MalformedScopedTagsError extends Error {}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Map a validated hub JWT's `permissions` claim into the `scoped_tags`
|
|
328
|
+
* allowlist for `AuthResult` (auth-unification arc, C0 — the READ side).
|
|
329
|
+
*
|
|
330
|
+
* Wire contract (the shape the SPA + manage-token mint sides target):
|
|
331
|
+
*
|
|
332
|
+
* permissions: { scoped_tags: string[] } // root tag names
|
|
333
|
+
*
|
|
334
|
+
* Each entry is a ROOT tag name; the token sees notes carrying that tag or a
|
|
335
|
+
* sub-tag thereof (hierarchy expansion happens in tag-scope.ts).
|
|
336
|
+
*
|
|
337
|
+
* Three outcomes, chosen for a strict FAIL-CLOSED invariant — tag-scoping is
|
|
338
|
+
* always a RESTRICTION, so a misread must NEVER widen access:
|
|
339
|
+
*
|
|
340
|
+
* 1. Claim absent (no `permissions`, or no `scoped_tags` key) → returns
|
|
341
|
+
* `null` = UNSCOPED = full vault. This is legitimate and is today's
|
|
342
|
+
* behavior for every hub JWT. Absence genuinely means "not tag-scoped".
|
|
343
|
+
*
|
|
344
|
+
* 2. `scoped_tags` is a non-empty array of non-empty strings → returns
|
|
345
|
+
* that array. The token is tag-scoped; the allowlist is enforced.
|
|
346
|
+
*
|
|
347
|
+
* 3. `scoped_tags` is present but MALFORMED — a string, a number, an
|
|
348
|
+
* object, an array containing a non-string / empty-string, or an empty
|
|
349
|
+
* array `[]` — throws `MalformedScopedTagsError`. The caller REJECTS
|
|
350
|
+
* the request (401). We do NOT coerce to `null` or `[]`:
|
|
351
|
+
*
|
|
352
|
+
* - Coercing to `null` would WIDEN a token that was MEANT to be scoped
|
|
353
|
+
* up to full-vault — a privilege leak. Forbidden.
|
|
354
|
+
* - Coercing to `[]` is NOT reliably fail-closed across enforcement
|
|
355
|
+
* paths. The MCP read-tool wrappers fast-path out on
|
|
356
|
+
* `scoped_tags.length === 0` (mcp-tools.ts ~L178) and the REST path
|
|
357
|
+
* collapses `[]` → null inside `expandTokenTagScope` (tag-scope.ts
|
|
358
|
+
* ~L35) — both treat `[]` as UNSCOPED = full vault. So `[]` would
|
|
359
|
+
* ALSO widen. (Note: `filterNotesByTagScope`/`noteWithinTagScope`
|
|
360
|
+
* would treat a raw `[]` as "matches nothing", but the call sites
|
|
361
|
+
* never reach them with `[]` because of the upstream short-circuits —
|
|
362
|
+
* the two enforcement paths disagree on `[]`, which is exactly why we
|
|
363
|
+
* refuse to manufacture it here.)
|
|
364
|
+
*
|
|
365
|
+
* The only correct fail-closed action for a present-but-unreadable
|
|
366
|
+
* scope is to reject the whole request — never serve it wide.
|
|
367
|
+
*/
|
|
368
|
+
function parseScopedTagsFromPermissions(
|
|
369
|
+
permissions: Record<string, unknown> | undefined,
|
|
370
|
+
): string[] | null {
|
|
371
|
+
if (!permissions || !("scoped_tags" in permissions)) return null;
|
|
372
|
+
const raw = permissions.scoped_tags;
|
|
373
|
+
if (raw === null || raw === undefined) return null; // explicit "unscoped"
|
|
374
|
+
if (
|
|
375
|
+
Array.isArray(raw) &&
|
|
376
|
+
raw.length > 0 &&
|
|
377
|
+
raw.every((t) => typeof t === "string" && t.length > 0)
|
|
378
|
+
) {
|
|
379
|
+
return raw as string[];
|
|
380
|
+
}
|
|
381
|
+
// Present but malformed (incl. `[]`): fail closed — never widen.
|
|
382
|
+
throw new MalformedScopedTagsError(
|
|
383
|
+
"hub JWT permissions.scoped_tags is present but not a non-empty array of tag names",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
334
387
|
/**
|
|
335
388
|
* Validate a JWT-shaped bearer and convert the result into an `AuthResult`.
|
|
336
389
|
* The token's scope claim becomes the granted scopes; permission is derived
|
|
@@ -353,6 +406,12 @@ export async function authenticateVaultRequest(
|
|
|
353
406
|
* unchanged. The 403 (not 401) signals "your credential is valid but
|
|
354
407
|
* doesn't grant access to this vault" — distinct from authentication
|
|
355
408
|
* failures upstream. See hub#283 + scope-guard 0.3.0 for the mint side.
|
|
409
|
+
*
|
|
410
|
+
* Tag-scoping (auth-unification arc, C0): the token's `permissions.scoped_tags`
|
|
411
|
+
* claim (when present and well-formed) maps into `AuthResult.scoped_tags`,
|
|
412
|
+
* restricting which notes the token sees. A present-but-malformed claim FAILS
|
|
413
|
+
* CLOSED with a 401 rather than widening to full-vault — see
|
|
414
|
+
* `parseScopedTagsFromPermissions`.
|
|
356
415
|
*/
|
|
357
416
|
async function authenticateHubJwt(
|
|
358
417
|
token: string,
|
|
@@ -413,11 +472,15 @@ async function authenticateHubJwt(
|
|
|
413
472
|
hasScope(claims.scopes, SCOPE_WRITE) || hasScope(claims.scopes, SCOPE_ADMIN)
|
|
414
473
|
? "full"
|
|
415
474
|
: "read";
|
|
475
|
+
// C0: read tag-scoping from the validated token's `permissions` claim.
|
|
476
|
+
// Throws MalformedScopedTagsError (caught below → 401) on a present-but-
|
|
477
|
+
// malformed claim so we never widen access on a misread.
|
|
478
|
+
const scoped_tags = parseScopedTagsFromPermissions(claims.permissions);
|
|
416
479
|
return {
|
|
417
480
|
permission,
|
|
418
481
|
scopes: claims.scopes,
|
|
419
482
|
legacyDerived: false,
|
|
420
|
-
scoped_tags
|
|
483
|
+
scoped_tags,
|
|
421
484
|
vault_name: null,
|
|
422
485
|
// claims.jti is `undefined` when the issuer didn't stamp one. Pass it
|
|
423
486
|
// through verbatim — manage-token's session-pin will be null in that
|
|
@@ -425,6 +488,19 @@ async function authenticateHubJwt(
|
|
|
425
488
|
caller_jti: claims.jti ?? null,
|
|
426
489
|
};
|
|
427
490
|
} catch (err) {
|
|
491
|
+
if (err instanceof MalformedScopedTagsError) {
|
|
492
|
+
// Fail-closed (C0): a present-but-malformed `permissions.scoped_tags`
|
|
493
|
+
// means the token wanted to be tag-scoped but we can't read the
|
|
494
|
+
// allowlist. Reject rather than widen to full-vault. The audit log
|
|
495
|
+
// carries the diagnostic; the client gets a generic 401.
|
|
496
|
+
console.warn(`[auth] hub JWT rejected: ${err.message}`);
|
|
497
|
+
return {
|
|
498
|
+
error: Response.json(
|
|
499
|
+
{ error: "Unauthorized", message: "token has a malformed tag-scope claim" },
|
|
500
|
+
{ status: 401 },
|
|
501
|
+
),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
428
504
|
if (err instanceof HubJwtError) {
|
|
429
505
|
// Revocation-related codes get sanitized client messages: server-side
|
|
430
506
|
// audit log carries the full diagnostic (jti for `revoked`,
|
|
@@ -464,10 +540,12 @@ async function authenticateHubJwt(
|
|
|
464
540
|
}
|
|
465
541
|
|
|
466
542
|
/**
|
|
467
|
-
* Authenticate for the unified /mcp endpoint
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
543
|
+
* Authenticate for the unified /mcp endpoint (cross-vault: /vaults metadata,
|
|
544
|
+
* /health detail). Accepts only VAULT_AUTH_TOKEN + global config.yaml keys
|
|
545
|
+
* (vault#282 Stage 2 — the per-vault pvt_* DB fallback is GONE). Hub JWTs are
|
|
546
|
+
* vault-bound (aud=vault.<name>) and have no single audience to strict-check
|
|
547
|
+
* across every vault, so they're rejected here with a redirect hint to the
|
|
548
|
+
* per-vault `/vault/<name>/*` surface.
|
|
471
549
|
*/
|
|
472
550
|
export async function authenticateGlobalRequest(
|
|
473
551
|
req: Request,
|
|
@@ -516,35 +594,8 @@ export async function authenticateGlobalRequest(
|
|
|
516
594
|
}
|
|
517
595
|
}
|
|
518
596
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
// authenticate against the unified /mcp endpoint. The token's vault
|
|
522
|
-
// binding (if any) is propagated via AuthResult.vault_name; downstream
|
|
523
|
-
// handlers that operate on a specific vault are responsible for
|
|
524
|
-
// checking that binding matches their target. The unified surface
|
|
525
|
-
// itself doesn't reject here — a vault-bound token authenticating to
|
|
526
|
-
// call back into its own vault via /mcp is legitimate.
|
|
527
|
-
for (const vaultName of listVaults()) {
|
|
528
|
-
try {
|
|
529
|
-
const store = getVaultStore(vaultName);
|
|
530
|
-
const resolved = resolveToken(store.db, key);
|
|
531
|
-
if (resolved) {
|
|
532
|
-
if (resolved.legacyDerived) {
|
|
533
|
-
warnLegacyOnce(`vault-token:${vaultName}`, "vault token without scopes column");
|
|
534
|
-
}
|
|
535
|
-
return {
|
|
536
|
-
permission: resolved.permission,
|
|
537
|
-
scopes: resolved.scopes,
|
|
538
|
-
legacyDerived: resolved.legacyDerived,
|
|
539
|
-
scoped_tags: resolved.scoped_tags,
|
|
540
|
-
vault_name: resolved.vault_name,
|
|
541
|
-
caller_jti: resolved.jti,
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
} catch {
|
|
545
|
-
// Skip vaults that can't be opened
|
|
546
|
-
}
|
|
597
|
+
if (looksLikeDroppedPvtToken(key)) {
|
|
598
|
+
return { error: droppedPvtTokenResponse() };
|
|
547
599
|
}
|
|
548
|
-
|
|
549
600
|
return { error: Response.json({ error: "Unauthorized", message: "Invalid API key" }, { status: 401 }) };
|
|
550
601
|
}
|