@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.
- package/README.md +608 -0
- package/dist/src/EvidenceHasher.d.ts +7 -0
- package/dist/src/EvidenceHasher.js +32 -0
- package/dist/src/EvidenceJsonBuilder.d.ts +6 -0
- package/dist/src/EvidenceJsonBuilder.js +50 -0
- package/dist/src/EvidencePublisher.d.ts +35 -0
- package/dist/src/EvidencePublisher.js +105 -0
- package/dist/src/EvidencePublisherFactory.d.ts +83 -0
- package/dist/src/EvidencePublisherFactory.js +251 -0
- package/dist/src/MetaEvidenceJsonBuilder.d.ts +25 -0
- package/dist/src/MetaEvidenceJsonBuilder.js +104 -0
- package/dist/src/MetaEvidencePublisher.d.ts +39 -0
- package/dist/src/MetaEvidencePublisher.js +104 -0
- package/dist/src/advanced.d.ts +15 -0
- package/dist/src/advanced.js +7 -0
- package/dist/src/config.d.ts +51 -0
- package/dist/src/config.js +245 -0
- package/dist/src/helia/HeliaAttachmentStore.d.ts +8 -0
- package/dist/src/helia/HeliaAttachmentStore.js +20 -0
- package/dist/src/helia/HeliaEvidenceStore.d.ts +8 -0
- package/dist/src/helia/HeliaEvidenceStore.js +16 -0
- package/dist/src/helia/HeliaIpfsClient.d.ts +15 -0
- package/dist/src/helia/HeliaIpfsClient.js +63 -0
- package/dist/src/http/HttpIpfsAttachmentStore.d.ts +8 -0
- package/dist/src/http/HttpIpfsAttachmentStore.js +23 -0
- package/dist/src/http/HttpIpfsClient.d.ts +24 -0
- package/dist/src/http/HttpIpfsClient.js +126 -0
- package/dist/src/http/HttpIpfsEvidenceStore.d.ts +8 -0
- package/dist/src/http/HttpIpfsEvidenceStore.js +19 -0
- package/dist/src/http/HttpMultipartUploadClient.d.ts +36 -0
- package/dist/src/http/HttpMultipartUploadClient.js +183 -0
- package/dist/src/http/HttpPinByCidClient.d.ts +23 -0
- package/dist/src/http/HttpPinByCidClient.js +137 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +5 -0
- package/dist/src/storage-types.d.ts +21 -0
- package/dist/src/storage-types.js +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.js +1 -0
- 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
|
+
}
|