primcli 1.13.3 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/oclif/index.js +579 -171
- package/package.json +1 -1
package/dist/oclif/index.js
CHANGED
|
@@ -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
|
|
@@ -8974,56 +8975,25 @@ const openapiDocument = {
|
|
|
8974
8975
|
"consecutive_fails"
|
|
8975
8976
|
]
|
|
8976
8977
|
},
|
|
8977
|
-
"
|
|
8978
|
+
"CreateEndpointInput": {
|
|
8978
8979
|
"type": "object",
|
|
8979
8980
|
"additionalProperties": false,
|
|
8980
8981
|
"properties": {
|
|
8981
8982
|
"kind": {
|
|
8982
8983
|
"type": "string",
|
|
8983
|
-
"enum": ["http"],
|
|
8984
|
+
"enum": ["http", "function"],
|
|
8984
8985
|
"default": "http",
|
|
8985
|
-
"description": "
|
|
8986
|
+
"description": "http: deliver to a webhook URL (provide url). function: invoke a Primitive Function (provide function_id, omit url)."
|
|
8986
8987
|
},
|
|
8987
8988
|
"url": {
|
|
8988
8989
|
"type": "string",
|
|
8989
8990
|
"minLength": 1,
|
|
8990
|
-
"description": "The webhook URL to deliver events to."
|
|
8991
|
-
},
|
|
8992
|
-
"enabled": {
|
|
8993
|
-
"type": "boolean",
|
|
8994
|
-
"default": true,
|
|
8995
|
-
"description": "Whether the endpoint is active"
|
|
8996
|
-
},
|
|
8997
|
-
"domain_id": {
|
|
8998
|
-
"type": ["string", "null"],
|
|
8999
|
-
"format": "uuid",
|
|
9000
|
-
"description": "Restrict to emails from a specific domain"
|
|
9001
|
-
},
|
|
9002
|
-
"rules": {
|
|
9003
|
-
"type": "object",
|
|
9004
|
-
"description": "Endpoint-specific filtering rules"
|
|
9005
|
-
},
|
|
9006
|
-
"is_route_target": {
|
|
9007
|
-
"type": "boolean",
|
|
9008
|
-
"default": false,
|
|
9009
|
-
"description": "Create this endpoint as a route-target: reachable only via an\nexplicit recipient route, never a domain's default destination, and\nexempt from the one-endpoint-per-domain rule.\n"
|
|
9010
|
-
}
|
|
9011
|
-
},
|
|
9012
|
-
"required": ["url"]
|
|
9013
|
-
},
|
|
9014
|
-
"CreateFunctionEndpointInput": {
|
|
9015
|
-
"type": "object",
|
|
9016
|
-
"additionalProperties": false,
|
|
9017
|
-
"properties": {
|
|
9018
|
-
"kind": {
|
|
9019
|
-
"type": "string",
|
|
9020
|
-
"enum": ["function"],
|
|
9021
|
-
"description": "Invoke a Primitive Function."
|
|
8991
|
+
"description": "The webhook URL to deliver events to. Required when kind is http; omit for function endpoints."
|
|
9022
8992
|
},
|
|
9023
8993
|
"function_id": {
|
|
9024
8994
|
"type": "string",
|
|
9025
8995
|
"format": "uuid",
|
|
9026
|
-
"description": "The Function to invoke."
|
|
8996
|
+
"description": "The Function to invoke. Required when kind is function."
|
|
9027
8997
|
},
|
|
9028
8998
|
"enabled": {
|
|
9029
8999
|
"type": "boolean",
|
|
@@ -9044,17 +9014,6 @@ const openapiDocument = {
|
|
|
9044
9014
|
"default": false,
|
|
9045
9015
|
"description": "Create this endpoint as a route-target: reachable only via an\nexplicit recipient route, never a domain's default destination, and\nexempt from the one-endpoint-per-domain rule.\n"
|
|
9046
9016
|
}
|
|
9047
|
-
},
|
|
9048
|
-
"required": ["kind", "function_id"]
|
|
9049
|
-
},
|
|
9050
|
-
"CreateEndpointInput": {
|
|
9051
|
-
"oneOf": [{ "$ref": "#/components/schemas/CreateHttpEndpointInput" }, { "$ref": "#/components/schemas/CreateFunctionEndpointInput" }],
|
|
9052
|
-
"discriminator": {
|
|
9053
|
-
"propertyName": "kind",
|
|
9054
|
-
"mapping": {
|
|
9055
|
-
"http": "#/components/schemas/CreateHttpEndpointInput",
|
|
9056
|
-
"function": "#/components/schemas/CreateFunctionEndpointInput"
|
|
9057
|
-
}
|
|
9058
9017
|
}
|
|
9059
9018
|
},
|
|
9060
9019
|
"UpdateEndpointInput": {
|
|
@@ -13369,83 +13328,43 @@ const operationManifest = [
|
|
|
13369
13328
|
"pathParams": [],
|
|
13370
13329
|
"queryParams": [],
|
|
13371
13330
|
"requestSchema": {
|
|
13372
|
-
"
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
"
|
|
13376
|
-
"
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
"description": "Deliver to a webhook URL."
|
|
13381
|
-
},
|
|
13382
|
-
"url": {
|
|
13383
|
-
"type": "string",
|
|
13384
|
-
"minLength": 1,
|
|
13385
|
-
"description": "The webhook URL to deliver events to."
|
|
13386
|
-
},
|
|
13387
|
-
"enabled": {
|
|
13388
|
-
"type": "boolean",
|
|
13389
|
-
"default": true,
|
|
13390
|
-
"description": "Whether the endpoint is active"
|
|
13391
|
-
},
|
|
13392
|
-
"domain_id": {
|
|
13393
|
-
"type": ["string", "null"],
|
|
13394
|
-
"format": "uuid",
|
|
13395
|
-
"description": "Restrict to emails from a specific domain"
|
|
13396
|
-
},
|
|
13397
|
-
"rules": {
|
|
13398
|
-
"type": "object",
|
|
13399
|
-
"description": "Endpoint-specific filtering rules"
|
|
13400
|
-
},
|
|
13401
|
-
"is_route_target": {
|
|
13402
|
-
"type": "boolean",
|
|
13403
|
-
"default": false,
|
|
13404
|
-
"description": "Create this endpoint as a route-target: reachable only via an\nexplicit recipient route, never a domain's default destination, and\nexempt from the one-endpoint-per-domain rule.\n"
|
|
13405
|
-
}
|
|
13331
|
+
"type": "object",
|
|
13332
|
+
"additionalProperties": false,
|
|
13333
|
+
"properties": {
|
|
13334
|
+
"kind": {
|
|
13335
|
+
"type": "string",
|
|
13336
|
+
"enum": ["http", "function"],
|
|
13337
|
+
"default": "http",
|
|
13338
|
+
"description": "http: deliver to a webhook URL (provide url). function: invoke a Primitive Function (provide function_id, omit url)."
|
|
13406
13339
|
},
|
|
13407
|
-
"
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
"properties": {
|
|
13412
|
-
"kind": {
|
|
13413
|
-
"type": "string",
|
|
13414
|
-
"enum": ["function"],
|
|
13415
|
-
"description": "Invoke a Primitive Function."
|
|
13416
|
-
},
|
|
13417
|
-
"function_id": {
|
|
13418
|
-
"type": "string",
|
|
13419
|
-
"format": "uuid",
|
|
13420
|
-
"description": "The Function to invoke."
|
|
13421
|
-
},
|
|
13422
|
-
"enabled": {
|
|
13423
|
-
"type": "boolean",
|
|
13424
|
-
"default": true,
|
|
13425
|
-
"description": "Whether the endpoint is active"
|
|
13426
|
-
},
|
|
13427
|
-
"domain_id": {
|
|
13428
|
-
"type": ["string", "null"],
|
|
13429
|
-
"format": "uuid",
|
|
13430
|
-
"description": "Restrict to emails from a specific domain"
|
|
13431
|
-
},
|
|
13432
|
-
"rules": {
|
|
13433
|
-
"type": "object",
|
|
13434
|
-
"description": "Endpoint-specific filtering rules"
|
|
13435
|
-
},
|
|
13436
|
-
"is_route_target": {
|
|
13437
|
-
"type": "boolean",
|
|
13438
|
-
"default": false,
|
|
13439
|
-
"description": "Create this endpoint as a route-target: reachable only via an\nexplicit recipient route, never a domain's default destination, and\nexempt from the one-endpoint-per-domain rule.\n"
|
|
13440
|
-
}
|
|
13340
|
+
"url": {
|
|
13341
|
+
"type": "string",
|
|
13342
|
+
"minLength": 1,
|
|
13343
|
+
"description": "The webhook URL to deliver events to. Required when kind is http; omit for function endpoints."
|
|
13441
13344
|
},
|
|
13442
|
-
"
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
"
|
|
13345
|
+
"function_id": {
|
|
13346
|
+
"type": "string",
|
|
13347
|
+
"format": "uuid",
|
|
13348
|
+
"description": "The Function to invoke. Required when kind is function."
|
|
13349
|
+
},
|
|
13350
|
+
"enabled": {
|
|
13351
|
+
"type": "boolean",
|
|
13352
|
+
"default": true,
|
|
13353
|
+
"description": "Whether the endpoint is active"
|
|
13354
|
+
},
|
|
13355
|
+
"domain_id": {
|
|
13356
|
+
"type": ["string", "null"],
|
|
13357
|
+
"format": "uuid",
|
|
13358
|
+
"description": "Restrict to emails from a specific domain"
|
|
13359
|
+
},
|
|
13360
|
+
"rules": {
|
|
13361
|
+
"type": "object",
|
|
13362
|
+
"description": "Endpoint-specific filtering rules"
|
|
13363
|
+
},
|
|
13364
|
+
"is_route_target": {
|
|
13365
|
+
"type": "boolean",
|
|
13366
|
+
"default": false,
|
|
13367
|
+
"description": "Create this endpoint as a route-target: reachable only via an\nexplicit recipient route, never a domain's default destination, and\nexempt from the one-endpoint-per-domain rule.\n"
|
|
13449
13368
|
}
|
|
13450
13369
|
}
|
|
13451
13370
|
},
|
|
@@ -23864,8 +23783,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
23864
23783
|
name: "Primitive Team",
|
|
23865
23784
|
url: "https://primitive.dev"
|
|
23866
23785
|
};
|
|
23867
|
-
const SDK_VERSION_RANGE = "^1.
|
|
23868
|
-
const CLI_VERSION_RANGE = "^1.
|
|
23786
|
+
const SDK_VERSION_RANGE = "^1.14.0";
|
|
23787
|
+
const CLI_VERSION_RANGE = "^1.14.0";
|
|
23869
23788
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
23870
23789
|
function renderHandler() {
|
|
23871
23790
|
return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
|
|
@@ -27547,7 +27466,80 @@ var X402Error = class extends Error {
|
|
|
27547
27466
|
this.retryAfter = options?.retryAfter ?? null;
|
|
27548
27467
|
}
|
|
27549
27468
|
};
|
|
27550
|
-
|
|
27469
|
+
/** A UUID, as carried by `interaction_id`'s local part and the step ids. */
|
|
27470
|
+
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;
|
|
27471
|
+
/** An interaction id is `uuid@domain`. */
|
|
27472
|
+
const EXTRACT_WIRE_ID_RE = new RegExp(`^${EXTRACT_UUID_RE.source.slice(1, -1)}@[^\\s@]+$`, "i");
|
|
27473
|
+
/**
|
|
27474
|
+
* Parse the bytes of an inbound `interaction.json` MIME part into a typed
|
|
27475
|
+
* {@link X402EmailChallenge} ready for {@link X402Client.payEmailChallenge}.
|
|
27476
|
+
*
|
|
27477
|
+
* A payer receives the x402 challenge as an `interaction.json` attachment on an
|
|
27478
|
+
* inbound email (filename `interaction.json`, content type `application/json`).
|
|
27479
|
+
* This validates the envelope (the strict snake_case wire shape, that it is the
|
|
27480
|
+
* `x402.payment` `challenge` step, and that the embedded payload carries the
|
|
27481
|
+
* fields a payer signs over) and re-assembles the nonce binding from the
|
|
27482
|
+
* envelope's `interaction_id` + `step_id` + the payload's `challenge_nonce`, so
|
|
27483
|
+
* the caller never has to hand-parse the part.
|
|
27484
|
+
*
|
|
27485
|
+
* Accepts the part body as a UTF-8 string, a `Uint8Array`/`Buffer`, or an
|
|
27486
|
+
* already-parsed envelope object. Throws {@link X402Error} (status 0) on any
|
|
27487
|
+
* malformed or non-challenge part.
|
|
27488
|
+
*
|
|
27489
|
+
* The resulting `challenge_id` is empty: the platform's private challenge id is
|
|
27490
|
+
* not carried on the wire, and `payEmailChallenge` does not need it (it binds to
|
|
27491
|
+
* the `interaction_id` and the challenge step id).
|
|
27492
|
+
*/
|
|
27493
|
+
function parseEmailChallengeFromPart(part) {
|
|
27494
|
+
const bad = (field) => {
|
|
27495
|
+
throw new X402Error(`interaction.json part is not a valid x402 challenge: ${field}`, 0);
|
|
27496
|
+
};
|
|
27497
|
+
let envelope;
|
|
27498
|
+
if (typeof part === "string" || part instanceof Uint8Array) {
|
|
27499
|
+
const text = typeof part === "string" ? part : new TextDecoder().decode(part);
|
|
27500
|
+
let parsed;
|
|
27501
|
+
try {
|
|
27502
|
+
parsed = JSON.parse(text);
|
|
27503
|
+
} catch (cause) {
|
|
27504
|
+
throw new X402Error(`interaction.json part is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`, 0, void 0, { cause });
|
|
27505
|
+
}
|
|
27506
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) bad("envelope (expected a JSON object)");
|
|
27507
|
+
envelope = parsed;
|
|
27508
|
+
} else if (part && typeof part === "object" && !Array.isArray(part) && !(part instanceof Uint8Array)) envelope = part;
|
|
27509
|
+
else return bad("envelope (expected JSON bytes, a string, or an object)");
|
|
27510
|
+
if (envelope.interaction_version !== 1) bad("interaction_version (expected 1)");
|
|
27511
|
+
const interactionId = envelope.interaction_id;
|
|
27512
|
+
if (typeof interactionId !== "string" || !EXTRACT_WIRE_ID_RE.test(interactionId)) bad("interaction_id (expected uuid@domain)");
|
|
27513
|
+
if (envelope.protocol !== "x402.payment") bad(`protocol (expected "${X402_INTERACTION_PROTOCOL}")`);
|
|
27514
|
+
if (envelope.protocol_version !== 1) bad(`protocol_version (expected 1)`);
|
|
27515
|
+
if (envelope.step !== "challenge") bad(`step (expected "challenge", got "${String(envelope.step)}")`);
|
|
27516
|
+
const stepId = envelope.step_id;
|
|
27517
|
+
if (typeof stepId !== "string" || !EXTRACT_UUID_RE.test(stepId)) bad("step_id (expected a uuid)");
|
|
27518
|
+
const expiresAt = envelope.expires_at;
|
|
27519
|
+
if (typeof expiresAt !== "string" || !expiresAt) bad("expires_at (expected an ISO-8601 timestamp on the challenge step)");
|
|
27520
|
+
const payload = envelope.payload;
|
|
27521
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) bad("payload (expected an object)");
|
|
27522
|
+
const p = payload;
|
|
27523
|
+
const challengeNonce = p.challenge_nonce;
|
|
27524
|
+
if (typeof challengeNonce !== "string" || !/^[0-9a-f]{64}$/.test(challengeNonce)) bad("payload.challenge_nonce (expected 64 lowercase hex chars)");
|
|
27525
|
+
const paymentRequirements = p.payment_requirements;
|
|
27526
|
+
if (!paymentRequirements || typeof paymentRequirements !== "object" || Array.isArray(paymentRequirements)) bad("payload.payment_requirements (expected an object)");
|
|
27527
|
+
const challenge = {
|
|
27528
|
+
interaction_id: interactionId,
|
|
27529
|
+
challenge_id: "",
|
|
27530
|
+
challenge: {
|
|
27531
|
+
payment_requirements: paymentRequirements,
|
|
27532
|
+
nonce_binding: {
|
|
27533
|
+
interaction_id: interactionId,
|
|
27534
|
+
challenge_step_id: stepId,
|
|
27535
|
+
challenge_nonce: challengeNonce
|
|
27536
|
+
},
|
|
27537
|
+
expires_at: expiresAt
|
|
27538
|
+
}
|
|
27539
|
+
};
|
|
27540
|
+
validateEmailChallenge(challenge);
|
|
27541
|
+
return challenge;
|
|
27542
|
+
}
|
|
27551
27543
|
/**
|
|
27552
27544
|
* Assert a challenge is fully hydrated before signing, so a missing field fails
|
|
27553
27545
|
* with a named X402Error instead of an opaque viem/BigInt error mid-sign.
|
|
@@ -27867,6 +27859,149 @@ function createX402Client(options = {}) {
|
|
|
27867
27859
|
return new X402Client(options);
|
|
27868
27860
|
}
|
|
27869
27861
|
//#endregion
|
|
27862
|
+
//#region src/oclif/commands/payments-email-challenge.ts
|
|
27863
|
+
/** The exact attachment part name the x402 challenge travels in. */
|
|
27864
|
+
const INTERACTION_PART_FILENAME$1 = "interaction.json";
|
|
27865
|
+
/** Content type the challenge attachment is sent with. */
|
|
27866
|
+
const INTERACTION_PART_CONTENT_TYPE$1 = "application/json";
|
|
27867
|
+
/**
|
|
27868
|
+
* Strip a leading `<digits>_` part-index prefix from an archive entry's
|
|
27869
|
+
* basename. The attachments archive names each member `<part_index>_<filename>`,
|
|
27870
|
+
* so `0_interaction.json` -> `interaction.json`.
|
|
27871
|
+
*/
|
|
27872
|
+
function stripPartIndexPrefix(basename) {
|
|
27873
|
+
return basename.replace(/^\d+_/, "");
|
|
27874
|
+
}
|
|
27875
|
+
/**
|
|
27876
|
+
* Resolve the exact archive entry name for the challenge attachment from email
|
|
27877
|
+
* attachment metadata. Returns the matching entry's `tar_path` (e.g.
|
|
27878
|
+
* `0_interaction.json`), or null when no `interaction.json` attachment is
|
|
27879
|
+
* described. Prefers an exact `filename === "interaction.json"` match; falls back
|
|
27880
|
+
* to a single `application/json` part when the filename is absent.
|
|
27881
|
+
*/
|
|
27882
|
+
function interactionTarPathFromMeta(attachments) {
|
|
27883
|
+
if (!attachments || attachments.length === 0) return null;
|
|
27884
|
+
const chosen = attachments.find((a) => a.filename === "interaction.json") ?? attachments.find((a) => a.content_type === INTERACTION_PART_CONTENT_TYPE$1);
|
|
27885
|
+
if (!chosen) return null;
|
|
27886
|
+
if (chosen.tar_path) return chosen.tar_path;
|
|
27887
|
+
if (typeof chosen.part_index === "number" && chosen.filename) return `${chosen.part_index}_${chosen.filename}`;
|
|
27888
|
+
return chosen.filename ?? null;
|
|
27889
|
+
}
|
|
27890
|
+
/** Build the attachments-archive URL for an inbound email id. */
|
|
27891
|
+
function attachmentsArchiveUrl(baseUrl, emailId) {
|
|
27892
|
+
return `${baseUrl.replace(/\/$/, "")}/emails/${encodeURIComponent(emailId)}/attachments.tar.gz`;
|
|
27893
|
+
}
|
|
27894
|
+
/**
|
|
27895
|
+
* Minimal POSIX (ustar) tar reader: yield each regular-file entry's name and
|
|
27896
|
+
* bytes. We avoid a third-party tar dependency because the published CLI bundles
|
|
27897
|
+
* everything (the tarball-isolation check forbids unexpected runtime deps) and a
|
|
27898
|
+
* tar archive of email attachments is a flat list of small regular files, which
|
|
27899
|
+
* the base format already covers.
|
|
27900
|
+
*
|
|
27901
|
+
* Tar layout: a sequence of 512-byte records. Each file is one 512-byte header
|
|
27902
|
+
* followed by ceil(size/512) data records. Two consecutive all-zero headers mark
|
|
27903
|
+
* the end. The header's name is at offset 0 (100 bytes), size at offset 124 (12
|
|
27904
|
+
* bytes, octal), and the type flag at offset 156 ('0' or '\0' = regular file).
|
|
27905
|
+
* GNU long names (type 'L') and the ustar `prefix` field are handled so a longer
|
|
27906
|
+
* path still resolves; other special types are skipped.
|
|
27907
|
+
*/
|
|
27908
|
+
function* readTarEntries(buffer) {
|
|
27909
|
+
const BLOCK = 512;
|
|
27910
|
+
let offset = 0;
|
|
27911
|
+
let longName = null;
|
|
27912
|
+
const readString = (start, length) => {
|
|
27913
|
+
const slice = buffer.subarray(start, start + length);
|
|
27914
|
+
let end = slice.indexOf(0);
|
|
27915
|
+
if (end === -1) end = slice.length;
|
|
27916
|
+
return new TextDecoder().decode(slice.subarray(0, end));
|
|
27917
|
+
};
|
|
27918
|
+
while (offset + BLOCK <= buffer.length) {
|
|
27919
|
+
const header = buffer.subarray(offset, offset + BLOCK);
|
|
27920
|
+
if (header.every((byte) => byte === 0)) break;
|
|
27921
|
+
const sizeField = readString(offset + 124, 12).trim();
|
|
27922
|
+
const size = sizeField ? Number.parseInt(sizeField, 8) : 0;
|
|
27923
|
+
const safeSize = Number.isFinite(size) && size >= 0 ? size : 0;
|
|
27924
|
+
const typeFlag = String.fromCharCode(header[156] ?? 0);
|
|
27925
|
+
const dataStart = offset + BLOCK;
|
|
27926
|
+
const dataEnd = dataStart + safeSize;
|
|
27927
|
+
if (typeFlag === "L") longName = readString(dataStart, safeSize).replace(/\0+$/, "");
|
|
27928
|
+
else if (typeFlag === "0" || typeFlag === "\0") {
|
|
27929
|
+
const name = longName ?? (() => {
|
|
27930
|
+
const prefix = readString(offset + 345, 155);
|
|
27931
|
+
const base = readString(offset, 100);
|
|
27932
|
+
return prefix ? `${prefix}/${base}` : base;
|
|
27933
|
+
})();
|
|
27934
|
+
longName = null;
|
|
27935
|
+
yield {
|
|
27936
|
+
name,
|
|
27937
|
+
bytes: buffer.subarray(dataStart, dataEnd)
|
|
27938
|
+
};
|
|
27939
|
+
} else longName = null;
|
|
27940
|
+
offset = dataStart + Math.ceil(safeSize / BLOCK) * BLOCK;
|
|
27941
|
+
}
|
|
27942
|
+
}
|
|
27943
|
+
/**
|
|
27944
|
+
* Pull the `interaction.json` bytes out of a gzipped tar attachments archive.
|
|
27945
|
+
* Returns null when the archive has no such member (e.g. the inbound email is
|
|
27946
|
+
* not an x402 challenge).
|
|
27947
|
+
*
|
|
27948
|
+
* When `tarPath` is given (the exact entry name from the email's attachment
|
|
27949
|
+
* metadata, e.g. `0_interaction.json`), the member is matched by that exact name
|
|
27950
|
+
* first. As a fallback (and when no `tarPath` is supplied), the member is matched
|
|
27951
|
+
* by basename after stripping any leading `<digits>_` part-index prefix, so both
|
|
27952
|
+
* `0_interaction.json` and a bare `interaction.json` resolve, and a leading
|
|
27953
|
+
* directory in the archive path does not hide it.
|
|
27954
|
+
*/
|
|
27955
|
+
function interactionJsonFromArchive(gzipped, tarPath) {
|
|
27956
|
+
const tar = gunzipSync(gzipped);
|
|
27957
|
+
const wantedBase = tarPath ? tarPath.split("/").pop() ?? tarPath : null;
|
|
27958
|
+
for (const entry of readTarEntries(tar)) {
|
|
27959
|
+
const base = entry.name.split("/").pop() ?? entry.name;
|
|
27960
|
+
if (wantedBase && base === wantedBase) return entry.bytes;
|
|
27961
|
+
if (!wantedBase && stripPartIndexPrefix(base) === "interaction.json") return entry.bytes;
|
|
27962
|
+
}
|
|
27963
|
+
if (wantedBase) {
|
|
27964
|
+
for (const entry of readTarEntries(tar)) if (stripPartIndexPrefix(entry.name.split("/").pop() ?? entry.name) === "interaction.json") return entry.bytes;
|
|
27965
|
+
}
|
|
27966
|
+
return null;
|
|
27967
|
+
}
|
|
27968
|
+
/**
|
|
27969
|
+
* Fetch an inbound email's attachments archive and return the raw
|
|
27970
|
+
* `interaction.json` bytes, or null when the email carries no such part.
|
|
27971
|
+
*
|
|
27972
|
+
* Mirrors the direct-fetch + bearer-auth pattern the zone-file download command
|
|
27973
|
+
* uses: the typed SDK client returns the archive as a Blob, which is awkward to
|
|
27974
|
+
* decompress; a plain fetch hands us an ArrayBuffer directly. `fetchImpl` is
|
|
27975
|
+
* injectable so tests can stub the network boundary.
|
|
27976
|
+
*/
|
|
27977
|
+
async function fetchInteractionJsonBytes(params) {
|
|
27978
|
+
const response = await (params.fetchImpl ?? fetch)(attachmentsArchiveUrl(params.baseUrl, params.emailId), { headers: {
|
|
27979
|
+
...params.headers ?? {},
|
|
27980
|
+
...params.apiKey ? { authorization: `Bearer ${params.apiKey}` } : {}
|
|
27981
|
+
} });
|
|
27982
|
+
if (!response.ok) {
|
|
27983
|
+
const text = await response.text().catch(() => "");
|
|
27984
|
+
throw new Error(`Could not download attachments for inbound email ${params.emailId}: HTTP ${response.status}${text.trim() ? ` ${text.trim().slice(0, 200)}` : ""}`);
|
|
27985
|
+
}
|
|
27986
|
+
return interactionJsonFromArchive(new Uint8Array(await response.arrayBuffer()), interactionTarPathFromMeta(params.attachments));
|
|
27987
|
+
}
|
|
27988
|
+
/**
|
|
27989
|
+
* Derive the signable {@link X402EmailChallenge} from an inbound payment-request
|
|
27990
|
+
* email's `interaction.json` attachment. This is the auto-derive path behind
|
|
27991
|
+
* `pay-email --in-reply-to <id>` (with no `--challenge`) and
|
|
27992
|
+
* `challenge-from-email`: download the part, then reshape the wire envelope into
|
|
27993
|
+
* the challenge object via the SDK's canonical `parseEmailChallengeFromPart`.
|
|
27994
|
+
*
|
|
27995
|
+
* Throws a descriptive error when the email has no `interaction.json` part (so a
|
|
27996
|
+
* non-challenge email gives a clear message instead of a confusing signer crash)
|
|
27997
|
+
* or when the part is not a valid x402 challenge envelope (the SDK validator).
|
|
27998
|
+
*/
|
|
27999
|
+
async function deriveEmailChallengeFromInbound(params) {
|
|
28000
|
+
const bytes = await fetchInteractionJsonBytes(params);
|
|
28001
|
+
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.`);
|
|
28002
|
+
return parseEmailChallengeFromPart(bytes);
|
|
28003
|
+
}
|
|
28004
|
+
//#endregion
|
|
27870
28005
|
//#region src/oclif/commands/payments-shared.ts
|
|
27871
28006
|
const PRIVATE_KEY_ENV = "PRIMITIVE_X402_PRIVATE_KEY";
|
|
27872
28007
|
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.`;
|
|
@@ -27981,6 +28116,71 @@ function reportX402Error(error, ctx) {
|
|
|
27981
28116
|
});
|
|
27982
28117
|
}
|
|
27983
28118
|
//#endregion
|
|
28119
|
+
//#region src/oclif/commands/payments-challenge-from-email.ts
|
|
28120
|
+
var PaymentsChallengeFromEmailCommand = class PaymentsChallengeFromEmailCommand extends Command {
|
|
28121
|
+
static description = `Derive and print the email x402 challenge object from a received payment-request email.
|
|
28122
|
+
|
|
28123
|
+
A payer receives an x402 payment request as an email carrying an
|
|
28124
|
+
\`interaction.json\` attachment (the wire envelope). The signing commands,
|
|
28125
|
+
however, take the challenge object the payee's \`create-email-challenge\` API
|
|
28126
|
+
returns, which has a different field layout. This command downloads the inbound
|
|
28127
|
+
email's \`interaction.json\` attachment and reshapes it into that challenge
|
|
28128
|
+
object, printing it to stdout.
|
|
28129
|
+
|
|
28130
|
+
Use it when you want the challenge as a portable artifact (e.g. to feed
|
|
28131
|
+
\`pay-email-step\` for offline signing, or an SDK \`payEmailChallenge\` call).
|
|
28132
|
+
For the simple one-shot, prefer \`pay-email --in-reply-to <id>\`, which derives
|
|
28133
|
+
the challenge internally and needs no intermediate file.
|
|
28134
|
+
|
|
28135
|
+
primitive payments challenge-from-email --id <inbound-id> > challenge.json
|
|
28136
|
+
primitive payments challenge-from-email --id <inbound-id> \\
|
|
28137
|
+
| primitive payments pay-email-step > interaction.json`;
|
|
28138
|
+
static summary = "Print the email x402 challenge object derived from a received payment-request email";
|
|
28139
|
+
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"];
|
|
28140
|
+
static flags = {
|
|
28141
|
+
"api-key": Flags.string({
|
|
28142
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
28143
|
+
env: "PRIMITIVE_API_KEY"
|
|
28144
|
+
}),
|
|
28145
|
+
"api-base-url": Flags.string({
|
|
28146
|
+
description: API_BASE_URL_FLAG_DESCRIPTION,
|
|
28147
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
28148
|
+
hidden: true
|
|
28149
|
+
}),
|
|
28150
|
+
id: Flags.string({
|
|
28151
|
+
description: "Id of the inbound payment-request email whose interaction.json attachment carries the challenge.",
|
|
28152
|
+
required: true
|
|
28153
|
+
}),
|
|
28154
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
28155
|
+
};
|
|
28156
|
+
async run() {
|
|
28157
|
+
const { flags } = await this.parse(PaymentsChallengeFromEmailCommand);
|
|
28158
|
+
const { auth, baseUrlOverridden, requestConfig } = await createAuthenticatedCliApiClient({
|
|
28159
|
+
apiKey: flags["api-key"],
|
|
28160
|
+
apiBaseUrl: flags["api-base-url"],
|
|
28161
|
+
configDir: this.config.configDir
|
|
28162
|
+
});
|
|
28163
|
+
await runWithTiming(flags.time, async () => {
|
|
28164
|
+
try {
|
|
28165
|
+
const challenge = await deriveEmailChallengeFromInbound({
|
|
28166
|
+
baseUrl: auth.apiBaseUrl,
|
|
28167
|
+
emailId: flags.id,
|
|
28168
|
+
apiKey: auth.apiKey,
|
|
28169
|
+
headers: requestConfig.headers
|
|
28170
|
+
});
|
|
28171
|
+
this.log(JSON.stringify(challenge, null, 2));
|
|
28172
|
+
} catch (error) {
|
|
28173
|
+
reportX402Error(error, {
|
|
28174
|
+
auth,
|
|
28175
|
+
baseUrlOverridden,
|
|
28176
|
+
configDir: this.config.configDir
|
|
28177
|
+
});
|
|
28178
|
+
process.exitCode = 1;
|
|
28179
|
+
}
|
|
28180
|
+
});
|
|
28181
|
+
}
|
|
28182
|
+
};
|
|
28183
|
+
//#endregion
|
|
27984
28184
|
//#region src/oclif/commands/payments-charge.ts
|
|
27985
28185
|
var PaymentsChargeCommand = class PaymentsChargeCommand extends Command {
|
|
27986
28186
|
static description = `Request an x402 payment by creating a challenge (payee side).
|
|
@@ -28289,7 +28489,136 @@ var PaymentsPayEmailStepCommand = class PaymentsPayEmailStepCommand extends Comm
|
|
|
28289
28489
|
}
|
|
28290
28490
|
};
|
|
28291
28491
|
//#endregion
|
|
28492
|
+
//#region src/oclif/commands/payments-settlement.ts
|
|
28493
|
+
/** The human-readable notice describing the async-settlement model. */
|
|
28494
|
+
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.";
|
|
28495
|
+
/** The steps a payer has already seen / sent, so a matching later step is the receipt. */
|
|
28496
|
+
const NON_RECEIPT_STEPS = new Set(["challenge", "payment"]);
|
|
28497
|
+
/**
|
|
28498
|
+
* Best-effort extraction of a `settle_tx` from a receipt envelope. The exact
|
|
28499
|
+
* receipt wire shape is owned by the platform and not pinned in the SDK, so we
|
|
28500
|
+
* do not assert a rigid schema: we look for a `settle_tx` string at the top
|
|
28501
|
+
* level or one level into `payload` (the two places interaction envelopes carry
|
|
28502
|
+
* step data). Returns null when absent; the caller always prints the full
|
|
28503
|
+
* envelope regardless, so a miss never hides data.
|
|
28504
|
+
*/
|
|
28505
|
+
function extractSettleTx(envelope) {
|
|
28506
|
+
const top = envelope.settle_tx;
|
|
28507
|
+
if (typeof top === "string" && top) return top;
|
|
28508
|
+
const payload = envelope.payload;
|
|
28509
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
28510
|
+
const nested = payload.settle_tx;
|
|
28511
|
+
if (typeof nested === "string" && nested) return nested;
|
|
28512
|
+
}
|
|
28513
|
+
return null;
|
|
28514
|
+
}
|
|
28515
|
+
/**
|
|
28516
|
+
* Parse interaction.json bytes into an envelope object, or null when the bytes
|
|
28517
|
+
* are not a JSON object (so a non-interaction attachment is simply skipped).
|
|
28518
|
+
*/
|
|
28519
|
+
function parseInteractionEnvelope(bytes) {
|
|
28520
|
+
let parsed;
|
|
28521
|
+
try {
|
|
28522
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
28523
|
+
} catch {
|
|
28524
|
+
return null;
|
|
28525
|
+
}
|
|
28526
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
28527
|
+
return parsed;
|
|
28528
|
+
}
|
|
28529
|
+
/**
|
|
28530
|
+
* True when a downloaded interaction.json envelope is the settlement receipt for
|
|
28531
|
+
* the interaction we paid: same `interaction_id`, and a step the payer has not
|
|
28532
|
+
* already produced/received (i.e. not `challenge` / `payment`). Matching on the
|
|
28533
|
+
* confirmed interaction_id keeps this correct without depending on the exact
|
|
28534
|
+
* receipt step name, which the platform owns.
|
|
28535
|
+
*/
|
|
28536
|
+
function isSettlementReceiptFor(envelope, interactionId) {
|
|
28537
|
+
if (envelope.interaction_id !== interactionId) return false;
|
|
28538
|
+
const step = envelope.step;
|
|
28539
|
+
if (typeof step === "string" && NON_RECEIPT_STEPS.has(step)) return false;
|
|
28540
|
+
return true;
|
|
28541
|
+
}
|
|
28542
|
+
/**
|
|
28543
|
+
* Poll the inbox for the x402 settlement interaction email that answers the
|
|
28544
|
+
* payment we just sent, and return it once found. Returns null on timeout.
|
|
28545
|
+
*
|
|
28546
|
+
* Searches inbound mail from the payee (with attachments) received since the
|
|
28547
|
+
* send, downloads each candidate's interaction.json, and matches it by
|
|
28548
|
+
* interaction_id via {@link isSettlementReceiptFor}. Network access is via the
|
|
28549
|
+
* injected api client (search) plus a plain fetch for the attachment archive,
|
|
28550
|
+
* mirroring how the rest of the payments surface reads attachments; `fetchImpl`
|
|
28551
|
+
* is injectable for tests.
|
|
28552
|
+
*/
|
|
28553
|
+
async function pollForSettlementInteraction(params) {
|
|
28554
|
+
const deadline = params.timeoutSeconds === 0 ? null : Date.now() + params.timeoutSeconds * 1e3;
|
|
28555
|
+
const checked = /* @__PURE__ */ new Set();
|
|
28556
|
+
while (deadline === null || Date.now() < deadline) {
|
|
28557
|
+
let cursor = null;
|
|
28558
|
+
let receipt = null;
|
|
28559
|
+
let drained = false;
|
|
28560
|
+
while (!drained) {
|
|
28561
|
+
const page = await fetchEmailSearchPage({
|
|
28562
|
+
apiClient: params.apiClient,
|
|
28563
|
+
cursor,
|
|
28564
|
+
filters: {
|
|
28565
|
+
from: params.payeeFrom,
|
|
28566
|
+
hasAttachment: true
|
|
28567
|
+
},
|
|
28568
|
+
pageSize: 50,
|
|
28569
|
+
since: params.since
|
|
28570
|
+
});
|
|
28571
|
+
if (!page.ok) break;
|
|
28572
|
+
for (const row of page.rows) {
|
|
28573
|
+
if (checked.has(row.id)) continue;
|
|
28574
|
+
let bytes;
|
|
28575
|
+
try {
|
|
28576
|
+
bytes = await fetchInteractionJsonBytes({
|
|
28577
|
+
baseUrl: params.baseUrl,
|
|
28578
|
+
emailId: row.id,
|
|
28579
|
+
apiKey: params.apiKey,
|
|
28580
|
+
headers: params.headers,
|
|
28581
|
+
fetchImpl: params.fetchImpl
|
|
28582
|
+
});
|
|
28583
|
+
} catch {
|
|
28584
|
+
continue;
|
|
28585
|
+
}
|
|
28586
|
+
if (!bytes) continue;
|
|
28587
|
+
const envelope = parseInteractionEnvelope(bytes);
|
|
28588
|
+
if (!envelope) continue;
|
|
28589
|
+
checked.add(row.id);
|
|
28590
|
+
if (isSettlementReceiptFor(envelope, params.interactionId)) {
|
|
28591
|
+
receipt = {
|
|
28592
|
+
emailId: row.id,
|
|
28593
|
+
envelope,
|
|
28594
|
+
settleTx: extractSettleTx(envelope)
|
|
28595
|
+
};
|
|
28596
|
+
break;
|
|
28597
|
+
}
|
|
28598
|
+
}
|
|
28599
|
+
if (receipt) break;
|
|
28600
|
+
const nextCursor = page.cursor;
|
|
28601
|
+
if (!nextCursor || nextCursor === cursor || page.rows.length === 0) drained = true;
|
|
28602
|
+
else cursor = nextCursor;
|
|
28603
|
+
}
|
|
28604
|
+
if (receipt) return receipt;
|
|
28605
|
+
if (deadline !== null && Date.now() >= deadline) break;
|
|
28606
|
+
await sleep$1(params.intervalSeconds * 1e3);
|
|
28607
|
+
}
|
|
28608
|
+
return null;
|
|
28609
|
+
}
|
|
28610
|
+
//#endregion
|
|
28292
28611
|
//#region src/oclif/commands/payments-pay-email.ts
|
|
28612
|
+
/**
|
|
28613
|
+
* True when none of the explicit challenge sources are present (no --challenge,
|
|
28614
|
+
* no --challenge-file, and stdin is an interactive TTY rather than a pipe). In
|
|
28615
|
+
* that case `pay-email` auto-derives the challenge from the inbound email's
|
|
28616
|
+
* interaction.json attachment. We treat a TTY stdin as "no challenge piped" so
|
|
28617
|
+
* the one-shot does not block on readFileSync(0) waiting for the user to paste.
|
|
28618
|
+
*/
|
|
28619
|
+
function shouldDeriveChallenge(flags) {
|
|
28620
|
+
return flags.challenge === void 0 && flags["challenge-file"] === void 0;
|
|
28621
|
+
}
|
|
28293
28622
|
/** The exact part name + content type the inbound matcher requires. */
|
|
28294
28623
|
const INTERACTION_PART_FILENAME = "interaction.json";
|
|
28295
28624
|
const INTERACTION_PART_CONTENT_TYPE = "application/json";
|
|
@@ -28312,34 +28641,64 @@ function emailDetailFromEnvelope(data) {
|
|
|
28312
28641
|
return data?.data ?? null;
|
|
28313
28642
|
}
|
|
28314
28643
|
var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
28315
|
-
static description = `Pay an email-native x402 challenge in one step: sign it and send the signed interaction.json
|
|
28644
|
+
static description = `Pay an email-native x402 challenge in one step: sign it and send the signed interaction.json.
|
|
28316
28645
|
|
|
28317
|
-
|
|
28318
|
-
\`
|
|
28319
|
-
EIP-3009 authorization locally with your wallet key (read from
|
|
28320
|
-
${PRIVATE_KEY_ENV} by default), and sends the signed \`interaction.json\`
|
|
28321
|
-
attached as an \`application/json\` part. The key never leaves your machine.
|
|
28646
|
+
THE ONE-COMMAND PAYER FLOW. You received a payment-request email carrying an
|
|
28647
|
+
\`interaction.json\` attachment. Run:
|
|
28322
28648
|
|
|
28323
|
-
|
|
28324
|
-
\`pay-email-step\` + \`send --attachment interaction.json\` dance: no manual
|
|
28325
|
-
address or threading wrangling. Use \`pay-email-step\` only when you need the
|
|
28326
|
-
signed bytes as a portable artifact without sending.
|
|
28649
|
+
<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id>
|
|
28327
28650
|
|
|
28328
|
-
|
|
28329
|
-
|
|
28330
|
-
|
|
28331
|
-
|
|
28332
|
-
|
|
28333
|
-
associates by interaction_id instead. Pass --from only to override the derived
|
|
28334
|
-
payer From.
|
|
28651
|
+
with your wallet key in ${PRIVATE_KEY_ENV}, and this command auto-derives the
|
|
28652
|
+
challenge from that inbound email's attachment, signs the interaction-bound
|
|
28653
|
+
EIP-3009 authorization locally, and sends the signed \`interaction.json\` back
|
|
28654
|
+
to the payee. The key never leaves your machine. You do NOT need --challenge or
|
|
28655
|
+
--challenge-file: with just --in-reply-to the challenge is derived for you.
|
|
28335
28656
|
|
|
28336
|
-
|
|
28337
|
-
|
|
28338
|
-
|
|
28657
|
+
THE CHALLENGE OBJECT (only needed for the --challenge / --challenge-file
|
|
28658
|
+
override). \`--challenge\` expects the object the PAYEE's
|
|
28659
|
+
\`create-email-challenge\` API returns, NOT the inbound email's
|
|
28660
|
+
\`interaction.json\` attachment (those are different shapes). Its fields are:
|
|
28661
|
+
|
|
28662
|
+
{
|
|
28663
|
+
"interaction_id": "<uuid@domain>", // the email thread id
|
|
28664
|
+
"challenge": {
|
|
28665
|
+
"payment_requirements": { ... }, // scheme, network, payTo, asset, ...
|
|
28666
|
+
"nonce_binding": {
|
|
28667
|
+
"interaction_id": "<uuid@domain>", // must equal the outer interaction_id
|
|
28668
|
+
"challenge_step_id": "<uuid>", // the challenge step id
|
|
28669
|
+
"challenge_nonce": "<64 hex chars>"
|
|
28670
|
+
},
|
|
28671
|
+
"expires_at": "<ISO-8601>"
|
|
28672
|
+
}
|
|
28673
|
+
}
|
|
28674
|
+
|
|
28675
|
+
As a real payer you usually have only the inbound email's attachment, whose
|
|
28676
|
+
layout differs (top-level \`step\`, \`step_id\`, \`expires_at\`, and a nested
|
|
28677
|
+
\`payload.payment_requirements\` / \`payload.challenge_nonce\`). You do NOT
|
|
28678
|
+
reshape it by hand: omit --challenge and let --in-reply-to derive it, or run
|
|
28679
|
+
\`payments challenge-from-email --id <inbound-id>\` to print the correctly
|
|
28680
|
+
shaped challenge object. Pass --challenge / --challenge-file only when you have
|
|
28681
|
+
the payee-side challenge from another source; it is used as-is and overrides
|
|
28682
|
+
the auto-derive.
|
|
28683
|
+
|
|
28684
|
+
SETTLEMENT IS ASYNC. --wait only confirms the receiving MTA accepted the
|
|
28685
|
+
message (SMTP 250); it does NOT confirm on-chain settlement. The \`settle_tx\`
|
|
28686
|
+
hash arrives later in a follow-up inbound x402 settlement interaction email
|
|
28687
|
+
from the payee. Use --wait-settle to poll for that settlement email after
|
|
28688
|
+
sending and surface the receipt.
|
|
28689
|
+
|
|
28690
|
+
ADDRESSING. --in-reply-to is the inbound challenge email's id. It is fetched to
|
|
28691
|
+
derive the recipient (the payee, from the inbound's sender) and the From (you,
|
|
28692
|
+
the payer, from the inbound's recipient). The outgoing send does NOT thread
|
|
28693
|
+
under the challenge: threading would trigger the send endpoint's parent-thread
|
|
28694
|
+
dedup and the payment would never settle. The interaction associates by
|
|
28695
|
+
interaction_id instead. Pass --from only to override the derived payer From.`;
|
|
28696
|
+
static summary = "Sign an email x402 challenge and send the signed interaction.json (one step; derives the challenge from --in-reply-to)";
|
|
28339
28697
|
static examples = [
|
|
28698
|
+
"<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id>",
|
|
28699
|
+
"<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id> --wait-settle",
|
|
28340
28700
|
"<%= config.bin %> payments pay-email --challenge-file challenge.json --in-reply-to <inbound-email-id>",
|
|
28341
|
-
"
|
|
28342
|
-
"<%= config.bin %> payments pay-email --challenge-file challenge.json --in-reply-to <inbound-email-id> --from 'Payer <payer@your-domain.example>'"
|
|
28701
|
+
"<%= config.bin %> payments pay-email --in-reply-to <inbound-email-id> --from 'Payer <payer@your-domain.example>'"
|
|
28343
28702
|
];
|
|
28344
28703
|
static flags = {
|
|
28345
28704
|
"api-key": Flags.string({
|
|
@@ -28356,26 +28715,37 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28356
28715
|
env: PRIVATE_KEY_ENV
|
|
28357
28716
|
}),
|
|
28358
28717
|
challenge: Flags.string({
|
|
28359
|
-
description: "
|
|
28718
|
+
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.",
|
|
28360
28719
|
exclusive: ["challenge-file"]
|
|
28361
28720
|
}),
|
|
28362
28721
|
"challenge-file": Flags.string({
|
|
28363
|
-
description: "
|
|
28722
|
+
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.",
|
|
28364
28723
|
exclusive: ["challenge"]
|
|
28365
28724
|
}),
|
|
28366
28725
|
"in-reply-to": Flags.string({
|
|
28367
|
-
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.",
|
|
28726
|
+
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.",
|
|
28368
28727
|
required: true
|
|
28369
28728
|
}),
|
|
28370
28729
|
from: Flags.string({ description: "Optional From header override. Defaults to the inbound's recipient (the payer the challenge was addressed to)." }),
|
|
28371
28730
|
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}".` }),
|
|
28372
|
-
wait: Flags.boolean({ description: "Block until the receiving MTA returns
|
|
28731
|
+
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." }),
|
|
28732
|
+
"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." }),
|
|
28733
|
+
"settle-timeout": Flags.integer({
|
|
28734
|
+
default: 180,
|
|
28735
|
+
description: "With --wait-settle: seconds to wait for the settlement email before giving up (0 waits forever).",
|
|
28736
|
+
min: 0
|
|
28737
|
+
}),
|
|
28738
|
+
"settle-interval": Flags.integer({
|
|
28739
|
+
default: 5,
|
|
28740
|
+
description: "With --wait-settle: seconds between settlement-email polls.",
|
|
28741
|
+
min: 1
|
|
28742
|
+
}),
|
|
28373
28743
|
json: Flags.boolean({ description: "Print a JSON object with the signed interaction step and the send result." }),
|
|
28374
28744
|
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
28375
28745
|
};
|
|
28376
28746
|
async run() {
|
|
28377
28747
|
const { flags } = await this.parse(PaymentsPayEmailCommand);
|
|
28378
|
-
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
28748
|
+
const { apiClient, auth, baseUrlOverridden, requestConfig } = await createAuthenticatedCliApiClient({
|
|
28379
28749
|
apiKey: flags["api-key"],
|
|
28380
28750
|
apiBaseUrl: flags["api-base-url"],
|
|
28381
28751
|
configDir: this.config.configDir
|
|
@@ -28386,22 +28756,6 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28386
28756
|
configDir: this.config.configDir
|
|
28387
28757
|
};
|
|
28388
28758
|
await runWithTiming(flags.time, async () => {
|
|
28389
|
-
let built;
|
|
28390
|
-
try {
|
|
28391
|
-
built = await signEmailChallenge({
|
|
28392
|
-
challenge: readEmailChallenge({
|
|
28393
|
-
inline: flags.challenge,
|
|
28394
|
-
file: flags["challenge-file"]
|
|
28395
|
-
}),
|
|
28396
|
-
privateKey: flags["private-key"] ?? "",
|
|
28397
|
-
resolvedApiBaseUrl: auth.apiBaseUrl,
|
|
28398
|
-
apiKey: flags["api-key"]
|
|
28399
|
-
});
|
|
28400
|
-
} catch (error) {
|
|
28401
|
-
reportX402Error(error, authFailureContext);
|
|
28402
|
-
process.exitCode = 1;
|
|
28403
|
-
return;
|
|
28404
|
-
}
|
|
28405
28759
|
const inboundResult = await getEmail({
|
|
28406
28760
|
client: apiClient.client,
|
|
28407
28761
|
path: { id: flags["in-reply-to"] },
|
|
@@ -28423,6 +28777,31 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28423
28777
|
process.exitCode = 1;
|
|
28424
28778
|
return;
|
|
28425
28779
|
}
|
|
28780
|
+
let built;
|
|
28781
|
+
try {
|
|
28782
|
+
let challenge;
|
|
28783
|
+
if (shouldDeriveChallenge(flags)) challenge = await deriveEmailChallengeFromInbound({
|
|
28784
|
+
baseUrl: auth.apiBaseUrl,
|
|
28785
|
+
emailId: flags["in-reply-to"],
|
|
28786
|
+
apiKey: auth.apiKey,
|
|
28787
|
+
headers: requestConfig.headers,
|
|
28788
|
+
attachments: inbound.parsed?.attachments
|
|
28789
|
+
});
|
|
28790
|
+
else challenge = readEmailChallenge({
|
|
28791
|
+
inline: flags.challenge,
|
|
28792
|
+
file: flags["challenge-file"]
|
|
28793
|
+
});
|
|
28794
|
+
built = await signEmailChallenge({
|
|
28795
|
+
challenge,
|
|
28796
|
+
privateKey: flags["private-key"] ?? "",
|
|
28797
|
+
resolvedApiBaseUrl: auth.apiBaseUrl,
|
|
28798
|
+
apiKey: flags["api-key"]
|
|
28799
|
+
});
|
|
28800
|
+
} catch (error) {
|
|
28801
|
+
reportX402Error(error, authFailureContext);
|
|
28802
|
+
process.exitCode = 1;
|
|
28803
|
+
return;
|
|
28804
|
+
}
|
|
28426
28805
|
const payeeTo = inbound.from_email;
|
|
28427
28806
|
const derivedPayerFrom = inbound.to_email || inbound.recipient;
|
|
28428
28807
|
const from = flags.from ?? derivedPayerFrom;
|
|
@@ -28436,6 +28815,7 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28436
28815
|
process.exitCode = 1;
|
|
28437
28816
|
return;
|
|
28438
28817
|
}
|
|
28818
|
+
const sendStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28439
28819
|
const result = await sendEmail({
|
|
28440
28820
|
body: {
|
|
28441
28821
|
from,
|
|
@@ -28463,13 +28843,40 @@ var PaymentsPayEmailCommand = class PaymentsPayEmailCommand extends Command {
|
|
|
28463
28843
|
if (replayed) writeIdempotentReplayBannerIfReplay(sent, { write: (chunk) => {
|
|
28464
28844
|
process.stderr.write(chunk);
|
|
28465
28845
|
} });
|
|
28846
|
+
let settlement = null;
|
|
28847
|
+
if (flags["wait-settle"] && !replayed && payeeTo) {
|
|
28848
|
+
process.stderr.write("Payment sent. Waiting for the x402 settlement interaction email (settlement is async)...\n");
|
|
28849
|
+
settlement = await pollForSettlementInteraction({
|
|
28850
|
+
apiClient,
|
|
28851
|
+
baseUrl: auth.apiBaseUrl,
|
|
28852
|
+
apiKey: auth.apiKey,
|
|
28853
|
+
headers: requestConfig.headers,
|
|
28854
|
+
interactionId: built.envelope.interaction_id,
|
|
28855
|
+
payeeFrom: payeeTo,
|
|
28856
|
+
since: sendStartedAt,
|
|
28857
|
+
timeoutSeconds: flags["settle-timeout"],
|
|
28858
|
+
intervalSeconds: flags["settle-interval"]
|
|
28859
|
+
});
|
|
28860
|
+
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");
|
|
28861
|
+
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");
|
|
28862
|
+
}
|
|
28466
28863
|
if (flags.json) this.log(JSON.stringify({
|
|
28467
28864
|
interaction: built.envelope,
|
|
28468
28865
|
sent,
|
|
28469
|
-
idempotent_replay: replayed
|
|
28866
|
+
idempotent_replay: replayed,
|
|
28867
|
+
...flags["wait-settle"] ? { settlement: settlement ? {
|
|
28868
|
+
email_id: settlement.emailId,
|
|
28869
|
+
settle_tx: settlement.settleTx,
|
|
28870
|
+
receipt: settlement.envelope
|
|
28871
|
+
} : null } : {}
|
|
28470
28872
|
}, null, 2));
|
|
28471
|
-
else
|
|
28873
|
+
else {
|
|
28874
|
+
this.log(JSON.stringify(sent, null, 2));
|
|
28875
|
+
if (!flags["wait-settle"]) process.stderr.write(`${settlementWaitNotice}\n`);
|
|
28876
|
+
else if (settlement) this.log(JSON.stringify(settlement.envelope, null, 2));
|
|
28877
|
+
}
|
|
28472
28878
|
if (replayed) process.exitCode = 1;
|
|
28879
|
+
else if (flags["wait-settle"] && !settlement) process.exitCode = 1;
|
|
28473
28880
|
});
|
|
28474
28881
|
}
|
|
28475
28882
|
};
|
|
@@ -30087,7 +30494,8 @@ const COMMANDS = {
|
|
|
30087
30494
|
"payments:pay-challenge": PaymentsPayCommand,
|
|
30088
30495
|
"payments:pay": PaymentsPayCommand,
|
|
30089
30496
|
"payments:pay-email": PaymentsPayEmailCommand,
|
|
30090
|
-
"payments:pay-email-step": PaymentsPayEmailStepCommand
|
|
30497
|
+
"payments:pay-email-step": PaymentsPayEmailStepCommand,
|
|
30498
|
+
"payments:challenge-from-email": PaymentsChallengeFromEmailCommand
|
|
30091
30499
|
};
|
|
30092
30500
|
//#endregion
|
|
30093
30501
|
export { CANONICAL_OPERATION_ALIASES, COMMANDS, lookupOperation };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primcli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
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,
|