@fluxpointstudios/orynq-sdk-process-trace 0.2.0 → 0.3.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.
- package/LICENSE +21 -21
- package/dist/index.cjs +574 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +573 -9
- package/dist/index.d.ts +573 -9
- package/dist/index.js +562 -9
- package/dist/index.js.map +1 -1
- package/package.json +17 -3
- package/src/__tests__/bundle.test.ts +942 -860
- package/src/__tests__/governance-round4.test.ts +149 -0
- package/src/__tests__/governance.test.ts +382 -0
- package/src/__tests__/hardening-round2.test.ts +173 -0
- package/src/__tests__/hardening-round3.test.ts +208 -0
- package/src/__tests__/integration.test.ts +611 -611
- package/src/__tests__/merkle.test.ts +756 -756
- package/src/__tests__/model-manifest.test.ts +136 -0
- package/src/__tests__/rolling-hash.test.ts +622 -622
- package/src/__tests__/trace-builder.test.ts +1012 -1012
- package/src/__tests__/types.test.ts +420 -414
- package/src/bundle.ts +1004 -810
- package/src/disclosure.ts +527 -527
- package/src/governance.ts +711 -0
- package/src/index.ts +334 -265
- package/src/manifest.ts +687 -687
- package/src/merkle.ts +428 -428
- package/src/model-manifest.ts +0 -0
- package/src/rolling-hash.ts +375 -366
- package/src/trace-builder.ts +791 -725
- package/src/types.ts +713 -522
package/src/bundle.ts
CHANGED
|
@@ -1,810 +1,1004 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Bundle creation, extraction, verification, and signing for trace bundles.
|
|
3
|
-
*
|
|
4
|
-
* Location: packages/process-trace/src/bundle.ts
|
|
5
|
-
*
|
|
6
|
-
* This module provides the core functionality for working with trace bundles:
|
|
7
|
-
* - Creating bundles from finalized trace runs
|
|
8
|
-
* - Extracting public views for safe external sharing
|
|
9
|
-
* - Verifying bundle integrity (hashes, sequences, merkle proofs)
|
|
10
|
-
* - Signing and verifying bundle signatures
|
|
11
|
-
*
|
|
12
|
-
* A TraceBundle is the finalized, immutable form of a trace run that includes:
|
|
13
|
-
* - The complete private trace run data
|
|
14
|
-
* - A public view with redacted sensitive information
|
|
15
|
-
* - Cryptographic commitments (rootHash, merkleRoot)
|
|
16
|
-
* - Optional signature for authenticity verification
|
|
17
|
-
*
|
|
18
|
-
* Visibility Rules:
|
|
19
|
-
* - "public": Events/spans are included in the publicView
|
|
20
|
-
* - "private": Hash included in redactedSpanHashes, data not disclosed
|
|
21
|
-
* - "secret": Hash included in redactedSpanHashes, data never disclosed
|
|
22
|
-
*
|
|
23
|
-
* Used by:
|
|
24
|
-
* - TraceBuilder: Creates bundles when finalizing traces
|
|
25
|
-
* - TraceVerifier: Validates bundle integrity
|
|
26
|
-
* - TraceStorage: Prepares bundles for storage/transmission
|
|
27
|
-
* - Disclosure workflows: Extracts public views for sharing
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```typescript
|
|
31
|
-
* // Create a bundle from a finalized run
|
|
32
|
-
* const bundle = await createBundle(finalizedRun);
|
|
33
|
-
*
|
|
34
|
-
* // Extract public view for sharing
|
|
35
|
-
* const publicView = extractPublicView(bundle);
|
|
36
|
-
*
|
|
37
|
-
* // Verify bundle integrity
|
|
38
|
-
* const result = await verifyBundle(bundle);
|
|
39
|
-
* if (!result.valid) {
|
|
40
|
-
* console.error("Bundle verification failed:", result.errors);
|
|
41
|
-
* }
|
|
42
|
-
*
|
|
43
|
-
* // Sign a bundle
|
|
44
|
-
* const signedBundle = await signBundle(bundle, signatureProvider);
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
import {
|
|
49
|
-
canonicalize,
|
|
50
|
-
bytesToHex,
|
|
51
|
-
hexToBytes,
|
|
52
|
-
} from "@fluxpointstudios/orynq-sdk-core/utils";
|
|
53
|
-
|
|
54
|
-
import type {
|
|
55
|
-
TraceBundle,
|
|
56
|
-
TraceBundlePublicView,
|
|
57
|
-
TraceRun,
|
|
58
|
-
TraceSpan,
|
|
59
|
-
TraceEvent,
|
|
60
|
-
AnnotatedSpan,
|
|
61
|
-
TraceVerificationResult,
|
|
62
|
-
SignatureProvider,
|
|
63
|
-
Visibility,
|
|
64
|
-
} from "./types.js";
|
|
65
|
-
|
|
66
|
-
import {
|
|
67
|
-
computeEventHash,
|
|
68
|
-
computeRollingHash,
|
|
69
|
-
computeRootHash,
|
|
70
|
-
} from "./rolling-hash.js";
|
|
71
|
-
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
*/
|
|
96
|
-
export
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* @param
|
|
125
|
-
* @returns
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* ```typescript
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// ---------------------------------------------------------------------------
|
|
421
|
-
// Verify
|
|
422
|
-
// ---------------------------------------------------------------------------
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
)
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
//
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
* @param
|
|
692
|
-
* @returns
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Bundle creation, extraction, verification, and signing for trace bundles.
|
|
3
|
+
*
|
|
4
|
+
* Location: packages/process-trace/src/bundle.ts
|
|
5
|
+
*
|
|
6
|
+
* This module provides the core functionality for working with trace bundles:
|
|
7
|
+
* - Creating bundles from finalized trace runs
|
|
8
|
+
* - Extracting public views for safe external sharing
|
|
9
|
+
* - Verifying bundle integrity (hashes, sequences, merkle proofs)
|
|
10
|
+
* - Signing and verifying bundle signatures
|
|
11
|
+
*
|
|
12
|
+
* A TraceBundle is the finalized, immutable form of a trace run that includes:
|
|
13
|
+
* - The complete private trace run data
|
|
14
|
+
* - A public view with redacted sensitive information
|
|
15
|
+
* - Cryptographic commitments (rootHash, merkleRoot)
|
|
16
|
+
* - Optional signature for authenticity verification
|
|
17
|
+
*
|
|
18
|
+
* Visibility Rules:
|
|
19
|
+
* - "public": Events/spans are included in the publicView
|
|
20
|
+
* - "private": Hash included in redactedSpanHashes, data not disclosed
|
|
21
|
+
* - "secret": Hash included in redactedSpanHashes, data never disclosed
|
|
22
|
+
*
|
|
23
|
+
* Used by:
|
|
24
|
+
* - TraceBuilder: Creates bundles when finalizing traces
|
|
25
|
+
* - TraceVerifier: Validates bundle integrity
|
|
26
|
+
* - TraceStorage: Prepares bundles for storage/transmission
|
|
27
|
+
* - Disclosure workflows: Extracts public views for sharing
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // Create a bundle from a finalized run
|
|
32
|
+
* const bundle = await createBundle(finalizedRun);
|
|
33
|
+
*
|
|
34
|
+
* // Extract public view for sharing
|
|
35
|
+
* const publicView = extractPublicView(bundle);
|
|
36
|
+
*
|
|
37
|
+
* // Verify bundle integrity
|
|
38
|
+
* const result = await verifyBundle(bundle);
|
|
39
|
+
* if (!result.valid) {
|
|
40
|
+
* console.error("Bundle verification failed:", result.errors);
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // Sign a bundle
|
|
44
|
+
* const signedBundle = await signBundle(bundle, signatureProvider);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import {
|
|
49
|
+
canonicalize,
|
|
50
|
+
bytesToHex,
|
|
51
|
+
hexToBytes,
|
|
52
|
+
} from "@fluxpointstudios/orynq-sdk-core/utils";
|
|
53
|
+
|
|
54
|
+
import type {
|
|
55
|
+
TraceBundle,
|
|
56
|
+
TraceBundlePublicView,
|
|
57
|
+
TraceRun,
|
|
58
|
+
TraceSpan,
|
|
59
|
+
TraceEvent,
|
|
60
|
+
AnnotatedSpan,
|
|
61
|
+
TraceVerificationResult,
|
|
62
|
+
SignatureProvider,
|
|
63
|
+
Visibility,
|
|
64
|
+
} from "./types.js";
|
|
65
|
+
|
|
66
|
+
import {
|
|
67
|
+
computeEventHash,
|
|
68
|
+
computeRollingHash,
|
|
69
|
+
computeRootHash,
|
|
70
|
+
} from "./rolling-hash.js";
|
|
71
|
+
|
|
72
|
+
import { computeModelManifestHash } from "./model-manifest.js";
|
|
73
|
+
|
|
74
|
+
import { buildSpanMerkleTree, computeSpanHash } from "./merkle.js";
|
|
75
|
+
|
|
76
|
+
import {
|
|
77
|
+
verifyGovernanceAttestations,
|
|
78
|
+
type VerifyGovernanceOptions,
|
|
79
|
+
} from "./governance.js";
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// VERIFY OPTIONS
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Outcome shape returned by an injected tool-receipt verifier (provided by
|
|
87
|
+
* `@fluxpointstudios/orynq-sdk-tool-receipts` — passed in to avoid a circular
|
|
88
|
+
* dependency on that package from process-trace).
|
|
89
|
+
*/
|
|
90
|
+
export interface ToolReceiptVerifyOutcome {
|
|
91
|
+
valid: boolean;
|
|
92
|
+
errors: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Options for {@link verifyBundle}. */
|
|
96
|
+
export interface VerifyBundleOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Verify `governance-attestation` events. `true` uses the built-in
|
|
99
|
+
* sr25519/ed25519 verifiers; pass {@link VerifyGovernanceOptions} to register
|
|
100
|
+
* a pluggable verifier (e.g. eip712). Any unverified attestation fails the
|
|
101
|
+
* bundle.
|
|
102
|
+
*/
|
|
103
|
+
governance?: boolean | VerifyGovernanceOptions;
|
|
104
|
+
/**
|
|
105
|
+
* Verify `tool-receipt` events with an injected verifier from
|
|
106
|
+
* `@fluxpointstudios/orynq-sdk-tool-receipts`. Any failed receipt fails the
|
|
107
|
+
* bundle.
|
|
108
|
+
*/
|
|
109
|
+
toolReceipts?: (
|
|
110
|
+
bundle: TraceBundle
|
|
111
|
+
) => Promise<ToolReceiptVerifyOutcome> | ToolReceiptVerifyOutcome;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// =============================================================================
|
|
115
|
+
// VISIBILITY HELPERS
|
|
116
|
+
// =============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if a span should be included in public view.
|
|
120
|
+
*
|
|
121
|
+
* Only spans with visibility "public" are included in the public view.
|
|
122
|
+
* Private and secret spans are redacted (only their hashes are included).
|
|
123
|
+
*
|
|
124
|
+
* @param span - The span to check
|
|
125
|
+
* @returns true if the span is public and should be included in publicView
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* if (isPublicSpan(span)) {
|
|
130
|
+
* publicSpans.push(span);
|
|
131
|
+
* } else {
|
|
132
|
+
* redactedSpanHashes.push({ spanId: span.id, hash: span.hash });
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function isPublicSpan(span: TraceSpan): boolean {
|
|
137
|
+
return span.visibility === "public";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if an event should be included in public view.
|
|
142
|
+
*
|
|
143
|
+
* Only events with visibility "public" are included in the public view.
|
|
144
|
+
* Private and secret events are not disclosed.
|
|
145
|
+
*
|
|
146
|
+
* @param event - The event to check
|
|
147
|
+
* @returns true if the event is public and should be included in publicView
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const publicEvents = events.filter(isPublicEvent);
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function isPublicEvent(event: TraceEvent): boolean {
|
|
155
|
+
return event.visibility === "public";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Filter events by visibility, returning only public events.
|
|
160
|
+
*
|
|
161
|
+
* This function creates a new array containing only events with
|
|
162
|
+
* visibility === "public". The original array is not modified.
|
|
163
|
+
*
|
|
164
|
+
* @param events - Array of trace events to filter
|
|
165
|
+
* @returns Array containing only public events
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* const allEvents = getSpanEvents(span, run.events);
|
|
170
|
+
* const publicEvents = filterPublicEvents(allEvents);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function filterPublicEvents(events: TraceEvent[]): TraceEvent[] {
|
|
174
|
+
return events.filter(isPublicEvent);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// =============================================================================
|
|
178
|
+
// BUNDLE CREATION
|
|
179
|
+
// =============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create a bundle from a finalized trace run.
|
|
183
|
+
*
|
|
184
|
+
* The run should already have rootHash computed (i.e., be finalized).
|
|
185
|
+
* This function:
|
|
186
|
+
* 1. Validates the run is finalized
|
|
187
|
+
* 2. Builds the Merkle tree if not already computed
|
|
188
|
+
* 3. Creates the public view with redacted sensitive data
|
|
189
|
+
* 4. Returns the complete bundle
|
|
190
|
+
*
|
|
191
|
+
* @param run - The finalized trace run (must have rootHash)
|
|
192
|
+
* @returns Promise resolving to the complete TraceBundle
|
|
193
|
+
* @throws Error if the run is not finalized (missing rootHash)
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* // Finalize the run first
|
|
198
|
+
* const finalizedRun = await finalizeTraceRun(run);
|
|
199
|
+
*
|
|
200
|
+
* // Create the bundle
|
|
201
|
+
* const bundle = await createBundle(finalizedRun);
|
|
202
|
+
* console.log(bundle.rootHash); // Cryptographic commitment
|
|
203
|
+
* console.log(bundle.merkleRoot); // Merkle root for selective disclosure
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export async function createBundle(run: TraceRun): Promise<TraceBundle> {
|
|
207
|
+
// Validate run is finalized
|
|
208
|
+
if (!run.rootHash) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"Cannot create bundle from non-finalized run: rootHash is missing. " +
|
|
211
|
+
"Call finalizeTraceRun() before creating a bundle."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (run.status === "running") {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"Cannot create bundle from running trace. " +
|
|
218
|
+
"The trace must be completed, failed, or cancelled."
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Ensure all events have hashes computed
|
|
223
|
+
for (const event of run.events) {
|
|
224
|
+
if (!event.hash) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Event ${event.id} (seq ${event.seq}) is missing hash. ` +
|
|
227
|
+
"All events must have hashes computed before creating a bundle."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Ensure all spans have hashes computed
|
|
233
|
+
for (const span of run.spans) {
|
|
234
|
+
if (!span.hash) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Span ${span.id} (spanSeq ${span.spanSeq}) is missing hash. ` +
|
|
237
|
+
"All spans must have hashes computed before creating a bundle."
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Build Merkle tree from spans
|
|
243
|
+
const merkleTree = await buildSpanMerkleTree(run.spans, run.events);
|
|
244
|
+
|
|
245
|
+
// Create the public view
|
|
246
|
+
const publicView = createPublicView(run, merkleTree.rootHash);
|
|
247
|
+
|
|
248
|
+
// Construct the bundle
|
|
249
|
+
const bundle: TraceBundle = {
|
|
250
|
+
formatVersion: run.schemaVersion,
|
|
251
|
+
publicView,
|
|
252
|
+
privateRun: run,
|
|
253
|
+
merkleRoot: merkleTree.rootHash,
|
|
254
|
+
rootHash: run.rootHash,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return bundle;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Internal helper to create the public view from a run.
|
|
262
|
+
*
|
|
263
|
+
* @param run - The finalized trace run
|
|
264
|
+
* @param merkleRoot - The computed Merkle root
|
|
265
|
+
* @returns The TraceBundlePublicView
|
|
266
|
+
*/
|
|
267
|
+
function createPublicView(
|
|
268
|
+
run: TraceRun,
|
|
269
|
+
merkleRoot: string
|
|
270
|
+
): TraceBundlePublicView {
|
|
271
|
+
// Create event lookup map
|
|
272
|
+
const eventMap = new Map<string, TraceEvent>();
|
|
273
|
+
for (const event of run.events) {
|
|
274
|
+
eventMap.set(event.id, event);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Separate public and non-public spans
|
|
278
|
+
const publicSpans: AnnotatedSpan[] = [];
|
|
279
|
+
const redactedSpanHashes: Array<{ spanId: string; hash: string }> = [];
|
|
280
|
+
|
|
281
|
+
for (const span of run.spans) {
|
|
282
|
+
if (isPublicSpan(span)) {
|
|
283
|
+
// Get events for this span and filter to public only
|
|
284
|
+
const spanEvents = span.eventIds
|
|
285
|
+
.map((id) => eventMap.get(id))
|
|
286
|
+
.filter((e): e is TraceEvent => e !== undefined)
|
|
287
|
+
.filter(isPublicEvent)
|
|
288
|
+
.sort((a, b) => a.seq - b.seq);
|
|
289
|
+
|
|
290
|
+
// Create annotated span with embedded events
|
|
291
|
+
const annotatedSpan: AnnotatedSpan = {
|
|
292
|
+
...span,
|
|
293
|
+
events: spanEvents,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
publicSpans.push(annotatedSpan);
|
|
297
|
+
} else {
|
|
298
|
+
// Non-public span: include only hash reference
|
|
299
|
+
redactedSpanHashes.push({
|
|
300
|
+
spanId: span.id,
|
|
301
|
+
hash: span.hash ?? "",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Sort public spans by spanSeq for deterministic ordering
|
|
307
|
+
publicSpans.sort((a, b) => a.spanSeq - b.spanSeq);
|
|
308
|
+
|
|
309
|
+
// Sort redacted hashes by spanId for deterministic ordering
|
|
310
|
+
redactedSpanHashes.sort((a, b) => a.spanId.localeCompare(b.spanId));
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
runId: run.id,
|
|
314
|
+
agentId: run.agentId,
|
|
315
|
+
schemaVersion: run.schemaVersion,
|
|
316
|
+
startedAt: run.startedAt,
|
|
317
|
+
endedAt: run.endedAt ?? new Date().toISOString(),
|
|
318
|
+
durationMs: run.durationMs ?? 0,
|
|
319
|
+
status: run.status,
|
|
320
|
+
totalEvents: run.events.length,
|
|
321
|
+
totalSpans: run.spans.length,
|
|
322
|
+
rootHash: run.rootHash ?? "",
|
|
323
|
+
merkleRoot,
|
|
324
|
+
publicSpans,
|
|
325
|
+
redactedSpanHashes,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// =============================================================================
|
|
330
|
+
// PUBLIC VIEW EXTRACTION
|
|
331
|
+
// =============================================================================
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Extract the public view from a bundle.
|
|
335
|
+
*
|
|
336
|
+
* Returns only public spans with their public events.
|
|
337
|
+
* This is a convenience function that returns the pre-computed public view
|
|
338
|
+
* from the bundle. Use this for sharing trace information externally.
|
|
339
|
+
*
|
|
340
|
+
* Note: The public view is computed when the bundle is created, so this
|
|
341
|
+
* function simply returns the existing public view. If you need to
|
|
342
|
+
* re-compute the public view (e.g., with different redaction rules),
|
|
343
|
+
* you should create a new bundle.
|
|
344
|
+
*
|
|
345
|
+
* @param bundle - The trace bundle
|
|
346
|
+
* @returns The TraceBundlePublicView (safe to share externally)
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* const bundle = await createBundle(run);
|
|
351
|
+
* const publicView = extractPublicView(bundle);
|
|
352
|
+
*
|
|
353
|
+
* // Safe to share externally
|
|
354
|
+
* await sendToAuditSystem(publicView);
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
export function extractPublicView(bundle: TraceBundle): TraceBundlePublicView {
|
|
358
|
+
return bundle.publicView;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// =============================================================================
|
|
362
|
+
// BUNDLE VERIFICATION
|
|
363
|
+
// =============================================================================
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Verify a bundle's integrity.
|
|
367
|
+
*
|
|
368
|
+
* Performs comprehensive validation including:
|
|
369
|
+
* - Event hashes are correct (recomputed and compared)
|
|
370
|
+
* - Span hashes are correct (recomputed and compared)
|
|
371
|
+
* - Rolling hash matches (recomputed from events)
|
|
372
|
+
* - Root hash matches (recomputed from rolling hash + span hashes)
|
|
373
|
+
* - Merkle root matches (recomputed from span tree)
|
|
374
|
+
* - Event sequence is monotonic (0, 1, 2, ...)
|
|
375
|
+
* - Span sequence is monotonic (0, 1, 2, ...)
|
|
376
|
+
*
|
|
377
|
+
* @param bundle - The trace bundle to verify
|
|
378
|
+
* @returns Promise resolving to comprehensive verification result
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```typescript
|
|
382
|
+
* const result = await verifyBundle(bundle);
|
|
383
|
+
*
|
|
384
|
+
* if (!result.valid) {
|
|
385
|
+
* console.error("Bundle verification failed!");
|
|
386
|
+
* console.error("Errors:", result.errors);
|
|
387
|
+
* console.error("Warnings:", result.warnings);
|
|
388
|
+
* console.error("Checks:", result.checks);
|
|
389
|
+
* }
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
export async function verifyBundle(
|
|
393
|
+
bundle: TraceBundle,
|
|
394
|
+
options: VerifyBundleOptions = {}
|
|
395
|
+
): Promise<TraceVerificationResult> {
|
|
396
|
+
const errors: string[] = [];
|
|
397
|
+
const warnings: string[] = [];
|
|
398
|
+
const checks: TraceVerificationResult["checks"] = {
|
|
399
|
+
rollingHashValid: false,
|
|
400
|
+
rootHashValid: false,
|
|
401
|
+
merkleRootValid: false,
|
|
402
|
+
spanHashesValid: false,
|
|
403
|
+
eventHashesValid: false,
|
|
404
|
+
sequenceValid: false,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const run = bundle.privateRun;
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// Verify Event Sequence
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
const sequenceErrors = verifySequences(run);
|
|
414
|
+
if (sequenceErrors.length === 0) {
|
|
415
|
+
checks.sequenceValid = true;
|
|
416
|
+
} else {
|
|
417
|
+
errors.push(...sequenceErrors);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Verify Event Hashes
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
const eventHashErrors = await verifyEventHashes(run.events);
|
|
425
|
+
if (eventHashErrors.length === 0) {
|
|
426
|
+
checks.eventHashesValid = true;
|
|
427
|
+
} else {
|
|
428
|
+
errors.push(...eventHashErrors);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Verify Span Hashes
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
const spanHashErrors = await verifySpanHashes(run.spans, run.events);
|
|
436
|
+
if (spanHashErrors.length === 0) {
|
|
437
|
+
checks.spanHashesValid = true;
|
|
438
|
+
} else {
|
|
439
|
+
errors.push(...spanHashErrors);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Verify Rolling Hash
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const computedRollingHash = await computeRollingHash(run.events);
|
|
448
|
+
if (computedRollingHash === run.rollingHash) {
|
|
449
|
+
checks.rollingHashValid = true;
|
|
450
|
+
} else {
|
|
451
|
+
errors.push(
|
|
452
|
+
`Rolling hash mismatch: expected ${run.rollingHash}, computed ${computedRollingHash}`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
errors.push(
|
|
457
|
+
`Failed to compute rolling hash: ${error instanceof Error ? error.message : String(error)}`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// Verify Root Hash
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
// Recompute the root binding the recorded model-manifest commitment (#59),
|
|
467
|
+
// so a manifest swapped after commitment fails here.
|
|
468
|
+
const computedRootHash = await computeRootHash(
|
|
469
|
+
run.rollingHash,
|
|
470
|
+
run.spans,
|
|
471
|
+
run.modelManifestHash
|
|
472
|
+
);
|
|
473
|
+
if (computedRootHash === bundle.rootHash) {
|
|
474
|
+
checks.rootHashValid = true;
|
|
475
|
+
} else {
|
|
476
|
+
errors.push(
|
|
477
|
+
`Root hash mismatch: expected ${bundle.rootHash}, computed ${computedRootHash}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
errors.push(
|
|
482
|
+
`Failed to compute root hash: ${error instanceof Error ? error.message : String(error)}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Verify Merkle Root
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const merkleTree = await buildSpanMerkleTree(run.spans, run.events);
|
|
492
|
+
if (merkleTree.rootHash === bundle.merkleRoot) {
|
|
493
|
+
checks.merkleRootValid = true;
|
|
494
|
+
} else {
|
|
495
|
+
errors.push(
|
|
496
|
+
`Merkle root mismatch: expected ${bundle.merkleRoot}, computed ${merkleTree.rootHash}`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
errors.push(
|
|
501
|
+
`Failed to compute Merkle root: ${error instanceof Error ? error.message : String(error)}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Verify Model-Manifest Pin Binding (issue #59)
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
// When a manifest is pinned it MUST (a) hash to the recorded commitment and
|
|
509
|
+
// (b) be bound into the committed root. (b) is enforced by folding
|
|
510
|
+
// modelManifestHash into computeRootHash above — a swapped hash breaks
|
|
511
|
+
// rootHashValid. Here we additionally recompute the commitment from the
|
|
512
|
+
// manifest itself so that swapping the manifest (while leaving the recorded
|
|
513
|
+
// hash) is caught, and reject a manifest present but not bound into the root.
|
|
514
|
+
|
|
515
|
+
const hasManifest = run.modelManifest !== undefined;
|
|
516
|
+
const hasManifestHash =
|
|
517
|
+
run.modelManifestHash !== undefined && run.modelManifestHash.length > 0;
|
|
518
|
+
|
|
519
|
+
if (!hasManifest && !hasManifestHash) {
|
|
520
|
+
// No manifest pinned — nothing to bind (warn-only path lives in finalizeTrace).
|
|
521
|
+
checks.modelManifestValid = true;
|
|
522
|
+
} else {
|
|
523
|
+
let manifestBindingValid = true;
|
|
524
|
+
|
|
525
|
+
if (hasManifest && !hasManifestHash) {
|
|
526
|
+
manifestBindingValid = false;
|
|
527
|
+
errors.push(
|
|
528
|
+
"Model manifest present but modelManifestHash (its commitment) is missing"
|
|
529
|
+
);
|
|
530
|
+
} else if (!hasManifest && hasManifestHash) {
|
|
531
|
+
manifestBindingValid = false;
|
|
532
|
+
errors.push(
|
|
533
|
+
"modelManifestHash present but the model manifest itself is missing"
|
|
534
|
+
);
|
|
535
|
+
} else if (run.modelManifest !== undefined) {
|
|
536
|
+
try {
|
|
537
|
+
const recomputed = await computeModelManifestHash(run.modelManifest);
|
|
538
|
+
if (recomputed !== run.modelManifestHash) {
|
|
539
|
+
manifestBindingValid = false;
|
|
540
|
+
errors.push(
|
|
541
|
+
`Model manifest hash mismatch: recorded ${run.modelManifestHash}, computed ${recomputed}`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
manifestBindingValid = false;
|
|
546
|
+
errors.push(
|
|
547
|
+
`Failed to recompute model manifest hash: ${error instanceof Error ? error.message : String(error)}`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// The manifest must actually be bound into the committed root. If the root
|
|
553
|
+
// recompute (with the manifest folded in) matched, rootHashValid is true;
|
|
554
|
+
// a manifest that is NOT bound produces a root mismatch above.
|
|
555
|
+
if (manifestBindingValid && !checks.rootHashValid) {
|
|
556
|
+
manifestBindingValid = false;
|
|
557
|
+
errors.push(
|
|
558
|
+
"Model manifest is not bound into the committed root hash"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
checks.modelManifestValid = manifestBindingValid;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// Verify PublicView Model-Manifest (issue #59)
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
// publicView.{modelManifest,modelManifestHash} is the shared artifact an
|
|
569
|
+
// EXTERNAL verifier reads. It must (a) equal the bound privateRun commitment
|
|
570
|
+
// and (b) recompute consistently from the publicView manifest itself — else an
|
|
571
|
+
// attacker can leave privateRun honest but fabricate the public fields.
|
|
572
|
+
|
|
573
|
+
const pvManifest = bundle.publicView.modelManifest;
|
|
574
|
+
const pvManifestHash = bundle.publicView.modelManifestHash;
|
|
575
|
+
|
|
576
|
+
if (pvManifestHash !== undefined && pvManifestHash !== run.modelManifestHash) {
|
|
577
|
+
checks.modelManifestValid = false;
|
|
578
|
+
errors.push(
|
|
579
|
+
`PublicView modelManifestHash (${pvManifestHash}) does not match the bound commitment (${run.modelManifestHash})`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (pvManifest !== undefined && run.modelManifest === undefined) {
|
|
583
|
+
checks.modelManifestValid = false;
|
|
584
|
+
errors.push("PublicView carries a model manifest but the bound run has none");
|
|
585
|
+
}
|
|
586
|
+
if (pvManifest !== undefined) {
|
|
587
|
+
try {
|
|
588
|
+
const recomputed = await computeModelManifestHash(pvManifest);
|
|
589
|
+
const expected = pvManifestHash ?? run.modelManifestHash;
|
|
590
|
+
if (expected !== undefined && recomputed !== expected) {
|
|
591
|
+
checks.modelManifestValid = false;
|
|
592
|
+
errors.push(
|
|
593
|
+
`PublicView model manifest hash mismatch: recorded ${expected}, computed ${recomputed}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
} catch (error) {
|
|
597
|
+
checks.modelManifestValid = false;
|
|
598
|
+
errors.push(
|
|
599
|
+
`Failed to recompute publicView model manifest hash: ${error instanceof Error ? error.message : String(error)}`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// Additional Warnings
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
|
|
608
|
+
// Warn if there are no public spans
|
|
609
|
+
if (bundle.publicView.publicSpans.length === 0 && run.spans.length > 0) {
|
|
610
|
+
warnings.push(
|
|
611
|
+
"No public spans in bundle. The public view will be empty. " +
|
|
612
|
+
"Consider marking some spans as public for transparency."
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Warn if run status doesn't match public view status
|
|
617
|
+
if (bundle.publicView.status !== run.status) {
|
|
618
|
+
warnings.push(
|
|
619
|
+
`Status mismatch between publicView (${bundle.publicView.status}) and privateRun (${run.status})`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
// Verify Governance Attestations (issue #58, opt-in)
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
if (options.governance) {
|
|
628
|
+
try {
|
|
629
|
+
const govOpts: VerifyGovernanceOptions =
|
|
630
|
+
options.governance === true ? {} : options.governance;
|
|
631
|
+
const summaries = await verifyGovernanceAttestations(bundle, govOpts);
|
|
632
|
+
const failed = summaries.filter((s) => !s.verified);
|
|
633
|
+
checks.governanceValid = failed.length === 0;
|
|
634
|
+
for (const f of failed) {
|
|
635
|
+
errors.push(
|
|
636
|
+
`Governance attestation failed (${f.scheme}, role ${f.role}, attestor ${f.attestor})` +
|
|
637
|
+
(f.error ? `: ${f.error}` : "")
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
} catch (error) {
|
|
641
|
+
checks.governanceValid = false;
|
|
642
|
+
errors.push(
|
|
643
|
+
`Failed to verify governance attestations: ${error instanceof Error ? error.message : String(error)}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
// Verify Tool-Call Receipts (issue #60, opt-in, injected verifier)
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
if (options.toolReceipts) {
|
|
653
|
+
try {
|
|
654
|
+
const outcome = await options.toolReceipts(bundle);
|
|
655
|
+
checks.toolReceiptsValid = outcome.valid;
|
|
656
|
+
if (!outcome.valid) {
|
|
657
|
+
errors.push(...outcome.errors);
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
checks.toolReceiptsValid = false;
|
|
661
|
+
errors.push(
|
|
662
|
+
`Failed to verify tool receipts: ${error instanceof Error ? error.message : String(error)}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Determine overall validity. Optional checks only fail the bundle when
|
|
668
|
+
// explicitly run and false (undefined === "not checked").
|
|
669
|
+
const valid =
|
|
670
|
+
checks.rollingHashValid &&
|
|
671
|
+
checks.rootHashValid &&
|
|
672
|
+
checks.merkleRootValid &&
|
|
673
|
+
checks.spanHashesValid &&
|
|
674
|
+
checks.eventHashesValid &&
|
|
675
|
+
checks.sequenceValid &&
|
|
676
|
+
checks.modelManifestValid !== false &&
|
|
677
|
+
checks.governanceValid !== false &&
|
|
678
|
+
checks.toolReceiptsValid !== false;
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
valid,
|
|
682
|
+
errors,
|
|
683
|
+
warnings,
|
|
684
|
+
checks,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Verify event and span sequences are monotonic.
|
|
690
|
+
*
|
|
691
|
+
* @param run - The trace run to verify
|
|
692
|
+
* @returns Array of error messages (empty if valid)
|
|
693
|
+
*/
|
|
694
|
+
function verifySequences(run: TraceRun): string[] {
|
|
695
|
+
const errors: string[] = [];
|
|
696
|
+
|
|
697
|
+
// Sort events by seq to check monotonicity
|
|
698
|
+
const sortedEvents = [...run.events].sort((a, b) => a.seq - b.seq);
|
|
699
|
+
|
|
700
|
+
// Check event sequence is monotonic starting from 0
|
|
701
|
+
for (let i = 0; i < sortedEvents.length; i++) {
|
|
702
|
+
const event = sortedEvents[i];
|
|
703
|
+
// Handle noUncheckedIndexedAccess - event is guaranteed to exist after loop bounds check
|
|
704
|
+
if (event !== undefined && event.seq !== i) {
|
|
705
|
+
errors.push(
|
|
706
|
+
`Event sequence gap: expected seq ${i}, found ${event.seq} for event ${event.id}`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Sort spans by spanSeq to check monotonicity
|
|
712
|
+
const sortedSpans = [...run.spans].sort((a, b) => a.spanSeq - b.spanSeq);
|
|
713
|
+
|
|
714
|
+
// Check span sequence is monotonic starting from 0
|
|
715
|
+
for (let i = 0; i < sortedSpans.length; i++) {
|
|
716
|
+
const span = sortedSpans[i];
|
|
717
|
+
// Handle noUncheckedIndexedAccess - span is guaranteed to exist after loop bounds check
|
|
718
|
+
if (span !== undefined && span.spanSeq !== i) {
|
|
719
|
+
errors.push(
|
|
720
|
+
`Span sequence gap: expected spanSeq ${i}, found ${span.spanSeq} for span ${span.id}`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return errors;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Verify all event hashes are correct.
|
|
730
|
+
*
|
|
731
|
+
* @param events - Array of events to verify
|
|
732
|
+
* @returns Promise resolving to array of error messages (empty if valid)
|
|
733
|
+
*/
|
|
734
|
+
async function verifyEventHashes(events: TraceEvent[]): Promise<string[]> {
|
|
735
|
+
const errors: string[] = [];
|
|
736
|
+
|
|
737
|
+
for (const event of events) {
|
|
738
|
+
if (!event.hash) {
|
|
739
|
+
errors.push(`Event ${event.id} (seq ${event.seq}) is missing hash`);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
const computedHash = await computeEventHash(event);
|
|
745
|
+
if (computedHash !== event.hash) {
|
|
746
|
+
errors.push(
|
|
747
|
+
`Event hash mismatch for ${event.id} (seq ${event.seq}): ` +
|
|
748
|
+
`expected ${event.hash}, computed ${computedHash}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
} catch (error) {
|
|
752
|
+
errors.push(
|
|
753
|
+
`Failed to compute hash for event ${event.id}: ` +
|
|
754
|
+
`${error instanceof Error ? error.message : String(error)}`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return errors;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Verify all span hashes are correct.
|
|
764
|
+
*
|
|
765
|
+
* @param spans - Array of spans to verify
|
|
766
|
+
* @param events - Array of all events (for looking up event hashes)
|
|
767
|
+
* @returns Promise resolving to array of error messages (empty if valid)
|
|
768
|
+
*/
|
|
769
|
+
async function verifySpanHashes(
|
|
770
|
+
spans: TraceSpan[],
|
|
771
|
+
events: TraceEvent[]
|
|
772
|
+
): Promise<string[]> {
|
|
773
|
+
const errors: string[] = [];
|
|
774
|
+
|
|
775
|
+
// Create event lookup map
|
|
776
|
+
const eventMap = new Map<string, TraceEvent>();
|
|
777
|
+
for (const event of events) {
|
|
778
|
+
eventMap.set(event.id, event);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
for (const span of spans) {
|
|
782
|
+
if (!span.hash) {
|
|
783
|
+
errors.push(
|
|
784
|
+
`Span ${span.id} (spanSeq ${span.spanSeq}) is missing hash`
|
|
785
|
+
);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
// Get event hashes for this span in seq order
|
|
791
|
+
const spanEvents = span.eventIds
|
|
792
|
+
.map((id) => eventMap.get(id))
|
|
793
|
+
.filter((e): e is TraceEvent => e !== undefined)
|
|
794
|
+
.sort((a, b) => a.seq - b.seq);
|
|
795
|
+
|
|
796
|
+
const eventHashes = spanEvents.map((e) => e.hash ?? "");
|
|
797
|
+
|
|
798
|
+
const computedHash = await computeSpanHash(span, eventHashes);
|
|
799
|
+
if (computedHash !== span.hash) {
|
|
800
|
+
errors.push(
|
|
801
|
+
`Span hash mismatch for ${span.id} (spanSeq ${span.spanSeq}): ` +
|
|
802
|
+
`expected ${span.hash}, computed ${computedHash}`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
} catch (error) {
|
|
806
|
+
errors.push(
|
|
807
|
+
`Failed to compute hash for span ${span.id}: ` +
|
|
808
|
+
`${error instanceof Error ? error.message : String(error)}`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return errors;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// =============================================================================
|
|
817
|
+
// BUNDLE SIGNING
|
|
818
|
+
// =============================================================================
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Sign a bundle using the provided signature provider.
|
|
822
|
+
*
|
|
823
|
+
* Signs the canonical JSON of { rootHash, merkleRoot, manifestHash? }.
|
|
824
|
+
* The signature and signer ID are added to the bundle.
|
|
825
|
+
*
|
|
826
|
+
* @param bundle - The bundle to sign
|
|
827
|
+
* @param provider - The signature provider implementation
|
|
828
|
+
* @returns Promise resolving to the signed bundle (new object, original unchanged)
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```typescript
|
|
832
|
+
* const provider: SignatureProvider = {
|
|
833
|
+
* signerId: "agent-123",
|
|
834
|
+
* sign: async (data) => await myHSM.sign(data),
|
|
835
|
+
* verify: async (data, sig, signerId) => await myHSM.verify(data, sig),
|
|
836
|
+
* };
|
|
837
|
+
*
|
|
838
|
+
* const signedBundle = await signBundle(bundle, provider);
|
|
839
|
+
* console.log(signedBundle.signature); // Hex-encoded signature
|
|
840
|
+
* console.log(signedBundle.signerId); // "agent-123"
|
|
841
|
+
* ```
|
|
842
|
+
*/
|
|
843
|
+
export async function signBundle(
|
|
844
|
+
bundle: TraceBundle,
|
|
845
|
+
provider: SignatureProvider
|
|
846
|
+
): Promise<TraceBundle> {
|
|
847
|
+
// Create the signing payload
|
|
848
|
+
const signingPayload: {
|
|
849
|
+
rootHash: string;
|
|
850
|
+
merkleRoot: string;
|
|
851
|
+
manifestHash?: string;
|
|
852
|
+
} = {
|
|
853
|
+
rootHash: bundle.rootHash,
|
|
854
|
+
merkleRoot: bundle.merkleRoot,
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// Include manifestHash if present
|
|
858
|
+
if (bundle.manifestHash) {
|
|
859
|
+
signingPayload.manifestHash = bundle.manifestHash;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Canonicalize to get deterministic bytes
|
|
863
|
+
const canonicalPayload = canonicalize(signingPayload);
|
|
864
|
+
const payloadBytes = new TextEncoder().encode(canonicalPayload);
|
|
865
|
+
|
|
866
|
+
// Sign using the provider
|
|
867
|
+
const signatureBytes = await provider.sign(payloadBytes);
|
|
868
|
+
const signatureHex = bytesToHex(signatureBytes);
|
|
869
|
+
|
|
870
|
+
// Return new bundle with signature
|
|
871
|
+
return {
|
|
872
|
+
...bundle,
|
|
873
|
+
signerId: provider.signerId,
|
|
874
|
+
signature: signatureHex,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Verify a bundle's signature.
|
|
880
|
+
*
|
|
881
|
+
* Recomputes the signing payload and verifies the signature using
|
|
882
|
+
* the provider. The bundle must have both signature and signerId set.
|
|
883
|
+
*
|
|
884
|
+
* @param bundle - The signed bundle to verify
|
|
885
|
+
* @param provider - The signature provider implementation
|
|
886
|
+
* @returns Promise resolving to true if signature is valid, false otherwise
|
|
887
|
+
*
|
|
888
|
+
* @example
|
|
889
|
+
* ```typescript
|
|
890
|
+
* const isValid = await verifyBundleSignature(signedBundle, provider);
|
|
891
|
+
* if (!isValid) {
|
|
892
|
+
* throw new Error("Bundle signature verification failed!");
|
|
893
|
+
* }
|
|
894
|
+
* ```
|
|
895
|
+
*/
|
|
896
|
+
export async function verifyBundleSignature(
|
|
897
|
+
bundle: TraceBundle,
|
|
898
|
+
provider: SignatureProvider
|
|
899
|
+
): Promise<boolean> {
|
|
900
|
+
// Check required fields
|
|
901
|
+
if (!bundle.signature) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (!bundle.signerId) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
// Recreate the signing payload
|
|
911
|
+
const signingPayload: {
|
|
912
|
+
rootHash: string;
|
|
913
|
+
merkleRoot: string;
|
|
914
|
+
manifestHash?: string;
|
|
915
|
+
} = {
|
|
916
|
+
rootHash: bundle.rootHash,
|
|
917
|
+
merkleRoot: bundle.merkleRoot,
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// Include manifestHash if it was present when signed
|
|
921
|
+
if (bundle.manifestHash) {
|
|
922
|
+
signingPayload.manifestHash = bundle.manifestHash;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Canonicalize to get deterministic bytes
|
|
926
|
+
const canonicalPayload = canonicalize(signingPayload);
|
|
927
|
+
const payloadBytes = new TextEncoder().encode(canonicalPayload);
|
|
928
|
+
|
|
929
|
+
// Convert signature from hex
|
|
930
|
+
const signatureBytes = hexToBytes(bundle.signature);
|
|
931
|
+
|
|
932
|
+
// Verify using the provider
|
|
933
|
+
return await provider.verify(payloadBytes, signatureBytes, bundle.signerId);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
// Verification failed due to error (invalid format, etc.)
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// =============================================================================
|
|
941
|
+
// UTILITY FUNCTIONS
|
|
942
|
+
// =============================================================================
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Get all events belonging to a specific span.
|
|
946
|
+
*
|
|
947
|
+
* @param span - The span to get events for
|
|
948
|
+
* @param events - Array of all events
|
|
949
|
+
* @returns Array of events belonging to the span, sorted by seq
|
|
950
|
+
*/
|
|
951
|
+
export function getSpanEvents(
|
|
952
|
+
span: TraceSpan,
|
|
953
|
+
events: TraceEvent[]
|
|
954
|
+
): TraceEvent[] {
|
|
955
|
+
const eventMap = new Map<string, TraceEvent>();
|
|
956
|
+
for (const event of events) {
|
|
957
|
+
eventMap.set(event.id, event);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return span.eventIds
|
|
961
|
+
.map((id) => eventMap.get(id))
|
|
962
|
+
.filter((e): e is TraceEvent => e !== undefined)
|
|
963
|
+
.sort((a, b) => a.seq - b.seq);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Count events by visibility level in a run.
|
|
968
|
+
*
|
|
969
|
+
* @param run - The trace run to analyze
|
|
970
|
+
* @returns Object with counts for each visibility level
|
|
971
|
+
*/
|
|
972
|
+
export function countEventsByVisibility(run: TraceRun): Record<Visibility, number> {
|
|
973
|
+
const counts: Record<Visibility, number> = {
|
|
974
|
+
public: 0,
|
|
975
|
+
private: 0,
|
|
976
|
+
secret: 0,
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
for (const event of run.events) {
|
|
980
|
+
counts[event.visibility]++;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return counts;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Count spans by visibility level in a run.
|
|
988
|
+
*
|
|
989
|
+
* @param run - The trace run to analyze
|
|
990
|
+
* @returns Object with counts for each visibility level
|
|
991
|
+
*/
|
|
992
|
+
export function countSpansByVisibility(run: TraceRun): Record<Visibility, number> {
|
|
993
|
+
const counts: Record<Visibility, number> = {
|
|
994
|
+
public: 0,
|
|
995
|
+
private: 0,
|
|
996
|
+
secret: 0,
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
for (const span of run.spans) {
|
|
1000
|
+
counts[span.visibility]++;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return counts;
|
|
1004
|
+
}
|