primcli 1.6.0 → 1.6.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.
Files changed (2) hide show
  1. package/dist/oclif/index.js +347 -1
  2. package/package.json +1 -1
@@ -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 { X402Error, createX402Client } from "@primitivedotdev/sdk/x402";
10
+ import { concat, hexToBytes, keccak256, stringToBytes } from "viem";
11
11
  import { privateKeyToAccount } from "viem/accounts";
12
12
  //#region \0rolldown/runtime.js
13
13
  var __defProp = Object.defineProperty;
@@ -24411,6 +24411,352 @@ 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
+ const CHAIN_IDS = {
24532
+ "base-sepolia": 84532,
24533
+ base: 8453
24534
+ };
24535
+ const CLOCK_SKEW_SEC = 300;
24536
+ const SETTLEMENT_MARGIN_SEC = 300;
24537
+ const DEFAULT_BASE_URL = "https://api.primitive.dev";
24538
+ const CHARGE_INPUT_KEYS = {
24539
+ amount: true,
24540
+ amountUsdc: true,
24541
+ network: true,
24542
+ payerOrg: true,
24543
+ description: true,
24544
+ resource: true,
24545
+ expiresIn: true,
24546
+ idempotencyKey: true
24547
+ };
24548
+ function usdcToBaseUnits$1(human) {
24549
+ const trimmed = human.trim();
24550
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) return null;
24551
+ const [whole, frac = ""] = trimmed.split(".");
24552
+ if (frac.length > 6) return null;
24553
+ const base = BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
24554
+ return base > 0n ? base.toString() : null;
24555
+ }
24556
+ var X402Error = class extends Error {
24557
+ /** HTTP status, or 0 for a client-side / transport error that never reached the server. */
24558
+ status;
24559
+ body;
24560
+ /** The `Retry-After` response header, if the server sent one. */
24561
+ retryAfter;
24562
+ constructor(message, status, body, options) {
24563
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
24564
+ this.name = "X402Error";
24565
+ this.status = status;
24566
+ this.body = body;
24567
+ this.retryAfter = options?.retryAfter ?? null;
24568
+ }
24569
+ };
24570
+ /**
24571
+ * Assert a challenge is fully hydrated before signing, so a missing field fails
24572
+ * with a named X402Error instead of an opaque viem/BigInt error mid-sign.
24573
+ */
24574
+ function validateChallenge(c) {
24575
+ const bad = (field) => {
24576
+ throw new X402Error(`challenge is missing or malformed: ${field}`, 0);
24577
+ };
24578
+ if (!c || typeof c !== "object") bad("challenge");
24579
+ if (!c.id) bad("id");
24580
+ if (!c.network) bad("network");
24581
+ if (!c.expires_at) bad("expires_at");
24582
+ const nb = c.nonce_binding;
24583
+ if (!nb?.interaction_id || !nb.challenge_step_id || !nb.challenge_nonce) bad("nonce_binding");
24584
+ const pr = c.payment_requirements;
24585
+ if (!pr) bad("payment_requirements");
24586
+ if (!pr.maxAmountRequired) bad("payment_requirements.maxAmountRequired");
24587
+ if (!/^0x[0-9a-fA-F]{40}$/.test(pr.payTo ?? "")) bad("payment_requirements.payTo (expected a 0x address)");
24588
+ if (!/^0x[0-9a-fA-F]{40}$/.test(pr.asset ?? "")) bad("payment_requirements.asset (expected a 0x address)");
24589
+ if (!pr.extra?.name || !pr.extra.version) bad("payment_requirements.extra (name/version)");
24590
+ }
24591
+ var X402Client = class {
24592
+ #apiKey;
24593
+ #baseUrl;
24594
+ #fetch;
24595
+ #timeoutMs;
24596
+ constructor(options = {}) {
24597
+ this.#apiKey = options.apiKey ?? process.env.PRIMITIVE_API_KEY ?? "";
24598
+ this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
24599
+ this.#fetch = options.fetch ?? fetch;
24600
+ this.#timeoutMs = options.timeoutMs ?? 3e4;
24601
+ }
24602
+ async #request(method, path, body, init) {
24603
+ if (!this.#apiKey) throw new X402Error("no API key configured; set PRIMITIVE_API_KEY or pass { apiKey } to the client", 0);
24604
+ const timeout = AbortSignal.timeout(this.#timeoutMs);
24605
+ const signal = init?.signal ? AbortSignal.any([timeout, init.signal]) : timeout;
24606
+ let res;
24607
+ try {
24608
+ res = await this.#fetch(`${this.#baseUrl}${path}`, {
24609
+ method,
24610
+ headers: {
24611
+ authorization: `Bearer ${this.#apiKey}`,
24612
+ "content-type": "application/json",
24613
+ ...init?.headers
24614
+ },
24615
+ body: body === void 0 ? void 0 : JSON.stringify(body),
24616
+ signal
24617
+ });
24618
+ } catch (cause) {
24619
+ 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 });
24620
+ }
24621
+ const retryAfter = res.headers.get("retry-after");
24622
+ const text = await res.text().catch(() => "");
24623
+ let json;
24624
+ if (text) try {
24625
+ json = JSON.parse(text);
24626
+ } catch {
24627
+ throw new X402Error(`non-JSON response (${res.status}) from ${path}: ${text.slice(0, 200)}`, res.status, text.slice(0, 500), { retryAfter });
24628
+ }
24629
+ 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 });
24630
+ 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 });
24631
+ return json.data;
24632
+ }
24633
+ /** Request a payment (payee side). Returns the challenge to hand to the payer. */
24634
+ async charge(input) {
24635
+ 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);
24636
+ 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);
24637
+ const amount = input.amountUsdc !== void 0 ? usdcToBaseUnits$1(input.amountUsdc) : input.amount ?? null;
24638
+ 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);
24639
+ const body = {
24640
+ amount,
24641
+ network: input.network ?? "base-sepolia"
24642
+ };
24643
+ if (input.payerOrg) body.payer_org = input.payerOrg;
24644
+ if (input.description) body.description = input.description;
24645
+ if (input.resource) body.resource = input.resource;
24646
+ if (input.expiresIn !== void 0) body.expires_in = input.expiresIn;
24647
+ return this.#request("POST", "/v1/x402/challenges", body, { headers: input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0 });
24648
+ }
24649
+ /**
24650
+ * Pay a challenge (payer side). Derives the interaction-bound authorization,
24651
+ * signs it locally with the caller's key, and submits it for settlement.
24652
+ */
24653
+ async pay(challenge, options) {
24654
+ 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);
24655
+ validateChallenge(challenge);
24656
+ const chainId = CHAIN_IDS[challenge.network];
24657
+ if (chainId === void 0) throw new X402Error(`unsupported network: ${challenge.network}`, 0);
24658
+ const pr = challenge.payment_requirements;
24659
+ if (pr.network !== challenge.network) throw new X402Error(`challenge network mismatch: ${challenge.network} vs payment_requirements ${pr.network}`, 0);
24660
+ if (pr.scheme !== "exact") throw new X402Error(`unsupported payment scheme: ${pr.scheme}`, 0);
24661
+ const nonce = deriveEip3009Nonce({
24662
+ interactionId: challenge.nonce_binding.interaction_id,
24663
+ challengeStepId: challenge.nonce_binding.challenge_step_id,
24664
+ challengeNonce: challenge.nonce_binding.challenge_nonce
24665
+ });
24666
+ const nowSec = Math.floor(Date.now() / 1e3);
24667
+ const expiresAtMs = Date.parse(challenge.expires_at);
24668
+ if (Number.isNaN(expiresAtMs)) throw new X402Error(`challenge has an invalid expires_at: ${challenge.expires_at}`, 0);
24669
+ const expiresAtSec = Math.floor(expiresAtMs / 1e3);
24670
+ if (expiresAtSec <= nowSec) throw new X402Error(`challenge has already expired (expires_at ${challenge.expires_at}); not signing`, 0);
24671
+ const validAfter = BigInt(nowSec - CLOCK_SKEW_SEC);
24672
+ const validBefore = BigInt(expiresAtSec + SETTLEMENT_MARGIN_SEC);
24673
+ const auth = {
24674
+ from: options.signer.address,
24675
+ to: pr.payTo,
24676
+ value: BigInt(pr.maxAmountRequired),
24677
+ validAfter,
24678
+ validBefore,
24679
+ nonce
24680
+ };
24681
+ const signature = await options.signer.signTypedData(transferWithAuthorizationTypedData({
24682
+ name: pr.extra.name,
24683
+ version: pr.extra.version,
24684
+ chainId,
24685
+ verifyingContract: pr.asset
24686
+ }, auth));
24687
+ return this.#request("POST", `/v1/x402/challenges/${challenge.id}/pay`, { payment: toPaymentPayload(challenge.network, auth, signature) });
24688
+ }
24689
+ /** Fetch a challenge by id (scoped to the challenger org that created it). */
24690
+ async getChallenge(id) {
24691
+ if (!id) throw new X402Error("getChallenge() requires a challenge id", 0);
24692
+ return this.#request("GET", `/v1/x402/challenges/${encodeURIComponent(id)}`);
24693
+ }
24694
+ /** Resolve the caller's own organization id from the account endpoint. */
24695
+ async #resolveOrgId() {
24696
+ const account = await this.#request("GET", "/v1/account");
24697
+ if (!account?.id) throw new X402Error("could not resolve your organization id from /v1/account; pass { org } explicitly", 0);
24698
+ return account.id;
24699
+ }
24700
+ /**
24701
+ * Register a payout address for your org (payee side). The signer proves
24702
+ * control of its own address with an org-bound `personal_sign`; the proven
24703
+ * address becomes (or updates to) the default payout destination for the
24704
+ * network. `charge()` resolves its `pay_to` from this directory, so a payee
24705
+ * must register before requesting payments.
24706
+ *
24707
+ * `org` is optional: when omitted it is resolved from your authenticated
24708
+ * account, so most callers never need to supply it.
24709
+ */
24710
+ async registerPayoutAddress(input, options) {
24711
+ if (typeof options?.signer?.signMessage !== "function") throw new X402Error("registerPayoutAddress() requires a signer with signMessage (e.g. a viem LocalAccount)", 0);
24712
+ const org = input.org ?? await this.#resolveOrgId();
24713
+ const network = input.network ?? "base-sepolia";
24714
+ const issuedAt = input.issuedAt ?? (/* @__PURE__ */ new Date()).toISOString();
24715
+ const address = options.signer.address;
24716
+ const message = buildPayoutRegistrationMessage({
24717
+ org,
24718
+ address,
24719
+ network,
24720
+ issuedAt
24721
+ });
24722
+ const signature = await options.signer.signMessage({ message });
24723
+ return this.#request("POST", "/v1/x402/payout-addresses", {
24724
+ address,
24725
+ network,
24726
+ signature,
24727
+ issued_at: issuedAt,
24728
+ ...input.label !== void 0 ? { label: input.label } : {}
24729
+ });
24730
+ }
24731
+ /** List your org's registered payout addresses. */
24732
+ async listPayoutAddresses() {
24733
+ return this.#request("GET", "/v1/x402/payout-addresses");
24734
+ }
24735
+ /**
24736
+ * List the most recent payments your org's spend policy declined (newest
24737
+ * first). Use this to see why an outbound payment was refused.
24738
+ */
24739
+ async listDeclinedPayments() {
24740
+ return this.#request("GET", "/v1/x402/declined-payments");
24741
+ }
24742
+ /** Read your org's spend policy (kill-switch + caps + allowlist). */
24743
+ async getSpendPolicy() {
24744
+ return this.#request("GET", "/v1/x402/spend-policy");
24745
+ }
24746
+ /**
24747
+ * Update your org's spend policy. The endpoint is a PUT, but the server
24748
+ * applies it as a merge: only the fields you include are changed and omitted
24749
+ * fields keep their current value, so a partial update can't silently reset
24750
+ * the kill-switch. Pass `null` to clear a cap.
24751
+ */
24752
+ async setSpendPolicy(update) {
24753
+ return this.#request("PUT", "/v1/x402/spend-policy", update);
24754
+ }
24755
+ };
24756
+ function createX402Client(options = {}) {
24757
+ return new X402Client(options);
24758
+ }
24759
+ //#endregion
24414
24760
  //#region src/oclif/commands/payments-shared.ts
24415
24761
  const PRIVATE_KEY_ENV = "PRIMITIVE_X402_PRIVATE_KEY";
24416
24762
  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.6.0",
3
+ "version": "1.6.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,