@mikeargento/bitgraph 1.1.0

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 (80) hide show
  1. package/LICENSE +9 -0
  2. package/NOTICE +14 -0
  3. package/README.md +175 -0
  4. package/dist/__tests__/canonical.test.d.ts +2 -0
  5. package/dist/__tests__/canonical.test.d.ts.map +1 -0
  6. package/dist/__tests__/canonical.test.js +162 -0
  7. package/dist/__tests__/canonical.test.js.map +1 -0
  8. package/dist/__tests__/commit-service.integration.test.d.ts +2 -0
  9. package/dist/__tests__/commit-service.integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/commit-service.integration.test.js +95 -0
  11. package/dist/__tests__/commit-service.integration.test.js.map +1 -0
  12. package/dist/__tests__/constructor.test.d.ts +2 -0
  13. package/dist/__tests__/constructor.test.d.ts.map +1 -0
  14. package/dist/__tests__/constructor.test.js +267 -0
  15. package/dist/__tests__/constructor.test.js.map +1 -0
  16. package/dist/__tests__/hello-world.test.d.ts +2 -0
  17. package/dist/__tests__/hello-world.test.d.ts.map +1 -0
  18. package/dist/__tests__/hello-world.test.js +9 -0
  19. package/dist/__tests__/hello-world.test.js.map +1 -0
  20. package/dist/__tests__/policy-enforcement.test.d.ts +2 -0
  21. package/dist/__tests__/policy-enforcement.test.d.ts.map +1 -0
  22. package/dist/__tests__/policy-enforcement.test.js +188 -0
  23. package/dist/__tests__/policy-enforcement.test.js.map +1 -0
  24. package/dist/__tests__/proof-hash-regression.test.d.ts +2 -0
  25. package/dist/__tests__/proof-hash-regression.test.d.ts.map +1 -0
  26. package/dist/__tests__/proof-hash-regression.test.js +134 -0
  27. package/dist/__tests__/proof-hash-regression.test.js.map +1 -0
  28. package/dist/__tests__/proof-hash.test.d.ts +2 -0
  29. package/dist/__tests__/proof-hash.test.d.ts.map +1 -0
  30. package/dist/__tests__/proof-hash.test.js +134 -0
  31. package/dist/__tests__/proof-hash.test.js.map +1 -0
  32. package/dist/__tests__/verifier.test.d.ts +2 -0
  33. package/dist/__tests__/verifier.test.d.ts.map +1 -0
  34. package/dist/__tests__/verifier.test.js +457 -0
  35. package/dist/__tests__/verifier.test.js.map +1 -0
  36. package/dist/canonical.d.ts +54 -0
  37. package/dist/canonical.d.ts.map +1 -0
  38. package/dist/canonical.js +119 -0
  39. package/dist/canonical.js.map +1 -0
  40. package/dist/constructor.d.ts +66 -0
  41. package/dist/constructor.d.ts.map +1 -0
  42. package/dist/constructor.js +339 -0
  43. package/dist/constructor.js.map +1 -0
  44. package/dist/host.d.ts +138 -0
  45. package/dist/host.d.ts.map +1 -0
  46. package/dist/host.js +3 -0
  47. package/dist/host.js.map +1 -0
  48. package/dist/index.d.ts +20 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +9 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/policy.d.ts +68 -0
  53. package/dist/policy.d.ts.map +1 -0
  54. package/dist/policy.js +176 -0
  55. package/dist/policy.js.map +1 -0
  56. package/dist/proof-hash.d.ts +9 -0
  57. package/dist/proof-hash.d.ts.map +1 -0
  58. package/dist/proof-hash.js +75 -0
  59. package/dist/proof-hash.js.map +1 -0
  60. package/dist/types.d.ts +705 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +3 -0
  63. package/dist/types.js.map +1 -0
  64. package/dist/verifier.d.ts +27 -0
  65. package/dist/verifier.d.ts.map +1 -0
  66. package/dist/verifier.js +844 -0
  67. package/dist/verifier.js.map +1 -0
  68. package/package.json +40 -0
  69. package/src/__tests__/canonical.test.ts +253 -0
  70. package/src/__tests__/commit-service.integration.test.ts +117 -0
  71. package/src/__tests__/constructor.test.ts +340 -0
  72. package/src/__tests__/hello-world.test.ts +10 -0
  73. package/src/__tests__/policy-enforcement.test.ts +226 -0
  74. package/src/__tests__/proof-hash-regression.test.ts +147 -0
  75. package/src/__tests__/proof-hash.test.ts +148 -0
  76. package/src/__tests__/verifier.test.ts +541 -0
  77. package/src/constructor.ts +435 -0
  78. package/src/host.ts +153 -0
  79. package/src/index.ts +51 -0
  80. package/src/policy.ts +239 -0
