@openparachute/vault 0.4.9-rc.3 → 0.4.9-rc.4
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/core/src/mcp.ts +35 -0
- package/core/src/schema.ts +51 -2
- package/package.json +1 -1
- package/src/auth.ts +29 -1
- package/src/mcp-http.ts +24 -36
- package/src/mcp-tools.ts +286 -2
- package/src/mirror-routes.test.ts +59 -1
- package/src/mirror-routes.ts +41 -2
- package/src/routing.test.ts +73 -0
- package/src/routing.ts +34 -1
- package/src/token-store.ts +158 -5
- package/src/vault.test.ts +380 -1
- package/web/ui/dist/assets/index-BJX47k5V.js +60 -0
- package/web/ui/dist/assets/{index-BOa-JJtV.css → index-KA1P2P3z.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-BuhaC9k5.js +0 -60
package/src/vault.test.ts
CHANGED
|
@@ -3941,6 +3941,10 @@ describe("stateless MCP transport", async () => {
|
|
|
3941
3941
|
expect(toolNames).not.toContain("delete-note");
|
|
3942
3942
|
expect(toolNames).not.toContain("update-tag");
|
|
3943
3943
|
expect(toolNames).not.toContain("delete-tag");
|
|
3944
|
+
// Admin tools (vault#376) are hidden too
|
|
3945
|
+
expect(toolNames).not.toContain("manage-token");
|
|
3946
|
+
// Read tier is exactly 4 tools.
|
|
3947
|
+
expect(toolNames.length).toBe(4);
|
|
3944
3948
|
|
|
3945
3949
|
closeAllStores();
|
|
3946
3950
|
});
|
|
@@ -4077,7 +4081,13 @@ describe("stateless MCP transport", async () => {
|
|
|
4077
4081
|
expect(res.status).toBe(200); // JSON-RPC envelope is 200 even for tool errors
|
|
4078
4082
|
const body = await res.json() as any;
|
|
4079
4083
|
expect(body.result.isError).toBe(true);
|
|
4080
|
-
|
|
4084
|
+
// Post-vault#376: hidden tools surface as "Unknown tool" rather than
|
|
4085
|
+
// a verb-specific Forbidden — see mcp-http.ts dispatch-against-
|
|
4086
|
+
// visibleTools rationale. The contract is: tools not in tools/list
|
|
4087
|
+
// also can't be called explicitly. (Differential errors would leak
|
|
4088
|
+
// the existence of admin-only tools to write-scope sessions.)
|
|
4089
|
+
expect(body.result.content[0].text).toContain("Unknown tool");
|
|
4090
|
+
expect(body.result.content[0].text).toContain("create-note");
|
|
4081
4091
|
|
|
4082
4092
|
closeAllStores();
|
|
4083
4093
|
});
|
|
@@ -4129,6 +4139,375 @@ describe("stateless MCP transport", async () => {
|
|
|
4129
4139
|
});
|
|
4130
4140
|
});
|
|
4131
4141
|
|
|
4142
|
+
// ===========================================================================
|
|
4143
|
+
// vault#376 — Change 1: scope-filtered tool listing across all three tiers
|
|
4144
|
+
// ===========================================================================
|
|
4145
|
+
|
|
4146
|
+
describe("MCP tools/list scope tiers (vault#376)", () => {
|
|
4147
|
+
async function listToolNames(scopes: string[], scopedTags: string[] | null = null, vaultPrefix = "scope-tier") {
|
|
4148
|
+
const { handleScopedMcp } = await import("./mcp-http.ts");
|
|
4149
|
+
const { writeVaultConfig } = await import("./config.ts");
|
|
4150
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4151
|
+
|
|
4152
|
+
const vaultName = `${vaultPrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4153
|
+
writeVaultConfig({
|
|
4154
|
+
name: vaultName,
|
|
4155
|
+
api_keys: [],
|
|
4156
|
+
created_at: new Date().toISOString(),
|
|
4157
|
+
});
|
|
4158
|
+
|
|
4159
|
+
const req = new Request(`http://localhost:1940/vault/${vaultName}/mcp`, {
|
|
4160
|
+
method: "POST",
|
|
4161
|
+
headers: {
|
|
4162
|
+
"content-type": "application/json",
|
|
4163
|
+
"accept": "application/json, text/event-stream",
|
|
4164
|
+
},
|
|
4165
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
4166
|
+
});
|
|
4167
|
+
|
|
4168
|
+
const res = await handleScopedMcp(req, vaultName, {
|
|
4169
|
+
permission: scopes.includes("vault:write") || scopes.includes("vault:admin") ? "full" : "read",
|
|
4170
|
+
scopes,
|
|
4171
|
+
legacyDerived: false,
|
|
4172
|
+
scoped_tags: scopedTags,
|
|
4173
|
+
} as any);
|
|
4174
|
+
const body = await res.json() as any;
|
|
4175
|
+
const names: string[] = body.result.tools.map((t: any) => t.name);
|
|
4176
|
+
closeAllStores();
|
|
4177
|
+
return names;
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
test("vault:read sees exactly the 4 read tools", async () => {
|
|
4181
|
+
const names = await listToolNames(["vault:read"]);
|
|
4182
|
+
expect(new Set(names)).toEqual(
|
|
4183
|
+
new Set(["query-notes", "list-tags", "find-path", "vault-info"]),
|
|
4184
|
+
);
|
|
4185
|
+
expect(names.length).toBe(4);
|
|
4186
|
+
});
|
|
4187
|
+
|
|
4188
|
+
test("vault:read + vault:write sees the 9 read+write tools", async () => {
|
|
4189
|
+
const names = await listToolNames(["vault:read", "vault:write"]);
|
|
4190
|
+
expect(new Set(names)).toEqual(
|
|
4191
|
+
new Set([
|
|
4192
|
+
"query-notes",
|
|
4193
|
+
"list-tags",
|
|
4194
|
+
"find-path",
|
|
4195
|
+
"vault-info",
|
|
4196
|
+
"create-note",
|
|
4197
|
+
"update-note",
|
|
4198
|
+
"delete-note",
|
|
4199
|
+
"update-tag",
|
|
4200
|
+
"delete-tag",
|
|
4201
|
+
]),
|
|
4202
|
+
);
|
|
4203
|
+
expect(names.length).toBe(9);
|
|
4204
|
+
expect(names).not.toContain("manage-token");
|
|
4205
|
+
// Aaron 2026-05-27: delete-* are write-tier (same destructive verb as
|
|
4206
|
+
// update). Only manage-token is admin-gated.
|
|
4207
|
+
expect(names).toContain("delete-note");
|
|
4208
|
+
expect(names).toContain("delete-tag");
|
|
4209
|
+
});
|
|
4210
|
+
|
|
4211
|
+
test("vault:admin sees all 10 tools including manage-token", async () => {
|
|
4212
|
+
const names = await listToolNames(["vault:read", "vault:write", "vault:admin"]);
|
|
4213
|
+
expect(names).toContain("manage-token");
|
|
4214
|
+
expect(names.length).toBe(10);
|
|
4215
|
+
});
|
|
4216
|
+
|
|
4217
|
+
test("legacy-derived full token sees all 10 tools (back-compat)", async () => {
|
|
4218
|
+
const { handleScopedMcp } = await import("./mcp-http.ts");
|
|
4219
|
+
const { writeVaultConfig } = await import("./config.ts");
|
|
4220
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4221
|
+
|
|
4222
|
+
const vaultName = `legacy-token-${Date.now()}`;
|
|
4223
|
+
writeVaultConfig({
|
|
4224
|
+
name: vaultName,
|
|
4225
|
+
api_keys: [],
|
|
4226
|
+
created_at: new Date().toISOString(),
|
|
4227
|
+
});
|
|
4228
|
+
|
|
4229
|
+
const req = new Request(`http://localhost:1940/vault/${vaultName}/mcp`, {
|
|
4230
|
+
method: "POST",
|
|
4231
|
+
headers: {
|
|
4232
|
+
"content-type": "application/json",
|
|
4233
|
+
"accept": "application/json, text/event-stream",
|
|
4234
|
+
},
|
|
4235
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
4236
|
+
});
|
|
4237
|
+
|
|
4238
|
+
// Legacy permission-derived token: legacyDerived=true, scopes carry the
|
|
4239
|
+
// full admin set per `legacyPermissionToScopes("full")`. Compat shim
|
|
4240
|
+
// means the operator's existing pvt_* tokens minted pre-scope-column
|
|
4241
|
+
// see the full surface (including manage-token), not just the 9 they
|
|
4242
|
+
// had before.
|
|
4243
|
+
const res = await handleScopedMcp(req, vaultName, {
|
|
4244
|
+
permission: "full",
|
|
4245
|
+
scopes: ["vault:read", "vault:write", "vault:admin"],
|
|
4246
|
+
legacyDerived: true,
|
|
4247
|
+
scoped_tags: null,
|
|
4248
|
+
} as any);
|
|
4249
|
+
const body = await res.json() as any;
|
|
4250
|
+
const names: string[] = body.result.tools.map((t: any) => t.name);
|
|
4251
|
+
expect(names.length).toBe(10);
|
|
4252
|
+
expect(names).toContain("manage-token");
|
|
4253
|
+
closeAllStores();
|
|
4254
|
+
});
|
|
4255
|
+
|
|
4256
|
+
test("excluded tools surface as 'Unknown tool' if called explicitly", async () => {
|
|
4257
|
+
const { handleScopedMcp } = await import("./mcp-http.ts");
|
|
4258
|
+
const { writeVaultConfig } = await import("./config.ts");
|
|
4259
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4260
|
+
|
|
4261
|
+
const vaultName = `hidden-call-${Date.now()}`;
|
|
4262
|
+
writeVaultConfig({
|
|
4263
|
+
name: vaultName,
|
|
4264
|
+
api_keys: [],
|
|
4265
|
+
created_at: new Date().toISOString(),
|
|
4266
|
+
});
|
|
4267
|
+
|
|
4268
|
+
// Write-scope session calling manage-token (admin-only): should look
|
|
4269
|
+
// like the tool doesn't exist, not "Forbidden: requires vault:admin".
|
|
4270
|
+
// Differential messages would leak the admin tool's existence.
|
|
4271
|
+
const req = new Request(`http://localhost:1940/vault/${vaultName}/mcp`, {
|
|
4272
|
+
method: "POST",
|
|
4273
|
+
headers: {
|
|
4274
|
+
"content-type": "application/json",
|
|
4275
|
+
"accept": "application/json, text/event-stream",
|
|
4276
|
+
},
|
|
4277
|
+
body: JSON.stringify({
|
|
4278
|
+
jsonrpc: "2.0",
|
|
4279
|
+
id: 1,
|
|
4280
|
+
method: "tools/call",
|
|
4281
|
+
params: { name: "manage-token", arguments: { action: "list" } },
|
|
4282
|
+
}),
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
const res = await handleScopedMcp(req, vaultName, {
|
|
4286
|
+
permission: "full",
|
|
4287
|
+
scopes: ["vault:read", "vault:write"],
|
|
4288
|
+
legacyDerived: false,
|
|
4289
|
+
scoped_tags: null,
|
|
4290
|
+
} as any);
|
|
4291
|
+
const body = await res.json() as any;
|
|
4292
|
+
expect(body.result.isError).toBe(true);
|
|
4293
|
+
expect(body.result.content[0].text).toContain("Unknown tool");
|
|
4294
|
+
expect(body.result.content[0].text).toContain("manage-token");
|
|
4295
|
+
expect(body.result.content[0].text).not.toContain("vault:admin");
|
|
4296
|
+
closeAllStores();
|
|
4297
|
+
});
|
|
4298
|
+
});
|
|
4299
|
+
|
|
4300
|
+
// ===========================================================================
|
|
4301
|
+
// vault#376 — Change 2: manage-token mint/revoke/list
|
|
4302
|
+
// ===========================================================================
|
|
4303
|
+
|
|
4304
|
+
describe("manage-token MCP tool (vault#376)", () => {
|
|
4305
|
+
async function callTool(
|
|
4306
|
+
vaultName: string,
|
|
4307
|
+
auth: any,
|
|
4308
|
+
toolName: string,
|
|
4309
|
+
args: Record<string, unknown>,
|
|
4310
|
+
) {
|
|
4311
|
+
const { handleScopedMcp } = await import("./mcp-http.ts");
|
|
4312
|
+
const req = new Request(`http://localhost:1940/vault/${vaultName}/mcp`, {
|
|
4313
|
+
method: "POST",
|
|
4314
|
+
headers: {
|
|
4315
|
+
"content-type": "application/json",
|
|
4316
|
+
"accept": "application/json, text/event-stream",
|
|
4317
|
+
},
|
|
4318
|
+
body: JSON.stringify({
|
|
4319
|
+
jsonrpc: "2.0",
|
|
4320
|
+
id: 1,
|
|
4321
|
+
method: "tools/call",
|
|
4322
|
+
params: { name: toolName, arguments: args },
|
|
4323
|
+
}),
|
|
4324
|
+
});
|
|
4325
|
+
const res = await handleScopedMcp(req, vaultName, auth);
|
|
4326
|
+
const body = await res.json() as any;
|
|
4327
|
+
if (body.result?.content?.[0]?.text) {
|
|
4328
|
+
try {
|
|
4329
|
+
return { isError: !!body.result.isError, parsed: JSON.parse(body.result.content[0].text), raw: body };
|
|
4330
|
+
} catch {
|
|
4331
|
+
return { isError: !!body.result.isError, parsed: null, raw: body, text: body.result.content[0].text };
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
return { isError: false, parsed: null, raw: body };
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
async function setupAdminSession(prefix: string) {
|
|
4338
|
+
const { writeVaultConfig } = await import("./config.ts");
|
|
4339
|
+
const vaultName = `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4340
|
+
writeVaultConfig({
|
|
4341
|
+
name: vaultName,
|
|
4342
|
+
api_keys: [],
|
|
4343
|
+
created_at: new Date().toISOString(),
|
|
4344
|
+
});
|
|
4345
|
+
// Stable caller_jti so list/revoke can find the mints; we don't go
|
|
4346
|
+
// through the actual auth flow here (that would require a real pvt_
|
|
4347
|
+
// token; the unit-level test point is the manage-token logic itself).
|
|
4348
|
+
const auth: any = {
|
|
4349
|
+
permission: "full",
|
|
4350
|
+
scopes: ["vault:read", "vault:write", "vault:admin"],
|
|
4351
|
+
legacyDerived: false,
|
|
4352
|
+
scoped_tags: null,
|
|
4353
|
+
vault_name: vaultName,
|
|
4354
|
+
caller_jti: `t_session${Math.random().toString(36).slice(2, 12)}`,
|
|
4355
|
+
};
|
|
4356
|
+
return { vaultName, auth };
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
test("mint with default TTL returns valid token + jti + expires_at ~15min out", async () => {
|
|
4360
|
+
const { vaultName, auth } = await setupAdminSession("mint-default");
|
|
4361
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4362
|
+
const before = Date.now();
|
|
4363
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
4364
|
+
action: "mint",
|
|
4365
|
+
scope: "vault:read",
|
|
4366
|
+
});
|
|
4367
|
+
expect(parsed.action).toBe("mint");
|
|
4368
|
+
expect(parsed.token).toMatch(/^pvt_/);
|
|
4369
|
+
expect(parsed.jti).toMatch(/^t_/);
|
|
4370
|
+
const expiresAt = Date.parse(parsed.expires_at);
|
|
4371
|
+
expect(expiresAt - before).toBeGreaterThan(890 * 1000);
|
|
4372
|
+
expect(expiresAt - before).toBeLessThan(910 * 1000);
|
|
4373
|
+
closeAllStores();
|
|
4374
|
+
});
|
|
4375
|
+
|
|
4376
|
+
test("mint with custom TTL=3600 returns expires_at ~1 hour out", async () => {
|
|
4377
|
+
const { vaultName, auth } = await setupAdminSession("mint-max");
|
|
4378
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4379
|
+
const before = Date.now();
|
|
4380
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
4381
|
+
action: "mint",
|
|
4382
|
+
scope: "vault:read",
|
|
4383
|
+
ttl_seconds: 3600,
|
|
4384
|
+
});
|
|
4385
|
+
expect(parsed.action).toBe("mint");
|
|
4386
|
+
const expiresAt = Date.parse(parsed.expires_at);
|
|
4387
|
+
expect(expiresAt - before).toBeGreaterThan(3590 * 1000);
|
|
4388
|
+
expect(expiresAt - before).toBeLessThan(3610 * 1000);
|
|
4389
|
+
closeAllStores();
|
|
4390
|
+
});
|
|
4391
|
+
|
|
4392
|
+
test("mint with TTL=0 is rejected", async () => {
|
|
4393
|
+
const { vaultName, auth } = await setupAdminSession("mint-zero");
|
|
4394
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4395
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
4396
|
+
action: "mint",
|
|
4397
|
+
scope: "vault:read",
|
|
4398
|
+
ttl_seconds: 0,
|
|
4399
|
+
});
|
|
4400
|
+
expect(parsed.error).toBe("invalid_request");
|
|
4401
|
+
closeAllStores();
|
|
4402
|
+
});
|
|
4403
|
+
|
|
4404
|
+
test("mint with TTL=3601 is rejected (over the 3600 cap)", async () => {
|
|
4405
|
+
const { vaultName, auth } = await setupAdminSession("mint-over");
|
|
4406
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4407
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
4408
|
+
action: "mint",
|
|
4409
|
+
scope: "vault:read",
|
|
4410
|
+
ttl_seconds: 3601,
|
|
4411
|
+
});
|
|
4412
|
+
expect(parsed.error).toBe("invalid_request");
|
|
4413
|
+
closeAllStores();
|
|
4414
|
+
});
|
|
4415
|
+
|
|
4416
|
+
test("mint with scope outside caller's subset is rejected", async () => {
|
|
4417
|
+
const { vaultName, auth } = await setupAdminSession("mint-subset");
|
|
4418
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4419
|
+
// Caller's auth carries admin/write/read for THIS vault. Asking for a
|
|
4420
|
+
// scope naming a different vault is the canonical privilege-escalation
|
|
4421
|
+
// surface — must reject.
|
|
4422
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
4423
|
+
action: "mint",
|
|
4424
|
+
scope: "vault:other-vault:write",
|
|
4425
|
+
});
|
|
4426
|
+
expect(parsed.error).toBe("forbidden");
|
|
4427
|
+
expect(parsed.rejected).toBeDefined();
|
|
4428
|
+
closeAllStores();
|
|
4429
|
+
});
|
|
4430
|
+
|
|
4431
|
+
test("revoke own minted token returns ok=true; second revoke is idempotent", async () => {
|
|
4432
|
+
const { vaultName, auth } = await setupAdminSession("revoke-idem");
|
|
4433
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
4434
|
+
const mint = await callTool(vaultName, auth, "manage-token", {
|
|
4435
|
+
action: "mint",
|
|
4436
|
+
scope: "vault:read",
|
|
4437
|
+
});
|
|
4438
|
+
const jti = mint.parsed.jti;
|
|
4439
|
+
const first = await callTool(vaultName, auth, "manage-token", {
|
|
4440
|
+
action: "revoke",
|
|
4441
|
+
jti,
|
|
4442
|
+
});
|
|
4443
|
+
expect(first.parsed.ok).toBe(true);
|
|
4444
|
+
expect(first.parsed.already_revoked).toBe(false);
|
|
4445
|
+
const second = await callTool(vaultName, auth, "manage-token", {
|
|
4446
|
+
action: "revoke",
|
|
4447
|
+
jti,
|
|
4448
|
+
});
|
|
4449
|
+
expect(second.parsed.ok).toBe(true);
|
|
4450
|
+
expect(second.parsed.already_revoked).toBe(true);
|
|
4451
|
+
closeAllStores();
|
|
4452
|
+
});
|
|
4453
|
+
|
|
4454
|
+
test("list returns this session's mints, not other sessions' or CLI mints", async () => {
|
|
4455
|
+
const { vaultName, auth } = await setupAdminSession("list-session");
|
|
4456
|
+
const { closeAllStores, getVaultStore } = await import("./vault-store.ts");
|
|
4457
|
+
const { createToken, generateToken } = await import("./token-store.ts");
|
|
4458
|
+
|
|
4459
|
+
// Mint two via manage-token in THIS session.
|
|
4460
|
+
const m1 = await callTool(vaultName, auth, "manage-token", { action: "mint", scope: "vault:read", description: "alpha" });
|
|
4461
|
+
const m2 = await callTool(vaultName, auth, "manage-token", { action: "mint", scope: "vault:read", description: "beta" });
|
|
4462
|
+
|
|
4463
|
+
// Mint one CLI-style (no created_via) and one from another session.
|
|
4464
|
+
const store = getVaultStore(vaultName);
|
|
4465
|
+
createToken(store.db, generateToken().fullToken, {
|
|
4466
|
+
label: "cli-token",
|
|
4467
|
+
permission: "full",
|
|
4468
|
+
scopes: ["vault:read"],
|
|
4469
|
+
vault_name: vaultName,
|
|
4470
|
+
});
|
|
4471
|
+
createToken(store.db, generateToken().fullToken, {
|
|
4472
|
+
label: "other-session-mint",
|
|
4473
|
+
permission: "full",
|
|
4474
|
+
scopes: ["vault:read"],
|
|
4475
|
+
vault_name: vaultName,
|
|
4476
|
+
created_via: "mcp_mint",
|
|
4477
|
+
parent_jti: "t_othersession",
|
|
4478
|
+
});
|
|
4479
|
+
|
|
4480
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", { action: "list" });
|
|
4481
|
+
expect(parsed.action).toBe("list");
|
|
4482
|
+
const jtis = parsed.tokens.map((t: any) => t.jti);
|
|
4483
|
+
expect(jtis).toContain(m1.parsed.jti);
|
|
4484
|
+
expect(jtis).toContain(m2.parsed.jti);
|
|
4485
|
+
expect(parsed.tokens.length).toBe(2);
|
|
4486
|
+
closeAllStores();
|
|
4487
|
+
});
|
|
4488
|
+
|
|
4489
|
+
test("audit-log integration: minted row carries created_via='mcp_mint' and parent_jti", async () => {
|
|
4490
|
+
const { vaultName, auth } = await setupAdminSession("audit");
|
|
4491
|
+
const { closeAllStores, getVaultStore } = await import("./vault-store.ts");
|
|
4492
|
+
|
|
4493
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
4494
|
+
action: "mint",
|
|
4495
|
+
scope: "vault:read",
|
|
4496
|
+
});
|
|
4497
|
+
const jti = parsed.jti;
|
|
4498
|
+
const store = getVaultStore(vaultName);
|
|
4499
|
+
const hashPrefix = jti.slice(2);
|
|
4500
|
+
const row = store.db.prepare(`
|
|
4501
|
+
SELECT created_via, parent_jti, vault_name FROM tokens
|
|
4502
|
+
WHERE token_hash LIKE ?
|
|
4503
|
+
`).get(`sha256:${hashPrefix}%`) as { created_via: string | null; parent_jti: string | null; vault_name: string | null };
|
|
4504
|
+
expect(row.created_via).toBe("mcp_mint");
|
|
4505
|
+
expect(row.parent_jti).toBe(auth.caller_jti);
|
|
4506
|
+
expect(row.vault_name).toBe(vaultName);
|
|
4507
|
+
closeAllStores();
|
|
4508
|
+
});
|
|
4509
|
+
});
|
|
4510
|
+
|
|
4132
4511
|
describe("extractApiKey", () => {
|
|
4133
4512
|
test("extracts from Authorization: Bearer header", () => {
|
|
4134
4513
|
const req = new Request("http://localhost/api/notes", {
|