warpmetal 0.8.5 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warpmetal",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Agent-safe CLI and skill for purchasing, renewing, and managing WarpMetal VPS servers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- { requireChallenge = true } = {},
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
- await store.savePaymentChallenge(taskId, safe);
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;