warpmetal 0.7.9 → 0.8.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 +33 -13
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +14 -3
- package/skills/warpmetal/references/cli-reference.md +23 -7
- package/skills/warpmetal/references/payments.md +14 -6
- package/skills/warpmetal/references/renewals.md +8 -6
- package/skills/warpmetal/references/safety.md +3 -0
- package/src/api.js +28 -4
- package/src/cli.js +235 -56
- package/src/payment.js +4 -1
- package/src/state.js +80 -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.9
|
|
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.
|
|
61
|
+
artifact from the npm CLI and requires `warpmetal` CLI version 0.8.1 or newer.
|
|
62
62
|
|
|
63
63
|
To test the repository marketplace after the plugin lands on `main`:
|
|
64
64
|
|
|
@@ -130,6 +130,12 @@ external-recipient extension bindings to match the advertised alternatives
|
|
|
130
130
|
exactly, then preserves the chosen authorization artifact byte-for-byte for
|
|
131
131
|
every pending or ambiguous retry.
|
|
132
132
|
|
|
133
|
+
Submission output includes the server-validated `paymentId`, `confirmed`, and
|
|
134
|
+
`finalized` fields when WarpMetal supplies lifecycle evidence. Retry only while
|
|
135
|
+
confirmation is pending. As soon as `confirmed` is true, stop all payment
|
|
136
|
+
submission—even when `finalized` is false—and let WarpMetal continue signed-
|
|
137
|
+
receipt finality while provisioning or renewal proceeds.
|
|
138
|
+
|
|
133
139
|
- When a human is actively chatting with the agent, show the exact live terms
|
|
134
140
|
and ask for confirmation immediately before authorizing and submitting.
|
|
135
141
|
- In an unattended run, a pre-funded dedicated wallet is standing spend
|
|
@@ -197,7 +203,7 @@ or escalation mechanism or stop with `funding_required`.
|
|
|
197
203
|
|
|
198
204
|
Configure renewal only with explicit bounds. This example allows at most 12
|
|
199
205
|
renewals, enforces a 30 USDC per-payment ceiling and a 360 USDC cumulative
|
|
200
|
-
budget, and
|
|
206
|
+
budget, and activates a human notification recipient after server authorization:
|
|
201
207
|
|
|
202
208
|
```sh
|
|
203
209
|
warpmetal renewal configure \
|
|
@@ -213,12 +219,26 @@ warpmetal renewal configure \
|
|
|
213
219
|
--json
|
|
214
220
|
```
|
|
215
221
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
No email verification is required: the saved owner credential or an SSH-derived
|
|
223
|
+
server token authorizes the change. WarpMetal queues a branded transactional
|
|
224
|
+
advisory identifying the server, why the address was added, and a link that
|
|
225
|
+
removes only that address. Up to five active addresses may be attached
|
|
226
|
+
to one server. If none exists, `renewal configure` returns `email_required`
|
|
227
|
+
before changing policy. Supply `--email`, or deliberately continue with
|
|
228
|
+
`--without-email-notifications`; the latter does not enable signed refill-email
|
|
229
|
+
workflows. A recurring unattended agent can then run:
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
warpmetal notifications add --server <serverId> --email ops@example.com --json
|
|
233
|
+
warpmetal notifications list --server <serverId> --json
|
|
234
|
+
warpmetal notifications remove --server <serverId> --recipient <recipientId> --json
|
|
235
|
+
warpmetal notifications events --server <serverId> --events renewal.due,server.ready --json
|
|
236
|
+
warpmetal notifications disable --server <serverId> --json
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
After provisioning, `warpmetal order status` includes the structured
|
|
240
|
+
`ask_human_for_notification_email` next action until a recipient is active or
|
|
241
|
+
the human explicitly opts out. Public CLI output contains only masked addresses.
|
|
222
242
|
|
|
223
243
|
```sh
|
|
224
244
|
warpmetal renewal due --all --json
|
|
@@ -227,17 +247,17 @@ warpmetal renewal run --all-due --json
|
|
|
227
247
|
|
|
228
248
|
Inside policy, the CLI returns the exact Agent Wallet authorization and submit
|
|
229
249
|
argv. If balance is insufficient, `refillWorkflow` is returned only when the
|
|
230
|
-
server has an active
|
|
250
|
+
server has an active notification recipient. Run its argv with the
|
|
231
251
|
returned `X402API_NOTIFICATION_URL` environment value. `x402api wallet
|
|
232
252
|
notify-refill` signs an opaque subscription reference and wallet-produced
|
|
233
253
|
balance fields; it cannot choose an email address. WarpMetal verifies the
|
|
234
|
-
wallet signature and current on-chain balance before emailing
|
|
235
|
-
human the network, stablecoin, public wallet address, required minimum top-up,
|
|
254
|
+
wallet signature and current on-chain balance before emailing every active
|
|
255
|
+
human recipient the network, stablecoin, public wallet address, required minimum top-up,
|
|
236
256
|
and a locally generated QR encoding only that wallet address. The address is
|
|
237
257
|
also repeated as copyable text. The human may transfer more than that minimum;
|
|
238
258
|
the renewal policy—not the refill target—remains the spending authority.
|
|
239
259
|
|
|
240
|
-
The agent never sends a partial x402 payment. If no
|
|
260
|
+
The agent never sends a partial x402 payment. If no active recipient refill path
|
|
241
261
|
exists, it reports `funding_required`. If a previous payment is pending or
|
|
242
262
|
ambiguous, it reconciles the saved attempt and never signs a second payment.
|
|
243
263
|
|
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.
|
|
14
|
+
1. Run `warpmetal --version` and require version `0.8.1` 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`.
|
|
@@ -173,11 +173,13 @@ use `paymentWorkflow.fundingWorkflow` to obtain the public address and balance,
|
|
|
173
173
|
then show the address as both a QR code and copyable text. Only when
|
|
174
174
|
`refillNotification.available` is true, set the returned
|
|
175
175
|
`refillWorkflow.environment`, invoke its exact argv once, and stop until
|
|
176
|
-
funding arrives. The signed refill intent resolves to
|
|
176
|
+
funding arrives. The signed refill intent resolves server-side to the active
|
|
177
|
+
SSH-authorized human recipients;
|
|
177
178
|
never add an email address to it. Never make a partial payment.
|
|
178
179
|
|
|
179
180
|
After funding, prepare again, authorize exactly once, submit with WarpMetal,
|
|
180
|
-
and
|
|
181
|
+
and require `confirmed: true` before accepting the returned `termEndsAt`.
|
|
182
|
+
Stop payment submission at confirmation even when `finalized` is false. On `reconcile_pending` or
|
|
181
183
|
`manual_review`, do not create another authorization.
|
|
182
184
|
|
|
183
185
|
## Provision and manage
|
|
@@ -188,6 +190,15 @@ Poll a prepared or paid order with:
|
|
|
188
190
|
warpmetal order status --task <taskId> --wait --json
|
|
189
191
|
```
|
|
190
192
|
|
|
193
|
+
When the ready response contains
|
|
194
|
+
`nextAction.action: ask_human_for_notification_email`, ask the human whether
|
|
195
|
+
they want renewal and lifecycle notices. If yes, add the supplied address with
|
|
196
|
+
`warpmetal notifications add --server <serverId> --email <address> --json`.
|
|
197
|
+
The server credential is the authorization; there is no email verification
|
|
198
|
+
step. The address receives a branded advisory and can remove itself without
|
|
199
|
+
affecting other recipients. If the human declines, run the returned
|
|
200
|
+
`notifications disable` command so future status checks do not ask again.
|
|
201
|
+
|
|
191
202
|
`manual_review` remains terminal for payment and mutation attempts. A later
|
|
192
203
|
read-only status check may observe the backend's authoritative reconciliation;
|
|
193
204
|
prepare a replacement only for the exact `retrySafe: true` result described
|
|
@@ -72,7 +72,7 @@ retire that attempt and return a new `paymentAttemptId`. The CLI replaces the
|
|
|
72
72
|
saved challenge and stale wallet-attempt metadata; use only the newly returned
|
|
73
73
|
workflow.
|
|
74
74
|
|
|
75
|
-
The current integration targets `@x402api/agent-wallet-cli@0.2.
|
|
75
|
+
The current integration targets `@x402api/agent-wallet-cli@0.2.9`. A compatible
|
|
76
76
|
live term is marked `agentWalletSupported: true` and must use the sponsored
|
|
77
77
|
Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
|
|
78
78
|
For the current declaration, x402api pays actual gas from its platform treasury
|
|
@@ -83,6 +83,12 @@ term. Because the checkout is authenticated, do not substitute `x402api pay`,
|
|
|
83
83
|
`payment submit`, or `payment reconcile` for the returned WarpMetal submission
|
|
84
84
|
command.
|
|
85
85
|
|
|
86
|
+
`warpmetal checkout submit` and `warpmetal renewal submit` surface
|
|
87
|
+
`paymentId`, `confirmed`, and `finalized` from WarpMetal and persist those safe
|
|
88
|
+
lifecycle fields in private CLI state. `confirmed: true` is terminal for
|
|
89
|
+
payment submission and starts the business operation; `finalized: false` only
|
|
90
|
+
means signed-receipt reconciliation continues asynchronously.
|
|
91
|
+
|
|
86
92
|
`--payment-artifact` accepts the owner-only JSON artifact produced by a
|
|
87
93
|
compatible pinned x402api Agent Wallet release. WarpMetal validates its request
|
|
88
94
|
and payment-requirement digests, resource, extensions, buyer payment identifier,
|
|
@@ -125,15 +131,25 @@ warpmetal renewal prepare --server <serverId> --json
|
|
|
125
131
|
warpmetal renewal submit --server <serverId> \
|
|
126
132
|
--payment-artifact <path> [--wait] --json
|
|
127
133
|
warpmetal renewal run (--server <serverId> | --all-due) --json
|
|
128
|
-
warpmetal notifications
|
|
134
|
+
warpmetal notifications add --server <serverId> --email <address> \
|
|
129
135
|
[--events <comma-separated-events>] --json
|
|
130
|
-
warpmetal notifications
|
|
136
|
+
warpmetal notifications list --server <serverId> --json
|
|
137
|
+
warpmetal notifications remove --server <serverId> --recipient <recipientId> --json
|
|
138
|
+
warpmetal notifications events --server <serverId> \
|
|
139
|
+
--events <comma-separated-events> --json
|
|
140
|
+
warpmetal notifications disable --server <serverId> --json
|
|
131
141
|
```
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
After a server becomes ready, `order status` returns
|
|
144
|
+
`nextAction.action: ask_human_for_notification_email` when setup has not been
|
|
145
|
+
completed or dismissed. Ask the human for an optional address, then use
|
|
146
|
+
`notifications add`; the server credential authorizes immediate activation, so
|
|
147
|
+
there is no verification step. A branded advisory identifies the server and
|
|
148
|
+
provides recipient-scoped removal. Up to five active recipients are supported.
|
|
149
|
+
Renewal configuration asks for an email before mutating policy unless
|
|
150
|
+
`--without-email-notifications` explicitly opts out. `renewal prepare` always
|
|
151
|
+
returns safe funding address/balance argv and returns signed refill-email argv
|
|
152
|
+
only when at least one active recipient exists.
|
|
137
153
|
`renewal run` is an agent-facing state machine, not a wallet-signing daemon.
|
|
138
154
|
See [renewals.md](renewals.md).
|
|
139
155
|
|
|
@@ -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.9`. 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
|
|
|
@@ -39,7 +39,9 @@ skill automatically.
|
|
|
39
39
|
|
|
40
40
|
1. Install the exact `paymentWorkflow.signerPackage.install.argv` under Node
|
|
41
41
|
22. Run `command -v x402api` and the returned
|
|
42
|
-
`paymentWorkflow.signerContract.probe.argv`. Require contract version 1
|
|
42
|
+
`paymentWorkflow.signerContract.probe.argv`. Require contract version 1 and
|
|
43
|
+
every entry in `paymentWorkflow.signerContract.requiredCommands` to appear
|
|
44
|
+
in the probe's `commands` array.
|
|
43
45
|
2. Run the exact `paymentWorkflow.walletWorkflow.setup.argv`. The idempotent
|
|
44
46
|
`x402api wallet setup --json` command creates an owner-only managed unlock
|
|
45
47
|
file inside the x402api home directory and never prints its generated
|
|
@@ -152,7 +154,7 @@ reservation or any buyer-funded, unsupported, or unbound alternative.
|
|
|
152
154
|
|
|
153
155
|
x402api pays actual gas from its platform treasury. The merchant tenant's
|
|
154
156
|
active sponsorship allowance controls admission, but actual gas is not a
|
|
155
|
-
tenant debit. During the coordinated rollout, accept only the exact 0.2.
|
|
157
|
+
tenant debit. During the coordinated rollout, accept only the exact 0.2.9
|
|
156
158
|
platform-treasury declaration or the matched legacy tenant-credit declaration;
|
|
157
159
|
never accept a mixed billing and final-charge policy.
|
|
158
160
|
|
|
@@ -193,9 +195,15 @@ current `termEndsAt` generation are different.
|
|
|
193
195
|
wallet-attempt metadata when it saves the new challenge. Inspect the new
|
|
194
196
|
terms and authorize only the new workflow; never reuse an expired envelope or
|
|
195
197
|
artifact.
|
|
196
|
-
- `payment_pending
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
- `payment_pending`, or legacy `payment_finalizing`, without
|
|
199
|
+
`confirmed: true`: keep the same checkout bytes and artifact. Preserve the
|
|
200
|
+
returned durable `paymentId`; `--wait` records it in private WarpMetal state
|
|
201
|
+
and performs bounded retries with that exact authorization.
|
|
202
|
+
- `confirmed: true`: stop payment submission immediately, including when
|
|
203
|
+
`finalized` is false or a legacy pending-like status is present. WarpMetal
|
|
204
|
+
has admitted provisioning or renewal exactly once and will reconcile payment
|
|
205
|
+
detail plus the signed final receipt asynchronously. Never authorize a
|
|
206
|
+
replacement because receipt finality is pending.
|
|
199
207
|
- Timeout or process restart after authorization: use the saved x402api attempt
|
|
200
208
|
ID and artifact. Reconcile with WarpMetal and reuse its submit argv. Never
|
|
201
209
|
authorize again merely because submission is unknown.
|
|
@@ -27,9 +27,10 @@ warpmetal renewal configure \
|
|
|
27
27
|
--json
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Without an active
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Without an active notification recipient, configuration asks for one before
|
|
31
|
+
changing policy. `--email` immediately activates the address because the owner
|
|
32
|
+
or SSH-derived server credential authorizes the change. WarpMetal sends a
|
|
33
|
+
transactional advisory to the new address; it does not require verification.
|
|
33
34
|
Use `--without-email-notifications` only as an explicit opt-out; no signed
|
|
34
35
|
refill-email workflow is returned in that state. A refill target is a funding
|
|
35
36
|
target, not spend permission, and cannot exceed the per-renewal cap. The human
|
|
@@ -58,7 +59,8 @@ The CLI does not install a daemon. Treat actions as follows:
|
|
|
58
59
|
- `refill_required`: follow the signed refill workflow and stop.
|
|
59
60
|
- `reconcile_pending`: preserve the artifact and reconcile; never authorize again.
|
|
60
61
|
- `manual_review`: stop all payment and mutation retries.
|
|
61
|
-
- `renewed`: verify the new `termEndsAt` and stop
|
|
62
|
+
- `renewed` with `confirmed: true`: verify the new `termEndsAt` and stop
|
|
63
|
+
payment submission successfully, even when `finalized` is false.
|
|
62
64
|
|
|
63
65
|
## Authorize and submit renewal
|
|
64
66
|
|
|
@@ -75,8 +77,8 @@ paymentWorkflow.authorize.argv
|
|
|
75
77
|
paymentWorkflow.submit.argv
|
|
76
78
|
paymentWorkflow.fundingWorkflow
|
|
77
79
|
refillNotification
|
|
78
|
-
refillWorkflow.environment (
|
|
79
|
-
refillWorkflow.argv (
|
|
80
|
+
refillWorkflow.environment (active recipient only)
|
|
81
|
+
refillWorkflow.argv (active recipient only)
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
Invoke the authorize argv once. It writes an owner-only artifact. Then invoke
|
|
@@ -117,6 +117,9 @@ 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
|
+
- For `confirmed: true`, stop payment submission even when `finalized` is
|
|
121
|
+
false. Finality and reorg checks are read-only WarpMetal backend work; they
|
|
122
|
+
never authorize another payment.
|
|
120
123
|
- A signed HTTP 402 rejects that signature. A `paymentId` can be absent;
|
|
121
124
|
preserve the safe `errorCode`, `requestId`, and `replacementAllowed` result.
|
|
122
125
|
Request a new live challenge and create the one permitted replacement only
|
package/src/api.js
CHANGED
|
@@ -208,19 +208,43 @@ export class WarpMetalClient {
|
|
|
208
208
|
);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
putNotifications(serverId, body, token) {
|
|
211
|
+
putNotifications(serverId, body, token, idempotencyKey) {
|
|
212
212
|
return this.request(
|
|
213
213
|
"PUT",
|
|
214
214
|
`/servers/${encodeURIComponent(serverId)}/notifications`,
|
|
215
|
-
{ body, token },
|
|
215
|
+
{ body, token, idempotencyKey },
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
addNotificationRecipient(serverId, body, token, idempotencyKey) {
|
|
220
|
+
return this.request(
|
|
221
|
+
"POST",
|
|
222
|
+
`/servers/${encodeURIComponent(serverId)}/notification-recipients`,
|
|
223
|
+
{ body, token, idempotencyKey },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
patchNotifications(serverId, body, token, idempotencyKey) {
|
|
228
|
+
return this.request(
|
|
229
|
+
"PATCH",
|
|
230
|
+
`/servers/${encodeURIComponent(serverId)}/notifications`,
|
|
231
|
+
{ body, token, idempotencyKey },
|
|
216
232
|
);
|
|
217
233
|
}
|
|
218
234
|
|
|
219
|
-
deleteNotifications(serverId, token) {
|
|
235
|
+
deleteNotifications(serverId, token, idempotencyKey) {
|
|
220
236
|
return this.request(
|
|
221
237
|
"DELETE",
|
|
222
238
|
`/servers/${encodeURIComponent(serverId)}/notifications`,
|
|
223
|
-
{ token },
|
|
239
|
+
{ token, idempotencyKey },
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
deleteNotificationRecipient(serverId, recipientId, token, idempotencyKey) {
|
|
244
|
+
return this.request(
|
|
245
|
+
"DELETE",
|
|
246
|
+
`/servers/${encodeURIComponent(serverId)}/notification-recipients/${encodeURIComponent(recipientId)}`,
|
|
247
|
+
{ token, idempotencyKey },
|
|
224
248
|
);
|
|
225
249
|
}
|
|
226
250
|
|
package/src/cli.js
CHANGED
|
@@ -87,8 +87,14 @@ Usage:
|
|
|
87
87
|
[--email <address> | --without-email-notifications]
|
|
88
88
|
warpmetal renewal status|prepare|submit|run ...
|
|
89
89
|
warpmetal renewal due (--server <serverId> | --all)
|
|
90
|
-
warpmetal notifications
|
|
91
|
-
|
|
90
|
+
warpmetal notifications add --server <serverId> --email <address>
|
|
91
|
+
[--events <comma-separated-events>] [--idempotency-key <key>]
|
|
92
|
+
warpmetal notifications list --server <serverId>
|
|
93
|
+
warpmetal notifications remove --server <serverId> --recipient <recipientId>
|
|
94
|
+
[--idempotency-key <key>]
|
|
95
|
+
warpmetal notifications events --server <serverId> --events <comma-separated-events>
|
|
96
|
+
[--idempotency-key <key>]
|
|
97
|
+
warpmetal notifications disable --server <serverId> [--idempotency-key <key>]
|
|
92
98
|
warpmetal identity generate --hostname <name> [--ssh-key-name <name>]
|
|
93
99
|
warpmetal identity list
|
|
94
100
|
warpmetal server identity --server <serverId>
|
|
@@ -209,6 +215,42 @@ function suggestedDelay(result, fallback = 2) {
|
|
|
209
215
|
);
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
function paymentLifecycle(result) {
|
|
219
|
+
const confirmed = result.data?.confirmed;
|
|
220
|
+
const finalized = result.data?.finalized;
|
|
221
|
+
if (confirmed === undefined && finalized === undefined) {
|
|
222
|
+
return { confirmed: undefined, finalized: undefined };
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
typeof confirmed !== "boolean" ||
|
|
226
|
+
typeof finalized !== "boolean" ||
|
|
227
|
+
(finalized && !confirmed)
|
|
228
|
+
) {
|
|
229
|
+
throw new CliError("WarpMetal returned contradictory payment lifecycle evidence.", {
|
|
230
|
+
exitCode: 3,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (confirmed && !result.data?.paymentId) {
|
|
234
|
+
throw new CliError("WarpMetal confirmed payment without a payment ID.", {
|
|
235
|
+
exitCode: 3,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (["provisioning", "renewed"].includes(result.data?.status) && !confirmed) {
|
|
239
|
+
throw new CliError("WarpMetal returned a terminal payment status without confirmation.", {
|
|
240
|
+
exitCode: 3,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return { confirmed, finalized };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function paymentSubmissionPending(result, lifecycle) {
|
|
247
|
+
return (
|
|
248
|
+
result.status === 202 &&
|
|
249
|
+
lifecycle.confirmed !== true &&
|
|
250
|
+
["payment_pending", "payment_finalizing"].includes(result.data?.status)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
212
254
|
async function readHeaderValueFile(path, label) {
|
|
213
255
|
const value = (await readFile(resolve(path), "utf8")).trim();
|
|
214
256
|
if (!value || value.includes("\n") || value.includes("\r")) {
|
|
@@ -673,11 +715,40 @@ async function handleTaskStatus(client, store, options, context) {
|
|
|
673
715
|
const result = booleanOption(options, "wait")
|
|
674
716
|
? await pollTask(client, taskId, token, timeout)
|
|
675
717
|
: await client.getTask(taskId, token);
|
|
718
|
+
const output = { ...result.data };
|
|
719
|
+
let notificationMessage = "";
|
|
720
|
+
if (result.data.task.state === "ready" && result.data.task.serverId) {
|
|
721
|
+
try {
|
|
722
|
+
const notifications = (
|
|
723
|
+
await client.getNotifications(result.data.task.serverId, token)
|
|
724
|
+
).data;
|
|
725
|
+
output.notifications = notifications;
|
|
726
|
+
if (notifications.setupRecommended) {
|
|
727
|
+
output.nextAction = {
|
|
728
|
+
action: "ask_human_for_notification_email",
|
|
729
|
+
optional: true,
|
|
730
|
+
reason:
|
|
731
|
+
"A human can receive renewal, wallet refill, and server lifecycle notifications.",
|
|
732
|
+
addCommand: `warpmetal notifications add --server ${result.data.task.serverId} --email <address> --json`,
|
|
733
|
+
skipCommand: `warpmetal notifications disable --server ${result.data.task.serverId} --json`,
|
|
734
|
+
};
|
|
735
|
+
notificationMessage =
|
|
736
|
+
`\nAsk the human whether they want renewal and lifecycle notifications. ` +
|
|
737
|
+
`If yes, run: warpmetal notifications add --server ${result.data.task.serverId} --email <address>`;
|
|
738
|
+
}
|
|
739
|
+
} catch (error) {
|
|
740
|
+
output.notificationSetup = {
|
|
741
|
+
action: "check_notifications",
|
|
742
|
+
unavailable: true,
|
|
743
|
+
reason: toErrorMessage(error),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
676
747
|
emit(
|
|
677
748
|
context.stdout,
|
|
678
|
-
|
|
749
|
+
output,
|
|
679
750
|
context.json,
|
|
680
|
-
`${result.data.task.id}: ${result.data.task.state}${result.data.task.publicIp ? ` (${result.data.task.publicIp})` : ""}`,
|
|
751
|
+
`${result.data.task.id}: ${result.data.task.state}${result.data.task.publicIp ? ` (${result.data.task.publicIp})` : ""}${notificationMessage}`,
|
|
681
752
|
);
|
|
682
753
|
return result.data.task.state === "manual_review" ? 6 : 0;
|
|
683
754
|
}
|
|
@@ -771,8 +842,12 @@ async function handleCheckoutSubmit(client, store, options, context) {
|
|
|
771
842
|
token,
|
|
772
843
|
paymentSignature,
|
|
773
844
|
});
|
|
845
|
+
const lifecycle = paymentLifecycle(response);
|
|
774
846
|
if (response.data?.paymentId) {
|
|
775
847
|
await store.saveOrderPaymentId(taskId, response.data.paymentId);
|
|
848
|
+
if (lifecycle.confirmed !== undefined) {
|
|
849
|
+
await store.saveOrderPaymentOutcome(taskId, response.data.paymentId, lifecycle);
|
|
850
|
+
}
|
|
776
851
|
}
|
|
777
852
|
const safe = await attachPaymentWorkflow(
|
|
778
853
|
client,
|
|
@@ -791,9 +866,7 @@ async function handleCheckoutSubmit(client, store, options, context) {
|
|
|
791
866
|
replacementAllowed: safe.replacementAllowed,
|
|
792
867
|
});
|
|
793
868
|
}
|
|
794
|
-
const retryable =
|
|
795
|
-
response.status === 202 &&
|
|
796
|
-
["payment_pending", "payment_finalizing"].includes(response.data?.status);
|
|
869
|
+
const retryable = paymentSubmissionPending(response, lifecycle);
|
|
797
870
|
if (wait && retryable) {
|
|
798
871
|
ensureBeforeDeadline(deadline, `Checkout ${taskId}`);
|
|
799
872
|
await delay(suggestedDelay(response));
|
|
@@ -804,6 +877,8 @@ async function handleCheckoutSubmit(client, store, options, context) {
|
|
|
804
877
|
...safe,
|
|
805
878
|
task: response.data?.task,
|
|
806
879
|
paymentId: response.data?.paymentId,
|
|
880
|
+
confirmed: lifecycle.confirmed,
|
|
881
|
+
finalized: lifecycle.finalized,
|
|
807
882
|
message: response.data?.message,
|
|
808
883
|
...(walletPayment
|
|
809
884
|
? {
|
|
@@ -853,8 +928,9 @@ function activeNotificationSubscription(notifications) {
|
|
|
853
928
|
const subscription = notifications?.subscription;
|
|
854
929
|
return Boolean(
|
|
855
930
|
notifications?.configured &&
|
|
856
|
-
subscription?.
|
|
857
|
-
|
|
931
|
+
!subscription?.disabled &&
|
|
932
|
+
Array.isArray(subscription?.recipients) &&
|
|
933
|
+
subscription.recipients.length > 0,
|
|
858
934
|
);
|
|
859
935
|
}
|
|
860
936
|
|
|
@@ -863,14 +939,14 @@ function refillNotificationState(notifications, policy) {
|
|
|
863
939
|
if (!notifications?.configured || !subscription || subscription.disabled) {
|
|
864
940
|
return {
|
|
865
941
|
available: false,
|
|
866
|
-
reason: "
|
|
942
|
+
reason: "active_notification_recipient_required",
|
|
867
943
|
subscription: subscription || null,
|
|
868
944
|
};
|
|
869
945
|
}
|
|
870
|
-
if (!subscription.
|
|
946
|
+
if (!Array.isArray(subscription.recipients) || subscription.recipients.length === 0) {
|
|
871
947
|
return {
|
|
872
948
|
available: false,
|
|
873
|
-
reason: "
|
|
949
|
+
reason: "active_notification_recipient_required",
|
|
874
950
|
subscription,
|
|
875
951
|
};
|
|
876
952
|
}
|
|
@@ -949,20 +1025,10 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
949
1025
|
!withoutEmailNotifications &&
|
|
950
1026
|
!activeNotificationSubscription(notifications)
|
|
951
1027
|
) {
|
|
952
|
-
const verificationPending = Boolean(
|
|
953
|
-
notifications.configured &&
|
|
954
|
-
notifications.subscription &&
|
|
955
|
-
!notifications.subscription.disabled,
|
|
956
|
-
);
|
|
957
|
-
const action = verificationPending
|
|
958
|
-
? "email_verification_required"
|
|
959
|
-
: "email_required";
|
|
960
1028
|
const output = {
|
|
961
|
-
action,
|
|
1029
|
+
action: "email_required",
|
|
962
1030
|
serverId,
|
|
963
|
-
reason:
|
|
964
|
-
? "Verify the existing notification email or resend verification with --email."
|
|
965
|
-
: "A verified email enables renewal and wallet-refill notifications.",
|
|
1031
|
+
reason: "A human email enables renewal and wallet-refill notifications.",
|
|
966
1032
|
notifications,
|
|
967
1033
|
next: {
|
|
968
1034
|
configureEmail: `warpmetal renewal configure ... --email <address>`,
|
|
@@ -974,27 +1040,27 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
974
1040
|
context.stdout,
|
|
975
1041
|
output,
|
|
976
1042
|
context.json,
|
|
977
|
-
|
|
978
|
-
? `Verify the notification email for ${serverId}, or rerun with --email to resend verification. To opt out explicitly, rerun with --without-email-notifications.`
|
|
979
|
-
: `An email is needed for renewal and wallet-refill notifications. Rerun with --email <address>, or opt out explicitly with --without-email-notifications.`,
|
|
1043
|
+
`Ask the human for an email to receive renewal and lifecycle notifications for ${serverId}, then rerun with --email <address>. To opt out explicitly, rerun with --without-email-notifications.`,
|
|
980
1044
|
);
|
|
981
1045
|
return 6;
|
|
982
1046
|
}
|
|
983
1047
|
if (email) {
|
|
984
1048
|
notifications = (
|
|
985
|
-
await client.
|
|
1049
|
+
await client.addNotificationRecipient(
|
|
1050
|
+
serverId,
|
|
1051
|
+
{ email },
|
|
1052
|
+
token,
|
|
1053
|
+
stringOption(options, "idempotency-key") || idempotencyKey("notification-add"),
|
|
1054
|
+
)
|
|
1055
|
+
).data;
|
|
1056
|
+
} else if (withoutEmailNotifications) {
|
|
1057
|
+
notifications = (
|
|
1058
|
+
await client.deleteNotifications(
|
|
1059
|
+
serverId,
|
|
1060
|
+
token,
|
|
1061
|
+
stringOption(options, "idempotency-key") || idempotencyKey("notification-disable"),
|
|
1062
|
+
)
|
|
986
1063
|
).data;
|
|
987
|
-
} else if (
|
|
988
|
-
withoutEmailNotifications &&
|
|
989
|
-
notifications.configured &&
|
|
990
|
-
notifications.subscription &&
|
|
991
|
-
!notifications.subscription.disabled
|
|
992
|
-
) {
|
|
993
|
-
await client.deleteNotifications(serverId, token);
|
|
994
|
-
notifications = {
|
|
995
|
-
...notifications,
|
|
996
|
-
subscription: { ...notifications.subscription, disabled: true },
|
|
997
|
-
};
|
|
998
1064
|
}
|
|
999
1065
|
const result = await client.putRenewalPolicy(serverId, body, token);
|
|
1000
1066
|
await store.saveRenewalPolicy(
|
|
@@ -1009,8 +1075,8 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
1009
1075
|
status: withoutEmailNotifications
|
|
1010
1076
|
? "opted_out"
|
|
1011
1077
|
: activeNotificationSubscription(notifications)
|
|
1012
|
-
? "
|
|
1013
|
-
: "
|
|
1078
|
+
? "active"
|
|
1079
|
+
: "recipient_required",
|
|
1014
1080
|
};
|
|
1015
1081
|
const output = {
|
|
1016
1082
|
...result.data,
|
|
@@ -1023,7 +1089,7 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
1023
1089
|
context.stdout,
|
|
1024
1090
|
output,
|
|
1025
1091
|
context.json,
|
|
1026
|
-
`Configured bounded renewal for ${serverId} with wallet ${wallet}.${email
|
|
1092
|
+
`Configured bounded renewal for ${serverId} with wallet ${wallet}.${email ? " The notification recipient is active and its security advisory was queued." : withoutEmailNotifications ? " Email and wallet-refill notifications were explicitly skipped." : ""}`,
|
|
1027
1093
|
);
|
|
1028
1094
|
return 0;
|
|
1029
1095
|
}
|
|
@@ -1257,9 +1323,9 @@ async function handleRenewalPrepare(client, store, options, context) {
|
|
|
1257
1323
|
? `\n${walletWorkflowInstructions(safe.paymentWorkflow)}. Show the returned payer address as both a QR code and copyable text, with the exact network, asset, and deficit.`
|
|
1258
1324
|
: "";
|
|
1259
1325
|
const refillInstructions = safe.refillWorkflow
|
|
1260
|
-
? `\nFor
|
|
1326
|
+
? `\nFor human refill notification, set ${Object.entries(safe.refillWorkflow.environment).map(([key, value]) => `${key}=${value}`).join(" ")} and run: ${shellCommand(safe.refillWorkflow.argv)}`
|
|
1261
1327
|
: safe.paymentWorkflow
|
|
1262
|
-
? `\
|
|
1328
|
+
? `\nEmail refill is unavailable (${safe.refillNotification.reason}). Add a notification recipient, or fund the displayed payer address directly.`
|
|
1263
1329
|
: "";
|
|
1264
1330
|
const instructions = safe.paymentWorkflow
|
|
1265
1331
|
? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}${fundingInstructions}${refillInstructions}\nSubmit: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
@@ -1378,8 +1444,16 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1378
1444
|
paymentSignature: walletPayment.paymentSignature,
|
|
1379
1445
|
},
|
|
1380
1446
|
);
|
|
1447
|
+
const lifecycle = paymentLifecycle(response);
|
|
1381
1448
|
if (response.data?.paymentId) {
|
|
1382
1449
|
await store.saveRenewalPaymentId(serverId, response.data.paymentId);
|
|
1450
|
+
if (lifecycle.confirmed !== undefined) {
|
|
1451
|
+
await store.saveRenewalPaymentOutcome(
|
|
1452
|
+
serverId,
|
|
1453
|
+
response.data.paymentId,
|
|
1454
|
+
lifecycle,
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1383
1457
|
}
|
|
1384
1458
|
if (response.status === 402 && response.data?.status === "payment_rejected") {
|
|
1385
1459
|
await store.saveRenewalPaymentRejection(serverId, {
|
|
@@ -1391,9 +1465,7 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1391
1465
|
replacementAllowed: response.data?.replacementAllowed,
|
|
1392
1466
|
});
|
|
1393
1467
|
}
|
|
1394
|
-
const retryable =
|
|
1395
|
-
response.status === 202 &&
|
|
1396
|
-
["payment_pending", "payment_finalizing"].includes(response.data?.status);
|
|
1468
|
+
const retryable = paymentSubmissionPending(response, lifecycle);
|
|
1397
1469
|
if (booleanOption(options, "wait") && retryable) {
|
|
1398
1470
|
ensureBeforeDeadline(deadline, `Renewal ${serverId}`);
|
|
1399
1471
|
await delay(suggestedDelay(response));
|
|
@@ -1404,6 +1476,8 @@ async function handleRenewalSubmit(client, store, options, context) {
|
|
|
1404
1476
|
serverId,
|
|
1405
1477
|
task: response.data?.task,
|
|
1406
1478
|
paymentId: response.data?.paymentId,
|
|
1479
|
+
confirmed: lifecycle.confirmed,
|
|
1480
|
+
finalized: lifecycle.finalized,
|
|
1407
1481
|
paymentAttemptId:
|
|
1408
1482
|
response.data?.paymentAttemptId ||
|
|
1409
1483
|
response.headers["x-warpmetal-payment-attempt"],
|
|
@@ -1464,38 +1538,113 @@ async function handleRenewalDue(client, store, options, context) {
|
|
|
1464
1538
|
return due.length ? 8 : 0;
|
|
1465
1539
|
}
|
|
1466
1540
|
|
|
1467
|
-
|
|
1541
|
+
function notificationEvents(options, { required = false } = {}) {
|
|
1542
|
+
const eventValue = stringOption(options, "events", { required });
|
|
1543
|
+
if (!eventValue) return undefined;
|
|
1544
|
+
const events = eventValue.split(",").map((value) => value.trim());
|
|
1545
|
+
if (events.some((value) => !value)) {
|
|
1546
|
+
throw new CliError("--events must contain comma-separated event names.", {
|
|
1547
|
+
exitCode: 2,
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
return events;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
async function handleNotificationsAdd(client, store, options, context) {
|
|
1468
1554
|
const serverId = stringOption(options, "server", { required: true });
|
|
1469
1555
|
const email = stringOption(options, "email", { required: true });
|
|
1470
|
-
const eventValue = stringOption(options, "events");
|
|
1471
1556
|
const body = { email };
|
|
1472
|
-
|
|
1557
|
+
const events = notificationEvents(options);
|
|
1558
|
+
if (events) body.events = events;
|
|
1473
1559
|
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1474
|
-
const result = await client.
|
|
1560
|
+
const result = await client.addNotificationRecipient(
|
|
1561
|
+
serverId,
|
|
1562
|
+
body,
|
|
1563
|
+
token,
|
|
1564
|
+
stringOption(options, "idempotency-key") || idempotencyKey("notification-add"),
|
|
1565
|
+
);
|
|
1475
1566
|
emit(
|
|
1476
1567
|
context.stdout,
|
|
1477
1568
|
result.data,
|
|
1478
1569
|
context.json,
|
|
1479
|
-
|
|
1570
|
+
result.data.created
|
|
1571
|
+
? `${result.data.recipient.email} is active for ${serverId}; WarpMetal queued a security advisory to that address.`
|
|
1572
|
+
: `${result.data.recipient.email} was already active for ${serverId}.`,
|
|
1480
1573
|
);
|
|
1481
1574
|
return 0;
|
|
1482
1575
|
}
|
|
1483
1576
|
|
|
1484
|
-
async function
|
|
1577
|
+
async function handleNotificationsList(client, store, options, context) {
|
|
1485
1578
|
const serverId = stringOption(options, "server", { required: true });
|
|
1486
1579
|
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1487
1580
|
const result = await client.getNotifications(serverId, token);
|
|
1581
|
+
const count = result.data.subscription?.recipients?.length || 0;
|
|
1488
1582
|
emit(
|
|
1489
1583
|
context.stdout,
|
|
1490
1584
|
result.data,
|
|
1491
1585
|
context.json,
|
|
1492
1586
|
result.data.configured
|
|
1493
|
-
? `${serverId}:
|
|
1587
|
+
? `${serverId}: ${count} active notification recipient${count === 1 ? "" : "s"}`
|
|
1494
1588
|
: `${serverId}: notifications are not configured`,
|
|
1495
1589
|
);
|
|
1496
1590
|
return 0;
|
|
1497
1591
|
}
|
|
1498
1592
|
|
|
1593
|
+
async function handleNotificationsRemove(client, store, options, context) {
|
|
1594
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1595
|
+
const recipientId = stringOption(options, "recipient", { required: true });
|
|
1596
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1597
|
+
const result = await client.deleteNotificationRecipient(
|
|
1598
|
+
serverId,
|
|
1599
|
+
recipientId,
|
|
1600
|
+
token,
|
|
1601
|
+
stringOption(options, "idempotency-key") || idempotencyKey("notification-remove"),
|
|
1602
|
+
);
|
|
1603
|
+
emit(
|
|
1604
|
+
context.stdout,
|
|
1605
|
+
result.data,
|
|
1606
|
+
context.json,
|
|
1607
|
+
`Removed ${recipientId} from ${serverId} notifications.`,
|
|
1608
|
+
);
|
|
1609
|
+
return 0;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
async function handleNotificationsEvents(client, store, options, context) {
|
|
1613
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1614
|
+
const events = notificationEvents(options, { required: true });
|
|
1615
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1616
|
+
const result = await client.patchNotifications(
|
|
1617
|
+
serverId,
|
|
1618
|
+
{ events },
|
|
1619
|
+
token,
|
|
1620
|
+
stringOption(options, "idempotency-key") || idempotencyKey("notification-events"),
|
|
1621
|
+
);
|
|
1622
|
+
emit(
|
|
1623
|
+
context.stdout,
|
|
1624
|
+
result.data,
|
|
1625
|
+
context.json,
|
|
1626
|
+
`Updated notification events for ${serverId}.`,
|
|
1627
|
+
);
|
|
1628
|
+
return 0;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
async function handleNotificationsDisable(client, store, options, context) {
|
|
1632
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1633
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1634
|
+
const result = await client.deleteNotifications(
|
|
1635
|
+
serverId,
|
|
1636
|
+
token,
|
|
1637
|
+
stringOption(options, "idempotency-key") || idempotencyKey("notification-disable"),
|
|
1638
|
+
);
|
|
1639
|
+
emit(
|
|
1640
|
+
context.stdout,
|
|
1641
|
+
result.data,
|
|
1642
|
+
context.json,
|
|
1643
|
+
`Disabled lifecycle notifications for ${serverId}.`,
|
|
1644
|
+
);
|
|
1645
|
+
return 0;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1499
1648
|
async function loginServer(client, store, serverId, identity) {
|
|
1500
1649
|
const challenge = (await client.issueSshChallenge(serverId)).data;
|
|
1501
1650
|
const signature = await signSshChallenge(challenge.payload, identity);
|
|
@@ -2553,6 +2702,7 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
2553
2702
|
"email",
|
|
2554
2703
|
"without-email-notifications",
|
|
2555
2704
|
"disabled",
|
|
2705
|
+
"idempotency-key",
|
|
2556
2706
|
]);
|
|
2557
2707
|
return handleRenewalConfigure(client, store, options, context);
|
|
2558
2708
|
case "renewal status":
|
|
@@ -2597,6 +2747,7 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
2597
2747
|
"request-envelope-out",
|
|
2598
2748
|
]);
|
|
2599
2749
|
return handleRenewalRun(client, store, options, context);
|
|
2750
|
+
case "notifications add":
|
|
2600
2751
|
case "notifications configure":
|
|
2601
2752
|
rejectUnknownOptions(options, [
|
|
2602
2753
|
...COMMON_OPTIONS,
|
|
@@ -2604,15 +2755,43 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
2604
2755
|
"token-file",
|
|
2605
2756
|
"email",
|
|
2606
2757
|
"events",
|
|
2758
|
+
"idempotency-key",
|
|
2607
2759
|
]);
|
|
2608
|
-
return
|
|
2760
|
+
return handleNotificationsAdd(client, store, options, context);
|
|
2761
|
+
case "notifications list":
|
|
2609
2762
|
case "notifications status":
|
|
2610
2763
|
rejectUnknownOptions(options, [
|
|
2611
2764
|
...COMMON_OPTIONS,
|
|
2612
2765
|
"server",
|
|
2613
2766
|
"token-file",
|
|
2614
2767
|
]);
|
|
2615
|
-
return
|
|
2768
|
+
return handleNotificationsList(client, store, options, context);
|
|
2769
|
+
case "notifications remove":
|
|
2770
|
+
rejectUnknownOptions(options, [
|
|
2771
|
+
...COMMON_OPTIONS,
|
|
2772
|
+
"server",
|
|
2773
|
+
"token-file",
|
|
2774
|
+
"recipient",
|
|
2775
|
+
"idempotency-key",
|
|
2776
|
+
]);
|
|
2777
|
+
return handleNotificationsRemove(client, store, options, context);
|
|
2778
|
+
case "notifications events":
|
|
2779
|
+
rejectUnknownOptions(options, [
|
|
2780
|
+
...COMMON_OPTIONS,
|
|
2781
|
+
"server",
|
|
2782
|
+
"token-file",
|
|
2783
|
+
"events",
|
|
2784
|
+
"idempotency-key",
|
|
2785
|
+
]);
|
|
2786
|
+
return handleNotificationsEvents(client, store, options, context);
|
|
2787
|
+
case "notifications disable":
|
|
2788
|
+
rejectUnknownOptions(options, [
|
|
2789
|
+
...COMMON_OPTIONS,
|
|
2790
|
+
"server",
|
|
2791
|
+
"token-file",
|
|
2792
|
+
"idempotency-key",
|
|
2793
|
+
]);
|
|
2794
|
+
return handleNotificationsDisable(client, store, options, context);
|
|
2616
2795
|
case "server login":
|
|
2617
2796
|
rejectUnknownOptions(options, [...COMMON_OPTIONS, "server", "identity"]);
|
|
2618
2797
|
return handleServerLogin(client, store, options, context);
|
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.9";
|
|
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";
|
|
@@ -842,6 +842,9 @@ export function paymentWorkflow({
|
|
|
842
842
|
signerContract: {
|
|
843
843
|
version: 1,
|
|
844
844
|
probe: { argv: ["x402api", "help", "--json"] },
|
|
845
|
+
requiredCommands: [
|
|
846
|
+
"payment authorize --wallet <name> --request-envelope <file> --artifact-out <file>",
|
|
847
|
+
],
|
|
845
848
|
},
|
|
846
849
|
signerReleaseStatus:
|
|
847
850
|
"published launch payer; sponsored Base USDC and Solana USDC/USDT only",
|
package/src/state.js
CHANGED
|
@@ -14,6 +14,8 @@ const WALLET_PAYMENT_STATE_FIELDS = [
|
|
|
14
14
|
"paymentArtifactExpiresAt",
|
|
15
15
|
"paymentArtifactSavedAt",
|
|
16
16
|
"gatewayPaymentId",
|
|
17
|
+
"gatewayPaymentConfirmed",
|
|
18
|
+
"gatewayPaymentFinalized",
|
|
17
19
|
"rejectedPaymentAttemptId",
|
|
18
20
|
"paymentRejectionErrorCode",
|
|
19
21
|
"paymentRejectionRequestId",
|
|
@@ -437,6 +439,43 @@ export class StateStore {
|
|
|
437
439
|
});
|
|
438
440
|
}
|
|
439
441
|
|
|
442
|
+
async saveOrderPaymentOutcome(taskId, paymentId, { confirmed, finalized }) {
|
|
443
|
+
if (
|
|
444
|
+
!X402API_PAYMENT_ID.test(paymentId || "") ||
|
|
445
|
+
typeof confirmed !== "boolean" ||
|
|
446
|
+
typeof finalized !== "boolean" ||
|
|
447
|
+
(finalized && !confirmed)
|
|
448
|
+
) {
|
|
449
|
+
throw new CliError("WarpMetal returned invalid payment lifecycle evidence.", {
|
|
450
|
+
exitCode: 3,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
await this.update((state) => {
|
|
454
|
+
const order = state.orders[taskId];
|
|
455
|
+
if (!order) {
|
|
456
|
+
throw new CliError(`No local order state exists for ${taskId}.`, {
|
|
457
|
+
exitCode: 2,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (order.gatewayPaymentId && order.gatewayPaymentId !== paymentId) {
|
|
461
|
+
throw new CliError("WarpMetal changed the x402api payment ID for this attempt.", {
|
|
462
|
+
exitCode: 3,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (
|
|
466
|
+
(order.gatewayPaymentConfirmed === true && !confirmed) ||
|
|
467
|
+
(order.gatewayPaymentFinalized === true && !finalized)
|
|
468
|
+
) {
|
|
469
|
+
throw new CliError("WarpMetal payment lifecycle evidence moved backward.", {
|
|
470
|
+
exitCode: 3,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
order.gatewayPaymentId = paymentId;
|
|
474
|
+
order.gatewayPaymentConfirmed = confirmed;
|
|
475
|
+
order.gatewayPaymentFinalized = finalized;
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
440
479
|
async saveOrderPaymentRejection(taskId, rejection) {
|
|
441
480
|
const safe = validatedPaymentRejection(rejection);
|
|
442
481
|
await this.update((state) => {
|
|
@@ -481,6 +520,43 @@ export class StateStore {
|
|
|
481
520
|
});
|
|
482
521
|
}
|
|
483
522
|
|
|
523
|
+
async saveRenewalPaymentOutcome(serverId, paymentId, { confirmed, finalized }) {
|
|
524
|
+
if (
|
|
525
|
+
!X402API_PAYMENT_ID.test(paymentId || "") ||
|
|
526
|
+
typeof confirmed !== "boolean" ||
|
|
527
|
+
typeof finalized !== "boolean" ||
|
|
528
|
+
(finalized && !confirmed)
|
|
529
|
+
) {
|
|
530
|
+
throw new CliError("WarpMetal returned invalid payment lifecycle evidence.", {
|
|
531
|
+
exitCode: 3,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
await this.update((state) => {
|
|
535
|
+
const renewal = state.renewals[serverId];
|
|
536
|
+
if (!renewal) {
|
|
537
|
+
throw new CliError(`No local renewal state exists for ${serverId}.`, {
|
|
538
|
+
exitCode: 2,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (renewal.gatewayPaymentId && renewal.gatewayPaymentId !== paymentId) {
|
|
542
|
+
throw new CliError("WarpMetal changed the x402api payment ID for this renewal.", {
|
|
543
|
+
exitCode: 3,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (
|
|
547
|
+
(renewal.gatewayPaymentConfirmed === true && !confirmed) ||
|
|
548
|
+
(renewal.gatewayPaymentFinalized === true && !finalized)
|
|
549
|
+
) {
|
|
550
|
+
throw new CliError("WarpMetal renewal payment lifecycle evidence moved backward.", {
|
|
551
|
+
exitCode: 3,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
renewal.gatewayPaymentId = paymentId;
|
|
555
|
+
renewal.gatewayPaymentConfirmed = confirmed;
|
|
556
|
+
renewal.gatewayPaymentFinalized = finalized;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
484
560
|
async saveRenewalPaymentRejection(serverId, rejection) {
|
|
485
561
|
const safe = validatedPaymentRejection(rejection);
|
|
486
562
|
await this.update((state) => {
|
|
@@ -631,6 +707,8 @@ export class StateStore {
|
|
|
631
707
|
planId: order.planId,
|
|
632
708
|
paymentAttemptId: order.paymentAttemptId,
|
|
633
709
|
paymentId: order.gatewayPaymentId,
|
|
710
|
+
paymentConfirmed: order.gatewayPaymentConfirmed,
|
|
711
|
+
paymentFinalized: order.gatewayPaymentFinalized,
|
|
634
712
|
challengeHandle: order.challengeHandle,
|
|
635
713
|
walletPaymentAttemptId: order.walletPaymentAttemptId,
|
|
636
714
|
walletName: order.walletName,
|
|
@@ -671,6 +749,8 @@ export class StateStore {
|
|
|
671
749
|
policy: renewal.policy,
|
|
672
750
|
paymentAttemptId: renewal.paymentAttemptId,
|
|
673
751
|
paymentId: renewal.gatewayPaymentId,
|
|
752
|
+
paymentConfirmed: renewal.gatewayPaymentConfirmed,
|
|
753
|
+
paymentFinalized: renewal.gatewayPaymentFinalized,
|
|
674
754
|
challengeHandle: renewal.challengeHandle,
|
|
675
755
|
paymentArtifactExpiresAt: renewal.paymentArtifactExpiresAt,
|
|
676
756
|
rejectedPaymentAttemptId: renewal.rejectedPaymentAttemptId,
|