warpmetal 0.7.6 → 0.7.8
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 +2 -2
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +20 -5
- package/skills/warpmetal/references/cli-reference.md +4 -1
- package/skills/warpmetal/references/payments.md +17 -7
- package/skills/warpmetal/references/renewals.md +3 -1
- package/skills/warpmetal/references/safety.md +10 -5
- package/src/cli.js +54 -1
- package/src/payment.js +13 -4
- package/src/state.js +103 -0
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ published wallet CLI with the exact version WarpMetal reports:
|
|
|
34
34
|
npm install --global warpmetal
|
|
35
35
|
warpmetal --help
|
|
36
36
|
|
|
37
|
-
npm install --global @x402api/agent-wallet-cli@0.2.
|
|
37
|
+
npm install --global @x402api/agent-wallet-cli@0.2.8
|
|
38
38
|
x402api help --json
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -58,7 +58,7 @@ integration.
|
|
|
58
58
|
|
|
59
59
|
The repository also contains a skills-only WarpMetal plugin for the public
|
|
60
60
|
Plugins Directory shared by Codex and ChatGPT. The plugin remains a separate
|
|
61
|
-
artifact from the npm CLI and requires `warpmetal` CLI version 0.7.
|
|
61
|
+
artifact from the npm CLI and requires `warpmetal` CLI version 0.7.8 or newer.
|
|
62
62
|
|
|
63
63
|
To test the repository marketplace after the plugin lands on `main`:
|
|
64
64
|
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ with ad hoc HTTP commands.
|
|
|
11
11
|
|
|
12
12
|
## Start safely
|
|
13
13
|
|
|
14
|
-
1. Run `warpmetal --version` and require version `0.7.
|
|
14
|
+
1. Run `warpmetal --version` and require version `0.7.8` or newer for this
|
|
15
15
|
plugin. If it is missing or older, explain the compatibility requirement,
|
|
16
16
|
ask before installing or upgrading software, and use only the official npm
|
|
17
17
|
package from `https://www.npmjs.com/package/warpmetal`.
|
|
@@ -136,12 +136,22 @@ WarpMetal verifies that the artifact matches the exact saved request and an
|
|
|
136
136
|
advertised sponsored requirement, gas reservation, resource, extensions, and
|
|
137
137
|
buyer payment identifier before sending its signature. The legacy
|
|
138
138
|
`--payment-signature-file` input remains available for another compatible
|
|
139
|
-
external signer. If a signed request returns
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
external signer. If a signed request returns `payment_rejected`, preserve its
|
|
140
|
+
safe `errorCode` and `requestId` diagnostics and inspect `replacementAllowed`.
|
|
141
|
+
When it is true, run the unsigned challenge flow again and re-evaluate payment
|
|
142
|
+
authority before creating the one permitted replacement authorization. When it
|
|
143
|
+
is false or absent, stop without signing again. On `manual_review` or an
|
|
143
144
|
ambiguous attempt, stop and never create another payment.
|
|
144
145
|
|
|
146
|
+
WarpMetal continues read-only backend reconciliation for
|
|
147
|
+
`manual_review/stale_payment_outcome`; this is not a client bypass. Re-read the
|
|
148
|
+
task on a later autonomous cycle. Only when it becomes `expired` with
|
|
149
|
+
`failure.code` equal to `payment_expired_unsettled`, `paidAt` null, and
|
|
150
|
+
`retrySafe` true has WarpMetal proved that the prior charge expired unpaid. An
|
|
151
|
+
autonomous workflow may then prepare a new order if the original intent and
|
|
152
|
+
standing authority still apply. Never reuse the old signature or infer safety
|
|
153
|
+
from wallet balance, timeout, `paidAt`, `expired`, or `manual_review` alone.
|
|
154
|
+
|
|
145
155
|
## Renew autonomously within policy
|
|
146
156
|
|
|
147
157
|
Configure a bounded server policy before unattended renewal. Require a
|
|
@@ -178,6 +188,11 @@ Poll a prepared or paid order with:
|
|
|
178
188
|
warpmetal order status --task <taskId> --wait --json
|
|
179
189
|
```
|
|
180
190
|
|
|
191
|
+
`manual_review` remains terminal for payment and mutation attempts. A later
|
|
192
|
+
read-only status check may observe the backend's authoritative reconciliation;
|
|
193
|
+
prepare a replacement only for the exact `retrySafe: true` result described
|
|
194
|
+
above.
|
|
195
|
+
|
|
181
196
|
After initial provisioning and every OS reload, WarpMetal reapplies the same
|
|
182
197
|
key-only policy: `PasswordAuthentication no`,
|
|
183
198
|
`KbdInteractiveAuthentication no`, `PermitRootLogin prohibit-password`, and
|
|
@@ -64,12 +64,15 @@ suggested artifact paths live under the
|
|
|
64
64
|
private WarpMetal state directory. An explicit output path must not already
|
|
65
65
|
contain different content. `challengeHandle` is intentionally absent from the
|
|
66
66
|
wallet envelope: it is neither a buyer payment identifier nor signing input.
|
|
67
|
+
`paymentChallengeDigest` is the distinct authoritative signing input forwarded
|
|
68
|
+
from x402api; the CLI fails closed instead of deriving it from
|
|
69
|
+
`PAYMENT-REQUIRED`.
|
|
67
70
|
If a previously issued unsigned sponsorship reservation expired, WarpMetal may
|
|
68
71
|
retire that attempt and return a new `paymentAttemptId`. The CLI replaces the
|
|
69
72
|
saved challenge and stale wallet-attempt metadata; use only the newly returned
|
|
70
73
|
workflow.
|
|
71
74
|
|
|
72
|
-
The current integration targets `@x402api/agent-wallet-cli@0.2.
|
|
75
|
+
The current integration targets `@x402api/agent-wallet-cli@0.2.8`. A compatible
|
|
73
76
|
live term is marked `agentWalletSupported: true` and must use the sponsored
|
|
74
77
|
Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
|
|
75
78
|
For the current declaration, x402api pays actual gas from its platform treasury
|
|
@@ -25,7 +25,7 @@ contract.
|
|
|
25
25
|
WarpMetal runs on Node.js 20 or 22. The x402api Agent Wallet currently requires
|
|
26
26
|
Node.js 22. Use the exact published package reported by
|
|
27
27
|
`paymentWorkflow.signerPackage.spec`; the current contract is
|
|
28
|
-
`@x402api/agent-wallet-cli@0.2.
|
|
28
|
+
`@x402api/agent-wallet-cli@0.2.8`. Do not add it as a WarpMetal dependency,
|
|
29
29
|
install executable wallet code from an unpinned repository URL, or substitute
|
|
30
30
|
a similarly named package.
|
|
31
31
|
|
|
@@ -140,6 +140,9 @@ also contains `challengeHandle`, an opaque value retained by WarpMetal for
|
|
|
140
140
|
merchant reconciliation. It is deliberately excluded from the x402api V1
|
|
141
141
|
request envelope and must not be confused with the wallet-created buyer payment
|
|
142
142
|
identifier.
|
|
143
|
+
The wallet nonce is bound to the authoritative `paymentChallengeDigest` that
|
|
144
|
+
WarpMetal forwards from x402api's charge response. Never derive or replace that
|
|
145
|
+
digest with a hash of `PAYMENT-REQUIRED`.
|
|
143
146
|
|
|
144
147
|
Choose only a term marked `agentWalletSupported: true`,
|
|
145
148
|
`sponsoredNetworkFee: true`, and `buyerNativeFeeRequired: false`. The supported
|
|
@@ -149,7 +152,7 @@ reservation or any buyer-funded, unsupported, or unbound alternative.
|
|
|
149
152
|
|
|
150
153
|
x402api pays actual gas from its platform treasury. The merchant tenant's
|
|
151
154
|
active sponsorship allowance controls admission, but actual gas is not a
|
|
152
|
-
tenant debit. During the coordinated rollout, accept only the exact 0.2.
|
|
155
|
+
tenant debit. During the coordinated rollout, accept only the exact 0.2.8
|
|
153
156
|
platform-treasury declaration or the matched legacy tenant-credit declaration;
|
|
154
157
|
never accept a mixed billing and final-charge policy.
|
|
155
158
|
|
|
@@ -196,10 +199,12 @@ current `termEndsAt` generation are different.
|
|
|
196
199
|
- Timeout or process restart after authorization: use the saved x402api attempt
|
|
197
200
|
ID and artifact. Reconcile with WarpMetal and reuse its submit argv. Never
|
|
198
201
|
authorize again merely because submission is unknown.
|
|
199
|
-
- Signed HTTP 402: the artifact was definitively rejected.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
- Signed HTTP 402: the artifact was definitively rejected. A `paymentId` may be
|
|
203
|
+
absent because settlement never began; preserve the safe `errorCode`,
|
|
204
|
+
`requestId`, and `replacementAllowed` result instead. Only when
|
|
205
|
+
`replacementAllowed` is true may you run the unsigned WarpMetal challenge
|
|
206
|
+
flow again, inspect the replacement terms, and authorize the one permitted
|
|
207
|
+
replacement artifact. When false or absent, stop without signing again.
|
|
203
208
|
- `sponsorship_allowance_unavailable`, `sponsorship_payment_cap_exceeded`,
|
|
204
209
|
`sponsorship_payment_allowance_exhausted`,
|
|
205
210
|
`sponsorship_volume_allowance_exhausted`, or
|
|
@@ -207,7 +212,12 @@ current `termEndsAt` generation are different.
|
|
|
207
212
|
terminal. The merchant tenant must restore or change its allowance and issue
|
|
208
213
|
a fresh challenge; never retry the artifact or fall back to buyer-funded gas.
|
|
209
214
|
- `manual_review`: payment or fulfillment may be final. Stop all payment and
|
|
210
|
-
mutation retries.
|
|
215
|
+
mutation retries. WarpMetal may continue read-only backend reconciliation for
|
|
216
|
+
`stale_payment_outcome`; check status on a later autonomous cycle without
|
|
217
|
+
submitting a payment. Only `expired` plus
|
|
218
|
+
`failure.code: payment_expired_unsettled`, `paidAt: null`, and
|
|
219
|
+
`retrySafe: true` proves the old charge expired unpaid and permits preparing
|
|
220
|
+
a new order under current standing authority. Never reuse the old signature.
|
|
211
221
|
- Expired or corrupt signed artifact, changed request digest, unexpected recipient,
|
|
212
222
|
unsupported network, asset, profile, sponsorship error, or request-binding
|
|
213
223
|
mismatch: stop instead of falling back or asking the buyer to fund ETH/SOL.
|
|
@@ -127,7 +127,9 @@ generated QR encoding only that address.
|
|
|
127
127
|
## Stop and recovery states
|
|
128
128
|
|
|
129
129
|
- Insufficient balance: notify once, stop, and recheck later.
|
|
130
|
-
- Signed HTTP 402:
|
|
130
|
+
- Signed HTTP 402: preserve the safe rejection diagnostics. Re-run prepare and
|
|
131
|
+
re-evaluate one replacement challenge only when `replacementAllowed` is true;
|
|
132
|
+
stop when false or absent.
|
|
131
133
|
- Pending or timeout after authorization: reuse the same artifact; do not sign again.
|
|
132
134
|
- Price or payment rail outside policy: stop for deliberate policy revision.
|
|
133
135
|
- Exhausted count, horizon, or cumulative budget: stop for deliberate policy revision.
|
|
@@ -117,9 +117,11 @@ Confirm the plan, hostname, OS, and public key before preparing it.
|
|
|
117
117
|
- For `payment_pending`, retry the exact checkout body and the exact same
|
|
118
118
|
payment artifact. Preserve the returned `paymentId` as the durable x402api
|
|
119
119
|
reconciliation key. Do not create a replacement payment.
|
|
120
|
-
- A signed HTTP 402 rejects that signature.
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
- A signed HTTP 402 rejects that signature. A `paymentId` can be absent;
|
|
121
|
+
preserve the safe `errorCode`, `requestId`, and `replacementAllowed` result.
|
|
122
|
+
Request a new live challenge and create the one permitted replacement only
|
|
123
|
+
when `replacementAllowed` is true and the new terms remain within current
|
|
124
|
+
interactive or autonomous payment authority. When false or absent, stop.
|
|
123
125
|
- Treat `sponsorship_allowance_unavailable`,
|
|
124
126
|
`sponsorship_payment_cap_exceeded`,
|
|
125
127
|
`sponsorship_payment_allowance_exhausted`,
|
|
@@ -131,8 +133,11 @@ Confirm the plan, hostname, OS, and public key before preparing it.
|
|
|
131
133
|
attempt and artifact through WarpMetal. Never authorize again merely because
|
|
132
134
|
submission or fulfillment is uncertain, and never send the private owner
|
|
133
135
|
token through `x402api pay`, `payment submit`, or `payment reconcile`.
|
|
134
|
-
- Treat `manual_review` as terminal
|
|
135
|
-
mutation.
|
|
136
|
+
- Treat `manual_review` as terminal for payments and mutations. Do not pay
|
|
137
|
+
again and do not repeat the mutation. A later read-only status check may
|
|
138
|
+
observe automatic reconciliation of `stale_payment_outcome`; only the exact
|
|
139
|
+
`expired/payment_expired_unsettled` result with `paidAt: null` and
|
|
140
|
+
`retrySafe: true` permits preparing a new order.
|
|
136
141
|
- Treat HTTP 202 as accepted or pending, never as proof of success. Poll the
|
|
137
142
|
returned task or operation until a documented terminal state.
|
|
138
143
|
- Temporary sandbox expiry is a local hard deadline. Stop, restart, and
|
package/src/cli.js
CHANGED
|
@@ -385,8 +385,23 @@ function challengeResult(
|
|
|
385
385
|
response,
|
|
386
386
|
{ requireChallenge = true } = {},
|
|
387
387
|
) {
|
|
388
|
+
const status = response.data?.status;
|
|
389
|
+
const rejected = response.status === 402 && status === "payment_rejected";
|
|
388
390
|
const paymentRequired = response.headers["payment-required"];
|
|
389
391
|
const challengeHandle = response.headers["x-x402api-challenge-handle"];
|
|
392
|
+
const headerChallengeDigest =
|
|
393
|
+
response.headers["x-x402api-challenge-digest"];
|
|
394
|
+
const bodyChallengeDigest = response.data?.challengeDigest;
|
|
395
|
+
if (
|
|
396
|
+
headerChallengeDigest &&
|
|
397
|
+
bodyChallengeDigest &&
|
|
398
|
+
headerChallengeDigest !== bodyChallengeDigest
|
|
399
|
+
) {
|
|
400
|
+
throw new CliError(
|
|
401
|
+
"WarpMetal returned conflicting x402 challenge digests.",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const challengeDigest = bodyChallengeDigest || headerChallengeDigest;
|
|
390
405
|
if (requireChallenge && response.status === 402 && !paymentRequired) {
|
|
391
406
|
throw new CliError("WarpMetal returned HTTP 402 without PAYMENT-REQUIRED.");
|
|
392
407
|
}
|
|
@@ -395,15 +410,28 @@ function challengeResult(
|
|
|
395
410
|
"WarpMetal returned HTTP 402 without X-X402API-Challenge-Handle.",
|
|
396
411
|
);
|
|
397
412
|
}
|
|
413
|
+
if (paymentRequired && !challengeDigest) {
|
|
414
|
+
throw new CliError(
|
|
415
|
+
"WarpMetal returned PAYMENT-REQUIRED without X-X402API-Challenge-Digest.",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
398
418
|
return {
|
|
399
|
-
status
|
|
419
|
+
status,
|
|
400
420
|
taskId,
|
|
401
421
|
paymentAttemptId:
|
|
402
422
|
response.data?.paymentAttemptId ||
|
|
403
423
|
response.headers["x-warpmetal-payment-attempt"],
|
|
404
424
|
paymentRequired,
|
|
405
425
|
challengeHandle,
|
|
426
|
+
challengeDigest,
|
|
406
427
|
checkoutBodySha256: createHash("sha256").update(checkoutBody).digest("hex"),
|
|
428
|
+
...(rejected
|
|
429
|
+
? {
|
|
430
|
+
errorCode: response.data?.errorCode,
|
|
431
|
+
requestId: response.data?.requestId,
|
|
432
|
+
replacementAllowed: response.data?.replacementAllowed,
|
|
433
|
+
}
|
|
434
|
+
: {}),
|
|
407
435
|
};
|
|
408
436
|
}
|
|
409
437
|
|
|
@@ -421,6 +449,7 @@ async function attachPaymentWorkflow(
|
|
|
421
449
|
checkoutPath: order.checkoutPath,
|
|
422
450
|
checkoutBody: order.checkoutBody,
|
|
423
451
|
paymentRequired: safe.paymentRequired,
|
|
452
|
+
challengeDigest: safe.challengeDigest,
|
|
424
453
|
merchantReference: taskId,
|
|
425
454
|
});
|
|
426
455
|
const defaults = defaultPaymentPaths(
|
|
@@ -718,6 +747,7 @@ async function handleCheckoutSubmit(client, store, options, context) {
|
|
|
718
747
|
checkoutPath: order.checkoutPath,
|
|
719
748
|
checkoutBody: order.checkoutBody,
|
|
720
749
|
paymentRequired: order.paymentRequired,
|
|
750
|
+
challengeDigest: order.paymentChallengeDigest,
|
|
721
751
|
merchantReference: taskId,
|
|
722
752
|
});
|
|
723
753
|
walletPayment = await readPaymentArtifact(artifactFile, expected);
|
|
@@ -753,6 +783,14 @@ async function handleCheckoutSubmit(client, store, options, context) {
|
|
|
753
783
|
requireChallenge: false,
|
|
754
784
|
}),
|
|
755
785
|
);
|
|
786
|
+
if (response.status === 402 && safe.status === "payment_rejected") {
|
|
787
|
+
await store.saveOrderPaymentRejection(taskId, {
|
|
788
|
+
paymentAttemptId: safe.paymentAttemptId,
|
|
789
|
+
errorCode: safe.errorCode,
|
|
790
|
+
requestId: safe.requestId,
|
|
791
|
+
replacementAllowed: safe.replacementAllowed,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
756
794
|
const retryable =
|
|
757
795
|
response.status === 202 &&
|
|
758
796
|
["payment_pending", "payment_finalizing"].includes(response.data?.status);
|
|
@@ -1026,6 +1064,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
1026
1064
|
checkoutPath,
|
|
1027
1065
|
checkoutBody: bodyText,
|
|
1028
1066
|
paymentRequired: safe.paymentRequired,
|
|
1067
|
+
challengeDigest: safe.challengeDigest,
|
|
1029
1068
|
merchantReference: `renew:${serverId}:${server.termEndsAt || "unset"}`,
|
|
1030
1069
|
});
|
|
1031
1070
|
const matchingTerms = request.terms.filter(
|
|
@@ -1300,6 +1339,7 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1300
1339
|
checkoutPath: renewal.checkoutPath,
|
|
1301
1340
|
checkoutBody: renewal.checkoutBody,
|
|
1302
1341
|
paymentRequired: renewal.paymentRequired,
|
|
1342
|
+
challengeDigest: renewal.paymentChallengeDigest,
|
|
1303
1343
|
merchantReference: `renew:${serverId}:${renewal.termEndsAt || "unset"}`,
|
|
1304
1344
|
});
|
|
1305
1345
|
const walletPayment = await readPaymentArtifact(artifactFile, expected);
|
|
@@ -1341,6 +1381,16 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1341
1381
|
if (response.data?.paymentId) {
|
|
1342
1382
|
await store.saveRenewalPaymentId(serverId, response.data.paymentId);
|
|
1343
1383
|
}
|
|
1384
|
+
if (response.status === 402 && response.data?.status === "payment_rejected") {
|
|
1385
|
+
await store.saveRenewalPaymentRejection(serverId, {
|
|
1386
|
+
paymentAttemptId:
|
|
1387
|
+
response.data?.paymentAttemptId ||
|
|
1388
|
+
response.headers["x-warpmetal-payment-attempt"],
|
|
1389
|
+
errorCode: response.data?.errorCode,
|
|
1390
|
+
requestId: response.data?.requestId,
|
|
1391
|
+
replacementAllowed: response.data?.replacementAllowed,
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1344
1394
|
const retryable =
|
|
1345
1395
|
response.status === 202 &&
|
|
1346
1396
|
["payment_pending", "payment_finalizing"].includes(response.data?.status);
|
|
@@ -1357,6 +1407,9 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1357
1407
|
paymentAttemptId:
|
|
1358
1408
|
response.data?.paymentAttemptId ||
|
|
1359
1409
|
response.headers["x-warpmetal-payment-attempt"],
|
|
1410
|
+
errorCode: response.data?.errorCode,
|
|
1411
|
+
requestId: response.data?.requestId,
|
|
1412
|
+
replacementAllowed: response.data?.replacementAllowed,
|
|
1360
1413
|
walletPayment: {
|
|
1361
1414
|
attemptId: walletPayment.attemptId,
|
|
1362
1415
|
wallet: walletPayment.wallet,
|
package/src/payment.js
CHANGED
|
@@ -15,7 +15,7 @@ const MAX_ARTIFACT_BYTES = 1024 * 1024;
|
|
|
15
15
|
const MAX_SIGNATURE_BYTES = 512 * 1024;
|
|
16
16
|
|
|
17
17
|
export const AGENT_WALLET_PACKAGE = "@x402api/agent-wallet-cli";
|
|
18
|
-
export const AGENT_WALLET_VERSION = "0.2.
|
|
18
|
+
export const AGENT_WALLET_VERSION = "0.2.8";
|
|
19
19
|
const AGENT_WALLET_SPEC = `${AGENT_WALLET_PACKAGE}@${AGENT_WALLET_VERSION}`;
|
|
20
20
|
const GAS_SPONSORSHIP_EXTENSION = "com.x402api.gas-sponsorship";
|
|
21
21
|
const BASE_NETWORK = "eip155:8453";
|
|
@@ -399,7 +399,7 @@ export function parsePaymentRequired(value, expectedUrl) {
|
|
|
399
399
|
const sponsorship = extensions[GAS_SPONSORSHIP_EXTENSION]?.info;
|
|
400
400
|
return {
|
|
401
401
|
challenge: parsed,
|
|
402
|
-
|
|
402
|
+
paymentRequiredDigest: digestJson(parsed),
|
|
403
403
|
accepts,
|
|
404
404
|
sponsorshipExpiresAt: sponsorship?.expiresAt,
|
|
405
405
|
terms: accepts.map((requirement) => {
|
|
@@ -434,6 +434,7 @@ export function createPaymentRequestEnvelope({
|
|
|
434
434
|
checkoutPath,
|
|
435
435
|
checkoutBody,
|
|
436
436
|
paymentRequired,
|
|
437
|
+
challengeDigest,
|
|
437
438
|
merchantReference,
|
|
438
439
|
}) {
|
|
439
440
|
const url = new URL(
|
|
@@ -441,6 +442,14 @@ export function createPaymentRequestEnvelope({
|
|
|
441
442
|
`${baseUrl.replace(/\/$/, "")}/`,
|
|
442
443
|
).toString();
|
|
443
444
|
const parsed = parsePaymentRequired(paymentRequired, url);
|
|
445
|
+
if (typeof challengeDigest !== "string" || !SHA256.test(challengeDigest)) {
|
|
446
|
+
throw invalid("The authoritative x402 challenge digest is malformed.");
|
|
447
|
+
}
|
|
448
|
+
if (challengeDigest === parsed.paymentRequiredDigest) {
|
|
449
|
+
throw invalid(
|
|
450
|
+
"The x402 challenge digest was derived from PAYMENT-REQUIRED instead of the authoritative charge response.",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
444
453
|
const supportedTerms = parsed.terms.filter(
|
|
445
454
|
(term) => term.agentWalletSupported,
|
|
446
455
|
);
|
|
@@ -466,13 +475,13 @@ export function createPaymentRequestEnvelope({
|
|
|
466
475
|
contentType: "application/json",
|
|
467
476
|
bodyBase64: Buffer.from(checkoutBody, "utf8").toString("base64"),
|
|
468
477
|
paymentRequired,
|
|
469
|
-
challengeDigest
|
|
478
|
+
challengeDigest,
|
|
470
479
|
merchantReference,
|
|
471
480
|
};
|
|
472
481
|
return {
|
|
473
482
|
envelope,
|
|
474
483
|
requestDigest: digestJson(envelope),
|
|
475
|
-
challengeDigest
|
|
484
|
+
challengeDigest,
|
|
476
485
|
challenge: parsed.challenge,
|
|
477
486
|
sponsorshipExpiresAt: parsed.sponsorshipExpiresAt,
|
|
478
487
|
requirementDigests: new Set(
|
package/src/state.js
CHANGED
|
@@ -14,9 +14,60 @@ const WALLET_PAYMENT_STATE_FIELDS = [
|
|
|
14
14
|
"paymentArtifactExpiresAt",
|
|
15
15
|
"paymentArtifactSavedAt",
|
|
16
16
|
"gatewayPaymentId",
|
|
17
|
+
"rejectedPaymentAttemptId",
|
|
18
|
+
"paymentRejectionErrorCode",
|
|
19
|
+
"paymentRejectionRequestId",
|
|
20
|
+
"paymentReplacementAllowed",
|
|
21
|
+
"paymentRejectedAt",
|
|
17
22
|
];
|
|
18
23
|
const X402API_PAYMENT_ID =
|
|
19
24
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
25
|
+
const SAFE_PROTOCOL_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
26
|
+
|
|
27
|
+
function validatedPaymentRejection({
|
|
28
|
+
paymentAttemptId,
|
|
29
|
+
errorCode,
|
|
30
|
+
requestId,
|
|
31
|
+
replacementAllowed,
|
|
32
|
+
}) {
|
|
33
|
+
if (
|
|
34
|
+
typeof paymentAttemptId !== "string" ||
|
|
35
|
+
!SAFE_PROTOCOL_IDENTIFIER.test(paymentAttemptId)
|
|
36
|
+
) {
|
|
37
|
+
throw new CliError("WarpMetal returned an invalid payment-attempt ID.", {
|
|
38
|
+
exitCode: 3,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
for (const [label, value] of [
|
|
42
|
+
["payment rejection error code", errorCode],
|
|
43
|
+
["payment rejection request ID", requestId],
|
|
44
|
+
]) {
|
|
45
|
+
if (
|
|
46
|
+
value !== undefined &&
|
|
47
|
+
(typeof value !== "string" || !SAFE_PROTOCOL_IDENTIFIER.test(value))
|
|
48
|
+
) {
|
|
49
|
+
throw new CliError(`WarpMetal returned an invalid ${label}.`, {
|
|
50
|
+
exitCode: 3,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
replacementAllowed !== undefined &&
|
|
56
|
+
typeof replacementAllowed !== "boolean"
|
|
57
|
+
) {
|
|
58
|
+
throw new CliError(
|
|
59
|
+
"WarpMetal returned an invalid payment replacement decision.",
|
|
60
|
+
{ exitCode: 3 },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
rejectedPaymentAttemptId: paymentAttemptId,
|
|
65
|
+
paymentRejectionErrorCode: errorCode,
|
|
66
|
+
paymentRejectionRequestId: requestId,
|
|
67
|
+
paymentReplacementAllowed: replacementAllowed,
|
|
68
|
+
paymentRejectedAt: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
20
71
|
|
|
21
72
|
function replaceChallenge(record, challenge) {
|
|
22
73
|
const changed =
|
|
@@ -386,6 +437,28 @@ export class StateStore {
|
|
|
386
437
|
});
|
|
387
438
|
}
|
|
388
439
|
|
|
440
|
+
async saveOrderPaymentRejection(taskId, rejection) {
|
|
441
|
+
const safe = validatedPaymentRejection(rejection);
|
|
442
|
+
await this.update((state) => {
|
|
443
|
+
const order = state.orders[taskId];
|
|
444
|
+
if (!order) {
|
|
445
|
+
throw new CliError(`No local order state exists for ${taskId}.`, {
|
|
446
|
+
exitCode: 2,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (
|
|
450
|
+
order.paymentAttemptId &&
|
|
451
|
+
order.paymentAttemptId !== safe.rejectedPaymentAttemptId
|
|
452
|
+
) {
|
|
453
|
+
throw new CliError(
|
|
454
|
+
"WarpMetal changed the payment-attempt ID for this authorization.",
|
|
455
|
+
{ exitCode: 3 },
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
Object.assign(order, safe);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
389
462
|
async saveRenewalPaymentId(serverId, paymentId) {
|
|
390
463
|
if (!X402API_PAYMENT_ID.test(paymentId || "")) {
|
|
391
464
|
throw new CliError("WarpMetal returned an invalid x402api payment ID.", {
|
|
@@ -408,6 +481,28 @@ export class StateStore {
|
|
|
408
481
|
});
|
|
409
482
|
}
|
|
410
483
|
|
|
484
|
+
async saveRenewalPaymentRejection(serverId, rejection) {
|
|
485
|
+
const safe = validatedPaymentRejection(rejection);
|
|
486
|
+
await this.update((state) => {
|
|
487
|
+
const renewal = state.renewals[serverId];
|
|
488
|
+
if (!renewal) {
|
|
489
|
+
throw new CliError(`No local renewal state exists for ${serverId}.`, {
|
|
490
|
+
exitCode: 2,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (
|
|
494
|
+
renewal.paymentAttemptId &&
|
|
495
|
+
renewal.paymentAttemptId !== safe.rejectedPaymentAttemptId
|
|
496
|
+
) {
|
|
497
|
+
throw new CliError(
|
|
498
|
+
"WarpMetal changed the payment-attempt ID for this renewal authorization.",
|
|
499
|
+
{ exitCode: 3 },
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
Object.assign(renewal, safe);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
411
506
|
async saveAccessToken(serverId, accessToken, expiresAt) {
|
|
412
507
|
await this.update((state) => {
|
|
413
508
|
const server = state.servers[serverId] || { serverId };
|
|
@@ -541,6 +636,10 @@ export class StateStore {
|
|
|
541
636
|
walletName: order.walletName,
|
|
542
637
|
payerAddress: order.walletPayerAddress,
|
|
543
638
|
paymentArtifactExpiresAt: order.paymentArtifactExpiresAt,
|
|
639
|
+
rejectedPaymentAttemptId: order.rejectedPaymentAttemptId,
|
|
640
|
+
paymentRejectionErrorCode: order.paymentRejectionErrorCode,
|
|
641
|
+
paymentRejectionRequestId: order.paymentRejectionRequestId,
|
|
642
|
+
paymentReplacementAllowed: order.paymentReplacementAllowed,
|
|
544
643
|
credentialStored: Boolean(order.ownerToken),
|
|
545
644
|
})),
|
|
546
645
|
servers: Object.values(state.servers).map((server) => ({
|
|
@@ -574,6 +673,10 @@ export class StateStore {
|
|
|
574
673
|
paymentId: renewal.gatewayPaymentId,
|
|
575
674
|
challengeHandle: renewal.challengeHandle,
|
|
576
675
|
paymentArtifactExpiresAt: renewal.paymentArtifactExpiresAt,
|
|
676
|
+
rejectedPaymentAttemptId: renewal.rejectedPaymentAttemptId,
|
|
677
|
+
paymentRejectionErrorCode: renewal.paymentRejectionErrorCode,
|
|
678
|
+
paymentRejectionRequestId: renewal.paymentRejectionRequestId,
|
|
679
|
+
paymentReplacementAllowed: renewal.paymentReplacementAllowed,
|
|
577
680
|
})),
|
|
578
681
|
};
|
|
579
682
|
}
|