@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { buildGatewayUrls } from '../helia/HeliaIpfsClient.js';
|
|
2
|
+
export class HttpIpfsClient {
|
|
3
|
+
options;
|
|
4
|
+
apiEndpoint;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
const apiEndpoint = options.provider?.url?.trim();
|
|
8
|
+
if (!apiEndpoint) {
|
|
9
|
+
throw new Error('HttpIpfsClient requires provider.url');
|
|
10
|
+
}
|
|
11
|
+
this.apiEndpoint = apiEndpoint;
|
|
12
|
+
}
|
|
13
|
+
async addBytes(bytes, input = {}) {
|
|
14
|
+
const response = await fetch(this.buildUploadUrl(), {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: this.buildHeaders(),
|
|
17
|
+
body: this.buildFormData(bytes, input),
|
|
18
|
+
});
|
|
19
|
+
const responseBody = await response.text();
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`HTTP IPFS upload failed with status ${response.status} ${response.statusText}: ${responseBody || '(empty response)'}`);
|
|
22
|
+
}
|
|
23
|
+
const cid = extractCid(responseBody, this.options.responseCidFields);
|
|
24
|
+
if (!cid) {
|
|
25
|
+
throw new Error('HTTP IPFS upload succeeded but no CID was returned');
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
cid,
|
|
29
|
+
uri: `ipfs://${cid}`,
|
|
30
|
+
gatewayUrls: buildGatewayUrls(cid, this.options.gatewayBaseUrls),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
buildUploadUrl() {
|
|
34
|
+
return appendPathSegment(this.apiEndpoint, this.options.uploadPath ?? '/api/v0/add');
|
|
35
|
+
}
|
|
36
|
+
buildFormData(bytes, input) {
|
|
37
|
+
const fieldName = this.options.uploadFieldName ?? 'file';
|
|
38
|
+
const fileName = input.fileName?.trim() || 'evidence.bin';
|
|
39
|
+
const mediaType = input.mediaType?.trim() || 'application/octet-stream';
|
|
40
|
+
const blobPart = (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
|
|
41
|
+
? bytes.buffer
|
|
42
|
+
: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
43
|
+
const form = new FormData();
|
|
44
|
+
form.append(fieldName, new Blob([blobPart], { type: mediaType }), fileName);
|
|
45
|
+
return form;
|
|
46
|
+
}
|
|
47
|
+
buildHeaders() {
|
|
48
|
+
const headers = new Headers();
|
|
49
|
+
applyAuthHeader(headers, this.options.provider?.auth);
|
|
50
|
+
for (const [name, value] of Object.entries(this.options.requestHeaders ?? {})) {
|
|
51
|
+
if (value !== undefined && value !== null) {
|
|
52
|
+
headers.set(name, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return headers;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function extractCid(responseBody, responseCidFields) {
|
|
59
|
+
const trimmed = responseBody.trim();
|
|
60
|
+
if (!trimmed) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const candidates = responseCidFields && responseCidFields.length > 0
|
|
64
|
+
? responseCidFields
|
|
65
|
+
: ['Hash', 'IpfsHash', 'cid', 'CID', 'Cid'];
|
|
66
|
+
let payload = trimmed;
|
|
67
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
68
|
+
try {
|
|
69
|
+
payload = JSON.parse(trimmed);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
payload = trimmed;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (typeof payload === 'string') {
|
|
76
|
+
return payload.trim() || undefined;
|
|
77
|
+
}
|
|
78
|
+
if (typeof payload === 'object' && payload !== null) {
|
|
79
|
+
for (const field of candidates) {
|
|
80
|
+
const value = payload[field];
|
|
81
|
+
if (typeof value === 'string' && value.trim()) {
|
|
82
|
+
return value.trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function applyAuthHeader(headers, auth) {
|
|
89
|
+
if (!auth || auth.type === 'none') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
switch (auth.type) {
|
|
93
|
+
case 'basic': {
|
|
94
|
+
const token = Buffer.from(`${auth.username}:${auth.password}`, 'utf8').toString('base64');
|
|
95
|
+
headers.set('Authorization', `Basic ${token}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
case 'bearer':
|
|
99
|
+
headers.set('Authorization', `Bearer ${auth.token}`);
|
|
100
|
+
return;
|
|
101
|
+
case 'header':
|
|
102
|
+
headers.set(auth.name, auth.value);
|
|
103
|
+
return;
|
|
104
|
+
default:
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function appendPathSegment(baseUrl, pathSegment) {
|
|
109
|
+
const base = baseUrl.trim();
|
|
110
|
+
const segment = pathSegment.trim();
|
|
111
|
+
if (!base) {
|
|
112
|
+
return segment;
|
|
113
|
+
}
|
|
114
|
+
if (!segment) {
|
|
115
|
+
return base;
|
|
116
|
+
}
|
|
117
|
+
const baseEndsWithSlash = base.endsWith('/');
|
|
118
|
+
const segmentStartsWithSlash = segment.startsWith('/');
|
|
119
|
+
if (baseEndsWithSlash && segmentStartsWithSlash) {
|
|
120
|
+
return base + segment.slice(1);
|
|
121
|
+
}
|
|
122
|
+
if (!baseEndsWithSlash && !segmentStartsWithSlash) {
|
|
123
|
+
return `${base}/${segment}`;
|
|
124
|
+
}
|
|
125
|
+
return base + segment;
|
|
126
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceStore } from '../storage-types.js';
|
|
2
|
+
import type { EvidenceJsonDocument, PublishedEvidenceDocument } from '../types.js';
|
|
3
|
+
import { HttpIpfsClient, type HttpIpfsClientOptions } from './HttpIpfsClient.js';
|
|
4
|
+
export declare class HttpIpfsEvidenceStore implements EvidenceStore {
|
|
5
|
+
private readonly client;
|
|
6
|
+
constructor(options: HttpIpfsClientOptions | HttpIpfsClient);
|
|
7
|
+
putEvidenceDocument(document: EvidenceJsonDocument): Promise<PublishedEvidenceDocument>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EvidenceHasher } from '../EvidenceHasher.js';
|
|
2
|
+
import { HttpIpfsClient } from './HttpIpfsClient.js';
|
|
3
|
+
export class HttpIpfsEvidenceStore {
|
|
4
|
+
client;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.client = options instanceof HttpIpfsClient ? options : new HttpIpfsClient(options);
|
|
7
|
+
}
|
|
8
|
+
async putEvidenceDocument(document) {
|
|
9
|
+
const added = await this.client.addBytes(EvidenceHasher.serialize(document), {
|
|
10
|
+
fileName: 'evidence.json',
|
|
11
|
+
mediaType: 'application/json',
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
cid: added.cid,
|
|
15
|
+
uri: added.uri,
|
|
16
|
+
gatewayUrls: added.gatewayUrls,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RequestContext, RequestFields, UploadRequest } from '../types.js';
|
|
2
|
+
import type { ContentAddResult } from '../storage-types.js';
|
|
3
|
+
export type HttpMultipartUploadInput = Partial<Omit<UploadRequest, 'file'>> & {
|
|
4
|
+
file: Uint8Array;
|
|
5
|
+
};
|
|
6
|
+
export interface HttpMultipartUploadClientOptions<RequestModel = UploadRequest> extends Partial<RequestContext> {
|
|
7
|
+
providerUrl?: string;
|
|
8
|
+
gatewayBaseUrls?: string[];
|
|
9
|
+
/** Upload endpoint path relative to provider.url. Defaults to /files. */
|
|
10
|
+
requestPath?: string;
|
|
11
|
+
/** Multipart field name expected by the provider. Defaults to file. */
|
|
12
|
+
fileFieldName?: string;
|
|
13
|
+
/** Client-level default provider-specific fields, merged with per-request fields. */
|
|
14
|
+
fields?: RequestFields;
|
|
15
|
+
/** Candidate JSON field names used to extract a CID from the response. */
|
|
16
|
+
responseCidFields?: string[];
|
|
17
|
+
/** Build a typed request model before serialization. Defaults to the UploadRequest itself. */
|
|
18
|
+
buildRequestModel?: (input: UploadRequest) => RequestModel;
|
|
19
|
+
/** Serialize the request model to the wire body. Defaults to multipart/form-data. */
|
|
20
|
+
serializeRequestModel?: (request: RequestModel) => BodyInit;
|
|
21
|
+
/** Parse a CID out of the response model. */
|
|
22
|
+
parseResponse?: (responseBody: unknown) => string | undefined;
|
|
23
|
+
}
|
|
24
|
+
export declare class HttpMultipartUploadClient<RequestModel = UploadRequest> {
|
|
25
|
+
private readonly options;
|
|
26
|
+
private readonly apiEndpoint;
|
|
27
|
+
constructor(options: HttpMultipartUploadClientOptions<RequestModel>);
|
|
28
|
+
upload(request: UploadRequest): Promise<ContentAddResult>;
|
|
29
|
+
addBytes(bytes: Uint8Array, input?: Omit<HttpMultipartUploadInput, 'file'>): Promise<ContentAddResult>;
|
|
30
|
+
private publish;
|
|
31
|
+
private buildRequestUrl;
|
|
32
|
+
private buildHeaders;
|
|
33
|
+
private buildDefaultFormData;
|
|
34
|
+
private mergeHeaders;
|
|
35
|
+
private mergeFields;
|
|
36
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { buildGatewayUrls } from '../helia/HeliaIpfsClient.js';
|
|
2
|
+
export class HttpMultipartUploadClient {
|
|
3
|
+
options;
|
|
4
|
+
apiEndpoint;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
const apiEndpoint = options.providerUrl?.trim();
|
|
8
|
+
if (!apiEndpoint && !options.requestPath) {
|
|
9
|
+
throw new Error('HttpMultipartUploadClient requires providerUrl or requestPath');
|
|
10
|
+
}
|
|
11
|
+
this.apiEndpoint = apiEndpoint ?? '';
|
|
12
|
+
}
|
|
13
|
+
async upload(request) {
|
|
14
|
+
return this.publish(request);
|
|
15
|
+
}
|
|
16
|
+
async addBytes(bytes, input = {}) {
|
|
17
|
+
const request = {
|
|
18
|
+
auth: input.auth ?? this.options.auth ?? { type: 'none' },
|
|
19
|
+
headers: this.mergeHeaders(input.headers),
|
|
20
|
+
fields: this.mergeFields(input.fields),
|
|
21
|
+
file: bytes,
|
|
22
|
+
fileName: input.fileName,
|
|
23
|
+
mediaType: input.mediaType,
|
|
24
|
+
};
|
|
25
|
+
return this.publish(request);
|
|
26
|
+
}
|
|
27
|
+
async publish(request) {
|
|
28
|
+
const model = this.options.buildRequestModel ? this.options.buildRequestModel(request) : request;
|
|
29
|
+
const response = await fetch(this.buildRequestUrl(), {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: this.buildHeaders(request),
|
|
32
|
+
body: this.options.serializeRequestModel ? this.options.serializeRequestModel(model) : this.buildDefaultFormData(request),
|
|
33
|
+
});
|
|
34
|
+
const responseText = await response.text();
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`HTTP multipart upload failed with status ${response.status} ${response.statusText}: ${responseText || '(empty response)'}`);
|
|
37
|
+
}
|
|
38
|
+
const cid = this.options.parseResponse?.(parseJsonIfPossible(responseText)) ?? extractCid(responseText, this.options.responseCidFields);
|
|
39
|
+
if (!cid) {
|
|
40
|
+
throw new Error('HTTP multipart upload succeeded but no CID was returned');
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
cid,
|
|
44
|
+
uri: `ipfs://${cid}`,
|
|
45
|
+
gatewayUrls: buildGatewayUrls(cid, this.options.gatewayBaseUrls),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
buildRequestUrl() {
|
|
49
|
+
return this.apiEndpoint ? appendPathSegment(this.apiEndpoint, this.options.requestPath ?? '') : (this.options.requestPath ?? '');
|
|
50
|
+
}
|
|
51
|
+
buildHeaders(request) {
|
|
52
|
+
const headers = new Headers();
|
|
53
|
+
applyAuthHeader(headers, request.auth ?? this.options.auth ?? { type: 'none' });
|
|
54
|
+
for (const source of [this.options.headers, request.headers]) {
|
|
55
|
+
for (const [name, value] of Object.entries(source ?? {})) {
|
|
56
|
+
if (value !== undefined && value !== null) {
|
|
57
|
+
headers.set(name, value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return headers;
|
|
62
|
+
}
|
|
63
|
+
buildDefaultFormData(request) {
|
|
64
|
+
const fieldName = this.options.fileFieldName ?? 'file';
|
|
65
|
+
const fileName = request.fileName?.trim() || 'evidence.bin';
|
|
66
|
+
const mediaType = request.mediaType?.trim() || 'application/octet-stream';
|
|
67
|
+
const blobPart = (request.file.byteOffset === 0 && request.file.byteLength === request.file.buffer.byteLength
|
|
68
|
+
? request.file.buffer
|
|
69
|
+
: request.file.buffer.slice(request.file.byteOffset, request.file.byteOffset + request.file.byteLength));
|
|
70
|
+
const form = new FormData();
|
|
71
|
+
form.append(fieldName, new Blob([blobPart], { type: mediaType }), fileName);
|
|
72
|
+
for (const [name, value] of Object.entries(request.fields ?? {})) {
|
|
73
|
+
appendFieldValue(form, name, value);
|
|
74
|
+
}
|
|
75
|
+
return form;
|
|
76
|
+
}
|
|
77
|
+
mergeHeaders(headers) {
|
|
78
|
+
return { ...(this.options.headers ?? {}), ...(headers ?? {}) };
|
|
79
|
+
}
|
|
80
|
+
mergeFields(fields) {
|
|
81
|
+
return { ...(this.options.fields ?? {}), ...(fields ?? {}) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function extractCid(responseBody, responseCidFields) {
|
|
85
|
+
const trimmed = responseBody.trim();
|
|
86
|
+
if (!trimmed) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const candidates = responseCidFields && responseCidFields.length > 0
|
|
90
|
+
? responseCidFields
|
|
91
|
+
: ['Hash', 'IpfsHash', 'cid', 'CID', 'Cid'];
|
|
92
|
+
let payload = trimmed;
|
|
93
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
94
|
+
try {
|
|
95
|
+
payload = JSON.parse(trimmed);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
payload = trimmed;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (typeof payload === 'string') {
|
|
102
|
+
return payload.trim() || undefined;
|
|
103
|
+
}
|
|
104
|
+
if (typeof payload === 'object' && payload !== null) {
|
|
105
|
+
const record = payload;
|
|
106
|
+
const nested = asRecord(record.data) ?? record;
|
|
107
|
+
for (const field of candidates) {
|
|
108
|
+
const value = nested[field];
|
|
109
|
+
if (typeof value === 'string' && value.trim()) {
|
|
110
|
+
return value.trim();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function asRecord(value) {
|
|
117
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
function parseJsonIfPossible(value) {
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
if (!trimmed) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(trimmed);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return trimmed;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function appendFieldValue(form, name, value) {
|
|
135
|
+
if (value === undefined || value === null) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (typeof value === 'string') {
|
|
139
|
+
form.append(name, value);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
143
|
+
form.append(name, String(value));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
form.append(name, JSON.stringify(value));
|
|
147
|
+
}
|
|
148
|
+
function applyAuthHeader(headers, auth) {
|
|
149
|
+
switch (auth.type) {
|
|
150
|
+
case 'none':
|
|
151
|
+
return;
|
|
152
|
+
case 'basic': {
|
|
153
|
+
const token = Buffer.from(`${auth.username}:${auth.password}`, 'utf8').toString('base64');
|
|
154
|
+
headers.set('Authorization', `Basic ${token}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case 'bearer':
|
|
158
|
+
headers.set('Authorization', `Bearer ${auth.token}`);
|
|
159
|
+
return;
|
|
160
|
+
case 'header':
|
|
161
|
+
headers.set(auth.name, auth.value);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function appendPathSegment(baseUrl, pathSegment) {
|
|
166
|
+
const base = baseUrl.trim();
|
|
167
|
+
const segment = pathSegment.trim();
|
|
168
|
+
if (!base) {
|
|
169
|
+
return segment;
|
|
170
|
+
}
|
|
171
|
+
if (!segment) {
|
|
172
|
+
return base;
|
|
173
|
+
}
|
|
174
|
+
const baseEndsWithSlash = base.endsWith('/');
|
|
175
|
+
const segmentStartsWithSlash = segment.startsWith('/');
|
|
176
|
+
if (baseEndsWithSlash && segmentStartsWithSlash) {
|
|
177
|
+
return base + segment.slice(1);
|
|
178
|
+
}
|
|
179
|
+
if (!baseEndsWithSlash && !segmentStartsWithSlash) {
|
|
180
|
+
return `${base}/${segment}`;
|
|
181
|
+
}
|
|
182
|
+
return base + segment;
|
|
183
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RequestContext, PinByCidRequest } from '../types.js';
|
|
2
|
+
export interface HttpPinByCidClientOptions extends Partial<RequestContext> {
|
|
3
|
+
providerUrl?: string;
|
|
4
|
+
gatewayBaseUrls?: string[];
|
|
5
|
+
/** Pin-by-CID endpoint path relative to provider.url. Defaults to /files/public/pin_by_cid. */
|
|
6
|
+
requestPath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HttpPinByCidResult {
|
|
9
|
+
status: number;
|
|
10
|
+
responseText: string;
|
|
11
|
+
responseBody: unknown;
|
|
12
|
+
cid?: string;
|
|
13
|
+
pinId?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class HttpPinByCidClient {
|
|
16
|
+
private readonly options;
|
|
17
|
+
private readonly apiEndpoint;
|
|
18
|
+
constructor(options: HttpPinByCidClientOptions);
|
|
19
|
+
pinByCid(input: PinByCidRequest): Promise<HttpPinByCidResult>;
|
|
20
|
+
private buildRequestUrl;
|
|
21
|
+
private buildRequestBody;
|
|
22
|
+
private buildHeaders;
|
|
23
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export class HttpPinByCidClient {
|
|
2
|
+
options;
|
|
3
|
+
apiEndpoint;
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.options = options;
|
|
6
|
+
this.apiEndpoint = options.providerUrl?.trim() ?? '';
|
|
7
|
+
}
|
|
8
|
+
async pinByCid(input) {
|
|
9
|
+
const cid = input.cid.trim();
|
|
10
|
+
if (!cid) {
|
|
11
|
+
throw new Error('HttpPinByCidClient requires input.cid');
|
|
12
|
+
}
|
|
13
|
+
const response = await fetch(this.buildRequestUrl(), {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: this.buildHeaders(input),
|
|
16
|
+
body: JSON.stringify(this.buildRequestBody({ ...input, cid })),
|
|
17
|
+
});
|
|
18
|
+
const responseText = await response.text();
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`HTTP pin-by-CID failed with status ${response.status} ${response.statusText}: ${responseText || '(empty response)'}`);
|
|
21
|
+
}
|
|
22
|
+
const responseBody = parseJsonIfPossible(responseText);
|
|
23
|
+
const extracted = extractPinByCidMetadata(responseBody);
|
|
24
|
+
return {
|
|
25
|
+
status: response.status,
|
|
26
|
+
responseText,
|
|
27
|
+
responseBody,
|
|
28
|
+
...extracted,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
buildRequestUrl() {
|
|
32
|
+
return appendPathSegment(this.apiEndpoint, this.options.requestPath ?? '/files/public/pin_by_cid');
|
|
33
|
+
}
|
|
34
|
+
buildRequestBody(input) {
|
|
35
|
+
const body = {
|
|
36
|
+
cid: input.cid,
|
|
37
|
+
};
|
|
38
|
+
for (const [name, value] of Object.entries(input.fields ?? {})) {
|
|
39
|
+
body[name] = value;
|
|
40
|
+
}
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
buildHeaders(input) {
|
|
44
|
+
const headers = new Headers({
|
|
45
|
+
'content-type': 'application/json',
|
|
46
|
+
});
|
|
47
|
+
applyAuthHeader(headers, input.auth ?? this.options.auth ?? { type: 'none' });
|
|
48
|
+
for (const [name, value] of Object.entries(this.options.headers ?? {})) {
|
|
49
|
+
if (value !== undefined && value !== null) {
|
|
50
|
+
headers.set(name, value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const [name, value] of Object.entries(input.headers ?? {})) {
|
|
54
|
+
if (value !== undefined && value !== null) {
|
|
55
|
+
headers.set(name, value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function parseJsonIfPossible(value) {
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(trimmed);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return trimmed;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function extractPinByCidMetadata(responseBody) {
|
|
74
|
+
if (typeof responseBody !== 'object' || responseBody === null) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
const root = responseBody;
|
|
78
|
+
const data = asRecord(root.data) ?? root;
|
|
79
|
+
return {
|
|
80
|
+
cid: readStringField(data, ['cid', 'CID', 'content_cid']),
|
|
81
|
+
pinId: readStringField(data, ['id', 'pinId', 'pin_id']),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function asRecord(value) {
|
|
85
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function readStringField(record, names) {
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const value = record[name];
|
|
93
|
+
if (typeof value === 'string' && value.trim()) {
|
|
94
|
+
return value.trim();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
function applyAuthHeader(headers, auth) {
|
|
100
|
+
if (!auth || auth.type === 'none') {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
switch (auth.type) {
|
|
104
|
+
case 'basic': {
|
|
105
|
+
const token = Buffer.from(`${auth.username}:${auth.password}`, 'utf8').toString('base64');
|
|
106
|
+
headers.set('Authorization', `Basic ${token}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
case 'bearer':
|
|
110
|
+
headers.set('Authorization', `Bearer ${auth.token}`);
|
|
111
|
+
return;
|
|
112
|
+
case 'header':
|
|
113
|
+
headers.set(auth.name, auth.value);
|
|
114
|
+
return;
|
|
115
|
+
default:
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function appendPathSegment(baseUrl, pathSegment) {
|
|
120
|
+
const base = baseUrl.trim();
|
|
121
|
+
const segment = pathSegment.trim();
|
|
122
|
+
if (!base) {
|
|
123
|
+
return segment;
|
|
124
|
+
}
|
|
125
|
+
if (!segment) {
|
|
126
|
+
return base;
|
|
127
|
+
}
|
|
128
|
+
const baseEndsWithSlash = base.endsWith('/');
|
|
129
|
+
const segmentStartsWithSlash = segment.startsWith('/');
|
|
130
|
+
if (baseEndsWithSlash && segmentStartsWithSlash) {
|
|
131
|
+
return base + segment.slice(1);
|
|
132
|
+
}
|
|
133
|
+
if (!baseEndsWithSlash && !segmentStartsWithSlash) {
|
|
134
|
+
return `${base}/${segment}`;
|
|
135
|
+
}
|
|
136
|
+
return base + segment;
|
|
137
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { RulingOptionType, CourtEncodingVersion, RulingOptions, MetaEvidence, MetaEvidenceDraft, ManualMetaEvidencePublishRequest, AssistedMetaEvidencePublishRequest, MetaEvidencePublishRequest, MetaEvidencePublishResult, EvidenceJson, EvidenceDraft, EvidencePublishRequest, EvidencePublishResult, PublishedEvidenceDocument, RemotePinningConfig, } from './types.js';
|
|
2
|
+
export type { AttachmentStore, EvidenceStore, MetaEvidenceStore } from './storage-types.js';
|
|
3
|
+
export type { EvidencePublisherFactoryOptions, MetaEvidencePublisherFactoryOptions } from './EvidencePublisherFactory.js';
|
|
4
|
+
export { EvidencePublisher } from './EvidencePublisher.js';
|
|
5
|
+
export { MetaEvidencePublisher } from './MetaEvidencePublisher.js';
|
|
6
|
+
export { MetaEvidenceJsonBuilder } from './MetaEvidenceJsonBuilder.js';
|
|
7
|
+
export { createEvidencePublisher, createMetaEvidencePublisher } from './EvidencePublisherFactory.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// ─── Main publishers ──────────────────────────────────────────────────────────
|
|
2
|
+
export { EvidencePublisher } from './EvidencePublisher.js';
|
|
3
|
+
export { MetaEvidencePublisher } from './MetaEvidencePublisher.js';
|
|
4
|
+
export { MetaEvidenceJsonBuilder } from './MetaEvidenceJsonBuilder.js';
|
|
5
|
+
export { createEvidencePublisher, createMetaEvidencePublisher } from './EvidencePublisherFactory.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProviderConfig, PinningConfig } from './config.js';
|
|
2
|
+
import type { Attachment, EvidenceJsonDocument, MetaEvidence, PublishedAttachment, PublishedEvidenceDocument } from './types.js';
|
|
3
|
+
export interface ContentPublishOptions {
|
|
4
|
+
provider?: ProviderConfig;
|
|
5
|
+
pinning?: PinningConfig;
|
|
6
|
+
gatewayBaseUrls?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ContentAddResult {
|
|
9
|
+
cid: string;
|
|
10
|
+
uri: string;
|
|
11
|
+
gatewayUrls?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface AttachmentStore {
|
|
14
|
+
putAttachment(input: Attachment): Promise<PublishedAttachment>;
|
|
15
|
+
}
|
|
16
|
+
export interface EvidenceStore {
|
|
17
|
+
putEvidenceDocument(document: EvidenceJsonDocument): Promise<PublishedEvidenceDocument>;
|
|
18
|
+
}
|
|
19
|
+
export interface MetaEvidenceStore {
|
|
20
|
+
putMetaEvidenceDocument(document: MetaEvidence): Promise<PublishedEvidenceDocument>;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|