@cloudpayx/x402-pay 0.0.0-stage → 0.2.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/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Temporary Holding Version
1
+ # cpX x402 Pay — XRP + RLUSD + USDC XRPL agent SDK (Alpha)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Purpose:** Let an unaffiliated, self-funded agent participate in the XRPL x402 economy without a cloudpayX registration, dashboard, Supabase session, or required wallet brand. This is a *local client integration* for buying x402 resources, **not** a publicly deployed service, published npm package, or operator-funded wallet gateway.
4
+
5
+ This package is **pre-release security work**. It does not authenticate an operator, implement managed mandates, custody funds, create wallets, enforce global daily budgets, or provide guaranteed purchase idempotency. It has not yet been tested end-to-end with a third-party wallet/signer or a live 402 seller. **Do not give it unattended signing access to a valuable wallet.**
6
+
7
+ ## Source
8
+
9
+ - `src/policy.ts` — strict, dependency-free XRPL x402 V2 policy checks.
10
+ - `src/assets.ts` — verified XRPL mainnet XRP / Ripple RLUSD / Circle USDC identity constants and three-asset policy builder.
11
+ - `src/index.ts` — adapter to the existing proven `@x402/core`, `@x402/fetch`, `@x402/xrpl` client.
12
+ - `examples/feral-agent.example.ts` — signer-injection example; no secrets or hardcoded wallet provider.
13
+ - `examples/simulation-only.mjs` — no-wallet, no-network, no-payment demonstration.
14
+ - `tests/policy.test.mjs` and `tests/three-assets.test.mjs` — locally executable approval/denial tests for all three assets.
15
+
16
+ ## On your Mac (do not deploy yet)
17
+
18
+ **Keep this a separate test folder, do not copy over `src/cloudpayx_x402_pay.ts` or `src/cloudpayx_x402_pay_configured.ts`.**
19
+
20
+ 1. Unzip, open Terminal in the unzipped directory.
21
+ 2. Run `npm install` (the official `@x402/*` packages are required for the SDK adapter).
22
+ 3. Run `npm run test:policy` and `npm run demo:dry-run`.
23
+ 4. Run `npm run build` to check SDK integration types against the actual installed packages.
24
+
25
+ `npm run test:policy` needs only the TypeScript compiler, Node.js, and the dependency-free policy module. The build needs official x402 packages; network/package installation was unavailable during packaging.
26
+
27
+ ## Three verified XRPL mainnet assets
28
+
29
+ The optional helper `createThreeAssetPolicy` pins the network to `xrpl:0` and enables ONLY these official asset identities:
30
+
31
+ | Asset | x402 `asset` | x402 `extra.issuer` | Max amount unit |
32
+ | --- | --- | --- | --- |
33
+ | XRP | `XRP` | None | Integer drops (1 XRP = 1,000,000 drops) |
34
+ | Ripple RLUSD | `524C555344000000000000000000000000000000` | `rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De` | Decimal RLUSD tokens |
35
+ | Circle USDC on XRPL | `5553444300000000000000000000000000000000` | `rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE` | Decimal USDC tokens |
36
+
37
+ Only these XRPL issuances are accepted by this helper, *not* similarly named bridged tokens, Base USDC, or arbitrary USD IOUs. Signing always uses a caller-supplied `ClientXrplSigner`. **There is no automatic token conversion, exchange quoting, currency netting, or account registration.**
38
+
39
+ ## Sample: permissionless, self-funded agent using all three
40
+
41
+ ```ts
42
+ import { createCpXX402AgentPay, createThreeAssetPolicy } from './src/index.js';
43
+
44
+ const { paidFetch } = createCpXX402AgentPay({
45
+ signer: MY_SECURE_XRPL_SIGNER, // compatible @x402/xrpl ClientXrplSigner
46
+ policy: createThreeAssetPolicy({
47
+ limits: {
48
+ xrpDrops: '50000', // 0.05 XRP
49
+ rlusd: '0.05', // 0.05 RLUSD
50
+ usdc: '0.05' // 0.05 Circle USDC (XRPL)
51
+ },
52
+ recipients: { mode: 'any' }, // opt-in open seller participation
53
+ approval: { mode: 'autonomous' } // explicit local operator/agent opt-in
54
+ })
55
+ });
56
+ const response = await paidFetch('https://a-trusted-x402-resource.example/report');
57
+ ```
58
+
59
+ `MY_SECURE_XRPL_SIGNER` and seller URL are placeholders. Don't paste wallet secrets into this file. **An unknown agent can use the SDK with its own signer, but does not gain authority over anyone else's funds.**
60
+
61
+ `npm run demo:dry-run` simulates XRP, RLUSD, and USDC policy decisions without a wallet or network.
62
+
63
+ ## Units are not interchangeable
64
+
65
+ | XRPL asset | x402 `amount` units | `maxAmount...` example |
66
+ | --- | --- | --- |
67
+ | Native XRP | Drops (1,000,000 drops = 1 XRP) | `{kind:'xrp',asset:'XRP',maxAmountDrops:'50000'}` = 0.05 XRP |
68
+ | RLUSD, USDC or other XRPL-issued token | Decimal issued-token value | `{kind:'issued', asset:'...', issuer:'...', maxAmount:'0.05'}` = 0.05 token |
69
+
70
+ This module rejects scientific notation and prices above its exact cap. Limits are independent; they are **not** an equivalent USD portfolio budget. **Network fees are extra; the per-payment cap bounds the x402 transfer amount, not the combined cost including XRPL fees, trustline reserves, or issuer transfer fees.** XRPL recipient validation here only checks basic address *shape*. The underlying SDK/signer/facilitator must validate destination checksum, invoice/transaction binding, signature authorization, settlement, and errors.
71
+
72
+ ## Security boundaries
73
+
74
+ - No Xaman dependency and no human cloudpayX account required for self-funded agents.
75
+ - Strict XRPL mainnet `xrpl:0`, x402 V2, `exact` scheme, `areFeesSponsored=false`.
76
+ - Explicit assets and exact amount caps; opt-in seller-neutral recipient policy or allowlist.
77
+ - Explicit `autonomous` opt-in or application-provided authorization callback, with fail-closed exceptions.
78
+ - A self-funded agent retains *unrestricted authority over its own signer*. A local SDK is **not a trust boundary** against an agent holding the key: it can bypass these checks. Hardware signers, dedicated wallets, and controlled signer services are needed for stronger enforcement.
79
+ - **Not yet solved:** daily budgets; concurrent duplicate payments; resource-host validation, anti-SSRF, payment replays; wallet cross-network replay risks; fee caps; seller fraud; external signing adapters; end-to-end validated settlement checks; managed/delegated spending and revocation. The XRPL scheme uses account sequence/tickets; concurrent sequence-method payments require coordination.
80
+ - The live x402 facilitator/service should validate transaction signatures and bind invoice, amount, asset, destination and network, as described in the XRPL exact scheme spec.
81
+
82
+ ## Operational conditions for USDC and RLUSD
83
+
84
+ The agent wallet must have the required XRPL token trustline/balance/authorization (where applicable) and enough XRP for transaction fees and reserves. A valid policy decision **does not** guarantee a successful token payment or a resource response. The resource server must advertise a compatible x402 `accepts` entry; the SDK does not auto-swap XRP into stablecoins.
85
+
86
+ The underlying XRPL exact scheme includes issuer binding and fee-payment rules. The current Alpha does not make independent fee ceilings, balance checks, trustline checks, daily budgets, concurrency locks, or validated settlement guarantees. A self-funded agent holding its own signing authority can bypass an SDK policy. **Do not enable unattended real-money signing until those boundaries are addressed.**
87
+
88
+ **Official issuers:** [Ripple RLUSD](https://docs.ripple.com/products/stablecoin/developer-resources/rlusd-on-the-xrpl) and [Circle XRPL USDC](https://www.circle.com/blog/now-available-usdc-on-the-xrpl).
89
+
90
+ ## Next milestone
91
+
92
+ Use a **dedicated funded test wallet** and a real compatible XRPL x402 resource in a supervised, deliberately low-value interoperability test. First reconcile Node/Bun and dependency-compatibility, then add durable budget reservation and signing isolation for *operator-funded* agents. Do not enable real-money unattended signing on this Alpha example.
93
+
94
+ Specifications: https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact_xrpl.md
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Verified XRPL MAINNET asset identifiers for cpX x402 Pay.
3
+ * Issued tokens are ALWAYS identified by (currency code, issuer), not by symbol.
4
+ *
5
+ * XRP: native drops, no issuer.
6
+ * RLUSD: Ripple's XRPL issuance (not EVM RLUSD).
7
+ * USDC: Circle's native XRPL issuance (not bridged USDC or USDC on Base).
8
+ *
9
+ * Sources:
10
+ * https://docs.ripple.com/products/stablecoin/developer-resources/rlusd-on-the-xrpl
11
+ * https://www.circle.com/blog/now-available-usdc-on-the-xrpl
12
+ * https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact_xrpl.md
13
+ */
14
+ import type { AgentPaymentConfig } from './policy.js';
15
+ export declare const CPX_XRPL_MAINNET_ASSETS: Readonly<{
16
+ XRP: Readonly<{
17
+ kind: "xrp";
18
+ asset: "XRP";
19
+ network: "xrpl:0";
20
+ }>;
21
+ RLUSD: Readonly<{
22
+ kind: "issued";
23
+ asset: "524C555344000000000000000000000000000000";
24
+ issuer: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De";
25
+ network: "xrpl:0";
26
+ }>;
27
+ USDC: Readonly<{
28
+ kind: "issued";
29
+ asset: "5553444300000000000000000000000000000000";
30
+ issuer: "rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE";
31
+ network: "xrpl:0";
32
+ }>;
33
+ }>;
34
+ export type ThreeAssetLimits = {
35
+ /** XRP's x402 amount is integer DROPS, not XRP units. 50000 = 0.05 XRP. */
36
+ xrpDrops: string;
37
+ /** RLUSD's x402 amount is a decimal token value, e.g. "0.05". */
38
+ rlusd: string;
39
+ /** Circle's XRPL USDC amount is a decimal token value, e.g. "0.05". */
40
+ usdc: string;
41
+ };
42
+ export type ThreeAssetPolicyOptions = {
43
+ /** All three limits are mandatory and must be positive; no implied unlimited cap. */
44
+ limits: ThreeAssetLimits;
45
+ recipients: AgentPaymentConfig['recipients'];
46
+ approval: AgentPaymentConfig['approval'];
47
+ };
48
+ /**
49
+ * Builds a policy restricted to the three official cpX mainnet asset identities.
50
+ * This helper DOES NOT create a signer, send a payment, or authorize an operator.
51
+ * Pass to createCpXX402AgentPay({ signer, policy }) and keep the signer isolated.
52
+ */
53
+ export declare function createThreeAssetPolicy(options: ThreeAssetPolicyOptions): AgentPaymentConfig;
@@ -0,0 +1,36 @@
1
+ export const CPX_XRPL_MAINNET_ASSETS = Object.freeze({
2
+ XRP: Object.freeze({ kind: 'xrp', asset: 'XRP', network: 'xrpl:0' }),
3
+ RLUSD: Object.freeze({
4
+ kind: 'issued',
5
+ asset: '524C555344000000000000000000000000000000',
6
+ issuer: 'rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De',
7
+ network: 'xrpl:0'
8
+ }),
9
+ USDC: Object.freeze({
10
+ kind: 'issued',
11
+ asset: '5553444300000000000000000000000000000000',
12
+ issuer: 'rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE',
13
+ network: 'xrpl:0'
14
+ })
15
+ });
16
+ /**
17
+ * Builds a policy restricted to the three official cpX mainnet asset identities.
18
+ * This helper DOES NOT create a signer, send a payment, or authorize an operator.
19
+ * Pass to createCpXX402AgentPay({ signer, policy }) and keep the signer isolated.
20
+ */
21
+ export function createThreeAssetPolicy(options) {
22
+ if (!options?.limits)
23
+ throw new Error('CPX_THREE_ASSET_LIMITS_REQUIRED');
24
+ const { limits, recipients, approval } = options;
25
+ return {
26
+ assets: [
27
+ { kind: 'xrp', asset: CPX_XRPL_MAINNET_ASSETS.XRP.asset, maxAmountDrops: limits.xrpDrops },
28
+ { kind: 'issued', asset: CPX_XRPL_MAINNET_ASSETS.RLUSD.asset,
29
+ issuer: CPX_XRPL_MAINNET_ASSETS.RLUSD.issuer, maxAmount: limits.rlusd },
30
+ { kind: 'issued', asset: CPX_XRPL_MAINNET_ASSETS.USDC.asset,
31
+ issuer: CPX_XRPL_MAINNET_ASSETS.USDC.issuer, maxAmount: limits.usdc }
32
+ ],
33
+ recipients,
34
+ approval
35
+ };
36
+ }
@@ -0,0 +1,31 @@
1
+ import { x402Client } from '@x402/core/client';
2
+ import { type ClientXrplSigner } from '@x402/xrpl';
3
+ import { type AgentPaymentConfig } from './policy.js';
4
+ import { type GuardOptions } from './one_shot_guard.js';
5
+ export { compareAmount, createAgentPaymentGate } from './policy.js';
6
+ export { CPX_XRPL_MAINNET_ASSETS, createThreeAssetPolicy } from './assets.js';
7
+ export { createOneShotXrplSigner } from './one_shot_guard.js';
8
+ export type { GuardOptions } from './one_shot_guard.js';
9
+ export type { ThreeAssetLimits, ThreeAssetPolicyOptions } from './assets.js';
10
+ export type { AgentPaymentConfig, PaymentIntent, XrplAsset, X402Requirements } from './policy.js';
11
+ /**
12
+ * Bring-your-own XRPL signer. One-shot mode binds the selected x402 offer
13
+ * to a checked transaction just before sign() runs.
14
+ * Self-funded agents own their keys; they can bypass SDK restrictions if they
15
+ * deliberately use their own signer outside the SDK.
16
+ */
17
+ export declare function createCpXX402AgentPay(options: {
18
+ signer: ClientXrplSigner;
19
+ policy: AgentPaymentConfig;
20
+ requestFetch?: typeof fetch;
21
+ /** Strongly recommended for the first supervised purchase; NOT a persistent budget. */
22
+ oneShot?: GuardOptions;
23
+ }): {
24
+ paidFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
25
+ client: x402Client;
26
+ /** For instrumentation only; not evidence of settlement. */
27
+ oneShotStatus: () => {
28
+ armed: boolean;
29
+ consumed: boolean;
30
+ } | undefined;
31
+ };
@@ -0,0 +1,85 @@
1
+ import { Client as CpXXrplClient } from "xrpl";
2
+ import { createCpXWireFetch } from "./xrpl_x402_wire.js";
3
+ import { x402Client } from '@x402/core/client';
4
+ import { wrapFetchWithPayment } from '@x402/fetch';
5
+ import { ExactXrplScheme } from '@x402/xrpl';
6
+ import { createAgentPaymentGate } from './policy.js';
7
+ import { createOneShotXrplSigner } from './one_shot_guard.js';
8
+ export { compareAmount, createAgentPaymentGate } from './policy.js';
9
+ export { CPX_XRPL_MAINNET_ASSETS, createThreeAssetPolicy } from './assets.js';
10
+ export { createOneShotXrplSigner } from './one_shot_guard.js';
11
+ /**
12
+ * Bring-your-own XRPL signer. One-shot mode binds the selected x402 offer
13
+ * to a checked transaction just before sign() runs.
14
+ * Self-funded agents own their keys; they can bypass SDK restrictions if they
15
+ * deliberately use their own signer outside the SDK.
16
+ */
17
+ export function createCpXX402AgentPay(options) {
18
+ const gate = createAgentPaymentGate(options.policy);
19
+ const guarded = options.oneShot ? createOneShotXrplSigner(options.signer, options.oneShot) : undefined;
20
+ const signer = guarded?.signer ?? options.signer;
21
+ const client = new x402Client((_version, requirements) => {
22
+ const chosen = requirements.find(req => req.network === 'xrpl:0' &&
23
+ req.scheme === 'exact' &&
24
+ options.policy.assets.some(asset => asset.asset === req.asset &&
25
+ (asset.kind === 'xrp' || asset.issuer === req.extra?.issuer)));
26
+ if (!chosen)
27
+ throw new Error('CPX_NO_PERMITTED_XRPL_ASSET');
28
+ return chosen;
29
+ }).setSpendControls({
30
+ // Explicitly select only the asset(s) authorized by this agent's policy.
31
+ allowedAssets: options.policy.assets.map(a => ({ network: 'xrpl:0', asset: a.asset }))
32
+ }).register('xrpl:0', new ExactXrplScheme(signer, {
33
+ preparePaymentTransaction: async (transaction, requirements) => {
34
+ if (requirements.network !== "xrpl:0") {
35
+ throw new Error("CPX_XRPL_MAINNET_ONLY");
36
+ }
37
+ const seconds = Number(requirements.maxTimeoutSeconds);
38
+ if (!Number.isSafeInteger(seconds) || seconds <= 0) {
39
+ throw new Error("CPX_INVALID_PAYMENT_TIMEOUT");
40
+ }
41
+ const ledger = new CpXXrplClient("wss://s1.ripple.com");
42
+ try {
43
+ await ledger.connect();
44
+ const currentLedger = await ledger.getLedgerIndex();
45
+ const maxLedger = currentLedger + Math.ceil(seconds / 5) + 2;
46
+ const expiry = transaction.LastLedgerSequence;
47
+ if (expiry !== undefined &&
48
+ (!Number.isSafeInteger(expiry) ||
49
+ expiry <= currentLedger ||
50
+ expiry > maxLedger)) {
51
+ throw new Error("CPX_INVALID_LEDGER_EXPIRY");
52
+ }
53
+ return await ledger.autofill({
54
+ ...transaction,
55
+ LastLedgerSequence: expiry ?? maxLedger
56
+ });
57
+ }
58
+ finally {
59
+ await ledger.disconnect();
60
+ }
61
+ },
62
+ wsUrlByNetwork: { 'xrpl:0': 'wss://s1.ripple.com' },
63
+ // Never let the guarded signer authorize a TicketCreate transaction.
64
+ ...(guarded ? { ticketCreateCount: 0 } : {})
65
+ }));
66
+ client.onBeforePaymentCreation(async ({ paymentRequired, selectedRequirements }) => {
67
+ const result = await gate.evaluate({ paymentRequired, selectedRequirements });
68
+ if (!result.allowed)
69
+ return { abort: true, reason: result.reason };
70
+ if (guarded) {
71
+ try {
72
+ await guarded.arm(selectedRequirements);
73
+ }
74
+ catch {
75
+ return { abort: true, reason: 'CPX_ONE_SHOT_ARM_FAILED' };
76
+ }
77
+ }
78
+ });
79
+ return {
80
+ paidFetch: wrapFetchWithPayment(createCpXWireFetch(options.requestFetch ?? globalThis.fetch), client),
81
+ client,
82
+ /** For instrumentation only; not evidence of settlement. */
83
+ oneShotStatus: () => guarded?.getStatus()
84
+ };
85
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * cpX x402 Pay: bounded, single-signature guard for a supervised XRPL purchase.
3
+ * NO wallet keys, network requests, or transaction submission in this module.
4
+ * The guarded signer may only sign one validated Payment for one selected x402 offer.
5
+ */
6
+ import { type X402Requirements } from './policy.js';
7
+ export type XrplSigningTransaction = Record<string, unknown>;
8
+ export type MinimalXrplSigner<T extends object> = {
9
+ classicAddress: string;
10
+ sign(transaction: T): Promise<{
11
+ signedTxBlob: string;
12
+ hash?: string;
13
+ }> | {
14
+ signedTxBlob: string;
15
+ hash?: string;
16
+ };
17
+ };
18
+ export type GuardOptions = {
19
+ recipient: string;
20
+ asset: string;
21
+ issuer?: string;
22
+ /** XRP in drops, or issued-token decimal units. */
23
+ exactAmount: string;
24
+ /** For issued tokens, cap token SendMax including any transfer fee. */
25
+ maxSendMax?: string;
26
+ /** The largest signing fee in integer XRP drops. */
27
+ maxFeeDrops: string;
28
+ /** Explicitly opt in to deployed t54 seller conventions for this purchase only. */
29
+ legacyT54Offer?: boolean;
30
+ /** Query XRPL MAINNET's current validated ledger index. Never use a user-controlled value in production. */
31
+ getValidatedLedgerIndex: () => Promise<number>;
32
+ };
33
+ export declare function createOneShotXrplSigner<T extends object>(original: MinimalXrplSigner<T>, options: GuardOptions): {
34
+ signer: {
35
+ classicAddress: string;
36
+ sign(tx: T): Promise<{
37
+ signedTxBlob: string;
38
+ hash?: string;
39
+ } | {
40
+ signedTxBlob: string;
41
+ hash?: string;
42
+ }>;
43
+ };
44
+ /** Arms the guard for exactly one challenge AFTER x402 policy and human approval. */
45
+ arm(req: X402Requirements & {
46
+ maxTimeoutSeconds?: number;
47
+ }): Promise<void>;
48
+ getStatus(): {
49
+ armed: boolean;
50
+ consumed: boolean;
51
+ };
52
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * cpX x402 Pay: bounded, single-signature guard for a supervised XRPL purchase.
3
+ * NO wallet keys, network requests, or transaction submission in this module.
4
+ * The guarded signer may only sign one validated Payment for one selected x402 offer.
5
+ */
6
+ import { compareAmount } from './policy.js';
7
+ const INT = /^(0|[1-9][0-9]*)$/;
8
+ const ALLOWED_TX_FIELDS = new Set([
9
+ 'TransactionType', 'Account', 'Destination', 'DestinationTag', 'SourceTag',
10
+ 'Amount', 'DeliverMax', 'SendMax', 'Fee', 'Flags', 'Sequence',
11
+ 'LastLedgerSequence', 'InvoiceID', 'NetworkID' // xrpl.js may add an undefined property on mainnet.
12
+ ]);
13
+ function requireCondition(condition, error) {
14
+ if (!condition)
15
+ throw new Error(error);
16
+ }
17
+ function isUint32(value) {
18
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 0xffffffff;
19
+ }
20
+ function validUnsignedDrops(value) {
21
+ return typeof value === 'string' && value.length <= 18 && INT.test(value);
22
+ }
23
+ function validIssued(value, asset, issuer) {
24
+ if (!value || typeof value !== 'object' || Array.isArray(value))
25
+ return false;
26
+ const x = value;
27
+ return Object.keys(x).sort().join(',') === 'currency,issuer,value' &&
28
+ x.currency === asset && x.issuer === issuer && typeof x.value === 'string';
29
+ }
30
+ async function invoiceHash(invoice) {
31
+ const input = new TextEncoder().encode(invoice);
32
+ const digest = await crypto.subtle.digest('SHA-256', input);
33
+ return Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
34
+ }
35
+ export function createOneShotXrplSigner(original, options) {
36
+ requireCondition(typeof original?.classicAddress === 'string' && original.classicAddress.startsWith('r'), 'CPX_INVALID_SIGNER');
37
+ requireCondition(validUnsignedDrops(options.maxFeeDrops) && BigInt(options.maxFeeDrops) > 0n, 'CPX_INVALID_FEE_CAP');
38
+ requireCondition(options.recipient.length > 0 && options.asset.length > 0, 'CPX_INVALID_GUARD');
39
+ requireCondition(typeof options.getValidatedLedgerIndex === 'function', 'CPX_LEDGER_CHECK_REQUIRED');
40
+ const unit = options.asset === 'XRP' ? 'drops' : 'token';
41
+ requireCondition(compareAmount(options.exactAmount, '0', unit) === 1, 'CPX_INVALID_AMOUNT');
42
+ if (unit === 'token') {
43
+ requireCondition(typeof options.issuer === 'string' && !!options.issuer, 'CPX_ISSUER_REQUIRED');
44
+ requireCondition(typeof options.maxSendMax === 'string' && compareAmount(options.maxSendMax, options.exactAmount, 'token') !== null && compareAmount(options.maxSendMax, options.exactAmount, 'token') >= 0, 'CPX_INVALID_SENDMAX_CAP');
45
+ }
46
+ else {
47
+ requireCondition(options.issuer === undefined && options.maxSendMax === undefined, 'CPX_UNEXPECTED_XRP_FIELDS');
48
+ }
49
+ let armed;
50
+ let started = false;
51
+ let consumed = false;
52
+ return {
53
+ signer: {
54
+ classicAddress: original.classicAddress,
55
+ async sign(tx) {
56
+ requireCondition(armed !== undefined && !started && !consumed, 'CPX_SIGNING_NOT_AUTHORIZED');
57
+ started = true; // BEFORE awaiting anything: racing sign() requests fail closed.
58
+ consumed = true; // No automatic second signing, even after failure.
59
+ const offer = armed;
60
+ requireCondition(tx && typeof tx === 'object' && !Array.isArray(tx), 'CPX_INVALID_TRANSACTION');
61
+ const fields = tx;
62
+ requireCondition(Object.keys(fields).every(k => ALLOWED_TX_FIELDS.has(k)), 'CPX_UNKNOWN_TRANSACTION_FIELD');
63
+ requireCondition(fields.TransactionType === 'Payment', 'CPX_PAYMENT_ONLY');
64
+ requireCondition(fields.Account === original.classicAddress, 'CPX_WRONG_PAYER');
65
+ requireCondition(fields.Destination === offer.recipient, 'CPX_WRONG_DESTINATION');
66
+ requireCondition(fields.DestinationTag === offer.destinationTag, 'CPX_WRONG_DESTINATION_TAG');
67
+ requireCondition(fields.SourceTag === undefined || offer.sourceTag !== undefined, 'CPX_UNKNOWN_TRANSACTION_FIELD');
68
+ requireCondition(fields.SourceTag === undefined || fields.SourceTag === offer.sourceTag, 'CPX_WRONG_SOURCE_TAG');
69
+ requireCondition(typeof fields.InvoiceID === 'string' && fields.InvoiceID.toUpperCase() === offer.invoiceID, 'CPX_WRONG_INVOICE');
70
+ // XRPL mainnet (xrpl:0) requires NetworkID to be absent from the SIGNED transaction.
71
+ // Official xrpl.js autofill may supply a JavaScript own-property with value undefined.
72
+ // Accept that placeholder only; never accept a concrete NetworkID (including zero).
73
+ requireCondition(fields.NetworkID === undefined, 'CPX_MAINNET_NETWORK_ID_FORBIDDEN');
74
+ requireCondition(!('Paths' in fields) && !('DeliverMin' in fields) && !('Delegate' in fields) && !('Memos' in fields), 'CPX_FORBIDDEN_TRANSACTION_FIELD');
75
+ requireCondition(fields.Flags === undefined || fields.Flags === 0 || fields.Flags === 0x80000000, 'CPX_FORBIDDEN_FLAGS');
76
+ requireCondition(validUnsignedDrops(fields.Fee) && BigInt(fields.Fee) > 0n && BigInt(fields.Fee) <= BigInt(options.maxFeeDrops), 'CPX_FEE_EXCEEDS_LIMIT');
77
+ requireCondition(isUint32(fields.Sequence) && fields.Sequence > 0 && !('TicketSequence' in fields), 'CPX_SEQUENCE_ONLY');
78
+ requireCondition(isUint32(fields.LastLedgerSequence) && fields.LastLedgerSequence > 0, 'CPX_EXPIRY_REQUIRED');
79
+ const ledger = await options.getValidatedLedgerIndex();
80
+ requireCondition(isUint32(ledger) && ledger > 0, 'CPX_LEDGER_CHECK_FAILED');
81
+ const maxLedgerDelta = Math.ceil(offer.timeout / 5) + 2;
82
+ requireCondition(fields.LastLedgerSequence > ledger && fields.LastLedgerSequence <= ledger + maxLedgerDelta, 'CPX_EXPIRY_OUT_OF_RANGE');
83
+ requireCondition(('Amount' in fields) !== ('DeliverMax' in fields), 'CPX_EXACTLY_ONE_DELIVERY_FIELD');
84
+ const destAmount = fields.DeliverMax ?? fields.Amount;
85
+ if (offer.asset === 'XRP') {
86
+ requireCondition(validUnsignedDrops(destAmount) && compareAmount(destAmount, offer.amount, 'drops') === 0, 'CPX_WRONG_XRP_AMOUNT');
87
+ requireCondition(!('SendMax' in fields), 'CPX_XRP_SENDMAX_FORBIDDEN');
88
+ }
89
+ else {
90
+ requireCondition(validIssued(destAmount, offer.asset, offer.issuer) && compareAmount(destAmount.value, offer.amount, 'token') === 0, 'CPX_WRONG_ISSUED_AMOUNT');
91
+ requireCondition(validIssued(fields.SendMax, offer.asset, offer.issuer), 'CPX_INVALID_SENDMAX');
92
+ const sendMax = fields.SendMax.value;
93
+ requireCondition(compareAmount(sendMax, offer.amount, 'token') !== null && compareAmount(sendMax, offer.amount, 'token') >= 0, 'CPX_SENDMAX_BELOW_DELIVERY');
94
+ requireCondition(compareAmount(sendMax, options.maxSendMax, 'token') !== null && compareAmount(sendMax, options.maxSendMax, 'token') <= 0, 'CPX_SENDMAX_EXCEEDS_LIMIT');
95
+ }
96
+ // Normalize ONLY the undefined placeholder, removing it before the wallet signs.
97
+ // This does not add/change a network ID or construct an XRPL transaction.
98
+ if (Object.prototype.hasOwnProperty.call(fields, 'NetworkID') || offer.sourceTag !== undefined) {
99
+ const normalized = { ...fields };
100
+ if (Object.prototype.hasOwnProperty.call(normalized, 'NetworkID'))
101
+ delete normalized.NetworkID;
102
+ if (normalized.DestinationTag === undefined)
103
+ delete normalized.DestinationTag;
104
+ if (normalized.SourceTag === undefined)
105
+ delete normalized.SourceTag;
106
+ if (offer.sourceTag !== undefined)
107
+ normalized.SourceTag = offer.sourceTag;
108
+ return await original.sign(normalized);
109
+ }
110
+ return await original.sign(tx);
111
+ }
112
+ },
113
+ /** Arms the guard for exactly one challenge AFTER x402 policy and human approval. */
114
+ async arm(req) {
115
+ requireCondition(!started && !consumed && !armed, 'CPX_ALREADY_ARMED');
116
+ started = true; // Reserve immediately; asynchronous hash may yield.
117
+ try {
118
+ requireCondition(req.network === 'xrpl:0' && req.scheme === 'exact' &&
119
+ (req.extra?.areFeesSponsored === false ||
120
+ (options.legacyT54Offer === true && req.extra?.areFeesSponsored === undefined)), 'CPX_NOT_XRPL_EXACT');
121
+ requireCondition(req.payTo === options.recipient && req.asset === options.asset && req.amount === options.exactAmount, 'CPX_OFFER_MISMATCH');
122
+ requireCondition(req.extra?.issuer === options.issuer, 'CPX_OFFER_ISSUER_MISMATCH');
123
+ requireCondition(req.extra?.assetTransferMethod === undefined || req.extra.assetTransferMethod === 'sequence', 'CPX_TICKETS_NOT_SUPPORTED');
124
+ requireCondition(isUint32(req.extra?.destinationTag) ||
125
+ (options.legacyT54Offer === true && req.extra?.destinationTag === undefined), 'CPX_DESTINATION_TAG_REQUIRED');
126
+ requireCondition(req.extra?.sourceTag === undefined ||
127
+ (options.legacyT54Offer === true && isUint32(req.extra.sourceTag)), 'CPX_SOURCE_TAG_INVALID');
128
+ requireCondition(typeof req.extra?.invoiceId === 'string' && req.extra.invoiceId.length > 0 && req.extra.invoiceId.length <= 256, 'CPX_INVOICE_REQUIRED');
129
+ requireCondition(typeof req.maxTimeoutSeconds === 'number' && Number.isInteger(req.maxTimeoutSeconds) && req.maxTimeoutSeconds > 0 && req.maxTimeoutSeconds <= (options.legacyT54Offer === true ? 600 : 120), 'CPX_TIMEOUT_INVALID');
130
+ const id = await invoiceHash(req.extra.invoiceId);
131
+ armed = { recipient: req.payTo,
132
+ ...(req.extra?.destinationTag === undefined ? {} : { destinationTag: req.extra.destinationTag }),
133
+ ...(req.extra?.sourceTag === undefined ? {} : { sourceTag: req.extra.sourceTag }),
134
+ invoiceID: id,
135
+ asset: req.asset, issuer: req.extra.issuer, amount: req.amount, timeout: req.maxTimeoutSeconds };
136
+ }
137
+ finally {
138
+ started = false;
139
+ }
140
+ },
141
+ getStatus() { return { armed: !!armed, consumed }; }
142
+ };
143
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * cpX x402 Pay — self-funded agent policy gate.
3
+ * No account, Supabase, Cloudflare, Xaman, or private keys are required here.
4
+ * NOTE: XRP x402 amounts are DROPS. XRPL-issued tokens use decimal token values.
5
+ */
6
+ export type XrplAsset = {
7
+ kind: 'xrp';
8
+ asset: 'XRP';
9
+ maxAmountDrops: string;
10
+ } | {
11
+ kind: 'issued';
12
+ asset: string;
13
+ issuer: string;
14
+ maxAmount: string;
15
+ };
16
+ export type X402Requirements = {
17
+ network: string;
18
+ scheme: string;
19
+ asset: string;
20
+ amount: string;
21
+ payTo: string;
22
+ extra?: {
23
+ issuer?: string;
24
+ areFeesSponsored?: unknown;
25
+ assetTransferMethod?: string;
26
+ [name: string]: unknown;
27
+ };
28
+ };
29
+ export type PaymentChallenge = {
30
+ paymentRequired: {
31
+ x402Version?: number;
32
+ };
33
+ selectedRequirements: X402Requirements;
34
+ };
35
+ export type PaymentIntent = {
36
+ network: 'xrpl:0';
37
+ asset: string;
38
+ amount: string;
39
+ amountUnit: 'drops' | 'token';
40
+ issuer?: string;
41
+ recipient: string;
42
+ };
43
+ export type AgentPaymentConfig = {
44
+ /** Required positive caps; no implicit unlimited spending. */
45
+ assets: readonly XrplAsset[];
46
+ /** Explicitly choose permissionless destinations or an allowlist. */
47
+ recipients: {
48
+ mode: 'any';
49
+ } | {
50
+ mode: 'allowlist';
51
+ addresses: readonly string[];
52
+ };
53
+ /** Autonomous approval must be explicitly selected by the agent operator. */
54
+ /** Opt-in support for live t54-oriented challenges omitting areFeesSponsored. */
55
+ legacyT54Offer?: boolean;
56
+ approval: {
57
+ mode: 'autonomous';
58
+ } | {
59
+ mode: 'callback';
60
+ authorize: (intent: PaymentIntent) => Promise<boolean>;
61
+ };
62
+ };
63
+ export type GateResult = {
64
+ allowed: true;
65
+ intent: PaymentIntent;
66
+ } | {
67
+ allowed: false;
68
+ reason: string;
69
+ };
70
+ /** Compare nonnegative decimal strings without Number / float rounding. */
71
+ export declare function compareAmount(a: string, b: string, units: 'drops' | 'token'): number | null;
72
+ export declare function createAgentPaymentGate(config: AgentPaymentConfig): {
73
+ evaluate(challenge: PaymentChallenge): Promise<GateResult>;
74
+ };