@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +1 -2
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  5. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
  6. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  8. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  9. package/package.json +2 -3
  10. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  11. package/src/__tests__/local-standalone.test.ts +5 -36
  12. package/src/__tests__/managed-integration.test.ts +112 -91
  13. package/src/__tests__/managed-reconnect.test.ts +2 -2
  14. package/src/__tests__/transport.test.ts +23 -27
  15. package/src/cli.ts +1 -1
  16. package/src/index.ts +8 -88
  17. package/src/main.ts +228 -340
  18. package/src/paths.ts +4 -20
  19. package/src/server.ts +52 -469
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  21. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  22. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  23. package/src/__tests__/command-executor.test.ts +0 -1879
  24. package/src/__tests__/command-validator.test.ts +0 -1405
  25. package/src/__tests__/command-workspace.test.ts +0 -1050
  26. package/src/__tests__/grant-store.test.ts +0 -689
  27. package/src/__tests__/http-executor.test.ts +0 -1336
  28. package/src/__tests__/http-policy.test.ts +0 -1069
  29. package/src/__tests__/local-materializers.test.ts +0 -860
  30. package/src/__tests__/local-token-refresh.test.ts +0 -361
  31. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  32. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  33. package/src/__tests__/managed-materializers.test.ts +0 -1028
  34. package/src/__tests__/managed-rejection.test.ts +0 -43
  35. package/src/__tests__/toolstore.test.ts +0 -773
  36. package/src/audit/store.ts +0 -188
  37. package/src/commands/auth-adapters.ts +0 -169
  38. package/src/commands/egress-hooks.ts +0 -203
  39. package/src/commands/executor.ts +0 -1155
  40. package/src/commands/output-scan.ts +0 -157
  41. package/src/commands/profiles.ts +0 -286
  42. package/src/commands/validator.ts +0 -702
  43. package/src/commands/workspace.ts +0 -550
  44. package/src/grants/index.ts +0 -17
  45. package/src/grants/persistent-store.ts +0 -309
  46. package/src/grants/rpc-handlers.ts +0 -293
  47. package/src/grants/temporary-store.ts +0 -289
  48. package/src/http/audit.ts +0 -84
  49. package/src/http/executor.ts +0 -684
  50. package/src/http/path-template.ts +0 -245
  51. package/src/http/policy.ts +0 -238
  52. package/src/http/response-filter.ts +0 -233
  53. package/src/managed-errors.ts +0 -9
  54. package/src/managed-lazy-getters.ts +0 -106
  55. package/src/managed-main.ts +0 -822
  56. package/src/materializers/local-oauth-lookup.ts +0 -98
  57. package/src/materializers/local-token-refresh.ts +0 -287
  58. package/src/materializers/local.ts +0 -316
  59. package/src/materializers/managed-platform.ts +0 -295
  60. package/src/subjects/local.ts +0 -177
  61. package/src/subjects/managed.ts +0 -311
  62. package/src/subjects/policy.ts +0 -79
  63. package/src/toolstore/integrity.ts +0 -94
  64. package/src/toolstore/manifest.ts +0 -154
  65. package/src/toolstore/publish.ts +0 -571
