warpmetal 0.3.0 → 0.4.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/src/cli.js CHANGED
@@ -14,6 +14,13 @@ import { connectionProfile, writeConnectionProfile } from "./connection.js";
14
14
  import { CliError, toErrorMessage } from "./errors.js";
15
15
  import { installSkill } from "./install-skill.js";
16
16
  import { installRuntime } from "./installer.js";
17
+ import {
18
+ createPaymentRequestEnvelope,
19
+ defaultPaymentPaths,
20
+ paymentWorkflow,
21
+ readPaymentArtifact,
22
+ writePaymentRequestEnvelope,
23
+ } from "./payment.js";
17
24
  import {
18
25
  readSandboxFile,
19
26
  requireTemporaryConfirmation,
@@ -66,8 +73,9 @@ Usage:
66
73
  --ssh-public-key-file <path> [--runtime-file <path>] [--confirm TEMPORARY]
67
74
  [--email <address>] [--idempotency-key <key>]
68
75
  warpmetal order status --task <taskId> [--wait] [--timeout-seconds <n>]
69
- warpmetal checkout challenge --task <taskId>
70
- warpmetal checkout submit --task <taskId> --payment-signature-file <path>
76
+ warpmetal checkout challenge --task <taskId> [--request-envelope-out <path>]
77
+ warpmetal checkout submit --task <taskId>
78
+ (--payment-artifact <path> | --payment-signature-file <path>)
71
79
  [--wait] [--timeout-seconds <n>]
72
80
  warpmetal server login --server <serverId> --identity <private-key-path>
73
81
  warpmetal server get --server <serverId>
@@ -112,6 +120,16 @@ function writeLine(stream, value = "") {
112
120
  stream.write(`${value}\n`);
113
121
  }
114
122
 
123
+ function shellCommand(argv) {
124
+ return argv
125
+ .map((value) =>
126
+ /^[A-Za-z0-9_./:=+@-]+$/.test(value)
127
+ ? value
128
+ : `'${value.replaceAll("'", `'\\''`)}'`,
129
+ )
130
+ .join(" ");
131
+ }
132
+
115
133
  function emit(stream, value, json, human) {
116
134
  if (json) writeLine(stream, JSON.stringify(value, null, 2));
117
135
  else writeLine(stream, human);
@@ -344,6 +362,46 @@ function challengeResult(taskId, checkoutBody, response) {
344
362
  };
345
363
  }
346
364
 
365
+ async function attachPaymentWorkflow(
366
+ client,
367
+ store,
368
+ taskId,
369
+ order,
370
+ safe,
371
+ requestedEnvelopePath,
372
+ ) {
373
+ if (!safe.paymentRequired) return safe;
374
+ const request = createPaymentRequestEnvelope({
375
+ baseUrl: client.baseUrl,
376
+ checkoutPath: order.checkoutPath,
377
+ checkoutBody: order.checkoutBody,
378
+ paymentRequired: safe.paymentRequired,
379
+ merchantReference: taskId,
380
+ });
381
+ const defaults = defaultPaymentPaths(
382
+ store.directory,
383
+ taskId,
384
+ request.challengeDigest,
385
+ );
386
+ const requestEnvelopePath = await writePaymentRequestEnvelope(
387
+ requestedEnvelopePath || defaults.requestEnvelopePath,
388
+ request.envelope,
389
+ );
390
+ const workflow = paymentWorkflow({
391
+ taskId,
392
+ requestEnvelopePath,
393
+ paymentArtifactPath: defaults.paymentArtifactPath,
394
+ });
395
+ Object.assign(safe, {
396
+ paymentTerms: request.terms,
397
+ paymentRequestDigest: request.requestDigest,
398
+ paymentChallengeDigest: request.challengeDigest,
399
+ paymentWorkflow: workflow,
400
+ });
401
+ await store.savePaymentChallenge(taskId, safe);
402
+ return safe;
403
+ }
404
+
347
405
  async function handleHealth(client, { json, stdout }) {
348
406
  const result = await client.health();
349
407
  emit(
@@ -470,16 +528,23 @@ async function handleCheckoutChallenge(client, store, options, context) {
470
528
  bodyText: order.checkoutBody,
471
529
  token,
472
530
  });
473
- const safe = challengeResult(taskId, order.checkoutBody, response);
474
- if (safe.paymentRequired) {
475
- await store.savePaymentChallenge(taskId, safe);
476
- }
531
+ const safe = await attachPaymentWorkflow(
532
+ client,
533
+ store,
534
+ taskId,
535
+ order,
536
+ challengeResult(taskId, order.checkoutBody, response),
537
+ stringOption(options, "request-envelope-out"),
538
+ );
539
+ const paymentInstructions = safe.paymentWorkflow
540
+ ? `\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)}`
541
+ : "";
477
542
  emit(
478
543
  context.stdout,
479
544
  safe,
480
545
  context.json,
481
546
  response.status === 402
482
- ? `Payment authorization required for ${taskId}.\nPAYMENT-REQUIRED: ${safe.paymentRequired}`
547
+ ? `Payment authorization required for ${taskId}.${paymentInstructions}`
483
548
  : `Checkout status for ${taskId}: ${safe.status}`,
484
549
  );
485
550
  return response.status === 409 ? 6 : response.status === 402 ? 7 : 0;
@@ -487,19 +552,45 @@ async function handleCheckoutChallenge(client, store, options, context) {
487
552
 
488
553
  async function handleCheckoutSubmit(client, store, options, context) {
489
554
  const taskId = stringOption(options, "task", { required: true });
490
- const signatureFile = stringOption(options, "payment-signature-file", {
491
- required: true,
492
- });
493
- const paymentSignature = await readHeaderValueFile(
494
- signatureFile,
495
- "The payment signature file",
496
- );
555
+ const signatureFile = stringOption(options, "payment-signature-file");
556
+ const artifactFile = stringOption(options, "payment-artifact");
557
+ if (Boolean(signatureFile) === Boolean(artifactFile)) {
558
+ throw new CliError(
559
+ "Use exactly one of --payment-artifact or --payment-signature-file.",
560
+ { exitCode: 2 },
561
+ );
562
+ }
497
563
  const order = await store.order(taskId);
498
564
  if (!order?.checkoutPath || !order?.checkoutBody) {
499
565
  throw new CliError(`No exact checkout state exists for ${taskId}.`, {
500
566
  exitCode: 2,
501
567
  });
502
568
  }
569
+ let walletPayment;
570
+ let paymentSignature;
571
+ if (artifactFile) {
572
+ if (!order.paymentRequired) {
573
+ throw new CliError(
574
+ `No saved PAYMENT-REQUIRED challenge exists for ${taskId}. Run warpmetal checkout challenge first.`,
575
+ { exitCode: 2 },
576
+ );
577
+ }
578
+ const expected = createPaymentRequestEnvelope({
579
+ baseUrl: client.baseUrl,
580
+ checkoutPath: order.checkoutPath,
581
+ checkoutBody: order.checkoutBody,
582
+ paymentRequired: order.paymentRequired,
583
+ merchantReference: taskId,
584
+ });
585
+ walletPayment = await readPaymentArtifact(artifactFile, expected);
586
+ paymentSignature = walletPayment.paymentSignature;
587
+ await store.saveWalletPaymentAttempt(taskId, walletPayment);
588
+ } else {
589
+ paymentSignature = await readHeaderValueFile(
590
+ signatureFile,
591
+ "The payment signature file",
592
+ );
593
+ }
503
594
  const token = await requireTaskToken(store, taskId, options, context.env);
504
595
  const wait = booleanOption(options, "wait");
505
596
  const deadline = timeoutDeadline(
@@ -512,8 +603,13 @@ async function handleCheckoutSubmit(client, store, options, context) {
512
603
  token,
513
604
  paymentSignature,
514
605
  });
515
- const safe = challengeResult(taskId, order.checkoutBody, response);
516
- if (safe.paymentRequired) await store.savePaymentChallenge(taskId, safe);
606
+ const safe = await attachPaymentWorkflow(
607
+ client,
608
+ store,
609
+ taskId,
610
+ order,
611
+ challengeResult(taskId, order.checkoutBody, response),
612
+ );
517
613
  const retryable =
518
614
  response.status === 202 &&
519
615
  ["payment_pending", "payment_finalizing"].includes(response.data?.status);
@@ -527,6 +623,19 @@ async function handleCheckoutSubmit(client, store, options, context) {
527
623
  ...safe,
528
624
  task: response.data?.task,
529
625
  message: response.data?.message,
626
+ ...(walletPayment
627
+ ? {
628
+ walletPayment: {
629
+ attemptId: walletPayment.attemptId,
630
+ requestDigest: walletPayment.requestDigest,
631
+ buyerPaymentIdentifier: walletPayment.buyerPaymentIdentifier,
632
+ wallet: walletPayment.wallet,
633
+ payerAddress: walletPayment.payerAddress,
634
+ artifactPath: walletPayment.path,
635
+ expiresAt: walletPayment.expiresAt,
636
+ },
637
+ }
638
+ : {}),
530
639
  };
531
640
  emit(
532
641
  context.stdout,
@@ -662,12 +771,7 @@ async function handleServerReload(client, store, options, context) {
662
771
  options,
663
772
  "acknowledge-agent-runtime-reset",
664
773
  );
665
- const token = await requireReloadToken(
666
- store,
667
- serverId,
668
- options,
669
- context.env,
670
- );
774
+ const token = await requireReloadToken(store, serverId, options, context.env);
671
775
  const server = (await client.getServer(serverId, token)).data?.task;
672
776
  if (server?.agentRuntime && !acknowledgeRuntimeReset) {
673
777
  throw new CliError(
@@ -717,7 +821,7 @@ async function handleServerReload(client, store, options, context) {
717
821
  ? " Verify and refresh the owner SSH host key, reinstall Agent Runtime, then refresh every sandbox connection profile."
718
822
  : operation?.state === "succeeded"
719
823
  ? " Verify and refresh the owner SSH host key before reconnecting."
720
- : ""),
824
+ : ""),
721
825
  );
722
826
  if (operation?.state === "manual_review") return 6;
723
827
  if (operation?.state === "failed") return 5;
@@ -1399,13 +1503,19 @@ async function dispatch(positionals, options, passthrough, context) {
1399
1503
  ]);
1400
1504
  return handleTaskStatus(client, store, options, context);
1401
1505
  case "checkout challenge":
1402
- rejectUnknownOptions(options, [...COMMON_OPTIONS, "task", "token-file"]);
1506
+ rejectUnknownOptions(options, [
1507
+ ...COMMON_OPTIONS,
1508
+ "task",
1509
+ "token-file",
1510
+ "request-envelope-out",
1511
+ ]);
1403
1512
  return handleCheckoutChallenge(client, store, options, context);
1404
1513
  case "checkout submit":
1405
1514
  rejectUnknownOptions(options, [
1406
1515
  ...COMMON_OPTIONS,
1407
1516
  "task",
1408
1517
  "token-file",
1518
+ "payment-artifact",
1409
1519
  "payment-signature-file",
1410
1520
  "wait",
1411
1521
  "timeout-seconds",