@primitivedotdev/sdk 1.22.0 → 1.23.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 +12 -12
- package/dist/api/index.d.ts +5 -5
- package/dist/api/index.js +4 -4
- package/dist/{api-Oi6Mo4Us.js → api-mauNzbhi.js} +237 -11
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.js +1 -1
- package/dist/{errors-DAHv2kWU.d.ts → errors-BgI9FKHK.d.ts} +1 -1
- package/dist/{errors-D6WjisrD.js → errors-Bo0f7eId.js} +1 -1
- package/dist/{index-CAwgOxWk.d.ts → index-C11B41Hb.d.ts} +982 -12
- package/dist/{index-DJeUuGUn.d.ts → index-fS1gVNuX.d.ts} +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +1 -1
- package/dist/{operations.generated-CqlK_7LO.js → operations.generated-COaLBzbH.js} +5318 -2388
- package/dist/parser/address.d.ts +1 -1
- package/dist/parser/address.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/payloads/index.js +1 -391
- package/dist/payloads-CQtSsUT6.js +409 -0
- package/dist/{types-DVjBmOg0.d.ts → types-DcXtFARL.d.ts} +1 -1
- package/dist/webhook/index.d.ts +3 -3
- package/dist/webhook/index.js +2 -2
- package/dist/{webhook-1cz088XC.js → webhook-BtyuJtUF.js} +2 -2
- package/package.json +7 -6
- /package/dist/{address-parser-CQbFjgRC.js → address-parser-C2JJzLdA.js} +0 -0
- /package/dist/{address-parser-CA6G7R-h.d.ts → address-parser-CJqzx713.d.ts} +0 -0
- /package/dist/{operations.generated-BAniFZK9.d.ts → operations.generated-DJS4dWr7.d.ts} +0 -0
package/dist/parser/address.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as parseFromHeader, i as ValidatedAddress, n as ParseFromHeaderResult, o as parseFromHeaderLoose, r as ParsedAddress, t as ParseFromHeaderFailureReason } from "../address-parser-
|
|
1
|
+
import { a as parseFromHeader, i as ValidatedAddress, n as ParseFromHeaderResult, o as parseFromHeaderLoose, r as ParsedAddress, t as ParseFromHeaderFailureReason } from "../address-parser-CJqzx713.js";
|
|
2
2
|
export { type ParseFromHeaderFailureReason, type ParseFromHeaderResult, type ParsedAddress, type ValidatedAddress, parseFromHeader, parseFromHeaderLoose };
|
package/dist/parser/address.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as parseFromHeaderLoose, t as parseFromHeader } from "../address-parser-
|
|
1
|
+
import { n as parseFromHeaderLoose, t as parseFromHeader } from "../address-parser-C2JJzLdA.js";
|
|
2
2
|
export { parseFromHeader, parseFromHeaderLoose };
|
package/dist/parser/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as WebhookAttachment, l as EmailAddress, w as ParsedDataComplete } from "../types-
|
|
2
|
-
import { a as parseFromHeader, i as ValidatedAddress, n as ParseFromHeaderResult, o as parseFromHeaderLoose, r as ParsedAddress, t as ParseFromHeaderFailureReason } from "../address-parser-
|
|
1
|
+
import { I as WebhookAttachment, l as EmailAddress, w as ParsedDataComplete } from "../types-DcXtFARL.js";
|
|
2
|
+
import { a as parseFromHeader, i as ValidatedAddress, n as ParseFromHeaderResult, o as parseFromHeaderLoose, r as ParsedAddress, t as ParseFromHeaderFailureReason } from "../address-parser-CJqzx713.js";
|
|
3
3
|
|
|
4
4
|
//#region src/parser/attachment-parser.d.ts
|
|
5
5
|
interface ParsedAttachment {
|
package/dist/parser/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as parseFromHeaderLoose, t as parseFromHeader } from "../address-parser-
|
|
1
|
+
import { n as parseFromHeaderLoose, t as parseFromHeader } from "../address-parser-C2JJzLdA.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { createGzip } from "node:zlib";
|
|
4
4
|
import { pack } from "tar-stream";
|
package/dist/payloads/index.js
CHANGED
|
@@ -1,392 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createWriteStream } from "node:fs";
|
|
3
|
-
//#region src/payloads/index.ts
|
|
4
|
-
/**
|
|
5
|
-
* Primitive Payloads — streaming client for large, content-addressed,
|
|
6
|
-
* end-to-end-encrypted objects (attachments up to ~1 TB).
|
|
7
|
-
*
|
|
8
|
-
* Objects are split into fixed 64 MiB chunks; each chunk is encrypted with a
|
|
9
|
-
* per-chunk AES-256-GCM key (HKDF-derived from the object CEK), content-addressed
|
|
10
|
-
* by its ciphertext SHA-256, and committed under a Merkle root. This module
|
|
11
|
-
* streams: it processes one chunk at a time and never holds the whole object in
|
|
12
|
-
* memory, so a multi-GB file uploads/downloads in bounded memory.
|
|
13
|
-
*
|
|
14
|
-
* The chunk/manifest construction is byte-compatible with the server's object
|
|
15
|
-
* model (packages/payloads-core in the API monorepo).
|
|
16
|
-
*/
|
|
17
|
-
const CHUNK_SIZE = 64 * 1024 * 1024;
|
|
18
|
-
const MANIFEST_VERSION = 1;
|
|
19
|
-
const CHUNK_KDF_INFO = "payloads-chunk";
|
|
20
|
-
const OBJECT_ID_BYTES = 16;
|
|
21
|
-
const CEK_BYTES = 32;
|
|
22
|
-
const subtle = globalThis.crypto.subtle;
|
|
23
|
-
const textEncoder = new TextEncoder();
|
|
24
|
-
function randomBytes(length) {
|
|
25
|
-
const out = new Uint8Array(length);
|
|
26
|
-
globalThis.crypto.getRandomValues(out);
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
function toHex(bytes) {
|
|
30
|
-
let out = "";
|
|
31
|
-
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
function fromHex(hex) {
|
|
35
|
-
if (hex.length % 2 !== 0) throw new Error("invalid hex: odd length");
|
|
36
|
-
if (hex.length > 0 && !/^[0-9a-fA-F]+$/.test(hex)) throw new Error("invalid hex: non-hex character");
|
|
37
|
-
const out = new Uint8Array(hex.length / 2);
|
|
38
|
-
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
async function contentHash(bytes) {
|
|
42
|
-
return new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
43
|
-
}
|
|
44
|
-
async function contentHashHex(bytes) {
|
|
45
|
-
return toHex(await contentHash(bytes));
|
|
46
|
-
}
|
|
47
|
-
/** Binary Merkle root over ordered hex leaf hashes (odd node promoted). */
|
|
48
|
-
async function merkleRoot(leafHashesHex) {
|
|
49
|
-
if (leafHashesHex.length === 0) return contentHashHex(new Uint8Array(0));
|
|
50
|
-
let level = leafHashesHex.map(fromHex);
|
|
51
|
-
while (level.length > 1) {
|
|
52
|
-
const next = [];
|
|
53
|
-
for (let i = 0; i < level.length; i += 2) {
|
|
54
|
-
const left = level[i];
|
|
55
|
-
const right = i + 1 < level.length ? level[i + 1] : level[i];
|
|
56
|
-
const combined = new Uint8Array(left.length + right.length);
|
|
57
|
-
combined.set(left, 0);
|
|
58
|
-
combined.set(right, left.length);
|
|
59
|
-
next.push(await contentHash(combined));
|
|
60
|
-
}
|
|
61
|
-
level = next;
|
|
62
|
-
}
|
|
63
|
-
return toHex(level[0]);
|
|
64
|
-
}
|
|
65
|
-
async function deriveChunkKey(cek, index) {
|
|
66
|
-
const baseKey = await subtle.importKey("raw", cek, "HKDF", false, ["deriveKey"]);
|
|
67
|
-
const info = textEncoder.encode(`${CHUNK_KDF_INFO}:${index}`);
|
|
68
|
-
return subtle.deriveKey({
|
|
69
|
-
name: "HKDF",
|
|
70
|
-
hash: "SHA-256",
|
|
71
|
-
salt: new Uint8Array(32),
|
|
72
|
-
info
|
|
73
|
-
}, baseKey, {
|
|
74
|
-
name: "AES-GCM",
|
|
75
|
-
length: 256
|
|
76
|
-
}, false, ["encrypt", "decrypt"]);
|
|
77
|
-
}
|
|
78
|
-
function chunkNonce(index) {
|
|
79
|
-
const nonce = new Uint8Array(12);
|
|
80
|
-
new DataView(nonce.buffer).setUint32(8, index >>> 0, false);
|
|
81
|
-
return nonce;
|
|
82
|
-
}
|
|
83
|
-
function chunkAad(objectId, index) {
|
|
84
|
-
const aad = new Uint8Array(objectId.length + 4);
|
|
85
|
-
aad.set(objectId, 0);
|
|
86
|
-
new DataView(aad.buffer).setUint32(objectId.length, index >>> 0, false);
|
|
87
|
-
return aad;
|
|
88
|
-
}
|
|
89
|
-
async function encryptChunk(cek, objectId, index, plaintext) {
|
|
90
|
-
const key = await deriveChunkKey(cek, index);
|
|
91
|
-
const ct = await subtle.encrypt({
|
|
92
|
-
name: "AES-GCM",
|
|
93
|
-
iv: chunkNonce(index),
|
|
94
|
-
additionalData: chunkAad(objectId, index)
|
|
95
|
-
}, key, plaintext);
|
|
96
|
-
return new Uint8Array(ct);
|
|
97
|
-
}
|
|
98
|
-
async function decryptChunk(cek, objectId, index, ciphertext) {
|
|
99
|
-
const key = await deriveChunkKey(cek, index);
|
|
100
|
-
const pt = await subtle.decrypt({
|
|
101
|
-
name: "AES-GCM",
|
|
102
|
-
iv: chunkNonce(index),
|
|
103
|
-
additionalData: chunkAad(objectId, index)
|
|
104
|
-
}, key, ciphertext);
|
|
105
|
-
return new Uint8Array(pt);
|
|
106
|
-
}
|
|
107
|
-
function authHeaders(apiKey) {
|
|
108
|
-
return { authorization: `Bearer ${apiKey}` };
|
|
109
|
-
}
|
|
110
|
-
function apiRoot(baseUrl) {
|
|
111
|
-
let end = baseUrl.length;
|
|
112
|
-
while (end > 0 && baseUrl.charCodeAt(end - 1) === 47) end--;
|
|
113
|
-
return `${baseUrl.slice(0, end)}/v1/payloads`;
|
|
114
|
-
}
|
|
115
|
-
const DEFAULT_MAX_RETRIES = 6;
|
|
116
|
-
const RETRYABLE_STATUS = new Set([
|
|
117
|
-
429,
|
|
118
|
-
500,
|
|
119
|
-
502,
|
|
120
|
-
503,
|
|
121
|
-
504
|
|
122
|
-
]);
|
|
123
|
-
function sleep(ms) {
|
|
124
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* fetch with exponential backoff on transient failures. Request bodies here are
|
|
128
|
-
* in-memory buffers/strings (never streams), so a retry can safely resend them.
|
|
129
|
-
*/
|
|
130
|
-
async function retryingFetch(url, init, label, maxRetries = DEFAULT_MAX_RETRIES) {
|
|
131
|
-
for (let attempt = 0;; attempt++) {
|
|
132
|
-
let res;
|
|
133
|
-
let networkError;
|
|
134
|
-
try {
|
|
135
|
-
res = await fetch(url, init);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
networkError = err;
|
|
138
|
-
}
|
|
139
|
-
if (res && !RETRYABLE_STATUS.has(res.status)) return res;
|
|
140
|
-
if (attempt >= maxRetries) {
|
|
141
|
-
if (res) return res;
|
|
142
|
-
throw new Error(`${label}: network error after ${attempt} retries: ${networkError?.message ?? networkError}`);
|
|
143
|
-
}
|
|
144
|
-
if (res) await res.text().catch(() => {});
|
|
145
|
-
await sleep(Math.min(1e3 * 2 ** attempt, 15e3) + Math.floor(Math.random() * 250));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async function initiate(opts, manifest) {
|
|
149
|
-
const res = await retryingFetch(apiRoot(opts.baseUrl), {
|
|
150
|
-
method: "POST",
|
|
151
|
-
headers: {
|
|
152
|
-
...authHeaders(opts.apiKey),
|
|
153
|
-
"content-type": "application/json"
|
|
154
|
-
},
|
|
155
|
-
body: JSON.stringify({ manifest })
|
|
156
|
-
}, "initiate");
|
|
157
|
-
if (!res.ok) throw new Error(`initiate failed: HTTP ${res.status} ${await res.text()}`);
|
|
158
|
-
await res.text();
|
|
159
|
-
}
|
|
160
|
-
async function putChunk(opts, root, d, bytes) {
|
|
161
|
-
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/chunks/${d.ciphertextHash}`, {
|
|
162
|
-
method: "PUT",
|
|
163
|
-
headers: {
|
|
164
|
-
...authHeaders(opts.apiKey),
|
|
165
|
-
"content-type": "application/octet-stream"
|
|
166
|
-
},
|
|
167
|
-
body: bytes
|
|
168
|
-
}, `chunk ${d.index} upload`);
|
|
169
|
-
if (!res.ok) throw new Error(`chunk ${d.index} upload failed: HTTP ${res.status} ${await res.text()}`);
|
|
170
|
-
await res.text();
|
|
171
|
-
}
|
|
172
|
-
async function finalize(opts, root) {
|
|
173
|
-
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/finalize`, {
|
|
174
|
-
method: "POST",
|
|
175
|
-
headers: authHeaders(opts.apiKey)
|
|
176
|
-
}, "finalize");
|
|
177
|
-
if (!res.ok) throw new Error(`finalize failed: HTTP ${res.status} ${await res.text()}`);
|
|
178
|
-
await res.text();
|
|
179
|
-
}
|
|
180
|
-
async function fetchManifest(opts, root) {
|
|
181
|
-
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/manifest`, { headers: authHeaders(opts.apiKey) }, "get manifest");
|
|
182
|
-
if (!res.ok) throw new Error(`get manifest failed: HTTP ${res.status} ${await res.text()}`);
|
|
183
|
-
return (await res.json()).data.manifest;
|
|
184
|
-
}
|
|
185
|
-
async function getChunkBytes(opts, root, d) {
|
|
186
|
-
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/chunks/${d.ciphertextHash}`, { headers: authHeaders(opts.apiKey) }, `get chunk ${d.index}`);
|
|
187
|
-
if (!res.ok) throw new Error(`get chunk ${d.index} failed: HTTP ${res.status}`);
|
|
188
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Encode in-memory bytes into a payload manifest (no upload). Exposed for
|
|
192
|
-
* precomputing an object's content address and for conformance-testing the
|
|
193
|
-
* chunk/crypto/Merkle construction against the server object model. With a fixed
|
|
194
|
-
* cek/objectId the output is fully deterministic.
|
|
195
|
-
*/
|
|
196
|
-
async function encodeManifest(bytes, opts = {}) {
|
|
197
|
-
const chunkSize = opts.chunkSize ?? CHUNK_SIZE;
|
|
198
|
-
const cek = opts.cekHex ? fromHex(opts.cekHex) : randomBytes(CEK_BYTES);
|
|
199
|
-
const objectId = opts.objectIdHex ? fromHex(opts.objectIdHex) : randomBytes(OBJECT_ID_BYTES);
|
|
200
|
-
const chunkCount = bytes.length === 0 ? 0 : Math.ceil(bytes.length / chunkSize);
|
|
201
|
-
const descriptors = [];
|
|
202
|
-
for (let index = 0; index < chunkCount; index++) {
|
|
203
|
-
const start = index * chunkSize;
|
|
204
|
-
const plaintext = bytes.subarray(start, Math.min(start + chunkSize, bytes.length));
|
|
205
|
-
const ciphertext = await encryptChunk(cek, objectId, index, plaintext);
|
|
206
|
-
descriptors.push({
|
|
207
|
-
index,
|
|
208
|
-
ciphertextHash: await contentHashHex(ciphertext),
|
|
209
|
-
plaintextSize: plaintext.length,
|
|
210
|
-
ciphertextSize: ciphertext.length
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
version: MANIFEST_VERSION,
|
|
215
|
-
objectId: toHex(objectId),
|
|
216
|
-
chunkSize,
|
|
217
|
-
totalPlaintextSize: bytes.length,
|
|
218
|
-
chunkCount,
|
|
219
|
-
chunks: descriptors,
|
|
220
|
-
merkleRoot: await merkleRoot(descriptors.map((d) => d.ciphertextHash))
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
async function readWindow(fh, position, length) {
|
|
224
|
-
const buf = Buffer.allocUnsafe(length);
|
|
225
|
-
let read = 0;
|
|
226
|
-
while (read < length) {
|
|
227
|
-
const { bytesRead } = await fh.read(buf, read, length - read, position + read);
|
|
228
|
-
if (bytesRead === 0) break;
|
|
229
|
-
read += bytesRead;
|
|
230
|
-
}
|
|
231
|
-
return read === length ? buf : buf.subarray(0, read);
|
|
232
|
-
}
|
|
233
|
-
async function mapLimit(items, limit, fn) {
|
|
234
|
-
let next = 0;
|
|
235
|
-
let firstError;
|
|
236
|
-
const worker = async () => {
|
|
237
|
-
while (next < items.length && firstError === void 0) {
|
|
238
|
-
const i = next++;
|
|
239
|
-
try {
|
|
240
|
-
await fn(items[i], i);
|
|
241
|
-
} catch (err) {
|
|
242
|
-
if (firstError === void 0) firstError = err;
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length || 1) }, () => worker()));
|
|
248
|
-
if (firstError !== void 0) throw firstError;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Stream a file up as a Primitive Payload. Two passes over the file (encrypt to
|
|
252
|
-
* build the manifest, then encrypt again to upload) keep memory bounded to a few
|
|
253
|
-
* chunks — the whole object is never resident. Returns the content address
|
|
254
|
-
* (merkleRoot) and the hex CEK needed to download it.
|
|
255
|
-
*/
|
|
256
|
-
async function pushFile(filePath, opts) {
|
|
257
|
-
const chunkSize = opts.chunkSize ?? CHUNK_SIZE;
|
|
258
|
-
const concurrency = opts.concurrency ?? 3;
|
|
259
|
-
const { size } = await stat(filePath);
|
|
260
|
-
const cek = randomBytes(CEK_BYTES);
|
|
261
|
-
const objectId = randomBytes(OBJECT_ID_BYTES);
|
|
262
|
-
const chunkCount = size === 0 ? 0 : Math.ceil(size / chunkSize);
|
|
263
|
-
const fh = await open(filePath, "r");
|
|
264
|
-
try {
|
|
265
|
-
const descriptors = [];
|
|
266
|
-
for (let index = 0; index < chunkCount; index++) {
|
|
267
|
-
const position = index * chunkSize;
|
|
268
|
-
const length = Math.min(chunkSize, size - position);
|
|
269
|
-
const plaintext = await readWindow(fh, position, length);
|
|
270
|
-
const ciphertext = await encryptChunk(cek, objectId, index, plaintext);
|
|
271
|
-
descriptors.push({
|
|
272
|
-
index,
|
|
273
|
-
ciphertextHash: await contentHashHex(ciphertext),
|
|
274
|
-
plaintextSize: length,
|
|
275
|
-
ciphertextSize: ciphertext.length
|
|
276
|
-
});
|
|
277
|
-
opts.onProgress?.("encrypt", index + 1, chunkCount);
|
|
278
|
-
}
|
|
279
|
-
const root = await merkleRoot(descriptors.map((d) => d.ciphertextHash));
|
|
280
|
-
await initiate(opts, {
|
|
281
|
-
version: MANIFEST_VERSION,
|
|
282
|
-
objectId: toHex(objectId),
|
|
283
|
-
chunkSize,
|
|
284
|
-
totalPlaintextSize: size,
|
|
285
|
-
chunkCount,
|
|
286
|
-
chunks: descriptors,
|
|
287
|
-
merkleRoot: root
|
|
288
|
-
});
|
|
289
|
-
let uploaded = 0;
|
|
290
|
-
await mapLimit(descriptors, concurrency, async (d) => {
|
|
291
|
-
const plaintext = await readWindow(fh, d.index * chunkSize, d.plaintextSize);
|
|
292
|
-
await putChunk(opts, root, d, await encryptChunk(cek, objectId, d.index, plaintext));
|
|
293
|
-
opts.onProgress?.("upload", ++uploaded, chunkCount);
|
|
294
|
-
});
|
|
295
|
-
await finalize(opts, root);
|
|
296
|
-
return {
|
|
297
|
-
merkleRoot: root,
|
|
298
|
-
cek: toHex(cek),
|
|
299
|
-
chunkCount,
|
|
300
|
-
totalBytes: size
|
|
301
|
-
};
|
|
302
|
-
} finally {
|
|
303
|
-
await fh.close();
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Push an in-memory buffer as a Primitive Payload. Same chunked, content-
|
|
308
|
-
* addressed, per-chunk-AEAD encoding as {@link pushFile}, over a Uint8Array
|
|
309
|
-
* (chunks are views, so no copy). Use {@link pushFile} for large objects that
|
|
310
|
-
* shouldn't be resident in memory. Returns the content address (merkleRoot) and
|
|
311
|
-
* the hex CEK.
|
|
312
|
-
*/
|
|
313
|
-
async function pushBytes(bytes, opts) {
|
|
314
|
-
const chunkSize = opts.chunkSize ?? CHUNK_SIZE;
|
|
315
|
-
const concurrency = opts.concurrency ?? 3;
|
|
316
|
-
const size = bytes.length;
|
|
317
|
-
const cek = randomBytes(CEK_BYTES);
|
|
318
|
-
const objectId = randomBytes(OBJECT_ID_BYTES);
|
|
319
|
-
const chunkCount = size === 0 ? 0 : Math.ceil(size / chunkSize);
|
|
320
|
-
const chunkOf = (index) => bytes.subarray(index * chunkSize, Math.min(size, (index + 1) * chunkSize));
|
|
321
|
-
const descriptors = [];
|
|
322
|
-
for (let index = 0; index < chunkCount; index++) {
|
|
323
|
-
const plaintext = chunkOf(index);
|
|
324
|
-
const ciphertext = await encryptChunk(cek, objectId, index, plaintext);
|
|
325
|
-
descriptors.push({
|
|
326
|
-
index,
|
|
327
|
-
ciphertextHash: await contentHashHex(ciphertext),
|
|
328
|
-
plaintextSize: plaintext.length,
|
|
329
|
-
ciphertextSize: ciphertext.length
|
|
330
|
-
});
|
|
331
|
-
opts.onProgress?.("encrypt", index + 1, chunkCount);
|
|
332
|
-
}
|
|
333
|
-
const root = await merkleRoot(descriptors.map((d) => d.ciphertextHash));
|
|
334
|
-
await initiate(opts, {
|
|
335
|
-
version: MANIFEST_VERSION,
|
|
336
|
-
objectId: toHex(objectId),
|
|
337
|
-
chunkSize,
|
|
338
|
-
totalPlaintextSize: size,
|
|
339
|
-
chunkCount,
|
|
340
|
-
chunks: descriptors,
|
|
341
|
-
merkleRoot: root
|
|
342
|
-
});
|
|
343
|
-
let uploaded = 0;
|
|
344
|
-
await mapLimit(descriptors, concurrency, async (d) => {
|
|
345
|
-
await putChunk(opts, root, d, await encryptChunk(cek, objectId, d.index, chunkOf(d.index)));
|
|
346
|
-
opts.onProgress?.("upload", ++uploaded, chunkCount);
|
|
347
|
-
});
|
|
348
|
-
await finalize(opts, root);
|
|
349
|
-
return {
|
|
350
|
-
merkleRoot: root,
|
|
351
|
-
cek: toHex(cek),
|
|
352
|
-
chunkCount,
|
|
353
|
-
totalBytes: size
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Stream a Primitive Payload down to a file, verifying and decrypting one chunk
|
|
358
|
-
* at a time (bounded memory). Every chunk is content-address-checked before
|
|
359
|
-
* decryption, so a corrupt or substituted chunk throws.
|
|
360
|
-
*/
|
|
361
|
-
async function pullFile(root, outPath, opts) {
|
|
362
|
-
const manifest = await fetchManifest(opts, root);
|
|
363
|
-
const computedRoot = await merkleRoot(manifest.chunks.map((c) => c.ciphertextHash));
|
|
364
|
-
if (computedRoot !== root) throw new Error(`manifest does not match the requested content address (got ${computedRoot})`);
|
|
365
|
-
const cek = fromHex(opts.cek);
|
|
366
|
-
const objectId = fromHex(manifest.objectId);
|
|
367
|
-
const out = createWriteStream(outPath);
|
|
368
|
-
let onWriteError = () => {};
|
|
369
|
-
const writeErrored = new Promise((_, reject) => {
|
|
370
|
-
onWriteError = reject;
|
|
371
|
-
});
|
|
372
|
-
writeErrored.catch(() => {});
|
|
373
|
-
out.on("error", (err) => onWriteError(err));
|
|
374
|
-
try {
|
|
375
|
-
let done = 0;
|
|
376
|
-
for (const d of manifest.chunks) {
|
|
377
|
-
const ciphertext = await getChunkBytes(opts, root, d);
|
|
378
|
-
if (await contentHashHex(ciphertext) !== d.ciphertextHash) throw new Error(`chunk ${d.index} failed integrity check`);
|
|
379
|
-
const plaintext = await decryptChunk(cek, objectId, d.index, ciphertext);
|
|
380
|
-
if (!out.write(plaintext)) await Promise.race([new Promise((resolve) => out.once("drain", resolve)), writeErrored]);
|
|
381
|
-
opts.onProgress?.("download", ++done, manifest.chunkCount);
|
|
382
|
-
}
|
|
383
|
-
await Promise.race([new Promise((resolve) => out.end(resolve)), writeErrored]);
|
|
384
|
-
} catch (err) {
|
|
385
|
-
out.destroy();
|
|
386
|
-
await rm(outPath, { force: true });
|
|
387
|
-
throw err;
|
|
388
|
-
}
|
|
389
|
-
return manifest;
|
|
390
|
-
}
|
|
391
|
-
//#endregion
|
|
1
|
+
import { a as pushFile, i as pushBytes, n as fetchManifest, r as pullFile, t as encodeManifest } from "../payloads-CQtSsUT6.js";
|
|
392
2
|
export { encodeManifest, fetchManifest, pullFile, pushBytes, pushFile };
|