@rakelabs/evidence-publisher 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +608 -0
  2. package/dist/src/EvidenceHasher.d.ts +7 -0
  3. package/dist/src/EvidenceHasher.js +32 -0
  4. package/dist/src/EvidenceJsonBuilder.d.ts +6 -0
  5. package/dist/src/EvidenceJsonBuilder.js +50 -0
  6. package/dist/src/EvidencePublisher.d.ts +35 -0
  7. package/dist/src/EvidencePublisher.js +105 -0
  8. package/dist/src/EvidencePublisherFactory.d.ts +83 -0
  9. package/dist/src/EvidencePublisherFactory.js +251 -0
  10. package/dist/src/MetaEvidenceJsonBuilder.d.ts +25 -0
  11. package/dist/src/MetaEvidenceJsonBuilder.js +104 -0
  12. package/dist/src/MetaEvidencePublisher.d.ts +39 -0
  13. package/dist/src/MetaEvidencePublisher.js +104 -0
  14. package/dist/src/advanced.d.ts +15 -0
  15. package/dist/src/advanced.js +7 -0
  16. package/dist/src/config.d.ts +51 -0
  17. package/dist/src/config.js +245 -0
  18. package/dist/src/helia/HeliaAttachmentStore.d.ts +8 -0
  19. package/dist/src/helia/HeliaAttachmentStore.js +20 -0
  20. package/dist/src/helia/HeliaEvidenceStore.d.ts +8 -0
  21. package/dist/src/helia/HeliaEvidenceStore.js +16 -0
  22. package/dist/src/helia/HeliaIpfsClient.d.ts +15 -0
  23. package/dist/src/helia/HeliaIpfsClient.js +63 -0
  24. package/dist/src/http/HttpIpfsAttachmentStore.d.ts +8 -0
  25. package/dist/src/http/HttpIpfsAttachmentStore.js +23 -0
  26. package/dist/src/http/HttpIpfsClient.d.ts +24 -0
  27. package/dist/src/http/HttpIpfsClient.js +126 -0
  28. package/dist/src/http/HttpIpfsEvidenceStore.d.ts +8 -0
  29. package/dist/src/http/HttpIpfsEvidenceStore.js +19 -0
  30. package/dist/src/http/HttpMultipartUploadClient.d.ts +36 -0
  31. package/dist/src/http/HttpMultipartUploadClient.js +183 -0
  32. package/dist/src/http/HttpPinByCidClient.d.ts +23 -0
  33. package/dist/src/http/HttpPinByCidClient.js +137 -0
  34. package/dist/src/index.d.ts +7 -0
  35. package/dist/src/index.js +5 -0
  36. package/dist/src/storage-types.d.ts +21 -0
  37. package/dist/src/storage-types.js +1 -0
  38. package/dist/src/types.d.ts +238 -0
  39. package/dist/src/types.js +1 -0
  40. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,608 @@
