primcli 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/oclif/index.js +417 -3
- package/package.json +1 -1
package/dist/oclif/index.js
CHANGED
|
@@ -7,7 +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 {
|
|
10
|
+
import { concat, getAddress, hexToBytes, keccak256, stringToBytes } from "viem";
|
|
11
11
|
import { privateKeyToAccount } from "viem/accounts";
|
|
12
12
|
//#region \0rolldown/runtime.js
|
|
13
13
|
var __defProp = Object.defineProperty;
|
|
@@ -21020,8 +21020,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
21020
21020
|
name: "Primitive Team",
|
|
21021
21021
|
url: "https://primitive.dev"
|
|
21022
21022
|
};
|
|
21023
|
-
const SDK_VERSION_RANGE = "^1.
|
|
21024
|
-
const CLI_VERSION_RANGE = "^1.
|
|
21023
|
+
const SDK_VERSION_RANGE = "^1.7.0";
|
|
21024
|
+
const CLI_VERSION_RANGE = "^1.7.0";
|
|
21025
21025
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
21026
21026
|
function renderHandler() {
|
|
21027
21027
|
return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
|
|
@@ -24411,6 +24411,420 @@ var OrgSecretsSetCommand = class OrgSecretsSetCommand extends Command {
|
|
|
24411
24411
|
}
|
|
24412
24412
|
};
|
|
24413
24413
|
//#endregion
|
|
24414
|
+
//#region ../sdk-node/dist/x402/index.js
|
|
24415
|
+
/**
|
|
24416
|
+
* x402 client-side signing.
|
|
24417
|
+
*
|
|
24418
|
+
* The payer signs an EIP-3009 `transferWithAuthorization` over the customer's
|
|
24419
|
+
* own key; the key never leaves them. This module derives the interaction-bound
|
|
24420
|
+
* nonce and assembles the EIP-712 typed data and the wire payload. The byte
|
|
24421
|
+
* layout here MUST match the platform verifier exactly; a normative test vector
|
|
24422
|
+
* (see sign.test.ts) locks the nonce derivation to the same value the server
|
|
24423
|
+
* recomputes.
|
|
24424
|
+
*/
|
|
24425
|
+
/** A challenge nonce is 32 bytes rendered as 64 lowercase hex chars, no 0x. */
|
|
24426
|
+
const CHALLENGE_NONCE_RE = /^[0-9a-f]{64}$/;
|
|
24427
|
+
/** Single-byte domain separator between the variable-length string fields. */
|
|
24428
|
+
const FIELD_SEPARATOR = new Uint8Array([0]);
|
|
24429
|
+
/**
|
|
24430
|
+
* Derive the EIP-3009 nonce bound to a specific interaction step:
|
|
24431
|
+
*
|
|
24432
|
+
* keccak256( utf8(lower(interaction_id)) || 0x00
|
|
24433
|
+
* || utf8(lower(challenge_step_id)) || 0x00
|
|
24434
|
+
* || hexdecode(challenge_nonce) )
|
|
24435
|
+
*
|
|
24436
|
+
* The `0x00` separators pin the field boundaries (undelimited concatenation of
|
|
24437
|
+
* variable-length strings is collision-ambiguous), and the challenge nonce is
|
|
24438
|
+
* decoded to its 32 raw bytes before hashing. The platform recomputes this and
|
|
24439
|
+
* rejects a mismatch.
|
|
24440
|
+
*/
|
|
24441
|
+
function deriveEip3009Nonce(input) {
|
|
24442
|
+
if (!CHALLENGE_NONCE_RE.test(input.challengeNonce)) throw new Error("challengeNonce must be exactly 64 lowercase hex chars (32 bytes), no 0x prefix");
|
|
24443
|
+
return keccak256(concat([
|
|
24444
|
+
stringToBytes(input.interactionId.toLowerCase()),
|
|
24445
|
+
FIELD_SEPARATOR,
|
|
24446
|
+
stringToBytes(input.challengeStepId.toLowerCase()),
|
|
24447
|
+
FIELD_SEPARATOR,
|
|
24448
|
+
hexToBytes(`0x${input.challengeNonce}`)
|
|
24449
|
+
]));
|
|
24450
|
+
}
|
|
24451
|
+
/**
|
|
24452
|
+
* The EIP-3009 `TransferWithAuthorization` EIP-712 type. The field order and
|
|
24453
|
+
* types are part of the on-chain contract and MUST NOT change.
|
|
24454
|
+
*/
|
|
24455
|
+
const TRANSFER_WITH_AUTHORIZATION_TYPES = { TransferWithAuthorization: [
|
|
24456
|
+
{
|
|
24457
|
+
name: "from",
|
|
24458
|
+
type: "address"
|
|
24459
|
+
},
|
|
24460
|
+
{
|
|
24461
|
+
name: "to",
|
|
24462
|
+
type: "address"
|
|
24463
|
+
},
|
|
24464
|
+
{
|
|
24465
|
+
name: "value",
|
|
24466
|
+
type: "uint256"
|
|
24467
|
+
},
|
|
24468
|
+
{
|
|
24469
|
+
name: "validAfter",
|
|
24470
|
+
type: "uint256"
|
|
24471
|
+
},
|
|
24472
|
+
{
|
|
24473
|
+
name: "validBefore",
|
|
24474
|
+
type: "uint256"
|
|
24475
|
+
},
|
|
24476
|
+
{
|
|
24477
|
+
name: "nonce",
|
|
24478
|
+
type: "bytes32"
|
|
24479
|
+
}
|
|
24480
|
+
] };
|
|
24481
|
+
function transferWithAuthorizationTypedData(domain, auth) {
|
|
24482
|
+
return {
|
|
24483
|
+
domain: {
|
|
24484
|
+
name: domain.name,
|
|
24485
|
+
version: domain.version,
|
|
24486
|
+
chainId: domain.chainId,
|
|
24487
|
+
verifyingContract: domain.verifyingContract
|
|
24488
|
+
},
|
|
24489
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
24490
|
+
primaryType: "TransferWithAuthorization",
|
|
24491
|
+
message: auth
|
|
24492
|
+
};
|
|
24493
|
+
}
|
|
24494
|
+
/**
|
|
24495
|
+
* Build the payout-address ownership message. This MUST be byte-identical to the
|
|
24496
|
+
* platform's `buildPayoutRegistrationMessage`, or registration fails the
|
|
24497
|
+
* ownership proof. The org id is in the signed bytes, so a captured signature
|
|
24498
|
+
* can never register the address under a different org.
|
|
24499
|
+
*/
|
|
24500
|
+
function buildPayoutRegistrationMessage(input) {
|
|
24501
|
+
return [
|
|
24502
|
+
"Primitive x402 payout address authorization",
|
|
24503
|
+
"",
|
|
24504
|
+
"I authorize this address as a payout destination for my Primitive organization.",
|
|
24505
|
+
"",
|
|
24506
|
+
`org: ${input.org}`,
|
|
24507
|
+
`address: ${input.address.toLowerCase()}`,
|
|
24508
|
+
`network: ${input.network}`,
|
|
24509
|
+
`issued: ${input.issuedAt}`
|
|
24510
|
+
].join("\n");
|
|
24511
|
+
}
|
|
24512
|
+
/** Assemble the wire payload from a signed authorization. */
|
|
24513
|
+
function toPaymentPayload(network, auth, signature) {
|
|
24514
|
+
return {
|
|
24515
|
+
x402Version: 1,
|
|
24516
|
+
scheme: "exact",
|
|
24517
|
+
network,
|
|
24518
|
+
payload: {
|
|
24519
|
+
signature,
|
|
24520
|
+
authorization: {
|
|
24521
|
+
from: auth.from,
|
|
24522
|
+
to: auth.to,
|
|
24523
|
+
value: auth.value.toString(),
|
|
24524
|
+
validAfter: auth.validAfter.toString(),
|
|
24525
|
+
validBefore: auth.validBefore.toString(),
|
|
24526
|
+
nonce: auth.nonce
|
|
24527
|
+
}
|
|
24528
|
+
}
|
|
24529
|
+
};
|
|
24530
|
+
}
|
|
24531
|
+
/**
|
|
24532
|
+
* Compute the EIP-3009 validity window for a payment. `validBefore` is the
|
|
24533
|
+
* value that governs on-chain validity, so it MUST cover the challenge's
|
|
24534
|
+
* `expires_at` plus a settlement margin; `validAfter` is set generously in the
|
|
24535
|
+
* past for clock skew. The total window is hard-capped at `maxWindowSec` so
|
|
24536
|
+
* neither a far-future `challengeExpiresAtSec` nor a widened margin can produce
|
|
24537
|
+
* a window the platform verifier would later reject.
|
|
24538
|
+
*/
|
|
24539
|
+
function computePaymentValidityWindow(params) {
|
|
24540
|
+
const margin = params.settlementMarginSec ?? 300;
|
|
24541
|
+
const skew = params.clockSkewSec ?? 300;
|
|
24542
|
+
const validBefore = BigInt(params.challengeExpiresAtSec + margin);
|
|
24543
|
+
const validAfter = BigInt(params.nowSec - skew);
|
|
24544
|
+
if (validBefore <= validAfter) throw new Error("invalid validity window: validBefore must be after validAfter (challenge already expired?)");
|
|
24545
|
+
const maxWindow = BigInt(params.maxWindowSec ?? 86400);
|
|
24546
|
+
if (validBefore - validAfter > maxWindow) throw new Error(`invalid validity window: total window exceeds the ${maxWindow}s cap (challenge expiry too far out?)`);
|
|
24547
|
+
return {
|
|
24548
|
+
validAfter,
|
|
24549
|
+
validBefore
|
|
24550
|
+
};
|
|
24551
|
+
}
|
|
24552
|
+
/**
|
|
24553
|
+
* The interaction-aware signer: derive the bound nonce, assemble the
|
|
24554
|
+
* authorization, and sign it. This is the one piece a stock x402 signer cannot
|
|
24555
|
+
* do (it generates the nonce internally with no injection point), so the payer
|
|
24556
|
+
* side needs this Primitive-provided helper. The key never leaves the caller.
|
|
24557
|
+
*/
|
|
24558
|
+
async function signInteractionPayment(params) {
|
|
24559
|
+
const authorization = {
|
|
24560
|
+
from: getAddress(params.payer),
|
|
24561
|
+
to: getAddress(params.payTo),
|
|
24562
|
+
value: params.amount,
|
|
24563
|
+
validAfter: params.validAfter,
|
|
24564
|
+
validBefore: params.validBefore,
|
|
24565
|
+
nonce: deriveEip3009Nonce(params.nonceBinding)
|
|
24566
|
+
};
|
|
24567
|
+
return {
|
|
24568
|
+
authorization,
|
|
24569
|
+
signature: await params.sign(transferWithAuthorizationTypedData(params.domain, authorization))
|
|
24570
|
+
};
|
|
24571
|
+
}
|
|
24572
|
+
/** The authorization nonce is 32 bytes rendered as a 0x-prefixed 64-char hex string. */
|
|
24573
|
+
const NONCE_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
24574
|
+
/** A shape-valid EIP signature is 65 bytes (r,s,v) rendered as 130 hex chars. */
|
|
24575
|
+
const SIGNATURE_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
24576
|
+
/**
|
|
24577
|
+
* Assemble (and validate) the exact-EVM x402 wire payload from an
|
|
24578
|
+
* interaction-bound, locally-signed authorization. The numeric authorization
|
|
24579
|
+
* fields are decimal strings in the wire schema, so the bigints are stringified
|
|
24580
|
+
* here; the nonce passes through as hex. Validation rejects a malformed nonce or
|
|
24581
|
+
* signature loudly rather than emitting a payload the platform will reject.
|
|
24582
|
+
*/
|
|
24583
|
+
function buildExactEvmPaymentPayload(params) {
|
|
24584
|
+
if (params.network !== "base" && params.network !== "base-sepolia") throw new Error(`buildExactEvmPaymentPayload: unsupported network ${params.network}`);
|
|
24585
|
+
if (!SIGNATURE_HEX_RE.test(params.signature)) throw new Error("buildExactEvmPaymentPayload: signature must be a 0x-prefixed 65-byte (130 hex char) EIP signature");
|
|
24586
|
+
if (!NONCE_HEX_RE.test(params.authorization.nonce)) throw new Error("buildExactEvmPaymentPayload: authorization.nonce must be a 0x-prefixed 32-byte (64 hex char) value");
|
|
24587
|
+
return toPaymentPayload(params.network, params.authorization, params.signature);
|
|
24588
|
+
}
|
|
24589
|
+
const CHAIN_IDS = {
|
|
24590
|
+
"base-sepolia": 84532,
|
|
24591
|
+
base: 8453
|
|
24592
|
+
};
|
|
24593
|
+
const DEFAULT_BASE_URL = "https://api.primitive.dev";
|
|
24594
|
+
const CHARGE_INPUT_KEYS = {
|
|
24595
|
+
amount: true,
|
|
24596
|
+
amountUsdc: true,
|
|
24597
|
+
network: true,
|
|
24598
|
+
payerOrg: true,
|
|
24599
|
+
description: true,
|
|
24600
|
+
resource: true,
|
|
24601
|
+
expiresIn: true,
|
|
24602
|
+
idempotencyKey: true
|
|
24603
|
+
};
|
|
24604
|
+
function usdcToBaseUnits$1(human) {
|
|
24605
|
+
const trimmed = human.trim();
|
|
24606
|
+
if (!/^\d+(\.\d+)?$/.test(trimmed)) return null;
|
|
24607
|
+
const [whole, frac = ""] = trimmed.split(".");
|
|
24608
|
+
if (frac.length > 6) return null;
|
|
24609
|
+
const base = BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
|
|
24610
|
+
return base > 0n ? base.toString() : null;
|
|
24611
|
+
}
|
|
24612
|
+
var X402Error = class extends Error {
|
|
24613
|
+
/** HTTP status, or 0 for a client-side / transport error that never reached the server. */
|
|
24614
|
+
status;
|
|
24615
|
+
body;
|
|
24616
|
+
/** The `Retry-After` response header, if the server sent one. */
|
|
24617
|
+
retryAfter;
|
|
24618
|
+
constructor(message, status, body, options) {
|
|
24619
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
24620
|
+
this.name = "X402Error";
|
|
24621
|
+
this.status = status;
|
|
24622
|
+
this.body = body;
|
|
24623
|
+
this.retryAfter = options?.retryAfter ?? null;
|
|
24624
|
+
}
|
|
24625
|
+
};
|
|
24626
|
+
/**
|
|
24627
|
+
* Assert a challenge is fully hydrated before signing, so a missing field fails
|
|
24628
|
+
* with a named X402Error instead of an opaque viem/BigInt error mid-sign.
|
|
24629
|
+
*/
|
|
24630
|
+
function validateChallenge(c) {
|
|
24631
|
+
const bad = (field) => {
|
|
24632
|
+
throw new X402Error(`challenge is missing or malformed: ${field}`, 0);
|
|
24633
|
+
};
|
|
24634
|
+
if (!c || typeof c !== "object") bad("challenge");
|
|
24635
|
+
if (!c.id) bad("id");
|
|
24636
|
+
if (!c.network) bad("network");
|
|
24637
|
+
if (!c.expires_at) bad("expires_at");
|
|
24638
|
+
const nb = c.nonce_binding;
|
|
24639
|
+
if (!nb?.interaction_id || !nb.challenge_step_id || !nb.challenge_nonce) bad("nonce_binding");
|
|
24640
|
+
const pr = c.payment_requirements;
|
|
24641
|
+
if (!pr) bad("payment_requirements");
|
|
24642
|
+
if (!/^[1-9][0-9]{0,38}$/.test(pr.maxAmountRequired ?? "")) bad("payment_requirements.maxAmountRequired (expected a positive integer string in token base units)");
|
|
24643
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(pr.payTo ?? "")) bad("payment_requirements.payTo (expected a 0x address)");
|
|
24644
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(pr.asset ?? "")) bad("payment_requirements.asset (expected a 0x address)");
|
|
24645
|
+
if (!pr.extra?.name || !pr.extra.version) bad("payment_requirements.extra (name/version)");
|
|
24646
|
+
}
|
|
24647
|
+
var X402Client = class {
|
|
24648
|
+
#apiKey;
|
|
24649
|
+
#baseUrl;
|
|
24650
|
+
#fetch;
|
|
24651
|
+
#timeoutMs;
|
|
24652
|
+
constructor(options = {}) {
|
|
24653
|
+
this.#apiKey = options.apiKey ?? process.env.PRIMITIVE_API_KEY ?? "";
|
|
24654
|
+
this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
24655
|
+
this.#fetch = options.fetch ?? fetch;
|
|
24656
|
+
this.#timeoutMs = options.timeoutMs ?? 3e4;
|
|
24657
|
+
}
|
|
24658
|
+
async #request(method, path, body, init) {
|
|
24659
|
+
if (!this.#apiKey) throw new X402Error("no API key configured; set PRIMITIVE_API_KEY or pass { apiKey } to the client", 0);
|
|
24660
|
+
const timeout = AbortSignal.timeout(this.#timeoutMs);
|
|
24661
|
+
const signal = init?.signal ? AbortSignal.any([timeout, init.signal]) : timeout;
|
|
24662
|
+
let res;
|
|
24663
|
+
try {
|
|
24664
|
+
res = await this.#fetch(`${this.#baseUrl}${path}`, {
|
|
24665
|
+
method,
|
|
24666
|
+
headers: {
|
|
24667
|
+
authorization: `Bearer ${this.#apiKey}`,
|
|
24668
|
+
"content-type": "application/json",
|
|
24669
|
+
...init?.headers
|
|
24670
|
+
},
|
|
24671
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
24672
|
+
signal
|
|
24673
|
+
});
|
|
24674
|
+
} catch (cause) {
|
|
24675
|
+
throw new X402Error(cause instanceof Error && cause.name === "TimeoutError" ? `request to ${path} timed out after ${this.#timeoutMs}ms` : `request to ${path} failed: ${cause instanceof Error ? cause.message : String(cause)}`, 0, void 0, { cause });
|
|
24676
|
+
}
|
|
24677
|
+
const retryAfter = res.headers.get("retry-after");
|
|
24678
|
+
const text = await res.text().catch(() => "");
|
|
24679
|
+
let json;
|
|
24680
|
+
if (text) try {
|
|
24681
|
+
json = JSON.parse(text);
|
|
24682
|
+
} catch {
|
|
24683
|
+
throw new X402Error(`non-JSON response (${res.status}) from ${path}: ${text.slice(0, 200)}`, res.status, text.slice(0, 500), { retryAfter });
|
|
24684
|
+
}
|
|
24685
|
+
if (!res.ok || json?.success === false) throw new X402Error(json?.error?.message ?? `request failed with ${res.status}`, res.status, json ?? text.slice(0, 500), { retryAfter });
|
|
24686
|
+
if (!json || json.success !== true || json.data === void 0) throw new X402Error(`unexpected response shape (${res.status}) from ${path}: missing success/data envelope`, res.status, json ?? text.slice(0, 500), { retryAfter });
|
|
24687
|
+
return json.data;
|
|
24688
|
+
}
|
|
24689
|
+
/** Request a payment (payee side). Returns the challenge to hand to the payer. */
|
|
24690
|
+
async charge(input) {
|
|
24691
|
+
for (const key of Object.keys(input)) if (!(key in CHARGE_INPUT_KEYS)) throw new X402Error(`unknown charge() option "${key}"; expected one of: ${Object.keys(CHARGE_INPUT_KEYS).join(", ")}`, 0);
|
|
24692
|
+
if (input.amount !== void 0 && input.amountUsdc !== void 0) throw new X402Error("charge() takes exactly one of `amount` (base units) or `amountUsdc` (human USDC), not both", 0);
|
|
24693
|
+
const amount = input.amountUsdc !== void 0 ? usdcToBaseUnits$1(input.amountUsdc) : input.amount ?? null;
|
|
24694
|
+
if (!amount || !/^[1-9][0-9]{0,38}$/.test(amount)) throw new X402Error("charge() requires `amount` as a positive integer string in token base units (e.g. \"10000\"), or `amountUsdc` as a positive USDC amount with at most 6 decimals (e.g. \"0.01\")", 0);
|
|
24695
|
+
const body = {
|
|
24696
|
+
amount,
|
|
24697
|
+
network: input.network ?? "base-sepolia"
|
|
24698
|
+
};
|
|
24699
|
+
if (input.payerOrg) body.payer_org = input.payerOrg;
|
|
24700
|
+
if (input.description) body.description = input.description;
|
|
24701
|
+
if (input.resource) body.resource = input.resource;
|
|
24702
|
+
if (input.expiresIn !== void 0) body.expires_in = input.expiresIn;
|
|
24703
|
+
return this.#request("POST", "/v1/x402/challenges", body, { headers: input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0 });
|
|
24704
|
+
}
|
|
24705
|
+
/**
|
|
24706
|
+
* Pay a challenge (payer side). Derives the interaction-bound authorization,
|
|
24707
|
+
* signs it locally with the caller's key, and submits it for settlement.
|
|
24708
|
+
*/
|
|
24709
|
+
async pay(challenge, options) {
|
|
24710
|
+
if (!options?.signer?.address || typeof options.signer.signTypedData !== "function") throw new X402Error("pay() requires options.signer with { address, signTypedData } (e.g. a viem LocalAccount)", 0);
|
|
24711
|
+
validateChallenge(challenge);
|
|
24712
|
+
const chainId = CHAIN_IDS[challenge.network];
|
|
24713
|
+
if (chainId === void 0) throw new X402Error(`unsupported network: ${challenge.network}`, 0);
|
|
24714
|
+
const pr = challenge.payment_requirements;
|
|
24715
|
+
if (pr.network !== challenge.network) throw new X402Error(`challenge network mismatch: ${challenge.network} vs payment_requirements ${pr.network}`, 0);
|
|
24716
|
+
if (pr.scheme !== "exact") throw new X402Error(`unsupported payment scheme: ${pr.scheme}`, 0);
|
|
24717
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
24718
|
+
const expiresAtMs = Date.parse(challenge.expires_at);
|
|
24719
|
+
if (Number.isNaN(expiresAtMs)) throw new X402Error(`challenge has an invalid expires_at: ${challenge.expires_at}`, 0);
|
|
24720
|
+
const expiresAtSec = Math.floor(expiresAtMs / 1e3);
|
|
24721
|
+
if (expiresAtSec <= nowSec) throw new X402Error(`challenge has already expired (expires_at ${challenge.expires_at}); not signing`, 0);
|
|
24722
|
+
let validAfter;
|
|
24723
|
+
let validBefore;
|
|
24724
|
+
try {
|
|
24725
|
+
({validAfter, validBefore} = computePaymentValidityWindow({
|
|
24726
|
+
challengeExpiresAtSec: expiresAtSec,
|
|
24727
|
+
nowSec
|
|
24728
|
+
}));
|
|
24729
|
+
} catch (cause) {
|
|
24730
|
+
throw new X402Error(cause instanceof Error ? cause.message : String(cause), 0, void 0, { cause });
|
|
24731
|
+
}
|
|
24732
|
+
const { authorization, signature } = await signInteractionPayment({
|
|
24733
|
+
sign: (typedData) => options.signer.signTypedData(typedData),
|
|
24734
|
+
payer: options.signer.address,
|
|
24735
|
+
domain: {
|
|
24736
|
+
name: pr.extra.name,
|
|
24737
|
+
version: pr.extra.version,
|
|
24738
|
+
chainId,
|
|
24739
|
+
verifyingContract: pr.asset
|
|
24740
|
+
},
|
|
24741
|
+
payTo: pr.payTo,
|
|
24742
|
+
amount: BigInt(pr.maxAmountRequired),
|
|
24743
|
+
nonceBinding: {
|
|
24744
|
+
interactionId: challenge.nonce_binding.interaction_id,
|
|
24745
|
+
challengeStepId: challenge.nonce_binding.challenge_step_id,
|
|
24746
|
+
challengeNonce: challenge.nonce_binding.challenge_nonce
|
|
24747
|
+
},
|
|
24748
|
+
validAfter,
|
|
24749
|
+
validBefore
|
|
24750
|
+
});
|
|
24751
|
+
return this.#request("POST", `/v1/x402/challenges/${challenge.id}/pay`, { payment: buildExactEvmPaymentPayload({
|
|
24752
|
+
network: challenge.network,
|
|
24753
|
+
authorization,
|
|
24754
|
+
signature
|
|
24755
|
+
}) });
|
|
24756
|
+
}
|
|
24757
|
+
/** Fetch a challenge by id (scoped to the challenger org that created it). */
|
|
24758
|
+
async getChallenge(id) {
|
|
24759
|
+
if (!id) throw new X402Error("getChallenge() requires a challenge id", 0);
|
|
24760
|
+
return this.#request("GET", `/v1/x402/challenges/${encodeURIComponent(id)}`);
|
|
24761
|
+
}
|
|
24762
|
+
/** Resolve the caller's own organization id from the account endpoint. */
|
|
24763
|
+
async #resolveOrgId() {
|
|
24764
|
+
const account = await this.#request("GET", "/v1/account");
|
|
24765
|
+
if (!account?.id) throw new X402Error("could not resolve your organization id from /v1/account; pass { org } explicitly", 0);
|
|
24766
|
+
return account.id;
|
|
24767
|
+
}
|
|
24768
|
+
/**
|
|
24769
|
+
* Register a payout address for your org (payee side). The signer proves
|
|
24770
|
+
* control of its own address with an org-bound `personal_sign`; the proven
|
|
24771
|
+
* address becomes (or updates to) the default payout destination for the
|
|
24772
|
+
* network. `charge()` resolves its `pay_to` from this directory, so a payee
|
|
24773
|
+
* must register before requesting payments.
|
|
24774
|
+
*
|
|
24775
|
+
* `org` is optional: when omitted it is resolved from your authenticated
|
|
24776
|
+
* account, so most callers never need to supply it.
|
|
24777
|
+
*/
|
|
24778
|
+
async registerPayoutAddress(input, options) {
|
|
24779
|
+
if (typeof options?.signer?.signMessage !== "function") throw new X402Error("registerPayoutAddress() requires a signer with signMessage (e.g. a viem LocalAccount)", 0);
|
|
24780
|
+
const org = input.org ?? await this.#resolveOrgId();
|
|
24781
|
+
const network = input.network ?? "base-sepolia";
|
|
24782
|
+
const issuedAt = input.issuedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
24783
|
+
const address = options.signer.address;
|
|
24784
|
+
const message = buildPayoutRegistrationMessage({
|
|
24785
|
+
org,
|
|
24786
|
+
address,
|
|
24787
|
+
network,
|
|
24788
|
+
issuedAt
|
|
24789
|
+
});
|
|
24790
|
+
const signature = await options.signer.signMessage({ message });
|
|
24791
|
+
return this.#request("POST", "/v1/x402/payout-addresses", {
|
|
24792
|
+
address,
|
|
24793
|
+
network,
|
|
24794
|
+
signature,
|
|
24795
|
+
issued_at: issuedAt,
|
|
24796
|
+
...input.label !== void 0 ? { label: input.label } : {}
|
|
24797
|
+
});
|
|
24798
|
+
}
|
|
24799
|
+
/** List your org's registered payout addresses. */
|
|
24800
|
+
async listPayoutAddresses() {
|
|
24801
|
+
return this.#request("GET", "/v1/x402/payout-addresses");
|
|
24802
|
+
}
|
|
24803
|
+
/**
|
|
24804
|
+
* List the most recent payments your org's spend policy declined (newest
|
|
24805
|
+
* first). Use this to see why an outbound payment was refused.
|
|
24806
|
+
*/
|
|
24807
|
+
async listDeclinedPayments() {
|
|
24808
|
+
return this.#request("GET", "/v1/x402/declined-payments");
|
|
24809
|
+
}
|
|
24810
|
+
/** Read your org's spend policy (kill-switch + caps + allowlist). */
|
|
24811
|
+
async getSpendPolicy() {
|
|
24812
|
+
return this.#request("GET", "/v1/x402/spend-policy");
|
|
24813
|
+
}
|
|
24814
|
+
/**
|
|
24815
|
+
* Update your org's spend policy. The endpoint is a PUT, but the server
|
|
24816
|
+
* applies it as a merge: only the fields you include are changed and omitted
|
|
24817
|
+
* fields keep their current value, so a partial update can't silently reset
|
|
24818
|
+
* the kill-switch. Pass `null` to clear a cap.
|
|
24819
|
+
*/
|
|
24820
|
+
async setSpendPolicy(update) {
|
|
24821
|
+
return this.#request("PUT", "/v1/x402/spend-policy", update);
|
|
24822
|
+
}
|
|
24823
|
+
};
|
|
24824
|
+
function createX402Client(options = {}) {
|
|
24825
|
+
return new X402Client(options);
|
|
24826
|
+
}
|
|
24827
|
+
//#endregion
|
|
24414
24828
|
//#region src/oclif/commands/payments-shared.ts
|
|
24415
24829
|
const PRIVATE_KEY_ENV = "PRIMITIVE_X402_PRIVATE_KEY";
|
|
24416
24830
|
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.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primcli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|