warpmetal 0.7.6 → 0.7.7
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 +1 -1
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +20 -5
- package/skills/warpmetal/references/payments.md +12 -5
- package/skills/warpmetal/references/renewals.md +3 -1
- package/skills/warpmetal/references/safety.md +10 -5
- package/src/cli.js +31 -1
- package/src/state.js +103 -0
package/README.md
CHANGED
|
@@ -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.7 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.7` 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
|
|
@@ -196,10 +196,12 @@ current `termEndsAt` generation are different.
|
|
|
196
196
|
- Timeout or process restart after authorization: use the saved x402api attempt
|
|
197
197
|
ID and artifact. Reconcile with WarpMetal and reuse its submit argv. Never
|
|
198
198
|
authorize again merely because submission is unknown.
|
|
199
|
-
- Signed HTTP 402: the artifact was definitively rejected.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
- Signed HTTP 402: the artifact was definitively rejected. A `paymentId` may be
|
|
200
|
+
absent because settlement never began; preserve the safe `errorCode`,
|
|
201
|
+
`requestId`, and `replacementAllowed` result instead. Only when
|
|
202
|
+
`replacementAllowed` is true may you run the unsigned WarpMetal challenge
|
|
203
|
+
flow again, inspect the replacement terms, and authorize the one permitted
|
|
204
|
+
replacement artifact. When false or absent, stop without signing again.
|
|
203
205
|
- `sponsorship_allowance_unavailable`, `sponsorship_payment_cap_exceeded`,
|
|
204
206
|
`sponsorship_payment_allowance_exhausted`,
|
|
205
207
|
`sponsorship_volume_allowance_exhausted`, or
|
|
@@ -207,7 +209,12 @@ current `termEndsAt` generation are different.
|
|
|
207
209
|
terminal. The merchant tenant must restore or change its allowance and issue
|
|
208
210
|
a fresh challenge; never retry the artifact or fall back to buyer-funded gas.
|
|
209
211
|
- `manual_review`: payment or fulfillment may be final. Stop all payment and
|
|
210
|
-
mutation retries.
|
|
212
|
+
mutation retries. WarpMetal may continue read-only backend reconciliation for
|
|
213
|
+
`stale_payment_outcome`; check status on a later autonomous cycle without
|
|
214
|
+
submitting a payment. Only `expired` plus
|
|
215
|
+
`failure.code: payment_expired_unsettled`, `paidAt: null`, and
|
|
216
|
+
`retrySafe: true` proves the old charge expired unpaid and permits preparing
|
|
217
|
+
a new order under current standing authority. Never reuse the old signature.
|
|
211
218
|
- Expired or corrupt signed artifact, changed request digest, unexpected recipient,
|
|
212
219
|
unsupported network, asset, profile, sponsorship error, or request-binding
|
|
213
220
|
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,6 +385,8 @@ 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"];
|
|
390
392
|
if (requireChallenge && response.status === 402 && !paymentRequired) {
|
|
@@ -396,7 +398,7 @@ function challengeResult(
|
|
|
396
398
|
);
|
|
397
399
|
}
|
|
398
400
|
return {
|
|
399
|
-
status
|
|
401
|
+
status,
|
|
400
402
|
taskId,
|
|
401
403
|
paymentAttemptId:
|
|
402
404
|
response.data?.paymentAttemptId ||
|
|
@@ -404,6 +406,13 @@ function challengeResult(
|
|
|
404
406
|
paymentRequired,
|
|
405
407
|
challengeHandle,
|
|
406
408
|
checkoutBodySha256: createHash("sha256").update(checkoutBody).digest("hex"),
|
|
409
|
+
...(rejected
|
|
410
|
+
? {
|
|
411
|
+
errorCode: response.data?.errorCode,
|
|
412
|
+
requestId: response.data?.requestId,
|
|
413
|
+
replacementAllowed: response.data?.replacementAllowed,
|
|
414
|
+
}
|
|
415
|
+
: {}),
|
|
407
416
|
};
|
|
408
417
|
}
|
|
409
418
|
|
|
@@ -753,6 +762,14 @@ async function handleCheckoutSubmit(client, store, options, context) {
|
|
|
753
762
|
requireChallenge: false,
|
|
754
763
|
}),
|
|
755
764
|
);
|
|
765
|
+
if (response.status === 402 && safe.status === "payment_rejected") {
|
|
766
|
+
await store.saveOrderPaymentRejection(taskId, {
|
|
767
|
+
paymentAttemptId: safe.paymentAttemptId,
|
|
768
|
+
errorCode: safe.errorCode,
|
|
769
|
+
requestId: safe.requestId,
|
|
770
|
+
replacementAllowed: safe.replacementAllowed,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
756
773
|
const retryable =
|
|
757
774
|
response.status === 202 &&
|
|
758
775
|
["payment_pending", "payment_finalizing"].includes(response.data?.status);
|
|
@@ -1341,6 +1358,16 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1341
1358
|
if (response.data?.paymentId) {
|
|
1342
1359
|
await store.saveRenewalPaymentId(serverId, response.data.paymentId);
|
|
1343
1360
|
}
|
|
1361
|
+
if (response.status === 402 && response.data?.status === "payment_rejected") {
|
|
1362
|
+
await store.saveRenewalPaymentRejection(serverId, {
|
|
1363
|
+
paymentAttemptId:
|
|
1364
|
+
response.data?.paymentAttemptId ||
|
|
1365
|
+
response.headers["x-warpmetal-payment-attempt"],
|
|
1366
|
+
errorCode: response.data?.errorCode,
|
|
1367
|
+
requestId: response.data?.requestId,
|
|
1368
|
+
replacementAllowed: response.data?.replacementAllowed,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1344
1371
|
const retryable =
|
|
1345
1372
|
response.status === 202 &&
|
|
1346
1373
|
["payment_pending", "payment_finalizing"].includes(response.data?.status);
|
|
@@ -1357,6 +1384,9 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1357
1384
|
paymentAttemptId:
|
|
1358
1385
|
response.data?.paymentAttemptId ||
|
|
1359
1386
|
response.headers["x-warpmetal-payment-attempt"],
|
|
1387
|
+
errorCode: response.data?.errorCode,
|
|
1388
|
+
requestId: response.data?.requestId,
|
|
1389
|
+
replacementAllowed: response.data?.replacementAllowed,
|
|
1360
1390
|
walletPayment: {
|
|
1361
1391
|
attemptId: walletPayment.attemptId,
|
|
1362
1392
|
wallet: walletPayment.wallet,
|
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
|
}
|