@@ -1,689 +0,0 @@
1
- /**
2
- * Tests for CES grant stores.
3
- *
4
- * Covers:
5
- * - Persistent store: initialization, duplicate prevention by canonical grant
6
- * hash, fail-closed behavior on corrupt/unreadable files.
7
- * - Temporary store: allow_once consumption, allow_10m expiry, allow_conversation
8
- * scoping by conversation ID, clearConversation cleanup.
9
- */
10
-
11
- import { describe, expect, test, beforeEach, afterEach } from "bun:test";
12
- import {
13
- existsSync,
14
- mkdirSync,
15
- readFileSync,
16
- rmSync,
17
- writeFileSync,
18
- } from "node:fs";
19
- import { join } from "node:path";
20
- import { tmpdir } from "node:os";
21
- import { randomUUID } from "node:crypto";
22
-
23
- import { PersistentGrantStore, type PersistentGrant } from "../grants/persistent-store.js";
24
- import { TemporaryGrantStore } from "../grants/temporary-store.js";
25
-
26
- // ---------------------------------------------------------------------------
27
- // Test helpers
28
- // ---------------------------------------------------------------------------
29
-
30
- function makeTmpDir(): string {
31
- const dir = join(tmpdir(), `ces-grant-test-${randomUUID()}`);
32
- mkdirSync(dir, { recursive: true });
33
- return dir;
34
- }
35
-
36
- function makeGrant(overrides?: Partial<PersistentGrant>): PersistentGrant {
37
- return {
38
- id: overrides?.id ?? randomUUID(),
39
- tool: overrides?.tool ?? "host_bash",
40
- pattern: overrides?.pattern ?? "npm install",
41
- scope: overrides?.scope ?? "/project",
42
- createdAt: overrides?.createdAt ?? Date.now(),
43
- sessionId: overrides?.sessionId ?? "test-session",
44
- revokedAt: overrides?.revokedAt,
45
- revokedReason: overrides?.revokedReason,
46
- };
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // Persistent store
51
- // ---------------------------------------------------------------------------
52
-
53
- describe("PersistentGrantStore", () => {
54
- let tmpDir: string;
55
-
56
- beforeEach(() => {
57
- tmpDir = makeTmpDir();
58
- });
59
-
60
- afterEach(() => {
61
- rmSync(tmpDir, { recursive: true, force: true });
62
- });
63
-
64
- describe("initialization", () => {
65
- test("creates grants.json when file does not exist", () => {
66
- const store = new PersistentGrantStore(tmpDir);
67
- store.init();
68
-
69
- const filePath = join(tmpDir, "grants.json");
70
- expect(existsSync(filePath)).toBe(true);
71
-
72
- const data = JSON.parse(readFileSync(filePath, "utf-8"));
73
- expect(data.version).toBe(1);
74
- expect(data.grants).toEqual([]);
75
- });
76
-
77
- test("creates parent directory if missing", () => {
78
- const nestedDir = join(tmpDir, "nested", "grants");
79
- const store = new PersistentGrantStore(nestedDir);
80
- store.init();
81
-
82
- expect(existsSync(join(nestedDir, "grants.json"))).toBe(true);
83
- });
84
-
85
- test("loads existing valid grants file", () => {
86
- const grant = makeGrant();
87
- writeFileSync(
88
- join(tmpDir, "grants.json"),
89
- JSON.stringify({ version: 1, grants: [grant] }, null, 2),
90
- );
91
-
92
- const store = new PersistentGrantStore(tmpDir);
93
- store.init();
94
-
95
- const grants = store.getAll();
96
- expect(grants).toHaveLength(1);
97
- expect(grants[0].id).toBe(grant.id);
98
- });
99
- });
100
-
101
- describe("add and deduplication", () => {
102
- test("adds a new grant and persists it", () => {
103
- const store = new PersistentGrantStore(tmpDir);
104
- store.init();
105
-
106
- const grant = makeGrant();
107
- const added = store.add(grant);
108
- expect(added).toBe(true);
109
-
110
- // Verify persisted by creating a new store instance
111
- const store2 = new PersistentGrantStore(tmpDir);
112
- const grants = store2.getAll();
113
- expect(grants).toHaveLength(1);
114
- expect(grants[0].id).toBe(grant.id);
115
- });
116
-
117
- test("deduplicates by canonical grant id", () => {
118
- const store = new PersistentGrantStore(tmpDir);
119
- store.init();
120
-
121
- const grant = makeGrant({ id: "canonical-hash-123" });
122
- expect(store.add(grant)).toBe(true);
123
- expect(store.add(grant)).toBe(false); // duplicate
124
-
125
- expect(store.getAll()).toHaveLength(1);
126
- });
127
-
128
- test("allows grants with different IDs", () => {
129
- const store = new PersistentGrantStore(tmpDir);
130
- store.init();
131
-
132
- store.add(makeGrant({ id: "grant-a" }));
133
- store.add(makeGrant({ id: "grant-b" }));
134
-
135
- expect(store.getAll()).toHaveLength(2);
136
- });
137
- });
138
-
139
- describe("reactivation of revoked grants", () => {
140
- test("adding a grant with same ID as revoked returns true and reactivates", () => {
141
- const store = new PersistentGrantStore(tmpDir);
142
- store.init();
143
-
144
- const grant = makeGrant({ id: "revoke-reactivate" });
145
- store.add(grant);
146
- store.markRevoked("revoke-reactivate", "test revocation");
147
-
148
- // Revoked grant should not appear in getAll
149
- expect(store.getAll()).toHaveLength(0);
150
-
151
- // Re-add with same ID — should reactivate
152
- const reactivated = store.add(
153
- makeGrant({ id: "revoke-reactivate", pattern: "bun install" }),
154
- );
155
- expect(reactivated).toBe(true);
156
- });
157
-
158
- test("reactivated grant appears in getAll()", () => {
159
- const store = new PersistentGrantStore(tmpDir);
160
- store.init();
161
-
162
- store.add(makeGrant({ id: "revoke-visible" }));
163
- store.markRevoked("revoke-visible");
164
- store.add(makeGrant({ id: "revoke-visible" }));
165
-
166
- const all = store.getAll();
167
- expect(all).toHaveLength(1);
168
- expect(all[0].id).toBe("revoke-visible");
169
- expect(all[0].revokedAt).toBeUndefined();
170
- expect(all[0].revokedReason).toBeUndefined();
171
- });
172
-
173
- test("reactivated grant fields are updated from the new grant", () => {
174
- const store = new PersistentGrantStore(tmpDir);
175
- store.init();
176
-
177
- store.add(
178
- makeGrant({
179
- id: "revoke-update",
180
- tool: "host_bash",
181
- pattern: "npm install",
182
- scope: "/old-project",
183
- sessionId: "session-1",
184
- }),
185
- );
186
- store.markRevoked("revoke-update", "no longer needed");
187
-
188
- const newGrant = makeGrant({
189
- id: "revoke-update",
190
- tool: "command",
191
- pattern: "bun install",
192
- scope: "/new-project",
193
- sessionId: "session-2",
194
- });
195
- store.add(newGrant);
196
-
197
- const result = store.getById("revoke-update");
198
- expect(result).toBeDefined();
199
- expect(result!.tool).toBe("command");
200
- expect(result!.pattern).toBe("bun install");
201
- expect(result!.scope).toBe("/new-project");
202
- expect(result!.sessionId).toBe("session-2");
203
- expect(result!.revokedAt).toBeUndefined();
204
- expect(result!.revokedReason).toBeUndefined();
205
- });
206
-
207
- test("reactivated grant round-trips through serialization", () => {
208
- const store = new PersistentGrantStore(tmpDir);
209
- store.init();
210
-
211
- store.add(makeGrant({ id: "revoke-persist" }));
212
- store.markRevoked("revoke-persist", "reason");
213
- store.add(makeGrant({ id: "revoke-persist", pattern: "bun test" }));
214
-
215
- // Load from a fresh store instance to verify serialization
216
- const store2 = new PersistentGrantStore(tmpDir);
217
- const grants = store2.getAll();
218
- expect(grants).toHaveLength(1);
219
- expect(grants[0].id).toBe("revoke-persist");
220
- expect(grants[0].pattern).toBe("bun test");
221
- expect(grants[0].revokedAt).toBeUndefined();
222
- expect(grants[0].revokedReason).toBeUndefined();
223
- });
224
- });
225
-
226
- describe("legacy sessionId migration", () => {
227
- test("backfills sessionId on legacy grants during init", () => {
228
- // Write a grants file with a grant missing sessionId (legacy format)
229
- const legacyGrant = {
230
- id: "legacy-grant",
231
- tool: "host_bash",
232
- pattern: "npm install",
233
- scope: "/project",
234
- createdAt: Date.now(),
235
- };
236
- writeFileSync(
237
- join(tmpDir, "grants.json"),
238
- JSON.stringify({ version: 1, grants: [legacyGrant] }, null, 2),
239
- );
240
-
241
- const store = new PersistentGrantStore(tmpDir);
242
- store.init();
243
-
244
- const grants = store.getAll();
245
- expect(grants).toHaveLength(1);
246
- expect(grants[0].sessionId).toBe("unknown");
247
-
248
- // Verify it was persisted
249
- const raw = JSON.parse(readFileSync(join(tmpDir, "grants.json"), "utf-8"));
250
- expect(raw.grants[0].sessionId).toBe("unknown");
251
- });
252
- });
253
-
254
- describe("getById and has", () => {
255
- test("returns grant by ID", () => {
256
- const store = new PersistentGrantStore(tmpDir);
257
- store.init();
258
-
259
- const grant = makeGrant({ id: "lookup-id" });
260
- store.add(grant);
261
-
262
- expect(store.getById("lookup-id")).toBeDefined();
263
- expect(store.getById("lookup-id")!.tool).toBe(grant.tool);
264
- });
265
-
266
- test("returns undefined for missing ID", () => {
267
- const store = new PersistentGrantStore(tmpDir);
268
- store.init();
269
-
270
- expect(store.getById("nonexistent")).toBeUndefined();
271
- });
272
-
273
- test("has() returns true for existing grant", () => {
274
- const store = new PersistentGrantStore(tmpDir);
275
- store.init();
276
-
277
- store.add(makeGrant({ id: "exists" }));
278
- expect(store.has("exists")).toBe(true);
279
- expect(store.has("missing")).toBe(false);
280
- });
281
- });
282
-
283
- describe("remove", () => {
284
- test("removes an existing grant", () => {
285
- const store = new PersistentGrantStore(tmpDir);
286
- store.init();
287
-
288
- store.add(makeGrant({ id: "to-remove" }));
289
- expect(store.remove("to-remove")).toBe(true);
290
- expect(store.has("to-remove")).toBe(false);
291
- });
292
-
293
- test("returns false for non-existent grant", () => {
294
- const store = new PersistentGrantStore(tmpDir);
295
- store.init();
296
-
297
- expect(store.remove("nonexistent")).toBe(false);
298
- });
299
- });
300
-
301
- describe("clear", () => {
302
- test("removes all grants", () => {
303
- const store = new PersistentGrantStore(tmpDir);
304
- store.init();
305
-
306
- store.add(makeGrant({ id: "a" }));
307
- store.add(makeGrant({ id: "b" }));
308
- store.clear();
309
-
310
- expect(store.getAll()).toHaveLength(0);
311
- });
312
- });
313
-
314
- describe("fail-closed behavior", () => {
315
- test("throws on malformed JSON", () => {
316
- writeFileSync(join(tmpDir, "grants.json"), "not json at all");
317
-
318
- const store = new PersistentGrantStore(tmpDir);
319
- expect(() => store.init()).toThrow();
320
- });
321
-
322
- test("throws on missing version field", () => {
323
- writeFileSync(
324
- join(tmpDir, "grants.json"),
325
- JSON.stringify({ grants: [] }),
326
- );
327
-
328
- const store = new PersistentGrantStore(tmpDir);
329
- expect(() => store.init()).toThrow(/malformed/i);
330
- });
331
-
332
- test("throws on missing grants field", () => {
333
- writeFileSync(
334
- join(tmpDir, "grants.json"),
335
- JSON.stringify({ version: 1 }),
336
- );
337
-
338
- const store = new PersistentGrantStore(tmpDir);
339
- expect(() => store.init()).toThrow(/malformed/i);
340
- });
341
-
342
- test("throws on unsupported version", () => {
343
- writeFileSync(
344
- join(tmpDir, "grants.json"),
345
- JSON.stringify({ version: 999, grants: [] }),
346
- );
347
-
348
- const store = new PersistentGrantStore(tmpDir);
349
- expect(() => store.init()).toThrow(/unsupported version/i);
350
- });
351
-
352
- test("blocks all operations after corruption is detected", () => {
353
- writeFileSync(join(tmpDir, "grants.json"), "corrupt");
354
-
355
- const store = new PersistentGrantStore(tmpDir);
356
- expect(() => store.init()).toThrow();
357
-
358
- // Subsequent operations should also fail
359
- expect(() => store.getAll()).toThrow(/corrupt/i);
360
- expect(() => store.add(makeGrant())).toThrow(/corrupt/i);
361
- expect(() => store.remove("any")).toThrow(/corrupt/i);
362
- expect(() => store.clear()).toThrow(/corrupt/i);
363
- });
364
-
365
- test("throws on grants field being non-array", () => {
366
- writeFileSync(
367
- join(tmpDir, "grants.json"),
368
- JSON.stringify({ version: 1, grants: "not-an-array" }),
369
- );
370
-
371
- const store = new PersistentGrantStore(tmpDir);
372
- expect(() => store.init()).toThrow(/not an array/i);
373
- });
374
- });
375
-
376
- describe("atomic writes", () => {
377
- test("grants.json is valid after write", () => {
378
- const store = new PersistentGrantStore(tmpDir);
379
- store.init();
380
-
381
- store.add(makeGrant({ id: "atom-1" }));
382
- store.add(makeGrant({ id: "atom-2" }));
383
-
384
- const raw = readFileSync(join(tmpDir, "grants.json"), "utf-8");
385
- const data = JSON.parse(raw);
386
- expect(data.version).toBe(1);
387
- expect(data.grants).toHaveLength(2);
388
- });
389
- });
390
- });
391
-
392
- // ---------------------------------------------------------------------------
393
- // Temporary store
394
- // ---------------------------------------------------------------------------
395
-
396
- describe("TemporaryGrantStore", () => {
397
- let store: TemporaryGrantStore;
398
-
399
- beforeEach(() => {
400
- store = new TemporaryGrantStore();
401
- });
402
-
403
- describe("allow_once", () => {
404
- test("grant is consumed on first check", () => {
405
- store.add("allow_once", "hash-abc");
406
-
407
- expect(store.check("allow_once", "hash-abc")).toBe(true);
408
- // Second check should fail — consumed
409
- expect(store.check("allow_once", "hash-abc")).toBe(false);
410
- });
411
-
412
- test("different proposal hashes are independent", () => {
413
- store.add("allow_once", "hash-1");
414
- store.add("allow_once", "hash-2");
415
-
416
- expect(store.check("allow_once", "hash-1")).toBe(true);
417
- expect(store.check("allow_once", "hash-2")).toBe(true);
418
- // Both consumed
419
- expect(store.check("allow_once", "hash-1")).toBe(false);
420
- expect(store.check("allow_once", "hash-2")).toBe(false);
421
- });
422
-
423
- test("remains usable immediately (default TTL is not instantaneous)", () => {
424
- // The default `allow_once` TTL must still allow a prompt retry of the
425
- // just-approved operation.
426
- store.add("allow_once", "hash-default-ttl");
427
- expect(store.check("allow_once", "hash-default-ttl")).toBe(true);
428
- });
429
-
430
- test("expires after its TTL even if never consumed (ATL-935)", () => {
431
- // An unconsumed single-use approval must not live forever.
432
- store.add("allow_once", "hash-once-expire", { durationMs: 1 });
433
-
434
- const start = Date.now();
435
- while (Date.now() - start < 5) {
436
- // spin
437
- }
438
-
439
- expect(store.check("allow_once", "hash-once-expire")).toBe(false);
440
- });
441
- });
442
-
443
- // -------------------------------------------------------------------------
444
- // ATL-935: ephemeral approvals are bounded by TTLs, not by connection
445
- // teardown.
446
- //
447
- // In managed mode the store instance — contents included — is deliberately
448
- // shared across assistant reconnects and, in the multi-process daemon model,
449
- // across the connections that each talk to CES, so one guardian approval can
450
- // be used by any connection entitled to it. The security boundary is the
451
- // per-grant TTL: an approval that is recorded but never consumed must expire
452
- // on its own rather than survive indefinitely and be replayed by a much later
453
- // connection. These tests encode both halves of that contract — an approval
454
- // stays usable across a reconnect (sharing preserved) but does not outlive
455
- // its TTL (replay window bounded).
456
- // -------------------------------------------------------------------------
457
- describe("ATL-935: ephemeral approvals are bounded by TTLs, not teardown", () => {
458
- test("every grant kind is bounded by a TTL (no unbounded approvals)", () => {
459
- // The finding was that allow_once and allow_conversation had no time
460
- // bound at all. Every kind must now expire on its own.
461
- store.add("allow_once", "exp-once", { durationMs: 1 });
462
- store.add("allow_10m", "exp-10m", { durationMs: 1 });
463
- store.add("allow_conversation", "exp-conv", {
464
- conversationId: "c",
465
- durationMs: 1,
466
- });
467
-
468
- const start = Date.now();
469
- while (Date.now() - start < 5) {
470
- // spin until all three TTLs lapse
471
- }
472
-
473
- expect(store.check("allow_once", "exp-once")).toBe(false);
474
- expect(store.check("allow_10m", "exp-10m")).toBe(false);
475
- expect(store.check("allow_conversation", "exp-conv", "c")).toBe(false);
476
- });
477
-
478
- test("allow_once that is never consumed expires instead of lingering", () => {
479
- // The headline scenario: an approval recorded but not consumed before a
480
- // connection drops must not be replayable by a later connection.
481
- store.add("allow_once", "hash-stale", { durationMs: 1 });
482
-
483
- const start = Date.now();
484
- while (Date.now() - start < 5) {
485
- // spin
486
- }
487
-
488
- expect(store.checkAny("hash-stale")).toBeUndefined();
489
- });
490
-
491
- test("allow_conversation is bounded by an absolute TTL backstop", () => {
492
- store.add("allow_conversation", "hash-conv-ttl", {
493
- conversationId: "conv-1",
494
- durationMs: 1,
495
- });
496
-
497
- const start = Date.now();
498
- while (Date.now() - start < 5) {
499
- // spin
500
- }
501
-
502
- expect(store.checkAny("hash-conv-ttl", "conv-1")).toBeUndefined();
503
- });
504
-
505
- test("an unexpired approval is still shared across a reconnect (no teardown)", () => {
506
- // The multi-process daemon model depends on this: the store is NOT
507
- // cleared on disconnect, so an approval granted while one connection was
508
- // live remains usable by a later or sibling connection within its TTL.
509
- store.add("allow_conversation", "hash-shared", {
510
- conversationId: "conv-1",
511
- });
512
-
513
- // Simulate a reconnect — no clear() happens between connections.
514
- expect(store.checkAny("hash-shared", "conv-1")).toBe(
515
- "allow_conversation",
516
- );
517
- // Still usable again (conversation grants are not consumed on use).
518
- expect(store.checkAny("hash-shared", "conv-1")).toBe(
519
- "allow_conversation",
520
- );
521
-
522
- // An allow_10m approval likewise survives a reconnect within its window.
523
- store.add("allow_10m", "hash-shared-10m");
524
- expect(store.checkAny("hash-shared-10m")).toBe("allow_10m");
525
- });
526
- });
527
-
528
- describe("allow_10m", () => {
529
- test("grant is active before expiry", () => {
530
- store.add("allow_10m", "hash-timed");
531
-
532
- expect(store.check("allow_10m", "hash-timed")).toBe(true);
533
- // Can be checked multiple times (not consumed)
534
- expect(store.check("allow_10m", "hash-timed")).toBe(true);
535
- });
536
-
537
- test("grant expires after TTL", () => {
538
- // Add with a very short duration (1ms)
539
- store.add("allow_10m", "hash-expire", { durationMs: 1 });
540
-
541
- // Wait for expiry — use a synchronous busy-wait to avoid flakes
542
- const start = Date.now();
543
- while (Date.now() - start < 5) {
544
- // spin
545
- }
546
-
547
- expect(store.check("allow_10m", "hash-expire")).toBe(false);
548
- });
549
-
550
- test("expired grant is lazily purged", () => {
551
- store.add("allow_10m", "hash-lazy", { durationMs: 1 });
552
-
553
- const start = Date.now();
554
- while (Date.now() - start < 5) {
555
- // spin
556
- }
557
-
558
- // Check triggers lazy purge
559
- store.check("allow_10m", "hash-lazy");
560
- expect(store.size).toBe(0);
561
- });
562
- });
563
-
564
- describe("allow_conversation", () => {
565
- test("requires conversationId", () => {
566
- expect(() => store.add("allow_conversation", "hash-t", {})).toThrow(
567
- /conversationId/,
568
- );
569
- });
570
-
571
- test("scoped to conversation ID", () => {
572
- store.add("allow_conversation", "hash-t", { conversationId: "conv-1" });
573
-
574
- expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
575
- // Different conversation should not match
576
- expect(store.check("allow_conversation", "hash-t", "conv-2")).toBe(false);
577
- });
578
-
579
- test("same proposal hash in different conversations are independent", () => {
580
- store.add("allow_conversation", "hash-t", { conversationId: "conv-a" });
581
- store.add("allow_conversation", "hash-t", { conversationId: "conv-b" });
582
-
583
- expect(store.check("allow_conversation", "hash-t", "conv-a")).toBe(true);
584
- expect(store.check("allow_conversation", "hash-t", "conv-b")).toBe(true);
585
- });
586
-
587
- test("not consumed on check (persists within conversation)", () => {
588
- store.add("allow_conversation", "hash-t", { conversationId: "conv-1" });
589
-
590
- expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
591
- expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
592
- expect(store.check("allow_conversation", "hash-t", "conv-1")).toBe(true);
593
- });
594
- });
595
-
596
- describe("checkAny", () => {
597
- test("finds allow_once grant", () => {
598
- store.add("allow_once", "hash-any");
599
- expect(store.checkAny("hash-any")).toBe("allow_once");
600
- // Consumed — should not match again
601
- expect(store.checkAny("hash-any")).toBeUndefined();
602
- });
603
-
604
- test("finds allow_10m grant", () => {
605
- store.add("allow_10m", "hash-any-t");
606
- expect(store.checkAny("hash-any-t")).toBe("allow_10m");
607
- });
608
-
609
- test("finds allow_conversation grant with conversationId", () => {
610
- store.add("allow_conversation", "hash-any-th", { conversationId: "conv-x" });
611
- expect(store.checkAny("hash-any-th", "conv-x")).toBe("allow_conversation");
612
- });
613
-
614
- test("returns undefined when no grants match", () => {
615
- expect(store.checkAny("no-such-hash")).toBeUndefined();
616
- });
617
-
618
- test("prefers allow_once over allow_10m", () => {
619
- store.add("allow_once", "hash-prio");
620
- store.add("allow_10m", "hash-prio");
621
-
622
- expect(store.checkAny("hash-prio")).toBe("allow_once");
623
- });
624
- });
625
-
626
- describe("remove", () => {
627
- test("removes a specific grant", () => {
628
- store.add("allow_10m", "hash-rm");
629
- expect(store.remove("allow_10m", "hash-rm")).toBe(true);
630
- expect(store.check("allow_10m", "hash-rm")).toBe(false);
631
- });
632
-
633
- test("returns false for non-existent grant", () => {
634
- expect(store.remove("allow_once", "nope")).toBe(false);
635
- });
636
- });
637
-
638
- describe("clearConversation", () => {
639
- test("removes all conversation grants for a conversation", () => {
640
- store.add("allow_conversation", "hash-1", { conversationId: "conv-clear" });
641
- store.add("allow_conversation", "hash-2", { conversationId: "conv-clear" });
642
- store.add("allow_conversation", "hash-3", { conversationId: "conv-keep" });
643
-
644
- store.clearConversation("conv-clear");
645
-
646
- expect(store.check("allow_conversation", "hash-1", "conv-clear")).toBe(false);
647
- expect(store.check("allow_conversation", "hash-2", "conv-clear")).toBe(false);
648
- // Other conversation unaffected
649
- expect(store.check("allow_conversation", "hash-3", "conv-keep")).toBe(true);
650
- });
651
-
652
- test("does not affect non-conversation grants", () => {
653
- store.add("allow_once", "hash-non-conv");
654
- store.add("allow_10m", "hash-non-conv-t");
655
-
656
- store.clearConversation("any-conv");
657
-
658
- // Non-conversation grants unaffected
659
- expect(store.check("allow_once", "hash-non-conv")).toBe(true);
660
- expect(store.check("allow_10m", "hash-non-conv-t")).toBe(true);
661
- });
662
- });
663
-
664
- describe("clear", () => {
665
- test("removes all grants", () => {
666
- store.add("allow_once", "h1");
667
- store.add("allow_10m", "h2");
668
- store.add("allow_conversation", "h3", { conversationId: "c1" });
669
-
670
- store.clear();
671
- expect(store.size).toBe(0);
672
- });
673
- });
674
-
675
- describe("process restart simulation", () => {
676
- test("grants do not survive new store instance", () => {
677
- store.add("allow_once", "hash-restart");
678
- store.add("allow_10m", "hash-restart-t");
679
- store.add("allow_conversation", "hash-restart-th", {
680
- conversationId: "conv-r",
681
- });
682
-
683
- // "Restart" — new instance
684
- const newStore = new TemporaryGrantStore();
685
- expect(newStore.size).toBe(0);
686
- expect(newStore.checkAny("hash-restart")).toBeUndefined();
687
- });
688
- });
689
- });