warpmetal 0.8.4 → 0.8.6
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 +12 -0
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +12 -0
- package/skills/warpmetal/references/cli-reference.md +9 -0
- package/skills/warpmetal/references/payments.md +10 -0
- package/src/cli.js +96 -4
- package/src/installer.js +10 -2
package/README.md
CHANGED
|
@@ -141,6 +141,18 @@ confirmation is pending. As soon as `confirmed` is true, stop all payment
|
|
|
141
141
|
submission—even when `finalized` is false—and let WarpMetal continue signed-
|
|
142
142
|
receipt finality while provisioning or renewal proceeds.
|
|
143
143
|
|
|
144
|
+
For an interactive initial purchase, `checkout challenge` may also return a
|
|
145
|
+
short-lived `humanCheckout` object. Its `url` and `qrPayload` are the same
|
|
146
|
+
`https://pay.x402api.com/c/...` bearer capability for the exact charge; encode
|
|
147
|
+
that URL as the QR, never the WarpMetal recipient address. The buyer connects
|
|
148
|
+
their own wallet and needs only the advertised USDC/USDT balance because
|
|
149
|
+
x402api sponsors the native gas. This is an alternative to the agent-wallet
|
|
150
|
+
workflow, not a second payment. After browser payment, run the exact
|
|
151
|
+
`humanCheckout.afterPayment.argv` status command. When the ready result asks
|
|
152
|
+
for `ask_human_for_notification_email`, ask the owner and add the optional
|
|
153
|
+
lifecycle-notification address they provide. Autonomous purchases and renewals
|
|
154
|
+
continue to use the bounded agent-wallet path.
|
|
155
|
+
|
|
144
156
|
- When a human is actively chatting with the agent, show the exact live terms
|
|
145
157
|
and ask for confirmation immediately before authorizing and submitting.
|
|
146
158
|
- In an unattended run, a pre-funded dedicated wallet is standing spend
|
package/package.json
CHANGED
|
@@ -88,6 +88,18 @@ returns `paymentTerms` plus exact `paymentWorkflow.authorize.argv` and
|
|
|
88
88
|
`paymentWorkflow.submit.argv` arrays. Do not reconstruct those commands or
|
|
89
89
|
open either file.
|
|
90
90
|
|
|
91
|
+
In an interactive initial purchase, the result may also contain
|
|
92
|
+
`humanCheckout`. Offer it as an alternative to the local agent wallet. Show
|
|
93
|
+
`humanCheckout.url` as clickable text and render only the identical
|
|
94
|
+
`humanCheckout.qrPayload` URL as the purchase QR; never render the recipient
|
|
95
|
+
address as this QR. The URL is an expiring bearer capability, so do not log,
|
|
96
|
+
save, or send it anywhere except to the buyer who requested this purchase. If
|
|
97
|
+
the buyer uses it, do not authorize or submit through the agent wallet. Run the
|
|
98
|
+
exact `humanCheckout.afterPayment.argv` command, wait for the server to become
|
|
99
|
+
ready, then follow `ask_human_for_notification_email`: ask the owner for the
|
|
100
|
+
optional lifecycle-notification address and add only the address they provide.
|
|
101
|
+
Ignore this path in unattended purchasing and renewal automation.
|
|
102
|
+
|
|
91
103
|
Determine payment authority from the current execution context. In an
|
|
92
104
|
interactive conversation, show the human the exact amount, asset, network,
|
|
93
105
|
recipient, profile, and maximum authorization lifetime from `paymentTerms`,
|
|
@@ -72,6 +72,15 @@ 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
|
+
For interactive initial purchases, the response may additionally include
|
|
76
|
+
`humanCheckout.url`, the identical `qrPayload`, `expiresAt`, and an exact
|
|
77
|
+
`afterPayment.argv` command. The URL is a short-lived bearer capability for the
|
|
78
|
+
same charge, not a recipient address. If the buyer pays in the hosted checkout,
|
|
79
|
+
do not submit an agent-wallet artifact; run the returned status command and,
|
|
80
|
+
after ready, follow `ask_human_for_notification_email` to offer lifecycle
|
|
81
|
+
notices. The CLI does not persist the hosted URL. Renewal commands never expose
|
|
82
|
+
or use this interactive option.
|
|
83
|
+
|
|
75
84
|
The current integration targets `@x402api/agent-wallet-cli@0.2.9`. A compatible
|
|
76
85
|
live term is marked `agentWalletSupported: true` and must use the sponsored
|
|
77
86
|
Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
|
|
@@ -123,6 +123,16 @@ warpmetal checkout challenge --task <taskId> --json
|
|
|
123
123
|
Read only the returned safe JSON. Confirm `paymentTerms`, then use the exact
|
|
124
124
|
argv arrays returned under `paymentWorkflow`:
|
|
125
125
|
|
|
126
|
+
For an interactive initial purchase, an optional `humanCheckout` object offers
|
|
127
|
+
a second presentation path for the same charge. Its `url` and `qrPayload` must
|
|
128
|
+
be identical hosted-checkout URLs; render that URL as the QR and copyable link,
|
|
129
|
+
never a recipient address. If the buyer completes this path, skip agent-wallet
|
|
130
|
+
authorization and run `humanCheckout.afterPayment.argv`. Continue bounded
|
|
131
|
+
status polling until ready, then ask for the optional lifecycle-notification
|
|
132
|
+
email when the result returns `ask_human_for_notification_email`. The hosted
|
|
133
|
+
URL expires at `humanCheckout.expiresAt`, is not persisted by the CLI, and is
|
|
134
|
+
never used for unattended purchases or renewals.
|
|
135
|
+
|
|
126
136
|
```text
|
|
127
137
|
paymentWorkflow.authorize.argv
|
|
128
138
|
x402api payment authorize --wallet <wallet-name>
|
package/src/cli.js
CHANGED
|
@@ -427,7 +427,11 @@ function challengeResult(
|
|
|
427
427
|
taskId,
|
|
428
428
|
checkoutBody,
|
|
429
429
|
response,
|
|
430
|
-
{
|
|
430
|
+
{
|
|
431
|
+
requireChallenge = true,
|
|
432
|
+
allowHumanCheckout = false,
|
|
433
|
+
humanCheckoutContext = undefined,
|
|
434
|
+
} = {},
|
|
431
435
|
) {
|
|
432
436
|
const status = response.data?.status;
|
|
433
437
|
const rejected = response.status === 402 && status === "payment_rejected";
|
|
@@ -459,6 +463,13 @@ function challengeResult(
|
|
|
459
463
|
"WarpMetal returned PAYMENT-REQUIRED without X-X402API-Challenge-Digest.",
|
|
460
464
|
);
|
|
461
465
|
}
|
|
466
|
+
const humanCheckout = allowHumanCheckout
|
|
467
|
+
? humanCheckoutResult(
|
|
468
|
+
response.data?.humanCheckout,
|
|
469
|
+
taskId,
|
|
470
|
+
humanCheckoutContext,
|
|
471
|
+
)
|
|
472
|
+
: undefined;
|
|
462
473
|
return {
|
|
463
474
|
status,
|
|
464
475
|
taskId,
|
|
@@ -469,6 +480,7 @@ function challengeResult(
|
|
|
469
480
|
challengeHandle,
|
|
470
481
|
challengeDigest,
|
|
471
482
|
checkoutBodySha256: createHash("sha256").update(checkoutBody).digest("hex"),
|
|
483
|
+
...(humanCheckout ? { humanCheckout } : {}),
|
|
472
484
|
...(rejected
|
|
473
485
|
? {
|
|
474
486
|
errorCode: response.data?.errorCode,
|
|
@@ -479,6 +491,74 @@ function challengeResult(
|
|
|
479
491
|
};
|
|
480
492
|
}
|
|
481
493
|
|
|
494
|
+
function humanCheckoutResult(value, taskId, { baseUrl, stateDirectory } = {}) {
|
|
495
|
+
if (value === undefined) return undefined;
|
|
496
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
497
|
+
throw new CliError("WarpMetal returned malformed hosted checkout details.");
|
|
498
|
+
}
|
|
499
|
+
const keys = Object.keys(value).sort();
|
|
500
|
+
if (keys.join(",") !== "expiresAt,qrPayload,url") {
|
|
501
|
+
throw new CliError("WarpMetal returned malformed hosted checkout details.");
|
|
502
|
+
}
|
|
503
|
+
if (
|
|
504
|
+
typeof value.url !== "string" ||
|
|
505
|
+
typeof value.qrPayload !== "string" ||
|
|
506
|
+
value.url !== value.qrPayload ||
|
|
507
|
+
typeof value.expiresAt !== "string"
|
|
508
|
+
) {
|
|
509
|
+
throw new CliError("WarpMetal returned malformed hosted checkout details.");
|
|
510
|
+
}
|
|
511
|
+
let url;
|
|
512
|
+
try {
|
|
513
|
+
url = new URL(value.url);
|
|
514
|
+
} catch {
|
|
515
|
+
throw new CliError("WarpMetal returned an invalid hosted checkout URL.");
|
|
516
|
+
}
|
|
517
|
+
if (
|
|
518
|
+
url.protocol !== "https:" ||
|
|
519
|
+
url.origin !== "https://pay.x402api.com" ||
|
|
520
|
+
url.username ||
|
|
521
|
+
url.password ||
|
|
522
|
+
url.search ||
|
|
523
|
+
url.hash ||
|
|
524
|
+
!/^\/c\/chk_[A-Za-z0-9_-]{32}$/.test(url.pathname) ||
|
|
525
|
+
url.toString() !== value.url
|
|
526
|
+
) {
|
|
527
|
+
throw new CliError("WarpMetal returned an invalid hosted checkout URL.");
|
|
528
|
+
}
|
|
529
|
+
const rfc3339 =
|
|
530
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
531
|
+
const expiresAt = Date.parse(value.expiresAt);
|
|
532
|
+
if (
|
|
533
|
+
!rfc3339.test(value.expiresAt) ||
|
|
534
|
+
!Number.isFinite(expiresAt) ||
|
|
535
|
+
expiresAt <= Date.now()
|
|
536
|
+
) {
|
|
537
|
+
throw new CliError(
|
|
538
|
+
"WarpMetal returned an invalid or expired hosted checkout expiry.",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
url: value.url,
|
|
543
|
+
qrPayload: value.qrPayload,
|
|
544
|
+
expiresAt: value.expiresAt,
|
|
545
|
+
afterPayment: {
|
|
546
|
+
argv: [
|
|
547
|
+
"warpmetal",
|
|
548
|
+
"order",
|
|
549
|
+
"status",
|
|
550
|
+
"--task",
|
|
551
|
+
taskId,
|
|
552
|
+
"--wait",
|
|
553
|
+
...(baseUrl ? ["--base-url", baseUrl] : []),
|
|
554
|
+
...(stateDirectory ? ["--state-dir", stateDirectory] : []),
|
|
555
|
+
"--json",
|
|
556
|
+
],
|
|
557
|
+
notificationNextAction: "ask_human_for_notification_email",
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
482
562
|
async function attachPaymentWorkflow(
|
|
483
563
|
client,
|
|
484
564
|
store,
|
|
@@ -517,7 +597,8 @@ async function attachPaymentWorkflow(
|
|
|
517
597
|
paymentChallengeDigest: request.challengeDigest,
|
|
518
598
|
paymentWorkflow: workflow,
|
|
519
599
|
});
|
|
520
|
-
|
|
600
|
+
const { humanCheckout: _ephemeralHostedCheckout, ...persisted } = safe;
|
|
601
|
+
await store.savePaymentChallenge(taskId, persisted);
|
|
521
602
|
return safe;
|
|
522
603
|
}
|
|
523
604
|
|
|
@@ -773,9 +854,20 @@ async function handleCheckoutChallenge(client, store, options, context) {
|
|
|
773
854
|
store,
|
|
774
855
|
taskId,
|
|
775
856
|
order,
|
|
776
|
-
challengeResult(taskId, order.checkoutBody, response
|
|
857
|
+
challengeResult(taskId, order.checkoutBody, response, {
|
|
858
|
+
// Initial checkout is the only interactive direct-payment surface.
|
|
859
|
+
// Autonomous renewal continues to use the bounded agent-wallet policy.
|
|
860
|
+
allowHumanCheckout: true,
|
|
861
|
+
humanCheckoutContext: {
|
|
862
|
+
baseUrl: client.baseUrl,
|
|
863
|
+
stateDirectory: store.directory,
|
|
864
|
+
},
|
|
865
|
+
}),
|
|
777
866
|
stringOption(options, "request-envelope-out"),
|
|
778
867
|
);
|
|
868
|
+
const directCheckoutInstructions = safe.humanCheckout
|
|
869
|
+
? `\nDirect wallet checkout (optional, expires ${safe.humanCheckout.expiresAt}): ${safe.humanCheckout.url}\nEncode only that URL as the purchase QR. After payment, continue with: ${shellCommand(safe.humanCheckout.afterPayment.argv)}. When provisioning is ready, ask the owner for the optional lifecycle-notification email before adding it.`
|
|
870
|
+
: "";
|
|
779
871
|
const paymentInstructions = safe.paymentWorkflow
|
|
780
872
|
? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\n${walletWorkflowInstructions(safe.paymentWorkflow)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize only after selecting/funding one wallet: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
781
873
|
: "";
|
|
@@ -784,7 +876,7 @@ async function handleCheckoutChallenge(client, store, options, context) {
|
|
|
784
876
|
safe,
|
|
785
877
|
context.json,
|
|
786
878
|
response.status === 402
|
|
787
|
-
? `Payment authorization required for ${taskId}.${paymentInstructions}`
|
|
879
|
+
? `Payment authorization required for ${taskId}.${directCheckoutInstructions}${paymentInstructions}`
|
|
788
880
|
: `Checkout status for ${taskId}: ${safe.status}`,
|
|
789
881
|
);
|
|
790
882
|
return response.status === 409 ? 6 : response.status === 402 ? 7 : 0;
|
package/src/installer.js
CHANGED
|
@@ -123,6 +123,14 @@ function validateArtifact(artifact) {
|
|
|
123
123
|
return { ...artifact, url: url.toString() };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
function serverAllowsRuntimeInstall(server, currentTime = Date.now()) {
|
|
127
|
+
if (!server?.publicIp) return false;
|
|
128
|
+
if (server.state === "ready") return true;
|
|
129
|
+
if (!["cancellation_pending", "cancelled"].includes(server.state)) return false;
|
|
130
|
+
const termEndsAt = Date.parse(server.termEndsAt || "");
|
|
131
|
+
return Number.isFinite(termEndsAt) && termEndsAt > currentTime;
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
export function verifyRuntimeArtifact(content, metadata) {
|
|
127
135
|
const artifact = validateArtifact(metadata);
|
|
128
136
|
const digest = createHash("sha256").update(content).digest("hex");
|
|
@@ -270,8 +278,8 @@ export async function installRuntime({
|
|
|
270
278
|
throw new CliError("The server ID is invalid.", { exitCode: 2 });
|
|
271
279
|
}
|
|
272
280
|
const server = (await client.getServer(serverId, token)).data?.task;
|
|
273
|
-
if (
|
|
274
|
-
throw new CliError("The VPS must be ready before runtime installation.", {
|
|
281
|
+
if (!serverAllowsRuntimeInstall(server)) {
|
|
282
|
+
throw new CliError("The VPS must be ready and within its active paid term before runtime installation.", {
|
|
275
283
|
exitCode: 5,
|
|
276
284
|
});
|
|
277
285
|
}
|