@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/token-store.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Token operations for per-vault
|
|
2
|
+
* Token operations for the per-vault `tokens` table.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* VESTIGIAL as of 0.5.0 (vault#282 Stage 2). Vault is a pure hub
|
|
5
|
+
* resource-server: it no longer mints (`pvt_*`) or validates rows in this
|
|
6
|
+
* table. What survives here:
|
|
7
|
+
* - `listTokens` / `revokeToken` / `findTokensReferencingTag` — read/clean up
|
|
8
|
+
* any leftover pre-0.5.0 rows.
|
|
9
|
+
* - `migrateVaultKeys` — the legacy-YAML-api_keys import landing zone (raw
|
|
10
|
+
* INSERT; the only writer left).
|
|
11
|
+
* - the `mcp_mint_ledger` helpers — hub-JWT attribution for manage-token.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
13
|
+
* The `Token`/field docs below describe the historical (pre-0.5.0) auth
|
|
14
|
+
* semantics for the surviving read/cleanup paths; no validation path reads
|
|
15
|
+
* `scoped_tags` / `vault_name` off these rows anymore.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
17
|
+
* Permission levels ("full" / "read") and the legacy "admin"/"write" → "full"
|
|
18
|
+
* normalization are kept for displaying any leftover rows.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import { Database } from "bun:sqlite";
|
|
17
|
-
import crypto from "node:crypto";
|
|
18
22
|
import { hashKey } from "./config.ts";
|
|
19
23
|
import { legacyPermissionToScopes, parseScopes, serializeScopes } from "./scopes.ts";
|
|
20
24
|
|
|
@@ -71,51 +75,19 @@ export interface Token {
|
|
|
71
75
|
created_via: string | null;
|
|
72
76
|
/**
|
|
73
77
|
* Session pin (v19). When this token was minted via manage-token, this
|
|
74
|
-
* is the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
78
|
+
* is the hub JWT's jti claim of the minting session. NULL otherwise.
|
|
79
|
+
* (Vestigial post-0.5.0 — no new rows are written; vault no longer mints
|
|
80
|
+
* vault-DB tokens. See vault#282 Stage 2.)
|
|
77
81
|
*/
|
|
78
82
|
parent_jti: string | null;
|
|
79
83
|
/**
|
|
80
|
-
* Soft-revoke timestamp (v19).
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* with ok=true. NULL = active.
|
|
84
|
+
* Soft-revoke timestamp (v19). Marked the row revoked while keeping it in
|
|
85
|
+
* place for audit history. Vestigial post-0.5.0 (vault#282 Stage 2) — no
|
|
86
|
+
* validation path reads these rows anymore. NULL = active.
|
|
84
87
|
*/
|
|
85
88
|
revoked_at: string | null;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
export interface ResolvedToken {
|
|
89
|
-
permission: TokenPermission;
|
|
90
|
-
/**
|
|
91
|
-
* Granted scopes, parsed from the token row's `scopes` column. Pre-v12
|
|
92
|
-
* tokens (where the column is NULL) fall back to the legacy permission
|
|
93
|
-
* → scopes mapping and `legacyDerived` is set true so callers can log
|
|
94
|
-
* a deprecation warning on first use.
|
|
95
|
-
*/
|
|
96
|
-
scopes: string[];
|
|
97
|
-
/** True iff `scopes` was derived from the legacy `permission` column. */
|
|
98
|
-
legacyDerived: boolean;
|
|
99
|
-
/**
|
|
100
|
-
* Tag-allowlist for tag-scoped tokens (root tag names). NULL = unscoped.
|
|
101
|
-
* See `Token.scoped_tags`.
|
|
102
|
-
*/
|
|
103
|
-
scoped_tags: string[] | null;
|
|
104
|
-
/**
|
|
105
|
-
* Per-vault binding (v16). Non-null = token is bound to this vault;
|
|
106
|
-
* `authenticateVaultRequest` rejects when the bound vault doesn't match
|
|
107
|
-
* the request's vault. NULL = legacy / server-wide, accepted for any
|
|
108
|
-
* vault. See vault#257.
|
|
109
|
-
*/
|
|
110
|
-
vault_name: string | null;
|
|
111
|
-
/**
|
|
112
|
-
* Display id (`t_<hashprefix>`) of THIS token. Surfaced so callers that
|
|
113
|
-
* later mint child tokens (manage-token MCP tool) can stamp parent_jti
|
|
114
|
-
* without re-derivation. Pre-v19 lookups still compute this on the fly.
|
|
115
|
-
*/
|
|
116
|
-
jti: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
91
|
/**
|
|
120
92
|
* Parse the JSON-encoded `scoped_tags` column. Returns null for NULL/empty
|
|
121
93
|
* input. Defensive: malformed JSON or non-array shapes degrade to null
|
|
@@ -136,161 +108,15 @@ export function parseScopedTags(raw: string | null): string[] | null {
|
|
|
136
108
|
|
|
137
109
|
// ---------------------------------------------------------------------------
|
|
138
110
|
// Token operations
|
|
111
|
+
//
|
|
112
|
+
// vault#282 Stage 2: the pvt_* mint (`generateToken` / `createToken`) and
|
|
113
|
+
// validation (`resolveToken`) were removed at 0.5.0 — vault is a pure hub
|
|
114
|
+
// resource-server and no longer issues or accepts opaque vault-DB tokens.
|
|
115
|
+
// What survives: `listTokens` / `revokeToken` (cleanup of vestigial pre-0.5.0
|
|
116
|
+
// rows), the YAML-import landing zone (`migrateVaultKeys`, raw INSERT), and the
|
|
117
|
+
// `mcp_mint_ledger` machinery (hub-JWT attribution for manage-token).
|
|
139
118
|
// ---------------------------------------------------------------------------
|
|
140
119
|
|
|
141
|
-
export function generateToken(): { fullToken: string; tokenHash: string } {
|
|
142
|
-
const random = crypto.randomBytes(32).toString("base64url").slice(0, 32);
|
|
143
|
-
const fullToken = `pvt_${random}`;
|
|
144
|
-
return { fullToken, tokenHash: hashKey(fullToken) };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function createToken(
|
|
148
|
-
db: Database,
|
|
149
|
-
fullToken: string,
|
|
150
|
-
opts: {
|
|
151
|
-
label: string;
|
|
152
|
-
permission?: TokenPermission;
|
|
153
|
-
/**
|
|
154
|
-
* Explicit OAuth-standard scopes to persist. If omitted, derived from
|
|
155
|
-
* `permission` (read → [vault:read], anything else → [vault:read,
|
|
156
|
-
* vault:write, vault:admin]). Written as a whitespace-separated string.
|
|
157
|
-
*/
|
|
158
|
-
scopes?: string[];
|
|
159
|
-
/** @deprecated Written to DB but not enforced at runtime. */
|
|
160
|
-
scope_tag?: string | null;
|
|
161
|
-
/** @deprecated Written to DB but not enforced at runtime. */
|
|
162
|
-
scope_path_prefix?: string | null;
|
|
163
|
-
/**
|
|
164
|
-
* Tag-allowlist (root tag names). null/undefined → unscoped (full vault
|
|
165
|
-
* access per `scopes`). When provided, must be already-validated root tag
|
|
166
|
-
* names per patterns/tag-scoped-tokens.md (no path separators); the mint
|
|
167
|
-
* endpoint validates against existing tags before passing through.
|
|
168
|
-
*/
|
|
169
|
-
scoped_tags?: string[] | null;
|
|
170
|
-
/**
|
|
171
|
-
* Per-vault binding (v16). Non-null = token can only authenticate
|
|
172
|
-
* against this vault. NULL = legacy / server-wide; auth accepts the
|
|
173
|
-
* token for any vault. New mints via per-vault routes set this; the
|
|
174
|
-
* legacy YAML-import path leaves it NULL. See vault#257.
|
|
175
|
-
*/
|
|
176
|
-
vault_name?: string | null;
|
|
177
|
-
expires_at?: string | null;
|
|
178
|
-
/**
|
|
179
|
-
* Provenance tag (v19). `'mcp_mint'` for tokens minted via the
|
|
180
|
-
* manage-token MCP tool; omit/null for CLI / REST / YAML paths.
|
|
181
|
-
*/
|
|
182
|
-
created_via?: string | null;
|
|
183
|
-
/**
|
|
184
|
-
* Session pin (v19). Display id (`t_<prefix>`) or hub JWT `jti` of the
|
|
185
|
-
* caller that minted this token via manage-token. Used by the
|
|
186
|
-
* manage-token list/revoke surface to scope itself to one session.
|
|
187
|
-
*/
|
|
188
|
-
parent_jti?: string | null;
|
|
189
|
-
},
|
|
190
|
-
): Token {
|
|
191
|
-
const tokenHash = hashKey(fullToken);
|
|
192
|
-
const now = new Date().toISOString();
|
|
193
|
-
const permission = opts.permission ?? "full";
|
|
194
|
-
const scopes = opts.scopes ?? legacyPermissionToScopes(permission);
|
|
195
|
-
const scopesStr = serializeScopes(scopes);
|
|
196
|
-
const scopedTags = opts.scoped_tags && opts.scoped_tags.length > 0 ? opts.scoped_tags : null;
|
|
197
|
-
const scopedTagsStr = scopedTags ? JSON.stringify(scopedTags) : null;
|
|
198
|
-
const vaultName = opts.vault_name ?? null;
|
|
199
|
-
const createdVia = opts.created_via ?? null;
|
|
200
|
-
const parentJti = opts.parent_jti ?? null;
|
|
201
|
-
|
|
202
|
-
db.prepare(`
|
|
203
|
-
INSERT INTO tokens (token_hash, label, permission, scopes, scoped_tags, scope_tag, scope_path_prefix, expires_at, created_at, vault_name, created_via, parent_jti)
|
|
204
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
205
|
-
`).run(
|
|
206
|
-
tokenHash,
|
|
207
|
-
opts.label,
|
|
208
|
-
permission,
|
|
209
|
-
scopesStr,
|
|
210
|
-
scopedTagsStr,
|
|
211
|
-
opts.scope_tag ?? null,
|
|
212
|
-
opts.scope_path_prefix ?? null,
|
|
213
|
-
opts.expires_at ?? null,
|
|
214
|
-
now,
|
|
215
|
-
vaultName,
|
|
216
|
-
createdVia,
|
|
217
|
-
parentJti,
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
token_hash: tokenHash,
|
|
222
|
-
label: opts.label,
|
|
223
|
-
permission,
|
|
224
|
-
scope_tag: opts.scope_tag ?? null,
|
|
225
|
-
scope_path_prefix: opts.scope_path_prefix ?? null,
|
|
226
|
-
scoped_tags: scopedTags,
|
|
227
|
-
vault_name: vaultName,
|
|
228
|
-
expires_at: opts.expires_at ?? null,
|
|
229
|
-
created_at: now,
|
|
230
|
-
last_used_at: null,
|
|
231
|
-
created_via: createdVia,
|
|
232
|
-
parent_jti: parentJti,
|
|
233
|
-
revoked_at: null,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Resolve a bearer token. Returns the token info if valid, null if not found or expired.
|
|
239
|
-
* Updates last_used_at on successful resolution.
|
|
240
|
-
*/
|
|
241
|
-
export function resolveToken(db: Database, providedToken: string): ResolvedToken | null {
|
|
242
|
-
// Hash-then-lookup: the SQL = comparison on SHA-256 output is not timing-safe,
|
|
243
|
-
// but this is acceptable — the attacker would need to guess a valid SHA-256
|
|
244
|
-
// preimage, which is computationally infeasible regardless of timing leaks.
|
|
245
|
-
const candidateHash = hashKey(providedToken);
|
|
246
|
-
|
|
247
|
-
// Defensive SELECT for revoked_at: the column exists post-v19, but a
|
|
248
|
-
// freshly-opened ResolvedToken-only test fixture might run on a DB the
|
|
249
|
-
// migration hasn't touched. SQLite returns NULL for missing columns when
|
|
250
|
-
// the table is queried via prepared statements only after migration; here
|
|
251
|
-
// initSchema fires on every store-open path, so the column is guaranteed
|
|
252
|
-
// present in production. Tests instantiating bare DBs against this
|
|
253
|
-
// module are expected to call initSchema first.
|
|
254
|
-
const row = db.prepare(`
|
|
255
|
-
SELECT token_hash, permission, scopes, scoped_tags, expires_at, vault_name, revoked_at
|
|
256
|
-
FROM tokens WHERE token_hash = ?
|
|
257
|
-
`).get(candidateHash) as {
|
|
258
|
-
token_hash: string;
|
|
259
|
-
permission: string;
|
|
260
|
-
scopes: string | null;
|
|
261
|
-
scoped_tags: string | null;
|
|
262
|
-
expires_at: string | null;
|
|
263
|
-
vault_name: string | null;
|
|
264
|
-
revoked_at: string | null;
|
|
265
|
-
} | null;
|
|
266
|
-
|
|
267
|
-
if (!row) return null;
|
|
268
|
-
|
|
269
|
-
// Soft-revoked tokens never authenticate (v19). The row stays in place
|
|
270
|
-
// for audit; resolveToken just treats it as not-found from the caller's
|
|
271
|
-
// perspective.
|
|
272
|
-
if (row.revoked_at) return null;
|
|
273
|
-
|
|
274
|
-
// Check expiry
|
|
275
|
-
if (row.expires_at && new Date(row.expires_at) < new Date()) {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Update last_used_at
|
|
280
|
-
db.prepare("UPDATE tokens SET last_used_at = ? WHERE token_hash = ?")
|
|
281
|
-
.run(new Date().toISOString(), row.token_hash);
|
|
282
|
-
|
|
283
|
-
const permission = normalizePermission(row.permission);
|
|
284
|
-
const parsed = parseScopes(row.scopes);
|
|
285
|
-
const hasVaultScope = parsed.some((s) => s.startsWith("vault:"));
|
|
286
|
-
const scopes = hasVaultScope ? parsed : legacyPermissionToScopes(permission);
|
|
287
|
-
const legacyDerived = !hasVaultScope;
|
|
288
|
-
const scoped_tags = parseScopedTags(row.scoped_tags);
|
|
289
|
-
const jti = `t_${row.token_hash.slice(7, 19)}`;
|
|
290
|
-
|
|
291
|
-
return { permission, scopes, legacyDerived, scoped_tags, vault_name: row.vault_name, jti };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
120
|
/**
|
|
295
121
|
* List tokens (for CLI display + admin SPA). Never exposes the hash
|
|
296
122
|
* directly — shows a truncated prefix for identification.
|
|
@@ -325,17 +151,70 @@ export function listTokens(
|
|
|
325
151
|
}));
|
|
326
152
|
}
|
|
327
153
|
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// mcp_mint_ledger — session-pinned index of HUB JWTs minted by manage-token
|
|
156
|
+
// (vault#403, MGT). After the auth-unification arc the tool mints hub JWTs,
|
|
157
|
+
// not pvt_* rows, so the session attribution (parent_jti → minted jti) lives
|
|
158
|
+
// here instead of in the `tokens` table. Rows are NOT credentials — only the
|
|
159
|
+
// hub `jti` (the revocation handle) plus display metadata is stored; the
|
|
160
|
+
// signed token never touches the vault DB.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
328
163
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* don't see each other's tokens, and CLI/REST-minted tokens never appear.
|
|
164
|
+
* Record a hub-minted JWT in the session-pinned ledger. `jti` is hub's
|
|
165
|
+
* returned jti; `parentJti` is the minting MCP session (the caller's
|
|
166
|
+
* `caller_jti`).
|
|
333
167
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
168
|
+
* Uses INSERT OR IGNORE, NOT OR REPLACE: hub guarantees jti uniqueness, so a
|
|
169
|
+
* pre-existing row with this jti shouldn't happen. If it does, it's a real bug
|
|
170
|
+
* (a hub jti collision) — we log a warning and KEEP the existing row rather
|
|
171
|
+
* than overwriting it, because OR REPLACE would silently reset a previously-set
|
|
172
|
+
* `revoked_at` and resurrect a revoked token in the list/revoke surface.
|
|
337
173
|
*/
|
|
338
|
-
export function
|
|
174
|
+
export function recordMcpMintLedger(
|
|
175
|
+
db: Database,
|
|
176
|
+
entry: {
|
|
177
|
+
jti: string;
|
|
178
|
+
parentJti: string;
|
|
179
|
+
vaultName: string;
|
|
180
|
+
label: string;
|
|
181
|
+
scopes: string[];
|
|
182
|
+
scopedTags: string[] | null;
|
|
183
|
+
expiresAt: string | null;
|
|
184
|
+
},
|
|
185
|
+
): void {
|
|
186
|
+
const scopedTags = entry.scopedTags && entry.scopedTags.length > 0 ? entry.scopedTags : null;
|
|
187
|
+
const result = db.prepare(`
|
|
188
|
+
INSERT OR IGNORE INTO mcp_mint_ledger
|
|
189
|
+
(jti, parent_jti, vault_name, label, scopes, scoped_tags, created_at, expires_at, revoked_at)
|
|
190
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
191
|
+
`).run(
|
|
192
|
+
entry.jti,
|
|
193
|
+
entry.parentJti,
|
|
194
|
+
entry.vaultName,
|
|
195
|
+
entry.label,
|
|
196
|
+
serializeScopes(entry.scopes),
|
|
197
|
+
scopedTags ? JSON.stringify(scopedTags) : null,
|
|
198
|
+
new Date().toISOString(),
|
|
199
|
+
entry.expiresAt,
|
|
200
|
+
);
|
|
201
|
+
if (result.changes === 0) {
|
|
202
|
+
// Row already existed — IGNORE swallowed the conflict. Surface it: a hub
|
|
203
|
+
// jti collision is a real bug worth investigating (the existing row is
|
|
204
|
+
// left untouched, including any `revoked_at`).
|
|
205
|
+
console.warn(
|
|
206
|
+
`[manage-token] mcp_mint_ledger already has a row for jti '${entry.jti}' — skipped insert (kept existing row). A hub jti collision shouldn't happen; investigate.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* List hub JWTs minted by a given MCP session (parent_jti) against a vault.
|
|
213
|
+
* Returns metadata only (the hub `jti`, label, scopes, timestamps) so the
|
|
214
|
+
* manage-token list surface stays session-scoped. Includes `revoked_at` so
|
|
215
|
+
* callers can render a tombstone for soft-revoked rows.
|
|
216
|
+
*/
|
|
217
|
+
export function listMcpMintedHubJwts(
|
|
339
218
|
db: Database,
|
|
340
219
|
parentJti: string,
|
|
341
220
|
vaultName: string,
|
|
@@ -349,14 +228,12 @@ export function listMcpMintedTokens(
|
|
|
349
228
|
revoked_at: string | null;
|
|
350
229
|
}> {
|
|
351
230
|
const rows = db.prepare(`
|
|
352
|
-
SELECT
|
|
353
|
-
FROM
|
|
354
|
-
WHERE
|
|
355
|
-
AND parent_jti = ?
|
|
356
|
-
AND vault_name = ?
|
|
231
|
+
SELECT jti, label, scopes, scoped_tags, created_at, expires_at, revoked_at
|
|
232
|
+
FROM mcp_mint_ledger
|
|
233
|
+
WHERE parent_jti = ? AND vault_name = ?
|
|
357
234
|
ORDER BY created_at DESC
|
|
358
235
|
`).all(parentJti, vaultName) as {
|
|
359
|
-
|
|
236
|
+
jti: string;
|
|
360
237
|
label: string;
|
|
361
238
|
scopes: string | null;
|
|
362
239
|
scoped_tags: string | null;
|
|
@@ -365,7 +242,7 @@ export function listMcpMintedTokens(
|
|
|
365
242
|
revoked_at: string | null;
|
|
366
243
|
}[];
|
|
367
244
|
return rows.map((r) => ({
|
|
368
|
-
jti:
|
|
245
|
+
jti: r.jti,
|
|
369
246
|
label: r.label,
|
|
370
247
|
scopes: parseScopes(r.scopes),
|
|
371
248
|
scoped_tags: parseScopedTags(r.scoped_tags),
|
|
@@ -376,47 +253,44 @@ export function listMcpMintedTokens(
|
|
|
376
253
|
}
|
|
377
254
|
|
|
378
255
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* resolveToken treats revoked_at-set rows as not-found.
|
|
385
|
-
*
|
|
386
|
-
* `parentJti` + `vaultName` scope the lookup: a token minted by a
|
|
387
|
-
* different MCP session (or against a different vault) returns ok=false.
|
|
388
|
-
* Returns { ok: true, already_revoked? } when the operation matched a row.
|
|
256
|
+
* Look up a single ledger row by (jti, parent_jti, vault_name) — the
|
|
257
|
+
* session-pin gate for revoke. Returns null when the jti isn't in THIS
|
|
258
|
+
* session's ledger (a different session's mint, or a never-minted jti),
|
|
259
|
+
* which the caller turns into the not_found path. Returns the row (incl.
|
|
260
|
+
* `revoked_at`) when it belongs to this session.
|
|
389
261
|
*/
|
|
390
|
-
export function
|
|
262
|
+
export function findMcpMintLedgerEntry(
|
|
391
263
|
db: Database,
|
|
392
264
|
jti: string,
|
|
393
265
|
parentJti: string,
|
|
394
266
|
vaultName: string,
|
|
395
|
-
): {
|
|
396
|
-
if (!jti.startsWith("t_")) {
|
|
397
|
-
return { ok: false, reason: "not_found" };
|
|
398
|
-
}
|
|
399
|
-
const hashPrefix = jti.slice(2);
|
|
267
|
+
): { jti: string; revoked_at: string | null } | null {
|
|
400
268
|
const row = db.prepare(`
|
|
401
|
-
SELECT
|
|
402
|
-
WHERE
|
|
403
|
-
AND created_via = 'mcp_mint'
|
|
404
|
-
AND parent_jti = ?
|
|
405
|
-
AND vault_name = ?
|
|
269
|
+
SELECT jti, revoked_at FROM mcp_mint_ledger
|
|
270
|
+
WHERE jti = ? AND parent_jti = ? AND vault_name = ?
|
|
406
271
|
LIMIT 1
|
|
407
|
-
`).get(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
} | null;
|
|
272
|
+
`).get(jti, parentJti, vaultName) as { jti: string; revoked_at: string | null } | null;
|
|
273
|
+
return row ?? null;
|
|
274
|
+
}
|
|
411
275
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Mark a ledger row revoked (the local attribution-side soft-revoke; the
|
|
278
|
+
* authoritative revocation happens in hub's registry via the revoke-token
|
|
279
|
+
* call). Idempotent: a second call on an already-revoked row leaves the
|
|
280
|
+
* existing timestamp in place. Only flips rows belonging to the given
|
|
281
|
+
* session — defense-in-depth on top of the caller's `findMcpMintLedgerEntry`
|
|
282
|
+
* gate.
|
|
283
|
+
*/
|
|
284
|
+
export function markMcpMintLedgerRevoked(
|
|
285
|
+
db: Database,
|
|
286
|
+
jti: string,
|
|
287
|
+
parentJti: string,
|
|
288
|
+
vaultName: string,
|
|
289
|
+
): void {
|
|
290
|
+
db.prepare(`
|
|
291
|
+
UPDATE mcp_mint_ledger SET revoked_at = ?
|
|
292
|
+
WHERE jti = ? AND parent_jti = ? AND vault_name = ? AND revoked_at IS NULL
|
|
293
|
+
`).run(new Date().toISOString(), jti, parentJti, vaultName);
|
|
420
294
|
}
|
|
421
295
|
|
|
422
296
|
/**
|
package/src/vault-create.test.ts
CHANGED
|
@@ -60,7 +60,13 @@ describe("vault create --json", () => {
|
|
|
60
60
|
expect(lines).toHaveLength(1);
|
|
61
61
|
const payload = JSON.parse(lines[0]!);
|
|
62
62
|
expect(payload.name).toBe("myvault");
|
|
63
|
-
|
|
63
|
+
// vault#282 Stage 2: vault no longer mints pvt_* tokens. The contract
|
|
64
|
+
// hub's admin-vaults.ts requires still holds (`token` is a string). In
|
|
65
|
+
// this sandbox there's no hub/operator.token, so no token is issued: the
|
|
66
|
+
// token field is the empty string and `token_guidance` explains why.
|
|
67
|
+
expect(typeof payload.token).toBe("string");
|
|
68
|
+
expect(payload.token).toBe("");
|
|
69
|
+
expect(payload.token_guidance).toContain("No token issued");
|
|
64
70
|
expect(payload.set_as_default).toBe(true);
|
|
65
71
|
expect(payload.paths.vault_dir).toBe(join(home, "vault", "data", "myvault"));
|
|
66
72
|
expect(payload.paths.vault_db).toBe(join(home, "vault", "data", "myvault", "vault.db"));
|
|
@@ -177,7 +183,7 @@ describe("vault create — services.json registration (#208)", () => {
|
|
|
177
183
|
});
|
|
178
184
|
});
|
|
179
185
|
|
|
180
|
-
describe("vault create (human mode
|
|
186
|
+
describe("vault create (human mode)", () => {
|
|
181
187
|
test("prints multi-line human output without --json", () => {
|
|
182
188
|
const { exitCode, stdout } = runCli(
|
|
183
189
|
["create", "human"],
|
|
@@ -185,8 +191,10 @@ describe("vault create (human mode unchanged)", () => {
|
|
|
185
191
|
);
|
|
186
192
|
expect(exitCode).toBe(0);
|
|
187
193
|
expect(stdout).toContain('Vault "human" created.');
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
// vault#282 Stage 2: with no hub reachable in this sandbox, no token is
|
|
195
|
+
// issued — the human output prints the guidance instead of "API token:".
|
|
196
|
+
expect(stdout).toContain("No token issued");
|
|
197
|
+
expect(stdout).toContain("Install the hub");
|
|
190
198
|
// Human output should NOT be valid JSON.
|
|
191
199
|
expect(() => JSON.parse(stdout.trim())).toThrow();
|
|
192
200
|
});
|