1
+ # @rakelabs/evidence-publisher
2
+
3
+ Build and publish ERC-1497 / Kleros **Evidence** and **MetaEvidence** documents to IPFS.
4
+ Produces the `evidenceUri` and `metaEvidenceUri` values you pass into your contract or dispute SDKs.
5
+
6
+ This package is for **developers integrating the SDK** into Kleros-style dispute systems.
7
+ It does not decide disputes or enforce rulings. It helps you:
8
+
9
+ - build valid MetaEvidence and Evidence documents,
10
+ - upload them to IPFS through Helia, self-hosted, or third-party provider flows,
11
+ - optionally remote-pin the resulting CIDs,
12
+ - get back stable `ipfs://...` URIs to use in your dispute workflow.
13
+
14
+ ---
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ npm install @rakelabs/evidence-publisher
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Start here: the mental model
25
+
26
+ If you are new to Kleros, the most important thing to understand is that there are **two different document layers**:
27
+
28
+ ### MetaEvidence = the dispute container / template
29
+
30
+ A **MetaEvidence** document describes the **framework** for a dispute:
31
+
32
+ - what category the dispute belongs to,
33
+ - what question jurors should answer,
34
+ - what ruling options exist,
35
+ - what policy or rules apply,
36
+ - optionally, a PDF or other attachment containing the full written policy.
37
+
38
+ Think of MetaEvidence as the **container**, **template**, or **policy envelope** for a class of disputes.
39
+ It is **not required to describe one specific dispute instance**.
40
+
41
+ A single MetaEvidence document can be:
42
+
43
+ - **reused across many disputes** in the same category, or
44
+ - made **more specific** for a narrower dispute type, policy version, or product flow.
45
+
46
+ For example, an Amazon-like marketplace might:
47
+
48
+ - publish **one reusable MetaEvidence document** for a general buyer-vs-seller dispute flow, or
49
+ - publish **multiple MetaEvidence documents** such as:
50
+ - item-not-received disputes,
51
+ - item-not-as-described disputes,
52
+ - seller chargeback disputes,
53
+ - premium marketplace policy v2 disputes.
54
+
55
+ All of those are valid. The right choice depends on how much policy reuse vs specialization you want.
56
+
57
+ ### Evidence = the proof for one actual dispute
58
+
59
+ An **Evidence** document is different. It is the actual proof submitted later by a party or end user for a **specific dispute instance**.
60
+
61
+ Examples:
62
+
63
+ - an invoice PDF,
64
+ - a screenshot,
65
+ - a tracking export,
66
+ - a conversation transcript,
67
+ - a signed contract attachment.
68
+
69
+ So the rule of thumb is:
70
+
71
+ - **MetaEvidence** = reusable dispute framework
72
+ - **Evidence** = specific proof for one dispute
73
+
74
+ ---
75
+
76
+ ## How Kleros uses these documents
77
+
78
+ In a typical Kleros-style flow:
79
+
80
+ 1. You publish a **MetaEvidence** document.
81
+ 2. Your contract or dispute system stores or references the returned `metaEvidenceUri`.
82
+ 3. When a dispute is created, jurors can use that MetaEvidence to understand the dispute rules and ruling choices.
83
+ 4. Later, parties submit **Evidence** documents for that particular dispute.
84
+ 5. Those Evidence documents are linked to the dispute as the factual record.
85
+
86
+ So when integrating this SDK, you usually need to do **three** things:
87
+
88
+ 1. publish the reusable **policy / category MetaEvidence**,
89
+ 2. optionally publish a **policy attachment** as part of that MetaEvidence,
90
+ 3. publish **Evidence** documents as users submit proof during disputes.
91
+
92
+ ---
93
+
94
+ ## The core integration workflow
95
+
96
+ ### Step 1: configure storage once
97
+
98
+ Create `evidence.storage.yml` next to your code:
99
+
100
+ ```yaml
101
+ addressing: content
102
+ provider:
103
+ name: my-provider
104
+ url: https://your-upload-endpoint.example/files
105
+ auth:
106
+ type: bearer
107
+ token: ${UPLOAD_TOKEN}
108
+ fields:
109
+ network: public
110
+ ```
111
+
112
+ `${UPLOAD_TOKEN}` is resolved from your environment:
113
+
114
+ - **CI/CD / Docker / Kubernetes**: set it in `process.env`
115
+ - **Local dev**: optionally use a `.env` file next to the config
116
+
117
+ ```env
118
+ # .env (local dev only — never commit this)
119
+ UPLOAD_TOKEN=your-upload-token-here
120
+ ```
121
+
122
+ Once this is in place, both publishers reuse the same config:
123
+
124
+ ```ts
125
+ import {
126
+ createMetaEvidencePublisher,
127
+ createEvidencePublisher,
128
+ } from '@rakelabs/evidence-publisher';
129
+
130
+ const metaEvidencePublisher = await createMetaEvidencePublisher();
131
+ const evidencePublisher = await createEvidencePublisher();
132
+ ```
133
+
134
+ You configure storage **once**. Then you use the publisher that matches the document type you are creating.
135
+
136
+ ---
137
+
138
+ ### Step 2: publish a reusable MetaEvidence document
139
+
140
+ This is the most common first step.
141
+
142
+ Suppose your marketplace has a general buyer-vs-seller delivery dispute flow.
143
+ You can publish one reusable MetaEvidence document for that category and use its URI across many disputes.
144
+
145
+ ```ts
146
+ import { createMetaEvidencePublisher } from '@rakelabs/evidence-publisher';
147
+
148
+ const metaEvidencePublisher = await createMetaEvidencePublisher();
149
+
150
+ const deliveryDisputeMetaEvidence = await metaEvidencePublisher.publish({
151
+ category: 'Marketplace buyer-seller disputes',
152
+ title: 'Buyer vs Seller Delivery Dispute Policy',
153
+ description: 'Reusable dispute policy for delivery-related marketplace disputes.',
154
+ question: 'Did the seller fulfill the delivery obligation under the marketplace rules?',
155
+ rulingOptions: {
156
+ type: 'single-select',
157
+ precision: 0,
158
+ titles: ['Buyer Wins', 'Seller Wins'],
159
+ descriptions: [
160
+ 'Refund the buyer or rule in the buyer’s favor.',
161
+ 'Release funds to the seller or rule in the seller’s favor.',
162
+ ],
163
+ reserved: {},
164
+ },
165
+ aliases: {
166
+ buyer: 'Buyer',
167
+ seller: 'Seller',
168
+ },
169
+ });
170
+
171
+ console.log(deliveryDisputeMetaEvidence.document.uri); // ipfs://...
172
+ ```
173
+
174
+ You would then store or pass that `document.uri` wherever your contract or dispute system expects the MetaEvidence URI.
175
+
176
+ ---
177
+
178
+ ### Step 3: publish a more specific MetaEvidence document when needed
179
+
180
+ Sometimes one broad template is not enough.
181
+ If different dispute types have different questions or ruling choices, publish multiple MetaEvidence documents.
182
+
183
+ For example, you might separate "item not received" from "item not as described":
184
+
185
+ ```ts
186
+ const itemNotReceivedMetaEvidence = await metaEvidencePublisher.publish({
187
+ category: 'Marketplace buyer-seller disputes',
188
+ title: 'Item Not Received Policy',
189
+ description: 'Used when the buyer claims the seller never delivered the item.',
190
+ question: 'Did the seller deliver the item to the buyer under the marketplace rules?',
191
+ rulingOptions: {
192
+ type: 'single-select',
193
+ precision: 0,
194
+ titles: ['Buyer Wins', 'Seller Wins'],
195
+ descriptions: [
196
+ 'The item was not delivered under the applicable policy.',
197
+ 'The seller satisfied the delivery obligation.',
198
+ ],
199
+ reserved: {},
200
+ },
201
+ });
202
+
203
+ console.log(itemNotReceivedMetaEvidence.document.uri); // ipfs://...
204
+ ```
205
+
206
+ That is the key architectural idea:
207
+
208
+ - reuse one MetaEvidence document when your dispute framework is stable,
209
+ - publish multiple MetaEvidence documents when categories, policy versions, or ruling logic differ.
210
+
211
+ ---
212
+
213
+ ### Step 4: attach a PDF policy document to MetaEvidence when useful
214
+
215
+ If you already have a written policy PDF, you can publish it together with MetaEvidence.
216
+ The SDK uploads the attachment first, then fills:
217
+
218
+ - `fileURI`
219
+ - `fileHash`
220
+ - `fileTypeExtension`
221
+
222
+ for you.
223
+
224
+ ```ts
225
+ const policyWithPdf = await metaEvidencePublisher.publish({
226
+ category: 'Marketplace buyer-seller disputes',
227
+ title: 'Buyer vs Seller Policy v2',
228
+ description: 'Reusable policy with a PDF attachment.',
229
+ question: 'Did the seller satisfy the marketplace delivery rules?',
230
+ rulingOptions: {
231
+ type: 'single-select',
232
+ precision: 0,
233
+ titles: ['Buyer Wins', 'Seller Wins'],
234
+ descriptions: ['Rule for the buyer.', 'Rule for the seller.'],
235
+ reserved: {},
236
+ },
237
+ attachment: {
238
+ bytes: policyPdfBytes,
239
+ fileName: 'marketplace-policy-v2.pdf',
240
+ mediaType: 'application/pdf',
241
+ fileTypeExtension: 'pdf',
242
+ },
243
+ });
244
+
245
+ console.log(policyWithPdf.attachment?.uri); // ipfs://... PDF
246
+ console.log(policyWithPdf.document.uri); // ipfs://... MetaEvidence JSON
247
+ ```
248
+
249
+ Use this when jurors should be able to inspect a full written policy document, not just the short JSON fields.
250
+
251
+ ---
252
+
253
+ ### Step 5: publish Evidence for one actual dispute
254
+
255
+ Once a specific dispute exists, parties or end users can upload their proof.
256
+ That is what `EvidencePublisher` is for.
257
+
258
+ ```ts
259
+ import { createEvidencePublisher } from '@rakelabs/evidence-publisher';
260
+
261
+ const evidencePublisher = await createEvidencePublisher();
262
+
263
+ const evidenceResult = await evidencePublisher.publish({
264
+ title: 'Tracking screenshot',
265
+ description: 'Carrier page showing the package was never marked delivered.',
266
+ attachment: {
267
+ bytes: fileBytes,
268
+ fileName: 'tracking-screenshot.png',
269
+ mediaType: 'image/png',
270
+ fileTypeExtension: 'png',
271
+ },
272
+ });
273
+
274
+ console.log(evidenceResult.document.uri); // ipfs://... evidence JSON
275
+ console.log(evidenceResult.attachment?.uri); // ipfs://... attachment
276
+ ```
277
+
278
+ This Evidence document is for **one concrete dispute submission**, not the reusable dispute policy.
279
+
280
+ ---
281
+
282
+ ## What to upload, in plain English
283
+
284
+ If you are integrating a Kleros-style system, the usual pattern is:
285
+
286
+ ### A) Upload the reusable dispute policy
287
+
288
+ Use **MetaEvidence** for:
289
+
290
+ - marketplace-wide buyer/seller policy,
291
+ - one dispute category template,
292
+ - one product-line policy,
293
+ - one policy version,
294
+ - one narrow dispute type if needed.
295
+
296
+ ### B) Optionally attach the full written policy
297
+
298
+ Still use **MetaEvidence**, but include an attachment so the final JSON points to the PDF.
299
+
300
+ ### C) Upload the evidence users submit later
301
+
302
+ Use **Evidence** for:
303
+
304
+ - invoices,
305
+ - screenshots,
306
+ - delivery records,
307
+ - chat logs,
308
+ - signed contracts,
309
+ - any case-specific proof.
310
+
311
+ ---
312
+
313
+ ## MetaEvidence vs Evidence
314
+
315
+ Use this rule of thumb:
316
+
317
+ - **MetaEvidence** = the reusable dispute template / container
318
+ - **Evidence** = the proof for one specific dispute
319
+
320
+ Another way to think about it:
321
+
322
+ - MetaEvidence tells jurors **how to think about the dispute**
323
+ - Evidence tells jurors **what happened in this specific case**
324
+
325
+ ---
326
+
327
+ ## 30-second quickstart
328
+
329
+ If you already understand the concepts, this is the shortest working flow.
330
+
331
+ ### Publish MetaEvidence
332
+
333
+ ```ts
334
+ import { createMetaEvidencePublisher } from '@rakelabs/evidence-publisher';
335
+
336
+ const metaEvidencePublisher = await createMetaEvidencePublisher();
337
+
338
+ const metaEvidenceResult = await metaEvidencePublisher.publish({
339
+ category: 'Escrow',
340
+ title: 'Late delivery dispute',
341
+ description: 'Used when a seller claims delivery was completed late.',
342
+ question: 'Did the seller deliver the work on time?',
343
+ rulingOptions: {
344
+ type: 'single-select',
345
+ precision: 0,
346
+ titles: ['Buyer Wins', 'Seller Wins'],
347
+ descriptions: ['Refund the buyer.', 'Release funds to the seller.'],
348
+ reserved: {},
349
+ },
350
+ });
351
+
352
+ console.log(metaEvidenceResult.document.uri); // ipfs://...
353
+ ```
354
+
355
+ ### Publish Evidence
356
+
357
+ ```ts
358
+ import { createEvidencePublisher } from '@rakelabs/evidence-publisher';
359
+
360
+ const evidencePublisher = await createEvidencePublisher();
361
+
362
+ const evidenceResult = await evidencePublisher.publish({
363
+ title: 'Proof of delivery delay',
364
+ description: 'Screenshots and invoice attached.',
365
+ attachment: {
366
+ bytes: fileBytes,
367
+ fileName: 'invoice.pdf',
368
+ mediaType: 'application/pdf',
369
+ fileTypeExtension: 'pdf',
370
+ },
371
+ });
372
+
373
+ console.log(evidenceResult.document.uri); // ipfs://...
374
+ ```
375
+
376
+ `createEvidencePublisher()` and `createMetaEvidencePublisher()` both read `evidence.storage.yml` from `process.cwd()`. No `.env` file is required in production.
377
+
378
+ ---
379
+
380
+ ## Storage configuration model
381
+
382
+ The SDK is **not tied to Pinata**. It works with a generic storage configuration model and can target:
383
+
384
+ - a third-party hosted upload provider,
385
+ - a self-hosted HTTP upload endpoint,
386
+ - a local or self-hosted Kubo-style endpoint,
387
+ - in-process Helia when no provider URL is supplied.
388
+
389
+ The same config model works for both `createEvidencePublisher()` and `createMetaEvidencePublisher()`.
390
+
391
+ ### Generic content-addressed example
392
+
393
+ ```yaml
394
+ addressing: content
395
+ provider:
396
+ name: my-provider
397
+ url: https://your-upload-endpoint.example/files
398
+ auth:
399
+ type: bearer
400
+ token: ${UPLOAD_TOKEN}
401
+ headers:
402
+ x-custom-header: my-value
403
+ fields:
404
+ network: public
405
+ ```
406
+
407
+ Important fields:
408
+
409
+ - `addressing`: usually `content` for IPFS-style content addressing
410
+ - `provider.name`: a human-readable provider label
411
+ - `provider.url`: the upload endpoint; omit it to use in-process Helia
412
+ - `provider.auth`: authentication strategy (`none`, `bearer`, `basic`, or custom header auth)
413
+ - `provider.headers`: optional extra HTTP headers
414
+ - `provider.fields`: optional provider-specific request fields
415
+ - `remotePinning`: optional second-step CID pinning after upload
416
+
417
+ When `provider.url` is present, the SDK uses HTTP upload behavior.
418
+ When `provider.url` is omitted under content addressing, the SDK starts local Helia automatically.
419
+
420
+ ### Provider examples
421
+
422
+ #### Pinata v3
423
+
424
+ ```yaml
425
+ addressing: content
426
+ provider:
427
+ name: pinata-v3
428
+ url: https://uploads.pinata.cloud/v3/files
429
+ auth:
430
+ type: bearer
431
+ token: ${PINATA_JWT}
432
+ fields:
433
+ network: public # required by Pinata v3
434
+ ```
435
+
436
+ #### Self-hosted Kubo node
437
+
438
+ ```yaml
439
+ addressing: content
440
+ provider:
441
+ name: kubo-local
442
+ url: http://localhost:5001/api/v0/add
443
+ auth:
444
+ type: none
445
+ ```
446
+
447
+ #### Local in-process Helia (no network required)
448
+
449
+ ```yaml
450
+ addressing: content
451
+ provider:
452
+ name: helia-local
453
+ # no url = Helia starts in-process automatically
454
+ ```
455
+
456
+
457
+ ---
458
+
459
+ ## Remote pinning (optional durability step)
460
+
461
+ After any upload, you can pin the resulting CID to a separate pinning service.
462
+ Add a `remotePinning` block to your config:
463
+
464
+ ```yaml
465
+ addressing: content
466
+ provider:
467
+ name: kubo-local
468
+ url: http://localhost:5001/api/v0/add
469
+ auth:
470
+ type: none
471
+ remotePinning:
472
+ endpoint: https://api.pinata.cloud/v3
473
+ auth:
474
+ type: bearer
475
+ token: ${PINATA_JWT}
476
+ ```
477
+
478
+ The publish result carries the outcome:
479
+
480
+ ```ts
481
+ metaEvidenceResult.remotePinning?.documentPin
482
+ metaEvidenceResult.remotePinning?.error
483
+
484
+ evidenceResult.remotePinning?.documentPin
485
+ evidenceResult.remotePinning?.attachmentPin
486
+ evidenceResult.remotePinning?.error
487
+ ```
488
+
489
+ The document upload still succeeds even if remote pinning fails.
490
+
491
+ ---
492
+
493
+ ## API at a glance
494
+
495
+ ```ts
496
+ import {
497
+ createEvidencePublisher,
498
+ createMetaEvidencePublisher,
499
+ MetaEvidenceJsonBuilder,
500
+ } from '@rakelabs/evidence-publisher';
501
+
502
+ // Config-driven publishers
503
+ const evidencePublisher = await createEvidencePublisher();
504
+ const metaEvidencePublisher = await createMetaEvidencePublisher();
505
+
506
+ // Explicit config in code
507
+ const evidencePublisherWithConfig = await createEvidencePublisher({
508
+ config: {
509
+ addressing: 'content',
510
+ provider: {
511
+ name: 'pinata-v3',
512
+ url: 'https://uploads.pinata.cloud/v3/files',
513
+ auth: { type: 'bearer', token: process.env.PINATA_JWT! },
514
+ fields: { network: 'public' },
515
+ },
516
+ pinning: { enabled: false },
517
+ },
518
+ });
519
+
520
+ // Build MetaEvidence JSON without publishing
521
+ const metaEvidenceJson = MetaEvidenceJsonBuilder.build({
522
+ category: 'Escrow',
523
+ title: 'Delivery dispute',
524
+ description: 'Reusable dispute template',
525
+ question: 'Did the seller deliver on time?',
526
+ rulingOptions: {
527
+ type: 'single-select',
528
+ precision: 0,
529
+ titles: ['Buyer Wins', 'Seller Wins'],
530
+ descriptions: ['Refund buyer', 'Release to seller'],
531
+ reserved: {},
532
+ },
533
+ });
534
+
535
+ // Publish Evidence
536
+ const evidenceResult = await evidencePublisher.publish({
537
+ title: 'Tracking proof',
538
+ description: 'Carrier export',
539
+ });
540
+
541
+ // Publish MetaEvidence
542
+ const metaEvidenceResult = await metaEvidencePublisher.publish({
543
+ category: 'Escrow',
544
+ title: 'Delivery dispute',
545
+ description: 'Reusable dispute template',
546
+ question: 'Did the seller deliver on time?',
547
+ rulingOptions: {
548
+ type: 'single-select',
549
+ precision: 0,
550
+ titles: ['Buyer Wins', 'Seller Wins'],
551
+ descriptions: ['Refund buyer', 'Release to seller'],
552
+ reserved: {},
553
+ },
554
+ });
555
+ ```
556
+
557
+ ---
558
+
559
+ ## Advanced / power users
560
+
561
+ Import from the `/advanced` subpath for raw config helpers and HTTP transport clients:
562
+
563
+ ```ts
564
+ import {
565
+ createHttpEvidencePublisher,
566
+ createHttpMetaEvidencePublisher,
567
+ parseStorageConfig,
568
+ readStorageConfigFile,
569
+ HttpMultipartUploadClient,
570
+ HttpPinByCidClient,
571
+ } from '@rakelabs/evidence-publisher/advanced';
572
+ ```
573
+
574
+ Use `createHttpEvidencePublisher()` / `createHttpMetaEvidencePublisher()` when you need:
575
+
576
+ - browser or edge-friendly synchronous construction,
577
+ - custom `parseResponse` logic,
578
+ - custom `serializeRequest` logic,
579
+ - direct control over the HTTP upload setup.
580
+
581
+ ```ts
582
+ import {
583
+ createHttpEvidencePublisher,
584
+ createHttpMetaEvidencePublisher,
585
+ } from '@rakelabs/evidence-publisher/advanced';
586
+
587
+ const sharedConfig = {
588
+ endpoint: 'https://uploads.pinata.cloud/v3/files',
589
+ auth: { type: 'bearer', token: process.env.PINATA_JWT! },
590
+ fields: { network: 'public' },
591
+ };
592
+
593
+ const evidencePublisher = createHttpEvidencePublisher({
594
+ ...sharedConfig,
595
+ parseResponse: (body) => (body as any).data?.cid,
596
+ });
597
+
598
+ const metaEvidencePublisher = createHttpMetaEvidencePublisher(sharedConfig);
599
+ ```
600
+
601
+ ---
602
+
603
+ ## Running tests
604
+
605
+ ```sh
606
+ npm test # unit tests only (fast)
607
+ npm run test:all # unit + all e2e (requires Docker for Kubo tests)
608
+ ```
@@ -0,0 +1,7 @@
1
+ import type { EvidenceJsonDocument } from './types.js';
2
+ export declare class EvidenceHasher {
3
+ static hashBytes(bytes: Uint8Array): string;
4
+ static serializeWithoutSelfHash(document: EvidenceJsonDocument): Uint8Array;
5
+ static serialize(document: EvidenceJsonDocument): Uint8Array;
6
+ static hashEvidenceDocumentWithoutSelfHash(document: EvidenceJsonDocument): string;
7
+ }
@@ -0,0 +1,32 @@
1
+ import { keccak256, toUtf8Bytes } from 'ethers';
2
+ export class EvidenceHasher {
3
+ static hashBytes(bytes) {
4
+ return keccak256(bytes);
5
+ }
6
+ static serializeWithoutSelfHash(document) {
7
+ const normalized = {
8
+ title: document.title,
9
+ name: document.name,
10
+ description: document.description,
11
+ ...(document.fileURI ? { fileURI: document.fileURI } : {}),
12
+ ...(document.fileHash ? { fileHash: document.fileHash } : {}),
13
+ ...(document.fileTypeExtension ? { fileTypeExtension: document.fileTypeExtension } : {}),
14
+ };
15
+ return toUtf8Bytes(JSON.stringify(normalized));
16
+ }
17
+ static serialize(document) {
18
+ const normalized = {
19
+ title: document.title,
20
+ name: document.name,
21
+ description: document.description,
22
+ ...(document.fileURI ? { fileURI: document.fileURI } : {}),
23
+ ...(document.fileHash ? { fileHash: document.fileHash } : {}),
24
+ ...(document.fileTypeExtension ? { fileTypeExtension: document.fileTypeExtension } : {}),
25
+ ...(document.selfHash ? { selfHash: document.selfHash } : {}),
26
+ };
27
+ return toUtf8Bytes(JSON.stringify(normalized));
28
+ }
29
+ static hashEvidenceDocumentWithoutSelfHash(document) {
30
+ return keccak256(this.serializeWithoutSelfHash(document));
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ import type { EvidenceDraft, EvidenceJsonDocument, PublishedAttachment } from './types.js';
2
+ export declare class EvidenceJsonBuilder {
3
+ static build(draft: EvidenceDraft): EvidenceJsonDocument;
4
+ static withAttachment(draft: EvidenceDraft, attachment: PublishedAttachment): EvidenceJsonDocument;
5
+ static withSelfHash(document: EvidenceJsonDocument, selfHash: string): EvidenceJsonDocument;
6
+ }
@@ -0,0 +1,50 @@
1
+ export class EvidenceJsonBuilder {
2
+ static build(draft) {
3
+ const title = requireNonBlank(draft.title, 'title');
4
+ const description = requireNonBlank(draft.description, 'description');
5
+ const fileUri = normalizeOptional(draft.fileUri);
6
+ const fileHash = normalizeOptional(draft.fileHash);
7
+ const fileTypeExtension = normalizeOptional(draft.fileTypeExtension);
8
+ if (!fileUri && (fileHash || fileTypeExtension)) {
9
+ throw new Error('fileHash and fileTypeExtension require fileUri');
10
+ }
11
+ return {
12
+ title,
13
+ name: title,
14
+ description,
15
+ ...(fileUri ? { fileURI: fileUri } : {}),
16
+ ...(fileHash ? { fileHash } : {}),
17
+ ...(fileTypeExtension ? { fileTypeExtension } : {}),
18
+ };
19
+ }
20
+ static withAttachment(draft, attachment) {
21
+ return this.build({
22
+ title: draft.title,
23
+ description: draft.description,
24
+ fileUri: attachment.uri,
25
+ fileHash: attachment.fileHash,
26
+ fileTypeExtension: attachment.fileTypeExtension,
27
+ });
28
+ }
29
+ static withSelfHash(document, selfHash) {
30
+ return {
31
+ ...this.build({
32
+ title: document.title,
33
+ description: document.description,
34
+ fileUri: document.fileURI,
35
+ fileHash: document.fileHash,
36
+ fileTypeExtension: document.fileTypeExtension,
37
+ }),
38
+ selfHash: requireNonBlank(selfHash, 'selfHash'),
39
+ };
40
+ }
41
+ }
42
+ function requireNonBlank(value, name) {
43
+ if (!value?.trim()) {
44
+ throw new Error(`${name} must not be blank`);
45
+ }
46
+ return value.trim();
47
+ }
48
+ function normalizeOptional(value) {
49
+ return value?.trim() ? value.trim() : undefined;
50
+ }