@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
@@ -0,0 +1,35 @@
1
+ import { HttpPinByCidClient } from './http/HttpPinByCidClient.js';
2
+ import type { AttachmentStore, EvidenceStore } from './storage-types.js';
3
+ import type { Attachment, CidInput, EvidencePublishRequest, EvidencePublishResult, PinResult, PublishedAttachment, RemotePinningConfig } from './types.js';
4
+ export interface EvidencePublisherOptions {
5
+ attachmentStore?: AttachmentStore;
6
+ documentStore: EvidenceStore;
7
+ /** Optional client for explicit CID pinning operations via {@link EvidencePublisher.pinCid}. */
8
+ pinByCidClient?: HttpPinByCidClient;
9
+ /**
10
+ * Optional remote pinning configuration.
11
+ * When set (and not disabled), the publisher forwards every produced CID to the
12
+ * configured pinning service as a post-publish durability step.
13
+ */
14
+ remotePinning?: RemotePinningConfig;
15
+ /** Called by {@link EvidencePublisher.close} to release underlying resources (e.g. stop Helia). */
16
+ onClose?: () => Promise<void>;
17
+ }
18
+ export declare class EvidencePublisher {
19
+ private readonly options;
20
+ constructor(options: EvidencePublisherOptions);
21
+ publish(input: EvidencePublishRequest): Promise<EvidencePublishResult>;
22
+ uploadAttachment(input: Attachment): Promise<PublishedAttachment>;
23
+ pinCid(input: CidInput): Promise<PinResult>;
24
+ /**
25
+ * Release underlying resources (e.g. stop the Helia node).
26
+ * Always call this when the publisher is no longer needed.
27
+ */
28
+ close(): Promise<void>;
29
+ /**
30
+ * Runs the remote pinning step if configured.
31
+ * Failures are caught and surfaced via {@link RemotePinOutcome.error} so the
32
+ * publish result is never hidden by a persistence failure.
33
+ */
34
+ private runRemotePinning;
35
+ }
@@ -0,0 +1,105 @@
1
+ import { EvidenceHasher } from './EvidenceHasher.js';
2
+ import { EvidenceJsonBuilder } from './EvidenceJsonBuilder.js';
3
+ import { HttpPinByCidClient } from './http/HttpPinByCidClient.js';
4
+ export class EvidencePublisher {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ async publish(input) {
10
+ let documentDraft;
11
+ let publishedAttachment;
12
+ if (input.attachment) {
13
+ if (!this.options.attachmentStore) {
14
+ throw new Error('attachmentStore is required when attachment is provided');
15
+ }
16
+ publishedAttachment = await this.options.attachmentStore.putAttachment(input.attachment);
17
+ documentDraft = EvidenceJsonBuilder.withAttachment(input, publishedAttachment);
18
+ }
19
+ else {
20
+ documentDraft = EvidenceJsonBuilder.build(input);
21
+ }
22
+ const selfHash = EvidenceHasher.hashEvidenceDocumentWithoutSelfHash(documentDraft);
23
+ const documentWithSelfHash = EvidenceJsonBuilder.withSelfHash(documentDraft, selfHash);
24
+ const publishedDocument = await this.options.documentStore.putEvidenceDocument(documentWithSelfHash);
25
+ const baseResult = {
26
+ selfHash,
27
+ documentJson: documentWithSelfHash,
28
+ ...(publishedAttachment ? { attachment: publishedAttachment } : {}),
29
+ document: publishedDocument,
30
+ };
31
+ const remotePinning = await this.runRemotePinning(publishedDocument, publishedAttachment);
32
+ return {
33
+ ...baseResult,
34
+ ...(remotePinning !== undefined ? { remotePinning } : {}),
35
+ };
36
+ }
37
+ async uploadAttachment(input) {
38
+ if (!this.options.attachmentStore) {
39
+ throw new Error('attachmentStore is required to upload attachments');
40
+ }
41
+ return this.options.attachmentStore.putAttachment(input);
42
+ }
43
+ async pinCid(input) {
44
+ if (!this.options.pinByCidClient) {
45
+ throw new Error('pinByCidClient is required to pin CIDs');
46
+ }
47
+ // No per-request auth override — the client uses its own configured auth.
48
+ const result = await this.options.pinByCidClient.pinByCid({ cid: input.cid });
49
+ return {
50
+ cid: result.cid ?? input.cid,
51
+ ...(result.cid ? { uri: `ipfs://${result.cid}` } : {}),
52
+ };
53
+ }
54
+ /**
55
+ * Release underlying resources (e.g. stop the Helia node).
56
+ * Always call this when the publisher is no longer needed.
57
+ */
58
+ async close() {
59
+ await this.options.onClose?.();
60
+ }
61
+ // ── Remote pinning pipeline ──────────────────────────────────────────────
62
+ /**
63
+ * Runs the remote pinning step if configured.
64
+ * Failures are caught and surfaced via {@link RemotePinOutcome.error} so the
65
+ * publish result is never hidden by a persistence failure.
66
+ */
67
+ async runRemotePinning(document, attachment) {
68
+ const config = this.options.remotePinning;
69
+ if (!config || config.enabled === false) {
70
+ return undefined;
71
+ }
72
+ const client = new HttpPinByCidClient({
73
+ providerUrl: config.endpoint,
74
+ auth: config.auth,
75
+ requestPath: config.requestPath,
76
+ headers: config.headers,
77
+ });
78
+ try {
79
+ const docPinResponse = await client.pinByCid({ cid: document.cid });
80
+ const documentPin = {
81
+ cid: docPinResponse.cid ?? document.cid,
82
+ ...(docPinResponse.cid ? { uri: `ipfs://${docPinResponse.cid}` } : {}),
83
+ ...(docPinResponse.pinId ? { metadata: { pinId: docPinResponse.pinId } } : {}),
84
+ };
85
+ let attachmentPin;
86
+ if (attachment) {
87
+ const attPinResponse = await client.pinByCid({ cid: attachment.cid });
88
+ attachmentPin = {
89
+ cid: attPinResponse.cid ?? attachment.cid,
90
+ ...(attPinResponse.cid ? { uri: `ipfs://${attPinResponse.cid}` } : {}),
91
+ ...(attPinResponse.pinId ? { metadata: { pinId: attPinResponse.pinId } } : {}),
92
+ };
93
+ }
94
+ return {
95
+ documentPin,
96
+ ...(attachmentPin ? { attachmentPin } : {}),
97
+ };
98
+ }
99
+ catch (err) {
100
+ return {
101
+ error: err instanceof Error ? err : new Error(String(err)),
102
+ };
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,83 @@
1
+ import { type StorageConfig, type StorageConfigReadOptions } from './config.js';
2
+ import { EvidencePublisher } from './EvidencePublisher.js';
3
+ import { MetaEvidencePublisher } from './MetaEvidencePublisher.js';
4
+ import type { AttachmentStore, EvidenceStore, MetaEvidenceStore } from './storage-types.js';
5
+ import type { RemotePinningConfig, RequestAuth, RequestFields, UploadRequest } from './types.js';
6
+ export interface EvidencePublisherFactoryOptions extends StorageConfigReadOptions {
7
+ /**
8
+ * Explicit config supplied by code. When present, file reads are bypassed.
9
+ */
10
+ config?: StorageConfig;
11
+ /** Optional YAML file path used only when `config` is not supplied. */
12
+ configFilePath?: string;
13
+ gatewayBaseUrls?: string[];
14
+ attachmentStore?: AttachmentStore;
15
+ documentStore?: EvidenceStore;
16
+ }
17
+ export declare function createEvidencePublisher(options?: EvidencePublisherFactoryOptions): Promise<EvidencePublisher>;
18
+ /**
19
+ * Simple, synchronous configuration for publishers that store evidence via an
20
+ * HTTP multipart upload endpoint (Kubo, Pinata v3, or any compatible service).
21
+ */
22
+ export interface EvidencePublisherConfig {
23
+ /** Base URL of the upload endpoint (e.g. https://uploads.pinata.cloud/v3). */
24
+ endpoint: string;
25
+ /** Authentication strategy. */
26
+ auth: RequestAuth;
27
+ /** Extra request headers applied to every upload. */
28
+ headers?: Record<string, string>;
29
+ /** Path relative to endpoint for uploads. Defaults to /files. */
30
+ requestPath?: string;
31
+ /** Multipart field name for the file blob. Defaults to "file". */
32
+ fileFieldName?: string;
33
+ /** Provider-specific extra fields sent with every upload request. */
34
+ fields?: RequestFields;
35
+ /** IPFS gateway base URLs used to build gateway links in results. */
36
+ gatewayBaseUrls?: string[];
37
+ /** Override the serialization of the upload request. */
38
+ serializeRequest?: (request: UploadRequest) => BodyInit;
39
+ /** Override CID extraction from the upload response. */
40
+ parseResponse?: (responseBody: unknown) => string | undefined;
41
+ /**
42
+ * Base URL for pin-by-CID requests. When set, enables {@link EvidencePublisher.pinCid}.
43
+ * For Pinata v3 this is https://api.pinata.cloud/v3 (path defaults to /files/public/pin_by_cid).
44
+ */
45
+ pinByCidEndpoint?: string;
46
+ /** Request path for pin-by-CID relative to pinByCidEndpoint. Defaults to /files/public/pin_by_cid. */
47
+ pinByCidRequestPath?: string;
48
+ /**
49
+ * Optional remote pinning step applied automatically after every publish.
50
+ * Works across all upload backends — the produced CID is forwarded to the
51
+ * configured remote pinning service.
52
+ */
53
+ remotePinning?: RemotePinningConfig;
54
+ }
55
+ /**
56
+ * Create an {@link EvidencePublisher} backed by an HTTP multipart upload endpoint.
57
+ * No file reads or async setup required — suitable for browser or edge environments.
58
+ */
59
+ export declare function createHttpEvidencePublisher(config: EvidencePublisherConfig): EvidencePublisher;
60
+ export interface MetaEvidencePublisherFactoryOptions extends StorageConfigReadOptions {
61
+ /** Explicit config supplied by code. When present, file reads are bypassed. */
62
+ config?: StorageConfig;
63
+ /** Optional YAML file path used only when `config` is not supplied. */
64
+ configFilePath?: string;
65
+ gatewayBaseUrls?: string[];
66
+ attachmentStore?: AttachmentStore;
67
+ documentStore?: MetaEvidenceStore;
68
+ }
69
+ /**
70
+ * Create a {@link MetaEvidencePublisher} using the same config-file / Helia
71
+ * resolution rules as {@link createEvidencePublisher}.
72
+ */
73
+ export declare function createMetaEvidencePublisher(options?: MetaEvidencePublisherFactoryOptions): Promise<MetaEvidencePublisher>;
74
+ /**
75
+ * Create a {@link MetaEvidencePublisher} backed by an HTTP multipart upload
76
+ * endpoint. No file reads or async setup required — suitable for browser or
77
+ * edge environments.
78
+ *
79
+ * Accepts the same {@link EvidencePublisherConfig} as
80
+ * {@link createHttpEvidencePublisher} so callers can share a single config
81
+ * object for both document types.
82
+ */
83
+ export declare function createHttpMetaEvidencePublisher(config: EvidencePublisherConfig): MetaEvidencePublisher;
@@ -0,0 +1,251 @@
1
+ import { join } from 'node:path';
2
+ import { readStorageConfigFile } from './config.js';
3
+ import { EvidenceHasher } from './EvidenceHasher.js';
4
+ import { EvidencePublisher } from './EvidencePublisher.js';
5
+ import { MetaEvidencePublisher } from './MetaEvidencePublisher.js';
6
+ import { HeliaAttachmentStore } from './helia/HeliaAttachmentStore.js';
7
+ import { HeliaEvidenceStore } from './helia/HeliaEvidenceStore.js';
8
+ import { HeliaIpfsClient } from './helia/HeliaIpfsClient.js';
9
+ import { HttpMultipartUploadClient } from './http/HttpMultipartUploadClient.js';
10
+ import { HttpPinByCidClient } from './http/HttpPinByCidClient.js';
11
+ export async function createEvidencePublisher(options = {}) {
12
+ const { config, configFilePath, gatewayBaseUrls, attachmentStore, documentStore, ...readOptions } = options;
13
+ const resolvedConfig = await resolveConfig(config, configFilePath, readOptions);
14
+ // ── content addressing: auto-dispatch on whether a provider URL is present ──
15
+ if (resolvedConfig.addressing === 'content') {
16
+ if (!resolvedConfig.provider.url) {
17
+ // No URL → in-process Helia node
18
+ const heliaClient = new HeliaIpfsClient({
19
+ provider: resolvedConfig.provider,
20
+ gatewayBaseUrls,
21
+ });
22
+ return new EvidencePublisher({
23
+ attachmentStore: attachmentStore ?? new HeliaAttachmentStore(heliaClient),
24
+ documentStore: documentStore ?? new HeliaEvidenceStore(heliaClient),
25
+ onClose: () => heliaClient.stop(),
26
+ ...(resolvedConfig.remotePinning ? { remotePinning: resolvedConfig.remotePinning } : {}),
27
+ });
28
+ }
29
+ // URL present → HTTP multipart upload (Pinata, Kubo, or any compatible provider)
30
+ const uploadClient = new HttpMultipartUploadClient({
31
+ providerUrl: resolvedConfig.provider.url,
32
+ auth: resolvedConfig.provider.auth,
33
+ ...(resolvedConfig.provider.headers ? { headers: resolvedConfig.provider.headers } : {}),
34
+ ...(resolvedConfig.provider.fields ? { fields: resolvedConfig.provider.fields } : {}),
35
+ ...(resolvedConfig.provider.fileFieldName ? { fileFieldName: resolvedConfig.provider.fileFieldName } : {}),
36
+ gatewayBaseUrls,
37
+ });
38
+ return new EvidencePublisher({
39
+ attachmentStore: attachmentStore ?? new MultipartAttachmentStore(uploadClient),
40
+ documentStore: documentStore ?? new MultipartEvidenceStore(uploadClient),
41
+ ...(resolvedConfig.remotePinning ? { remotePinning: resolvedConfig.remotePinning } : {}),
42
+ });
43
+ }
44
+ // ── location addressing: requires caller-supplied stores ──────────────────
45
+ if (!documentStore) {
46
+ throw new Error('location addressing requires a custom documentStore to be supplied');
47
+ }
48
+ return new EvidencePublisher({
49
+ ...(attachmentStore ? { attachmentStore } : {}),
50
+ documentStore,
51
+ ...(resolvedConfig.remotePinning ? { remotePinning: resolvedConfig.remotePinning } : {}),
52
+ });
53
+ }
54
+ async function resolveConfig(config, configFilePath, readOptions) {
55
+ const envFilePath = readOptions.envFilePath ?? join(process.cwd(), '.env');
56
+ if (config) {
57
+ return config;
58
+ }
59
+ if (!configFilePath) {
60
+ return readStorageConfigFile(join(process.cwd(), 'evidence.storage.yml'), {
61
+ env: readOptions.env,
62
+ envFilePath,
63
+ });
64
+ }
65
+ return readStorageConfigFile(configFilePath, {
66
+ env: readOptions.env,
67
+ envFilePath,
68
+ });
69
+ }
70
+ /**
71
+ * Create an {@link EvidencePublisher} backed by an HTTP multipart upload endpoint.
72
+ * No file reads or async setup required — suitable for browser or edge environments.
73
+ */
74
+ export function createHttpEvidencePublisher(config) {
75
+ const uploadClient = new HttpMultipartUploadClient({
76
+ providerUrl: config.endpoint,
77
+ auth: config.auth,
78
+ headers: config.headers,
79
+ requestPath: config.requestPath,
80
+ fileFieldName: config.fileFieldName,
81
+ fields: config.fields,
82
+ gatewayBaseUrls: config.gatewayBaseUrls,
83
+ ...(config.serializeRequest ? { serializeRequestModel: config.serializeRequest } : {}),
84
+ ...(config.parseResponse ? { parseResponse: config.parseResponse } : {}),
85
+ });
86
+ const pinByCidClient = config.pinByCidEndpoint
87
+ ? new HttpPinByCidClient({
88
+ providerUrl: config.pinByCidEndpoint,
89
+ auth: config.auth,
90
+ headers: config.headers,
91
+ requestPath: config.pinByCidRequestPath,
92
+ })
93
+ : undefined;
94
+ return new EvidencePublisher({
95
+ attachmentStore: new MultipartAttachmentStore(uploadClient),
96
+ documentStore: new MultipartEvidenceStore(uploadClient),
97
+ ...(pinByCidClient ? { pinByCidClient } : {}),
98
+ ...(config.remotePinning ? { remotePinning: config.remotePinning } : {}),
99
+ });
100
+ }
101
+ /**
102
+ * Create a {@link MetaEvidencePublisher} using the same config-file / Helia
103
+ * resolution rules as {@link createEvidencePublisher}.
104
+ */
105
+ export async function createMetaEvidencePublisher(options = {}) {
106
+ const { config, configFilePath, gatewayBaseUrls, attachmentStore, documentStore, ...readOptions } = options;
107
+ const resolvedConfig = await resolveConfig(config, configFilePath, readOptions);
108
+ if (resolvedConfig.addressing === 'content') {
109
+ if (!resolvedConfig.provider.url) {
110
+ const heliaClient = new HeliaIpfsClient({
111
+ provider: resolvedConfig.provider,
112
+ gatewayBaseUrls,
113
+ });
114
+ return new MetaEvidencePublisher({
115
+ attachmentStore: attachmentStore ?? new HeliaAttachmentStore(heliaClient),
116
+ documentStore: documentStore ?? new HeliaMetaEvidenceStore(heliaClient),
117
+ onClose: () => heliaClient.stop(),
118
+ ...(resolvedConfig.remotePinning ? { remotePinning: resolvedConfig.remotePinning } : {}),
119
+ });
120
+ }
121
+ const uploadClient = new HttpMultipartUploadClient({
122
+ providerUrl: resolvedConfig.provider.url,
123
+ auth: resolvedConfig.provider.auth,
124
+ ...(resolvedConfig.provider.headers ? { headers: resolvedConfig.provider.headers } : {}),
125
+ ...(resolvedConfig.provider.fields ? { fields: resolvedConfig.provider.fields } : {}),
126
+ ...(resolvedConfig.provider.fileFieldName ? { fileFieldName: resolvedConfig.provider.fileFieldName } : {}),
127
+ gatewayBaseUrls,
128
+ });
129
+ return new MetaEvidencePublisher({
130
+ attachmentStore: attachmentStore ?? new MultipartAttachmentStore(uploadClient),
131
+ documentStore: documentStore ?? new MultipartMetaEvidenceStore(uploadClient),
132
+ ...(resolvedConfig.remotePinning ? { remotePinning: resolvedConfig.remotePinning } : {}),
133
+ });
134
+ }
135
+ if (!documentStore) {
136
+ throw new Error('location addressing requires a custom documentStore to be supplied');
137
+ }
138
+ return new MetaEvidencePublisher({
139
+ ...(attachmentStore ? { attachmentStore } : {}),
140
+ documentStore,
141
+ ...(resolvedConfig.remotePinning ? { remotePinning: resolvedConfig.remotePinning } : {}),
142
+ });
143
+ }
144
+ /**
145
+ * Create a {@link MetaEvidencePublisher} backed by an HTTP multipart upload
146
+ * endpoint. No file reads or async setup required — suitable for browser or
147
+ * edge environments.
148
+ *
149
+ * Accepts the same {@link EvidencePublisherConfig} as
150
+ * {@link createHttpEvidencePublisher} so callers can share a single config
151
+ * object for both document types.
152
+ */
153
+ export function createHttpMetaEvidencePublisher(config) {
154
+ const uploadClient = new HttpMultipartUploadClient({
155
+ providerUrl: config.endpoint,
156
+ auth: config.auth,
157
+ headers: config.headers,
158
+ requestPath: config.requestPath,
159
+ fileFieldName: config.fileFieldName,
160
+ fields: config.fields,
161
+ gatewayBaseUrls: config.gatewayBaseUrls,
162
+ ...(config.serializeRequest ? { serializeRequestModel: config.serializeRequest } : {}),
163
+ ...(config.parseResponse ? { parseResponse: config.parseResponse } : {}),
164
+ });
165
+ const pinByCidClient = config.pinByCidEndpoint
166
+ ? new HttpPinByCidClient({
167
+ providerUrl: config.pinByCidEndpoint,
168
+ auth: config.auth,
169
+ headers: config.headers,
170
+ requestPath: config.pinByCidRequestPath,
171
+ })
172
+ : undefined;
173
+ return new MetaEvidencePublisher({
174
+ attachmentStore: new MultipartAttachmentStore(uploadClient),
175
+ documentStore: new MultipartMetaEvidenceStore(uploadClient),
176
+ ...(pinByCidClient ? { pinByCidClient } : {}),
177
+ ...(config.remotePinning ? { remotePinning: config.remotePinning } : {}),
178
+ });
179
+ }
180
+ // ─── Internal store adapters ─────────────────────────────────────────────────
181
+ class MultipartAttachmentStore {
182
+ client;
183
+ constructor(client) {
184
+ this.client = client;
185
+ }
186
+ async putAttachment(input) {
187
+ const added = await this.client.addBytes(input.bytes, {
188
+ fileName: input.fileName,
189
+ mediaType: input.mediaType,
190
+ });
191
+ return {
192
+ cid: added.cid,
193
+ uri: added.uri,
194
+ fileHash: EvidenceHasher.hashBytes(input.bytes),
195
+ fileTypeExtension: input.fileTypeExtension,
196
+ mediaType: input.mediaType,
197
+ sizeBytes: input.bytes.length,
198
+ gatewayUrls: added.gatewayUrls,
199
+ };
200
+ }
201
+ }
202
+ class MultipartEvidenceStore {
203
+ client;
204
+ constructor(client) {
205
+ this.client = client;
206
+ }
207
+ async putEvidenceDocument(document) {
208
+ const added = await this.client.addBytes(EvidenceHasher.serialize(document), {
209
+ fileName: 'evidence.json',
210
+ mediaType: 'application/json',
211
+ });
212
+ return {
213
+ cid: added.cid,
214
+ uri: added.uri,
215
+ gatewayUrls: added.gatewayUrls,
216
+ };
217
+ }
218
+ }
219
+ class MultipartMetaEvidenceStore {
220
+ client;
221
+ constructor(client) {
222
+ this.client = client;
223
+ }
224
+ async putMetaEvidenceDocument(document) {
225
+ const bytes = new TextEncoder().encode(JSON.stringify(document));
226
+ const added = await this.client.addBytes(bytes, {
227
+ fileName: 'metaEvidence.json',
228
+ mediaType: 'application/json',
229
+ });
230
+ return {
231
+ cid: added.cid,
232
+ uri: added.uri,
233
+ gatewayUrls: added.gatewayUrls,
234
+ };
235
+ }
236
+ }
237
+ class HeliaMetaEvidenceStore {
238
+ client;
239
+ constructor(client) {
240
+ this.client = client;
241
+ }
242
+ async putMetaEvidenceDocument(document) {
243
+ const bytes = new TextEncoder().encode(JSON.stringify(document));
244
+ const added = await this.client.addBytes(bytes);
245
+ return {
246
+ cid: added.cid,
247
+ uri: added.uri,
248
+ gatewayUrls: added.gatewayUrls,
249
+ };
250
+ }
251
+ }
@@ -0,0 +1,25 @@
1
+ import type { MetaEvidence, MetaEvidenceDraft, PublishedAttachment } from './types.js';
2
+ /**
3
+ * Builds the canonical {@link MetaEvidence} JSON object from a caller-supplied
4
+ * draft. Enforces required fields and normalizes optional ones.
5
+ *
6
+ * Business-specific defaults (escrow roles, dispute categories, ruling labels)
7
+ * are intentionally **not** provided here. Defaults belong in consuming SDKs
8
+ * or applications, not in the shared evidence SDK.
9
+ */
10
+ export declare class MetaEvidenceJsonBuilder {
11
+ /**
12
+ * Build a MetaEvidence document from a draft.
13
+ * Required fields (`category`, `title`, `description`, `question`,
14
+ * `rulingOptions`) are validated. Optional fields are included only when
15
+ * non-blank (strings) or truthy (numbers / objects).
16
+ */
17
+ static build(draft: MetaEvidenceDraft): MetaEvidence;
18
+ /**
19
+ * Build a MetaEvidence document for the assisted publish path.
20
+ * Attachment-derived metadata (`fileURI`, `fileHash`, `fileTypeExtension`)
21
+ * is injected from the published attachment, overriding any values that
22
+ * may have been supplied on the draft.
23
+ */
24
+ static withAttachment(draft: MetaEvidenceDraft, attachment: PublishedAttachment): MetaEvidence;
25
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Builds the canonical {@link MetaEvidence} JSON object from a caller-supplied
3
+ * draft. Enforces required fields and normalizes optional ones.
4
+ *
5
+ * Business-specific defaults (escrow roles, dispute categories, ruling labels)
6
+ * are intentionally **not** provided here. Defaults belong in consuming SDKs
7
+ * or applications, not in the shared evidence SDK.
8
+ */
9
+ export class MetaEvidenceJsonBuilder {
10
+ /**
11
+ * Build a MetaEvidence document from a draft.
12
+ * Required fields (`category`, `title`, `description`, `question`,
13
+ * `rulingOptions`) are validated. Optional fields are included only when
14
+ * non-blank (strings) or truthy (numbers / objects).
15
+ */
16
+ static build(draft) {
17
+ const category = requireNonBlank(draft.category, 'category');
18
+ const title = requireNonBlank(draft.title, 'title');
19
+ const description = requireNonBlank(draft.description, 'description');
20
+ const question = requireNonBlank(draft.question, 'question');
21
+ if (!draft.rulingOptions) {
22
+ throw new Error('rulingOptions must be provided');
23
+ }
24
+ const fileURI = normalizeString(draft.fileURI);
25
+ const fileHash = normalizeString(draft.fileHash);
26
+ const fileTypeExtension = normalizeString(draft.fileTypeExtension);
27
+ if (!fileURI && (fileHash || fileTypeExtension)) {
28
+ throw new Error('fileHash and fileTypeExtension require fileURI');
29
+ }
30
+ // Build the document with stable key ordering that matches the Kleros
31
+ // Court expected shape (category first, then title/description, etc.)
32
+ const doc = {
33
+ category,
34
+ title,
35
+ description,
36
+ question,
37
+ rulingOptions: draft.rulingOptions,
38
+ };
39
+ if (draft.aliases && Object.keys(draft.aliases).length > 0) {
40
+ doc.aliases = draft.aliases;
41
+ }
42
+ if (fileURI) {
43
+ doc.fileURI = fileURI;
44
+ if (fileHash)
45
+ doc.fileHash = fileHash;
46
+ if (fileTypeExtension)
47
+ doc.fileTypeExtension = fileTypeExtension;
48
+ }
49
+ const evidenceDisplayInterfaceURI = normalizeString(draft.evidenceDisplayInterfaceURI);
50
+ if (evidenceDisplayInterfaceURI) {
51
+ doc.evidenceDisplayInterfaceURI = evidenceDisplayInterfaceURI;
52
+ const evidenceDisplayInterfaceHash = normalizeString(draft.evidenceDisplayInterfaceHash);
53
+ if (evidenceDisplayInterfaceHash)
54
+ doc.evidenceDisplayInterfaceHash = evidenceDisplayInterfaceHash;
55
+ }
56
+ const dynamicScriptURI = normalizeString(draft.dynamicScriptURI);
57
+ if (dynamicScriptURI) {
58
+ doc.dynamicScriptURI = dynamicScriptURI;
59
+ const dynamicScriptHash = normalizeString(draft.dynamicScriptHash);
60
+ if (dynamicScriptHash)
61
+ doc.dynamicScriptHash = dynamicScriptHash;
62
+ }
63
+ const arbitrableInterfaceURI = normalizeString(draft.arbitrableInterfaceURI);
64
+ if (arbitrableInterfaceURI)
65
+ doc.arbitrableInterfaceURI = arbitrableInterfaceURI;
66
+ if (draft.arbitrableChainID != null)
67
+ doc.arbitrableChainID = draft.arbitrableChainID;
68
+ if (draft.arbitratorChainID != null)
69
+ doc.arbitratorChainID = draft.arbitratorChainID;
70
+ const arbitrableJsonRpcUrl = normalizeString(draft.arbitrableJsonRpcUrl);
71
+ if (arbitrableJsonRpcUrl)
72
+ doc.arbitrableJsonRpcUrl = arbitrableJsonRpcUrl;
73
+ const arbitratorJsonRpcUrl = normalizeString(draft.arbitratorJsonRpcUrl);
74
+ if (arbitratorJsonRpcUrl)
75
+ doc.arbitratorJsonRpcUrl = arbitratorJsonRpcUrl;
76
+ if (draft._v != null)
77
+ doc._v = draft._v;
78
+ return doc;
79
+ }
80
+ /**
81
+ * Build a MetaEvidence document for the assisted publish path.
82
+ * Attachment-derived metadata (`fileURI`, `fileHash`, `fileTypeExtension`)
83
+ * is injected from the published attachment, overriding any values that
84
+ * may have been supplied on the draft.
85
+ */
86
+ static withAttachment(draft, attachment) {
87
+ return this.build({
88
+ ...draft,
89
+ fileURI: attachment.uri,
90
+ fileHash: attachment.fileHash,
91
+ fileTypeExtension: attachment.fileTypeExtension,
92
+ });
93
+ }
94
+ }
95
+ // ── Helpers ──────────────────────────────────────────────────────────────────
96
+ function requireNonBlank(value, name) {
97
+ if (!value?.trim()) {
98
+ throw new Error(`${name} must not be blank`);
99
+ }
100
+ return value.trim();
101
+ }
102
+ function normalizeString(value) {
103
+ return value?.trim() ? value.trim() : undefined;
104
+ }
@@ -0,0 +1,39 @@
1
+ import { HttpPinByCidClient } from './http/HttpPinByCidClient.js';
2
+ import type { AttachmentStore, MetaEvidenceStore } from './storage-types.js';
3
+ import type { Attachment, CidInput, MetaEvidencePublishRequest, MetaEvidencePublishResult, PinResult, PublishedAttachment, RemotePinningConfig } from './types.js';
4
+ export interface MetaEvidencePublisherOptions {
5
+ attachmentStore?: AttachmentStore;
6
+ documentStore: MetaEvidenceStore;
7
+ /** Optional client for explicit CID pinning via {@link MetaEvidencePublisher.pinCid}. */
8
+ pinByCidClient?: HttpPinByCidClient;
9
+ /**
10
+ * Optional remote pinning configuration.
11
+ * When set (and not disabled), every produced CID is forwarded to the
12
+ * configured pinning service as a post-publish durability step.
13
+ */
14
+ remotePinning?: RemotePinningConfig;
15
+ /** Called by {@link MetaEvidencePublisher.close} to release underlying resources. */
16
+ onClose?: () => Promise<void>;
17
+ }
18
+ export declare class MetaEvidencePublisher {
19
+ private readonly options;
20
+ constructor(options: MetaEvidencePublisherOptions);
21
+ /**
22
+ * Publish a MetaEvidence document.
23
+ *
24
+ * - **Manual path**: supply the full draft (file fields optional).
25
+ * - **Assisted path**: include `attachment` on the request; the SDK uploads
26
+ * the attachment first and wires its URI / hash into the MetaEvidence JSON.
27
+ * Attachment-derived file metadata takes precedence over any file fields
28
+ * provided on the draft.
29
+ */
30
+ publish(input: MetaEvidencePublishRequest): Promise<MetaEvidencePublishResult>;
31
+ uploadAttachment(input: Attachment): Promise<PublishedAttachment>;
32
+ pinCid(input: CidInput): Promise<PinResult>;
33
+ /**
34
+ * Release underlying resources (e.g. stop the Helia node).
35
+ * Always call this when the publisher is no longer needed.
36
+ */
37
+ close(): Promise<void>;
38
+ private runRemotePinning;
39
+ }