primcli 1.14.0 → 1.14.2
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
CHANGED
|
@@ -73,6 +73,26 @@ primitive deliveries replay --id <delivery-id>
|
|
|
73
73
|
|
|
74
74
|
Generated API commands remain available for compatibility and full schema parity, for example `primitive emails:list-emails` and `primitive sending:reply-to-email`.
|
|
75
75
|
|
|
76
|
+
## Recipient routing
|
|
77
|
+
|
|
78
|
+
Bind a recipient address to a destination so inbound mail resolves to a single
|
|
79
|
+
endpoint. Pass `--function` to route an address to a function (its route-target
|
|
80
|
+
endpoint is created in the same call, enabling per-address routing like
|
|
81
|
+
`alice@acme.com -> functionA`), or `--endpoint` for an existing endpoint.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
primitive routes add alice@acme.com --function <function-id>
|
|
85
|
+
primitive routes add 'support+*@acme.com' --match wildcard --endpoint <endpoint-id>
|
|
86
|
+
primitive routes list
|
|
87
|
+
primitive routes test alice@acme.com # preview where an address resolves, with the rule trace
|
|
88
|
+
primitive routes update <route-id> --priority 5
|
|
89
|
+
primitive routes reorder --set <route-id>=10 --set <other-id>=20
|
|
90
|
+
primitive routes remove <route-id>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Recipient routing is gated by an organization entitlement; routes are inert
|
|
94
|
+
until it is enabled.
|
|
95
|
+
|
|
76
96
|
## x402 payments
|
|
77
97
|
|
|
78
98
|
The `primitive payments` command group drives non-custodial x402 USDC payments. One agent registers a payout address and requests a payment; the paying agent signs locally with its own wallet key and settles. The key never leaves your machine. Networks are `base` and `base-sepolia`. Amounts take a human USDC value (`--amount-usdc 0.01`) or token base units (`--amount 10000`, since USDC has 6 decimals). Your org is resolved automatically from your API key, so payout registration takes no org flag.
|
|
@@ -1124,6 +1124,21 @@ function detectPrimitiveKeyEnvMisname(env = process.env) {
|
|
|
1124
1124
|
if ((primitiveKey?.length ?? 0) > 0 && (primitiveApiKey?.length ?? 0) === 0) return "PRIMITIVE_KEY is set but the CLI reads PRIMITIVE_API_KEY. Rename your env var, or re-run with PRIMITIVE_API_KEY=$PRIMITIVE_KEY.";
|
|
1125
1125
|
return null;
|
|
1126
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* True when a usable OAuth login session is saved on disk. Used by `pay-email`
|
|
1129
|
+
* to decide whether it can safely ignore an env-only `PRIMITIVE_API_KEY` in
|
|
1130
|
+
* favor of the payer's logged-in account (which owns the inbound challenge
|
|
1131
|
+
* inbox). Returns false when nothing is stored or the stored credential is
|
|
1132
|
+
* unreadable/legacy, so the caller falls back to whatever key did resolve
|
|
1133
|
+
* rather than silently dropping the user's only auth.
|
|
1134
|
+
*/
|
|
1135
|
+
function hasStoredCliLogin(configDir) {
|
|
1136
|
+
try {
|
|
1137
|
+
return loadCliCredentials(configDir) !== null;
|
|
1138
|
+
} catch {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1127
1142
|
function resolveCliAuth(params) {
|
|
1128
1143
|
const apiKey = params.apiKey?.trim();
|
|
1129
1144
|
const apiBaseUrl = normalizeApiBaseUrl(params.apiBaseUrl);
|
|
@@ -1327,4 +1342,4 @@ function redactCliEnvironment(environment) {
|
|
|
1327
1342
|
};
|
|
1328
1343
|
}
|
|
1329
1344
|
//#endregion
|
|
1330
|
-
export {
|
|
1345
|
+
export { saveActiveChatState as A, resolveCliAuth as C, deleteChatState as D, chatStatePath as E, createClient as M, createConfig as N, loadActiveChatState as O, normalizeApiBaseUrl as S, saveSignupCredentials as T, deleteCliCredentials as _, normalizeCliEnvironmentName as a, hasStoredCliLogin as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, PrimitiveApiClient as j, loadChatConversationByLocalId as k, saveCliConfig as l, cliAccessTokenExpiresAt as m, deleteCliConfig as n, redactCliEnvironment as o, acquireCliCredentialsLock as p, emptyCliConfig as r, removeCliEnvironment as s, DEFAULT_ENVIRONMENT as t, upsertCliEnvironment as u, deleteCliCredentialsLock as v, saveCliCredentials as w, loadCliCredentials as x, detectPrimitiveKeyEnvMisname as y };
|
package/dist/oclif/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
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-C-7V83Qd.js";
|
|
2
2
|
import { Args, Command, Errors, Flags, ux } from "@oclif/core";
|
|
3
3
|
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -7,6 +7,7 @@ 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 { gunzipSync } from "node:zlib";
|
|
10
11
|
import { concat, getAddress, hexToBytes, keccak256, stringToBytes } from "viem";
|
|
11
12
|
import { privateKeyToAccount } from "viem/accounts";
|
|
12
13
|
//#region \0rolldown/runtime.js
|
|
@@ -3606,6 +3607,7 @@ const openapiDocument = {
|
|
|
3606
3607
|
},
|
|
3607
3608
|
"400": { "$ref": "#/components/responses/ValidationError" },
|
|
3608
3609
|
"401": { "$ref": "#/components/responses/Unauthorized" },
|
|
3610
|
+
"403": { "$ref": "#/components/responses/Forbidden" },
|
|
3609
3611
|
"409": { "$ref": "#/components/responses/Conflict" }
|
|
3610
3612
|
}
|
|
3611
3613
|
}
|
|
@@ -3675,6 +3677,7 @@ const openapiDocument = {
|
|
|
3675
3677
|
},
|
|
3676
3678
|
"400": { "$ref": "#/components/responses/ValidationError" },
|
|
3677
3679
|
"401": { "$ref": "#/components/responses/Unauthorized" },
|
|
3680
|
+
"403": { "$ref": "#/components/responses/Forbidden" },
|
|
3678
3681
|
"404": { "$ref": "#/components/responses/NotFound" },
|
|
3679
3682
|
"409": { "$ref": "#/components/responses/Conflict" }
|
|
3680
3683
|
}
|
|
@@ -27465,7 +27468,80 @@ var X402Error = class extends Error {
|
|
|
27465
27468
|
this.retryAfter = options?.retryAfter ?? null;
|
|
27466
27469
|
}
|
|
27467
27470
|
};
|
|
27468
|
-
|
|
27471
|
+
/** A UUID, as carried by `interaction_id`'s local part and the step ids. */
|
|
27472
|
+
const EXTRACT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27473
|
+
/** An interaction id is `uuid@domain`. */
|
|
27474
|
+
const EXTRACT_WIRE_ID_RE = new RegExp(`^${EXTRACT_UUID_RE.source.slice(1, -1)}@[^\\s@]+$`, "i");
|
|
27475
|
+
/**
|
|
27476
|
+
* Parse the bytes of an inbound `interaction.json` MIME part into a typed
|
|
27477
|
+
* {@link X402EmailChallenge} ready for {@link X402Client.payEmailChallenge}.
|
|
27478
|
+
*
|
|
27479
|
+
* A payer receives the x402 challenge as an `interaction.json` attachment on an
|
|
27480
|
+
* inbound email (filename `interaction.json`, content type `application/json`).
|
|
27481
|
+
* This validates the envelope (the strict snake_case wire shape, that it is the
|
|
27482
|
+
* `x402.payment` `challenge` step, and that the embedded payload carries the
|
|
27483
|
+
* fields a payer signs over) and re-assembles the nonce binding from the
|
|
27484
|
+
* envelope's `interaction_id` + `step_id` + the payload's `challenge_nonce`, so
|
|
27485
|
+
* the caller never has to hand-parse the part.
|
|
27486
|
+
*
|
|
27487
|
+
* Accepts the part body as a UTF-8 string, a `Uint8Array`/`Buffer`, or an
|
|
27488
|
+
* already-parsed envelope object. Throws {@link X402Error} (status 0) on any
|
|
27489
|
+
* malformed or non-challenge part.
|
|
27490
|
+
*
|
|
27491
|
+
* The resulting `challenge_id` is empty: the platform's private challenge id is
|
|
27492
|
+
* not carried on the wire, and `payEmailChallenge` does not need it (it binds to
|
|
27493
|
+
* the `interaction_id` and the challenge step id).
|
|
27494
|
+
*/
|
|
27495
|
+
function parseEmailChallengeFromPart(part) {
|
|
27496
|
+
const bad = (field) => {
|
|
27497
|
+
throw new X402Error(`interaction.json part is not a valid x402 challenge: ${field}`, 0);
|
|
27498
|
+
};
|
|
27499
|
+
let envelope;
|
|
27500
|
+
if (typeof part === "string" || part instanceof Uint8Array) {
|
|
27501
|
+
const text = typeof part === "string" ? part : new TextDecoder().decode(part);
|
|
27502
|
+
let parsed;
|
|
27503
|
+
try {
|
|
27504
|
+
parsed = JSON.parse(text);
|
|
27505
|
+
} catch (cause) {
|
|
27506
|
+
throw new X402Error(`interaction.json part is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`, 0, void 0, { cause });
|
|
27507
|
+
}
|
|
27508
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) bad("envelope (expected a JSON object)");
|
|
27509
|
+
envelope = parsed;
|
|
27510
|
+
} else if (part && typeof part === "object" && !Array.isArray(part) && !(part instanceof Uint8Array)) envelope = part;
|
|
27511
|
+
else return bad("envelope (expected JSON bytes, a string, or an object)");
|
|
27512
|
+
if (envelope.interaction_version !== 1) bad("interaction_version (expected 1)");
|
|
27513
|
+
const interactionId = envelope.interaction_id;
|
|
27514
|
+
if (typeof interactionId !== "string" || !EXTRACT_WIRE_ID_RE.test(interactionId)) bad("interaction_id (expected uuid@domain)");
|
|
27515
|
+
if (envelope.protocol !== "x402.payment") bad(`protocol (expected "${X402_INTERACTION_PROTOCOL}")`);
|
|
27516
|
+
if (envelope.protocol_version !== 1) bad(`protocol_version (expected 1)`);
|
|
27517
|
+
if (envelope.step !== "challenge") bad(`step (expected "challenge", got "${String(envelope.step)}")`);
|
|
27518
|
+
const stepId = envelope.step_id;
|
|
27519
|
+
if (typeof stepId !== "string" || !EXTRACT_UUID_RE.test(stepId)) bad("step_id (expected a uuid)");
|
|
27520
|
+
const expiresAt = envelope.expires_at;
|
|
27521
|
+
if (typeof expiresAt !== "string" || !expiresAt) bad("expires_at (expected an ISO-8601 timestamp on the challenge step)");
|
|
27522
|
+
const payload = envelope.payload;
|
|
27523
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) bad("payload (expected an object)");
|
|
27524
|
+
const p = payload;
|
|
27525
|
+
const challengeNonce = p.challenge_nonce;
|
|
27526
|
+
if (typeof challengeNonce !== "string" || !/^[0-9a-f]{64}$/.test(challengeNonce)) bad("payload.challenge_nonce (expected 64 lowercase hex chars)");
|
|
27527
|
+
const paymentRequirements = p.payment_requirements;
|
|
27528
|
+
if (!paymentRequirements || typeof paymentRequirements !== "object" || Array.isArray(paymentRequirements)) bad("payload.payment_requirements (expected an object)");
|
|
27529
|
+
const challenge = {
|
|
27530
|
+
interaction_id: interactionId,
|
|
27531
|
+
challenge_id: "",
|
|
27532
|
+
challenge: {
|
|
27533
|
+
payment_requirements: paymentRequirements,
|
|
27534
|
+
nonce_binding: {
|
|
27535
|
+
interaction_id: interactionId,
|
|
27536
|
+
challenge_step_id: stepId,
|
|
27537
|
+
challenge_nonce: challengeNonce
|
|
27538
|
+
},
|
|
27539
|
+
expires_at: expiresAt
|
|
27540
|
+
}
|
|
27541
|
+
};
|
|
27542
|
+
validateEmailChallenge(challenge);
|
|
27543
|
+
return challenge;
|
|
27544
|
+
}
|
|
27469
27545
|
/**
|
|
27470
27546
|
* Assert a challenge is fully hydrated before signing, so a missing field fails
|
|
27471
27547
|
* with a named X402Error instead of an opaque viem/BigInt error mid-sign.
|
|
@@ -27785,6 +27861,149 @@ function createX402Client(options = {}) {
|
|
|
27785
27861
|
return new X402Client(options);
|
|
27786
27862
|
}
|
|
27787
27863
|
//#endregion
|
|
27864
|
+
//#region src/oclif/commands/payments-email-challenge.ts
|
|
27865
|
+
/** The exact attachment part name the x402 challenge travels in. */
|
|
27866
|
+
const INTERACTION_PART_FILENAME$1 = "interaction.json";
|
|
27867
|
+
/** Content type the challenge attachment is sent with. */
|
|
27868
|
+
const INTERACTION_PART_CONTENT_TYPE$1 = "application/json";
|
|
27869
|
+
/**
|
|
27870
|
+
* Strip a leading `<digits>_` part-index prefix from an archive entry's
|
|
27871
|
+
* basename. The attachments archive names each member `<part_index>_<filename>`,
|
|
27872
|
+
* so `0_interaction.json` -> `interaction.json`.
|
|
27873
|
+
*/
|
|
27874
|
+
function stripPartIndexPrefix(basename) {
|
|
27875
|
+
return basename.replace(/^\d+_/, "");
|
|
27876
|
+
}
|
|
27877
|
+
/**
|
|
27878
|
+
* Resolve the exact archive entry name for the challenge attachment from email
|
|
27879
|
+
* attachment metadata. Returns the matching entry's `tar_path` (e.g.
|
|
27880
|
+
* `0_interaction.json`), or null when no `interaction.json` attachment is
|
|
27881
|
+
* described. Prefers an exact `filename === "interaction.json"` match; falls back
|
|
27882
|
+
* to a single `application/json` part when the filename is absent.
|
|
27883
|
+
*/
|
|
27884
|
+
function interactionTarPathFromMeta(attachments) {
|
|
27885
|
+
if (!attachments || attachments.length === 0) return null;
|
|
27886
|
+
const chosen = attachments.find((a) => a.filename === "interaction.json") ?? attachments.find((a) => a.content_type === INTERACTION_PART_CONTENT_TYPE$1);
|
|
27887
|
+
if (!chosen) return null;
|
|
27888
|
+
if (chosen.tar_path) return chosen.tar_path;
|
|
27889
|
+
if (typeof chosen.part_index === "number" && chosen.filename) return `${chosen.part_index}_${chosen.filename}`;
|
|
27890
|
+
return chosen.filename ?? null;
|
|
27891
|
+
}
|
|
27892
|
+
/** Build the attachments-archive URL for an inbound email id. */
|
|
27893
|
+
function attachmentsArchiveUrl(baseUrl, emailId) {
|
|
27894
|
+
return `${baseUrl.replace(/\/$/, "")}/emails/${encodeURIComponent(emailId)}/attachments.tar.gz`;
|
|
27895
|
+
}
|
|
27896
|
+
/**
|
|
27897
|
+
* Minimal POSIX (ustar) tar reader: yield each regular-file entry's name and
|
|
27898
|
+
* bytes. We avoid a third-party tar dependency because the published CLI bundles
|
|
27899
|
+
* everything (the tarball-isolation check forbids unexpected runtime deps) and a
|
|
27900
|
+
* tar archive of email attachments is a flat list of small regular files, which
|
|
27901
|
+
* the base format already covers.
|
|
27902
|
+
*
|
|
27903
|
+
* Tar layout: a sequence of 512-byte records. Each file is one 512-byte header
|
|
27904
|
+
* followed by ceil(size/512) data records. Two consecutive all-zero headers mark
|
|
27905
|
+
* the end. The header's name is at offset 0 (100 bytes), size at offset 124 (12
|
|
27906
|
+
* bytes, octal), and the type flag at offset 156 ('0' or '\0' = regular file).
|
|
27907
|
+
* GNU long names (type 'L') and the ustar `prefix` field are handled so a longer
|
|
27908
|
+
* path still resolves; other special types are skipped.
|
|
27909
|
+
*/
|
|
27910
|
+
function* readTarEntries(buffer) {
|
|
27911
|
+
const BLOCK = 512;
|
|
27912
|
+
let offset = 0;
|
|
27913
|
+
let longName = null;
|
|
27914
|
+
const readString = (start, length) => {
|
|
27915
|
+
const slice = buffer.subarray(start, start + length);
|
|
27916
|
+
let end = slice.indexOf(0);
|
|
27917
|
+
if (end === -1) end = slice.length;
|
|
27918
|
+
return new TextDecoder().decode(slice.subarray(0, end));
|
|
27919
|
+
};
|
|
27920
|
+
while (offset + BLOCK <= buffer.length) {
|
|
27921
|
+
const header = buffer.subarray(offset, offset + BLOCK);
|
|
27922
|
+
if (header.every((byte) => byte === 0)) break;
|
|
27923
|
+
const sizeField = readString(offset + 124, 12).trim();
|
|
27924
|
+
const size = sizeField ? Number.parseInt(sizeField, 8) : 0;
|
|
27925
|
+
const safeSize = Number.isFinite(size) && size >= 0 ? size : 0;
|
|
27926
|
+
const typeFlag = String.fromCharCode(header[156] ?? 0);
|
|
27927
|
+
const dataStart = offset + BLOCK;
|
|
27928
|
+
const dataEnd = dataStart + safeSize;
|
|
27929
|
+
if (typeFlag === "L") longName = readString(dataStart, safeSize).replace(/\0+$/, "");
|
|
27930
|
+
else if (typeFlag === "0" || typeFlag === "\0") {
|
|
27931
|
+
const name = longName ?? (() => {
|
|
27932
|
+
const prefix = readString(offset + 345, 155);
|
|
27933
|
+
const base = readString(offset, 100);
|
|
27934
|
+
return prefix ? `${prefix}/${base}` : base;
|
|
27935
|
+
})();
|
|
27936
|
+
longName = null;
|
|
27937
|
+
yield {
|
|
27938
|
+
name,
|
|
27939
|
+
bytes: buffer.subarray(dataStart, dataEnd)
|
|
27940
|
+
};
|
|
27941
|
+
} else longName = null;
|
|
27942
|
+
offset = dataStart + Math.ceil(safeSize / BLOCK) * BLOCK;
|
|
27943
|
+
}
|
|
27944
|
+
}
|
|
27945
|
+
/**
|
|
27946
|
+
* Pull the `interaction.json` bytes out of a gzipped tar attachments archive.
|
|
27947
|
+
* Returns null when the archive has no such member (e.g. the inbound email is
|
|
27948
|
+
* not an x402 challenge).
|
|
27949
|
+
*
|
|
27950
|
+
* When `tarPath` is given (the exact entry name from the email's attachment
|
|
27951
|
+
* metadata, e.g. `0_interaction.json`), the member is matched by that exact name
|
|
27952
|
+
* first. As a fallback (and when no `tarPath` is supplied), the member is matched
|
|
27953
|
+
* by basename after stripping any leading `<digits>_` part-index prefix, so both
|
|
27954
|
+
* `0_interaction.json` and a bare `interaction.json` resolve, and a leading
|
|
27955
|
+
* directory in the archive path does not hide it.
|
|
27956
|
+
*/
|
|
27957
|
+
function interactionJsonFromArchive(gzipped, tarPath) {
|
|
27958
|
+
const tar = gunzipSync(gzipped);
|
|
27959
|
+
const wantedBase = tarPath ? tarPath.split("/").pop() ?? tarPath : null;
|
|
27960
|
+
for (const entry of readTarEntries(tar)) {
|
|
27961
|
+
const base = entry.name.split("/").pop() ?? entry.name;
|
|
27962
|
+
if (wantedBase && base === wantedBase) return entry.bytes;
|
|
27963
|
+
if (!wantedBase && stripPartIndexPrefix(base) === "interaction.json") return entry.bytes;
|
|
27964
|
+
}
|
|
27965
|
+
if (wantedBase) {
|
|
27966
|
+
for (const entry of readTarEntries(tar)) if (stripPartIndexPrefix(entry.name.split("/").pop() ?? entry.name) === "interaction.json") return entry.bytes;
|
|
27967
|
+
}
|
|
27968
|
+
return null;
|
|
27969
|
+
}
|
|
27970
|
+
/**
|
|
27971
|
+
* Fetch an inbound email's attachments archive and return the raw
|
|
27972
|
+
* `interaction.json` bytes, or null when the email carries no such part.
|
|
27973
|
+
*
|
|
27974
|
+
* Mirrors the direct-fetch + bearer-auth pattern the zone-file download command
|
|
27975
|
+
* uses: the typed SDK client returns the archive as a Blob, which is awkward to
|
|
27976
|
+
* decompress; a plain fetch hands us an ArrayBuffer directly. `fetchImpl` is
|
|
27977
|
+
* injectable so tests can stub the network boundary.
|
|
27978
|
+
*/
|
|
27979
|
+
async function fetchInteractionJsonBytes(params) {
|
|
27980
|
+
const response = await (params.fetchImpl ?? fetch)(attachmentsArchiveUrl(params.baseUrl, params.emailId), { headers: {
|
|
27981
|
+
...params.headers ?? {},
|
|
27982
|
+
...params.apiKey ? { authorization: `Bearer ${params.apiKey}` } : {}
|
|
27983
|
+
} });
|
|
27984
|
+
if (!response.ok) {
|
|
27985
|
+
const text = await response.text().catch(() => "");
|
|
27986
|
+
throw new Error(`Could not download attachments for inbound email ${params.emailId}: HTTP ${response.status}${text.trim() ? ` ${text.trim().slice(0, 200)}` : ""}`);
|
|
27987
|
+
}
|
|
27988
|
+
return interactionJsonFromArchive(new Uint8Array(await response.arrayBuffer()), interactionTarPathFromMeta(params.attachments));
|
|
27989
|
+
}
|
|
27990
|
+
/**
|
|
27991
|
+
* Derive the signable {@link X402EmailChallenge} from an inbound payment-request
|
|
27992
|
+
* email's `interaction.json` attachment. This is the auto-derive path behind
|
|
27993
|
+
* `pay-email --in-reply-to <id>` (with no `--challenge`) and
|
|
27994
|
+
* `challenge-from-email`: download the part, then reshape the wire envelope into
|
|
27995
|
+
* the challenge object via the SDK's canonical `parseEmailChallengeFromPart`.
|
|
27996
|
+
*
|
|
27997
|
+
* Throws a descriptive error when the email has no `interaction.json` part (so a
|
|
27998
|
+
* non-challenge email gives a clear message instead of a confusing signer crash)
|
|
27999
|
+
* or when the part is not a valid x402 challenge envelope (the SDK validator).
|
|
28000
|
+
*/
|
|
28001
|
+
async function deriveEmailChallengeFromInbound(params) {
|
|
28002
|
+
const bytes = await fetchInteractionJsonBytes(params);
|
|
28003
|
+
if (!bytes) throw new Error(`Inbound email ${params.emailId} has no ${INTERACTION_PART_FILENAME$1} attachment, so it is not an x402 payment request. Pass --challenge / --challenge-file if you have the challenge from another source.`);
|
|
28004
|
+
return parseEmailChallengeFromPart(bytes);
|
|
28005
|
+
}
|
|
28006
|
+
//#endregion
|
|
27788
28007
|
//#region src/oclif/commands/payments-shared.ts
|
|
27789
28008
|
const PRIVATE_KEY_ENV = "PRIMITIVE_X402_PRIVATE_KEY";
|
|
27790
28009
|
const PRIVATE_KEY_FLAG_DESCRIPTION = `Hex private key (0x-prefixed) of the wallet that signs. Prefer the ${PRIVATE_KEY_ENV} environment variable so the key is not exposed in shell history or the process list. The key is used locally to sign and never sent to Primitive.`;
|
|
@@ -27899,6 +28118,71 @@ function reportX402Error(error, ctx) {
|
|
|
27899
28118
|
});
|
|
27900
28119
|
}
|
|
27901
28120
|
//#endregion
|
|
28121
|
+
//#region src/oclif/commands/payments-challenge-from-email.ts
|
|
28122
|
+
var PaymentsChallengeFromEmailCommand = class PaymentsChallengeFromEmailCommand extends Command {
|
|
28123
|
+
static description = `Derive and print the email x402 challenge object from a received payment-request email.
|
|
28124
|
+
|
|
28125
|
+
A payer receives an x402 payment request as an email carrying an
|
|
28126
|
+
\`interaction.json\` attachment (the wire envelope). The signing commands,
|
|
28127
|
+
however, take the challenge object the payee's \`create-email-challenge\` API
|
|
28128
|
+
returns, which has a different field layout. This command downloads the inbound
|
|
28129
|
+
email's \`interaction.json\` attachment and reshapes it into that challenge
|
|
28130
|
+
object, printing it to stdout.
|
|
28131
|
+
|
|
28132
|
+
Use it when you want the challenge as a portable artifact (e.g. to feed
|
|
28133
|
+
\`pay-email-step\` for offline signing, or an SDK \`payEmailChallenge\` call).
|
|
28134
|
+
For the simple one-shot, prefer \`pay-email --in-reply-to <id>\`, which derives
|
|
28135
|
+
the challenge internally and needs no intermediate file.
|
|
28136
|
+
|
|
28137
|
+
primitive payments challenge-from-email --id <inbound-id> > challenge.json
|
|
28138
|
+
primitive payments challenge-from-email --id <inbound-id> \\
|
|
28139
|
+
| primitive payments pay-email-step > interaction.json`;
|
|
28140
|
+
static summary = "Print the email x402 challenge object derived from a received payment-request email";
|
|
28141
|
+
static examples = ["<%= config.bin %> payments challenge-from-email --id <inbound-email-id>", "<%= config.bin %> payments challenge-from-email --id <inbound-email-id> | <%= config.bin %> payments pay-email-step > interaction.json"];
|
|
28142
|
+
static flags = {
|
|
28143
|
+
"api-key": Flags.string({
|
|
28144
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
28145
|
+
env: "PRIMITIVE_API_KEY"
|
|
28146
|
+
}),
|
|
28147
|
+
"api-base-url": Flags.string({
|
|
28148
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
28149
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
28150
|
+
hidden: true
|
|
28151
|
+
}),
|
|
28152
|
+
id: Flags.string({
|
|
28153
|
+
description: "Id of the inbound payment-request email whose interaction.json attachment carries the challenge.",
|
|
28154
|
+
required: true
|
|
28155
|
+
}),
|
|
28156
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
28157
|
+
};
|
|
28158
|
+
async run() {
|
|
28159
|
+
const { flags } = await this.parse(PaymentsChallengeFromEmailCommand);
|
|
28160
|
+
const { auth, baseUrlOverridden, requestConfig } = await createAuthenticatedCliApiClient({
|
|
28161
|
+
apiKey: flags["api-key"],
|
|
28162
|
+
apiBaseUrl: flags["api-base-url"],
|
|
28163
|
+
configDir: this.config.configDir
|
|
28164
|
+
});
|
|
28165
|
+
await runWithTiming(flags.time, async () => {
|
|
28166
|
+
try {
|
|
28167
|
+
const challenge = await deriveEmailChallengeFromInbound({
|
|
28168
|
+
baseUrl: auth.apiBaseUrl,
|
|
28169
|
+
emailId: flags.id,
|
|
28170
|
+
apiKey: auth.apiKey,
|
|
28171
|
+
headers: requestConfig.headers
|
|
28172
|
+
});
|
|
28173
|
+
this.log(JSON.stringify(challenge, null, 2));
|
|
28174
|
+
} catch (error) {
|
|
28175
|
+
reportX402Error(error, {
|
|
28176
|
+
auth,
|
|
28177
|
+
baseUrlOverridden,
|
|
28178
|
+
configDir: this.config.configDir
|
|
28179
|
+
});
|
|
28180
|
+
process.exitCode = 1;
|
|
28181
|
+
}
|
|
28182
|
+
});
|
|
28183
|
+
}
|
|
28184
|
+
};
|
|
28185
|
+
//#endregion
|
|
27902
28186
|
//#region src/oclif/commands/payments-charge.ts
|
|
27903
28187
|
var PaymentsChargeCommand = class PaymentsChargeCommand extends Command {
|
|
27904
28188
|
static description = `Request an x402 payment by creating a challenge (payee side).
|
|
@@ -28207,7 +28491,153 @@ var PaymentsPayEmailStepCommand = class PaymentsPayEmailStepCommand extends Comm
|
|
|
28207
28491
|
}
|
|
28208
28492
|
};
|
|
28209
28493
|
//#endregion
|
|
28494
|
+
//#region src/oclif/commands/payments-settlement.ts
|
|
28495
|
+
/** The human-readable notice describing the async-settlement model. */
|
|
28496
|
+
const settlementWaitNotice = "Note: the payment has been SENT, but x402 settlement is asynchronous. --wait only confirms email delivery (SMTP 250), not on-chain settlement. The settle_tx hash arrives in a follow-up x402 settlement interaction email from the payee. Use --wait-settle to poll for it.";
|
|
28497
|
+
/** The steps a payer has already seen / sent, so a matching later step is the receipt. */
|
|
28498
|
+
const NON_RECEIPT_STEPS = new Set(["challenge", "payment"]);
|
|
28499
|
+
/**
|
|
28500
|
+
* Best-effort extraction of a `settle_tx` from a receipt envelope. The exact
|
|
28501
|
+
* receipt wire shape is owned by the platform and not pinned in the SDK, so we
|
|
28502
|
+
* do not assert a rigid schema: we look for a `settle_tx` string at the top
|
|
28503
|
+
* level or one level into `payload` (the two places interaction envelopes carry
|
|
28504
|
+
* step data). Returns null when absent; the caller always prints the full
|
|
28505
|
+
* envelope regardless, so a miss never hides data.
|
|
28506
|
+
*/
|
|
28507
|
+
function extractSettleTx(envelope) {
|
|
28508
|
+
const top = envelope.settle_tx;
|
|
28509
|
+
if (typeof top === "string" && top) return top;
|
|
28510
|
+
const payload = envelope.payload;
|
|
28511
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
28512
|
+
const nested = payload.settle_tx;
|
|
28513
|
+
if (typeof nested === "string" && nested) return nested;
|
|
28514
|
+
}
|
|
28515
|
+
return null;
|
|
28516
|
+
}
|
|
28517
|
+
/**
|
|
28518
|
+
* Parse interaction.json bytes into an envelope object, or null when the bytes
|
|
28519
|
+
* are not a JSON object (so a non-interaction attachment is simply skipped).
|
|
28520
|
+
*/
|
|
28521
|
+
function parseInteractionEnvelope(bytes) {
|
|
28522
|
+
let parsed;
|
|
28523
|
+
try {
|
|
28524
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
28525
|
+
} catch {
|
|
28526
|
+
return null;
|
|
28527
|
+
}
|
|
28528
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
28529
|
+
return parsed;
|
|
28530
|
+
}
|
|
28531
|
+
/**
|
|
28532
|
+
* True when a downloaded interaction.json envelope is the settlement receipt for
|
|
28533
|
+
* the interaction we paid: same `interaction_id`, and a step the payer has not
|
|
28534
|
+
* already produced/received (i.e. not `challenge` / `payment`). Matching on the
|
|
28535
|
+
* confirmed interaction_id keeps this correct without depending on the exact
|
|
28536
|
+
* receipt step name, which the platform owns.
|
|
28537
|
+
*/
|
|
28538
|
+
function isSettlementReceiptFor(envelope, interactionId) {
|
|
28539
|
+
if (envelope.interaction_id !== interactionId) return false;
|
|
28540
|
+
const step = envelope.step;
|
|
28541
|
+
if (typeof step === "string" && NON_RECEIPT_STEPS.has(step)) return false;
|
|
28542
|
+
return true;
|
|
28543
|
+
}
|
|
28544
|
+
/**
|
|
28545
|
+
* Poll the inbox for the x402 settlement interaction email that answers the
|
|
28546
|
+
* payment we just sent, and return it once found. Returns null on timeout.
|
|
28547
|
+
*
|
|
28548
|
+
* Searches inbound mail from the payee (with attachments) received since the
|
|
28549
|
+
* send, downloads each candidate's interaction.json, and matches it by
|
|
28550
|
+
* interaction_id via {@link isSettlementReceiptFor}. Network access is via the
|
|
28551
|
+
* injected api client (search) plus a plain fetch for the attachment archive,
|
|
28552
|
+
* mirroring how the rest of the payments surface reads attachments; `fetchImpl`
|
|
28553
|
+
* is injectable for tests.
|
|
28554
|
+
*/
|
|
28555
|
+
async function pollForSettlementInteraction(params) {
|
|
28556
|
+
const deadline = params.timeoutSeconds === 0 ? null : Date.now() + params.timeoutSeconds * 1e3;
|
|
28557
|
+
const checked = /* @__PURE__ */ new Set();
|
|
28558
|
+
while (deadline === null || Date.now() < deadline) {
|
|
28559
|
+
let cursor = null;
|
|
28560
|
+
let receipt = null;
|
|
28561
|
+
let drained = false;
|
|
28562
|
+
while (!drained) {
|
|
28563
|
+
const page = await fetchEmailSearchPage({
|
|
28564
|
+
apiClient: params.apiClient,
|
|
28565
|
+
cursor,
|
|
28566
|
+
filters: {
|
|
28567
|
+
from: params.payeeFrom,
|
|
28568
|
+
hasAttachment: true
|
|
28569
|
+
},
|
|
28570
|
+
pageSize: 50,
|
|
28571
|
+
since: params.since
|
|
28572
|
+
});
|
|
28573
|
+
if (!page.ok) break;
|
|
28574
|
+
for (const row of page.rows) {
|
|
28575
|
+
if (checked.has(row.id)) continue;
|
|
28576
|
+
let bytes;
|
|
28577
|
+
try {
|
|
28578
|
+
bytes = await fetchInteractionJsonBytes({
|
|
28579
|
+
baseUrl: params.baseUrl,
|
|
28580
|
+
emailId: row.id,
|
|
28581
|
+
apiKey: params.apiKey,
|
|
28582
|
+
headers: params.headers,
|
|
28583
|
+
fetchImpl: params.fetchImpl
|
|
28584
|
+
});
|
|
28585
|
+
} catch {
|
|
28586
|
+
continue;
|
|
28587
|
+
}
|
|
28588
|
+
if (!bytes) continue;
|
|
28589
|
+
const envelope = parseInteractionEnvelope(bytes);
|
|
28590
|
+
if (!envelope) continue;
|
|
28591
|
+
checked.add(row.id);
|
|
28592
|
+
if (isSettlementReceiptFor(envelope, params.interactionId)) {
|
|
28593
|
+
receipt = {
|
|
28594
|
+
emailId: row.id,
|
|
28595
|
+
envelope,
|
|
28596
|
+
settleTx: extractSettleTx(envelope)
|
|
28597
|
+
};
|
|
28598
|
+
break;
|
|
28599
|
+
}
|
|
28600
|
+
}
|
|
28601
|
+
if (receipt) break;
|
|
28602
|
+
const nextCursor = page.cursor;
|
|
28603
|
+
if (!nextCursor || nextCursor === cursor || page.rows.length === 0) drained = true;
|
|
28604
|
+
else cursor = nextCursor;
|
|
28605
|
+
}
|
|
28606
|
+
if (receipt) return receipt;
|
|
28607
|
+
if (deadline !== null && Date.now() >= deadline) break;
|
|
28608
|
+
await sleep$1(params.intervalSeconds * 1e3);
|
|
28609
|
+
}
|
|
28610
|
+
return null;
|
|
28611
|
+
}
|
|
28612
|
+
//#endregion
|
|
28210
28613
|
//#region src/oclif/commands/payments-pay-email.ts
|
|
28614
|
+
/**
|
|
28615
|
+
* True when none of the explicit challenge sources are present (no --challenge,
|
|
28616
|
+
* no --challenge-file, and stdin is an interactive TTY rather than a pipe). In
|
|
28617
|
+
* that case `pay-email` auto-derives the challenge from the inbound email's
|
|
28618
|
+
* interaction.json attachment. We treat a TTY stdin as "no challenge piped" so
|
|
28619
|
+
* the one-shot does not block on readFileSync(0) waiting for the user to paste.
|
|
28620
|
+
*/
|
|
28621
|
+
function shouldDeriveChallenge(flags) {
|
|
28622
|
+
return flags.challenge === void 0 && flags["challenge-file"] === void 0;
|
|
28623
|
+
}
|
|
28624
|
+
/**
|
|
28625
|
+
* Decide the api-key `pay-email` should authenticate its inbox reads with, given
|
|
28626
|
+
* the parsed flag value, whether `--api-key` was passed explicitly on the
|
|
28627
|
+
* command line (vs sourced from `PRIMITIVE_API_KEY`), and whether a logged-in
|
|
28628
|
+
* session exists. Returns the key to use, or `undefined` to fall through to the
|
|
28629
|
+
* stored login. See the block comment above for the precedence rationale.
|
|
28630
|
+
*/
|
|
28631
|
+
function resolvePayerInboxApiKey(params) {
|
|
28632
|
+
if (params.apiKeyFlag !== void 0 && params.apiKeyFromEnvOnly && params.hasStoredLogin) return {
|
|
28633
|
+
apiKey: void 0,
|
|
28634
|
+
usedStoredLoginOverEnvKey: true
|
|
28635
|
+
};
|
|
28636
|
+
return {
|
|
28637
|
+
apiKey: params.apiKeyFlag,
|
|
28638
|
+
usedStoredLoginOverEnvKey: false
|
|
28639
|
+
};
|
|
28640
|
+
}
|
|
28211
28641
|
/** The exact part name + content type the inbound matcher requires. */
|
|
28212
28642
|
const INTERACTION_PART_FILENAME = "interaction.json";
|
|
28213
28643
|
const INTERACTION_PART_CONTENT_TYPE = "application/json";
|
|
@@ -28230,34 +28660,64 @@ function emailDetailFromEnvelope(data) {
|
|
|
28230
28660
|
return data?.data ?? null;
|
|
28231
28661
|
}
|
|
28232
28662
|
var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
28233
|
-
static description = `Pay an email-native x402 challenge in one step: sign it and send the signed interaction.json
|
|
28663
|
+
static description = `Pay an email-native x402 challenge in one step: sign it and send the signed interaction.json.
|
|
28234
28664
|
|
|
28235
|
-
|
|
28236
|
-
\`
|
|
28237
|
-
EIP-3009 authorization locally with your wallet key (read from
|
|
28238
|
-
${PRIVATE_KEY_ENV} by default), and sends the signed \`interaction.json\`
|
|
28239
|
-
attached as an \`application/json\` part. The key never leaves your machine.
|
|
28665
|
+
THE ONE-COMMAND PAYER FLOW. You received a payment-request email carrying an
|
|
28666
|
+
\`interaction.json\` attachment. Run:
|
|
28240
28667
|
|
|
28241
|
-
|
|
28242
|
-
\`pay-email-step\` + \`send --attachment interaction.json\` dance: no manual
|
|
28243
|
-
address or threading wrangling. Use \`pay-email-step\` only when you need the
|
|
28244
|
-
signed bytes as a portable artifact without sending.
|
|
28668
|
+
<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id>
|
|
28245
28669
|
|
|
28246
|
-
|
|
28247
|
-
|
|
28248
|
-
|
|
28249
|
-
|
|
28250
|
-
|
|
28251
|
-
associates by interaction_id instead. Pass --from only to override the derived
|
|
28252
|
-
payer From.
|
|
28670
|
+
with your wallet key in ${PRIVATE_KEY_ENV}, and this command auto-derives the
|
|
28671
|
+
challenge from that inbound email's attachment, signs the interaction-bound
|
|
28672
|
+
EIP-3009 authorization locally, and sends the signed \`interaction.json\` back
|
|
28673
|
+
to the payee. The key never leaves your machine. You do NOT need --challenge or
|
|
28674
|
+
--challenge-file: with just --in-reply-to the challenge is derived for you.
|
|
28253
28675
|
|
|
28254
|
-
|
|
28255
|
-
|
|
28256
|
-
|
|
28676
|
+
THE CHALLENGE OBJECT (only needed for the --challenge / --challenge-file
|
|
28677
|
+
override). \`--challenge\` expects the object the PAYEE's
|
|
28678
|
+
\`create-email-challenge\` API returns, NOT the inbound email's
|
|
28679
|
+
\`interaction.json\` attachment (those are different shapes). Its fields are:
|
|
28680
|
+
|
|
28681
|
+
{
|
|
28682
|
+
"interaction_id": "<uuid@domain>", // the email thread id
|
|
28683
|
+
"challenge": {
|
|
28684
|
+
"payment_requirements": { ... }, // scheme, network, payTo, asset, ...
|
|
28685
|
+
"nonce_binding": {
|
|
28686
|
+
"interaction_id": "<uuid@domain>", // must equal the outer interaction_id
|
|
28687
|
+
"challenge_step_id": "<uuid>", // the challenge step id
|
|
28688
|
+
"challenge_nonce": "<64 hex chars>"
|
|
28689
|
+
},
|
|
28690
|
+
"expires_at": "<ISO-8601>"
|
|
28691
|
+
}
|
|
28692
|
+
}
|
|
28693
|
+
|
|
28694
|
+
As a real payer you usually have only the inbound email's attachment, whose
|
|
28695
|
+
layout differs (top-level \`step\`, \`step_id\`, \`expires_at\`, and a nested
|
|
28696
|
+
\`payload.payment_requirements\` / \`payload.challenge_nonce\`). You do NOT
|
|
28697
|
+
reshape it by hand: omit --challenge and let --in-reply-to derive it, or run
|
|
28698
|
+
\`payments challenge-from-email --id <inbound-id>\` to print the correctly
|
|
28699
|
+
shaped challenge object. Pass --challenge / --challenge-file only when you have
|
|
28700
|
+
the payee-side challenge from another source; it is used as-is and overrides
|
|
28701
|
+
the auto-derive.
|
|
28702
|
+
|
|
28703
|
+
SETTLEMENT IS ASYNC. --wait only confirms the receiving MTA accepted the
|
|
28704
|
+
message (SMTP 250); it does NOT confirm on-chain settlement. The \`settle_tx\`
|
|
28705
|
+
hash arrives later in a follow-up inbound x402 settlement interaction email
|
|
28706
|
+
from the payee. Use --wait-settle to poll for that settlement email after
|
|
28707
|
+
sending and surface the receipt.
|
|
28708
|
+
|
|
28709
|
+
ADDRESSING. --in-reply-to is the inbound challenge email's id. It is fetched to
|
|
28710
|
+
derive the recipient (the payee, from the inbound's sender) and the From (you,
|
|
28711
|
+
the payer, from the inbound's recipient). The outgoing send does NOT thread
|
|
28712
|
+
under the challenge: threading would trigger the send endpoint's parent-thread
|
|
28713
|
+
dedup and the payment would never settle. The interaction associates by
|
|
28714
|
+
interaction_id instead. Pass --from only to override the derived payer From.`;
|
|
28715
|
+
static summary = "Sign an email x402 challenge and send the signed interaction.json (one step; derives the challenge from --in-reply-to)";
|
|
28257
28716
|
static examples = [
|
|
28717
|
+
"<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id>",
|
|
28718
|
+
"<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id> --wait-settle",
|
|
28258
28719
|
"<%= config.bin %> payments pay-email --challenge-file challenge.json --in-reply-to <inbound-email-id>",
|
|
28259
|
-
"
|
|
28260
|
-
"<%= config.bin %> payments pay-email --challenge-file challenge.json --in-reply-to <inbound-email-id> --from 'Payer <payer@your-domain.example>'"
|
|
28720
|
+
"<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id> --from 'Payer <payer@your-domain.example>'"
|
|
28261
28721
|
];
|
|
28262
28722
|
static flags = {
|
|
28263
28723
|
"api-key": Flags.string({
|
|
@@ -28274,27 +28734,46 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28274
28734
|
env: PRIVATE_KEY_ENV
|
|
28275
28735
|
}),
|
|
28276
28736
|
challenge: Flags.string({
|
|
28277
|
-
description: "
|
|
28737
|
+
description: "OVERRIDE: the payee-side email challenge object (the create-email-challenge response shape, NOT the inbound interaction.json attachment) as a JSON string. Optional: omit it and the challenge is auto-derived from the --in-reply-to email's attachment.",
|
|
28278
28738
|
exclusive: ["challenge-file"]
|
|
28279
28739
|
}),
|
|
28280
28740
|
"challenge-file": Flags.string({
|
|
28281
|
-
description: "
|
|
28741
|
+
description: "OVERRIDE: path to a file containing the payee-side email challenge JSON (the create-email-challenge response shape). Optional: omit it and the challenge is auto-derived from the --in-reply-to email's attachment.",
|
|
28282
28742
|
exclusive: ["challenge"]
|
|
28283
28743
|
}),
|
|
28284
28744
|
"in-reply-to": Flags.string({
|
|
28285
|
-
description: "Id of the inbound challenge email you received. It is fetched to derive the payee recipient and the payer From. The outgoing send is not threaded under the challenge; the payment associates by interaction_id.",
|
|
28745
|
+
description: "Id of the inbound challenge email you received. With no --challenge / --challenge-file, its interaction.json attachment is downloaded and reshaped into the challenge to sign. It is also fetched to derive the payee recipient and the payer From. The outgoing send is not threaded under the challenge; the payment associates by interaction_id.",
|
|
28286
28746
|
required: true
|
|
28287
28747
|
}),
|
|
28288
28748
|
from: Flags.string({ description: "Optional From header override. Defaults to the inbound's recipient (the payer the challenge was addressed to)." }),
|
|
28289
28749
|
body: Flags.string({ description: `Plain-text body. The signed authorization travels in the interaction.json attachment; this is the human-readable accompanying note. Defaults to "${DEFAULT_BODY_TEXT}".` }),
|
|
28290
|
-
wait: Flags.boolean({ description: "Block until the receiving MTA returns
|
|
28750
|
+
wait: Flags.boolean({ description: "Block until the receiving MTA returns a delivery outcome (SMTP 250). This confirms email DELIVERY only, NOT on-chain settlement. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
|
|
28751
|
+
"wait-settle": Flags.boolean({ description: "After sending, poll the inbox for the follow-up x402 settlement interaction email from the payee and print the settlement receipt (including settle_tx when present). Settlement is async, so this can take a while; tune with --settle-timeout / --settle-interval." }),
|
|
28752
|
+
"settle-timeout": Flags.integer({
|
|
28753
|
+
default: 180,
|
|
28754
|
+
description: "With --wait-settle: seconds to wait for the settlement email before giving up (0 waits forever).",
|
|
28755
|
+
min: 0
|
|
28756
|
+
}),
|
|
28757
|
+
"settle-interval": Flags.integer({
|
|
28758
|
+
default: 5,
|
|
28759
|
+
description: "With --wait-settle: seconds between settlement-email polls.",
|
|
28760
|
+
min: 1
|
|
28761
|
+
}),
|
|
28291
28762
|
json: Flags.boolean({ description: "Print a JSON object with the signed interaction step and the send result." }),
|
|
28292
28763
|
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
28293
28764
|
};
|
|
28294
28765
|
async run() {
|
|
28295
|
-
const { flags } = await this.parse(PaymentsPayEmailCommand);
|
|
28296
|
-
const
|
|
28297
|
-
|
|
28766
|
+
const { flags, raw } = await this.parse(PaymentsPayEmailCommand);
|
|
28767
|
+
const apiKeyFlagExplicit = raw.some((token) => token.type === "flag" && token.flag === "api-key");
|
|
28768
|
+
const apiKeyFromEnvOnly = flags["api-key"] !== void 0 && !apiKeyFlagExplicit;
|
|
28769
|
+
const { apiKey: effectiveApiKey, usedStoredLoginOverEnvKey } = resolvePayerInboxApiKey({
|
|
28770
|
+
apiKeyFlag: flags["api-key"],
|
|
28771
|
+
apiKeyFromEnvOnly,
|
|
28772
|
+
hasStoredLogin: hasStoredCliLogin(this.config.configDir)
|
|
28773
|
+
});
|
|
28774
|
+
if (usedStoredLoginOverEnvKey) process.stderr.write("PRIMITIVE_API_KEY is set, but pay-email reads the inbound challenge from your logged-in account's inbox, so it is using your saved login for this command. Pass --api-key explicitly to override.\n");
|
|
28775
|
+
const { apiClient, auth, baseUrlOverridden, requestConfig } = await createAuthenticatedCliApiClient({
|
|
28776
|
+
apiKey: effectiveApiKey,
|
|
28298
28777
|
apiBaseUrl: flags["api-base-url"],
|
|
28299
28778
|
configDir: this.config.configDir
|
|
28300
28779
|
});
|
|
@@ -28304,22 +28783,6 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28304
28783
|
configDir: this.config.configDir
|
|
28305
28784
|
};
|
|
28306
28785
|
await runWithTiming(flags.time, async () => {
|
|
28307
|
-
let built;
|
|
28308
|
-
try {
|
|
28309
|
-
built = await signEmailChallenge({
|
|
28310
|
-
challenge: readEmailChallenge({
|
|
28311
|
-
inline: flags.challenge,
|
|
28312
|
-
file: flags["challenge-file"]
|
|
28313
|
-
}),
|
|
28314
|
-
privateKey: flags["private-key"] ?? "",
|
|
28315
|
-
resolvedApiBaseUrl: auth.apiBaseUrl,
|
|
28316
|
-
apiKey: flags["api-key"]
|
|
28317
|
-
});
|
|
28318
|
-
} catch (error) {
|
|
28319
|
-
reportX402Error(error, authFailureContext);
|
|
28320
|
-
process.exitCode = 1;
|
|
28321
|
-
return;
|
|
28322
|
-
}
|
|
28323
28786
|
const inboundResult = await getEmail({
|
|
28324
28787
|
client: apiClient.client,
|
|
28325
28788
|
path: { id: flags["in-reply-to"] },
|
|
@@ -28332,6 +28795,7 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28332
28795
|
...authFailureContext,
|
|
28333
28796
|
payload: errorPayload
|
|
28334
28797
|
});
|
|
28798
|
+
if (extractErrorCode(errorPayload) === API_ERROR_CODES.notFound && auth.source === "flag-or-env") process.stderr.write("The inbound challenge email was not found for the org this API key belongs to. pay-email reads the challenge from the PAYER account's inbox: unset PRIMITIVE_API_KEY (or pass --api-key for the payer's org, or run `primitive signin` as the payer) and retry.\n");
|
|
28335
28799
|
process.exitCode = 1;
|
|
28336
28800
|
return;
|
|
28337
28801
|
}
|
|
@@ -28341,6 +28805,31 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28341
28805
|
process.exitCode = 1;
|
|
28342
28806
|
return;
|
|
28343
28807
|
}
|
|
28808
|
+
let built;
|
|
28809
|
+
try {
|
|
28810
|
+
let challenge;
|
|
28811
|
+
if (shouldDeriveChallenge(flags)) challenge = await deriveEmailChallengeFromInbound({
|
|
28812
|
+
baseUrl: auth.apiBaseUrl,
|
|
28813
|
+
emailId: flags["in-reply-to"],
|
|
28814
|
+
apiKey: auth.apiKey,
|
|
28815
|
+
headers: requestConfig.headers,
|
|
28816
|
+
attachments: inbound.parsed?.attachments
|
|
28817
|
+
});
|
|
28818
|
+
else challenge = readEmailChallenge({
|
|
28819
|
+
inline: flags.challenge,
|
|
28820
|
+
file: flags["challenge-file"]
|
|
28821
|
+
});
|
|
28822
|
+
built = await signEmailChallenge({
|
|
28823
|
+
challenge,
|
|
28824
|
+
privateKey: flags["private-key"] ?? "",
|
|
28825
|
+
resolvedApiBaseUrl: auth.apiBaseUrl,
|
|
28826
|
+
apiKey: effectiveApiKey
|
|
28827
|
+
});
|
|
28828
|
+
} catch (error) {
|
|
28829
|
+
reportX402Error(error, authFailureContext);
|
|
28830
|
+
process.exitCode = 1;
|
|
28831
|
+
return;
|
|
28832
|
+
}
|
|
28344
28833
|
const payeeTo = inbound.from_email;
|
|
28345
28834
|
const derivedPayerFrom = inbound.to_email || inbound.recipient;
|
|
28346
28835
|
const from = flags.from ?? derivedPayerFrom;
|
|
@@ -28354,6 +28843,7 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28354
28843
|
process.exitCode = 1;
|
|
28355
28844
|
return;
|
|
28356
28845
|
}
|
|
28846
|
+
const sendStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28357
28847
|
const result = await sendEmail({
|
|
28358
28848
|
body: {
|
|
28359
28849
|
from,
|
|
@@ -28381,13 +28871,40 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28381
28871
|
if (replayed) writeIdempotentReplayBannerIfReplay(sent, { write: (chunk) => {
|
|
28382
28872
|
process.stderr.write(chunk);
|
|
28383
28873
|
} });
|
|
28874
|
+
let settlement = null;
|
|
28875
|
+
if (flags["wait-settle"] && !replayed && payeeTo) {
|
|
28876
|
+
process.stderr.write("Payment sent. Waiting for the x402 settlement interaction email (settlement is async)...\n");
|
|
28877
|
+
settlement = await pollForSettlementInteraction({
|
|
28878
|
+
apiClient,
|
|
28879
|
+
baseUrl: auth.apiBaseUrl,
|
|
28880
|
+
apiKey: auth.apiKey,
|
|
28881
|
+
headers: requestConfig.headers,
|
|
28882
|
+
interactionId: built.envelope.interaction_id,
|
|
28883
|
+
payeeFrom: payeeTo,
|
|
28884
|
+
since: sendStartedAt,
|
|
28885
|
+
timeoutSeconds: flags["settle-timeout"],
|
|
28886
|
+
intervalSeconds: flags["settle-interval"]
|
|
28887
|
+
});
|
|
28888
|
+
if (settlement) process.stderr.write(settlement.settleTx ? `Settled. settle_tx: ${settlement.settleTx}\n` : "Settlement interaction received (no settle_tx field present; full receipt below).\n");
|
|
28889
|
+
else process.stderr.write("Timed out waiting for the x402 settlement interaction email. The payment was sent; the settle_tx will arrive in a follow-up x402 settlement interaction email from the payee. Re-run with --wait-settle, or check your inbox for an x402 settlement message.\n");
|
|
28890
|
+
}
|
|
28384
28891
|
if (flags.json) this.log(JSON.stringify({
|
|
28385
28892
|
interaction: built.envelope,
|
|
28386
28893
|
sent,
|
|
28387
|
-
idempotent_replay: replayed
|
|
28894
|
+
idempotent_replay: replayed,
|
|
28895
|
+
...flags["wait-settle"] ? { settlement: settlement ? {
|
|
28896
|
+
email_id: settlement.emailId,
|
|
28897
|
+
settle_tx: settlement.settleTx,
|
|
28898
|
+
receipt: settlement.envelope
|
|
28899
|
+
} : null } : {}
|
|
28388
28900
|
}, null, 2));
|
|
28389
|
-
else
|
|
28901
|
+
else {
|
|
28902
|
+
this.log(JSON.stringify(sent, null, 2));
|
|
28903
|
+
if (!flags["wait-settle"]) process.stderr.write(`${settlementWaitNotice}\n`);
|
|
28904
|
+
else if (settlement) this.log(JSON.stringify(settlement.envelope, null, 2));
|
|
28905
|
+
}
|
|
28390
28906
|
if (replayed) process.exitCode = 1;
|
|
28907
|
+
else if (flags["wait-settle"] && !settlement) process.exitCode = 1;
|
|
28391
28908
|
});
|
|
28392
28909
|
}
|
|
28393
28910
|
};
|
|
@@ -28674,6 +29191,434 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
28674
29191
|
}
|
|
28675
29192
|
};
|
|
28676
29193
|
//#endregion
|
|
29194
|
+
//#region src/oclif/commands/routes-add.ts
|
|
29195
|
+
function resolveCreateTarget(flags) {
|
|
29196
|
+
if (flags.function) return { function_id: flags.function };
|
|
29197
|
+
if (flags.endpoint) return { endpoint_id: flags.endpoint };
|
|
29198
|
+
return { error: "Provide exactly one of --function (route to a function) or --endpoint (an existing endpoint)." };
|
|
29199
|
+
}
|
|
29200
|
+
var RoutesAddCommand = class RoutesAddCommand extends Command {
|
|
29201
|
+
static description = `Create a recipient route binding an address pattern to a destination.
|
|
29202
|
+
|
|
29203
|
+
Provide exactly one of --function (route to a function; its route-target
|
|
29204
|
+
endpoint is created in the same transaction) or --endpoint (an existing
|
|
29205
|
+
endpoint). The match type defaults to exact; use --match wildcard for
|
|
29206
|
+
patterns like 'support+*@acme.com'.`;
|
|
29207
|
+
static summary = "Add a recipient route";
|
|
29208
|
+
static examples = [
|
|
29209
|
+
"<%= config.bin %> routes add alice@acme.com --function <fn-id>",
|
|
29210
|
+
"<%= config.bin %> routes add 'support+*@acme.com' --match wildcard --function <fn-id>",
|
|
29211
|
+
"<%= config.bin %> routes add billing@acme.com --endpoint <endpoint-id> --priority 10"
|
|
29212
|
+
];
|
|
29213
|
+
static args = { pattern: Args.string({
|
|
29214
|
+
description: "Recipient address or wildcard pattern (e.g. alice@acme.com).",
|
|
29215
|
+
required: true
|
|
29216
|
+
}) };
|
|
29217
|
+
static flags = {
|
|
29218
|
+
"api-key": Flags.string({
|
|
29219
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
29220
|
+
env: "PRIMITIVE_API_KEY"
|
|
29221
|
+
}),
|
|
29222
|
+
"api-base-url": Flags.string({
|
|
29223
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
29224
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
29225
|
+
hidden: true
|
|
29226
|
+
}),
|
|
29227
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION }),
|
|
29228
|
+
function: Flags.string({
|
|
29229
|
+
description: "Function id to route this address to (its route-target endpoint is minted if needed). Mutually exclusive with --endpoint.",
|
|
29230
|
+
exclusive: ["endpoint"]
|
|
29231
|
+
}),
|
|
29232
|
+
endpoint: Flags.string({
|
|
29233
|
+
description: "Existing endpoint id to route to. Mutually exclusive with --function.",
|
|
29234
|
+
exclusive: ["function"]
|
|
29235
|
+
}),
|
|
29236
|
+
match: Flags.string({
|
|
29237
|
+
description: "Match type for the pattern. regex requires the Power plan and may be rejected if not enabled.",
|
|
29238
|
+
options: [
|
|
29239
|
+
"exact",
|
|
29240
|
+
"wildcard",
|
|
29241
|
+
"regex"
|
|
29242
|
+
],
|
|
29243
|
+
default: "exact"
|
|
29244
|
+
}),
|
|
29245
|
+
domain: Flags.string({ description: "Scope the route to this domain id (defaults to the pattern's domain)." }),
|
|
29246
|
+
priority: Flags.integer({ description: "Evaluation order within a scope; lower is checked first." }),
|
|
29247
|
+
disabled: Flags.boolean({ description: "Create the route disabled." })
|
|
29248
|
+
};
|
|
29249
|
+
async run() {
|
|
29250
|
+
const { args, flags } = await this.parse(RoutesAddCommand);
|
|
29251
|
+
const target = resolveCreateTarget(flags);
|
|
29252
|
+
if ("error" in target) {
|
|
29253
|
+
process.stderr.write(`${target.error}\n`);
|
|
29254
|
+
process.exitCode = 1;
|
|
29255
|
+
return;
|
|
29256
|
+
}
|
|
29257
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
29258
|
+
apiKey: flags["api-key"],
|
|
29259
|
+
apiBaseUrl: flags["api-base-url"],
|
|
29260
|
+
configDir: this.config.configDir
|
|
29261
|
+
});
|
|
29262
|
+
await runWithTiming(flags.time, async () => {
|
|
29263
|
+
const result = await createRoute({
|
|
29264
|
+
client: apiClient.client,
|
|
29265
|
+
body: {
|
|
29266
|
+
match_type: flags.match,
|
|
29267
|
+
pattern: args.pattern,
|
|
29268
|
+
...target,
|
|
29269
|
+
...flags.domain ? { domain_id: flags.domain } : {},
|
|
29270
|
+
...flags.priority != null ? { priority: flags.priority } : {},
|
|
29271
|
+
...flags.disabled ? { enabled: false } : {}
|
|
29272
|
+
},
|
|
29273
|
+
responseStyle: "fields"
|
|
29274
|
+
});
|
|
29275
|
+
if (result.error) {
|
|
29276
|
+
const payload = extractErrorPayload(result.error);
|
|
29277
|
+
writeErrorWithHints(payload);
|
|
29278
|
+
surfaceUnauthorizedHint({
|
|
29279
|
+
auth,
|
|
29280
|
+
baseUrlOverridden,
|
|
29281
|
+
configDir: this.config.configDir,
|
|
29282
|
+
payload
|
|
29283
|
+
});
|
|
29284
|
+
process.exitCode = 1;
|
|
29285
|
+
return;
|
|
29286
|
+
}
|
|
29287
|
+
this.log(JSON.stringify(result.data.data, null, 2));
|
|
29288
|
+
});
|
|
29289
|
+
}
|
|
29290
|
+
};
|
|
29291
|
+
//#endregion
|
|
29292
|
+
//#region src/oclif/commands/routes-list.ts
|
|
29293
|
+
var RoutesListCommand = class RoutesListCommand extends Command {
|
|
29294
|
+
static description = "List the org's recipient routes in evaluation order.";
|
|
29295
|
+
static summary = "List recipient routes";
|
|
29296
|
+
static examples = ["<%= config.bin %> routes list"];
|
|
29297
|
+
static flags = {
|
|
29298
|
+
"api-key": Flags.string({
|
|
29299
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
29300
|
+
env: "PRIMITIVE_API_KEY"
|
|
29301
|
+
}),
|
|
29302
|
+
"api-base-url": Flags.string({
|
|
29303
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
29304
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
29305
|
+
hidden: true
|
|
29306
|
+
}),
|
|
29307
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
29308
|
+
};
|
|
29309
|
+
async run() {
|
|
29310
|
+
const { flags } = await this.parse(RoutesListCommand);
|
|
29311
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
29312
|
+
apiKey: flags["api-key"],
|
|
29313
|
+
apiBaseUrl: flags["api-base-url"],
|
|
29314
|
+
configDir: this.config.configDir
|
|
29315
|
+
});
|
|
29316
|
+
await runWithTiming(flags.time, async () => {
|
|
29317
|
+
const result = await listRoutes({
|
|
29318
|
+
client: apiClient.client,
|
|
29319
|
+
responseStyle: "fields"
|
|
29320
|
+
});
|
|
29321
|
+
if (result.error) {
|
|
29322
|
+
const payload = extractErrorPayload(result.error);
|
|
29323
|
+
writeErrorWithHints(payload);
|
|
29324
|
+
surfaceUnauthorizedHint({
|
|
29325
|
+
auth,
|
|
29326
|
+
baseUrlOverridden,
|
|
29327
|
+
configDir: this.config.configDir,
|
|
29328
|
+
payload
|
|
29329
|
+
});
|
|
29330
|
+
process.exitCode = 1;
|
|
29331
|
+
return;
|
|
29332
|
+
}
|
|
29333
|
+
this.log(JSON.stringify(result.data.data, null, 2));
|
|
29334
|
+
});
|
|
29335
|
+
}
|
|
29336
|
+
};
|
|
29337
|
+
//#endregion
|
|
29338
|
+
//#region src/oclif/commands/routes-remove.ts
|
|
29339
|
+
var RoutesRemoveCommand = class RoutesRemoveCommand extends Command {
|
|
29340
|
+
static description = "Delete a recipient route by id. Mail matching its pattern then falls back to the domain or organization default.";
|
|
29341
|
+
static summary = "Remove a recipient route";
|
|
29342
|
+
static examples = ["<%= config.bin %> routes remove <route-id>"];
|
|
29343
|
+
static args = { id: Args.string({
|
|
29344
|
+
description: "Route id (UUID) to delete.",
|
|
29345
|
+
required: true
|
|
29346
|
+
}) };
|
|
29347
|
+
static flags = {
|
|
29348
|
+
"api-key": Flags.string({
|
|
29349
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
29350
|
+
env: "PRIMITIVE_API_KEY"
|
|
29351
|
+
}),
|
|
29352
|
+
"api-base-url": Flags.string({
|
|
29353
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
29354
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
29355
|
+
hidden: true
|
|
29356
|
+
}),
|
|
29357
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
29358
|
+
};
|
|
29359
|
+
async run() {
|
|
29360
|
+
const { args, flags } = await this.parse(RoutesRemoveCommand);
|
|
29361
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
29362
|
+
apiKey: flags["api-key"],
|
|
29363
|
+
apiBaseUrl: flags["api-base-url"],
|
|
29364
|
+
configDir: this.config.configDir
|
|
29365
|
+
});
|
|
29366
|
+
await runWithTiming(flags.time, async () => {
|
|
29367
|
+
const result = await deleteRoute({
|
|
29368
|
+
client: apiClient.client,
|
|
29369
|
+
path: { id: args.id },
|
|
29370
|
+
responseStyle: "fields"
|
|
29371
|
+
});
|
|
29372
|
+
if (result.error) {
|
|
29373
|
+
const payload = extractErrorPayload(result.error);
|
|
29374
|
+
writeErrorWithHints(payload);
|
|
29375
|
+
surfaceUnauthorizedHint({
|
|
29376
|
+
auth,
|
|
29377
|
+
baseUrlOverridden,
|
|
29378
|
+
configDir: this.config.configDir,
|
|
29379
|
+
payload
|
|
29380
|
+
});
|
|
29381
|
+
process.exitCode = 1;
|
|
29382
|
+
return;
|
|
29383
|
+
}
|
|
29384
|
+
this.log(JSON.stringify(result.data.data, null, 2));
|
|
29385
|
+
});
|
|
29386
|
+
}
|
|
29387
|
+
};
|
|
29388
|
+
//#endregion
|
|
29389
|
+
//#region src/oclif/commands/routes-reorder.ts
|
|
29390
|
+
function parseReorderUpdates(set) {
|
|
29391
|
+
const updates = [];
|
|
29392
|
+
const seen = /* @__PURE__ */ new Set();
|
|
29393
|
+
for (const pair of set) {
|
|
29394
|
+
const eq = pair.lastIndexOf("=");
|
|
29395
|
+
const id = (eq >= 0 ? pair.slice(0, eq).trim() : "").toLowerCase();
|
|
29396
|
+
const priority = eq >= 0 ? Number(pair.slice(eq + 1).trim()) : NaN;
|
|
29397
|
+
if (!id || !Number.isInteger(priority) || priority < 0) return { error: `Invalid --set value "${pair}"; expected <route-id>=<priority> with a non-negative integer priority.` };
|
|
29398
|
+
if (seen.has(id)) return { error: `Route ${id} appears more than once in --set; specify each route at most once.` };
|
|
29399
|
+
seen.add(id);
|
|
29400
|
+
updates.push({
|
|
29401
|
+
id,
|
|
29402
|
+
priority
|
|
29403
|
+
});
|
|
29404
|
+
}
|
|
29405
|
+
return { updates };
|
|
29406
|
+
}
|
|
29407
|
+
var RoutesReorderCommand = class RoutesReorderCommand extends Command {
|
|
29408
|
+
static description = `Set the priority of one or more recipient routes in a single call.
|
|
29409
|
+
|
|
29410
|
+
Priority controls evaluation order within a scope; lower is checked first.
|
|
29411
|
+
Pass --set <route-id>=<priority> once per route you want to move.`;
|
|
29412
|
+
static summary = "Reorder recipient routes";
|
|
29413
|
+
static examples = ["<%= config.bin %> routes reorder --set <route-id>=10", "<%= config.bin %> routes reorder --set <id-a>=10 --set <id-b>=20"];
|
|
29414
|
+
static flags = {
|
|
29415
|
+
"api-key": Flags.string({
|
|
29416
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
29417
|
+
env: "PRIMITIVE_API_KEY"
|
|
29418
|
+
}),
|
|
29419
|
+
"api-base-url": Flags.string({
|
|
29420
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
29421
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
29422
|
+
hidden: true
|
|
29423
|
+
}),
|
|
29424
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION }),
|
|
29425
|
+
set: Flags.string({
|
|
29426
|
+
description: "A route and its new priority as <route-id>=<priority>. Repeatable.",
|
|
29427
|
+
multiple: true,
|
|
29428
|
+
required: true
|
|
29429
|
+
})
|
|
29430
|
+
};
|
|
29431
|
+
async run() {
|
|
29432
|
+
const { flags } = await this.parse(RoutesReorderCommand);
|
|
29433
|
+
const parsed = parseReorderUpdates(flags.set);
|
|
29434
|
+
if ("error" in parsed) {
|
|
29435
|
+
process.stderr.write(`${parsed.error}\n`);
|
|
29436
|
+
process.exitCode = 1;
|
|
29437
|
+
return;
|
|
29438
|
+
}
|
|
29439
|
+
const { updates } = parsed;
|
|
29440
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
29441
|
+
apiKey: flags["api-key"],
|
|
29442
|
+
apiBaseUrl: flags["api-base-url"],
|
|
29443
|
+
configDir: this.config.configDir
|
|
29444
|
+
});
|
|
29445
|
+
await runWithTiming(flags.time, async () => {
|
|
29446
|
+
const result = await reorderRoutes({
|
|
29447
|
+
client: apiClient.client,
|
|
29448
|
+
body: { updates },
|
|
29449
|
+
responseStyle: "fields"
|
|
29450
|
+
});
|
|
29451
|
+
if (result.error) {
|
|
29452
|
+
const payload = extractErrorPayload(result.error);
|
|
29453
|
+
writeErrorWithHints(payload);
|
|
29454
|
+
surfaceUnauthorizedHint({
|
|
29455
|
+
auth,
|
|
29456
|
+
baseUrlOverridden,
|
|
29457
|
+
configDir: this.config.configDir,
|
|
29458
|
+
payload
|
|
29459
|
+
});
|
|
29460
|
+
process.exitCode = 1;
|
|
29461
|
+
return;
|
|
29462
|
+
}
|
|
29463
|
+
this.log(JSON.stringify(result.data.data, null, 2));
|
|
29464
|
+
});
|
|
29465
|
+
}
|
|
29466
|
+
};
|
|
29467
|
+
//#endregion
|
|
29468
|
+
//#region src/oclif/commands/routes-test.ts
|
|
29469
|
+
var RoutesTestCommand = class RoutesTestCommand extends Command {
|
|
29470
|
+
static description = `Simulate routing for a recipient address.
|
|
29471
|
+
|
|
29472
|
+
Shows the resolved destination and a trace of every rule evaluated (which
|
|
29473
|
+
matched or missed, and why). Does not create or change anything.`;
|
|
29474
|
+
static summary = "Simulate routing for a recipient";
|
|
29475
|
+
static examples = ["<%= config.bin %> routes test alice@acme.com", "<%= config.bin %> routes test bounce@acme.com --event-type email.bounced"];
|
|
29476
|
+
static args = { recipient: Args.string({
|
|
29477
|
+
description: "Recipient address to simulate (e.g. alice@acme.com).",
|
|
29478
|
+
required: true
|
|
29479
|
+
}) };
|
|
29480
|
+
static flags = {
|
|
29481
|
+
"api-key": Flags.string({
|
|
29482
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
29483
|
+
env: "PRIMITIVE_API_KEY"
|
|
29484
|
+
}),
|
|
29485
|
+
"api-base-url": Flags.string({
|
|
29486
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
29487
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
29488
|
+
hidden: true
|
|
29489
|
+
}),
|
|
29490
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION }),
|
|
29491
|
+
"event-type": Flags.string({ description: "Event type to model (defaults to email.received)." })
|
|
29492
|
+
};
|
|
29493
|
+
async run() {
|
|
29494
|
+
const { args, flags } = await this.parse(RoutesTestCommand);
|
|
29495
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
29496
|
+
apiKey: flags["api-key"],
|
|
29497
|
+
apiBaseUrl: flags["api-base-url"],
|
|
29498
|
+
configDir: this.config.configDir
|
|
29499
|
+
});
|
|
29500
|
+
await runWithTiming(flags.time, async () => {
|
|
29501
|
+
const result = await simulateRoute({
|
|
29502
|
+
client: apiClient.client,
|
|
29503
|
+
body: {
|
|
29504
|
+
recipient: args.recipient,
|
|
29505
|
+
...flags["event-type"] ? { event_type: flags["event-type"] } : {}
|
|
29506
|
+
},
|
|
29507
|
+
responseStyle: "fields"
|
|
29508
|
+
});
|
|
29509
|
+
if (result.error) {
|
|
29510
|
+
const payload = extractErrorPayload(result.error);
|
|
29511
|
+
writeErrorWithHints(payload);
|
|
29512
|
+
surfaceUnauthorizedHint({
|
|
29513
|
+
auth,
|
|
29514
|
+
baseUrlOverridden,
|
|
29515
|
+
configDir: this.config.configDir,
|
|
29516
|
+
payload
|
|
29517
|
+
});
|
|
29518
|
+
process.exitCode = 1;
|
|
29519
|
+
return;
|
|
29520
|
+
}
|
|
29521
|
+
this.log(JSON.stringify(result.data.data, null, 2));
|
|
29522
|
+
});
|
|
29523
|
+
}
|
|
29524
|
+
};
|
|
29525
|
+
//#endregion
|
|
29526
|
+
//#region src/oclif/commands/routes-update.ts
|
|
29527
|
+
function buildUpdateBody(flags) {
|
|
29528
|
+
return {
|
|
29529
|
+
...flags.match ? { match_type: flags.match } : {},
|
|
29530
|
+
...flags.pattern ? { pattern: flags.pattern } : {},
|
|
29531
|
+
...flags.endpoint ? { endpoint_id: flags.endpoint } : {},
|
|
29532
|
+
...flags.domain ? { domain_id: flags.domain } : {},
|
|
29533
|
+
...flags.priority != null ? { priority: flags.priority } : {},
|
|
29534
|
+
...flags.enable ? { enabled: true } : {},
|
|
29535
|
+
...flags.disable ? { enabled: false } : {}
|
|
29536
|
+
};
|
|
29537
|
+
}
|
|
29538
|
+
var RoutesUpdateCommand = class RoutesUpdateCommand extends Command {
|
|
29539
|
+
static description = `Update an existing recipient route.
|
|
29540
|
+
|
|
29541
|
+
Change the pattern, match type, target endpoint, domain scope, priority, or
|
|
29542
|
+
enabled state. Provide at least one field; anything omitted is left as-is.`;
|
|
29543
|
+
static summary = "Update a recipient route";
|
|
29544
|
+
static examples = [
|
|
29545
|
+
"<%= config.bin %> routes update <route-id> --priority 5",
|
|
29546
|
+
"<%= config.bin %> routes update <route-id> --pattern alice@acme.com",
|
|
29547
|
+
"<%= config.bin %> routes update <route-id> --disable"
|
|
29548
|
+
];
|
|
29549
|
+
static args = { id: Args.string({
|
|
29550
|
+
description: "Route id (UUID) to update.",
|
|
29551
|
+
required: true
|
|
29552
|
+
}) };
|
|
29553
|
+
static flags = {
|
|
29554
|
+
"api-key": Flags.string({
|
|
29555
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
29556
|
+
env: "PRIMITIVE_API_KEY"
|
|
29557
|
+
}),
|
|
29558
|
+
"api-base-url": Flags.string({
|
|
29559
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
29560
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
29561
|
+
hidden: true
|
|
29562
|
+
}),
|
|
29563
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION }),
|
|
29564
|
+
match: Flags.string({
|
|
29565
|
+
description: "New match type for the pattern.",
|
|
29566
|
+
options: [
|
|
29567
|
+
"exact",
|
|
29568
|
+
"wildcard",
|
|
29569
|
+
"regex"
|
|
29570
|
+
]
|
|
29571
|
+
}),
|
|
29572
|
+
pattern: Flags.string({ description: "New recipient address or wildcard pattern." }),
|
|
29573
|
+
endpoint: Flags.string({ description: "New target endpoint id (UUID)." }),
|
|
29574
|
+
domain: Flags.string({ description: "New domain scope id (UUID)." }),
|
|
29575
|
+
priority: Flags.integer({ description: "New evaluation priority within a scope; lower is checked first." }),
|
|
29576
|
+
enable: Flags.boolean({
|
|
29577
|
+
description: "Enable the route.",
|
|
29578
|
+
exclusive: ["disable"]
|
|
29579
|
+
}),
|
|
29580
|
+
disable: Flags.boolean({
|
|
29581
|
+
description: "Disable the route.",
|
|
29582
|
+
exclusive: ["enable"]
|
|
29583
|
+
})
|
|
29584
|
+
};
|
|
29585
|
+
async run() {
|
|
29586
|
+
const { args, flags } = await this.parse(RoutesUpdateCommand);
|
|
29587
|
+
const body = buildUpdateBody(flags);
|
|
29588
|
+
if (Object.keys(body).length === 0) {
|
|
29589
|
+
process.stderr.write("Provide at least one field to update (--match, --pattern, --endpoint, --domain, --priority, --enable/--disable).\n");
|
|
29590
|
+
process.exitCode = 1;
|
|
29591
|
+
return;
|
|
29592
|
+
}
|
|
29593
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
29594
|
+
apiKey: flags["api-key"],
|
|
29595
|
+
apiBaseUrl: flags["api-base-url"],
|
|
29596
|
+
configDir: this.config.configDir
|
|
29597
|
+
});
|
|
29598
|
+
await runWithTiming(flags.time, async () => {
|
|
29599
|
+
const result = await updateRoute({
|
|
29600
|
+
client: apiClient.client,
|
|
29601
|
+
path: { id: args.id },
|
|
29602
|
+
body,
|
|
29603
|
+
responseStyle: "fields"
|
|
29604
|
+
});
|
|
29605
|
+
if (result.error) {
|
|
29606
|
+
const payload = extractErrorPayload(result.error);
|
|
29607
|
+
writeErrorWithHints(payload);
|
|
29608
|
+
surfaceUnauthorizedHint({
|
|
29609
|
+
auth,
|
|
29610
|
+
baseUrlOverridden,
|
|
29611
|
+
configDir: this.config.configDir,
|
|
29612
|
+
payload
|
|
29613
|
+
});
|
|
29614
|
+
process.exitCode = 1;
|
|
29615
|
+
return;
|
|
29616
|
+
}
|
|
29617
|
+
this.log(JSON.stringify(result.data.data, null, 2));
|
|
29618
|
+
});
|
|
29619
|
+
}
|
|
29620
|
+
};
|
|
29621
|
+
//#endregion
|
|
28677
29622
|
//#region src/oclif/commands/semantic-search.ts
|
|
28678
29623
|
const DEFAULT_LIMIT$1 = 10;
|
|
28679
29624
|
const MAX_LIMIT$1 = 100;
|
|
@@ -29992,6 +30937,12 @@ const COMMANDS = {
|
|
|
29992
30937
|
"functions:route-unset": FunctionsRouteUnsetCommand,
|
|
29993
30938
|
"functions:route-get": FunctionsRouteGetCommand,
|
|
29994
30939
|
"functions:routing-topology": FunctionsRoutingTopologyCommand,
|
|
30940
|
+
"routes:add": RoutesAddCommand,
|
|
30941
|
+
"routes:list": RoutesListCommand,
|
|
30942
|
+
"routes:test": RoutesTestCommand,
|
|
30943
|
+
"routes:update": RoutesUpdateCommand,
|
|
30944
|
+
"routes:reorder": RoutesReorderCommand,
|
|
30945
|
+
"routes:remove": RoutesRemoveCommand,
|
|
29995
30946
|
...Object.fromEntries(Object.entries(CANONICAL_OPERATION_ALIASES).map(([alias, target]) => {
|
|
29996
30947
|
const command = generatedCommands[target];
|
|
29997
30948
|
if (!command) throw new Error(`Missing generated command target for alias ${alias}`);
|
|
@@ -30005,7 +30956,8 @@ const COMMANDS = {
|
|
|
30005
30956
|
"payments:pay-challenge": PaymentsPayCommand,
|
|
30006
30957
|
"payments:pay": PaymentsPayCommand,
|
|
30007
30958
|
"payments:pay-email": PaymentsPayEmailCommand,
|
|
30008
|
-
"payments:pay-email-step": PaymentsPayEmailStepCommand
|
|
30959
|
+
"payments:pay-email-step": PaymentsPayEmailStepCommand,
|
|
30960
|
+
"payments:challenge-from-email": PaymentsChallengeFromEmailCommand
|
|
30009
30961
|
};
|
|
30010
30962
|
//#endregion
|
|
30011
30963
|
export { CANONICAL_OPERATION_ALIASES, COMMANDS, lookupOperation };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as normalizeApiBaseUrl, c as resolveConfigEnvironment, i as loadCliConfig } from "../cli-config-C-7V83Qd.js";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primcli",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
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,
|