@vellumai/credential-executor 0.10.7-dev.202607092332.9deedf4 → 0.10.7-dev.202607100313.fffdfea

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 (56) hide show
  1. package/node_modules/@vellumai/service-contracts/package.json +0 -2
  2. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  3. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  4. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  5. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  6. package/package.json +1 -1
  7. package/src/__tests__/managed-integration.test.ts +108 -87
  8. package/src/index.ts +5 -85
  9. package/src/main.ts +79 -549
  10. package/src/paths.ts +0 -15
  11. package/src/server.ts +52 -469
  12. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  13. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  14. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  15. package/src/__tests__/command-executor.test.ts +0 -1879
  16. package/src/__tests__/command-validator.test.ts +0 -1405
  17. package/src/__tests__/command-workspace.test.ts +0 -1050
  18. package/src/__tests__/grant-store.test.ts +0 -689
  19. package/src/__tests__/http-executor.test.ts +0 -1336
  20. package/src/__tests__/http-policy.test.ts +0 -1069
  21. package/src/__tests__/local-materializers.test.ts +0 -860
  22. package/src/__tests__/local-token-refresh.test.ts +0 -361
  23. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  24. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  25. package/src/__tests__/managed-materializers.test.ts +0 -1028
  26. package/src/__tests__/managed-rejection.test.ts +0 -43
  27. package/src/__tests__/toolstore.test.ts +0 -773
  28. package/src/audit/store.ts +0 -188
  29. package/src/commands/auth-adapters.ts +0 -169
  30. package/src/commands/egress-hooks.ts +0 -203
  31. package/src/commands/executor.ts +0 -1155
  32. package/src/commands/output-scan.ts +0 -157
  33. package/src/commands/profiles.ts +0 -286
  34. package/src/commands/validator.ts +0 -702
  35. package/src/commands/workspace.ts +0 -550
  36. package/src/grants/index.ts +0 -17
  37. package/src/grants/persistent-store.ts +0 -309
  38. package/src/grants/rpc-handlers.ts +0 -293
  39. package/src/grants/temporary-store.ts +0 -289
  40. package/src/http/audit.ts +0 -84
  41. package/src/http/executor.ts +0 -684
  42. package/src/http/path-template.ts +0 -245
  43. package/src/http/policy.ts +0 -238
  44. package/src/http/response-filter.ts +0 -233
  45. package/src/managed-errors.ts +0 -9
  46. package/src/managed-lazy-getters.ts +0 -106
  47. package/src/materializers/local-oauth-lookup.ts +0 -98
  48. package/src/materializers/local-token-refresh.ts +0 -287
  49. package/src/materializers/local.ts +0 -316
  50. package/src/materializers/managed-platform.ts +0 -295
  51. package/src/subjects/local.ts +0 -177
  52. package/src/subjects/managed.ts +0 -311
  53. package/src/subjects/policy.ts +0 -79
  54. package/src/toolstore/integrity.ts +0 -94
  55. package/src/toolstore/manifest.ts +0 -154
  56. 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
- });