primcli 1.20.0 → 1.21.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/dist/oclif/index.js +522 -12
- package/package.json +1 -1
package/dist/oclif/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { A as saveActiveChatState, C as resolveCliAuth, D as deleteChatState, E as chatStatePath, M as createClient, N as createConfig, O as loadActiveChatState, S as normalizeApiBaseUrl, T as saveSignupCredentials, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as hasStoredCliLogin, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as PrimitiveApiClient, k as loadChatConversationByLocalId, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as saveCliCredentials, x as loadCliCredentials, y as detectPrimitiveKeyEnvMisname } from "../cli-config-BKso8oJu.js";
|
|
2
2
|
import { Args, Command, Errors, Flags, ux } from "@oclif/core";
|
|
3
|
-
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
+
import { chmodSync, closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import path, { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import process$1 from "node:process";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { hostname } from "node:os";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
+
import { open, rm, stat } from "node:fs/promises";
|
|
10
11
|
import { gunzipSync } from "node:zlib";
|
|
11
12
|
import { concat, getAddress, hexToBytes, keccak256, stringToBytes } from "viem";
|
|
12
13
|
import { privateKeyToAccount } from "viem/accounts";
|
|
@@ -9241,6 +9242,40 @@ const openapiDocument = {
|
|
|
9241
9242
|
},
|
|
9242
9243
|
"required": ["filename", "content_base64"]
|
|
9243
9244
|
},
|
|
9245
|
+
"SendMailPayloadRef": {
|
|
9246
|
+
"type": "object",
|
|
9247
|
+
"additionalProperties": false,
|
|
9248
|
+
"description": "A reference to an already-uploaded Primitive Payloads object, delivered as an attachment without inlining the bytes — the way to send an attachment larger than the inline cap. Upload the object via /v1/payloads (with a client-held CEK the server never sees), then reference it here.",
|
|
9249
|
+
"properties": {
|
|
9250
|
+
"root": {
|
|
9251
|
+
"type": "string",
|
|
9252
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
9253
|
+
"description": "The 64-char lowercase-hex Merkle root of a finalized payloads object."
|
|
9254
|
+
},
|
|
9255
|
+
"filename": {
|
|
9256
|
+
"type": "string",
|
|
9257
|
+
"minLength": 1,
|
|
9258
|
+
"maxLength": 255,
|
|
9259
|
+
"description": "Attachment filename presented to the recipient."
|
|
9260
|
+
},
|
|
9261
|
+
"content_type": {
|
|
9262
|
+
"type": "string",
|
|
9263
|
+
"minLength": 1,
|
|
9264
|
+
"maxLength": 255,
|
|
9265
|
+
"description": "Optional MIME content type."
|
|
9266
|
+
},
|
|
9267
|
+
"cek": {
|
|
9268
|
+
"type": "string",
|
|
9269
|
+
"pattern": "^[A-Za-z0-9_-]{1,128}$",
|
|
9270
|
+
"description": "Base64url-encoded (unpadded) content-encryption key the recipient uses to decrypt. Travels with the email; the object store only ever holds ciphertext."
|
|
9271
|
+
}
|
|
9272
|
+
},
|
|
9273
|
+
"required": [
|
|
9274
|
+
"root",
|
|
9275
|
+
"filename",
|
|
9276
|
+
"cek"
|
|
9277
|
+
]
|
|
9278
|
+
},
|
|
9244
9279
|
"SendMailInput": {
|
|
9245
9280
|
"type": "object",
|
|
9246
9281
|
"additionalProperties": false,
|
|
@@ -9295,6 +9330,12 @@ const openapiDocument = {
|
|
|
9295
9330
|
"description": "Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.",
|
|
9296
9331
|
"items": { "$ref": "#/components/schemas/SendMailAttachment" }
|
|
9297
9332
|
},
|
|
9333
|
+
"payload_attachments": {
|
|
9334
|
+
"type": "array",
|
|
9335
|
+
"maxItems": 1,
|
|
9336
|
+
"description": "Deliver an already-uploaded Primitive Payloads object as an attachment by reference, without inlining the bytes — the way to send attachments larger than the inline cap. Upload the object via /v1/payloads (client-held CEK), then reference it here. v1 supports at most one.",
|
|
9337
|
+
"items": { "$ref": "#/components/schemas/SendMailPayloadRef" }
|
|
9338
|
+
},
|
|
9298
9339
|
"wait": {
|
|
9299
9340
|
"type": "boolean",
|
|
9300
9341
|
"description": "When true, wait for the first downstream SMTP delivery outcome before returning."
|
|
@@ -21832,6 +21873,45 @@ const operationManifest = [
|
|
|
21832
21873
|
"required": ["filename", "content_base64"]
|
|
21833
21874
|
}
|
|
21834
21875
|
},
|
|
21876
|
+
"payload_attachments": {
|
|
21877
|
+
"type": "array",
|
|
21878
|
+
"maxItems": 1,
|
|
21879
|
+
"description": "Deliver an already-uploaded Primitive Payloads object as an attachment by reference, without inlining the bytes — the way to send attachments larger than the inline cap. Upload the object via /v1/payloads (client-held CEK), then reference it here. v1 supports at most one.",
|
|
21880
|
+
"items": {
|
|
21881
|
+
"type": "object",
|
|
21882
|
+
"additionalProperties": false,
|
|
21883
|
+
"description": "A reference to an already-uploaded Primitive Payloads object, delivered as an attachment without inlining the bytes — the way to send an attachment larger than the inline cap. Upload the object via /v1/payloads (with a client-held CEK the server never sees), then reference it here.",
|
|
21884
|
+
"properties": {
|
|
21885
|
+
"root": {
|
|
21886
|
+
"type": "string",
|
|
21887
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
21888
|
+
"description": "The 64-char lowercase-hex Merkle root of a finalized payloads object."
|
|
21889
|
+
},
|
|
21890
|
+
"filename": {
|
|
21891
|
+
"type": "string",
|
|
21892
|
+
"minLength": 1,
|
|
21893
|
+
"maxLength": 255,
|
|
21894
|
+
"description": "Attachment filename presented to the recipient."
|
|
21895
|
+
},
|
|
21896
|
+
"content_type": {
|
|
21897
|
+
"type": "string",
|
|
21898
|
+
"minLength": 1,
|
|
21899
|
+
"maxLength": 255,
|
|
21900
|
+
"description": "Optional MIME content type."
|
|
21901
|
+
},
|
|
21902
|
+
"cek": {
|
|
21903
|
+
"type": "string",
|
|
21904
|
+
"pattern": "^[A-Za-z0-9_-]{1,128}$",
|
|
21905
|
+
"description": "Base64url-encoded (unpadded) content-encryption key the recipient uses to decrypt. Travels with the email; the object store only ever holds ciphertext."
|
|
21906
|
+
}
|
|
21907
|
+
},
|
|
21908
|
+
"required": [
|
|
21909
|
+
"root",
|
|
21910
|
+
"filename",
|
|
21911
|
+
"cek"
|
|
21912
|
+
]
|
|
21913
|
+
}
|
|
21914
|
+
},
|
|
21835
21915
|
"wait": {
|
|
21836
21916
|
"type": "boolean",
|
|
21837
21917
|
"description": "When true, wait for the first downstream SMTP delivery outcome before returning."
|
|
@@ -24841,7 +24921,7 @@ async function fetchEmailSearchPage(params) {
|
|
|
24841
24921
|
rows
|
|
24842
24922
|
};
|
|
24843
24923
|
}
|
|
24844
|
-
function sleep$
|
|
24924
|
+
function sleep$2(ms) {
|
|
24845
24925
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24846
24926
|
}
|
|
24847
24927
|
function cliError$6(message) {
|
|
@@ -25874,7 +25954,7 @@ async function waitForReply(params) {
|
|
|
25874
25954
|
if (lastAccepted !== void 0) continue;
|
|
25875
25955
|
if (phase.deadline !== null && Date.now() >= phase.deadline) break;
|
|
25876
25956
|
if (totalDeadline !== null && Date.now() >= totalDeadline) return null;
|
|
25877
|
-
await sleep$
|
|
25957
|
+
await sleep$2(params.interval * 1e3);
|
|
25878
25958
|
}
|
|
25879
25959
|
}
|
|
25880
25960
|
return null;
|
|
@@ -26656,7 +26736,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
|
26656
26736
|
}
|
|
26657
26737
|
if (cursorAdvanced) continue;
|
|
26658
26738
|
if (deadline !== null && Date.now() >= deadline) break;
|
|
26659
|
-
await sleep$
|
|
26739
|
+
await sleep$2(flags.interval * 1e3);
|
|
26660
26740
|
}
|
|
26661
26741
|
process.stderr.write(`Timed out waiting for ${flags.number} matching email${flags.number === 1 ? "" : "s"}; received ${matched}.\n`);
|
|
26662
26742
|
process.exitCode = 1;
|
|
@@ -26775,7 +26855,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
|
|
|
26775
26855
|
}
|
|
26776
26856
|
if (cursorAdvanced) continue;
|
|
26777
26857
|
if (deadline !== null && Date.now() >= deadline) break;
|
|
26778
|
-
await sleep$
|
|
26858
|
+
await sleep$2(flags.interval * 1e3);
|
|
26779
26859
|
}
|
|
26780
26860
|
}
|
|
26781
26861
|
};
|
|
@@ -27936,8 +28016,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
27936
28016
|
name: "Primitive Team",
|
|
27937
28017
|
url: "https://primitive.dev"
|
|
27938
28018
|
};
|
|
27939
|
-
const SDK_VERSION_RANGE = "^1.
|
|
27940
|
-
const CLI_VERSION_RANGE = "^1.
|
|
28019
|
+
const SDK_VERSION_RANGE = "^1.21.0";
|
|
28020
|
+
const CLI_VERSION_RANGE = "^1.21.0";
|
|
27941
28021
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
27942
28022
|
function renderHandler() {
|
|
27943
28023
|
return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
|
|
@@ -28672,7 +28752,7 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
|
|
|
28672
28752
|
return;
|
|
28673
28753
|
}
|
|
28674
28754
|
completedInitialFollowPoll = true;
|
|
28675
|
-
await sleep$
|
|
28755
|
+
await sleep$2(flags["poll-interval"] * 1e3);
|
|
28676
28756
|
}
|
|
28677
28757
|
});
|
|
28678
28758
|
}
|
|
@@ -29583,7 +29663,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
29583
29663
|
trace = fetched;
|
|
29584
29664
|
break;
|
|
29585
29665
|
}
|
|
29586
|
-
await sleep$
|
|
29666
|
+
await sleep$2(pollIntervalMs);
|
|
29587
29667
|
}
|
|
29588
29668
|
if (!trace) {
|
|
29589
29669
|
const finalTrace = ((await getFunctionTestRunTrace({
|
|
@@ -29962,7 +30042,7 @@ const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
|
|
|
29962
30042
|
function cliError$3(message) {
|
|
29963
30043
|
return new Errors.CLIError(message, { exit: 1 });
|
|
29964
30044
|
}
|
|
29965
|
-
function sleep(ms) {
|
|
30045
|
+
function sleep$1(ms) {
|
|
29966
30046
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29967
30047
|
}
|
|
29968
30048
|
function openBrowser(url) {
|
|
@@ -30127,7 +30207,7 @@ var LoginCommand$1 = class extends Command {
|
|
|
30127
30207
|
let interval = Math.min(Math.max(1, start.interval), MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
|
|
30128
30208
|
let nextPollDelay = 1;
|
|
30129
30209
|
while (Date.now() < deadline) {
|
|
30130
|
-
await sleep(nextPollDelay * 1e3);
|
|
30210
|
+
await sleep$1(nextPollDelay * 1e3);
|
|
30131
30211
|
nextPollDelay = interval;
|
|
30132
30212
|
const polled = await pollCliLogin({
|
|
30133
30213
|
body: { device_code: start.device_code },
|
|
@@ -31757,6 +31837,434 @@ var OrgSecretsSetCommand = class OrgSecretsSetCommand extends Command {
|
|
|
31757
31837
|
}
|
|
31758
31838
|
};
|
|
31759
31839
|
//#endregion
|
|
31840
|
+
//#region ../sdk-node/dist/payloads/index.js
|
|
31841
|
+
/**
|
|
31842
|
+
* Primitive Payloads — streaming client for large, content-addressed,
|
|
31843
|
+
* end-to-end-encrypted objects (attachments up to ~1 TB).
|
|
31844
|
+
*
|
|
31845
|
+
* Objects are split into fixed 64 MiB chunks; each chunk is encrypted with a
|
|
31846
|
+
* per-chunk AES-256-GCM key (HKDF-derived from the object CEK), content-addressed
|
|
31847
|
+
* by its ciphertext SHA-256, and committed under a Merkle root. This module
|
|
31848
|
+
* streams: it processes one chunk at a time and never holds the whole object in
|
|
31849
|
+
* memory, so a multi-GB file uploads/downloads in bounded memory.
|
|
31850
|
+
*
|
|
31851
|
+
* The chunk/manifest construction is byte-compatible with the server's object
|
|
31852
|
+
* model (packages/payloads-core in the API monorepo).
|
|
31853
|
+
*/
|
|
31854
|
+
const CHUNK_SIZE = 64 * 1024 * 1024;
|
|
31855
|
+
const MANIFEST_VERSION = 1;
|
|
31856
|
+
const CHUNK_KDF_INFO = "payloads-chunk";
|
|
31857
|
+
const OBJECT_ID_BYTES = 16;
|
|
31858
|
+
const CEK_BYTES = 32;
|
|
31859
|
+
const subtle = globalThis.crypto.subtle;
|
|
31860
|
+
const textEncoder = new TextEncoder();
|
|
31861
|
+
function randomBytes(length) {
|
|
31862
|
+
const out = new Uint8Array(length);
|
|
31863
|
+
globalThis.crypto.getRandomValues(out);
|
|
31864
|
+
return out;
|
|
31865
|
+
}
|
|
31866
|
+
function toHex(bytes) {
|
|
31867
|
+
let out = "";
|
|
31868
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
31869
|
+
return out;
|
|
31870
|
+
}
|
|
31871
|
+
function fromHex(hex) {
|
|
31872
|
+
if (hex.length % 2 !== 0) throw new Error("invalid hex: odd length");
|
|
31873
|
+
if (hex.length > 0 && !/^[0-9a-fA-F]+$/.test(hex)) throw new Error("invalid hex: non-hex character");
|
|
31874
|
+
const out = new Uint8Array(hex.length / 2);
|
|
31875
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
31876
|
+
return out;
|
|
31877
|
+
}
|
|
31878
|
+
async function contentHash(bytes) {
|
|
31879
|
+
return new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
31880
|
+
}
|
|
31881
|
+
async function contentHashHex(bytes) {
|
|
31882
|
+
return toHex(await contentHash(bytes));
|
|
31883
|
+
}
|
|
31884
|
+
/** Binary Merkle root over ordered hex leaf hashes (odd node promoted). */
|
|
31885
|
+
async function merkleRoot(leafHashesHex) {
|
|
31886
|
+
if (leafHashesHex.length === 0) return contentHashHex(new Uint8Array(0));
|
|
31887
|
+
let level = leafHashesHex.map(fromHex);
|
|
31888
|
+
while (level.length > 1) {
|
|
31889
|
+
const next = [];
|
|
31890
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
31891
|
+
const left = level[i];
|
|
31892
|
+
const right = i + 1 < level.length ? level[i + 1] : level[i];
|
|
31893
|
+
const combined = new Uint8Array(left.length + right.length);
|
|
31894
|
+
combined.set(left, 0);
|
|
31895
|
+
combined.set(right, left.length);
|
|
31896
|
+
next.push(await contentHash(combined));
|
|
31897
|
+
}
|
|
31898
|
+
level = next;
|
|
31899
|
+
}
|
|
31900
|
+
return toHex(level[0]);
|
|
31901
|
+
}
|
|
31902
|
+
async function deriveChunkKey(cek, index) {
|
|
31903
|
+
const baseKey = await subtle.importKey("raw", cek, "HKDF", false, ["deriveKey"]);
|
|
31904
|
+
const info = textEncoder.encode(`${CHUNK_KDF_INFO}:${index}`);
|
|
31905
|
+
return subtle.deriveKey({
|
|
31906
|
+
name: "HKDF",
|
|
31907
|
+
hash: "SHA-256",
|
|
31908
|
+
salt: new Uint8Array(32),
|
|
31909
|
+
info
|
|
31910
|
+
}, baseKey, {
|
|
31911
|
+
name: "AES-GCM",
|
|
31912
|
+
length: 256
|
|
31913
|
+
}, false, ["encrypt", "decrypt"]);
|
|
31914
|
+
}
|
|
31915
|
+
function chunkNonce(index) {
|
|
31916
|
+
const nonce = new Uint8Array(12);
|
|
31917
|
+
new DataView(nonce.buffer).setUint32(8, index >>> 0, false);
|
|
31918
|
+
return nonce;
|
|
31919
|
+
}
|
|
31920
|
+
function chunkAad(objectId, index) {
|
|
31921
|
+
const aad = new Uint8Array(objectId.length + 4);
|
|
31922
|
+
aad.set(objectId, 0);
|
|
31923
|
+
new DataView(aad.buffer).setUint32(objectId.length, index >>> 0, false);
|
|
31924
|
+
return aad;
|
|
31925
|
+
}
|
|
31926
|
+
async function encryptChunk(cek, objectId, index, plaintext) {
|
|
31927
|
+
const key = await deriveChunkKey(cek, index);
|
|
31928
|
+
const ct = await subtle.encrypt({
|
|
31929
|
+
name: "AES-GCM",
|
|
31930
|
+
iv: chunkNonce(index),
|
|
31931
|
+
additionalData: chunkAad(objectId, index)
|
|
31932
|
+
}, key, plaintext);
|
|
31933
|
+
return new Uint8Array(ct);
|
|
31934
|
+
}
|
|
31935
|
+
async function decryptChunk(cek, objectId, index, ciphertext) {
|
|
31936
|
+
const key = await deriveChunkKey(cek, index);
|
|
31937
|
+
const pt = await subtle.decrypt({
|
|
31938
|
+
name: "AES-GCM",
|
|
31939
|
+
iv: chunkNonce(index),
|
|
31940
|
+
additionalData: chunkAad(objectId, index)
|
|
31941
|
+
}, key, ciphertext);
|
|
31942
|
+
return new Uint8Array(pt);
|
|
31943
|
+
}
|
|
31944
|
+
function authHeaders(apiKey) {
|
|
31945
|
+
return { authorization: `Bearer ${apiKey}` };
|
|
31946
|
+
}
|
|
31947
|
+
function apiRoot(baseUrl) {
|
|
31948
|
+
let end = baseUrl.length;
|
|
31949
|
+
while (end > 0 && baseUrl.charCodeAt(end - 1) === 47) end--;
|
|
31950
|
+
return `${baseUrl.slice(0, end)}/v1/payloads`;
|
|
31951
|
+
}
|
|
31952
|
+
const DEFAULT_MAX_RETRIES = 6;
|
|
31953
|
+
const RETRYABLE_STATUS = new Set([
|
|
31954
|
+
429,
|
|
31955
|
+
500,
|
|
31956
|
+
502,
|
|
31957
|
+
503,
|
|
31958
|
+
504
|
|
31959
|
+
]);
|
|
31960
|
+
function sleep(ms) {
|
|
31961
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31962
|
+
}
|
|
31963
|
+
/**
|
|
31964
|
+
* fetch with exponential backoff on transient failures. Request bodies here are
|
|
31965
|
+
* in-memory buffers/strings (never streams), so a retry can safely resend them.
|
|
31966
|
+
*/
|
|
31967
|
+
async function retryingFetch(url, init, label, maxRetries = DEFAULT_MAX_RETRIES) {
|
|
31968
|
+
for (let attempt = 0;; attempt++) {
|
|
31969
|
+
let res;
|
|
31970
|
+
let networkError;
|
|
31971
|
+
try {
|
|
31972
|
+
res = await fetch(url, init);
|
|
31973
|
+
} catch (err) {
|
|
31974
|
+
networkError = err;
|
|
31975
|
+
}
|
|
31976
|
+
if (res && !RETRYABLE_STATUS.has(res.status)) return res;
|
|
31977
|
+
if (attempt >= maxRetries) {
|
|
31978
|
+
if (res) return res;
|
|
31979
|
+
throw new Error(`${label}: network error after ${attempt} retries: ${networkError?.message ?? networkError}`);
|
|
31980
|
+
}
|
|
31981
|
+
if (res) await res.text().catch(() => {});
|
|
31982
|
+
await sleep(Math.min(1e3 * 2 ** attempt, 15e3) + Math.floor(Math.random() * 250));
|
|
31983
|
+
}
|
|
31984
|
+
}
|
|
31985
|
+
async function initiate(opts, manifest) {
|
|
31986
|
+
const res = await retryingFetch(apiRoot(opts.baseUrl), {
|
|
31987
|
+
method: "POST",
|
|
31988
|
+
headers: {
|
|
31989
|
+
...authHeaders(opts.apiKey),
|
|
31990
|
+
"content-type": "application/json"
|
|
31991
|
+
},
|
|
31992
|
+
body: JSON.stringify({ manifest })
|
|
31993
|
+
}, "initiate");
|
|
31994
|
+
if (!res.ok) throw new Error(`initiate failed: HTTP ${res.status} ${await res.text()}`);
|
|
31995
|
+
await res.text();
|
|
31996
|
+
}
|
|
31997
|
+
async function putChunk(opts, root, d, bytes) {
|
|
31998
|
+
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/chunks/${d.ciphertextHash}`, {
|
|
31999
|
+
method: "PUT",
|
|
32000
|
+
headers: {
|
|
32001
|
+
...authHeaders(opts.apiKey),
|
|
32002
|
+
"content-type": "application/octet-stream"
|
|
32003
|
+
},
|
|
32004
|
+
body: bytes
|
|
32005
|
+
}, `chunk ${d.index} upload`);
|
|
32006
|
+
if (!res.ok) throw new Error(`chunk ${d.index} upload failed: HTTP ${res.status} ${await res.text()}`);
|
|
32007
|
+
await res.text();
|
|
32008
|
+
}
|
|
32009
|
+
async function finalize(opts, root) {
|
|
32010
|
+
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/finalize`, {
|
|
32011
|
+
method: "POST",
|
|
32012
|
+
headers: authHeaders(opts.apiKey)
|
|
32013
|
+
}, "finalize");
|
|
32014
|
+
if (!res.ok) throw new Error(`finalize failed: HTTP ${res.status} ${await res.text()}`);
|
|
32015
|
+
await res.text();
|
|
32016
|
+
}
|
|
32017
|
+
async function fetchManifest(opts, root) {
|
|
32018
|
+
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/manifest`, { headers: authHeaders(opts.apiKey) }, "get manifest");
|
|
32019
|
+
if (!res.ok) throw new Error(`get manifest failed: HTTP ${res.status} ${await res.text()}`);
|
|
32020
|
+
return (await res.json()).data.manifest;
|
|
32021
|
+
}
|
|
32022
|
+
async function getChunkBytes(opts, root, d) {
|
|
32023
|
+
const res = await retryingFetch(`${apiRoot(opts.baseUrl)}/${root}/chunks/${d.ciphertextHash}`, { headers: authHeaders(opts.apiKey) }, `get chunk ${d.index}`);
|
|
32024
|
+
if (!res.ok) throw new Error(`get chunk ${d.index} failed: HTTP ${res.status}`);
|
|
32025
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
32026
|
+
}
|
|
32027
|
+
async function readWindow(fh, position, length) {
|
|
32028
|
+
const buf = Buffer.allocUnsafe(length);
|
|
32029
|
+
let read = 0;
|
|
32030
|
+
while (read < length) {
|
|
32031
|
+
const { bytesRead } = await fh.read(buf, read, length - read, position + read);
|
|
32032
|
+
if (bytesRead === 0) break;
|
|
32033
|
+
read += bytesRead;
|
|
32034
|
+
}
|
|
32035
|
+
return read === length ? buf : buf.subarray(0, read);
|
|
32036
|
+
}
|
|
32037
|
+
async function mapLimit(items, limit, fn) {
|
|
32038
|
+
let next = 0;
|
|
32039
|
+
let firstError;
|
|
32040
|
+
const worker = async () => {
|
|
32041
|
+
while (next < items.length && firstError === void 0) {
|
|
32042
|
+
const i = next++;
|
|
32043
|
+
try {
|
|
32044
|
+
await fn(items[i], i);
|
|
32045
|
+
} catch (err) {
|
|
32046
|
+
if (firstError === void 0) firstError = err;
|
|
32047
|
+
return;
|
|
32048
|
+
}
|
|
32049
|
+
}
|
|
32050
|
+
};
|
|
32051
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length || 1) }, () => worker()));
|
|
32052
|
+
if (firstError !== void 0) throw firstError;
|
|
32053
|
+
}
|
|
32054
|
+
/**
|
|
32055
|
+
* Stream a file up as a Primitive Payload. Two passes over the file (encrypt to
|
|
32056
|
+
* build the manifest, then encrypt again to upload) keep memory bounded to a few
|
|
32057
|
+
* chunks — the whole object is never resident. Returns the content address
|
|
32058
|
+
* (merkleRoot) and the hex CEK needed to download it.
|
|
32059
|
+
*/
|
|
32060
|
+
async function pushFile(filePath, opts) {
|
|
32061
|
+
const chunkSize = opts.chunkSize ?? CHUNK_SIZE;
|
|
32062
|
+
const concurrency = opts.concurrency ?? 3;
|
|
32063
|
+
const { size } = await stat(filePath);
|
|
32064
|
+
const cek = randomBytes(CEK_BYTES);
|
|
32065
|
+
const objectId = randomBytes(OBJECT_ID_BYTES);
|
|
32066
|
+
const chunkCount = size === 0 ? 0 : Math.ceil(size / chunkSize);
|
|
32067
|
+
const fh = await open(filePath, "r");
|
|
32068
|
+
try {
|
|
32069
|
+
const descriptors = [];
|
|
32070
|
+
for (let index = 0; index < chunkCount; index++) {
|
|
32071
|
+
const position = index * chunkSize;
|
|
32072
|
+
const length = Math.min(chunkSize, size - position);
|
|
32073
|
+
const plaintext = await readWindow(fh, position, length);
|
|
32074
|
+
const ciphertext = await encryptChunk(cek, objectId, index, plaintext);
|
|
32075
|
+
descriptors.push({
|
|
32076
|
+
index,
|
|
32077
|
+
ciphertextHash: await contentHashHex(ciphertext),
|
|
32078
|
+
plaintextSize: length,
|
|
32079
|
+
ciphertextSize: ciphertext.length
|
|
32080
|
+
});
|
|
32081
|
+
opts.onProgress?.("encrypt", index + 1, chunkCount);
|
|
32082
|
+
}
|
|
32083
|
+
const root = await merkleRoot(descriptors.map((d) => d.ciphertextHash));
|
|
32084
|
+
await initiate(opts, {
|
|
32085
|
+
version: MANIFEST_VERSION,
|
|
32086
|
+
objectId: toHex(objectId),
|
|
32087
|
+
chunkSize,
|
|
32088
|
+
totalPlaintextSize: size,
|
|
32089
|
+
chunkCount,
|
|
32090
|
+
chunks: descriptors,
|
|
32091
|
+
merkleRoot: root
|
|
32092
|
+
});
|
|
32093
|
+
let uploaded = 0;
|
|
32094
|
+
await mapLimit(descriptors, concurrency, async (d) => {
|
|
32095
|
+
const plaintext = await readWindow(fh, d.index * chunkSize, d.plaintextSize);
|
|
32096
|
+
await putChunk(opts, root, d, await encryptChunk(cek, objectId, d.index, plaintext));
|
|
32097
|
+
opts.onProgress?.("upload", ++uploaded, chunkCount);
|
|
32098
|
+
});
|
|
32099
|
+
await finalize(opts, root);
|
|
32100
|
+
return {
|
|
32101
|
+
merkleRoot: root,
|
|
32102
|
+
cek: toHex(cek),
|
|
32103
|
+
chunkCount,
|
|
32104
|
+
totalBytes: size
|
|
32105
|
+
};
|
|
32106
|
+
} finally {
|
|
32107
|
+
await fh.close();
|
|
32108
|
+
}
|
|
32109
|
+
}
|
|
32110
|
+
/**
|
|
32111
|
+
* Stream a Primitive Payload down to a file, verifying and decrypting one chunk
|
|
32112
|
+
* at a time (bounded memory). Every chunk is content-address-checked before
|
|
32113
|
+
* decryption, so a corrupt or substituted chunk throws.
|
|
32114
|
+
*/
|
|
32115
|
+
async function pullFile(root, outPath, opts) {
|
|
32116
|
+
const manifest = await fetchManifest(opts, root);
|
|
32117
|
+
const computedRoot = await merkleRoot(manifest.chunks.map((c) => c.ciphertextHash));
|
|
32118
|
+
if (computedRoot !== root) throw new Error(`manifest does not match the requested content address (got ${computedRoot})`);
|
|
32119
|
+
const cek = fromHex(opts.cek);
|
|
32120
|
+
const objectId = fromHex(manifest.objectId);
|
|
32121
|
+
const out = createWriteStream(outPath);
|
|
32122
|
+
let onWriteError = () => {};
|
|
32123
|
+
const writeErrored = new Promise((_, reject) => {
|
|
32124
|
+
onWriteError = reject;
|
|
32125
|
+
});
|
|
32126
|
+
writeErrored.catch(() => {});
|
|
32127
|
+
out.on("error", (err) => onWriteError(err));
|
|
32128
|
+
try {
|
|
32129
|
+
let done = 0;
|
|
32130
|
+
for (const d of manifest.chunks) {
|
|
32131
|
+
const ciphertext = await getChunkBytes(opts, root, d);
|
|
32132
|
+
if (await contentHashHex(ciphertext) !== d.ciphertextHash) throw new Error(`chunk ${d.index} failed integrity check`);
|
|
32133
|
+
const plaintext = await decryptChunk(cek, objectId, d.index, ciphertext);
|
|
32134
|
+
if (!out.write(plaintext)) await Promise.race([new Promise((resolve) => out.once("drain", resolve)), writeErrored]);
|
|
32135
|
+
opts.onProgress?.("download", ++done, manifest.chunkCount);
|
|
32136
|
+
}
|
|
32137
|
+
await Promise.race([new Promise((resolve) => out.end(resolve)), writeErrored]);
|
|
32138
|
+
} catch (err) {
|
|
32139
|
+
out.destroy();
|
|
32140
|
+
await rm(outPath, { force: true });
|
|
32141
|
+
throw err;
|
|
32142
|
+
}
|
|
32143
|
+
return manifest;
|
|
32144
|
+
}
|
|
32145
|
+
//#endregion
|
|
32146
|
+
//#region src/oclif/commands/payloads.ts
|
|
32147
|
+
const DEFAULT_API_BASE_URL = "https://api.primitive.dev";
|
|
32148
|
+
const authFlags = {
|
|
32149
|
+
"api-key": Flags.string({
|
|
32150
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved login credentials)",
|
|
32151
|
+
env: "PRIMITIVE_API_KEY"
|
|
32152
|
+
}),
|
|
32153
|
+
"api-base-url": Flags.string({
|
|
32154
|
+
description: "Override the API base URL. Internal testing only.",
|
|
32155
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
32156
|
+
hidden: true
|
|
32157
|
+
})
|
|
32158
|
+
};
|
|
32159
|
+
function resolveClient(configDir, flags) {
|
|
32160
|
+
const auth = resolveCliAuth({
|
|
32161
|
+
configDir,
|
|
32162
|
+
apiKey: flags["api-key"],
|
|
32163
|
+
apiBaseUrl: flags["api-base-url"]
|
|
32164
|
+
});
|
|
32165
|
+
if (!auth.apiKey) throw new Error("Not authenticated: set PRIMITIVE_API_KEY, pass --api-key, or run `primitive login`.");
|
|
32166
|
+
return {
|
|
32167
|
+
baseUrl: auth.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
32168
|
+
apiKey: auth.apiKey
|
|
32169
|
+
};
|
|
32170
|
+
}
|
|
32171
|
+
function progressToStderr() {
|
|
32172
|
+
let lastPct = -1;
|
|
32173
|
+
return (phase, done, total) => {
|
|
32174
|
+
const pct = total === 0 ? 100 : Math.floor(done / total * 100);
|
|
32175
|
+
if (pct !== lastPct) {
|
|
32176
|
+
lastPct = pct;
|
|
32177
|
+
process.stderr.write(`\r${phase}: ${done}/${total} (${pct}%) `);
|
|
32178
|
+
if (done === total) process.stderr.write("\n");
|
|
32179
|
+
}
|
|
32180
|
+
};
|
|
32181
|
+
}
|
|
32182
|
+
var PayloadsPushCommand = class PayloadsPushCommand extends Command {
|
|
32183
|
+
static description = `Upload a file as a Primitive Payload — a large, content-addressed, end-to-end-encrypted object.
|
|
32184
|
+
|
|
32185
|
+
The file is chunked and encrypted client-side and streamed up in bounded
|
|
32186
|
+
memory (multi-GB files never load fully into RAM). Prints the object's content
|
|
32187
|
+
address (merkle_root) and the hex CEK required to download it — keep the CEK
|
|
32188
|
+
secret; without it the object cannot be decrypted.`;
|
|
32189
|
+
static summary = "Stream-upload a file as an encrypted payload";
|
|
32190
|
+
static examples = ["<%= config.bin %> payloads push ./big-video.mp4"];
|
|
32191
|
+
static args = { file: Args.string({
|
|
32192
|
+
required: true,
|
|
32193
|
+
description: "Path to the file to upload"
|
|
32194
|
+
}) };
|
|
32195
|
+
static flags = {
|
|
32196
|
+
...authFlags,
|
|
32197
|
+
concurrency: Flags.integer({
|
|
32198
|
+
description: "Parallel chunk uploads",
|
|
32199
|
+
default: 3
|
|
32200
|
+
}),
|
|
32201
|
+
quiet: Flags.boolean({
|
|
32202
|
+
description: "Suppress progress output",
|
|
32203
|
+
default: false
|
|
32204
|
+
})
|
|
32205
|
+
};
|
|
32206
|
+
async run() {
|
|
32207
|
+
const { args, flags } = await this.parse(PayloadsPushCommand);
|
|
32208
|
+
const { baseUrl, apiKey } = resolveClient(this.config.configDir, flags);
|
|
32209
|
+
const result = await pushFile(args.file, {
|
|
32210
|
+
baseUrl,
|
|
32211
|
+
apiKey,
|
|
32212
|
+
concurrency: flags.concurrency,
|
|
32213
|
+
onProgress: flags.quiet ? void 0 : progressToStderr()
|
|
32214
|
+
});
|
|
32215
|
+
this.log(JSON.stringify({
|
|
32216
|
+
merkle_root: result.merkleRoot,
|
|
32217
|
+
cek: result.cek,
|
|
32218
|
+
chunk_count: result.chunkCount,
|
|
32219
|
+
total_bytes: result.totalBytes
|
|
32220
|
+
}, null, 2));
|
|
32221
|
+
}
|
|
32222
|
+
};
|
|
32223
|
+
var PayloadsPullCommand = class PayloadsPullCommand extends Command {
|
|
32224
|
+
static description = `Download and decrypt a Primitive Payload to a file.
|
|
32225
|
+
|
|
32226
|
+
Streams one chunk at a time, verifying each against its content address before
|
|
32227
|
+
decryption, so a corrupt or substituted chunk fails loudly. Requires the hex
|
|
32228
|
+
CEK printed by \`payloads push\`.`;
|
|
32229
|
+
static summary = "Stream-download and decrypt a payload to a file";
|
|
32230
|
+
static examples = ["<%= config.bin %> payloads pull <merkle_root> --cek <hex> --out ./restored.mp4"];
|
|
32231
|
+
static args = { root: Args.string({
|
|
32232
|
+
required: true,
|
|
32233
|
+
description: "Object content address (merkle_root)"
|
|
32234
|
+
}) };
|
|
32235
|
+
static flags = {
|
|
32236
|
+
...authFlags,
|
|
32237
|
+
out: Flags.string({
|
|
32238
|
+
required: true,
|
|
32239
|
+
description: "Output file path"
|
|
32240
|
+
}),
|
|
32241
|
+
cek: Flags.string({
|
|
32242
|
+
required: true,
|
|
32243
|
+
description: "Hex content-encryption key from `payloads push`"
|
|
32244
|
+
}),
|
|
32245
|
+
quiet: Flags.boolean({
|
|
32246
|
+
description: "Suppress progress output",
|
|
32247
|
+
default: false
|
|
32248
|
+
})
|
|
32249
|
+
};
|
|
32250
|
+
async run() {
|
|
32251
|
+
const { args, flags } = await this.parse(PayloadsPullCommand);
|
|
32252
|
+
const { baseUrl, apiKey } = resolveClient(this.config.configDir, flags);
|
|
32253
|
+
const manifest = await pullFile(args.root, flags.out, {
|
|
32254
|
+
baseUrl,
|
|
32255
|
+
apiKey,
|
|
32256
|
+
cek: flags.cek,
|
|
32257
|
+
onProgress: flags.quiet ? void 0 : progressToStderr()
|
|
32258
|
+
});
|
|
32259
|
+
this.log(JSON.stringify({
|
|
32260
|
+
merkle_root: args.root,
|
|
32261
|
+
out: flags.out,
|
|
32262
|
+
chunk_count: manifest.chunkCount,
|
|
32263
|
+
total_bytes: manifest.totalPlaintextSize
|
|
32264
|
+
}, null, 2));
|
|
32265
|
+
}
|
|
32266
|
+
};
|
|
32267
|
+
//#endregion
|
|
31760
32268
|
//#region ../sdk-node/dist/x402/index.js
|
|
31761
32269
|
/**
|
|
31762
32270
|
* x402 client-side signing.
|
|
@@ -33186,7 +33694,7 @@ async function pollForSettlementInteraction(params) {
|
|
|
33186
33694
|
}
|
|
33187
33695
|
if (receipt) return receipt;
|
|
33188
33696
|
if (deadline !== null && Date.now() >= deadline) break;
|
|
33189
|
-
await sleep$
|
|
33697
|
+
await sleep$2(params.intervalSeconds * 1e3);
|
|
33190
33698
|
}
|
|
33191
33699
|
return null;
|
|
33192
33700
|
}
|
|
@@ -36148,6 +36656,8 @@ const COMMANDS = {
|
|
|
36148
36656
|
"org:secrets:list": OrgSecretsListCommand,
|
|
36149
36657
|
"org:secrets:set": OrgSecretsSetCommand,
|
|
36150
36658
|
"org:secrets:remove": OrgSecretsRemoveCommand,
|
|
36659
|
+
"payloads:push": PayloadsPushCommand,
|
|
36660
|
+
"payloads:pull": PayloadsPullCommand,
|
|
36151
36661
|
"functions:test": FunctionsTestFunctionCommand,
|
|
36152
36662
|
"functions:test-function": FunctionsTestFunctionCommand,
|
|
36153
36663
|
"functions:route-set": FunctionsRouteSetCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primcli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|