primitive 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 { PrimitiveApiClient as A, saveCliCredentials as C, loadActiveChatState as D, deleteChatState as E, createConfig as M, loadChatConversationByLocalId as O, resolveCliAuth as S, chatStatePath as T, deleteCliCredentials as _, normalizeCliEnvironmentName as a, loadCliCredentials as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, createClient as j, saveActiveChatState 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, saveSignupCredentials as w, normalizeApiBaseUrl as x, detectPrimitiveKeyEnvMisname as y };
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 };
@@ -1,4 +1,4 @@
1
- import { A as PrimitiveApiClient, C as saveCliCredentials, D as loadActiveChatState, E as deleteChatState, M as createConfig, O as loadChatConversationByLocalId, S as resolveCliAuth, T as chatStatePath, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as loadCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as createClient, k as saveActiveChatState, 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 saveSignupCredentials, x as normalizeApiBaseUrl, y as detectPrimitiveKeyEnvMisname } from "../cli-config-B5hrwe8q.js";
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
- new RegExp(`^${/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.source.slice(1, -1)}@[^\\s@]+$`, "i");
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 in-thread.
28663
+ static description = `Pay an email-native x402 challenge in one step: sign it and send the signed interaction.json.
28234
28664
 
28235
- Reads the email challenge the payee issued (the JSON from
28236
- \`payments create-email-challenge\`), derives and signs the interaction-bound
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
- This is the recommended payer path. It replaces the two-step
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
- --in-reply-to is the id of the inbound challenge email you received. It is
28247
- fetched to derive the recipient (the payee, from the inbound's sender) and the
28248
- From (you, the payer, from the inbound's recipient). The outgoing send does
28249
- NOT thread under the challenge: threading would trigger the send endpoint's
28250
- parent-thread dedup and the payment would never settle. The interaction
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
- Provide the challenge inline with --challenge, from a file with
28255
- --challenge-file, or piped on stdin.`;
28256
- static summary = "Sign an email x402 challenge and send the signed interaction.json in-thread (one step)";
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
- "cat challenge.json | <%= config.bin %> payments pay-email --in-reply-to <inbound-email-id> --wait",
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: "The email challenge object as a JSON string.",
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: "Path to a file containing the email challenge JSON.",
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 an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
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 { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
28297
- apiKey: flags["api-key"],
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 this.log(JSON.stringify(sent, null, 2));
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 { c as resolveConfigEnvironment, i as loadCliConfig, x as normalizeApiBaseUrl } from "../cli-config-B5hrwe8q.js";
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": "primitive",
3
- "version": "1.14.0",
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,