warpmetal 0.5.0 → 0.7.0
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 +32 -22
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +9 -4
- package/skills/warpmetal/references/cli-reference.md +13 -7
- package/skills/warpmetal/references/payments.md +12 -6
- package/skills/warpmetal/references/renewals.md +17 -10
- package/src/api.js +36 -28
- package/src/cli.js +180 -34
- package/src/payment.js +24 -2
- package/src/state.js +4 -0
package/README.md
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
The official command-line client and portable Agent Skill for WarpMetal.
|
|
4
4
|
|
|
5
|
-
The CLI uses
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
absorbing wallet custody.
|
|
5
|
+
The CLI uses `https://api.warpmetal.com` as the API root; endpoints begin at
|
|
6
|
+
`/health`, `/catalog`, `/orders`, and so on, without a second `/api` prefix. It
|
|
7
|
+
stores generated WarpMetal credentials in a user-private state file and never
|
|
8
|
+
reads or stores wallet private keys or SSH private-key contents. Version 0.6
|
|
9
|
+
adopts this canonical root-path API. WarpMetal writes the exact credential-free
|
|
10
|
+
payment request, explains the next commands to an agent, validates the returned
|
|
11
|
+
payment artifact, and submits it without absorbing wallet custody.
|
|
13
12
|
|
|
14
13
|
## Distribution
|
|
15
14
|
|
|
@@ -33,7 +32,7 @@ published wallet CLI with the exact version WarpMetal reports:
|
|
|
33
32
|
npm install --global warpmetal
|
|
34
33
|
warpmetal --help
|
|
35
34
|
|
|
36
|
-
npm install --global @x402api/agent-wallet-cli@0.2.
|
|
35
|
+
npm install --global @x402api/agent-wallet-cli@0.2.2
|
|
37
36
|
x402api help --json
|
|
38
37
|
```
|
|
39
38
|
|
|
@@ -86,10 +85,13 @@ After preparing an order, request its live payment challenge:
|
|
|
86
85
|
warpmetal checkout challenge --task <taskId> --json
|
|
87
86
|
```
|
|
88
87
|
|
|
89
|
-
On HTTP 402 the CLI returns exact `paymentTerms`, the
|
|
88
|
+
On HTTP 402 the CLI returns exact `paymentTerms`, the opaque `challengeHandle`
|
|
89
|
+
that WarpMetal uses for merchant-side reconciliation, the pinned
|
|
90
90
|
`@x402api/agent-wallet-cli` package contract, and argv arrays under
|
|
91
91
|
`paymentWorkflow`. It also writes an owner-only request envelope that contains
|
|
92
|
-
the exact checkout URL and body but no WarpMetal credential
|
|
92
|
+
the exact checkout URL and body but no WarpMetal credential or challenge
|
|
93
|
+
handle. The handle is not a buyer payment identifier and is never a wallet
|
|
94
|
+
signing input. The published
|
|
93
95
|
launch wallet accepts sponsored Base USDC and sponsored Solana USDC/USDT only;
|
|
94
96
|
the returned terms identify compatible alternatives and confirm that the buyer
|
|
95
97
|
does not need ETH or SOL. Payment authority depends on execution context:
|
|
@@ -142,11 +144,13 @@ after authorization.
|
|
|
142
144
|
|
|
143
145
|
If funding is short in an interactive conversation, tell the human the exact
|
|
144
146
|
top-up in normal and atomic units, the network, stablecoin and contract/mint,
|
|
145
|
-
and the payer wallet's public receiving address. The
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
and the payer wallet's public receiving address. The returned
|
|
148
|
+
`paymentWorkflow.fundingWorkflow` provides safe address and balance argv plus a
|
|
149
|
+
presentation contract: render that public address as both a QR code and
|
|
150
|
+
copyable text. The human sends the token to that wallet address, never to the
|
|
151
|
+
token contract/mint or WarpMetal's payment recipient, and never sends ETH or
|
|
152
|
+
SOL for a sponsored payment. In an unattended run, use a preconfigured refill
|
|
153
|
+
or escalation mechanism or stop with `funding_required`.
|
|
150
154
|
|
|
151
155
|
## Autonomous renewal and refill
|
|
152
156
|
|
|
@@ -169,7 +173,11 @@ warpmetal renewal configure \
|
|
|
169
173
|
```
|
|
170
174
|
|
|
171
175
|
The recipient must follow the one-time verification link before lifecycle or
|
|
172
|
-
refill mail is sent.
|
|
176
|
+
refill mail is sent. If no verified notification email exists, `renewal
|
|
177
|
+
configure` returns `email_required` before changing policy. Supply `--email`,
|
|
178
|
+
or deliberately continue with `--without-email-notifications`; the latter does
|
|
179
|
+
not enable signed refill-email workflows. A recurring unattended agent can then
|
|
180
|
+
run:
|
|
173
181
|
|
|
174
182
|
```sh
|
|
175
183
|
warpmetal renewal due --all --json
|
|
@@ -177,14 +185,16 @@ warpmetal renewal run --all-due --json
|
|
|
177
185
|
```
|
|
178
186
|
|
|
179
187
|
Inside policy, the CLI returns the exact Agent Wallet authorization and submit
|
|
180
|
-
argv. If balance is insufficient,
|
|
181
|
-
|
|
188
|
+
argv. If balance is insufficient, `refillWorkflow` is returned only when the
|
|
189
|
+
server has an active verified notification subscription. Run its argv with the
|
|
190
|
+
returned `X402API_NOTIFICATION_URL` environment value. `x402api wallet
|
|
182
191
|
notify-refill` signs an opaque subscription reference and wallet-produced
|
|
183
192
|
balance fields; it cannot choose an email address. WarpMetal verifies the
|
|
184
193
|
wallet signature and current on-chain balance before emailing the verified
|
|
185
|
-
human the network, stablecoin, public wallet address,
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
human the network, stablecoin, public wallet address, required minimum top-up,
|
|
195
|
+
and a locally generated QR encoding only that wallet address. The address is
|
|
196
|
+
also repeated as copyable text. The human may transfer more than that minimum;
|
|
197
|
+
the renewal policy—not the refill target—remains the spending authority.
|
|
188
198
|
|
|
189
199
|
The agent never sends a partial x402 payment. If no verified refill path
|
|
190
200
|
exists, it reports `funding_required`. If a previous payment is pending or
|
package/package.json
CHANGED
|
@@ -111,7 +111,9 @@ task or operator limit.
|
|
|
111
111
|
After interactive confirmation or autonomous policy validation, invoke the
|
|
112
112
|
returned authorize argv once. The separate
|
|
113
113
|
`x402api` executable owns the wallet, validates the envelope, and writes the
|
|
114
|
-
payment artifact.
|
|
114
|
+
payment artifact. The returned `challengeHandle` is opaque merchant
|
|
115
|
+
reconciliation metadata; never copy it into the request envelope or treat it as
|
|
116
|
+
the buyer payment identifier. Then invoke the returned submit argv, equivalent to:
|
|
115
117
|
|
|
116
118
|
```sh
|
|
117
119
|
warpmetal checkout submit \
|
|
@@ -152,9 +154,12 @@ price, asset, network, count, horizon, or total-budget mismatch.
|
|
|
152
154
|
Run `warpmetal renewal prepare --server <serverId> --json`, then invoke only
|
|
153
155
|
the returned `paymentWorkflow.authorize.argv` and
|
|
154
156
|
`paymentWorkflow.submit.argv`. If authorization reports insufficient balance,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
use `paymentWorkflow.fundingWorkflow` to obtain the public address and balance,
|
|
158
|
+
then show the address as both a QR code and copyable text. Only when
|
|
159
|
+
`refillNotification.available` is true, set the returned
|
|
160
|
+
`refillWorkflow.environment`, invoke its exact argv once, and stop until
|
|
161
|
+
funding arrives. The signed refill intent resolves to a verified human contact;
|
|
162
|
+
never add an email address to it. Never make a partial payment.
|
|
158
163
|
|
|
159
164
|
After funding, prepare again, authorize exactly once, submit with WarpMetal,
|
|
160
165
|
and confirm the returned `termEndsAt`. On `reconcile_pending` or
|
|
@@ -53,14 +53,16 @@ warpmetal order status \
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
On HTTP 402, `checkout challenge` validates and displays exact payment terms,
|
|
56
|
-
writes
|
|
56
|
+
writes the opaque x402api `challengeHandle` to private WarpMetal state for
|
|
57
|
+
merchant reconciliation, and writes a credential-free x402api V1 request envelope with owner-only
|
|
57
58
|
permissions, and returns the exact pinned wallet package, V1 contract probe,
|
|
58
59
|
matching wallet-skill install, authorization, and WarpMetal submission argv
|
|
59
60
|
arrays. The default envelope and suggested artifact paths live under the
|
|
60
61
|
private WarpMetal state directory. An explicit output path must not already
|
|
61
|
-
contain different content.
|
|
62
|
+
contain different content. `challengeHandle` is intentionally absent from the
|
|
63
|
+
wallet envelope: it is neither a buyer payment identifier nor signing input.
|
|
62
64
|
|
|
63
|
-
The current integration targets `@x402api/agent-wallet-cli@0.2.
|
|
65
|
+
The current integration targets `@x402api/agent-wallet-cli@0.2.2`. A compatible
|
|
64
66
|
live term is marked `agentWalletSupported: true` and must use the sponsored
|
|
65
67
|
Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
|
|
66
68
|
WarpMetal rejects a challenge with no compatible sponsored term. Because the
|
|
@@ -101,7 +103,8 @@ warpmetal renewal configure \
|
|
|
101
103
|
(--maximum-renewals <n> | --renew-through <UTC>) \
|
|
102
104
|
[--maximum-total-spend-atomic <amount>] \
|
|
103
105
|
--allowed-network <network> --allowed-asset <asset> \
|
|
104
|
-
--wallet <name> [--refill-target-atomic <amount>]
|
|
106
|
+
--wallet <name> [--refill-target-atomic <amount>] \
|
|
107
|
+
[--email <address> | --without-email-notifications] --json
|
|
105
108
|
warpmetal renewal status --server <serverId> --json
|
|
106
109
|
warpmetal renewal due (--server <serverId> | --all) --json
|
|
107
110
|
warpmetal renewal prepare --server <serverId> --json
|
|
@@ -113,9 +116,12 @@ warpmetal notifications configure --server <serverId> --email <address> \
|
|
|
113
116
|
warpmetal notifications status --server <serverId> --json
|
|
114
117
|
```
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
Without a verified notification email, renewal configuration asks for an email
|
|
120
|
+
before mutating policy unless `--without-email-notifications` explicitly opts
|
|
121
|
+
out. `renewal prepare` always returns safe funding address/balance argv and
|
|
122
|
+
returns signed refill-email argv only for a verified active subscription.
|
|
123
|
+
`renewal run` is an agent-facing state machine, not a wallet-signing daemon.
|
|
124
|
+
See [renewals.md](renewals.md).
|
|
119
125
|
|
|
120
126
|
## Server management
|
|
121
127
|
|
|
@@ -19,7 +19,7 @@ files or pass a WarpMetal credential to `x402api`.
|
|
|
19
19
|
WarpMetal runs on Node.js 20 or 22. The x402api Agent Wallet currently requires
|
|
20
20
|
Node.js 22. Use the exact published package reported by
|
|
21
21
|
`paymentWorkflow.signerPackage.spec`; the current contract is
|
|
22
|
-
`@x402api/agent-wallet-cli@0.2.
|
|
22
|
+
`@x402api/agent-wallet-cli@0.2.2`. Do not add it as a WarpMetal dependency,
|
|
23
23
|
install executable wallet code from an unpinned repository URL, or substitute
|
|
24
24
|
a similarly named package.
|
|
25
25
|
|
|
@@ -66,10 +66,12 @@ skill automatically.
|
|
|
66
66
|
|
|
67
67
|
6. If funding is short in an interactive conversation, show the exact deficit
|
|
68
68
|
in normal token units and atomic units, the exact network, token symbol and
|
|
69
|
-
contract/mint, and the payer wallet's public receiving address.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
contract/mint, and the payer wallet's public receiving address. Render only
|
|
70
|
+
that public address as a QR code and repeat the full address as copyable
|
|
71
|
+
text. Tell the human to transfer the token to the payer wallet address,
|
|
72
|
+
never to the token contract/mint or `paymentTerms.recipient`. Sponsored
|
|
73
|
+
launch payments never ask the buyer to fund ETH or SOL. Recheck the balance
|
|
74
|
+
after the transfer.
|
|
73
75
|
7. If funding is short in an unattended run, use only a preconfigured refill
|
|
74
76
|
mechanism returned by WarpMetal. For a configured renewal, set the returned
|
|
75
77
|
`refillWorkflow.environment`, invoke `refillWorkflow.argv` once, and stop
|
|
@@ -114,7 +116,11 @@ paymentWorkflow.submit.argv
|
|
|
114
116
|
Replace only `<wallet-name>` after selecting the wallet for the advertised
|
|
115
117
|
network. Do not parse, rewrite, copy, or display the request envelope or payment
|
|
116
118
|
artifact. The envelope excludes the WarpMetal owner token; the artifact holds
|
|
117
|
-
the complete payment signature and remains owner-only.
|
|
119
|
+
the complete payment signature and remains owner-only. The safe challenge JSON
|
|
120
|
+
also contains `challengeHandle`, an opaque value retained by WarpMetal for
|
|
121
|
+
merchant reconciliation. It is deliberately excluded from the x402api V1
|
|
122
|
+
request envelope and must not be confused with the wallet-created buyer payment
|
|
123
|
+
identifier.
|
|
118
124
|
|
|
119
125
|
Choose only a term marked `agentWalletSupported: true`,
|
|
120
126
|
`sponsoredNetworkFee: true`, and `buyerNativeFeeRequired: false`. The supported
|
|
@@ -23,15 +23,18 @@ warpmetal renewal configure \
|
|
|
23
23
|
--allowed-asset <exact-contract-or-mint> \
|
|
24
24
|
--wallet <agent-wallet-name> \
|
|
25
25
|
[--refill-target-atomic <target>] \
|
|
26
|
-
[--email <human-address>] \
|
|
26
|
+
[--email <human-address> | --without-email-notifications] \
|
|
27
27
|
--json
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
Without an active verified notification email, configuration asks for one
|
|
31
|
+
before changing policy. `--email` queues verification; refill and expanded
|
|
32
|
+
lifecycle notices remain disabled until the human opens the verification link.
|
|
33
|
+
Use `--without-email-notifications` only as an explicit opt-out; no signed
|
|
34
|
+
refill-email workflow is returned in that state. A refill target is a funding
|
|
35
|
+
target, not spend permission, and cannot exceed the per-renewal cap. The human
|
|
36
|
+
may transfer more than the displayed deficit. The agent may spend only the
|
|
37
|
+
exact renewal amount allowed by policy.
|
|
35
38
|
|
|
36
39
|
The WarpMetal CLI stores only the local wallet name. The backend stores
|
|
37
40
|
non-secret limits, counters, payment rail, time horizon, and an opaque
|
|
@@ -70,8 +73,10 @@ one policy-compatible rail, and returns:
|
|
|
70
73
|
```text
|
|
71
74
|
paymentWorkflow.authorize.argv
|
|
72
75
|
paymentWorkflow.submit.argv
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
paymentWorkflow.fundingWorkflow
|
|
77
|
+
refillNotification
|
|
78
|
+
refillWorkflow.environment (verified email only)
|
|
79
|
+
refillWorkflow.argv (verified email only)
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
Invoke the authorize argv once. It writes an owner-only artifact. Then invoke
|
|
@@ -112,10 +117,12 @@ authoritative balance before queuing email. The intent cannot name a recipient
|
|
|
112
117
|
email, tenant, or product.
|
|
113
118
|
|
|
114
119
|
In a human conversation, also display the exact deficit, network, token symbol,
|
|
115
|
-
contract or mint, and payer wallet address.
|
|
120
|
+
contract or mint, and payer wallet address. Render only that public address as
|
|
121
|
+
a QR code and repeat it as copyable text. Tell the human to fund the payer
|
|
116
122
|
wallet address—not the token contract/mint and not WarpMetal's merchant
|
|
117
123
|
recipient. Do not ask for ETH or SOL because supported network fees are
|
|
118
|
-
sponsored.
|
|
124
|
+
sponsored. A refill email repeats the address as text and includes a locally
|
|
125
|
+
generated QR encoding only that address.
|
|
119
126
|
|
|
120
127
|
## Stop and recovery states
|
|
121
128
|
|
package/src/api.js
CHANGED
|
@@ -128,19 +128,19 @@ export class WarpMetalClient {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
health() {
|
|
131
|
-
return this.request("GET", "/
|
|
131
|
+
return this.request("GET", "/health");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
catalog() {
|
|
135
|
-
return this.request("GET", "/
|
|
135
|
+
return this.request("GET", "/catalog");
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
prepareOrder(body, idempotencyKey) {
|
|
139
|
-
return this.request("POST", "/
|
|
139
|
+
return this.request("POST", "/orders", { body, idempotencyKey });
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
getTask(taskId, token) {
|
|
143
|
-
return this.request("GET", `/
|
|
143
|
+
return this.request("GET", `/tasks/${encodeURIComponent(taskId)}`, {
|
|
144
144
|
token,
|
|
145
145
|
});
|
|
146
146
|
}
|
|
@@ -157,7 +157,7 @@ export class WarpMetalClient {
|
|
|
157
157
|
issueSshChallenge(serverId) {
|
|
158
158
|
return this.request(
|
|
159
159
|
"POST",
|
|
160
|
-
`/
|
|
160
|
+
`/servers/${encodeURIComponent(serverId)}/auth/challenges`,
|
|
161
161
|
{ body: {} },
|
|
162
162
|
);
|
|
163
163
|
}
|
|
@@ -165,13 +165,13 @@ export class WarpMetalClient {
|
|
|
165
165
|
exchangeSshChallenge(serverId, challengeId, signature) {
|
|
166
166
|
return this.request(
|
|
167
167
|
"POST",
|
|
168
|
-
`/
|
|
168
|
+
`/servers/${encodeURIComponent(serverId)}/auth/tokens`,
|
|
169
169
|
{ body: { challengeId, signature } },
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
getServer(serverId, token) {
|
|
174
|
-
return this.request("GET", `/
|
|
174
|
+
return this.request("GET", `/servers/${encodeURIComponent(serverId)}`, {
|
|
175
175
|
token,
|
|
176
176
|
});
|
|
177
177
|
}
|
|
@@ -179,7 +179,7 @@ export class WarpMetalClient {
|
|
|
179
179
|
getRenewalPolicy(serverId, token) {
|
|
180
180
|
return this.request(
|
|
181
181
|
"GET",
|
|
182
|
-
`/
|
|
182
|
+
`/servers/${encodeURIComponent(serverId)}/renewal-policy`,
|
|
183
183
|
{ token },
|
|
184
184
|
);
|
|
185
185
|
}
|
|
@@ -187,7 +187,7 @@ export class WarpMetalClient {
|
|
|
187
187
|
putRenewalPolicy(serverId, body, token) {
|
|
188
188
|
return this.request(
|
|
189
189
|
"PUT",
|
|
190
|
-
`/
|
|
190
|
+
`/servers/${encodeURIComponent(serverId)}/renewal-policy`,
|
|
191
191
|
{ body, token },
|
|
192
192
|
);
|
|
193
193
|
}
|
|
@@ -195,7 +195,7 @@ export class WarpMetalClient {
|
|
|
195
195
|
deleteRenewalPolicy(serverId, token) {
|
|
196
196
|
return this.request(
|
|
197
197
|
"DELETE",
|
|
198
|
-
`/
|
|
198
|
+
`/servers/${encodeURIComponent(serverId)}/renewal-policy`,
|
|
199
199
|
{ token },
|
|
200
200
|
);
|
|
201
201
|
}
|
|
@@ -203,7 +203,7 @@ export class WarpMetalClient {
|
|
|
203
203
|
getNotifications(serverId, token) {
|
|
204
204
|
return this.request(
|
|
205
205
|
"GET",
|
|
206
|
-
`/
|
|
206
|
+
`/servers/${encodeURIComponent(serverId)}/notifications`,
|
|
207
207
|
{ token },
|
|
208
208
|
);
|
|
209
209
|
}
|
|
@@ -211,13 +211,21 @@ export class WarpMetalClient {
|
|
|
211
211
|
putNotifications(serverId, body, token) {
|
|
212
212
|
return this.request(
|
|
213
213
|
"PUT",
|
|
214
|
-
`/
|
|
214
|
+
`/servers/${encodeURIComponent(serverId)}/notifications`,
|
|
215
215
|
{ body, token },
|
|
216
216
|
);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
deleteNotifications(serverId, token) {
|
|
220
|
+
return this.request(
|
|
221
|
+
"DELETE",
|
|
222
|
+
`/servers/${encodeURIComponent(serverId)}/notifications`,
|
|
223
|
+
{ token },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
renewalCheckout(planId, { bodyText, token, paymentSignature }) {
|
|
220
|
-
return this.checkout(`/
|
|
228
|
+
return this.checkout(`/checkout/${encodeURIComponent(planId)}/renew`, {
|
|
221
229
|
bodyText,
|
|
222
230
|
token,
|
|
223
231
|
paymentSignature,
|
|
@@ -227,7 +235,7 @@ export class WarpMetalClient {
|
|
|
227
235
|
powerServer(serverId, action, token, idempotencyKey) {
|
|
228
236
|
return this.request(
|
|
229
237
|
"POST",
|
|
230
|
-
`/
|
|
238
|
+
`/servers/${encodeURIComponent(serverId)}/power`,
|
|
231
239
|
{
|
|
232
240
|
body: { action },
|
|
233
241
|
token,
|
|
@@ -239,7 +247,7 @@ export class WarpMetalClient {
|
|
|
239
247
|
reloadServer(serverId, body, token, idempotencyKey) {
|
|
240
248
|
return this.request(
|
|
241
249
|
"POST",
|
|
242
|
-
`/
|
|
250
|
+
`/servers/${encodeURIComponent(serverId)}/reload`,
|
|
243
251
|
{
|
|
244
252
|
body,
|
|
245
253
|
token,
|
|
@@ -251,7 +259,7 @@ export class WarpMetalClient {
|
|
|
251
259
|
getOperation(operationId, token) {
|
|
252
260
|
return this.request(
|
|
253
261
|
"GET",
|
|
254
|
-
`/
|
|
262
|
+
`/operations/${encodeURIComponent(operationId)}`,
|
|
255
263
|
{
|
|
256
264
|
token,
|
|
257
265
|
},
|
|
@@ -261,7 +269,7 @@ export class WarpMetalClient {
|
|
|
261
269
|
enableRuntime(serverId, token, idempotencyKey) {
|
|
262
270
|
return this.request(
|
|
263
271
|
"POST",
|
|
264
|
-
`/
|
|
272
|
+
`/servers/${encodeURIComponent(serverId)}/runtime/enable`,
|
|
265
273
|
{
|
|
266
274
|
body: {},
|
|
267
275
|
token,
|
|
@@ -273,7 +281,7 @@ export class WarpMetalClient {
|
|
|
273
281
|
getRuntime(serverId, token) {
|
|
274
282
|
return this.request(
|
|
275
283
|
"GET",
|
|
276
|
-
`/
|
|
284
|
+
`/servers/${encodeURIComponent(serverId)}/runtime`,
|
|
277
285
|
{ token },
|
|
278
286
|
);
|
|
279
287
|
}
|
|
@@ -281,7 +289,7 @@ export class WarpMetalClient {
|
|
|
281
289
|
bootstrapRuntime(serverId, token, idempotencyKey) {
|
|
282
290
|
return this.request(
|
|
283
291
|
"POST",
|
|
284
|
-
`/
|
|
292
|
+
`/servers/${encodeURIComponent(serverId)}/runtime/bootstrap`,
|
|
285
293
|
{ body: {}, token, idempotencyKey },
|
|
286
294
|
);
|
|
287
295
|
}
|
|
@@ -289,7 +297,7 @@ export class WarpMetalClient {
|
|
|
289
297
|
listSandboxes(serverId, token) {
|
|
290
298
|
return this.request(
|
|
291
299
|
"GET",
|
|
292
|
-
`/
|
|
300
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes`,
|
|
293
301
|
{ token },
|
|
294
302
|
);
|
|
295
303
|
}
|
|
@@ -297,7 +305,7 @@ export class WarpMetalClient {
|
|
|
297
305
|
createSandboxes(serverId, sandboxes, token, idempotencyKey) {
|
|
298
306
|
return this.request(
|
|
299
307
|
"POST",
|
|
300
|
-
`/
|
|
308
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes`,
|
|
301
309
|
{
|
|
302
310
|
body: { sandboxes },
|
|
303
311
|
token,
|
|
@@ -309,7 +317,7 @@ export class WarpMetalClient {
|
|
|
309
317
|
getSandbox(serverId, sandboxId, token) {
|
|
310
318
|
return this.request(
|
|
311
319
|
"GET",
|
|
312
|
-
`/
|
|
320
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}`,
|
|
313
321
|
{ token },
|
|
314
322
|
);
|
|
315
323
|
}
|
|
@@ -317,7 +325,7 @@ export class WarpMetalClient {
|
|
|
317
325
|
sandboxAction(serverId, sandboxId, action, token, idempotencyKey) {
|
|
318
326
|
return this.request(
|
|
319
327
|
"POST",
|
|
320
|
-
`/
|
|
328
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/actions`,
|
|
321
329
|
{ body: { action }, token, idempotencyKey },
|
|
322
330
|
);
|
|
323
331
|
}
|
|
@@ -325,7 +333,7 @@ export class WarpMetalClient {
|
|
|
325
333
|
deleteSandbox(serverId, sandboxId, token, idempotencyKey) {
|
|
326
334
|
return this.request(
|
|
327
335
|
"POST",
|
|
328
|
-
`/
|
|
336
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/delete`,
|
|
329
337
|
{ body: { confirm: "DELETE" }, token, idempotencyKey },
|
|
330
338
|
);
|
|
331
339
|
}
|
|
@@ -333,7 +341,7 @@ export class WarpMetalClient {
|
|
|
333
341
|
listAccessGrants(serverId, sandboxId, token) {
|
|
334
342
|
return this.request(
|
|
335
343
|
"GET",
|
|
336
|
-
`/
|
|
344
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/access-grants`,
|
|
337
345
|
{ token },
|
|
338
346
|
);
|
|
339
347
|
}
|
|
@@ -341,7 +349,7 @@ export class WarpMetalClient {
|
|
|
341
349
|
createAccessGrant(serverId, sandboxId, body, token, idempotencyKey) {
|
|
342
350
|
return this.request(
|
|
343
351
|
"POST",
|
|
344
|
-
`/
|
|
352
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/access-grants`,
|
|
345
353
|
{ body, token, idempotencyKey },
|
|
346
354
|
);
|
|
347
355
|
}
|
|
@@ -349,7 +357,7 @@ export class WarpMetalClient {
|
|
|
349
357
|
getAccessGrant(serverId, sandboxId, grantId, token) {
|
|
350
358
|
return this.request(
|
|
351
359
|
"GET",
|
|
352
|
-
`/
|
|
360
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/access-grants/${encodeURIComponent(grantId)}`,
|
|
353
361
|
{ token },
|
|
354
362
|
);
|
|
355
363
|
}
|
|
@@ -357,7 +365,7 @@ export class WarpMetalClient {
|
|
|
357
365
|
revokeAccessGrant(serverId, sandboxId, grantId, token, idempotencyKey) {
|
|
358
366
|
return this.request(
|
|
359
367
|
"POST",
|
|
360
|
-
`/
|
|
368
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/access-grants/${encodeURIComponent(grantId)}/revoke`,
|
|
361
369
|
{ body: { confirm: "REVOKE" }, token, idempotencyKey },
|
|
362
370
|
);
|
|
363
371
|
}
|
package/src/cli.js
CHANGED
|
@@ -84,6 +84,7 @@ Usage:
|
|
|
84
84
|
warpmetal renewal configure --server <serverId> --renew-before-days <n>
|
|
85
85
|
--maximum-payment-atomic <amount> (--maximum-renewals <n> | --renew-through <UTC>)
|
|
86
86
|
--allowed-network <CAIP-2> --allowed-asset <asset> --wallet <name>
|
|
87
|
+
[--email <address> | --without-email-notifications]
|
|
87
88
|
warpmetal renewal status|prepare|submit|run ...
|
|
88
89
|
warpmetal renewal due (--server <serverId> | --all)
|
|
89
90
|
warpmetal notifications configure --server <serverId> --email <address>
|
|
@@ -364,9 +365,15 @@ async function pollGrant(
|
|
|
364
365
|
|
|
365
366
|
function challengeResult(taskId, checkoutBody, response) {
|
|
366
367
|
const paymentRequired = response.headers["payment-required"];
|
|
368
|
+
const challengeHandle = response.headers["x-x402api-challenge-handle"];
|
|
367
369
|
if (response.status === 402 && !paymentRequired) {
|
|
368
370
|
throw new CliError("WarpMetal returned HTTP 402 without PAYMENT-REQUIRED.");
|
|
369
371
|
}
|
|
372
|
+
if (response.status === 402 && !challengeHandle) {
|
|
373
|
+
throw new CliError(
|
|
374
|
+
"WarpMetal returned HTTP 402 without X-X402API-Challenge-Handle.",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
370
377
|
return {
|
|
371
378
|
status: response.data?.status,
|
|
372
379
|
taskId,
|
|
@@ -374,6 +381,7 @@ function challengeResult(taskId, checkoutBody, response) {
|
|
|
374
381
|
response.data?.paymentAttemptId ||
|
|
375
382
|
response.headers["x-warpmetal-payment-attempt"],
|
|
376
383
|
paymentRequired,
|
|
384
|
+
challengeHandle,
|
|
377
385
|
checkoutBodySha256: createHash("sha256").update(checkoutBody).digest("hex"),
|
|
378
386
|
};
|
|
379
387
|
}
|
|
@@ -645,7 +653,7 @@ async function handleCheckoutChallenge(client, store, options, context) {
|
|
|
645
653
|
stringOption(options, "request-envelope-out"),
|
|
646
654
|
);
|
|
647
655
|
const paymentInstructions = safe.paymentWorkflow
|
|
648
|
-
? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
656
|
+
? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nIf funding is short, run ${shellCommand(safe.paymentWorkflow.fundingWorkflow.address.argv)} and ${shellCommand(safe.paymentWorkflow.fundingWorkflow.balance.argv)}, then show the payer address as both a QR code and copyable text.\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
649
657
|
: "";
|
|
650
658
|
emit(
|
|
651
659
|
context.stdout,
|
|
@@ -775,6 +783,44 @@ export function refillRenewBy(termEndsAt, currentTime = Date.now()) {
|
|
|
775
783
|
return new Date(Number.isFinite(parsed) && parsed > minimum ? parsed : minimum).toISOString();
|
|
776
784
|
}
|
|
777
785
|
|
|
786
|
+
function activeNotificationSubscription(notifications) {
|
|
787
|
+
const subscription = notifications?.subscription;
|
|
788
|
+
return Boolean(
|
|
789
|
+
notifications?.configured &&
|
|
790
|
+
subscription?.verified &&
|
|
791
|
+
!subscription?.disabled,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function refillNotificationState(notifications, policy) {
|
|
796
|
+
const subscription = notifications?.subscription;
|
|
797
|
+
if (!notifications?.configured || !subscription || subscription.disabled) {
|
|
798
|
+
return {
|
|
799
|
+
available: false,
|
|
800
|
+
reason: "verified_email_required",
|
|
801
|
+
subscription: subscription || null,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
if (!subscription.verified) {
|
|
805
|
+
return {
|
|
806
|
+
available: false,
|
|
807
|
+
reason: "email_verification_required",
|
|
808
|
+
subscription,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (
|
|
812
|
+
policy?.notificationReference &&
|
|
813
|
+
subscription.reference !== policy.notificationReference
|
|
814
|
+
) {
|
|
815
|
+
return {
|
|
816
|
+
available: false,
|
|
817
|
+
reason: "notification_reference_mismatch",
|
|
818
|
+
subscription,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
return { available: true, reason: null, subscription };
|
|
822
|
+
}
|
|
823
|
+
|
|
778
824
|
async function handleRenewalConfigure(client, store, options, context) {
|
|
779
825
|
const serverId = stringOption(options, "server", { required: true });
|
|
780
826
|
const wallet = stringOption(options, "wallet", { required: true });
|
|
@@ -818,7 +864,72 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
818
864
|
for (const [key, value] of Object.entries(body)) {
|
|
819
865
|
if (value === undefined) delete body[key];
|
|
820
866
|
}
|
|
867
|
+
const email = stringOption(options, "email");
|
|
868
|
+
const withoutEmailNotifications = booleanOption(
|
|
869
|
+
options,
|
|
870
|
+
"without-email-notifications",
|
|
871
|
+
);
|
|
872
|
+
if (email && withoutEmailNotifications) {
|
|
873
|
+
throw new CliError(
|
|
874
|
+
"Use either --email or --without-email-notifications, not both.",
|
|
875
|
+
{ exitCode: 2 },
|
|
876
|
+
);
|
|
877
|
+
}
|
|
821
878
|
const token = await requireServerToken(store, serverId, options, context.env);
|
|
879
|
+
let notifications = (await client.getNotifications(serverId, token)).data;
|
|
880
|
+
if (
|
|
881
|
+
body.enabled &&
|
|
882
|
+
!email &&
|
|
883
|
+
!withoutEmailNotifications &&
|
|
884
|
+
!activeNotificationSubscription(notifications)
|
|
885
|
+
) {
|
|
886
|
+
const verificationPending = Boolean(
|
|
887
|
+
notifications.configured &&
|
|
888
|
+
notifications.subscription &&
|
|
889
|
+
!notifications.subscription.disabled,
|
|
890
|
+
);
|
|
891
|
+
const action = verificationPending
|
|
892
|
+
? "email_verification_required"
|
|
893
|
+
: "email_required";
|
|
894
|
+
const output = {
|
|
895
|
+
action,
|
|
896
|
+
serverId,
|
|
897
|
+
reason: verificationPending
|
|
898
|
+
? "Verify the existing notification email or resend verification with --email."
|
|
899
|
+
: "A verified email enables renewal and wallet-refill notifications.",
|
|
900
|
+
notifications,
|
|
901
|
+
next: {
|
|
902
|
+
configureEmail: `warpmetal renewal configure ... --email <address>`,
|
|
903
|
+
continueWithoutEmail:
|
|
904
|
+
"warpmetal renewal configure ... --without-email-notifications",
|
|
905
|
+
},
|
|
906
|
+
};
|
|
907
|
+
emit(
|
|
908
|
+
context.stdout,
|
|
909
|
+
output,
|
|
910
|
+
context.json,
|
|
911
|
+
verificationPending
|
|
912
|
+
? `Verify the notification email for ${serverId}, or rerun with --email to resend verification. To opt out explicitly, rerun with --without-email-notifications.`
|
|
913
|
+
: `An email is needed for renewal and wallet-refill notifications. Rerun with --email <address>, or opt out explicitly with --without-email-notifications.`,
|
|
914
|
+
);
|
|
915
|
+
return 6;
|
|
916
|
+
}
|
|
917
|
+
if (email) {
|
|
918
|
+
notifications = (
|
|
919
|
+
await client.putNotifications(serverId, { email }, token)
|
|
920
|
+
).data;
|
|
921
|
+
} else if (
|
|
922
|
+
withoutEmailNotifications &&
|
|
923
|
+
notifications.configured &&
|
|
924
|
+
notifications.subscription &&
|
|
925
|
+
!notifications.subscription.disabled
|
|
926
|
+
) {
|
|
927
|
+
await client.deleteNotifications(serverId, token);
|
|
928
|
+
notifications = {
|
|
929
|
+
...notifications,
|
|
930
|
+
subscription: { ...notifications.subscription, disabled: true },
|
|
931
|
+
};
|
|
932
|
+
}
|
|
822
933
|
const result = await client.putRenewalPolicy(serverId, body, token);
|
|
823
934
|
await store.saveRenewalPolicy(
|
|
824
935
|
serverId,
|
|
@@ -826,19 +937,27 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
826
937
|
wallet,
|
|
827
938
|
refillTargetAtomic,
|
|
828
939
|
);
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
940
|
+
const notificationState = {
|
|
941
|
+
optedOut: withoutEmailNotifications,
|
|
942
|
+
refillAvailable: activeNotificationSubscription(notifications),
|
|
943
|
+
status: withoutEmailNotifications
|
|
944
|
+
? "opted_out"
|
|
945
|
+
: activeNotificationSubscription(notifications)
|
|
946
|
+
? "verified"
|
|
947
|
+
: "verification_required",
|
|
948
|
+
};
|
|
949
|
+
const output = {
|
|
950
|
+
...result.data,
|
|
951
|
+
wallet,
|
|
952
|
+
refillTargetAtomic,
|
|
953
|
+
notifications,
|
|
954
|
+
notificationState,
|
|
955
|
+
};
|
|
837
956
|
emit(
|
|
838
957
|
context.stdout,
|
|
839
958
|
output,
|
|
840
959
|
context.json,
|
|
841
|
-
`Configured bounded renewal for ${serverId} with wallet ${wallet}.${email ? " Check the email verification link." : ""}`,
|
|
960
|
+
`Configured bounded renewal for ${serverId} with wallet ${wallet}.${email && !notificationState.refillAvailable ? " Check the email verification link." : withoutEmailNotifications ? " Email and wallet-refill notifications were explicitly skipped." : ""}`,
|
|
842
961
|
);
|
|
843
962
|
return 0;
|
|
844
963
|
}
|
|
@@ -866,11 +985,12 @@ async function attachRenewalPaymentWorkflow(
|
|
|
866
985
|
serverId,
|
|
867
986
|
server,
|
|
868
987
|
renewal,
|
|
988
|
+
notifications,
|
|
869
989
|
response,
|
|
870
990
|
requestedEnvelopePath,
|
|
871
991
|
) {
|
|
872
992
|
const bodyText = JSON.stringify({ serverId });
|
|
873
|
-
const checkoutPath = `/
|
|
993
|
+
const checkoutPath = `/checkout/${server.planId}/renew`;
|
|
874
994
|
const safe = challengeResult(serverId, bodyText, response);
|
|
875
995
|
if (!safe.paymentRequired) return safe;
|
|
876
996
|
const request = createPaymentRequestEnvelope({
|
|
@@ -924,6 +1044,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
924
1044
|
taskId: server.id,
|
|
925
1045
|
serverId,
|
|
926
1046
|
kind: "renewal",
|
|
1047
|
+
wallet: renewal.wallet,
|
|
927
1048
|
requestEnvelopePath,
|
|
928
1049
|
paymentArtifactPath: defaults.paymentArtifactPath,
|
|
929
1050
|
});
|
|
@@ -932,27 +1053,39 @@ async function attachRenewalPaymentWorkflow(
|
|
|
932
1053
|
BigInt(renewal.refillTargetAtomic) >= BigInt(term.amountAtomic)
|
|
933
1054
|
? renewal.refillTargetAtomic
|
|
934
1055
|
: term.amountAtomic;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1056
|
+
Object.assign(workflow.fundingWorkflow, {
|
|
1057
|
+
network: term.network,
|
|
1058
|
+
asset: term.asset,
|
|
1059
|
+
requiredPaymentAtomic: term.amountAtomic,
|
|
1060
|
+
targetBalanceAtomic: refillTarget,
|
|
1061
|
+
});
|
|
1062
|
+
const refillNotification = refillNotificationState(
|
|
1063
|
+
notifications,
|
|
1064
|
+
renewal.policy,
|
|
1065
|
+
);
|
|
1066
|
+
const refillWorkflow = refillNotification.available
|
|
1067
|
+
? {
|
|
1068
|
+
environment: {
|
|
1069
|
+
X402API_NOTIFICATION_URL: `${client.baseUrl}/notifications/x402api/refill`,
|
|
1070
|
+
},
|
|
1071
|
+
argv: [
|
|
1072
|
+
"x402api",
|
|
1073
|
+
"wallet",
|
|
1074
|
+
"notify-refill",
|
|
1075
|
+
"--wallet",
|
|
1076
|
+
renewal.wallet,
|
|
1077
|
+
"--subscription-reference",
|
|
1078
|
+
refillNotification.subscription.reference,
|
|
1079
|
+
"--renew-by",
|
|
1080
|
+
refillRenewBy(server.termEndsAt),
|
|
1081
|
+
"--target-balance-atomic",
|
|
1082
|
+
refillTarget,
|
|
1083
|
+
"--reason",
|
|
1084
|
+
"renewal",
|
|
1085
|
+
"--json",
|
|
1086
|
+
],
|
|
1087
|
+
}
|
|
1088
|
+
: undefined;
|
|
956
1089
|
Object.assign(safe, {
|
|
957
1090
|
serverId,
|
|
958
1091
|
paymentTerms: request.terms,
|
|
@@ -960,6 +1093,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
960
1093
|
paymentRequestDigest: request.requestDigest,
|
|
961
1094
|
paymentChallengeDigest: request.challengeDigest,
|
|
962
1095
|
paymentWorkflow: workflow,
|
|
1096
|
+
refillNotification,
|
|
963
1097
|
refillWorkflow,
|
|
964
1098
|
});
|
|
965
1099
|
await store.saveRenewalChallenge(serverId, {
|
|
@@ -968,6 +1102,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
968
1102
|
termEndsAt: server.termEndsAt,
|
|
969
1103
|
paymentRequired: safe.paymentRequired,
|
|
970
1104
|
paymentAttemptId: safe.paymentAttemptId,
|
|
1105
|
+
challengeHandle: safe.challengeHandle,
|
|
971
1106
|
paymentRequestDigest: safe.paymentRequestDigest,
|
|
972
1107
|
paymentChallengeDigest: safe.paymentChallengeDigest,
|
|
973
1108
|
paymentRequestEnvelopePath: requestEnvelopePath,
|
|
@@ -988,9 +1123,10 @@ function renewalActionExitCode(action) {
|
|
|
988
1123
|
|
|
989
1124
|
async function prepareRenewal(client, store, serverId, options, context) {
|
|
990
1125
|
const token = await requireServerToken(store, serverId, options, context.env);
|
|
991
|
-
const [serverResult, policyResult] = await Promise.all([
|
|
1126
|
+
const [serverResult, policyResult, notificationsResult] = await Promise.all([
|
|
992
1127
|
client.getServer(serverId, token),
|
|
993
1128
|
client.getRenewalPolicy(serverId, token),
|
|
1129
|
+
client.getNotifications(serverId, token),
|
|
994
1130
|
]);
|
|
995
1131
|
if (!policyResult.data.configured) {
|
|
996
1132
|
return {
|
|
@@ -1032,6 +1168,7 @@ async function prepareRenewal(client, store, serverId, options, context) {
|
|
|
1032
1168
|
serverId,
|
|
1033
1169
|
serverResult.data.task,
|
|
1034
1170
|
local,
|
|
1171
|
+
notificationsResult.data,
|
|
1035
1172
|
response,
|
|
1036
1173
|
stringOption(options, "request-envelope-out"),
|
|
1037
1174
|
);
|
|
@@ -1053,8 +1190,16 @@ async function prepareRenewal(client, store, serverId, options, context) {
|
|
|
1053
1190
|
async function handleRenewalPrepare(client, store, options, context) {
|
|
1054
1191
|
const serverId = stringOption(options, "server", { required: true });
|
|
1055
1192
|
const safe = await prepareRenewal(client, store, serverId, options, context);
|
|
1193
|
+
const fundingInstructions = safe.paymentWorkflow
|
|
1194
|
+
? `\nIf funding is short, run ${shellCommand(safe.paymentWorkflow.fundingWorkflow.address.argv)} and ${shellCommand(safe.paymentWorkflow.fundingWorkflow.balance.argv)}. Show the returned payer address as both a QR code and copyable text, with the exact network, asset, and deficit.`
|
|
1195
|
+
: "";
|
|
1196
|
+
const refillInstructions = safe.refillWorkflow
|
|
1197
|
+
? `\nFor verified email refill, set ${Object.entries(safe.refillWorkflow.environment).map(([key, value]) => `${key}=${value}`).join(" ")} and run: ${shellCommand(safe.refillWorkflow.argv)}`
|
|
1198
|
+
: safe.paymentWorkflow
|
|
1199
|
+
? `\nVerified email refill is unavailable (${safe.refillNotification.reason}). Configure and verify notifications, or fund the displayed payer address directly.`
|
|
1200
|
+
: "";
|
|
1056
1201
|
const instructions = safe.paymentWorkflow
|
|
1057
|
-
? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}
|
|
1202
|
+
? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}${fundingInstructions}${refillInstructions}\nSubmit: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
1058
1203
|
: "";
|
|
1059
1204
|
emit(
|
|
1060
1205
|
context.stdout,
|
|
@@ -2325,6 +2470,7 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
2325
2470
|
"allowed-asset",
|
|
2326
2471
|
"refill-target-atomic",
|
|
2327
2472
|
"email",
|
|
2473
|
+
"without-email-notifications",
|
|
2328
2474
|
"disabled",
|
|
2329
2475
|
]);
|
|
2330
2476
|
return handleRenewalConfigure(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.2";
|
|
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";
|
|
@@ -696,6 +696,7 @@ export function paymentWorkflow({
|
|
|
696
696
|
taskId,
|
|
697
697
|
serverId,
|
|
698
698
|
kind = "checkout",
|
|
699
|
+
wallet = "<wallet-name>",
|
|
699
700
|
requestEnvelopePath,
|
|
700
701
|
paymentArtifactPath,
|
|
701
702
|
}) {
|
|
@@ -739,7 +740,7 @@ export function paymentWorkflow({
|
|
|
739
740
|
"payment",
|
|
740
741
|
"authorize",
|
|
741
742
|
"--wallet",
|
|
742
|
-
|
|
743
|
+
wallet,
|
|
743
744
|
"--request-envelope",
|
|
744
745
|
requestEnvelopePath,
|
|
745
746
|
"--artifact-out",
|
|
@@ -747,6 +748,27 @@ export function paymentWorkflow({
|
|
|
747
748
|
"--json",
|
|
748
749
|
],
|
|
749
750
|
},
|
|
751
|
+
fundingWorkflow: {
|
|
752
|
+
action: "fund_wallet",
|
|
753
|
+
trigger: "insufficient_asset_balance",
|
|
754
|
+
wallet,
|
|
755
|
+
address: {
|
|
756
|
+
argv: ["x402api", "wallet", "address", "--wallet", wallet, "--json"],
|
|
757
|
+
},
|
|
758
|
+
balance: {
|
|
759
|
+
argv: ["x402api", "wallet", "balance", "--wallet", wallet, "--json"],
|
|
760
|
+
},
|
|
761
|
+
presentation: {
|
|
762
|
+
qrPayloadField: "address",
|
|
763
|
+
textPayloadField: "address",
|
|
764
|
+
showQr: true,
|
|
765
|
+
showAddressString: true,
|
|
766
|
+
showExactDeficit: true,
|
|
767
|
+
showNetworkAndAsset: true,
|
|
768
|
+
},
|
|
769
|
+
safety:
|
|
770
|
+
"Encode and fund only the payer wallet public address. Never fund the token contract, mint, or WarpMetal merchant recipient; sponsored payments do not require ETH or SOL.",
|
|
771
|
+
},
|
|
750
772
|
submit: {
|
|
751
773
|
argv:
|
|
752
774
|
kind === "renewal"
|
package/src/state.js
CHANGED
|
@@ -280,6 +280,7 @@ export class StateStore {
|
|
|
280
280
|
{
|
|
281
281
|
paymentRequired,
|
|
282
282
|
paymentAttemptId,
|
|
283
|
+
challengeHandle,
|
|
283
284
|
paymentRequestDigest,
|
|
284
285
|
paymentChallengeDigest,
|
|
285
286
|
paymentWorkflow,
|
|
@@ -293,6 +294,7 @@ export class StateStore {
|
|
|
293
294
|
});
|
|
294
295
|
order.paymentRequired = paymentRequired;
|
|
295
296
|
order.paymentAttemptId = paymentAttemptId;
|
|
297
|
+
order.challengeHandle = challengeHandle;
|
|
296
298
|
order.paymentRequestDigest = paymentRequestDigest;
|
|
297
299
|
order.paymentChallengeDigest = paymentChallengeDigest;
|
|
298
300
|
order.paymentRequestEnvelopePath = paymentWorkflow?.requestEnvelopePath;
|
|
@@ -465,6 +467,7 @@ export class StateStore {
|
|
|
465
467
|
serverId: order.serverId,
|
|
466
468
|
planId: order.planId,
|
|
467
469
|
paymentAttemptId: order.paymentAttemptId,
|
|
470
|
+
challengeHandle: order.challengeHandle,
|
|
468
471
|
walletPaymentAttemptId: order.walletPaymentAttemptId,
|
|
469
472
|
walletName: order.walletName,
|
|
470
473
|
payerAddress: order.walletPayerAddress,
|
|
@@ -499,6 +502,7 @@ export class StateStore {
|
|
|
499
502
|
refillTargetAtomic: renewal.refillTargetAtomic,
|
|
500
503
|
policy: renewal.policy,
|
|
501
504
|
paymentAttemptId: renewal.paymentAttemptId,
|
|
505
|
+
challengeHandle: renewal.challengeHandle,
|
|
502
506
|
paymentArtifactExpiresAt: renewal.paymentArtifactExpiresAt,
|
|
503
507
|
})),
|
|
504
508
|
};
|