@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/src/disclosure.ts CHANGED
@@ -1,527 +1,527 @@
1
- /**
2
- * @fileoverview Selective disclosure of trace spans with Merkle proofs.
3
- *
4
- * Location: packages/process-trace/src/disclosure.ts
5
- *
6
- * This module implements selective disclosure functionality for trace bundles,
7
- * enabling privacy-preserving audits and compliance workflows. It allows verifiers
8
- * to prove the existence of specific spans without revealing the entire trace.
9
- *
10
- * Key Concepts:
11
- * - Selective Disclosure: Reveal only specific spans from a trace bundle
12
- * - Membership Proofs: Prove a span exists without revealing its contents
13
- * - Full Disclosure: Prove existence AND reveal span data with events
14
- *
15
- * Disclosure Modes:
16
- * - "membership": Merkle proof only - proves span exists with specific hash
17
- * without exposing the actual span data. Useful for compliance checks.
18
- * - "full": Merkle proof + span data + event data - allows verifier to
19
- * recompute hashes and fully verify the span contents.
20
- *
21
- * Use Cases:
22
- * - Audit: "Show me span 3" (full mode) - auditor sees exactly what happened
23
- * - Compliance: "Prove span exists" (membership) - no data exposure
24
- * - Selective sharing: Disclose only public spans to external parties
25
- *
26
- * Used by:
27
- * - Audit workflows: Selective disclosure of trace spans
28
- * - Compliance verification: Prove span existence without data exposure
29
- * - API endpoints: Create and verify disclosure requests
30
- *
31
- * @example
32
- * ```typescript
33
- * // Full disclosure of specific spans
34
- * const result = await selectiveDisclose(bundle, ["span-1", "span-3"], "full");
35
- * for (const disclosed of result.disclosedSpans) {
36
- * console.log("Span:", disclosed.span?.name);
37
- * console.log("Events:", disclosed.events?.length);
38
- * }
39
- *
40
- * // Membership proof only (no data exposure)
41
- * const membershipResult = await selectiveDisclose(bundle, ["span-2"], "membership");
42
- *
43
- * // Verify disclosure against anchor
44
- * const verification = await verifyDisclosure(result, anchor.rootHash, anchor.merkleRoot);
45
- * if (!verification.valid) {
46
- * console.error("Verification failed:", verification.errors);
47
- * }
48
- * ```
49
- */
50
-
51
- import type {
52
- TraceBundle,
53
- TraceSpan,
54
- TraceEvent,
55
- DisclosureMode,
56
- DisclosureResult,
57
- MerkleProof,
58
- } from "./types.js";
59
- import {
60
- generateMerkleProof,
61
- verifyMerkleProof,
62
- buildSpanMerkleTree,
63
- computeSpanHash,
64
- } from "./merkle.js";
65
- import { HASH_DOMAIN_PREFIXES } from "./types.js";
66
- import { sha256StringHex } from "@fluxpointstudios/orynq-sdk-core/utils";
67
-
68
- // =============================================================================
69
- // DISCLOSURE REQUEST
70
- // =============================================================================
71
-
72
- /**
73
- * Disclosure request structure for API use.
74
- *
75
- * This interface defines the shape of a disclosure request that can be
76
- * transmitted over network APIs. It contains all the information needed
77
- * to identify the bundle and specify which spans to disclose.
78
- *
79
- * @property bundleRootHash - The root hash of the bundle for identification
80
- * @property bundleMerkleRoot - The Merkle root for verification
81
- * @property spanIds - Array of span IDs to disclose
82
- * @property mode - Disclosure mode (membership or full)
83
- */
84
- export interface DisclosureRequest {
85
- bundleRootHash: string;
86
- bundleMerkleRoot: string;
87
- spanIds: string[];
88
- mode: DisclosureMode;
89
- }
90
-
91
- // =============================================================================
92
- // HELPER FUNCTIONS
93
- // =============================================================================
94
-
95
- /**
96
- * Check if a span can be disclosed (exists in bundle).
97
- *
98
- * This function performs a simple existence check to determine if a span
99
- * with the given ID exists in the bundle. It searches the privateRun.spans
100
- * array for a matching span ID.
101
- *
102
- * @param bundle - The trace bundle to check
103
- * @param spanId - The ID of the span to look for
104
- * @returns true if the span exists in the bundle, false otherwise
105
- *
106
- * @example
107
- * ```typescript
108
- * if (canDisclose(bundle, "span-123")) {
109
- * const result = await selectiveDisclose(bundle, ["span-123"], "full");
110
- * } else {
111
- * console.error("Span not found in bundle");
112
- * }
113
- * ```
114
- */
115
- export function canDisclose(bundle: TraceBundle, spanId: string): boolean {
116
- return bundle.privateRun.spans.some((span) => span.id === spanId);
117
- }
118
-
119
- /**
120
- * Get span index by ID (needed for proof generation).
121
- *
122
- * Returns the index of a span in the sorted spans array (sorted by spanSeq).
123
- * This index is used for Merkle proof generation, as the proof depends on
124
- * the position of the span's leaf in the Merkle tree.
125
- *
126
- * @param bundle - The trace bundle containing the span
127
- * @param spanId - The ID of the span to find
128
- * @returns The 0-indexed position of the span in the sorted array
129
- * @throws Error if the span is not found in the bundle
130
- *
131
- * @example
132
- * ```typescript
133
- * const index = getSpanIndex(bundle, "span-456");
134
- * console.log(`Span is at index ${index} in the Merkle tree`);
135
- * ```
136
- */
137
- export function getSpanIndex(bundle: TraceBundle, spanId: string): number {
138
- // Sort spans by spanSeq for consistent ordering (matches Merkle tree order)
139
- const sortedSpans = [...bundle.privateRun.spans].sort(
140
- (a, b) => a.spanSeq - b.spanSeq
141
- );
142
-
143
- const index = sortedSpans.findIndex((span) => span.id === spanId);
144
-
145
- if (index === -1) {
146
- throw new Error(
147
- `Span with ID "${spanId}" not found in bundle. ` +
148
- `Available span IDs: ${sortedSpans.map((s) => s.id).join(", ")}`
149
- );
150
- }
151
-
152
- return index;
153
- }
154
-
155
- /**
156
- * Get events belonging to a specific span from the bundle.
157
- *
158
- * @param bundle - The trace bundle
159
- * @param span - The span to get events for
160
- * @returns Array of events sorted by seq
161
- */
162
- function getSpanEventsFromBundle(
163
- bundle: TraceBundle,
164
- span: TraceSpan
165
- ): TraceEvent[] {
166
- // Create event lookup map
167
- const eventMap = new Map<string, TraceEvent>();
168
- for (const event of bundle.privateRun.events) {
169
- eventMap.set(event.id, event);
170
- }
171
-
172
- // Get events for this span and sort by seq
173
- return span.eventIds
174
- .map((id) => eventMap.get(id))
175
- .filter((e): e is TraceEvent => e !== undefined)
176
- .sort((a, b) => a.seq - b.seq);
177
- }
178
-
179
- // =============================================================================
180
- // DISCLOSURE REQUEST CREATION
181
- // =============================================================================
182
-
183
- /**
184
- * Create a disclosure request (for API use).
185
- *
186
- * This function creates a structured disclosure request object that can be
187
- * serialized and transmitted over network APIs. The request contains all
188
- * information needed to identify the bundle and specify which spans to disclose.
189
- *
190
- * @param bundle - The trace bundle to create a request for
191
- * @param spanIds - Array of span IDs to request disclosure for
192
- * @param mode - The disclosure mode (membership or full)
193
- * @returns A DisclosureRequest object ready for transmission
194
- *
195
- * @example
196
- * ```typescript
197
- * const request = createDisclosureRequest(
198
- * bundle,
199
- * ["span-1", "span-3"],
200
- * "full"
201
- * );
202
- *
203
- * // Send request to disclosure service
204
- * const response = await fetch("/api/disclose", {
205
- * method: "POST",
206
- * body: JSON.stringify(request),
207
- * });
208
- * ```
209
- */
210
- export function createDisclosureRequest(
211
- bundle: TraceBundle,
212
- spanIds: string[],
213
- mode: DisclosureMode
214
- ): DisclosureRequest {
215
- return {
216
- bundleRootHash: bundle.rootHash,
217
- bundleMerkleRoot: bundle.merkleRoot,
218
- spanIds: [...spanIds], // Create a copy to prevent external mutation
219
- mode,
220
- };
221
- }
222
-
223
- // =============================================================================
224
- // SELECTIVE DISCLOSURE
225
- // =============================================================================
226
-
227
- /**
228
- * Selectively disclose specific spans from a bundle.
229
- *
230
- * This function generates disclosure results for the specified spans. Depending
231
- * on the disclosure mode, it includes either just Merkle proofs (membership mode)
232
- * or Merkle proofs plus full span and event data (full mode).
233
- *
234
- * The function validates that all requested spans exist in the bundle before
235
- * proceeding with disclosure generation.
236
- *
237
- * @param bundle - The trace bundle containing all data
238
- * @param spanIds - IDs of spans to disclose
239
- * @param mode - Disclosure mode:
240
- * - "membership": Merkle proof only (proves span exists with hash)
241
- * - "full": Merkle proof + span data + event data
242
- * @returns Promise resolving to DisclosureResult with proofs and optionally data
243
- * @throws Error if any requested spanId does not exist in the bundle
244
- *
245
- * @example
246
- * ```typescript
247
- * // Full disclosure - includes span data and events
248
- * const fullResult = await selectiveDisclose(bundle, ["span-1"], "full");
249
- * console.log(fullResult.disclosedSpans[0].span?.name);
250
- * console.log(fullResult.disclosedSpans[0].events?.length);
251
- *
252
- * // Membership disclosure - proof only, no data
253
- * const membershipResult = await selectiveDisclose(bundle, ["span-1"], "membership");
254
- * // membershipResult.disclosedSpans[0].span is undefined
255
- * // membershipResult.disclosedSpans[0].events is undefined
256
- * ```
257
- */
258
- export async function selectiveDisclose(
259
- bundle: TraceBundle,
260
- spanIds: string[],
261
- mode: DisclosureMode
262
- ): Promise<DisclosureResult> {
263
- // Validate all spanIds exist in bundle
264
- const missingSpanIds: string[] = [];
265
- for (const spanId of spanIds) {
266
- if (!canDisclose(bundle, spanId)) {
267
- missingSpanIds.push(spanId);
268
- }
269
- }
270
-
271
- if (missingSpanIds.length > 0) {
272
- throw new Error(
273
- `Cannot disclose spans that do not exist in bundle: ${missingSpanIds.join(", ")}`
274
- );
275
- }
276
-
277
- // Build the Merkle tree for proof generation
278
- const merkleTree = await buildSpanMerkleTree(
279
- bundle.privateRun.spans,
280
- bundle.privateRun.events
281
- );
282
-
283
- // Sort spans by spanSeq for consistent indexing
284
- const sortedSpans = [...bundle.privateRun.spans].sort(
285
- (a, b) => a.spanSeq - b.spanSeq
286
- );
287
-
288
- // Create a map for quick span lookup
289
- const spanMap = new Map<string, TraceSpan>();
290
- for (const span of sortedSpans) {
291
- spanMap.set(span.id, span);
292
- }
293
-
294
- // Generate disclosures for each requested span
295
- const disclosedSpans: DisclosureResult["disclosedSpans"] = [];
296
-
297
- for (const spanId of spanIds) {
298
- // Get the span and its index
299
- const span = spanMap.get(spanId);
300
- if (!span) {
301
- // This should not happen since we validated above, but handle defensively
302
- throw new Error(`Span "${spanId}" not found after validation`);
303
- }
304
-
305
- const spanIndex = getSpanIndex(bundle, spanId);
306
-
307
- // Generate Merkle proof
308
- const proof = generateMerkleProof(merkleTree, spanIndex);
309
-
310
- // Build the disclosed span entry
311
- if (mode === "full") {
312
- // Full mode: include span data and events
313
- const events = getSpanEventsFromBundle(bundle, span);
314
-
315
- disclosedSpans.push({
316
- spanId,
317
- proof,
318
- span: { ...span }, // Clone to prevent external mutation
319
- events: events.map((e) => ({ ...e })), // Clone events
320
- });
321
- } else {
322
- // Membership mode: proof only, no data
323
- disclosedSpans.push({
324
- spanId,
325
- proof,
326
- // span and events are undefined in membership mode
327
- });
328
- }
329
- }
330
-
331
- return {
332
- mode,
333
- rootHash: bundle.rootHash,
334
- merkleRoot: bundle.merkleRoot,
335
- disclosedSpans,
336
- };
337
- }
338
-
339
- // =============================================================================
340
- // VERIFICATION FUNCTIONS
341
- // =============================================================================
342
-
343
- /**
344
- * Verify a disclosure result against expected hashes.
345
- *
346
- * This function performs comprehensive verification of a disclosure result:
347
- * 1. Checks that rootHash matches the expected value from the anchor
348
- * 2. Checks that merkleRoot matches the expected value from the anchor
349
- * 3. For each disclosed span, verifies the Merkle proof
350
- * 4. For full mode disclosures, verifies span hash recomputation
351
- *
352
- * @param disclosure - The disclosure result to verify
353
- * @param expectedRootHash - Expected root hash from anchor/on-chain commitment
354
- * @param expectedMerkleRoot - Expected Merkle root from anchor/on-chain commitment
355
- * @returns Promise resolving to verification result with validity status and errors
356
- *
357
- * @example
358
- * ```typescript
359
- * const disclosure = await selectiveDisclose(bundle, ["span-1"], "full");
360
- * const verification = await verifyDisclosure(
361
- * disclosure,
362
- * anchor.rootHash,
363
- * anchor.merkleRoot
364
- * );
365
- *
366
- * if (verification.valid) {
367
- * console.log("Disclosure verified successfully");
368
- * } else {
369
- * console.error("Verification failed:", verification.errors);
370
- * }
371
- * ```
372
- */
373
- export async function verifyDisclosure(
374
- disclosure: DisclosureResult,
375
- expectedRootHash: string,
376
- expectedMerkleRoot: string
377
- ): Promise<{ valid: boolean; errors: string[] }> {
378
- const errors: string[] = [];
379
-
380
- // Check rootHash matches expected
381
- if (disclosure.rootHash !== expectedRootHash) {
382
- errors.push(
383
- `Root hash mismatch: disclosure has "${disclosure.rootHash}", ` +
384
- `expected "${expectedRootHash}"`
385
- );
386
- }
387
-
388
- // Check merkleRoot matches expected
389
- if (disclosure.merkleRoot !== expectedMerkleRoot) {
390
- errors.push(
391
- `Merkle root mismatch: disclosure has "${disclosure.merkleRoot}", ` +
392
- `expected "${expectedMerkleRoot}"`
393
- );
394
- }
395
-
396
- // Verify each disclosed span
397
- for (const disclosed of disclosure.disclosedSpans) {
398
- // Verify the Merkle proof for this span
399
- const proofValid = await verifyMerkleProof(disclosed.proof);
400
-
401
- if (!proofValid) {
402
- errors.push(
403
- `Merkle proof verification failed for span "${disclosed.spanId}"`
404
- );
405
- continue; // Skip further checks for this span
406
- }
407
-
408
- // Verify proof rootHash matches expected
409
- if (disclosed.proof.rootHash !== expectedMerkleRoot) {
410
- errors.push(
411
- `Proof root hash mismatch for span "${disclosed.spanId}": ` +
412
- `proof has "${disclosed.proof.rootHash}", expected "${expectedMerkleRoot}"`
413
- );
414
- }
415
-
416
- // For full mode with data, verify span hash recomputation
417
- if (disclosure.mode === "full" && disclosed.span && disclosed.events) {
418
- const spanVerification = await verifySpanDisclosure(
419
- {
420
- spanId: disclosed.spanId,
421
- proof: disclosed.proof,
422
- span: disclosed.span,
423
- events: disclosed.events,
424
- },
425
- expectedMerkleRoot
426
- );
427
-
428
- if (!spanVerification.valid) {
429
- errors.push(...spanVerification.errors);
430
- }
431
- }
432
- }
433
-
434
- return {
435
- valid: errors.length === 0,
436
- errors,
437
- };
438
- }
439
-
440
- /**
441
- * Verify a single span's disclosure (with data).
442
- *
443
- * This function performs detailed verification of a disclosed span:
444
- * 1. Recomputes the span hash from the provided span and events
445
- * 2. Computes the expected leaf hash from the span hash
446
- * 3. Verifies the leaf hash matches the proof's leafHash
447
- * 4. Verifies the Merkle proof is valid
448
- *
449
- * This is used for "full" mode disclosures where span data is provided.
450
- *
451
- * @param disclosed - Object containing spanId, proof, span data, and events
452
- * @param expectedMerkleRoot - Expected Merkle root from anchor/on-chain commitment
453
- * @returns Promise resolving to verification result with validity status and errors
454
- *
455
- * @example
456
- * ```typescript
457
- * const result = await verifySpanDisclosure(
458
- * {
459
- * spanId: "span-123",
460
- * proof: merkleProof,
461
- * span: spanData,
462
- * events: spanEvents,
463
- * },
464
- * expectedMerkleRoot
465
- * );
466
- *
467
- * if (result.valid) {
468
- * console.log("Span data is authentic and included in the trace");
469
- * }
470
- * ```
471
- */
472
- export async function verifySpanDisclosure(
473
- disclosed: {
474
- spanId: string;
475
- proof: MerkleProof;
476
- span: TraceSpan;
477
- events: TraceEvent[];
478
- },
479
- expectedMerkleRoot: string
480
- ): Promise<{ valid: boolean; errors: string[] }> {
481
- const errors: string[] = [];
482
-
483
- // Sort events by seq for deterministic hash computation
484
- const sortedEvents = [...disclosed.events].sort((a, b) => a.seq - b.seq);
485
-
486
- // Get event hashes in seq order
487
- const eventHashes = sortedEvents.map((e) => e.hash ?? "");
488
-
489
- // Recompute span hash from span + events
490
- const computedSpanHash = await computeSpanHash(disclosed.span, eventHashes);
491
-
492
- // Compute expected leaf hash: H("poi-trace:leaf:v1|" + spanHash)
493
- const computedLeafHash = await sha256StringHex(
494
- HASH_DOMAIN_PREFIXES.leaf + computedSpanHash
495
- );
496
-
497
- // Verify leaf hash matches proof.leafHash
498
- if (computedLeafHash !== disclosed.proof.leafHash) {
499
- errors.push(
500
- `Span hash verification failed for "${disclosed.spanId}": ` +
501
- `computed leaf hash "${computedLeafHash}" does not match ` +
502
- `proof leaf hash "${disclosed.proof.leafHash}". ` +
503
- `The span data may have been modified.`
504
- );
505
- }
506
-
507
- // Verify Merkle proof
508
- const proofValid = await verifyMerkleProof(disclosed.proof);
509
- if (!proofValid) {
510
- errors.push(
511
- `Merkle proof verification failed for span "${disclosed.spanId}"`
512
- );
513
- }
514
-
515
- // Verify proof rootHash matches expected
516
- if (disclosed.proof.rootHash !== expectedMerkleRoot) {
517
- errors.push(
518
- `Proof root hash mismatch for span "${disclosed.spanId}": ` +
519
- `proof has "${disclosed.proof.rootHash}", expected "${expectedMerkleRoot}"`
520
- );
521
- }
522
-
523
- return {
524
- valid: errors.length === 0,
525
- errors,
526
- };
527
- }
1
+ /**
2
+ * @fileoverview Selective disclosure of trace spans with Merkle proofs.
3
+ *
4
+ * Location: packages/process-trace/src/disclosure.ts
5
+ *
6
+ * This module implements selective disclosure functionality for trace bundles,
7
+ * enabling privacy-preserving audits and compliance workflows. It allows verifiers
8
+ * to prove the existence of specific spans without revealing the entire trace.
9
+ *
10
+ * Key Concepts:
11
+ * - Selective Disclosure: Reveal only specific spans from a trace bundle
12
+ * - Membership Proofs: Prove a span exists without revealing its contents
13
+ * - Full Disclosure: Prove existence AND reveal span data with events
14
+ *
15
+ * Disclosure Modes:
16
+ * - "membership": Merkle proof only - proves span exists with specific hash
17
+ * without exposing the actual span data. Useful for compliance checks.
18
+ * - "full": Merkle proof + span data + event data - allows verifier to
19
+ * recompute hashes and fully verify the span contents.
20
+ *
21
+ * Use Cases:
22
+ * - Audit: "Show me span 3" (full mode) - auditor sees exactly what happened
23
+ * - Compliance: "Prove span exists" (membership) - no data exposure
24
+ * - Selective sharing: Disclose only public spans to external parties
25
+ *
26
+ * Used by:
27
+ * - Audit workflows: Selective disclosure of trace spans
28
+ * - Compliance verification: Prove span existence without data exposure
29
+ * - API endpoints: Create and verify disclosure requests
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Full disclosure of specific spans
34
+ * const result = await selectiveDisclose(bundle, ["span-1", "span-3"], "full");
35
+ * for (const disclosed of result.disclosedSpans) {
36
+ * console.log("Span:", disclosed.span?.name);
37
+ * console.log("Events:", disclosed.events?.length);
38
+ * }
39
+ *
40
+ * // Membership proof only (no data exposure)
41
+ * const membershipResult = await selectiveDisclose(bundle, ["span-2"], "membership");
42
+ *
43
+ * // Verify disclosure against anchor
44
+ * const verification = await verifyDisclosure(result, anchor.rootHash, anchor.merkleRoot);
45
+ * if (!verification.valid) {
46
+ * console.error("Verification failed:", verification.errors);
47
+ * }
48
+ * ```
49
+ */
50
+
51
+ import type {
52
+ TraceBundle,
53
+ TraceSpan,
54
+ TraceEvent,
55
+ DisclosureMode,
56
+ DisclosureResult,
57
+ MerkleProof,
58
+ } from "./types.js";
59
+ import {
60
+ generateMerkleProof,
61
+ verifyMerkleProof,
62
+ buildSpanMerkleTree,
63
+ computeSpanHash,
64
+ } from "./merkle.js";
65
+ import { HASH_DOMAIN_PREFIXES } from "./types.js";
66
+ import { sha256StringHex } from "@fluxpointstudios/orynq-sdk-core/utils";
67
+
68
+ // =============================================================================
69
+ // DISCLOSURE REQUEST
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Disclosure request structure for API use.
74
+ *
75
+ * This interface defines the shape of a disclosure request that can be
76
+ * transmitted over network APIs. It contains all the information needed
77
+ * to identify the bundle and specify which spans to disclose.
78
+ *
79
+ * @property bundleRootHash - The root hash of the bundle for identification
80
+ * @property bundleMerkleRoot - The Merkle root for verification
81
+ * @property spanIds - Array of span IDs to disclose
82
+ * @property mode - Disclosure mode (membership or full)
83
+ */
84
+ export interface DisclosureRequest {
85
+ bundleRootHash: string;
86
+ bundleMerkleRoot: string;
87
+ spanIds: string[];
88
+ mode: DisclosureMode;
89
+ }
90
+
91
+ // =============================================================================
92
+ // HELPER FUNCTIONS
93
+ // =============================================================================
94
+
95
+ /**
96
+ * Check if a span can be disclosed (exists in bundle).
97
+ *
98
+ * This function performs a simple existence check to determine if a span
99
+ * with the given ID exists in the bundle. It searches the privateRun.spans
100
+ * array for a matching span ID.
101
+ *
102
+ * @param bundle - The trace bundle to check
103
+ * @param spanId - The ID of the span to look for
104
+ * @returns true if the span exists in the bundle, false otherwise
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * if (canDisclose(bundle, "span-123")) {
109
+ * const result = await selectiveDisclose(bundle, ["span-123"], "full");
110
+ * } else {
111
+ * console.error("Span not found in bundle");
112
+ * }
113
+ * ```
114
+ */
115
+ export function canDisclose(bundle: TraceBundle, spanId: string): boolean {
116
+ return bundle.privateRun.spans.some((span) => span.id === spanId);
117
+ }
118
+
119
+ /**
120
+ * Get span index by ID (needed for proof generation).
121
+ *
122
+ * Returns the index of a span in the sorted spans array (sorted by spanSeq).
123
+ * This index is used for Merkle proof generation, as the proof depends on
124
+ * the position of the span's leaf in the Merkle tree.
125
+ *
126
+ * @param bundle - The trace bundle containing the span
127
+ * @param spanId - The ID of the span to find
128
+ * @returns The 0-indexed position of the span in the sorted array
129
+ * @throws Error if the span is not found in the bundle
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const index = getSpanIndex(bundle, "span-456");
134
+ * console.log(`Span is at index ${index} in the Merkle tree`);
135
+ * ```
136
+ */
137
+ export function getSpanIndex(bundle: TraceBundle, spanId: string): number {
138
+ // Sort spans by spanSeq for consistent ordering (matches Merkle tree order)
139
+ const sortedSpans = [...bundle.privateRun.spans].sort(
140
+ (a, b) => a.spanSeq - b.spanSeq
141
+ );
142
+
143
+ const index = sortedSpans.findIndex((span) => span.id === spanId);
144
+
145
+ if (index === -1) {
146
+ throw new Error(
147
+ `Span with ID "${spanId}" not found in bundle. ` +
148
+ `Available span IDs: ${sortedSpans.map((s) => s.id).join(", ")}`
149
+ );
150
+ }
151
+
152
+ return index;
153
+ }
154
+
155
+ /**
156
+ * Get events belonging to a specific span from the bundle.
157
+ *
158
+ * @param bundle - The trace bundle
159
+ * @param span - The span to get events for
160
+ * @returns Array of events sorted by seq
161
+ */
162
+ function getSpanEventsFromBundle(
163
+ bundle: TraceBundle,
164
+ span: TraceSpan
165
+ ): TraceEvent[] {
166
+ // Create event lookup map
167
+ const eventMap = new Map<string, TraceEvent>();
168
+ for (const event of bundle.privateRun.events) {
169
+ eventMap.set(event.id, event);
170
+ }
171
+
172
+ // Get events for this span and sort by seq
173
+ return span.eventIds
174
+ .map((id) => eventMap.get(id))
175
+ .filter((e): e is TraceEvent => e !== undefined)
176
+ .sort((a, b) => a.seq - b.seq);
177
+ }
178
+
179
+ // =============================================================================
180
+ // DISCLOSURE REQUEST CREATION
181
+ // =============================================================================
182
+
183
+ /**
184
+ * Create a disclosure request (for API use).
185
+ *
186
+ * This function creates a structured disclosure request object that can be
187
+ * serialized and transmitted over network APIs. The request contains all
188
+ * information needed to identify the bundle and specify which spans to disclose.
189
+ *
190
+ * @param bundle - The trace bundle to create a request for
191
+ * @param spanIds - Array of span IDs to request disclosure for
192
+ * @param mode - The disclosure mode (membership or full)
193
+ * @returns A DisclosureRequest object ready for transmission
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * const request = createDisclosureRequest(
198
+ * bundle,
199
+ * ["span-1", "span-3"],
200
+ * "full"
201
+ * );
202
+ *
203
+ * // Send request to disclosure service
204
+ * const response = await fetch("/api/disclose", {
205
+ * method: "POST",
206
+ * body: JSON.stringify(request),
207
+ * });
208
+ * ```
209
+ */
210
+ export function createDisclosureRequest(
211
+ bundle: TraceBundle,
212
+ spanIds: string[],
213
+ mode: DisclosureMode
214
+ ): DisclosureRequest {
215
+ return {
216
+ bundleRootHash: bundle.rootHash,
217
+ bundleMerkleRoot: bundle.merkleRoot,
218
+ spanIds: [...spanIds], // Create a copy to prevent external mutation
219
+ mode,
220
+ };
221
+ }
222
+
223
+ // =============================================================================
224
+ // SELECTIVE DISCLOSURE
225
+ // =============================================================================
226
+
227
+ /**
228
+ * Selectively disclose specific spans from a bundle.
229
+ *
230
+ * This function generates disclosure results for the specified spans. Depending
231
+ * on the disclosure mode, it includes either just Merkle proofs (membership mode)
232
+ * or Merkle proofs plus full span and event data (full mode).
233
+ *
234
+ * The function validates that all requested spans exist in the bundle before
235
+ * proceeding with disclosure generation.
236
+ *
237
+ * @param bundle - The trace bundle containing all data
238
+ * @param spanIds - IDs of spans to disclose
239
+ * @param mode - Disclosure mode:
240
+ * - "membership": Merkle proof only (proves span exists with hash)
241
+ * - "full": Merkle proof + span data + event data
242
+ * @returns Promise resolving to DisclosureResult with proofs and optionally data
243
+ * @throws Error if any requested spanId does not exist in the bundle
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * // Full disclosure - includes span data and events
248
+ * const fullResult = await selectiveDisclose(bundle, ["span-1"], "full");
249
+ * console.log(fullResult.disclosedSpans[0].span?.name);
250
+ * console.log(fullResult.disclosedSpans[0].events?.length);
251
+ *
252
+ * // Membership disclosure - proof only, no data
253
+ * const membershipResult = await selectiveDisclose(bundle, ["span-1"], "membership");
254
+ * // membershipResult.disclosedSpans[0].span is undefined
255
+ * // membershipResult.disclosedSpans[0].events is undefined
256
+ * ```
257
+ */
258
+ export async function selectiveDisclose(
259
+ bundle: TraceBundle,
260
+ spanIds: string[],
261
+ mode: DisclosureMode
262
+ ): Promise<DisclosureResult> {
263
+ // Validate all spanIds exist in bundle
264
+ const missingSpanIds: string[] = [];
265
+ for (const spanId of spanIds) {
266
+ if (!canDisclose(bundle, spanId)) {
267
+ missingSpanIds.push(spanId);
268
+ }
269
+ }
270
+
271
+ if (missingSpanIds.length > 0) {
272
+ throw new Error(
273
+ `Cannot disclose spans that do not exist in bundle: ${missingSpanIds.join(", ")}`
274
+ );
275
+ }
276
+
277
+ // Build the Merkle tree for proof generation
278
+ const merkleTree = await buildSpanMerkleTree(
279
+ bundle.privateRun.spans,
280
+ bundle.privateRun.events
281
+ );
282
+
283
+ // Sort spans by spanSeq for consistent indexing
284
+ const sortedSpans = [...bundle.privateRun.spans].sort(
285
+ (a, b) => a.spanSeq - b.spanSeq
286
+ );
287
+
288
+ // Create a map for quick span lookup
289
+ const spanMap = new Map<string, TraceSpan>();
290
+ for (const span of sortedSpans) {
291
+ spanMap.set(span.id, span);
292
+ }
293
+
294
+ // Generate disclosures for each requested span
295
+ const disclosedSpans: DisclosureResult["disclosedSpans"] = [];
296
+
297
+ for (const spanId of spanIds) {
298
+ // Get the span and its index
299
+ const span = spanMap.get(spanId);
300
+ if (!span) {
301
+ // This should not happen since we validated above, but handle defensively
302
+ throw new Error(`Span "${spanId}" not found after validation`);
303
+ }
304
+
305
+ const spanIndex = getSpanIndex(bundle, spanId);
306
+
307
+ // Generate Merkle proof
308
+ const proof = generateMerkleProof(merkleTree, spanIndex);
309
+
310
+ // Build the disclosed span entry
311
+ if (mode === "full") {
312
+ // Full mode: include span data and events
313
+ const events = getSpanEventsFromBundle(bundle, span);
314
+
315
+ disclosedSpans.push({
316
+ spanId,
317
+ proof,
318
+ span: { ...span }, // Clone to prevent external mutation
319
+ events: events.map((e) => ({ ...e })), // Clone events
320
+ });
321
+ } else {
322
+ // Membership mode: proof only, no data
323
+ disclosedSpans.push({
324
+ spanId,
325
+ proof,
326
+ // span and events are undefined in membership mode
327
+ });
328
+ }
329
+ }
330
+
331
+ return {
332
+ mode,
333
+ rootHash: bundle.rootHash,
334
+ merkleRoot: bundle.merkleRoot,
335
+ disclosedSpans,
336
+ };
337
+ }
338
+
339
+ // =============================================================================
340
+ // VERIFICATION FUNCTIONS
341
+ // =============================================================================
342
+
343
+ /**
344
+ * Verify a disclosure result against expected hashes.
345
+ *
346
+ * This function performs comprehensive verification of a disclosure result:
347
+ * 1. Checks that rootHash matches the expected value from the anchor
348
+ * 2. Checks that merkleRoot matches the expected value from the anchor
349
+ * 3. For each disclosed span, verifies the Merkle proof
350
+ * 4. For full mode disclosures, verifies span hash recomputation
351
+ *
352
+ * @param disclosure - The disclosure result to verify
353
+ * @param expectedRootHash - Expected root hash from anchor/on-chain commitment
354
+ * @param expectedMerkleRoot - Expected Merkle root from anchor/on-chain commitment
355
+ * @returns Promise resolving to verification result with validity status and errors
356
+ *
357
+ * @example
358
+ * ```typescript
359
+ * const disclosure = await selectiveDisclose(bundle, ["span-1"], "full");
360
+ * const verification = await verifyDisclosure(
361
+ * disclosure,
362
+ * anchor.rootHash,
363
+ * anchor.merkleRoot
364
+ * );
365
+ *
366
+ * if (verification.valid) {
367
+ * console.log("Disclosure verified successfully");
368
+ * } else {
369
+ * console.error("Verification failed:", verification.errors);
370
+ * }
371
+ * ```
372
+ */
373
+ export async function verifyDisclosure(
374
+ disclosure: DisclosureResult,
375
+ expectedRootHash: string,
376
+ expectedMerkleRoot: string
377
+ ): Promise<{ valid: boolean; errors: string[] }> {
378
+ const errors: string[] = [];
379
+
380
+ // Check rootHash matches expected
381
+ if (disclosure.rootHash !== expectedRootHash) {
382
+ errors.push(
383
+ `Root hash mismatch: disclosure has "${disclosure.rootHash}", ` +
384
+ `expected "${expectedRootHash}"`
385
+ );
386
+ }
387
+
388
+ // Check merkleRoot matches expected
389
+ if (disclosure.merkleRoot !== expectedMerkleRoot) {
390
+ errors.push(
391
+ `Merkle root mismatch: disclosure has "${disclosure.merkleRoot}", ` +
392
+ `expected "${expectedMerkleRoot}"`
393
+ );
394
+ }
395
+
396
+ // Verify each disclosed span
397
+ for (const disclosed of disclosure.disclosedSpans) {
398
+ // Verify the Merkle proof for this span
399
+ const proofValid = await verifyMerkleProof(disclosed.proof);
400
+
401
+ if (!proofValid) {
402
+ errors.push(
403
+ `Merkle proof verification failed for span "${disclosed.spanId}"`
404
+ );
405
+ continue; // Skip further checks for this span
406
+ }
407
+
408
+ // Verify proof rootHash matches expected
409
+ if (disclosed.proof.rootHash !== expectedMerkleRoot) {
410
+ errors.push(
411
+ `Proof root hash mismatch for span "${disclosed.spanId}": ` +
412
+ `proof has "${disclosed.proof.rootHash}", expected "${expectedMerkleRoot}"`
413
+ );
414
+ }
415
+
416
+ // For full mode with data, verify span hash recomputation
417
+ if (disclosure.mode === "full" && disclosed.span && disclosed.events) {
418
+ const spanVerification = await verifySpanDisclosure(
419
+ {
420
+ spanId: disclosed.spanId,
421
+ proof: disclosed.proof,
422
+ span: disclosed.span,
423
+ events: disclosed.events,
424
+ },
425
+ expectedMerkleRoot
426
+ );
427
+
428
+ if (!spanVerification.valid) {
429
+ errors.push(...spanVerification.errors);
430
+ }
431
+ }
432
+ }
433
+
434
+ return {
435
+ valid: errors.length === 0,
436
+ errors,
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Verify a single span's disclosure (with data).
442
+ *
443
+ * This function performs detailed verification of a disclosed span:
444
+ * 1. Recomputes the span hash from the provided span and events
445
+ * 2. Computes the expected leaf hash from the span hash
446
+ * 3. Verifies the leaf hash matches the proof's leafHash
447
+ * 4. Verifies the Merkle proof is valid
448
+ *
449
+ * This is used for "full" mode disclosures where span data is provided.
450
+ *
451
+ * @param disclosed - Object containing spanId, proof, span data, and events
452
+ * @param expectedMerkleRoot - Expected Merkle root from anchor/on-chain commitment
453
+ * @returns Promise resolving to verification result with validity status and errors
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * const result = await verifySpanDisclosure(
458
+ * {
459
+ * spanId: "span-123",
460
+ * proof: merkleProof,
461
+ * span: spanData,
462
+ * events: spanEvents,
463
+ * },
464
+ * expectedMerkleRoot
465
+ * );
466
+ *
467
+ * if (result.valid) {
468
+ * console.log("Span data is authentic and included in the trace");
469
+ * }
470
+ * ```
471
+ */
472
+ export async function verifySpanDisclosure(
473
+ disclosed: {
474
+ spanId: string;
475
+ proof: MerkleProof;
476
+ span: TraceSpan;
477
+ events: TraceEvent[];
478
+ },
479
+ expectedMerkleRoot: string
480
+ ): Promise<{ valid: boolean; errors: string[] }> {
481
+ const errors: string[] = [];
482
+
483
+ // Sort events by seq for deterministic hash computation
484
+ const sortedEvents = [...disclosed.events].sort((a, b) => a.seq - b.seq);
485
+
486
+ // Get event hashes in seq order
487
+ const eventHashes = sortedEvents.map((e) => e.hash ?? "");
488
+
489
+ // Recompute span hash from span + events
490
+ const computedSpanHash = await computeSpanHash(disclosed.span, eventHashes);
491
+
492
+ // Compute expected leaf hash: H("poi-trace:leaf:v1|" + spanHash)
493
+ const computedLeafHash = await sha256StringHex(
494
+ HASH_DOMAIN_PREFIXES.leaf + computedSpanHash
495
+ );
496
+
497
+ // Verify leaf hash matches proof.leafHash
498
+ if (computedLeafHash !== disclosed.proof.leafHash) {
499
+ errors.push(
500
+ `Span hash verification failed for "${disclosed.spanId}": ` +
501
+ `computed leaf hash "${computedLeafHash}" does not match ` +
502
+ `proof leaf hash "${disclosed.proof.leafHash}". ` +
503
+ `The span data may have been modified.`
504
+ );
505
+ }
506
+
507
+ // Verify Merkle proof
508
+ const proofValid = await verifyMerkleProof(disclosed.proof);
509
+ if (!proofValid) {
510
+ errors.push(
511
+ `Merkle proof verification failed for span "${disclosed.spanId}"`
512
+ );
513
+ }
514
+
515
+ // Verify proof rootHash matches expected
516
+ if (disclosed.proof.rootHash !== expectedMerkleRoot) {
517
+ errors.push(
518
+ `Proof root hash mismatch for span "${disclosed.spanId}": ` +
519
+ `proof has "${disclosed.proof.rootHash}", expected "${expectedMerkleRoot}"`
520
+ );
521
+ }
522
+
523
+ return {
524
+ valid: errors.length === 0,
525
+ errors,
526
+ };
527
+ }