@@ -0,0 +1,541 @@
1
+ // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+
3
+ import { describe, test, before } from "node:test";
4
+ import * as assert from "node:assert/strict";
5
+ import { getPublicKeyAsync, signAsync } from "@noble/ed25519";
6
+ import { Constructor } from "../constructor.js";
7
+ import { verify, resetEpochLinkState } from "../index.js";
8
+ import type { HostCapabilities } from "../host.js";
9
+ import type { BitGraphProof } from "../index.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Test-fixture helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ interface Fixture {
16
+ proof: BitGraphProof;
17
+ bytes: Uint8Array;
18
+ publicKeyB64: string;
19
+ measurement: string;
20
+ privateKey: Uint8Array;
21
+ }
22
+
23
+ async function makeFixture(opts?: {
24
+ epochId?: string;
25
+ withCounter?: boolean;
26
+ withTime?: number;
27
+ measurement?: string;
28
+ }): Promise<Fixture> {
29
+ const privateKey = crypto.getRandomValues(new Uint8Array(32));
30
+ const publicKeyBytes = await getPublicKeyAsync(privateKey);
31
+ const publicKeyB64 = Buffer.from(publicKeyBytes).toString("base64");
32
+ const measurement = opts?.measurement ?? "test-measurement-deadbeef";
33
+
34
+ let counter = 0;
35
+ const host: HostCapabilities = opts?.withCounter
36
+ ? {
37
+ enforcementTier: "stub" as const,
38
+ getMeasurement: async () => measurement,
39
+ getFreshNonce: async () => crypto.getRandomValues(new Uint8Array(16)),
40
+ sign: async (data: Uint8Array) => signAsync(data, privateKey),
41
+ getPublicKey: async () => publicKeyBytes,
42
+ nextCounter: async () => String(++counter),
43
+ }
44
+ : opts?.withTime !== undefined
45
+ ? {
46
+ enforcementTier: "stub" as const,
47
+ getMeasurement: async () => measurement,
48
+ getFreshNonce: async () => crypto.getRandomValues(new Uint8Array(16)),
49
+ sign: async (data: Uint8Array) => signAsync(data, privateKey),
50
+ getPublicKey: async () => publicKeyBytes,
51
+ secureTime: async () => opts.withTime as number,
52
+ }
53
+ : {
54
+ enforcementTier: "stub" as const,
55
+ getMeasurement: async () => measurement,
56
+ getFreshNonce: async () => crypto.getRandomValues(new Uint8Array(16)),
57
+ sign: async (data: Uint8Array) => signAsync(data, privateKey),
58
+ getPublicKey: async () => publicKeyBytes,
59
+ };
60
+
61
+ const ctor = await Constructor.initialize(
62
+ opts?.epochId !== undefined
63
+ ? { host, epochId: opts.epochId }
64
+ : { host },
65
+ );
66
+
67
+ const bytes = new TextEncoder().encode("bitgraph-test-payload-42");
68
+ const proof = await ctor.commit({ bytes });
69
+
70
+ return { proof, bytes, publicKeyB64, measurement, privateKey };
71
+ }
72
+
73
+ /** Deep-clone a proof so mutations don't affect the original. */
74
+ function clone(proof: BitGraphProof): BitGraphProof {
75
+ return JSON.parse(JSON.stringify(proof)) as BitGraphProof;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Module-level fixture (no optional capabilities)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ let fx!: Fixture;
83
+
84
+ before(async () => {
85
+ fx = await makeFixture();
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Happy path
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe("verify — valid proof", () => {
93
+ test("returns { valid: true } for a freshly created proof", async () => {
94
+ const result = await verify({ proof: fx.proof, bytes: fx.bytes });
95
+ assert.deepEqual(result, { valid: true });
96
+ });
97
+
98
+ test("verify is idempotent (same result on second call)", async () => {
99
+ const r1 = await verify({ proof: fx.proof, bytes: fx.bytes });
100
+ const r2 = await verify({ proof: fx.proof, bytes: fx.bytes });
101
+ assert.deepEqual(r1, { valid: true });
102
+ assert.deepEqual(r2, { valid: true });
103
+ });
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Artifact check (step 2)
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe("verify — artifact mismatch", () => {
111
+ test("fails when wrong bytes are provided", async () => {
112
+ const result = await verify({
113
+ proof: fx.proof,
114
+ bytes: new Uint8Array([99, 98, 97]),
115
+ });
116
+ assert.equal(result.valid, false);
117
+ assert.match(result.reason ?? "", /digest mismatch/);
118
+ });
119
+
120
+ test("fails with empty bytes when proof covers non-empty content", async () => {
121
+ const result = await verify({
122
+ proof: fx.proof,
123
+ bytes: new Uint8Array(0),
124
+ });
125
+ assert.equal(result.valid, false);
126
+ assert.match(result.reason ?? "", /digest mismatch/);
127
+ });
128
+
129
+ test("succeeds when proof covers empty bytes and empty bytes are provided", async () => {
130
+ const pk = crypto.getRandomValues(new Uint8Array(32));
131
+ const pkBytes = await getPublicKeyAsync(pk);
132
+ const host: HostCapabilities = {
133
+ enforcementTier: "stub",
134
+ getMeasurement: async () => "meas",
135
+ getFreshNonce: async () => crypto.getRandomValues(new Uint8Array(16)),
136
+ sign: async (data: Uint8Array) => signAsync(data, pk),
137
+ getPublicKey: async () => pkBytes,
138
+ };
139
+ const ctor = await Constructor.initialize({ host });
140
+ const emptyBytes = new Uint8Array(0);
141
+ const proof = await ctor.commit({ bytes: emptyBytes });
142
+ const result = await verify({ proof, bytes: emptyBytes });
143
+ assert.deepEqual(result, { valid: true });
144
+ });
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Signature verification (step 4)
149
+ // ---------------------------------------------------------------------------
150
+
151
+ describe("verify — signature tampering", () => {
152
+ test("fails when signatureB64 is replaced with all-zeros", async () => {
153
+ const tampered = clone(fx.proof);
154
+ tampered.signer.signatureB64 = Buffer.alloc(64).toString("base64");
155
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
156
+ assert.equal(result.valid, false);
157
+ assert.match(result.reason ?? "", /signature/);
158
+ });
159
+
160
+ test("fails when publicKeyB64 is replaced with a different key", async () => {
161
+ const otherKey = crypto.getRandomValues(new Uint8Array(32));
162
+ const otherPublic = await getPublicKeyAsync(otherKey);
163
+ const tampered = clone(fx.proof);
164
+ tampered.signer.publicKeyB64 = Buffer.from(otherPublic).toString("base64");
165
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
166
+ assert.equal(result.valid, false);
167
+ assert.match(result.reason ?? "", /signature/);
168
+ });
169
+
170
+ test("fails when artifact.digestB64 is tampered", async () => {
171
+ const tampered = clone(fx.proof);
172
+ tampered.artifact.digestB64 = Buffer.alloc(32, 0xff).toString("base64");
173
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
174
+ assert.equal(result.valid, false);
175
+ // Either artifact mismatch OR signature failure (both are valid)
176
+ assert.ok(
177
+ (result.reason ?? "").includes("digest mismatch") ||
178
+ (result.reason ?? "").includes("signature"),
179
+ );
180
+ });
181
+
182
+ test("fails when measurement is tampered (signed field)", async () => {
183
+ const tampered = clone(fx.proof);
184
+ tampered.environment.measurement = "evil-measurement";
185
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
186
+ assert.equal(result.valid, false);
187
+ assert.match(result.reason ?? "", /signature/);
188
+ });
189
+
190
+ test("fails when nonce is tampered (signed field)", async () => {
191
+ const tampered = clone(fx.proof);
192
+ tampered.commit.nonceB64 = Buffer.alloc(16, 0xab).toString("base64");
193
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
194
+ assert.equal(result.valid, false);
195
+ assert.match(result.reason ?? "", /signature/);
196
+ });
197
+ });
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Structural validation (step 1)
201
+ // ---------------------------------------------------------------------------
202
+
203
+ describe("verify — structural validation", () => {
204
+ test("fails for non-object proof", async () => {
205
+ const result = await verify({
206
+ proof: "not-a-proof" as unknown as BitGraphProof,
207
+ bytes: fx.bytes,
208
+ });
209
+ assert.equal(result.valid, false);
210
+ assert.match(result.reason ?? "", /object/);
211
+ });
212
+
213
+ test("fails for null proof", async () => {
214
+ const result = await verify({
215
+ proof: null as unknown as BitGraphProof,
216
+ bytes: fx.bytes,
217
+ });
218
+ assert.equal(result.valid, false);
219
+ });
220
+
221
+ test("fails for wrong version", async () => {
222
+ const tampered = clone(fx.proof);
223
+ (tampered as unknown as Record<string, unknown>)["version"] = "bitgraph/2";
224
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
225
+ assert.equal(result.valid, false);
226
+ assert.match(result.reason ?? "", /version/);
227
+ });
228
+
229
+ test("fails when artifact is missing", async () => {
230
+ const tampered = clone(fx.proof);
231
+ delete (tampered as unknown as Record<string, unknown>)["artifact"];
232
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
233
+ assert.equal(result.valid, false);
234
+ assert.match(result.reason ?? "", /artifact/);
235
+ });
236
+
237
+ test("fails when commit is missing", async () => {
238
+ const tampered = clone(fx.proof);
239
+ delete (tampered as unknown as Record<string, unknown>)["commit"];
240
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
241
+ assert.equal(result.valid, false);
242
+ assert.match(result.reason ?? "", /commit/);
243
+ });
244
+
245
+ test("fails when signer is missing", async () => {
246
+ const tampered = clone(fx.proof);
247
+ delete (tampered as unknown as Record<string, unknown>)["signer"];
248
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
249
+ assert.equal(result.valid, false);
250
+ assert.match(result.reason ?? "", /signer/);
251
+ });
252
+
253
+ test("fails when environment is missing", async () => {
254
+ const tampered = clone(fx.proof);
255
+ delete (tampered as unknown as Record<string, unknown>)["environment"];
256
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
257
+ assert.equal(result.valid, false);
258
+ assert.match(result.reason ?? "", /environment/);
259
+ });
260
+
261
+ test("fails for invalid enforcement tier", async () => {
262
+ const tampered = clone(fx.proof);
263
+ tampered.environment.enforcement = "invalid-tier" as "stub";
264
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
265
+ assert.equal(result.valid, false);
266
+ assert.match(result.reason ?? "", /enforcement/);
267
+ });
268
+
269
+ test("fails when publicKeyB64 is wrong length (too short)", async () => {
270
+ const tampered = clone(fx.proof);
271
+ tampered.signer.publicKeyB64 = Buffer.alloc(16).toString("base64");
272
+ const result = await verify({ proof: tampered, bytes: fx.bytes });
273
+ assert.equal(result.valid, false);
274
+ assert.match(result.reason ?? "", /32/);
275
+ });
276
+ });
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Policy checks (step 5)
280
+ // ---------------------------------------------------------------------------
281
+
282
+ describe("verify — policy: requireEnforcement", () => {
283
+ test("passes when enforcement matches required tier", async () => {
284
+ const result = await verify({
285
+ proof: fx.proof,
286
+ bytes: fx.bytes,
287
+ trustAnchors: { requireEnforcement: "stub" },
288
+ });
289
+ assert.deepEqual(result, { valid: true });
290
+ });
291
+
292
+ test("fails when enforcement does not match required tier", async () => {
293
+ const result = await verify({
294
+ proof: fx.proof,
295
+ bytes: fx.bytes,
296
+ trustAnchors: { requireEnforcement: "measured-tee" },
297
+ });
298
+ assert.equal(result.valid, false);
299
+ assert.match(result.reason ?? "", /enforcement/);
300
+ });
301
+ });
302
+
303
+ describe("verify — policy: allowedMeasurements", () => {
304
+ test("passes when measurement is in the allowlist", async () => {
305
+ const result = await verify({
306
+ proof: fx.proof,
307
+ bytes: fx.bytes,
308
+ trustAnchors: { allowedMeasurements: [fx.measurement] },
309
+ });
310
+ assert.deepEqual(result, { valid: true });
311
+ });
312
+
313
+ test("passes when measurement is one of several in the allowlist", async () => {
314
+ const result = await verify({
315
+ proof: fx.proof,
316
+ bytes: fx.bytes,
317
+ trustAnchors: {
318
+ allowedMeasurements: ["other-meas-1", fx.measurement, "other-meas-2"],
319
+ },
320
+ });
321
+ assert.deepEqual(result, { valid: true });
322
+ });
323
+
324
+ test("fails when measurement is not in the allowlist", async () => {
325
+ const result = await verify({
326
+ proof: fx.proof,
327
+ bytes: fx.bytes,
328
+ trustAnchors: { allowedMeasurements: ["expected-meas", "another-meas"] },
329
+ });
330
+ assert.equal(result.valid, false);
331
+ assert.match(result.reason ?? "", /measurement/);
332
+ });
333
+
334
+ test("passes with empty allowedMeasurements (no constraint)", async () => {
335
+ const result = await verify({
336
+ proof: fx.proof,
337
+ bytes: fx.bytes,
338
+ trustAnchors: { allowedMeasurements: [] },
339
+ });
340
+ assert.deepEqual(result, { valid: true });
341
+ });
342
+ });
343
+
344
+ describe("verify — policy: allowedPublicKeys", () => {
345
+ test("passes when public key is in the allowlist", async () => {
346
+ const result = await verify({
347
+ proof: fx.proof,
348
+ bytes: fx.bytes,
349
+ trustAnchors: { allowedPublicKeys: [fx.publicKeyB64] },
350
+ });
351
+ assert.deepEqual(result, { valid: true });
352
+ });
353
+
354
+ test("fails when public key is not in the allowlist", async () => {
355
+ const otherKey = crypto.getRandomValues(new Uint8Array(32));
356
+ const otherPublic = Buffer.from(await getPublicKeyAsync(otherKey)).toString("base64");
357
+ const result = await verify({
358
+ proof: fx.proof,
359
+ bytes: fx.bytes,
360
+ trustAnchors: { allowedPublicKeys: [otherPublic] },
361
+ });
362
+ assert.equal(result.valid, false);
363
+ assert.match(result.reason ?? "", /public key/i);
364
+ });
365
+ });
366
+
367
+ describe("verify — policy: requireAttestation", () => {
368
+ test("fails when attestation required but proof has none", async () => {
369
+ const result = await verify({
370
+ proof: fx.proof,
371
+ bytes: fx.bytes,
372
+ trustAnchors: { requireAttestation: true },
373
+ });
374
+ assert.equal(result.valid, false);
375
+ assert.match(result.reason ?? "", /attestation/);
376
+ });
377
+ });
378
+
379
+ describe("verify — policy: counter checks", () => {
380
+ let counterFx!: Fixture;
381
+
382
+ before(async () => {
383
+ counterFx = await makeFixture({ withCounter: true });
384
+ });
385
+
386
+ test("valid proof with counter passes minCounter check", async () => {
387
+ const result = await verify({
388
+ proof: counterFx.proof,
389
+ bytes: counterFx.bytes,
390
+ trustAnchors: { minCounter: "1" },
391
+ });
392
+ assert.deepEqual(result, { valid: true });
393
+ });
394
+
395
+ test("fails when counter is below minCounter", async () => {
396
+ const result = await verify({
397
+ proof: counterFx.proof,
398
+ bytes: counterFx.bytes,
399
+ trustAnchors: { minCounter: "999" },
400
+ });
401
+ assert.equal(result.valid, false);
402
+ assert.match(result.reason ?? "", /counter/);
403
+ });
404
+
405
+ test("passes when counter is at exactly maxCounter", async () => {
406
+ // counter is set because hostWithCounter provides nextCounter
407
+ const counter = counterFx.proof.commit.counter!;
408
+ const result = await verify({
409
+ proof: counterFx.proof,
410
+ bytes: counterFx.bytes,
411
+ trustAnchors: { maxCounter: counter },
412
+ });
413
+ assert.deepEqual(result, { valid: true });
414
+ });
415
+
416
+ test("fails when counter exceeds maxCounter", async () => {
417
+ const result = await verify({
418
+ proof: counterFx.proof,
419
+ bytes: counterFx.bytes,
420
+ trustAnchors: { maxCounter: "0" },
421
+ });
422
+ assert.equal(result.valid, false);
423
+ assert.match(result.reason ?? "", /counter/);
424
+ });
425
+
426
+ test("fails when minCounter policy requires counter but proof has none", async () => {
427
+ // fx.proof has no counter (base host has no nextCounter)
428
+ const result = await verify({
429
+ proof: fx.proof,
430
+ bytes: fx.bytes,
431
+ trustAnchors: { minCounter: "0" },
432
+ });
433
+ assert.equal(result.valid, false);
434
+ assert.match(result.reason ?? "", /counter/);
435
+ });
436
+ });
437
+
438
+ describe("verify — policy: time checks", () => {
439
+ const fixedTime = 1_700_000_000_000;
440
+ let timeFx!: Fixture;
441
+
442
+ before(async () => {
443
+ timeFx = await makeFixture({ withTime: fixedTime });
444
+ });
445
+
446
+ test("passes when commit time is within minTime/maxTime window", async () => {
447
+ const result = await verify({
448
+ proof: timeFx.proof,
449
+ bytes: timeFx.bytes,
450
+ trustAnchors: {
451
+ minTime: fixedTime - 1000,
452
+ maxTime: fixedTime + 1000,
453
+ },
454
+ });
455
+ assert.deepEqual(result, { valid: true });
456
+ });
457
+
458
+ test("fails when commit time is before minTime", async () => {
459
+ const result = await verify({
460
+ proof: timeFx.proof,
461
+ bytes: timeFx.bytes,
462
+ trustAnchors: { minTime: fixedTime + 1 },
463
+ });
464
+ assert.equal(result.valid, false);
465
+ assert.match(result.reason ?? "", /time/);
466
+ });
467
+
468
+ test("fails when commit time is after maxTime", async () => {
469
+ const result = await verify({
470
+ proof: timeFx.proof,
471
+ bytes: timeFx.bytes,
472
+ trustAnchors: { maxTime: fixedTime - 1 },
473
+ });
474
+ assert.equal(result.valid, false);
475
+ assert.match(result.reason ?? "", /time/);
476
+ });
477
+
478
+ test("fails when time policy requires time but proof has none", async () => {
479
+ // fx.proof has no time (base host has no secureTime)
480
+ const result = await verify({
481
+ proof: fx.proof,
482
+ bytes: fx.bytes,
483
+ trustAnchors: { minTime: 0 },
484
+ });
485
+ assert.equal(result.valid, false);
486
+ assert.match(result.reason ?? "", /time/);
487
+ });
488
+ });
489
+
490
+ describe("verify — policy: requireEpochId", () => {
491
+ test("fails when epochId is required but proof has none", async () => {
492
+ // fx.proof was created without epochId
493
+ const result = await verify({
494
+ proof: fx.proof,
495
+ bytes: fx.bytes,
496
+ trustAnchors: { requireEpochId: true },
497
+ });
498
+ assert.equal(result.valid, false);
499
+ assert.match(result.reason ?? "", /epochId/i);
500
+ });
501
+
502
+ test("passes when epochId is required and proof has it", async () => {
503
+ const epochFx = await makeFixture({ epochId: "epoch-test-xyz" });
504
+ const result = await verify({
505
+ proof: epochFx.proof,
506
+ bytes: epochFx.bytes,
507
+ trustAnchors: { requireEpochId: true },
508
+ });
509
+ assert.deepEqual(result, { valid: true });
510
+ });
511
+ });
512
+
513
+ describe("verify — policy: requireActor", () => {
514
+ test("fails when actor is required but proof has none", async () => {
515
+ const result = await verify({
516
+ proof: fx.proof,
517
+ bytes: fx.bytes,
518
+ trustAnchors: { requireActor: true },
519
+ });
520
+ assert.equal(result.valid, false);
521
+ assert.match(result.reason ?? "", /actor|agency/i);
522
+ });
523
+ });
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // resetEpochLinkState
527
+ // ---------------------------------------------------------------------------
528
+
529
+ describe("resetEpochLinkState", () => {
530
+ test("is a function that can be called without error", () => {
531
+ assert.doesNotThrow(() => resetEpochLinkState());
532
+ });
533
+
534
+ test("can be called multiple times", () => {
535
+ assert.doesNotThrow(() => {
536
+ resetEpochLinkState();
537
+ resetEpochLinkState();
538
+ resetEpochLinkState();
539
+ });
540
+ });
541
+ });