@peac/audit 0.10.9 → 0.10.11

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/dist/index.mjs ADDED
@@ -0,0 +1,1533 @@
1
+ import { createHash } from 'crypto';
2
+ import { posix } from 'path';
3
+ import { canonicalize, verify, CryptoError } from '@peac/crypto';
4
+ import { BUNDLE_ERRORS } from '@peac/kernel';
5
+ import * as yazl from 'yazl';
6
+ import * as yauzl from 'yauzl';
7
+
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+
15
+ // src/entry.ts
16
+ var AUDIT_VERSION = "peac.audit/0.9";
17
+ var AUDIT_EVENT_TYPES = [
18
+ "receipt_issued",
19
+ "receipt_verified",
20
+ "receipt_denied",
21
+ "access_decision",
22
+ "dispute_filed",
23
+ "dispute_acknowledged",
24
+ "dispute_resolved",
25
+ "dispute_rejected",
26
+ "dispute_appealed",
27
+ "dispute_final",
28
+ "attribution_created",
29
+ "attribution_verified",
30
+ "identity_verified",
31
+ "identity_rejected",
32
+ "policy_evaluated"
33
+ ];
34
+ var AUDIT_SEVERITIES = [
35
+ "info",
36
+ "warn",
37
+ "error",
38
+ "critical"
39
+ ];
40
+ function generateAuditId() {
41
+ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
42
+ const now = Date.now();
43
+ let timestampPart = "";
44
+ let time = now;
45
+ for (let i = 0; i < 10; i++) {
46
+ timestampPart = ALPHABET[time % 32] + timestampPart;
47
+ time = Math.floor(time / 32);
48
+ }
49
+ let randomPart = "";
50
+ for (let i = 0; i < 16; i++) {
51
+ randomPart += ALPHABET[Math.floor(Math.random() * 32)];
52
+ }
53
+ return timestampPart + randomPart;
54
+ }
55
+ function isValidUlid(id) {
56
+ return /^[0-9A-HJKMNP-TV-Z]{26}$/.test(id);
57
+ }
58
+ function isValidTraceContext(trace) {
59
+ if (!/^[0-9a-f]{32}$/i.test(trace.trace_id)) {
60
+ return false;
61
+ }
62
+ if (!/^[0-9a-f]{16}$/i.test(trace.span_id)) {
63
+ return false;
64
+ }
65
+ if (trace.parent_span_id && !/^[0-9a-f]{16}$/i.test(trace.parent_span_id)) {
66
+ return false;
67
+ }
68
+ if (trace.trace_flags && !/^[0-9a-f]{2}$/i.test(trace.trace_flags)) {
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+ function createAuditEntry(options) {
74
+ const entry = {
75
+ version: AUDIT_VERSION,
76
+ id: options.id ?? generateAuditId(),
77
+ event_type: options.event_type,
78
+ timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
79
+ severity: options.severity ?? "info",
80
+ actor: options.actor,
81
+ resource: options.resource,
82
+ outcome: options.outcome
83
+ };
84
+ if (options.trace) {
85
+ entry.trace = options.trace;
86
+ }
87
+ if (options.context) {
88
+ entry.context = options.context;
89
+ }
90
+ if (options.dispute_ref) {
91
+ entry.dispute_ref = options.dispute_ref;
92
+ }
93
+ return entry;
94
+ }
95
+ function validateAuditEntry(entry) {
96
+ const errors = [];
97
+ if (!entry || typeof entry !== "object") {
98
+ return { valid: false, errors: ["Entry must be an object"] };
99
+ }
100
+ const e = entry;
101
+ if (e.version !== AUDIT_VERSION) {
102
+ errors.push(`Invalid version: expected "${AUDIT_VERSION}", got "${e.version}"`);
103
+ }
104
+ if (typeof e.id !== "string" || !isValidUlid(e.id)) {
105
+ errors.push("Invalid or missing id (must be ULID format)");
106
+ }
107
+ if (!AUDIT_EVENT_TYPES.includes(e.event_type)) {
108
+ errors.push(`Invalid event_type: "${e.event_type}"`);
109
+ }
110
+ if (typeof e.timestamp !== "string" || isNaN(Date.parse(e.timestamp))) {
111
+ errors.push("Invalid or missing timestamp (must be ISO 8601)");
112
+ }
113
+ if (!AUDIT_SEVERITIES.includes(e.severity)) {
114
+ errors.push(`Invalid severity: "${e.severity}"`);
115
+ }
116
+ if (!e.actor || typeof e.actor !== "object") {
117
+ errors.push("Missing or invalid actor");
118
+ } else {
119
+ const actor = e.actor;
120
+ if (!["user", "agent", "system"].includes(actor.type)) {
121
+ errors.push(`Invalid actor.type: "${actor.type}"`);
122
+ }
123
+ if (typeof actor.id !== "string" || actor.id.length === 0) {
124
+ errors.push("Actor must have non-empty id");
125
+ }
126
+ }
127
+ if (!e.resource || typeof e.resource !== "object") {
128
+ errors.push("Missing or invalid resource");
129
+ } else {
130
+ const resource = e.resource;
131
+ const validTypes = ["receipt", "attribution", "identity", "policy", "dispute", "content"];
132
+ if (!validTypes.includes(resource.type)) {
133
+ errors.push(`Invalid resource.type: "${resource.type}"`);
134
+ }
135
+ if (typeof resource.id !== "string" || resource.id.length === 0) {
136
+ errors.push("Resource must have non-empty id");
137
+ }
138
+ }
139
+ if (!e.outcome || typeof e.outcome !== "object") {
140
+ errors.push("Missing or invalid outcome");
141
+ } else {
142
+ const outcome = e.outcome;
143
+ if (typeof outcome.success !== "boolean") {
144
+ errors.push("Outcome must have boolean success field");
145
+ }
146
+ }
147
+ if (e.trace) {
148
+ if (!isValidTraceContext(e.trace)) {
149
+ errors.push("Invalid trace context format");
150
+ }
151
+ }
152
+ if (e.dispute_ref) {
153
+ if (typeof e.dispute_ref !== "string" || !isValidUlid(e.dispute_ref)) {
154
+ errors.push("Invalid dispute_ref (must be ULID format)");
155
+ }
156
+ }
157
+ return {
158
+ valid: errors.length === 0,
159
+ errors
160
+ };
161
+ }
162
+ function isValidAuditEntry(entry) {
163
+ return validateAuditEntry(entry).valid;
164
+ }
165
+
166
+ // src/jsonl.ts
167
+ function formatJsonlLine(entry, options) {
168
+ if (options?.pretty) {
169
+ return JSON.stringify(entry, null, 2);
170
+ }
171
+ return JSON.stringify(entry);
172
+ }
173
+ function formatJsonl(entries, options) {
174
+ const lines = entries.map((entry) => formatJsonlLine(entry, { pretty: false }));
175
+ const result = lines.join("\n");
176
+ if (options?.trailingNewline && result.length > 0) {
177
+ return result + "\n";
178
+ }
179
+ return result;
180
+ }
181
+ function parseJsonlLine(line, lineNumber = 1) {
182
+ const trimmed = line.trim();
183
+ if (trimmed.length === 0) {
184
+ return {
185
+ ok: false,
186
+ error: "Empty line",
187
+ lineNumber
188
+ };
189
+ }
190
+ try {
191
+ const parsed = JSON.parse(trimmed);
192
+ if (!isValidAuditEntry(parsed)) {
193
+ return {
194
+ ok: false,
195
+ error: "Invalid audit entry structure",
196
+ lineNumber,
197
+ raw: trimmed.length > 100 ? trimmed.substring(0, 100) + "..." : trimmed
198
+ };
199
+ }
200
+ return {
201
+ ok: true,
202
+ entry: parsed,
203
+ lineNumber
204
+ };
205
+ } catch (e) {
206
+ return {
207
+ ok: false,
208
+ error: e instanceof Error ? e.message : "JSON parse error",
209
+ lineNumber,
210
+ raw: trimmed.length > 100 ? trimmed.substring(0, 100) + "..." : trimmed
211
+ };
212
+ }
213
+ }
214
+ function parseJsonl(content, options) {
215
+ const lines = content.split("\n");
216
+ const entries = [];
217
+ const errors = [];
218
+ const maxLines = options?.maxLines ?? 0;
219
+ let processed = 0;
220
+ for (let i = 0; i < lines.length; i++) {
221
+ const line = lines[i];
222
+ const lineNumber = i + 1;
223
+ if (line.trim().length === 0) {
224
+ continue;
225
+ }
226
+ if (maxLines > 0 && processed >= maxLines) {
227
+ break;
228
+ }
229
+ processed++;
230
+ const result = parseJsonlLine(line, lineNumber);
231
+ if (result.ok) {
232
+ entries.push(result.entry);
233
+ } else {
234
+ if (options?.skipInvalid) {
235
+ errors.push(result);
236
+ } else {
237
+ return {
238
+ entries,
239
+ errors: [result],
240
+ totalLines: processed,
241
+ successCount: entries.length,
242
+ errorCount: 1
243
+ };
244
+ }
245
+ }
246
+ }
247
+ return {
248
+ entries,
249
+ errors,
250
+ totalLines: processed,
251
+ successCount: entries.length,
252
+ errorCount: errors.length
253
+ };
254
+ }
255
+ function createJsonlAppender(options) {
256
+ return (entry) => {
257
+ const line = formatJsonlLine(entry, { pretty: false });
258
+ return line + "\n";
259
+ };
260
+ }
261
+
262
+ // src/bundle.ts
263
+ var BUNDLE_VERSION = "peac.bundle/0.9";
264
+ function createCaseBundle(options) {
265
+ const sortedEntries = [...options.entries].sort(
266
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
267
+ );
268
+ const traceIds = /* @__PURE__ */ new Set();
269
+ for (const entry of sortedEntries) {
270
+ if (entry.trace?.trace_id) {
271
+ traceIds.add(entry.trace.trace_id);
272
+ }
273
+ }
274
+ const summary = generateBundleSummary(sortedEntries);
275
+ return {
276
+ version: BUNDLE_VERSION,
277
+ dispute_ref: options.dispute_ref,
278
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
279
+ generated_by: options.generated_by,
280
+ entries: sortedEntries,
281
+ trace_ids: Array.from(traceIds),
282
+ summary
283
+ };
284
+ }
285
+ function generateBundleSummary(entries) {
286
+ const byEventType = {};
287
+ for (const entry of entries) {
288
+ byEventType[entry.event_type] = (byEventType[entry.event_type] ?? 0) + 1;
289
+ }
290
+ const bySeverity = {
291
+ info: 0,
292
+ warn: 0,
293
+ error: 0,
294
+ critical: 0
295
+ };
296
+ for (const entry of entries) {
297
+ bySeverity[entry.severity]++;
298
+ }
299
+ const actors = /* @__PURE__ */ new Set();
300
+ const resources = /* @__PURE__ */ new Set();
301
+ for (const entry of entries) {
302
+ actors.add(`${entry.actor.type}:${entry.actor.id}`);
303
+ resources.add(`${entry.resource.type}:${entry.resource.id}`);
304
+ }
305
+ const firstEvent = entries.length > 0 ? entries[0].timestamp : "";
306
+ const lastEvent = entries.length > 0 ? entries[entries.length - 1].timestamp : "";
307
+ return {
308
+ entry_count: entries.length,
309
+ by_event_type: byEventType,
310
+ by_severity: bySeverity,
311
+ first_event: firstEvent,
312
+ last_event: lastEvent,
313
+ actor_count: actors.size,
314
+ resource_count: resources.size
315
+ };
316
+ }
317
+ function filterByDispute(entries, disputeRef) {
318
+ return entries.filter((entry) => entry.dispute_ref === disputeRef);
319
+ }
320
+ function filterByTraceId(entries, traceId) {
321
+ return entries.filter((entry) => entry.trace?.trace_id === traceId);
322
+ }
323
+ function filterByTimeRange(entries, start, end) {
324
+ const startTime = new Date(start).getTime();
325
+ const endTime = new Date(end).getTime();
326
+ return entries.filter((entry) => {
327
+ const entryTime = new Date(entry.timestamp).getTime();
328
+ return entryTime >= startTime && entryTime <= endTime;
329
+ });
330
+ }
331
+ function filterByResource(entries, resourceType, resourceId) {
332
+ return entries.filter((entry) => {
333
+ if (entry.resource.type !== resourceType) {
334
+ return false;
335
+ }
336
+ if (resourceId && entry.resource.id !== resourceId) {
337
+ return false;
338
+ }
339
+ return true;
340
+ });
341
+ }
342
+ function correlateByTrace(entries) {
343
+ const byTrace = /* @__PURE__ */ new Map();
344
+ for (const entry of entries) {
345
+ if (entry.trace?.trace_id) {
346
+ const existing = byTrace.get(entry.trace.trace_id) ?? [];
347
+ existing.push(entry);
348
+ byTrace.set(entry.trace.trace_id, existing);
349
+ }
350
+ }
351
+ const correlations = [];
352
+ for (const [traceId, traceEntries] of byTrace) {
353
+ const sorted = [...traceEntries].sort(
354
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
355
+ );
356
+ const spanIds = /* @__PURE__ */ new Set();
357
+ for (const entry of sorted) {
358
+ if (entry.trace?.span_id) {
359
+ spanIds.add(entry.trace.span_id);
360
+ }
361
+ }
362
+ const firstTime = new Date(sorted[0].timestamp).getTime();
363
+ const lastTime = new Date(sorted[sorted.length - 1].timestamp).getTime();
364
+ correlations.push({
365
+ trace_id: traceId,
366
+ entries: sorted,
367
+ span_ids: Array.from(spanIds),
368
+ duration_ms: lastTime - firstTime
369
+ });
370
+ }
371
+ return correlations;
372
+ }
373
+ function serializeBundle(bundle, pretty = false) {
374
+ if (pretty) {
375
+ return JSON.stringify(bundle, null, 2);
376
+ }
377
+ return JSON.stringify(bundle);
378
+ }
379
+
380
+ // src/dispute-bundle-types.ts
381
+ var BUNDLE_VERSION2 = "peac-bundle/0.1";
382
+ var DISPUTE_BUNDLE_VERSION = BUNDLE_VERSION2;
383
+ var VERIFICATION_REPORT_VERSION = "peac-verification-report/0.1";
384
+ var MAX_ZIP_ENTRIES = 1e4;
385
+ var MAX_ENTRY_SIZE = 64 * 1024 * 1024;
386
+ var MAX_TOTAL_SIZE = 512 * 1024 * 1024;
387
+ var MAX_RECEIPTS = 1e4;
388
+ var ALLOWED_PATHS = ["manifest.json", "bundle.sig", "receipts.ndjson", "keys/", "policy/"];
389
+ var BundleErrorCodes = BUNDLE_ERRORS;
390
+ var ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
391
+ function generateBundleId() {
392
+ const timestamp = Date.now();
393
+ const timestampChars = [];
394
+ let ts = timestamp;
395
+ for (let i = 9; i >= 0; i--) {
396
+ timestampChars[i] = ULID_ALPHABET[ts % 32];
397
+ ts = Math.floor(ts / 32);
398
+ }
399
+ const { randomBytes: cryptoRandomBytes } = __require("crypto");
400
+ const randBytes = cryptoRandomBytes(10);
401
+ const randomChars = [];
402
+ let randomValue = BigInt(0);
403
+ for (let i = 0; i < 10; i++) {
404
+ randomValue = randomValue << BigInt(8) | BigInt(randBytes[i]);
405
+ }
406
+ for (let i = 15; i >= 0; i--) {
407
+ randomChars[i] = ULID_ALPHABET[Number(randomValue & BigInt(31))];
408
+ randomValue = randomValue >> BigInt(5);
409
+ }
410
+ return timestampChars.join("") + randomChars.join("");
411
+ }
412
+ function sha256Hex(data) {
413
+ const hash = createHash("sha256");
414
+ hash.update(data);
415
+ return `sha256:${hash.digest("hex")}`;
416
+ }
417
+ function base64urlDecode(str) {
418
+ const padded = str + "=".repeat((4 - str.length % 4) % 4);
419
+ const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
420
+ return Buffer.from(base64, "base64");
421
+ }
422
+ function parseJws(jws) {
423
+ const parts = jws.split(".");
424
+ if (parts.length !== 3) {
425
+ return null;
426
+ }
427
+ try {
428
+ const headerJson = base64urlDecode(parts[0]).toString("utf8");
429
+ const payloadJson = base64urlDecode(parts[1]).toString("utf8");
430
+ return {
431
+ header: JSON.parse(headerJson),
432
+ payload: JSON.parse(payloadJson),
433
+ signature: base64urlDecode(parts[2]),
434
+ signingInput: `${parts[0]}.${parts[1]}`
435
+ };
436
+ } catch {
437
+ return null;
438
+ }
439
+ }
440
+ function bundleError(code, message, details) {
441
+ return { code, message, details };
442
+ }
443
+ function handleZipError(zipErr) {
444
+ const isPathError = zipErr.message.includes("invalid relative path") || zipErr.message.includes("absolute path") || zipErr.message.includes("..") || zipErr.message.includes("\\");
445
+ if (isPathError) {
446
+ return bundleError(BundleErrorCodes.PATH_TRAVERSAL, `Unsafe path in bundle: ${zipErr.message}`);
447
+ }
448
+ return bundleError(BundleErrorCodes.INVALID_FORMAT, `ZIP error: ${zipErr.message}`);
449
+ }
450
+ var VIRTUAL_ROOT = "/bundle";
451
+ function isPathSafe(entryPath) {
452
+ if (entryPath.includes("\\")) return false;
453
+ if (entryPath.includes("\0")) return false;
454
+ const normalized = posix.normalize(entryPath);
455
+ if (normalized.startsWith("/")) return false;
456
+ if (normalized.startsWith("..")) return false;
457
+ if (normalized === ".") return false;
458
+ const resolved = posix.resolve(VIRTUAL_ROOT, normalized);
459
+ if (!resolved.startsWith(VIRTUAL_ROOT + "/") && resolved !== VIRTUAL_ROOT) {
460
+ return false;
461
+ }
462
+ return ALLOWED_PATHS.some((prefix) => normalized === prefix || normalized.startsWith(prefix));
463
+ }
464
+ async function createDisputeBundle(options) {
465
+ const {
466
+ kind = "dispute",
467
+ refs,
468
+ dispute_ref,
469
+ created_by,
470
+ receipts,
471
+ keys,
472
+ policy,
473
+ peac_txt,
474
+ bundle_id,
475
+ created_at,
476
+ signing_key,
477
+ signing_kid
478
+ } = options;
479
+ const bundleRefs = refs ?? (dispute_ref ? [{ type: "dispute", id: dispute_ref }] : []);
480
+ if (receipts.length === 0) {
481
+ return {
482
+ ok: false,
483
+ error: bundleError(BundleErrorCodes.MISSING_RECEIPTS, "No receipts provided")
484
+ };
485
+ }
486
+ if (receipts.length > MAX_RECEIPTS) {
487
+ return {
488
+ ok: false,
489
+ error: bundleError(
490
+ BundleErrorCodes.SIZE_EXCEEDED,
491
+ `Too many receipts: ${receipts.length} > ${MAX_RECEIPTS}`
492
+ )
493
+ };
494
+ }
495
+ if (keys.keys.length === 0) {
496
+ return {
497
+ ok: false,
498
+ error: bundleError(BundleErrorCodes.MISSING_KEYS, "No keys provided in JWKS")
499
+ };
500
+ }
501
+ const receiptEntries = [];
502
+ const seenReceiptIds = /* @__PURE__ */ new Set();
503
+ const ndjsonLines = [];
504
+ let minIssuedAt;
505
+ let maxIssuedAt;
506
+ for (let i = 0; i < receipts.length; i++) {
507
+ const jws = receipts[i];
508
+ const parsed = parseJws(jws);
509
+ if (!parsed) {
510
+ return {
511
+ ok: false,
512
+ error: bundleError(BundleErrorCodes.RECEIPT_INVALID, `Invalid JWS at index ${i}`)
513
+ };
514
+ }
515
+ const claims = parsed.payload;
516
+ const receiptId = claims.jti;
517
+ const issuedAtRaw = claims.iat;
518
+ if (!receiptId) {
519
+ return {
520
+ ok: false,
521
+ error: bundleError(
522
+ BundleErrorCodes.RECEIPT_INVALID,
523
+ `Receipt at index ${i} missing jti claim`
524
+ )
525
+ };
526
+ }
527
+ if (seenReceiptIds.has(receiptId)) {
528
+ return {
529
+ ok: false,
530
+ error: bundleError(
531
+ BundleErrorCodes.DUPLICATE_RECEIPT,
532
+ `Duplicate receipt ID: ${receiptId}`
533
+ )
534
+ };
535
+ }
536
+ seenReceiptIds.add(receiptId);
537
+ let issuedAt;
538
+ if (typeof issuedAtRaw === "number") {
539
+ issuedAt = new Date(issuedAtRaw * 1e3).toISOString();
540
+ } else if (typeof issuedAtRaw === "string") {
541
+ issuedAt = issuedAtRaw;
542
+ } else {
543
+ return {
544
+ ok: false,
545
+ error: bundleError(
546
+ BundleErrorCodes.RECEIPT_INVALID,
547
+ `Receipt ${receiptId} missing or invalid iat claim`
548
+ )
549
+ };
550
+ }
551
+ if (!minIssuedAt || issuedAt < minIssuedAt) minIssuedAt = issuedAt;
552
+ if (!maxIssuedAt || issuedAt > maxIssuedAt) maxIssuedAt = issuedAt;
553
+ const receiptHash = sha256Hex(Buffer.from(jws, "utf8"));
554
+ receiptEntries.push({
555
+ receipt_id: receiptId,
556
+ issued_at: issuedAt,
557
+ receipt_hash: receiptHash
558
+ });
559
+ ndjsonLines.push(jws);
560
+ }
561
+ const sortedIndices = receiptEntries.map((entry, i) => ({ entry, i })).sort((a, b) => {
562
+ if (a.entry.issued_at !== b.entry.issued_at) {
563
+ return a.entry.issued_at.localeCompare(b.entry.issued_at);
564
+ }
565
+ if (a.entry.receipt_id !== b.entry.receipt_id) {
566
+ return a.entry.receipt_id.localeCompare(b.entry.receipt_id);
567
+ }
568
+ return a.entry.receipt_hash.localeCompare(b.entry.receipt_hash);
569
+ });
570
+ const sortedReceiptEntries = sortedIndices.map((x) => x.entry);
571
+ const sortedNdjsonLines = sortedIndices.map((x) => ndjsonLines[x.i]);
572
+ const receiptsNdjson = sortedNdjsonLines.join("\n") + "\n";
573
+ const receiptsNdjsonBytes = Buffer.from(receiptsNdjson, "utf8");
574
+ const keyEntries = keys.keys.map((key) => ({
575
+ kid: key.kid,
576
+ alg: key.alg ?? "EdDSA"
577
+ }));
578
+ keyEntries.sort((a, b) => a.kid.localeCompare(b.kid));
579
+ const keysJson = JSON.stringify(keys, null, 2);
580
+ const keysBytes = Buffer.from(keysJson, "utf8");
581
+ const fileEntries = [
582
+ {
583
+ path: "receipts.ndjson",
584
+ sha256: sha256Hex(receiptsNdjsonBytes),
585
+ size: receiptsNdjsonBytes.length
586
+ },
587
+ {
588
+ path: "keys/keys.json",
589
+ sha256: sha256Hex(keysBytes),
590
+ size: keysBytes.length
591
+ }
592
+ ];
593
+ let policyHash;
594
+ let policyBytes;
595
+ if (policy) {
596
+ policyBytes = Buffer.from(policy, "utf8");
597
+ policyHash = sha256Hex(policyBytes);
598
+ fileEntries.push({
599
+ path: "policy/policy.yaml",
600
+ sha256: policyHash,
601
+ size: policyBytes.length
602
+ });
603
+ }
604
+ let peacTxtBytes;
605
+ let peacTxtHash;
606
+ if (peac_txt) {
607
+ peacTxtBytes = Buffer.from(peac_txt, "utf8");
608
+ peacTxtHash = sha256Hex(peacTxtBytes);
609
+ fileEntries.push({
610
+ path: "policy/peac.txt",
611
+ sha256: peacTxtHash,
612
+ size: peacTxtBytes.length
613
+ });
614
+ }
615
+ fileEntries.sort((a, b) => a.path.localeCompare(b.path));
616
+ const timeRange = {
617
+ start: minIssuedAt,
618
+ end: maxIssuedAt
619
+ };
620
+ const manifestWithoutHash = {
621
+ version: BUNDLE_VERSION2,
622
+ kind,
623
+ bundle_id: bundle_id ?? generateBundleId(),
624
+ refs: bundleRefs,
625
+ // Include deprecated dispute_ref for backwards compatibility
626
+ dispute_ref,
627
+ created_by,
628
+ created_at: created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
629
+ time_range: timeRange,
630
+ receipts: sortedReceiptEntries,
631
+ keys: keyEntries,
632
+ files: fileEntries
633
+ };
634
+ if (policyHash) {
635
+ manifestWithoutHash.policy_hash = policyHash;
636
+ }
637
+ if (peacTxtHash) {
638
+ manifestWithoutHash.peac_txt_hash = peacTxtHash;
639
+ }
640
+ const contentHash = sha256Hex(canonicalize(manifestWithoutHash));
641
+ const manifest = {
642
+ ...manifestWithoutHash,
643
+ content_hash: contentHash
644
+ };
645
+ const zipfile = new yazl.ZipFile();
646
+ const mtime = new Date(manifest.created_at);
647
+ const zipOptions = { mtime, compress: false };
648
+ const manifestJson = JSON.stringify(manifest, null, 2);
649
+ zipfile.addBuffer(Buffer.from(manifestJson), "manifest.json", zipOptions);
650
+ zipfile.addBuffer(receiptsNdjsonBytes, "receipts.ndjson", zipOptions);
651
+ zipfile.addBuffer(keysBytes, "keys/keys.json", zipOptions);
652
+ if (policyBytes) {
653
+ zipfile.addBuffer(policyBytes, "policy/policy.yaml", zipOptions);
654
+ }
655
+ if (peacTxtBytes) {
656
+ zipfile.addBuffer(peacTxtBytes, "policy/peac.txt", zipOptions);
657
+ }
658
+ if (signing_key && signing_kid) {
659
+ const sigResult = await createBundleSignature(contentHash, signing_key, signing_kid);
660
+ if (!sigResult.ok) {
661
+ return sigResult;
662
+ }
663
+ zipfile.addBuffer(Buffer.from(sigResult.value), "bundle.sig", zipOptions);
664
+ }
665
+ return new Promise((resolve) => {
666
+ const chunks = [];
667
+ zipfile.outputStream.on("data", (chunk) => chunks.push(chunk)).on("end", () => {
668
+ resolve({ ok: true, value: Buffer.concat(chunks) });
669
+ }).on("error", (err) => {
670
+ resolve({
671
+ ok: false,
672
+ error: bundleError(
673
+ BundleErrorCodes.INVALID_FORMAT,
674
+ `Failed to create ZIP: ${err.message}`
675
+ )
676
+ });
677
+ });
678
+ zipfile.end();
679
+ });
680
+ }
681
+ async function createBundleSignature(contentHash, privateKey, kid) {
682
+ try {
683
+ const { sign } = await import('@peac/crypto');
684
+ const jws = await sign({ content_hash: contentHash }, privateKey, kid);
685
+ return { ok: true, value: jws };
686
+ } catch (err) {
687
+ return {
688
+ ok: false,
689
+ error: bundleError(
690
+ BundleErrorCodes.SIGNATURE_INVALID,
691
+ `Failed to create bundle signature: ${err.message}`
692
+ )
693
+ };
694
+ }
695
+ }
696
+ async function readDisputeBundle(zipBuffer) {
697
+ return new Promise((resolve) => {
698
+ yauzl.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => {
699
+ if (err || !zipfile) {
700
+ resolve({
701
+ ok: false,
702
+ error: bundleError(
703
+ BundleErrorCodes.INVALID_FORMAT,
704
+ `Failed to open ZIP: ${err?.message ?? "unknown error"}`
705
+ )
706
+ });
707
+ return;
708
+ }
709
+ const files = /* @__PURE__ */ new Map();
710
+ let entryCount = 0;
711
+ let totalSize = 0;
712
+ let actualTotalBytes = 0;
713
+ zipfile.on("entry", (entry) => {
714
+ if (/\/$/.test(entry.fileName)) {
715
+ zipfile.readEntry();
716
+ return;
717
+ }
718
+ entryCount++;
719
+ if (entryCount > MAX_ZIP_ENTRIES) {
720
+ zipfile.close();
721
+ resolve({
722
+ ok: false,
723
+ error: bundleError(
724
+ BundleErrorCodes.SIZE_EXCEEDED,
725
+ `Too many ZIP entries: > ${MAX_ZIP_ENTRIES}`
726
+ )
727
+ });
728
+ return;
729
+ }
730
+ if (!isPathSafe(entry.fileName)) {
731
+ zipfile.close();
732
+ resolve({
733
+ ok: false,
734
+ error: bundleError(
735
+ BundleErrorCodes.PATH_TRAVERSAL,
736
+ `Unsafe path in bundle: ${entry.fileName}`
737
+ )
738
+ });
739
+ return;
740
+ }
741
+ if (entry.uncompressedSize > MAX_ENTRY_SIZE) {
742
+ zipfile.close();
743
+ resolve({
744
+ ok: false,
745
+ error: bundleError(
746
+ BundleErrorCodes.SIZE_EXCEEDED,
747
+ `Entry too large: ${entry.fileName}`
748
+ )
749
+ });
750
+ return;
751
+ }
752
+ totalSize += entry.uncompressedSize;
753
+ if (totalSize > MAX_TOTAL_SIZE) {
754
+ zipfile.close();
755
+ resolve({
756
+ ok: false,
757
+ error: bundleError(
758
+ BundleErrorCodes.SIZE_EXCEEDED,
759
+ `Total size exceeded: > ${MAX_TOTAL_SIZE} bytes`
760
+ )
761
+ });
762
+ return;
763
+ }
764
+ zipfile.openReadStream(entry, (readErr, readStream) => {
765
+ if (readErr || !readStream) {
766
+ zipfile.close();
767
+ resolve({
768
+ ok: false,
769
+ error: bundleError(
770
+ BundleErrorCodes.INVALID_FORMAT,
771
+ `Failed to read ${entry.fileName}`
772
+ )
773
+ });
774
+ return;
775
+ }
776
+ const chunks = [];
777
+ let actualBytes = 0;
778
+ entry.uncompressedSize > 0 ? entry.uncompressedSize : MAX_ENTRY_SIZE;
779
+ readStream.on("data", (chunk) => {
780
+ actualBytes += chunk.length;
781
+ actualTotalBytes += chunk.length;
782
+ if (actualBytes > MAX_ENTRY_SIZE) {
783
+ readStream.destroy();
784
+ zipfile.close();
785
+ resolve({
786
+ ok: false,
787
+ error: bundleError(
788
+ BundleErrorCodes.SIZE_EXCEEDED,
789
+ `Entry exceeds size limit during decompression: ${entry.fileName}`,
790
+ { claimed: entry.uncompressedSize, actual: actualBytes, limit: MAX_ENTRY_SIZE }
791
+ )
792
+ });
793
+ return;
794
+ }
795
+ if (actualTotalBytes > MAX_TOTAL_SIZE) {
796
+ readStream.destroy();
797
+ zipfile.close();
798
+ resolve({
799
+ ok: false,
800
+ error: bundleError(
801
+ BundleErrorCodes.SIZE_EXCEEDED,
802
+ `Total decompressed size exceeds limit: ${actualTotalBytes} > ${MAX_TOTAL_SIZE}`,
803
+ { actual: actualTotalBytes, limit: MAX_TOTAL_SIZE }
804
+ )
805
+ });
806
+ return;
807
+ }
808
+ if (entry.uncompressedSize > 0 && actualBytes > entry.uncompressedSize * 2) {
809
+ readStream.destroy();
810
+ zipfile.close();
811
+ resolve({
812
+ ok: false,
813
+ error: bundleError(
814
+ BundleErrorCodes.SIZE_EXCEEDED,
815
+ `Entry decompressed size exceeds claimed size: ${entry.fileName}`,
816
+ { claimed: entry.uncompressedSize, actual: actualBytes }
817
+ )
818
+ });
819
+ return;
820
+ }
821
+ chunks.push(chunk);
822
+ });
823
+ readStream.on("end", () => {
824
+ files.set(entry.fileName, Buffer.concat(chunks));
825
+ zipfile.readEntry();
826
+ });
827
+ readStream.on("error", (streamErr) => {
828
+ zipfile.close();
829
+ resolve({
830
+ ok: false,
831
+ error: bundleError(
832
+ BundleErrorCodes.INVALID_FORMAT,
833
+ `Stream error: ${streamErr.message}`
834
+ )
835
+ });
836
+ });
837
+ });
838
+ });
839
+ zipfile.on("end", () => {
840
+ processExtractedFiles(files, resolve);
841
+ });
842
+ zipfile.on("error", (zipErr) => {
843
+ resolve({
844
+ ok: false,
845
+ error: handleZipError(zipErr)
846
+ });
847
+ });
848
+ zipfile.readEntry();
849
+ });
850
+ });
851
+ }
852
+ function processExtractedFiles(files, resolve) {
853
+ const manifestBuffer = files.get("manifest.json");
854
+ if (!manifestBuffer) {
855
+ resolve({
856
+ ok: false,
857
+ error: bundleError(BundleErrorCodes.MANIFEST_MISSING, "manifest.json not found in bundle")
858
+ });
859
+ return;
860
+ }
861
+ let manifest;
862
+ try {
863
+ manifest = JSON.parse(manifestBuffer.toString("utf8"));
864
+ } catch (parseErr) {
865
+ resolve({
866
+ ok: false,
867
+ error: bundleError(
868
+ BundleErrorCodes.MANIFEST_INVALID,
869
+ `Failed to parse manifest.json: ${parseErr.message}`
870
+ )
871
+ });
872
+ return;
873
+ }
874
+ if (manifest.version !== BUNDLE_VERSION2) {
875
+ resolve({
876
+ ok: false,
877
+ error: bundleError(
878
+ BundleErrorCodes.MANIFEST_INVALID,
879
+ `Unsupported bundle version: ${manifest.version}`,
880
+ { expected: BUNDLE_VERSION2, actual: manifest.version }
881
+ )
882
+ });
883
+ return;
884
+ }
885
+ const { content_hash, ...manifestWithoutHash } = manifest;
886
+ const computedHash = sha256Hex(canonicalize(manifestWithoutHash));
887
+ if (computedHash !== content_hash) {
888
+ resolve({
889
+ ok: false,
890
+ error: bundleError(
891
+ BundleErrorCodes.HASH_MISMATCH,
892
+ "Bundle content_hash verification failed",
893
+ { expected: content_hash, computed: computedHash }
894
+ )
895
+ });
896
+ return;
897
+ }
898
+ for (const fileEntry of manifest.files) {
899
+ const fileBuffer = files.get(fileEntry.path);
900
+ if (!fileBuffer) {
901
+ resolve({
902
+ ok: false,
903
+ error: bundleError(BundleErrorCodes.INVALID_FORMAT, `File not found: ${fileEntry.path}`)
904
+ });
905
+ return;
906
+ }
907
+ const computedFileHash = sha256Hex(fileBuffer);
908
+ if (computedFileHash !== fileEntry.sha256) {
909
+ resolve({
910
+ ok: false,
911
+ error: bundleError(
912
+ BundleErrorCodes.HASH_MISMATCH,
913
+ `File hash mismatch: ${fileEntry.path}`,
914
+ { expected: fileEntry.sha256, computed: computedFileHash }
915
+ )
916
+ });
917
+ return;
918
+ }
919
+ if (fileBuffer.length !== fileEntry.size) {
920
+ resolve({
921
+ ok: false,
922
+ error: bundleError(
923
+ BundleErrorCodes.HASH_MISMATCH,
924
+ `File size mismatch: ${fileEntry.path}`,
925
+ { expected: fileEntry.size, actual: fileBuffer.length }
926
+ )
927
+ });
928
+ return;
929
+ }
930
+ }
931
+ const receiptsBuffer = files.get("receipts.ndjson");
932
+ const receipts = /* @__PURE__ */ new Map();
933
+ if (receiptsBuffer) {
934
+ const lines = receiptsBuffer.toString("utf8").trim().split("\n");
935
+ let lastKey = "";
936
+ for (let i = 0; i < lines.length; i++) {
937
+ const jws = lines[i].trim();
938
+ if (!jws) continue;
939
+ const parsed = parseJws(jws);
940
+ if (!parsed) {
941
+ resolve({
942
+ ok: false,
943
+ error: bundleError(BundleErrorCodes.RECEIPT_INVALID, `Invalid JWS at line ${i + 1}`)
944
+ });
945
+ return;
946
+ }
947
+ const receiptId = parsed.payload.jti;
948
+ if (receipts.has(receiptId)) {
949
+ resolve({
950
+ ok: false,
951
+ error: bundleError(
952
+ BundleErrorCodes.DUPLICATE_RECEIPT,
953
+ `Duplicate receipt ID in bundle: ${receiptId}`,
954
+ { receipt_id: receiptId, line: i + 1 }
955
+ )
956
+ });
957
+ return;
958
+ }
959
+ const issuedAt = typeof parsed.payload.iat === "number" ? new Date(parsed.payload.iat * 1e3).toISOString() : String(parsed.payload.iat);
960
+ const receiptHash = sha256Hex(Buffer.from(jws, "utf8"));
961
+ const currentKey = `${issuedAt}|${receiptId}|${receiptHash}`;
962
+ if (currentKey < lastKey) {
963
+ resolve({
964
+ ok: false,
965
+ error: bundleError(
966
+ BundleErrorCodes.RECEIPTS_UNORDERED,
967
+ "receipts.ndjson is not in deterministic order"
968
+ )
969
+ });
970
+ return;
971
+ }
972
+ lastKey = currentKey;
973
+ receipts.set(receiptId, jws);
974
+ }
975
+ }
976
+ let keys = { keys: [] };
977
+ const keysBuffer = files.get("keys/keys.json");
978
+ if (keysBuffer) {
979
+ try {
980
+ keys = JSON.parse(keysBuffer.toString("utf8"));
981
+ } catch {
982
+ resolve({
983
+ ok: false,
984
+ error: bundleError(BundleErrorCodes.MANIFEST_INVALID, "Failed to parse keys/keys.json")
985
+ });
986
+ return;
987
+ }
988
+ }
989
+ let policyContent;
990
+ const policyBuffer = files.get("policy/policy.yaml");
991
+ if (policyBuffer) {
992
+ policyContent = policyBuffer.toString("utf8");
993
+ if (manifest.policy_hash) {
994
+ const computedPolicyHash = sha256Hex(policyBuffer);
995
+ if (computedPolicyHash !== manifest.policy_hash) {
996
+ resolve({
997
+ ok: false,
998
+ error: bundleError(BundleErrorCodes.POLICY_HASH_MISMATCH, "Policy hash mismatch", {
999
+ expected: manifest.policy_hash,
1000
+ computed: computedPolicyHash
1001
+ })
1002
+ });
1003
+ return;
1004
+ }
1005
+ }
1006
+ }
1007
+ let peacTxtContent;
1008
+ const peacTxtBuffer = files.get("policy/peac.txt");
1009
+ if (peacTxtBuffer) {
1010
+ peacTxtContent = peacTxtBuffer.toString("utf8");
1011
+ if (manifest.peac_txt_hash) {
1012
+ const computedPeacTxtHash = sha256Hex(peacTxtBuffer);
1013
+ if (computedPeacTxtHash !== manifest.peac_txt_hash) {
1014
+ resolve({
1015
+ ok: false,
1016
+ error: bundleError(BundleErrorCodes.POLICY_HASH_MISMATCH, "peac.txt hash mismatch", {
1017
+ expected: manifest.peac_txt_hash,
1018
+ computed: computedPeacTxtHash
1019
+ })
1020
+ });
1021
+ return;
1022
+ }
1023
+ }
1024
+ }
1025
+ let bundleSig;
1026
+ const sigBuffer = files.get("bundle.sig");
1027
+ if (sigBuffer) {
1028
+ bundleSig = sigBuffer.toString("utf8");
1029
+ }
1030
+ resolve({
1031
+ ok: true,
1032
+ value: {
1033
+ manifest,
1034
+ receipts,
1035
+ keys,
1036
+ policy: policyContent,
1037
+ peac_txt: peacTxtContent,
1038
+ bundle_sig: bundleSig
1039
+ }
1040
+ });
1041
+ }
1042
+ async function verifyBundleIntegrity(zipBuffer) {
1043
+ const result = await readDisputeBundle(zipBuffer);
1044
+ if (!result.ok) {
1045
+ return result;
1046
+ }
1047
+ return { ok: true, value: { manifest: result.value.manifest } };
1048
+ }
1049
+ async function getBundleContentHash(zipBuffer) {
1050
+ return new Promise((resolve) => {
1051
+ yauzl.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => {
1052
+ if (err || !zipfile) {
1053
+ resolve({
1054
+ ok: false,
1055
+ error: bundleError(
1056
+ BundleErrorCodes.INVALID_FORMAT,
1057
+ `Failed to open ZIP: ${err?.message ?? "unknown"}`
1058
+ )
1059
+ });
1060
+ return;
1061
+ }
1062
+ let found = false;
1063
+ zipfile.on("entry", (entry) => {
1064
+ if (entry.fileName === "manifest.json") {
1065
+ found = true;
1066
+ zipfile.openReadStream(entry, (readErr, readStream) => {
1067
+ if (readErr || !readStream) {
1068
+ zipfile.close();
1069
+ resolve({
1070
+ ok: false,
1071
+ error: bundleError(BundleErrorCodes.INVALID_FORMAT, `Failed to read manifest.json`)
1072
+ });
1073
+ return;
1074
+ }
1075
+ const chunks = [];
1076
+ readStream.on("data", (chunk) => chunks.push(chunk));
1077
+ readStream.on("end", () => {
1078
+ zipfile.close();
1079
+ try {
1080
+ const manifest = JSON.parse(
1081
+ Buffer.concat(chunks).toString("utf8")
1082
+ );
1083
+ resolve({ ok: true, value: manifest.content_hash });
1084
+ } catch (parseErr) {
1085
+ resolve({
1086
+ ok: false,
1087
+ error: bundleError(
1088
+ BundleErrorCodes.MANIFEST_INVALID,
1089
+ `Failed to parse manifest.json`
1090
+ )
1091
+ });
1092
+ }
1093
+ });
1094
+ });
1095
+ } else {
1096
+ zipfile.readEntry();
1097
+ }
1098
+ });
1099
+ zipfile.on("end", () => {
1100
+ if (!found) {
1101
+ resolve({
1102
+ ok: false,
1103
+ error: bundleError(BundleErrorCodes.MANIFEST_MISSING, "manifest.json not found")
1104
+ });
1105
+ }
1106
+ });
1107
+ zipfile.on("error", (zipErr) => {
1108
+ resolve({
1109
+ ok: false,
1110
+ error: handleZipError(zipErr)
1111
+ });
1112
+ });
1113
+ zipfile.readEntry();
1114
+ });
1115
+ });
1116
+ }
1117
+ function sha256Hex2(data) {
1118
+ const hash = createHash("sha256");
1119
+ hash.update(data);
1120
+ return `sha256:${hash.digest("hex")}`;
1121
+ }
1122
+ function base64urlDecode2(str) {
1123
+ const padded = str + "=".repeat((4 - str.length % 4) % 4);
1124
+ const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
1125
+ return Buffer.from(base64, "base64");
1126
+ }
1127
+ function parseJws2(jws) {
1128
+ const parts = jws.split(".");
1129
+ if (parts.length !== 3) {
1130
+ return null;
1131
+ }
1132
+ try {
1133
+ const headerJson = base64urlDecode2(parts[0]).toString("utf8");
1134
+ const payloadJson = base64urlDecode2(parts[1]).toString("utf8");
1135
+ return {
1136
+ header: JSON.parse(headerJson),
1137
+ payload: JSON.parse(payloadJson),
1138
+ signature: parts[2]
1139
+ };
1140
+ } catch {
1141
+ return null;
1142
+ }
1143
+ }
1144
+ function stripUndefined(obj) {
1145
+ if (obj === null || obj === void 0) {
1146
+ return obj;
1147
+ }
1148
+ if (Array.isArray(obj)) {
1149
+ return obj.map(stripUndefined);
1150
+ }
1151
+ if (typeof obj === "object") {
1152
+ const result = {};
1153
+ for (const [key, value] of Object.entries(obj)) {
1154
+ if (value !== void 0) {
1155
+ result[key] = stripUndefined(value);
1156
+ }
1157
+ }
1158
+ return result;
1159
+ }
1160
+ return obj;
1161
+ }
1162
+ function verifyReceiptClaims(payload, now) {
1163
+ const errors = [];
1164
+ const nowSec = Math.floor(now.getTime() / 1e3);
1165
+ if (!payload.jti) {
1166
+ errors.push("E_RECEIPT_MISSING_JTI");
1167
+ }
1168
+ if (!payload.iss) {
1169
+ errors.push("E_RECEIPT_MISSING_ISS");
1170
+ }
1171
+ if (payload.iat === void 0) {
1172
+ errors.push("E_RECEIPT_MISSING_IAT");
1173
+ } else {
1174
+ const iat = typeof payload.iat === "number" ? payload.iat : NaN;
1175
+ if (isNaN(iat)) {
1176
+ errors.push("E_RECEIPT_INVALID_IAT");
1177
+ } else if (iat > nowSec + 300) {
1178
+ errors.push("E_RECEIPT_NOT_YET_VALID");
1179
+ }
1180
+ }
1181
+ if (payload.exp !== void 0) {
1182
+ const exp = typeof payload.exp === "number" ? payload.exp : NaN;
1183
+ if (isNaN(exp)) {
1184
+ errors.push("E_RECEIPT_INVALID_EXP");
1185
+ } else if (exp < nowSec) {
1186
+ errors.push("E_RECEIPT_EXPIRED");
1187
+ }
1188
+ }
1189
+ return {
1190
+ valid: errors.length === 0,
1191
+ errors
1192
+ };
1193
+ }
1194
+ function getReceiptKeyId(header) {
1195
+ return typeof header.kid === "string" ? header.kid : void 0;
1196
+ }
1197
+ function jwkToPublicKeyBytes(jwk) {
1198
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) {
1199
+ return null;
1200
+ }
1201
+ const padded = jwk.x + "=".repeat((4 - jwk.x.length % 4) % 4);
1202
+ const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
1203
+ const bytes = Buffer.from(base64, "base64");
1204
+ if (bytes.length !== 32) {
1205
+ return null;
1206
+ }
1207
+ return new Uint8Array(bytes);
1208
+ }
1209
+ function findKey(keys, kid) {
1210
+ return keys.find((k) => k.kid === kid);
1211
+ }
1212
+ async function verifyReceipt(receiptId, jws, bundleContents, options) {
1213
+ const parsed = parseJws2(jws);
1214
+ if (!parsed) {
1215
+ return {
1216
+ receipt_id: receiptId,
1217
+ signature_valid: false,
1218
+ claims_valid: false,
1219
+ errors: ["E_RECEIPT_INVALID_FORMAT"]
1220
+ };
1221
+ }
1222
+ const { header, payload } = parsed;
1223
+ const keyId = getReceiptKeyId(header);
1224
+ const errors = [];
1225
+ if (!keyId) {
1226
+ return {
1227
+ receipt_id: receiptId,
1228
+ signature_valid: false,
1229
+ claims_valid: false,
1230
+ errors: ["E_RECEIPT_MISSING_KID"]
1231
+ };
1232
+ }
1233
+ const jwk = findKey(bundleContents.keys.keys, keyId);
1234
+ if (!jwk) {
1235
+ if (options.offline) {
1236
+ return {
1237
+ receipt_id: receiptId,
1238
+ signature_valid: false,
1239
+ claims_valid: false,
1240
+ key_id: keyId,
1241
+ errors: [BundleErrorCodes.KEY_MISSING]
1242
+ };
1243
+ }
1244
+ return {
1245
+ receipt_id: receiptId,
1246
+ signature_valid: false,
1247
+ claims_valid: false,
1248
+ key_id: keyId,
1249
+ errors: [BundleErrorCodes.KEY_MISSING]
1250
+ };
1251
+ }
1252
+ const publicKeyBytes = jwkToPublicKeyBytes(jwk);
1253
+ if (!publicKeyBytes) {
1254
+ return {
1255
+ receipt_id: receiptId,
1256
+ signature_valid: false,
1257
+ claims_valid: false,
1258
+ key_id: keyId,
1259
+ errors: ["E_RECEIPT_INVALID_KEY_FORMAT"]
1260
+ };
1261
+ }
1262
+ let signatureValid = false;
1263
+ try {
1264
+ const result = await verify(jws, publicKeyBytes);
1265
+ signatureValid = result.valid;
1266
+ if (!signatureValid) {
1267
+ errors.push("E_RECEIPT_SIGNATURE_INVALID");
1268
+ }
1269
+ } catch (err) {
1270
+ if (err instanceof CryptoError) {
1271
+ errors.push(`E_RECEIPT_CRYPTO_ERROR:${err.code}`);
1272
+ } else {
1273
+ errors.push("E_RECEIPT_SIGNATURE_INVALID");
1274
+ }
1275
+ signatureValid = false;
1276
+ }
1277
+ const claimsResult = verifyReceiptClaims(payload, options.now ?? /* @__PURE__ */ new Date());
1278
+ errors.push(...claimsResult.errors);
1279
+ return {
1280
+ receipt_id: receiptId,
1281
+ signature_valid: signatureValid,
1282
+ claims_valid: claimsResult.valid,
1283
+ key_id: keyId,
1284
+ errors,
1285
+ claims: signatureValid && claimsResult.valid && errors.length === 0 ? payload : void 0
1286
+ };
1287
+ }
1288
+ function buildKeyUsage(results) {
1289
+ const usage = /* @__PURE__ */ new Map();
1290
+ for (const result of results) {
1291
+ if (result.key_id) {
1292
+ const existing = usage.get(result.key_id) ?? [];
1293
+ existing.push(result.receipt_id);
1294
+ usage.set(result.key_id, existing);
1295
+ }
1296
+ }
1297
+ const entries = [];
1298
+ for (const [kid, receiptIds] of usage) {
1299
+ entries.push({
1300
+ kid,
1301
+ receipts_signed: receiptIds.length,
1302
+ receipt_ids: receiptIds.sort()
1303
+ });
1304
+ }
1305
+ return entries.sort((a, b) => a.kid.localeCompare(b.kid));
1306
+ }
1307
+ function generateAuditorSummary(totalReceipts, validCount, invalidCount, results) {
1308
+ const headline = `${validCount}/${totalReceipts} receipts valid`;
1309
+ const issues = [];
1310
+ for (const result of results) {
1311
+ if (!result.signature_valid || !result.claims_valid) {
1312
+ const errorSummary = result.errors.length > 0 ? result.errors.join(", ") : "validation failed";
1313
+ issues.push(`Receipt ${result.receipt_id}: ${errorSummary}`);
1314
+ }
1315
+ }
1316
+ issues.sort();
1317
+ let recommendation;
1318
+ if (invalidCount === 0) {
1319
+ recommendation = "valid";
1320
+ } else if (invalidCount === totalReceipts) {
1321
+ recommendation = "invalid";
1322
+ } else {
1323
+ recommendation = "needs_review";
1324
+ }
1325
+ return {
1326
+ headline,
1327
+ issues,
1328
+ recommendation
1329
+ };
1330
+ }
1331
+ async function verifyBundleSignature(bundleContents) {
1332
+ const { bundle_sig, keys, manifest } = bundleContents;
1333
+ if (!bundle_sig) {
1334
+ return { present: false };
1335
+ }
1336
+ const parsed = parseJws2(bundle_sig);
1337
+ if (!parsed) {
1338
+ return {
1339
+ present: true,
1340
+ valid: false,
1341
+ error: "E_BUNDLE_SIGNATURE_INVALID_FORMAT"
1342
+ };
1343
+ }
1344
+ const keyId = typeof parsed.header.kid === "string" ? parsed.header.kid : void 0;
1345
+ if (!keyId) {
1346
+ return {
1347
+ present: true,
1348
+ valid: false,
1349
+ error: "E_BUNDLE_SIGNATURE_MISSING_KID"
1350
+ };
1351
+ }
1352
+ const jwk = keys.keys.find((k) => k.kid === keyId);
1353
+ if (!jwk) {
1354
+ return {
1355
+ present: true,
1356
+ valid: false,
1357
+ key_id: keyId,
1358
+ error: BundleErrorCodes.KEY_MISSING
1359
+ };
1360
+ }
1361
+ const publicKeyBytes = jwkToPublicKeyBytes(jwk);
1362
+ if (!publicKeyBytes) {
1363
+ return {
1364
+ present: true,
1365
+ valid: false,
1366
+ key_id: keyId,
1367
+ error: "E_BUNDLE_SIGNATURE_INVALID_KEY_FORMAT"
1368
+ };
1369
+ }
1370
+ try {
1371
+ const result = await verify(bundle_sig, publicKeyBytes);
1372
+ if (!result.valid) {
1373
+ return {
1374
+ present: true,
1375
+ valid: false,
1376
+ key_id: keyId,
1377
+ error: BundleErrorCodes.SIGNATURE_INVALID
1378
+ };
1379
+ }
1380
+ if (result.payload.content_hash !== manifest.content_hash) {
1381
+ return {
1382
+ present: true,
1383
+ valid: false,
1384
+ key_id: keyId,
1385
+ error: "E_BUNDLE_SIGNATURE_CONTENT_MISMATCH"
1386
+ };
1387
+ }
1388
+ return {
1389
+ present: true,
1390
+ valid: true,
1391
+ key_id: keyId
1392
+ };
1393
+ } catch (err) {
1394
+ const errorMsg = err instanceof CryptoError ? `E_BUNDLE_SIGNATURE_CRYPTO_ERROR:${err.code}` : BundleErrorCodes.SIGNATURE_INVALID;
1395
+ return {
1396
+ present: true,
1397
+ valid: false,
1398
+ key_id: keyId,
1399
+ error: errorMsg
1400
+ };
1401
+ }
1402
+ }
1403
+ async function verifyBundle(zipBuffer, options) {
1404
+ const readResult = await readDisputeBundle(zipBuffer);
1405
+ if (!readResult.ok) {
1406
+ return readResult;
1407
+ }
1408
+ const bundleContents = readResult.value;
1409
+ const { manifest, receipts } = bundleContents;
1410
+ const bundleSignature = await verifyBundleSignature(bundleContents);
1411
+ const results = [];
1412
+ for (const receiptEntry of manifest.receipts) {
1413
+ const jws = receipts.get(receiptEntry.receipt_id);
1414
+ if (!jws) {
1415
+ results.push({
1416
+ receipt_id: receiptEntry.receipt_id,
1417
+ signature_valid: false,
1418
+ claims_valid: false,
1419
+ errors: ["E_BUNDLE_RECEIPT_NOT_FOUND"]
1420
+ });
1421
+ continue;
1422
+ }
1423
+ const result = await verifyReceipt(receiptEntry.receipt_id, jws, bundleContents, options);
1424
+ results.push(result);
1425
+ }
1426
+ results.sort((a, b) => a.receipt_id.localeCompare(b.receipt_id));
1427
+ const validCount = results.filter(
1428
+ (r) => r.signature_valid && r.claims_valid && r.errors.length === 0
1429
+ ).length;
1430
+ const invalidCount = results.length - validCount;
1431
+ const keysUsed = buildKeyUsage(results);
1432
+ const auditorSummary = generateAuditorSummary(results.length, validCount, invalidCount, results);
1433
+ const reportWithoutHash = {
1434
+ version: VERIFICATION_REPORT_VERSION,
1435
+ bundle_content_hash: manifest.content_hash,
1436
+ bundle_signature: bundleSignature,
1437
+ summary: {
1438
+ total_receipts: results.length,
1439
+ valid: validCount,
1440
+ invalid: invalidCount
1441
+ },
1442
+ receipts: results,
1443
+ keys_used: keysUsed,
1444
+ auditor_summary: auditorSummary
1445
+ };
1446
+ const cleanedReport = stripUndefined(reportWithoutHash);
1447
+ const reportHash = sha256Hex2(canonicalize(cleanedReport));
1448
+ const report = {
1449
+ ...reportWithoutHash,
1450
+ report_hash: reportHash
1451
+ };
1452
+ return {
1453
+ ok: true,
1454
+ value: report
1455
+ };
1456
+ }
1457
+ function serializeReport(report, pretty = false) {
1458
+ if (pretty) {
1459
+ return JSON.stringify(report, null, 2);
1460
+ }
1461
+ return JSON.stringify(report);
1462
+ }
1463
+ function formatReportText(report) {
1464
+ const lines = [];
1465
+ lines.push("PEAC Dispute Bundle Verification Report");
1466
+ lines.push("========================================");
1467
+ lines.push("");
1468
+ lines.push(`Bundle content hash: ${report.bundle_content_hash}`);
1469
+ lines.push(`Report hash: ${report.report_hash}`);
1470
+ lines.push("");
1471
+ lines.push("Bundle Signature");
1472
+ lines.push("----------------");
1473
+ if (!report.bundle_signature.present) {
1474
+ lines.push(" Status: NOT SIGNED");
1475
+ } else if (report.bundle_signature.valid) {
1476
+ lines.push(" Status: VALID");
1477
+ lines.push(` Key ID: ${report.bundle_signature.key_id}`);
1478
+ } else {
1479
+ lines.push(" Status: INVALID");
1480
+ if (report.bundle_signature.key_id) {
1481
+ lines.push(` Key ID: ${report.bundle_signature.key_id}`);
1482
+ }
1483
+ if (report.bundle_signature.error) {
1484
+ lines.push(` Error: ${report.bundle_signature.error}`);
1485
+ }
1486
+ }
1487
+ lines.push("");
1488
+ lines.push("Summary");
1489
+ lines.push("-------");
1490
+ lines.push(`Total receipts: ${report.summary.total_receipts}`);
1491
+ lines.push(`Valid: ${report.summary.valid}`);
1492
+ lines.push(`Invalid: ${report.summary.invalid}`);
1493
+ lines.push("");
1494
+ lines.push(`Recommendation: ${report.auditor_summary.recommendation.toUpperCase()}`);
1495
+ lines.push(`Headline: ${report.auditor_summary.headline}`);
1496
+ lines.push("");
1497
+ if (report.auditor_summary.issues.length > 0) {
1498
+ lines.push("Issues");
1499
+ lines.push("------");
1500
+ for (const issue of report.auditor_summary.issues) {
1501
+ lines.push(` - ${issue}`);
1502
+ }
1503
+ lines.push("");
1504
+ }
1505
+ if (report.keys_used.length > 0) {
1506
+ lines.push("Keys Used");
1507
+ lines.push("---------");
1508
+ for (const keyUsage of report.keys_used) {
1509
+ lines.push(` ${keyUsage.kid}: ${keyUsage.receipts_signed} receipt(s)`);
1510
+ }
1511
+ lines.push("");
1512
+ }
1513
+ lines.push("Receipt Details");
1514
+ lines.push("---------------");
1515
+ for (const receipt of report.receipts) {
1516
+ const status = receipt.signature_valid && receipt.claims_valid ? "VALID" : "INVALID";
1517
+ lines.push(` ${receipt.receipt_id}: ${status}`);
1518
+ if (receipt.key_id) {
1519
+ lines.push(` Key: ${receipt.key_id}`);
1520
+ }
1521
+ if (receipt.errors.length > 0) {
1522
+ lines.push(` Errors: ${receipt.errors.join(", ")}`);
1523
+ }
1524
+ }
1525
+ return lines.join("\n");
1526
+ }
1527
+
1528
+ // src/index.ts
1529
+ var AUDIT_PACKAGE_VERSION = "0.9.27";
1530
+
1531
+ export { AUDIT_EVENT_TYPES, AUDIT_PACKAGE_VERSION, AUDIT_SEVERITIES, AUDIT_VERSION, BUNDLE_VERSION, DISPUTE_BUNDLE_VERSION, BUNDLE_VERSION2 as DISPUTE_BUNDLE_VERSION_v2, VERIFICATION_REPORT_VERSION, correlateByTrace, createAuditEntry, createCaseBundle, createDisputeBundle, createJsonlAppender, filterByDispute, filterByResource, filterByTimeRange, filterByTraceId, formatJsonl, formatJsonlLine, formatReportText, generateAuditId, generateBundleSummary, getBundleContentHash, isValidAuditEntry, isValidTraceContext, isValidUlid, parseJsonl, parseJsonlLine, readDisputeBundle, serializeBundle, serializeReport, validateAuditEntry, verifyBundle, verifyBundleIntegrity };
1532
+ //# sourceMappingURL=index.mjs.map
1533
+ //# sourceMappingURL=index.mjs.map