@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,104 @@
1
+ import { MetaEvidenceJsonBuilder } from './MetaEvidenceJsonBuilder.js';
2
+ import { HttpPinByCidClient } from './http/HttpPinByCidClient.js';
3
+ export class MetaEvidencePublisher {
4
+ options;
5
+ constructor(options) {
6
+ this.options = options;
7
+ }
8
+ /**
9
+ * Publish a MetaEvidence document.
10
+ *
11
+ * - **Manual path**: supply the full draft (file fields optional).
12
+ * - **Assisted path**: include `attachment` on the request; the SDK uploads
13
+ * the attachment first and wires its URI / hash into the MetaEvidence JSON.
14
+ * Attachment-derived file metadata takes precedence over any file fields
15
+ * provided on the draft.
16
+ */
17
+ async publish(input) {
18
+ let documentJson;
19
+ let publishedAttachment;
20
+ if ('attachment' in input && input.attachment) {
21
+ if (!this.options.attachmentStore) {
22
+ throw new Error('attachmentStore is required when attachment is provided');
23
+ }
24
+ publishedAttachment = await this.options.attachmentStore.putAttachment(input.attachment);
25
+ documentJson = MetaEvidenceJsonBuilder.withAttachment(input, publishedAttachment);
26
+ }
27
+ else {
28
+ documentJson = MetaEvidenceJsonBuilder.build(input);
29
+ }
30
+ const publishedDocument = await this.options.documentStore.putMetaEvidenceDocument(documentJson);
31
+ const baseResult = {
32
+ documentJson,
33
+ document: publishedDocument,
34
+ ...(publishedAttachment ? { attachment: publishedAttachment } : {}),
35
+ };
36
+ const remotePinning = await this.runRemotePinning(publishedDocument, publishedAttachment);
37
+ return {
38
+ ...baseResult,
39
+ ...(remotePinning !== undefined ? { remotePinning } : {}),
40
+ };
41
+ }
42
+ async uploadAttachment(input) {
43
+ if (!this.options.attachmentStore) {
44
+ throw new Error('attachmentStore is required to upload attachments');
45
+ }
46
+ return this.options.attachmentStore.putAttachment(input);
47
+ }
48
+ async pinCid(input) {
49
+ if (!this.options.pinByCidClient) {
50
+ throw new Error('pinByCidClient is required to pin CIDs');
51
+ }
52
+ const result = await this.options.pinByCidClient.pinByCid({ cid: input.cid });
53
+ return {
54
+ cid: result.cid ?? input.cid,
55
+ ...(result.cid ? { uri: `ipfs://${result.cid}` } : {}),
56
+ };
57
+ }
58
+ /**
59
+ * Release underlying resources (e.g. stop the Helia node).
60
+ * Always call this when the publisher is no longer needed.
61
+ */
62
+ async close() {
63
+ await this.options.onClose?.();
64
+ }
65
+ // ── Remote pinning pipeline ──────────────────────────────────────────────
66
+ async runRemotePinning(document, attachment) {
67
+ const config = this.options.remotePinning;
68
+ if (!config || config.enabled === false) {
69
+ return undefined;
70
+ }
71
+ const client = new HttpPinByCidClient({
72
+ providerUrl: config.endpoint,
73
+ auth: config.auth,
74
+ requestPath: config.requestPath,
75
+ headers: config.headers,
76
+ });
77
+ try {
78
+ const docPinResponse = await client.pinByCid({ cid: document.cid });
79
+ const documentPin = {
80
+ cid: docPinResponse.cid ?? document.cid,
81
+ ...(docPinResponse.cid ? { uri: `ipfs://${docPinResponse.cid}` } : {}),
82
+ ...(docPinResponse.pinId ? { metadata: { pinId: docPinResponse.pinId } } : {}),
83
+ };
84
+ let attachmentPin;
85
+ if (attachment) {
86
+ const attPinResponse = await client.pinByCid({ cid: attachment.cid });
87
+ attachmentPin = {
88
+ cid: attPinResponse.cid ?? attachment.cid,
89
+ ...(attPinResponse.cid ? { uri: `ipfs://${attPinResponse.cid}` } : {}),
90
+ ...(attPinResponse.pinId ? { metadata: { pinId: attPinResponse.pinId } } : {}),
91
+ };
92
+ }
93
+ return {
94
+ documentPin,
95
+ ...(attachmentPin ? { attachmentPin } : {}),
96
+ };
97
+ }
98
+ catch (err) {
99
+ return {
100
+ error: err instanceof Error ? err : new Error(String(err)),
101
+ };
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,15 @@
1
+ export type { RequestAuth, RequestContext, RequestFieldValue, RequestFields, UploadRequest, PinByCidRequest, PinningRequest, Attachment, PublishedAttachment, PublishedEvidenceDocument, CidInput, PinResult, RemotePinningConfig, RemotePinOutcome, EvidenceDraft, EvidenceJson, EvidenceJsonDocument, EvidencePublishRequest, EvidencePublishResult, RulingOptionType, CourtEncodingVersion, RulingOptions, MetaEvidence, MetaEvidenceDraft, ManualMetaEvidencePublishRequest, AssistedMetaEvidencePublishRequest, MetaEvidencePublishRequest, MetaEvidencePublishResult, } from './types.js';
2
+ export type { ContentPublishOptions, ContentAddResult, AttachmentStore, EvidenceStore, MetaEvidenceStore } from './storage-types.js';
3
+ export type { StorageConfig, StorageConfigReadOptions, AddressingModel, ProviderConfig, PinningConfig, ProviderAuth, } from './config.js';
4
+ export { parseStorageConfig, readStorageConfigFile, normalizeStorageConfig, readEnvFile, EvidenceConfigError, } from './config.js';
5
+ export { EvidencePublisher } from './EvidencePublisher.js';
6
+ export type { EvidencePublisherOptions } from './EvidencePublisher.js';
7
+ export { MetaEvidencePublisher } from './MetaEvidencePublisher.js';
8
+ export type { MetaEvidencePublisherOptions } from './MetaEvidencePublisher.js';
9
+ export { MetaEvidenceJsonBuilder } from './MetaEvidenceJsonBuilder.js';
10
+ export type { EvidencePublisherFactoryOptions, EvidencePublisherConfig, MetaEvidencePublisherFactoryOptions, } from './EvidencePublisherFactory.js';
11
+ export { createHttpEvidencePublisher, createHttpMetaEvidencePublisher } from './EvidencePublisherFactory.js';
12
+ export { HttpMultipartUploadClient } from './http/HttpMultipartUploadClient.js';
13
+ export type { HttpMultipartUploadClientOptions, HttpMultipartUploadInput } from './http/HttpMultipartUploadClient.js';
14
+ export { HttpPinByCidClient } from './http/HttpPinByCidClient.js';
15
+ export type { HttpPinByCidClientOptions, HttpPinByCidResult } from './http/HttpPinByCidClient.js';
@@ -0,0 +1,7 @@
1
+ export { parseStorageConfig, readStorageConfigFile, normalizeStorageConfig, readEnvFile, EvidenceConfigError, } from './config.js';
2
+ export { EvidencePublisher } from './EvidencePublisher.js';
3
+ export { MetaEvidencePublisher } from './MetaEvidencePublisher.js';
4
+ export { MetaEvidenceJsonBuilder } from './MetaEvidenceJsonBuilder.js';
5
+ export { createHttpEvidencePublisher, createHttpMetaEvidencePublisher } from './EvidencePublisherFactory.js';
6
+ export { HttpMultipartUploadClient } from './http/HttpMultipartUploadClient.js';
7
+ export { HttpPinByCidClient } from './http/HttpPinByCidClient.js';
@@ -0,0 +1,51 @@
1
+ import type { RequestAuth, RemotePinningConfig } from './types.js';
2
+ export type AddressingModel = 'content' | 'location';
3
+ export type ProviderAuth = RequestAuth;
4
+ export interface ProviderConfig {
5
+ /** Semantic identifier for logs/debugging. */
6
+ name: string;
7
+ /** Target endpoint when there is one. */
8
+ url?: string;
9
+ /** Thin overridable auth strategy. */
10
+ auth: RequestAuth;
11
+ /**
12
+ * Provider-specific extra fields sent with every multipart upload request.
13
+ * Example: `{ network: "public" }` for Pinata v3.
14
+ */
15
+ fields?: Record<string, string>;
16
+ /**
17
+ * Extra HTTP headers applied to every upload request.
18
+ * Merged before per-request headers; per-request headers take precedence.
19
+ */
20
+ headers?: Record<string, string>;
21
+ /**
22
+ * Multipart field name for the file blob.
23
+ * Defaults to "file" when not set.
24
+ */
25
+ fileFieldName?: string;
26
+ }
27
+ export interface PinningConfig {
28
+ /** Whether the published content should be kept durable/pinned. */
29
+ enabled: boolean;
30
+ }
31
+ export interface StorageConfig {
32
+ addressing: AddressingModel;
33
+ provider: ProviderConfig;
34
+ pinning: PinningConfig;
35
+ /** Optional remote pinning config loaded from the YAML `remotePinning` section. */
36
+ remotePinning?: RemotePinningConfig;
37
+ }
38
+ export interface StorageConfigReadOptions {
39
+ /** Explicit environment map used to resolve placeholders. Defaults to process.env. */
40
+ env?: NodeJS.ProcessEnv;
41
+ /** Optional `.env` file loaded before resolving placeholders. */
42
+ envFilePath?: string;
43
+ }
44
+ export declare class EvidenceConfigError extends Error {
45
+ readonly source: string;
46
+ constructor(message: string, source: string);
47
+ }
48
+ export declare function readEnvFile(filePath: string): Promise<Record<string, string>>;
49
+ export declare function readStorageConfigFile(filePath: string, options?: StorageConfigReadOptions): Promise<StorageConfig>;
50
+ export declare function parseStorageConfig(raw: string, env?: NodeJS.ProcessEnv, source?: string): StorageConfig;
51
+ export declare function normalizeStorageConfig(value: unknown, source?: string): StorageConfig;
@@ -0,0 +1,245 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parse as parseDotEnv } from 'dotenv';
3
+ import { parse as parseYaml } from 'yaml';
4
+ export class EvidenceConfigError extends Error {
5
+ source;
6
+ constructor(message, source) {
7
+ super(`${message} (${source})`);
8
+ this.source = source;
9
+ this.name = 'EvidenceConfigError';
10
+ }
11
+ }
12
+ export async function readEnvFile(filePath) {
13
+ const raw = await readFile(filePath, 'utf8');
14
+ return parseDotEnv(raw);
15
+ }
16
+ export async function readStorageConfigFile(filePath, options = {}) {
17
+ const raw = await readFile(filePath, 'utf8');
18
+ const env = await loadConfigEnvironment(options.env, options.envFilePath);
19
+ return parseStorageConfig(raw, env, filePath);
20
+ }
21
+ export function parseStorageConfig(raw, env = process.env, source = '(inline config)') {
22
+ let parsed;
23
+ try {
24
+ parsed = parseYaml(raw);
25
+ }
26
+ catch (cause) {
27
+ throw new EvidenceConfigError(`Failed to parse YAML config: ${cause.message}`, source);
28
+ }
29
+ const resolved = resolveEnvironmentValues(parsed, toEnvRecord(env), source);
30
+ return normalizeStorageConfig(resolved, source);
31
+ }
32
+ export function normalizeStorageConfig(value, source = '(inline config)') {
33
+ const record = asRecord(value, source, 'config');
34
+ const addressing = normalizeAddressing(record.addressing, source);
35
+ const provider = normalizeProvider(record.provider, addressing, source);
36
+ const pinning = normalizePinning(record.pinning, addressing, source);
37
+ const remotePinning = normalizeRemotePinning(record.remotePinning, source);
38
+ return {
39
+ addressing,
40
+ provider,
41
+ pinning,
42
+ ...(remotePinning ? { remotePinning } : {}),
43
+ };
44
+ }
45
+ function normalizeAddressing(value, source) {
46
+ const addressing = readString(value, source, 'addressing');
47
+ if (addressing !== 'content' && addressing !== 'location') {
48
+ throw new EvidenceConfigError(`addressing must be 'content' or 'location'`, source);
49
+ }
50
+ return addressing;
51
+ }
52
+ function normalizeProvider(value, addressing, source) {
53
+ const record = asRecord(value, source, 'provider');
54
+ const name = readString(record.name, source, 'provider.name');
55
+ const url = optionalString(record.url, source, 'provider.url');
56
+ const auth = normalizeAuth(record.auth, source, 'provider.auth');
57
+ const fields = normalizeStringMap(record.fields, source, 'provider.fields');
58
+ const headers = normalizeStringMap(record.headers, source, 'provider.headers');
59
+ const fileFieldName = optionalString(record.fileFieldName, source, 'provider.fileFieldName');
60
+ if (addressing === 'location' && !url) {
61
+ throw new EvidenceConfigError(`provider.url is required when addressing is 'location'`, source);
62
+ }
63
+ if (url) {
64
+ validateUrl(url, source, 'provider.url');
65
+ }
66
+ return {
67
+ name,
68
+ ...(url ? { url } : {}),
69
+ auth,
70
+ ...(fields ? { fields } : {}),
71
+ ...(headers ? { headers } : {}),
72
+ ...(fileFieldName ? { fileFieldName } : {}),
73
+ };
74
+ }
75
+ function normalizeAuth(value, source, fieldPath = 'provider.auth') {
76
+ if (value === undefined || value === null) {
77
+ return { type: 'none' };
78
+ }
79
+ const record = asRecord(value, source, fieldPath);
80
+ const type = readString(record.type, source, `${fieldPath}.type`);
81
+ switch (type) {
82
+ case 'none':
83
+ return { type: 'none' };
84
+ case 'basic': {
85
+ const username = readString(record.username, source, `${fieldPath}.username`);
86
+ const password = readString(record.password, source, `${fieldPath}.password`);
87
+ return { type: 'basic', username, password };
88
+ }
89
+ case 'bearer': {
90
+ const token = readString(record.token, source, `${fieldPath}.token`);
91
+ return { type: 'bearer', token };
92
+ }
93
+ case 'header': {
94
+ const name = readString(record.name, source, `${fieldPath}.name`);
95
+ const value = readString(record.value, source, `${fieldPath}.value`);
96
+ return { type: 'header', name, value };
97
+ }
98
+ default:
99
+ throw new EvidenceConfigError(`${fieldPath}.type must be one of: none, basic, bearer, header`, source);
100
+ }
101
+ }
102
+ function normalizePinning(value, addressing, source) {
103
+ if (value === undefined || value === null) {
104
+ return {
105
+ enabled: addressing === 'content',
106
+ };
107
+ }
108
+ const record = asRecord(value, source, 'pinning');
109
+ const enabled = readBoolean(record.enabled, source, 'pinning.enabled');
110
+ return {
111
+ enabled,
112
+ };
113
+ }
114
+ function normalizeRemotePinning(value, source) {
115
+ if (value === undefined || value === null) {
116
+ return undefined;
117
+ }
118
+ const record = asRecord(value, source, 'remotePinning');
119
+ const endpoint = readString(record.endpoint, source, 'remotePinning.endpoint');
120
+ validateUrl(endpoint, source, 'remotePinning.endpoint');
121
+ const auth = normalizeAuth(record.auth, source, 'remotePinning.auth');
122
+ const requestPath = optionalString(record.requestPath, source, 'remotePinning.requestPath');
123
+ const headers = normalizeStringMap(record.headers, source, 'remotePinning.headers');
124
+ let enabled;
125
+ if (record.enabled !== undefined && record.enabled !== null) {
126
+ enabled = readBoolean(record.enabled, source, 'remotePinning.enabled');
127
+ }
128
+ return {
129
+ endpoint,
130
+ auth,
131
+ ...(requestPath ? { requestPath } : {}),
132
+ ...(headers ? { headers } : {}),
133
+ ...(enabled !== undefined ? { enabled } : {}),
134
+ };
135
+ }
136
+ function normalizeStringMap(value, source, field) {
137
+ if (value === undefined || value === null) {
138
+ return undefined;
139
+ }
140
+ const record = asRecord(value, source, field);
141
+ const result = {};
142
+ for (const [key, item] of Object.entries(record)) {
143
+ if (typeof item !== 'string') {
144
+ throw new EvidenceConfigError(`${field}.${key} must be a string`, source);
145
+ }
146
+ result[key] = item;
147
+ }
148
+ return result;
149
+ }
150
+ async function loadConfigEnvironment(env, envFilePath) {
151
+ const fromFile = envFilePath ? await readEnvFileIfPresent(envFilePath) : {};
152
+ const fromProcess = toEnvRecord(process.env);
153
+ const fromOptions = toEnvRecord(env ?? process.env);
154
+ return {
155
+ ...fromFile,
156
+ ...fromProcess,
157
+ ...fromOptions,
158
+ };
159
+ }
160
+ async function readEnvFileIfPresent(filePath) {
161
+ try {
162
+ return await readEnvFile(filePath);
163
+ }
164
+ catch (error) {
165
+ if (isMissingFileError(error)) {
166
+ return {};
167
+ }
168
+ throw error;
169
+ }
170
+ }
171
+ function resolveEnvironmentValues(value, env, source, path = 'config') {
172
+ if (typeof value === 'string') {
173
+ return resolveEnvPlaceholders(value, env, source, path);
174
+ }
175
+ if (Array.isArray(value)) {
176
+ return value.map((item, index) => resolveEnvironmentValues(item, env, source, `${path}[${index}]`));
177
+ }
178
+ if (typeof value === 'object' && value !== null) {
179
+ const resolved = {};
180
+ for (const [key, item] of Object.entries(value)) {
181
+ resolved[key] = resolveEnvironmentValues(item, env, source, `${path}.${key}`);
182
+ }
183
+ return resolved;
184
+ }
185
+ return value;
186
+ }
187
+ function resolveEnvPlaceholders(value, env, source, path) {
188
+ const placeholderPattern = /\$\{([^}]+)}/g;
189
+ return value.replace(placeholderPattern, (_match, rawName) => {
190
+ const name = rawName.trim();
191
+ const resolved = env[name];
192
+ if (resolved === undefined || resolved.trim() === '') {
193
+ throw new EvidenceConfigError(`Environment variable ${name} referenced by ${path} was not found or was blank`, source);
194
+ }
195
+ return resolved;
196
+ });
197
+ }
198
+ function validateUrl(url, source, field) {
199
+ try {
200
+ // Ensure the URL is syntactically valid, but do not constrain protocol.
201
+ // eslint-disable-next-line no-new
202
+ new URL(url);
203
+ }
204
+ catch {
205
+ throw new EvidenceConfigError(`${field} must be a valid absolute URL`, source);
206
+ }
207
+ }
208
+ function asRecord(value, source, field) {
209
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
210
+ throw new EvidenceConfigError(`${field} must be an object`, source);
211
+ }
212
+ return value;
213
+ }
214
+ function readString(value, source, field) {
215
+ if (typeof value !== 'string') {
216
+ throw new EvidenceConfigError(`${field} must be a string`, source);
217
+ }
218
+ if (value.trim() === '') {
219
+ throw new EvidenceConfigError(`${field} must not be blank`, source);
220
+ }
221
+ return value;
222
+ }
223
+ function optionalString(value, source, field) {
224
+ if (value === undefined || value === null)
225
+ return undefined;
226
+ return readString(value, source, field);
227
+ }
228
+ function readBoolean(value, source, field) {
229
+ if (typeof value !== 'boolean') {
230
+ throw new EvidenceConfigError(`${field} must be a boolean`, source);
231
+ }
232
+ return value;
233
+ }
234
+ function toEnvRecord(env) {
235
+ const record = {};
236
+ for (const [key, value] of Object.entries(env)) {
237
+ if (typeof value === 'string' && value.length > 0) {
238
+ record[key] = value;
239
+ }
240
+ }
241
+ return record;
242
+ }
243
+ function isMissingFileError(error) {
244
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
245
+ }
@@ -0,0 +1,8 @@
1
+ import { HeliaIpfsClient, type HeliaIpfsClientOptions } from './HeliaIpfsClient.js';
2
+ import type { AttachmentStore } from '../storage-types.js';
3
+ import type { Attachment, PublishedAttachment } from '../types.js';
4
+ export declare class HeliaAttachmentStore implements AttachmentStore {
5
+ private readonly client;
6
+ constructor(options: HeliaIpfsClientOptions | HeliaIpfsClient);
7
+ putAttachment(input: Attachment): Promise<PublishedAttachment>;
8
+ }
@@ -0,0 +1,20 @@
1
+ import { HeliaIpfsClient } from './HeliaIpfsClient.js';
2
+ import { EvidenceHasher } from '../EvidenceHasher.js';
3
+ export class HeliaAttachmentStore {
4
+ client;
5
+ constructor(options) {
6
+ this.client = options instanceof HeliaIpfsClient ? options : new HeliaIpfsClient(options);
7
+ }
8
+ async putAttachment(input) {
9
+ const added = await this.client.addBytes(input.bytes);
10
+ return {
11
+ cid: added.cid,
12
+ uri: added.uri,
13
+ fileHash: EvidenceHasher.hashBytes(input.bytes),
14
+ fileTypeExtension: input.fileTypeExtension,
15
+ mediaType: input.mediaType,
16
+ sizeBytes: input.bytes.length,
17
+ gatewayUrls: added.gatewayUrls,
18
+ };
19
+ }
20
+ }
@@ -0,0 +1,8 @@
1
+ import { HeliaIpfsClient, type HeliaIpfsClientOptions } from './HeliaIpfsClient.js';
2
+ import type { EvidenceStore } from '../storage-types.js';
3
+ import type { EvidenceJsonDocument, PublishedEvidenceDocument } from '../types.js';
4
+ export declare class HeliaEvidenceStore implements EvidenceStore {
5
+ private readonly client;
6
+ constructor(options: HeliaIpfsClientOptions | HeliaIpfsClient);
7
+ putEvidenceDocument(document: EvidenceJsonDocument): Promise<PublishedEvidenceDocument>;
8
+ }
@@ -0,0 +1,16 @@
1
+ import { HeliaIpfsClient } from './HeliaIpfsClient.js';
2
+ import { EvidenceHasher } from '../EvidenceHasher.js';
3
+ export class HeliaEvidenceStore {
4
+ client;
5
+ constructor(options) {
6
+ this.client = options instanceof HeliaIpfsClient ? options : new HeliaIpfsClient(options);
7
+ }
8
+ async putEvidenceDocument(document) {
9
+ const added = await this.client.addBytes(EvidenceHasher.serialize(document));
10
+ return {
11
+ cid: added.cid,
12
+ uri: added.uri,
13
+ gatewayUrls: added.gatewayUrls,
14
+ };
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import type { ContentAddResult, ContentPublishOptions } from '../storage-types.js';
2
+ export interface HeliaIpfsClientOptions extends ContentPublishOptions {
3
+ helia?: unknown;
4
+ }
5
+ export declare class HeliaIpfsClient {
6
+ private readonly options;
7
+ private nodePromise?;
8
+ constructor(options: HeliaIpfsClientOptions);
9
+ addBytes(bytes: Uint8Array): Promise<ContentAddResult>;
10
+ private getHeliaNode;
11
+ private initializeHeliaNode;
12
+ stop(): Promise<void>;
13
+ private pinCid;
14
+ }
15
+ export declare function buildGatewayUrls(cid: string, gatewayBaseUrls?: string[]): string[];
@@ -0,0 +1,63 @@
1
+ import { createHelia } from 'helia';
2
+ import { unixfs } from '@helia/unixfs';
3
+ export class HeliaIpfsClient {
4
+ options;
5
+ nodePromise;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ async addBytes(bytes) {
10
+ const node = await this.getHeliaNode();
11
+ const fs = unixfs(node);
12
+ const cid = await fs.addBytes(bytes);
13
+ const cidText = cid.toString();
14
+ if (this.options.pinning?.enabled) {
15
+ await this.pinCid(node, cid);
16
+ }
17
+ return {
18
+ cid: cidText,
19
+ uri: `ipfs://${cidText}`,
20
+ gatewayUrls: buildGatewayUrls(cidText, this.options.gatewayBaseUrls),
21
+ };
22
+ }
23
+ getHeliaNode() {
24
+ if (!this.nodePromise) {
25
+ this.nodePromise = this.initializeHeliaNode();
26
+ }
27
+ return this.nodePromise;
28
+ }
29
+ async initializeHeliaNode() {
30
+ return this.options.helia ?? await createHelia();
31
+ }
32
+ async stop() {
33
+ if (!this.nodePromise)
34
+ return;
35
+ const node = await this.nodePromise;
36
+ const n = node;
37
+ if (typeof n.stop === 'function') {
38
+ await n.stop();
39
+ }
40
+ this.nodePromise = undefined;
41
+ }
42
+ async pinCid(node, cid) {
43
+ const pins = node.pins;
44
+ if (!pins?.add) {
45
+ throw new Error('Helia node does not expose pins.add(...)');
46
+ }
47
+ for await (const _ of pins.add(cid, { signal: AbortSignal.timeout(5000) })) {
48
+ // Consume async iterator so the pin operation completes.
49
+ }
50
+ }
51
+ }
52
+ export function buildGatewayUrls(cid, gatewayBaseUrls) {
53
+ if (!gatewayBaseUrls || gatewayBaseUrls.length === 0) {
54
+ return [];
55
+ }
56
+ return gatewayBaseUrls.map((baseUrl) => {
57
+ const trimmed = baseUrl.trim();
58
+ if (trimmed.includes('{cid}')) {
59
+ return trimmed.replace(/\{cid}/g, cid);
60
+ }
61
+ return `${trimmed.replace(/\/+$/, '')}/ipfs/${cid}`;
62
+ });
63
+ }
@@ -0,0 +1,8 @@
1
+ import type { AttachmentStore } from '../storage-types.js';
2
+ import type { Attachment, PublishedAttachment } from '../types.js';
3
+ import { HttpIpfsClient, type HttpIpfsClientOptions } from './HttpIpfsClient.js';
4
+ export declare class HttpIpfsAttachmentStore implements AttachmentStore {
5
+ private readonly client;
6
+ constructor(options: HttpIpfsClientOptions | HttpIpfsClient);
7
+ putAttachment(input: Attachment): Promise<PublishedAttachment>;
8
+ }
@@ -0,0 +1,23 @@
1
+ import { EvidenceHasher } from '../EvidenceHasher.js';
2
+ import { HttpIpfsClient } from './HttpIpfsClient.js';
3
+ export class HttpIpfsAttachmentStore {
4
+ client;
5
+ constructor(options) {
6
+ this.client = options instanceof HttpIpfsClient ? options : new HttpIpfsClient(options);
7
+ }
8
+ async putAttachment(input) {
9
+ const added = await this.client.addBytes(input.bytes, {
10
+ fileName: input.fileName,
11
+ mediaType: input.mediaType,
12
+ });
13
+ return {
14
+ cid: added.cid,
15
+ uri: added.uri,
16
+ fileHash: EvidenceHasher.hashBytes(input.bytes),
17
+ fileTypeExtension: input.fileTypeExtension,
18
+ mediaType: input.mediaType,
19
+ sizeBytes: input.bytes.length,
20
+ gatewayUrls: added.gatewayUrls,
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,24 @@
1
+ import type { ContentAddResult, ContentPublishOptions } from '../storage-types.js';
2
+ export interface HttpIpfsClientOptions extends ContentPublishOptions {
3
+ /** Upload endpoint path relative to provider.url. Defaults to /api/v0/add. */
4
+ uploadPath?: string;
5
+ /** Multipart field name expected by the provider. Defaults to file. */
6
+ uploadFieldName?: string;
7
+ /** Additional request headers merged into the upload request. */
8
+ requestHeaders?: Record<string, string>;
9
+ /** Candidate JSON field names used to extract a CID from the response. */
10
+ responseCidFields?: string[];
11
+ }
12
+ export interface HttpIpfsUploadInput {
13
+ fileName?: string;
14
+ mediaType?: string;
15
+ }
16
+ export declare class HttpIpfsClient {
17
+ private readonly options;
18
+ private readonly apiEndpoint;
19
+ constructor(options: HttpIpfsClientOptions);
20
+ addBytes(bytes: Uint8Array, input?: HttpIpfsUploadInput): Promise<ContentAddResult>;
21
+ private buildUploadUrl;
22
+ private buildFormData;
23
+ private buildHeaders;
24
+ }