@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.
@@ -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-CA6G7R-h.js";
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 };
@@ -1,2 +1,2 @@
1
- import { n as parseFromHeaderLoose, t as parseFromHeader } from "../address-parser-CQbFjgRC.js";
1
+ import { n as parseFromHeaderLoose, t as parseFromHeader } from "../address-parser-C2JJzLdA.js";
2
2
  export { parseFromHeader, parseFromHeaderLoose };
@@ -1,5 +1,5 @@
1
- import { I as WebhookAttachment, l as EmailAddress, w as ParsedDataComplete } from "../types-DVjBmOg0.js";
2
- import { a as parseFromHeader, i as ValidatedAddress, n as ParseFromHeaderResult, o as parseFromHeaderLoose, r as ParsedAddress, t as ParseFromHeaderFailureReason } from "../address-parser-CA6G7R-h.js";
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 {
@@ -1,4 +1,4 @@
1
- import { n as parseFromHeaderLoose, t as parseFromHeader } from "../address-parser-CQbFjgRC.js";
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";
@@ -1,392 +1,2 @@
1
- import { open, rm, stat } from "node:fs/promises";
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 };