warpmetal 0.4.2 → 0.6.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 +60 -9
- package/package.json +5 -3
- package/skills/warpmetal/SKILL.md +42 -6
- package/skills/warpmetal/agents/openai.yaml +2 -2
- package/skills/warpmetal/references/cli-reference.md +48 -4
- package/skills/warpmetal/references/payments.md +16 -2
- package/skills/warpmetal/references/renewals.md +127 -0
- package/skills/warpmetal/references/safety.md +14 -0
- package/src/api.js +70 -22
- package/src/cli.js +887 -20
- package/src/payment.js +26 -11
- package/src/ssh.js +111 -2
- package/src/state.js +155 -4
package/src/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
booleanOption,
|
|
@@ -30,8 +30,11 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
connectSandbox,
|
|
32
32
|
generateSandboxKey,
|
|
33
|
+
generateServerKey,
|
|
33
34
|
readSshPublicKey,
|
|
35
|
+
serverKeyName,
|
|
34
36
|
signSshChallenge,
|
|
37
|
+
sshFingerprint,
|
|
35
38
|
} from "./ssh.js";
|
|
36
39
|
import { resolveStateDirectory, StateStore } from "./state.js";
|
|
37
40
|
import { VERSION } from "./version.js";
|
|
@@ -70,23 +73,36 @@ Usage:
|
|
|
70
73
|
warpmetal health [--json]
|
|
71
74
|
warpmetal catalog [--plan <planId>] [--json]
|
|
72
75
|
warpmetal order prepare --plan <planId> --hostname <name> --os <exact-name>
|
|
73
|
-
--ssh-
|
|
76
|
+
(--generate-ssh-key [--ssh-key-name <name>] | --ssh-public-key-file <path>)
|
|
77
|
+
[--runtime-file <path>] [--confirm TEMPORARY]
|
|
74
78
|
[--email <address>] [--idempotency-key <key>]
|
|
75
79
|
warpmetal order status --task <taskId> [--wait] [--timeout-seconds <n>]
|
|
76
80
|
warpmetal checkout challenge --task <taskId> [--request-envelope-out <path>]
|
|
77
81
|
warpmetal checkout submit --task <taskId>
|
|
78
82
|
(--payment-artifact <path> | --payment-signature-file <path>)
|
|
79
83
|
[--wait] [--timeout-seconds <n>]
|
|
80
|
-
warpmetal
|
|
84
|
+
warpmetal renewal configure --server <serverId> --renew-before-days <n>
|
|
85
|
+
--maximum-payment-atomic <amount> (--maximum-renewals <n> | --renew-through <UTC>)
|
|
86
|
+
--allowed-network <CAIP-2> --allowed-asset <asset> --wallet <name>
|
|
87
|
+
warpmetal renewal status|prepare|submit|run ...
|
|
88
|
+
warpmetal renewal due (--server <serverId> | --all)
|
|
89
|
+
warpmetal notifications configure --server <serverId> --email <address>
|
|
90
|
+
warpmetal notifications status --server <serverId>
|
|
91
|
+
warpmetal identity generate --hostname <name> [--ssh-key-name <name>]
|
|
92
|
+
warpmetal identity list
|
|
93
|
+
warpmetal server identity --server <serverId>
|
|
94
|
+
warpmetal server identity attach --server <serverId> --identity <private-key-path>
|
|
95
|
+
warpmetal server login --server <serverId> [--identity <private-key-path>]
|
|
81
96
|
warpmetal server get --server <serverId>
|
|
82
97
|
warpmetal server power --server <serverId> --action <boot|reboot|shutdown>
|
|
83
98
|
--confirm <same-action> [--wait] [--idempotency-key <key>]
|
|
84
99
|
warpmetal server reload --server <serverId> --confirm ERASE --power-off-first
|
|
85
100
|
[--acknowledge-agent-runtime-reset] [--hostname <name>] [--os <exact-name>]
|
|
86
|
-
[--ssh-
|
|
101
|
+
[--generate-ssh-key [--ssh-key-name <name>] | --ssh-public-key-file <path>]
|
|
102
|
+
[--wait] [--idempotency-key <key>]
|
|
87
103
|
warpmetal operation get --operation <operationId> [--server <serverId>] [--wait]
|
|
88
104
|
warpmetal runtime enable|get --server <serverId> [--wait]
|
|
89
|
-
warpmetal runtime install --server <serverId> --identity <owner-key> --ssh-user <user>
|
|
105
|
+
warpmetal runtime install --server <serverId> [--identity <owner-key>] --ssh-user <user>
|
|
90
106
|
--confirm INSTALL [--wait]
|
|
91
107
|
warpmetal sandbox create --server <serverId> --name <name> --size <size>
|
|
92
108
|
[--lifetime temporary] [--expires-in-seconds <n>] [--confirm TEMPORARY] [--wait]
|
|
@@ -429,13 +445,88 @@ async function handleCatalog(client, options, { json, stdout }) {
|
|
|
429
445
|
return 0;
|
|
430
446
|
}
|
|
431
447
|
|
|
448
|
+
async function pathExists(path) {
|
|
449
|
+
try {
|
|
450
|
+
await stat(path);
|
|
451
|
+
return true;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (error?.code === "ENOENT") return false;
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function identityId() {
|
|
459
|
+
return `idn_${randomUUID()}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function generatedIdentity(store, hostname, options, context) {
|
|
463
|
+
const generated = await generateServerKey(
|
|
464
|
+
join(store.directory, "ssh"),
|
|
465
|
+
hostname,
|
|
466
|
+
stringOption(options, "ssh-key-name"),
|
|
467
|
+
{ spawn: context.spawnSyncImpl },
|
|
468
|
+
);
|
|
469
|
+
const identity = {
|
|
470
|
+
identityId: identityId(),
|
|
471
|
+
...generated,
|
|
472
|
+
generated: true,
|
|
473
|
+
};
|
|
474
|
+
delete identity.sshPublicKey;
|
|
475
|
+
await store.saveIdentity(identity);
|
|
476
|
+
return { identity, sshPublicKey: generated.sshPublicKey };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function existingIdentity(store, hostname, publicKeyFile, requestedName) {
|
|
480
|
+
const publicKeyPath = resolve(publicKeyFile);
|
|
481
|
+
const sshPublicKey = await readSshPublicKey(publicKeyPath);
|
|
482
|
+
const candidatePrivatePath = publicKeyPath.endsWith(".pub")
|
|
483
|
+
? publicKeyPath.slice(0, -4)
|
|
484
|
+
: undefined;
|
|
485
|
+
const keyName = serverKeyName(
|
|
486
|
+
hostname,
|
|
487
|
+
requestedName || basename(candidatePrivatePath || publicKeyPath),
|
|
488
|
+
).keyName;
|
|
489
|
+
const identity = {
|
|
490
|
+
identityId: identityId(),
|
|
491
|
+
keyName,
|
|
492
|
+
hostnameAtCreation: hostname.toLowerCase(),
|
|
493
|
+
privateKeyPath:
|
|
494
|
+
candidatePrivatePath && (await pathExists(candidatePrivatePath))
|
|
495
|
+
? candidatePrivatePath
|
|
496
|
+
: undefined,
|
|
497
|
+
publicKeyPath,
|
|
498
|
+
keyType: sshPublicKey.split(/\s+/)[0],
|
|
499
|
+
sshFingerprint: sshFingerprint(sshPublicKey),
|
|
500
|
+
generated: false,
|
|
501
|
+
};
|
|
502
|
+
await store.saveIdentity(identity);
|
|
503
|
+
return { identity, sshPublicKey };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function resolveServerIdentity(store, serverId, explicit) {
|
|
507
|
+
if (explicit) return resolve(explicit);
|
|
508
|
+
const identity = await store.identityForServer(serverId);
|
|
509
|
+
if (!identity?.privateKeyPath) {
|
|
510
|
+
throw new CliError(
|
|
511
|
+
`No private SSH identity is mapped to ${serverId}. Use --identity once or run warpmetal server identity attach.`,
|
|
512
|
+
{ exitCode: 4, details: { code: "identity_required", serverId } },
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return identity.privateKeyPath;
|
|
516
|
+
}
|
|
517
|
+
|
|
432
518
|
async function handlePrepareOrder(client, store, options, context) {
|
|
433
519
|
const planId = stringOption(options, "plan", { required: true });
|
|
434
520
|
const hostname = stringOption(options, "hostname", { required: true });
|
|
435
521
|
const osName = stringOption(options, "os", { required: true });
|
|
436
|
-
const publicKeyFile = stringOption(options, "ssh-public-key-file"
|
|
437
|
-
|
|
438
|
-
|
|
522
|
+
const publicKeyFile = stringOption(options, "ssh-public-key-file");
|
|
523
|
+
const generateSshKey = booleanOption(options, "generate-ssh-key");
|
|
524
|
+
if (Boolean(publicKeyFile) === generateSshKey) {
|
|
525
|
+
throw new CliError(
|
|
526
|
+
"Use exactly one of --generate-ssh-key or --ssh-public-key-file.",
|
|
527
|
+
{ exitCode: 2 },
|
|
528
|
+
);
|
|
529
|
+
}
|
|
439
530
|
const email = stringOption(options, "email");
|
|
440
531
|
const runtimeFile = stringOption(options, "runtime-file");
|
|
441
532
|
const sandboxes = runtimeFile
|
|
@@ -470,15 +561,32 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
470
561
|
}
|
|
471
562
|
if (sandboxes) validateRuntimeCatalog(product, osName, sandboxes);
|
|
472
563
|
|
|
473
|
-
const
|
|
474
|
-
|
|
564
|
+
const selected = generateSshKey
|
|
565
|
+
? await generatedIdentity(store, hostname, options, context)
|
|
566
|
+
: await existingIdentity(
|
|
567
|
+
store,
|
|
568
|
+
hostname,
|
|
569
|
+
publicKeyFile,
|
|
570
|
+
stringOption(options, "ssh-key-name"),
|
|
571
|
+
);
|
|
572
|
+
const request = {
|
|
573
|
+
planId,
|
|
574
|
+
hostname,
|
|
575
|
+
osName,
|
|
576
|
+
sshPublicKey: selected.sshPublicKey,
|
|
577
|
+
sshKeyLabel: selected.identity.keyName,
|
|
578
|
+
};
|
|
475
579
|
if (email) request.email = email;
|
|
476
580
|
if (sandboxes) request.agentRuntime = { sandboxes };
|
|
477
581
|
const key =
|
|
478
582
|
stringOption(options, "idempotency-key") || idempotencyKey("order");
|
|
479
583
|
const response = await client.prepareOrder(request, key);
|
|
480
584
|
const checkoutBody = JSON.stringify({ taskId: response.data.task.id });
|
|
481
|
-
await store.savePreparedOrder(
|
|
585
|
+
await store.savePreparedOrder(
|
|
586
|
+
response.data,
|
|
587
|
+
checkoutBody,
|
|
588
|
+
selected.identity.identityId,
|
|
589
|
+
);
|
|
482
590
|
const safe = safePreparedOrder(response.data, store.path);
|
|
483
591
|
if (sandboxes) {
|
|
484
592
|
safe.agentRuntimeIntent = {
|
|
@@ -650,6 +758,518 @@ async function handleCheckoutSubmit(client, store, options, context) {
|
|
|
650
758
|
}
|
|
651
759
|
}
|
|
652
760
|
|
|
761
|
+
function atomicOption(options, name, { required = false } = {}) {
|
|
762
|
+
const value = stringOption(options, name, { required });
|
|
763
|
+
if (value === undefined) return undefined;
|
|
764
|
+
if (!/^(?:0|[1-9][0-9]{0,77})$/.test(value) || value === "0") {
|
|
765
|
+
throw new CliError(`--${name} must be a positive canonical atomic amount.`, {
|
|
766
|
+
exitCode: 2,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
return value;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function refillRenewBy(termEndsAt, currentTime = Date.now()) {
|
|
773
|
+
const minimum = currentTime + 30 * 60 * 1000;
|
|
774
|
+
const parsed = Date.parse(termEndsAt || "");
|
|
775
|
+
return new Date(Number.isFinite(parsed) && parsed > minimum ? parsed : minimum).toISOString();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function handleRenewalConfigure(client, store, options, context) {
|
|
779
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
780
|
+
const wallet = stringOption(options, "wallet", { required: true });
|
|
781
|
+
const maximumRenewals = integerOption(options, "maximum-renewals");
|
|
782
|
+
const renewThrough = stringOption(options, "renew-through");
|
|
783
|
+
if (Boolean(maximumRenewals) === Boolean(renewThrough)) {
|
|
784
|
+
throw new CliError(
|
|
785
|
+
"Use exactly one of --maximum-renewals or --renew-through.",
|
|
786
|
+
{ exitCode: 2 },
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const maximumPaymentAtomic = atomicOption(
|
|
790
|
+
options,
|
|
791
|
+
"maximum-payment-atomic",
|
|
792
|
+
{ required: true },
|
|
793
|
+
);
|
|
794
|
+
const maximumTotalSpendAtomic = atomicOption(
|
|
795
|
+
options,
|
|
796
|
+
"maximum-total-spend-atomic",
|
|
797
|
+
);
|
|
798
|
+
const refillTargetAtomic =
|
|
799
|
+
atomicOption(options, "refill-target-atomic") || maximumPaymentAtomic;
|
|
800
|
+
if (BigInt(refillTargetAtomic) > BigInt(maximumPaymentAtomic)) {
|
|
801
|
+
throw new CliError(
|
|
802
|
+
"--refill-target-atomic cannot exceed --maximum-payment-atomic.",
|
|
803
|
+
{ exitCode: 2 },
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const body = {
|
|
807
|
+
enabled: !booleanOption(options, "disabled"),
|
|
808
|
+
renewBeforeDays: integerOption(options, "renew-before-days", 3),
|
|
809
|
+
maximumPaymentAtomic,
|
|
810
|
+
maximumRenewals,
|
|
811
|
+
renewThrough,
|
|
812
|
+
maximumTotalSpendAtomic,
|
|
813
|
+
allowedNetwork: stringOption(options, "allowed-network", {
|
|
814
|
+
required: true,
|
|
815
|
+
}),
|
|
816
|
+
allowedAsset: stringOption(options, "allowed-asset", { required: true }),
|
|
817
|
+
};
|
|
818
|
+
for (const [key, value] of Object.entries(body)) {
|
|
819
|
+
if (value === undefined) delete body[key];
|
|
820
|
+
}
|
|
821
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
822
|
+
const result = await client.putRenewalPolicy(serverId, body, token);
|
|
823
|
+
await store.saveRenewalPolicy(
|
|
824
|
+
serverId,
|
|
825
|
+
result.data.policy,
|
|
826
|
+
wallet,
|
|
827
|
+
refillTargetAtomic,
|
|
828
|
+
);
|
|
829
|
+
const email = stringOption(options, "email");
|
|
830
|
+
let notifications;
|
|
831
|
+
if (email) {
|
|
832
|
+
notifications = (
|
|
833
|
+
await client.putNotifications(serverId, { email }, token)
|
|
834
|
+
).data;
|
|
835
|
+
}
|
|
836
|
+
const output = { ...result.data, wallet, refillTargetAtomic, notifications };
|
|
837
|
+
emit(
|
|
838
|
+
context.stdout,
|
|
839
|
+
output,
|
|
840
|
+
context.json,
|
|
841
|
+
`Configured bounded renewal for ${serverId} with wallet ${wallet}.${email ? " Check the email verification link." : ""}`,
|
|
842
|
+
);
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function handleRenewalStatus(client, store, options, context) {
|
|
847
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
848
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
849
|
+
const result = await client.getRenewalPolicy(serverId, token);
|
|
850
|
+
const local = await store.renewal(serverId);
|
|
851
|
+
const output = { ...result.data, local: local ? { wallet: local.wallet, refillTargetAtomic: local.refillTargetAtomic } : null };
|
|
852
|
+
emit(
|
|
853
|
+
context.stdout,
|
|
854
|
+
output,
|
|
855
|
+
context.json,
|
|
856
|
+
result.data.configured
|
|
857
|
+
? `${serverId}: ${result.data.policy.nextAction}${result.data.policy.reason ? ` (${result.data.policy.reason})` : ""}`
|
|
858
|
+
: `${serverId}: no renewal policy configured`,
|
|
859
|
+
);
|
|
860
|
+
return result.data.policy?.nextAction === "approval_required" ? 6 : 0;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function attachRenewalPaymentWorkflow(
|
|
864
|
+
client,
|
|
865
|
+
store,
|
|
866
|
+
serverId,
|
|
867
|
+
server,
|
|
868
|
+
renewal,
|
|
869
|
+
response,
|
|
870
|
+
requestedEnvelopePath,
|
|
871
|
+
) {
|
|
872
|
+
const bodyText = JSON.stringify({ serverId });
|
|
873
|
+
const checkoutPath = `/checkout/${server.planId}/renew`;
|
|
874
|
+
const safe = challengeResult(serverId, bodyText, response);
|
|
875
|
+
if (!safe.paymentRequired) return safe;
|
|
876
|
+
const request = createPaymentRequestEnvelope({
|
|
877
|
+
baseUrl: client.baseUrl,
|
|
878
|
+
checkoutPath,
|
|
879
|
+
checkoutBody: bodyText,
|
|
880
|
+
paymentRequired: safe.paymentRequired,
|
|
881
|
+
merchantReference: `renew:${serverId}:${server.termEndsAt || "unset"}`,
|
|
882
|
+
});
|
|
883
|
+
const matchingTerms = request.terms.filter(
|
|
884
|
+
(term) =>
|
|
885
|
+
term.agentWalletSupported &&
|
|
886
|
+
term.network === renewal.policy.allowedNetwork &&
|
|
887
|
+
(term.asset.startsWith("0x")
|
|
888
|
+
? term.asset.toLowerCase() === renewal.policy.allowedAsset.toLowerCase()
|
|
889
|
+
: term.asset === renewal.policy.allowedAsset),
|
|
890
|
+
);
|
|
891
|
+
if (matchingTerms.length !== 1) {
|
|
892
|
+
throw new CliError(
|
|
893
|
+
"The renewal challenge does not contain exactly one payment rail allowed by policy.",
|
|
894
|
+
{ exitCode: 6 },
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
const term = matchingTerms[0];
|
|
898
|
+
if (BigInt(term.amountAtomic) > BigInt(renewal.policy.maximumPaymentAtomic)) {
|
|
899
|
+
throw new CliError("The renewal price is above the autonomous policy ceiling.", {
|
|
900
|
+
exitCode: 6,
|
|
901
|
+
details: { code: "renewal_price_above_limit", term },
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
if (
|
|
905
|
+
renewal.policy.maximumTotalSpendAtomic &&
|
|
906
|
+
BigInt(renewal.policy.totalSpendAtomic) + BigInt(term.amountAtomic) >
|
|
907
|
+
BigInt(renewal.policy.maximumTotalSpendAtomic)
|
|
908
|
+
) {
|
|
909
|
+
throw new CliError("The renewal would exceed the cumulative policy budget.", {
|
|
910
|
+
exitCode: 6,
|
|
911
|
+
details: { code: "renewal_budget_exhausted", term },
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
const defaults = defaultPaymentPaths(
|
|
915
|
+
store.directory,
|
|
916
|
+
`renew-${serverId}`,
|
|
917
|
+
request.challengeDigest,
|
|
918
|
+
);
|
|
919
|
+
const requestEnvelopePath = await writePaymentRequestEnvelope(
|
|
920
|
+
requestedEnvelopePath || defaults.requestEnvelopePath,
|
|
921
|
+
request.envelope,
|
|
922
|
+
);
|
|
923
|
+
const workflow = paymentWorkflow({
|
|
924
|
+
taskId: server.id,
|
|
925
|
+
serverId,
|
|
926
|
+
kind: "renewal",
|
|
927
|
+
requestEnvelopePath,
|
|
928
|
+
paymentArtifactPath: defaults.paymentArtifactPath,
|
|
929
|
+
});
|
|
930
|
+
const refillTarget =
|
|
931
|
+
renewal.refillTargetAtomic &&
|
|
932
|
+
BigInt(renewal.refillTargetAtomic) >= BigInt(term.amountAtomic)
|
|
933
|
+
? renewal.refillTargetAtomic
|
|
934
|
+
: term.amountAtomic;
|
|
935
|
+
const refillWorkflow = {
|
|
936
|
+
environment: {
|
|
937
|
+
X402API_NOTIFICATION_URL: `${client.baseUrl}/notifications/x402api/refill`,
|
|
938
|
+
},
|
|
939
|
+
argv: [
|
|
940
|
+
"x402api",
|
|
941
|
+
"wallet",
|
|
942
|
+
"notify-refill",
|
|
943
|
+
"--wallet",
|
|
944
|
+
renewal.wallet,
|
|
945
|
+
"--subscription-reference",
|
|
946
|
+
renewal.policy.notificationReference,
|
|
947
|
+
"--renew-by",
|
|
948
|
+
refillRenewBy(server.termEndsAt),
|
|
949
|
+
"--target-balance-atomic",
|
|
950
|
+
refillTarget,
|
|
951
|
+
"--reason",
|
|
952
|
+
"renewal",
|
|
953
|
+
"--json",
|
|
954
|
+
],
|
|
955
|
+
};
|
|
956
|
+
Object.assign(safe, {
|
|
957
|
+
serverId,
|
|
958
|
+
paymentTerms: request.terms,
|
|
959
|
+
selectedPolicyTerm: term,
|
|
960
|
+
paymentRequestDigest: request.requestDigest,
|
|
961
|
+
paymentChallengeDigest: request.challengeDigest,
|
|
962
|
+
paymentWorkflow: workflow,
|
|
963
|
+
refillWorkflow,
|
|
964
|
+
});
|
|
965
|
+
await store.saveRenewalChallenge(serverId, {
|
|
966
|
+
checkoutPath,
|
|
967
|
+
checkoutBody: bodyText,
|
|
968
|
+
termEndsAt: server.termEndsAt,
|
|
969
|
+
paymentRequired: safe.paymentRequired,
|
|
970
|
+
paymentAttemptId: safe.paymentAttemptId,
|
|
971
|
+
paymentRequestDigest: safe.paymentRequestDigest,
|
|
972
|
+
paymentChallengeDigest: safe.paymentChallengeDigest,
|
|
973
|
+
paymentRequestEnvelopePath: requestEnvelopePath,
|
|
974
|
+
paymentArtifactPath: defaults.paymentArtifactPath,
|
|
975
|
+
selectedPolicyTerm: term,
|
|
976
|
+
});
|
|
977
|
+
return safe;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function renewalActionExitCode(action) {
|
|
981
|
+
if (action === "sign_payment") return 7;
|
|
982
|
+
if (action === "reconcile_pending") return 8;
|
|
983
|
+
if (["policy_required", "approval_required", "manual_review"].includes(action)) {
|
|
984
|
+
return 6;
|
|
985
|
+
}
|
|
986
|
+
return 0;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function prepareRenewal(client, store, serverId, options, context) {
|
|
990
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
991
|
+
const [serverResult, policyResult] = await Promise.all([
|
|
992
|
+
client.getServer(serverId, token),
|
|
993
|
+
client.getRenewalPolicy(serverId, token),
|
|
994
|
+
]);
|
|
995
|
+
if (!policyResult.data.configured) {
|
|
996
|
+
return {
|
|
997
|
+
action: "policy_required",
|
|
998
|
+
serverId,
|
|
999
|
+
reason: "renewal_policy_required",
|
|
1000
|
+
task: serverResult.data.task,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const policy = policyResult.data.policy;
|
|
1004
|
+
if (policy.nextAction !== "sign_payment") {
|
|
1005
|
+
return {
|
|
1006
|
+
action: policy.nextAction,
|
|
1007
|
+
serverId,
|
|
1008
|
+
reason: policy.reason,
|
|
1009
|
+
policy,
|
|
1010
|
+
task: serverResult.data.task,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const local = await store.renewal(serverId);
|
|
1014
|
+
if (!local?.wallet) {
|
|
1015
|
+
return {
|
|
1016
|
+
action: "approval_required",
|
|
1017
|
+
serverId,
|
|
1018
|
+
reason: "wallet_binding_required",
|
|
1019
|
+
policy,
|
|
1020
|
+
task: serverResult.data.task,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
local.policy = policy;
|
|
1024
|
+
const bodyText = JSON.stringify({ serverId });
|
|
1025
|
+
const response = await client.renewalCheckout(serverResult.data.task.planId, {
|
|
1026
|
+
bodyText,
|
|
1027
|
+
token,
|
|
1028
|
+
});
|
|
1029
|
+
const safe = await attachRenewalPaymentWorkflow(
|
|
1030
|
+
client,
|
|
1031
|
+
store,
|
|
1032
|
+
serverId,
|
|
1033
|
+
serverResult.data.task,
|
|
1034
|
+
local,
|
|
1035
|
+
response,
|
|
1036
|
+
stringOption(options, "request-envelope-out"),
|
|
1037
|
+
);
|
|
1038
|
+
safe.action =
|
|
1039
|
+
response.status === 402
|
|
1040
|
+
? "sign_payment"
|
|
1041
|
+
: response.status === 409
|
|
1042
|
+
? "manual_review"
|
|
1043
|
+
: response.status === 202
|
|
1044
|
+
? "reconcile_pending"
|
|
1045
|
+
: safe.status === "renewed"
|
|
1046
|
+
? "renewed"
|
|
1047
|
+
: "reconcile_pending";
|
|
1048
|
+
safe.policy = policy;
|
|
1049
|
+
safe.task = response.data?.task || serverResult.data.task;
|
|
1050
|
+
return safe;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function handleRenewalPrepare(client, store, options, context) {
|
|
1054
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1055
|
+
const safe = await prepareRenewal(client, store, serverId, options, context);
|
|
1056
|
+
const instructions = safe.paymentWorkflow
|
|
1057
|
+
? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nIf the wallet is short of funds, set ${Object.entries(safe.refillWorkflow.environment).map(([key, value]) => `${key}=${value}`).join(" ")} and run: ${shellCommand(safe.refillWorkflow.argv)}\nSubmit: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
1058
|
+
: "";
|
|
1059
|
+
emit(
|
|
1060
|
+
context.stdout,
|
|
1061
|
+
safe,
|
|
1062
|
+
context.json,
|
|
1063
|
+
safe.action === "sign_payment"
|
|
1064
|
+
? `Renewal payment authorization is required for ${serverId}.${instructions}`
|
|
1065
|
+
: `${serverId}: ${safe.action}${safe.reason ? ` (${safe.reason})` : ""}`,
|
|
1066
|
+
);
|
|
1067
|
+
return renewalActionExitCode(safe.action);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function handleRenewalRun(client, store, options, context) {
|
|
1071
|
+
const explicit = stringOption(options, "server");
|
|
1072
|
+
const allDue = booleanOption(options, "all-due");
|
|
1073
|
+
if (Boolean(explicit) === allDue) {
|
|
1074
|
+
throw new CliError("Use exactly one of --server or --all-due.", { exitCode: 2 });
|
|
1075
|
+
}
|
|
1076
|
+
if (allDue && stringOption(options, "request-envelope-out")) {
|
|
1077
|
+
throw new CliError("--request-envelope-out cannot be shared by --all-due.", {
|
|
1078
|
+
exitCode: 2,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
if (explicit) return handleRenewalPrepare(client, store, options, context);
|
|
1082
|
+
|
|
1083
|
+
const serverIds = (await store.summary()).renewals.map(
|
|
1084
|
+
(renewal) => renewal.serverId,
|
|
1085
|
+
);
|
|
1086
|
+
const results = [];
|
|
1087
|
+
for (const serverId of serverIds) {
|
|
1088
|
+
try {
|
|
1089
|
+
results.push(await prepareRenewal(client, store, serverId, options, context));
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
results.push({
|
|
1092
|
+
action: "approval_required",
|
|
1093
|
+
serverId,
|
|
1094
|
+
reason: "renewal_check_failed",
|
|
1095
|
+
error: toErrorMessage(error),
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const output = { action: "batch", results };
|
|
1100
|
+
emit(
|
|
1101
|
+
context.stdout,
|
|
1102
|
+
output,
|
|
1103
|
+
context.json,
|
|
1104
|
+
results.length
|
|
1105
|
+
? results.map((result) => `${result.serverId}: ${result.action}`).join("\n")
|
|
1106
|
+
: "No local renewal policies are configured.",
|
|
1107
|
+
);
|
|
1108
|
+
const codes = results.map((result) => renewalActionExitCode(result.action));
|
|
1109
|
+
return codes.includes(6) ? 6 : codes.includes(8) ? 8 : codes.includes(7) ? 7 : 0;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async function handleRenewalSubmit(client, store, options, context) {
|
|
1113
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1114
|
+
const artifactFile = stringOption(options, "payment-artifact", {
|
|
1115
|
+
required: true,
|
|
1116
|
+
});
|
|
1117
|
+
const renewal = await store.renewal(serverId);
|
|
1118
|
+
if (
|
|
1119
|
+
!renewal?.checkoutPath ||
|
|
1120
|
+
!renewal?.checkoutBody ||
|
|
1121
|
+
!renewal?.paymentRequired ||
|
|
1122
|
+
!renewal?.policy
|
|
1123
|
+
) {
|
|
1124
|
+
throw new CliError(
|
|
1125
|
+
`No exact saved renewal challenge exists for ${serverId}. Run renewal prepare first.`,
|
|
1126
|
+
{ exitCode: 2 },
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
const expected = createPaymentRequestEnvelope({
|
|
1130
|
+
baseUrl: client.baseUrl,
|
|
1131
|
+
checkoutPath: renewal.checkoutPath,
|
|
1132
|
+
checkoutBody: renewal.checkoutBody,
|
|
1133
|
+
paymentRequired: renewal.paymentRequired,
|
|
1134
|
+
merchantReference: `renew:${serverId}:${renewal.termEndsAt || "unset"}`,
|
|
1135
|
+
});
|
|
1136
|
+
const walletPayment = await readPaymentArtifact(artifactFile, expected);
|
|
1137
|
+
const selected = expected.terms.find(
|
|
1138
|
+
(term) => term.requirementDigest === walletPayment.selectedRequirementDigest,
|
|
1139
|
+
);
|
|
1140
|
+
const policy = renewal.policy;
|
|
1141
|
+
const normalizedAsset = selected?.asset?.startsWith("0x")
|
|
1142
|
+
? selected.asset.toLowerCase()
|
|
1143
|
+
: selected?.asset;
|
|
1144
|
+
if (
|
|
1145
|
+
!selected ||
|
|
1146
|
+
selected.network !== policy.allowedNetwork ||
|
|
1147
|
+
normalizedAsset !==
|
|
1148
|
+
(policy.allowedAsset.startsWith("0x")
|
|
1149
|
+
? policy.allowedAsset.toLowerCase()
|
|
1150
|
+
: policy.allowedAsset) ||
|
|
1151
|
+
BigInt(selected.amountAtomic) > BigInt(policy.maximumPaymentAtomic)
|
|
1152
|
+
) {
|
|
1153
|
+
throw new CliError(
|
|
1154
|
+
"The payment artifact selected a term outside the autonomous renewal policy.",
|
|
1155
|
+
{ exitCode: 6 },
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
await store.saveRenewalPaymentAttempt(serverId, walletPayment);
|
|
1159
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1160
|
+
const deadline = timeoutDeadline(
|
|
1161
|
+
integerOption(options, "timeout-seconds", 900),
|
|
1162
|
+
);
|
|
1163
|
+
while (true) {
|
|
1164
|
+
const response = await client.renewalCheckout(
|
|
1165
|
+
(await client.getServer(serverId, token)).data.task.planId,
|
|
1166
|
+
{
|
|
1167
|
+
bodyText: renewal.checkoutBody,
|
|
1168
|
+
token,
|
|
1169
|
+
paymentSignature: walletPayment.paymentSignature,
|
|
1170
|
+
},
|
|
1171
|
+
);
|
|
1172
|
+
const retryable =
|
|
1173
|
+
response.status === 202 &&
|
|
1174
|
+
["payment_pending", "payment_finalizing"].includes(response.data?.status);
|
|
1175
|
+
if (booleanOption(options, "wait") && retryable) {
|
|
1176
|
+
ensureBeforeDeadline(deadline, `Renewal ${serverId}`);
|
|
1177
|
+
await delay(suggestedDelay(response));
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const output = {
|
|
1181
|
+
status: response.data?.status,
|
|
1182
|
+
serverId,
|
|
1183
|
+
task: response.data?.task,
|
|
1184
|
+
paymentAttemptId:
|
|
1185
|
+
response.data?.paymentAttemptId ||
|
|
1186
|
+
response.headers["x-warpmetal-payment-attempt"],
|
|
1187
|
+
walletPayment: {
|
|
1188
|
+
attemptId: walletPayment.attemptId,
|
|
1189
|
+
wallet: walletPayment.wallet,
|
|
1190
|
+
payerAddress: walletPayment.payerAddress,
|
|
1191
|
+
artifactPath: walletPayment.path,
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
emit(
|
|
1195
|
+
context.stdout,
|
|
1196
|
+
output,
|
|
1197
|
+
context.json,
|
|
1198
|
+
`Renewal status for ${serverId}: ${output.status}`,
|
|
1199
|
+
);
|
|
1200
|
+
if (response.status === 409) return 6;
|
|
1201
|
+
if (response.status === 402) return 7;
|
|
1202
|
+
if (retryable) return 8;
|
|
1203
|
+
return 0;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async function handleRenewalDue(client, store, options, context) {
|
|
1208
|
+
const explicit = stringOption(options, "server");
|
|
1209
|
+
const all = booleanOption(options, "all");
|
|
1210
|
+
if (Boolean(explicit) === all) {
|
|
1211
|
+
throw new CliError("Use exactly one of --server or --all.", { exitCode: 2 });
|
|
1212
|
+
}
|
|
1213
|
+
const serverIds = explicit
|
|
1214
|
+
? [explicit]
|
|
1215
|
+
: (await store.summary()).renewals.map((renewal) => renewal.serverId);
|
|
1216
|
+
const results = [];
|
|
1217
|
+
for (const serverId of serverIds) {
|
|
1218
|
+
try {
|
|
1219
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1220
|
+
const result = await client.getRenewalPolicy(serverId, token);
|
|
1221
|
+
results.push({ serverId, ...result.data });
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
results.push({ serverId, error: toErrorMessage(error) });
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const due = results.filter(
|
|
1227
|
+
(result) =>
|
|
1228
|
+
result.policy?.nextAction && result.policy.nextAction !== "not_due",
|
|
1229
|
+
);
|
|
1230
|
+
emit(
|
|
1231
|
+
context.stdout,
|
|
1232
|
+
{ due, checked: results },
|
|
1233
|
+
context.json,
|
|
1234
|
+
due.length
|
|
1235
|
+
? due.map((result) => `${result.serverId}: ${result.policy.nextAction}`).join("\n")
|
|
1236
|
+
: "No locally configured server is due for renewal.",
|
|
1237
|
+
);
|
|
1238
|
+
return due.length ? 8 : 0;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
async function handleNotificationsConfigure(client, store, options, context) {
|
|
1242
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1243
|
+
const email = stringOption(options, "email", { required: true });
|
|
1244
|
+
const eventValue = stringOption(options, "events");
|
|
1245
|
+
const body = { email };
|
|
1246
|
+
if (eventValue) body.events = eventValue.split(",").map((value) => value.trim());
|
|
1247
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1248
|
+
const result = await client.putNotifications(serverId, body, token);
|
|
1249
|
+
emit(
|
|
1250
|
+
context.stdout,
|
|
1251
|
+
result.data,
|
|
1252
|
+
context.json,
|
|
1253
|
+
`Verification email queued for ${result.data.subscription.email}.`,
|
|
1254
|
+
);
|
|
1255
|
+
return 0;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
async function handleNotificationsStatus(client, store, options, context) {
|
|
1259
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1260
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1261
|
+
const result = await client.getNotifications(serverId, token);
|
|
1262
|
+
emit(
|
|
1263
|
+
context.stdout,
|
|
1264
|
+
result.data,
|
|
1265
|
+
context.json,
|
|
1266
|
+
result.data.configured
|
|
1267
|
+
? `${serverId}: notifications ${result.data.subscription.verified ? "verified" : "awaiting verification"}`
|
|
1268
|
+
: `${serverId}: notifications are not configured`,
|
|
1269
|
+
);
|
|
1270
|
+
return 0;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
653
1273
|
async function loginServer(client, store, serverId, identity) {
|
|
654
1274
|
const challenge = (await client.issueSshChallenge(serverId)).data;
|
|
655
1275
|
const signature = await signSshChallenge(challenge.payload, identity);
|
|
@@ -664,9 +1284,102 @@ async function loginServer(client, store, serverId, identity) {
|
|
|
664
1284
|
return { challenge, token };
|
|
665
1285
|
}
|
|
666
1286
|
|
|
1287
|
+
async function handleIdentityGenerate(store, options, context) {
|
|
1288
|
+
const hostname = stringOption(options, "hostname", { required: true });
|
|
1289
|
+
const selected = await generatedIdentity(store, hostname, options, context);
|
|
1290
|
+
emit(
|
|
1291
|
+
context.stdout,
|
|
1292
|
+
selected.identity,
|
|
1293
|
+
context.json,
|
|
1294
|
+
`Generated ${selected.identity.keyName} at ${selected.identity.privateKeyPath}; fingerprint ${selected.identity.sshFingerprint}.`,
|
|
1295
|
+
);
|
|
1296
|
+
return 0;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
async function handleIdentityList(store, context) {
|
|
1300
|
+
const identities = (await store.summary()).identities;
|
|
1301
|
+
emit(
|
|
1302
|
+
context.stdout,
|
|
1303
|
+
{ identities },
|
|
1304
|
+
context.json,
|
|
1305
|
+
identities.length
|
|
1306
|
+
? identities
|
|
1307
|
+
.map(
|
|
1308
|
+
(identity) =>
|
|
1309
|
+
`${identity.keyName}: ${identity.serverId || "unbound"} (${identity.sshFingerprint})`,
|
|
1310
|
+
)
|
|
1311
|
+
.join("\n")
|
|
1312
|
+
: "No WarpMetal SSH identities are stored.",
|
|
1313
|
+
);
|
|
1314
|
+
return 0;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async function handleServerIdentity(store, options, context) {
|
|
1318
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1319
|
+
const identity = await store.identityForServer(serverId);
|
|
1320
|
+
if (!identity) {
|
|
1321
|
+
throw new CliError(`No SSH identity is mapped to ${serverId}.`, {
|
|
1322
|
+
exitCode: 4,
|
|
1323
|
+
details: { code: "identity_required", serverId },
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
emit(
|
|
1327
|
+
context.stdout,
|
|
1328
|
+
{ identity },
|
|
1329
|
+
context.json,
|
|
1330
|
+
`${serverId} uses ${identity.keyName} (${identity.sshFingerprint}) at ${identity.privateKeyPath || "private key unavailable"}.`,
|
|
1331
|
+
);
|
|
1332
|
+
return 0;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async function handleServerIdentityAttach(client, store, options, context) {
|
|
1336
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1337
|
+
const privateKeyPath = resolve(
|
|
1338
|
+
stringOption(options, "identity", { required: true }),
|
|
1339
|
+
);
|
|
1340
|
+
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
1341
|
+
const publicKey = await readSshPublicKey(publicKeyPath);
|
|
1342
|
+
const fingerprint = sshFingerprint(publicKey);
|
|
1343
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1344
|
+
const server = (await client.getServer(serverId, token)).data?.task;
|
|
1345
|
+
if (!server || server.sshFingerprint !== fingerprint) {
|
|
1346
|
+
throw new CliError(
|
|
1347
|
+
"The local public key fingerprint does not match the server owner key.",
|
|
1348
|
+
{ exitCode: 4 },
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
const keyName = serverKeyName(
|
|
1352
|
+
server.hostname,
|
|
1353
|
+
stringOption(options, "ssh-key-name") || basename(privateKeyPath),
|
|
1354
|
+
).keyName;
|
|
1355
|
+
const identity = {
|
|
1356
|
+
identityId: identityId(),
|
|
1357
|
+
keyName,
|
|
1358
|
+
hostnameAtCreation: server.hostname,
|
|
1359
|
+
privateKeyPath,
|
|
1360
|
+
publicKeyPath,
|
|
1361
|
+
keyType: publicKey.split(/\s+/)[0],
|
|
1362
|
+
sshFingerprint: fingerprint,
|
|
1363
|
+
generated: false,
|
|
1364
|
+
};
|
|
1365
|
+
await store.saveIdentity(identity);
|
|
1366
|
+
await store.bindIdentity(identity.identityId, server.taskId || server.id, serverId);
|
|
1367
|
+
emit(
|
|
1368
|
+
context.stdout,
|
|
1369
|
+
{ identity: await store.identity(identity.identityId) },
|
|
1370
|
+
context.json,
|
|
1371
|
+
`Mapped ${keyName} to ${serverId}.`,
|
|
1372
|
+
);
|
|
1373
|
+
return 0;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
667
1376
|
async function handleServerLogin(client, store, options, context) {
|
|
668
1377
|
const serverId = stringOption(options, "server", { required: true });
|
|
669
|
-
const identity =
|
|
1378
|
+
const identity = await resolveServerIdentity(
|
|
1379
|
+
store,
|
|
1380
|
+
serverId,
|
|
1381
|
+
stringOption(options, "identity"),
|
|
1382
|
+
);
|
|
670
1383
|
const { challenge, token } = await loginServer(
|
|
671
1384
|
client,
|
|
672
1385
|
store,
|
|
@@ -741,8 +1454,16 @@ async function handleServerPower(client, store, options, context) {
|
|
|
741
1454
|
return result.data.operation.state === "manual_review" ? 6 : 0;
|
|
742
1455
|
}
|
|
743
1456
|
|
|
744
|
-
async function applyReloadResult(
|
|
1457
|
+
async function applyReloadResult(
|
|
1458
|
+
store,
|
|
1459
|
+
serverId,
|
|
1460
|
+
operation,
|
|
1461
|
+
pendingIdentityId,
|
|
1462
|
+
) {
|
|
745
1463
|
if (operation?.state !== "succeeded") return;
|
|
1464
|
+
if (pendingIdentityId) {
|
|
1465
|
+
await store.bindIdentity(pendingIdentityId, undefined, serverId);
|
|
1466
|
+
}
|
|
746
1467
|
await store.invalidateServerAccess(serverId);
|
|
747
1468
|
if (operation.result?.reloadImpact?.agentRuntimeAffected) {
|
|
748
1469
|
await store.saveRuntime(serverId, {
|
|
@@ -788,9 +1509,37 @@ async function handleServerReload(client, store, options, context) {
|
|
|
788
1509
|
const hostname = stringOption(options, "hostname");
|
|
789
1510
|
const osName = stringOption(options, "os");
|
|
790
1511
|
const publicKeyFile = stringOption(options, "ssh-public-key-file");
|
|
1512
|
+
const generateSshKey = booleanOption(options, "generate-ssh-key");
|
|
1513
|
+
if (publicKeyFile && generateSshKey) {
|
|
1514
|
+
throw new CliError(
|
|
1515
|
+
"Use at most one of --generate-ssh-key or --ssh-public-key-file during reload.",
|
|
1516
|
+
{ exitCode: 2 },
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
791
1519
|
if (hostname) body.hostname = hostname;
|
|
792
1520
|
if (osName) body.osName = osName;
|
|
793
|
-
|
|
1521
|
+
let replacementIdentity;
|
|
1522
|
+
if (generateSshKey) {
|
|
1523
|
+
const selected = await generatedIdentity(
|
|
1524
|
+
store,
|
|
1525
|
+
hostname || server.hostname,
|
|
1526
|
+
options,
|
|
1527
|
+
context,
|
|
1528
|
+
);
|
|
1529
|
+
replacementIdentity = selected.identity;
|
|
1530
|
+
body.sshPublicKey = selected.sshPublicKey;
|
|
1531
|
+
body.sshKeyLabel = replacementIdentity.keyName;
|
|
1532
|
+
} else if (publicKeyFile) {
|
|
1533
|
+
const selected = await existingIdentity(
|
|
1534
|
+
store,
|
|
1535
|
+
hostname || server.hostname,
|
|
1536
|
+
publicKeyFile,
|
|
1537
|
+
stringOption(options, "ssh-key-name"),
|
|
1538
|
+
);
|
|
1539
|
+
replacementIdentity = selected.identity;
|
|
1540
|
+
body.sshPublicKey = selected.sshPublicKey;
|
|
1541
|
+
body.sshKeyLabel = replacementIdentity.keyName;
|
|
1542
|
+
}
|
|
794
1543
|
|
|
795
1544
|
const key =
|
|
796
1545
|
stringOption(options, "idempotency-key") || idempotencyKey("reload");
|
|
@@ -799,7 +1548,9 @@ async function handleServerReload(client, store, options, context) {
|
|
|
799
1548
|
if (!operationId) {
|
|
800
1549
|
throw new CliError("WarpMetal did not return a reload operation ID.");
|
|
801
1550
|
}
|
|
802
|
-
await store.saveOperation(operationId, serverId, "reload"
|
|
1551
|
+
await store.saveOperation(operationId, serverId, "reload", {
|
|
1552
|
+
pendingIdentityId: replacementIdentity?.identityId,
|
|
1553
|
+
});
|
|
803
1554
|
if (booleanOption(options, "wait")) {
|
|
804
1555
|
result = await pollOperation(
|
|
805
1556
|
client,
|
|
@@ -810,7 +1561,12 @@ async function handleServerReload(client, store, options, context) {
|
|
|
810
1561
|
}
|
|
811
1562
|
|
|
812
1563
|
const operation = result.data?.operation;
|
|
813
|
-
await applyReloadResult(
|
|
1564
|
+
await applyReloadResult(
|
|
1565
|
+
store,
|
|
1566
|
+
serverId,
|
|
1567
|
+
operation,
|
|
1568
|
+
replacementIdentity?.identityId,
|
|
1569
|
+
);
|
|
814
1570
|
emit(
|
|
815
1571
|
context.stdout,
|
|
816
1572
|
result.data,
|
|
@@ -853,7 +1609,12 @@ async function handleOperationGet(client, store, options, context) {
|
|
|
853
1609
|
)
|
|
854
1610
|
: await client.getOperation(operationId, token);
|
|
855
1611
|
if (saved?.kind === "reload") {
|
|
856
|
-
await applyReloadResult(
|
|
1612
|
+
await applyReloadResult(
|
|
1613
|
+
store,
|
|
1614
|
+
serverId,
|
|
1615
|
+
result.data?.operation,
|
|
1616
|
+
saved.pendingIdentityId,
|
|
1617
|
+
);
|
|
857
1618
|
}
|
|
858
1619
|
emit(
|
|
859
1620
|
context.stdout,
|
|
@@ -904,7 +1665,11 @@ async function handleRuntimeGet(client, store, options, context) {
|
|
|
904
1665
|
|
|
905
1666
|
async function handleRuntimeInstall(client, store, options, context) {
|
|
906
1667
|
const serverId = stringOption(options, "server", { required: true });
|
|
907
|
-
const identity =
|
|
1668
|
+
const identity = await resolveServerIdentity(
|
|
1669
|
+
store,
|
|
1670
|
+
serverId,
|
|
1671
|
+
stringOption(options, "identity"),
|
|
1672
|
+
);
|
|
908
1673
|
const sshUser = stringOption(options, "ssh-user", { required: true });
|
|
909
1674
|
if (stringOption(options, "confirm", { required: true }) !== "INSTALL") {
|
|
910
1675
|
throw new CliError(
|
|
@@ -1487,6 +2252,8 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
1487
2252
|
"hostname",
|
|
1488
2253
|
"os",
|
|
1489
2254
|
"ssh-public-key-file",
|
|
2255
|
+
"generate-ssh-key",
|
|
2256
|
+
"ssh-key-name",
|
|
1490
2257
|
"email",
|
|
1491
2258
|
"runtime-file",
|
|
1492
2259
|
"confirm",
|
|
@@ -1521,6 +2288,104 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
1521
2288
|
"timeout-seconds",
|
|
1522
2289
|
]);
|
|
1523
2290
|
return handleCheckoutSubmit(client, store, options, context);
|
|
2291
|
+
case "identity generate":
|
|
2292
|
+
rejectUnknownOptions(options, [
|
|
2293
|
+
...COMMON_OPTIONS,
|
|
2294
|
+
"hostname",
|
|
2295
|
+
"ssh-key-name",
|
|
2296
|
+
]);
|
|
2297
|
+
return handleIdentityGenerate(store, options, context);
|
|
2298
|
+
case "identity list":
|
|
2299
|
+
rejectUnknownOptions(options, COMMON_OPTIONS);
|
|
2300
|
+
return handleIdentityList(store, context);
|
|
2301
|
+
case "server identity":
|
|
2302
|
+
rejectUnknownOptions(options, [...COMMON_OPTIONS, "server"]);
|
|
2303
|
+
return handleServerIdentity(store, options, context);
|
|
2304
|
+
case "server identity attach":
|
|
2305
|
+
rejectUnknownOptions(options, [
|
|
2306
|
+
...COMMON_OPTIONS,
|
|
2307
|
+
"server",
|
|
2308
|
+
"identity",
|
|
2309
|
+
"ssh-key-name",
|
|
2310
|
+
"token-file",
|
|
2311
|
+
]);
|
|
2312
|
+
return handleServerIdentityAttach(client, store, options, context);
|
|
2313
|
+
case "renewal configure":
|
|
2314
|
+
rejectUnknownOptions(options, [
|
|
2315
|
+
...COMMON_OPTIONS,
|
|
2316
|
+
"server",
|
|
2317
|
+
"token-file",
|
|
2318
|
+
"wallet",
|
|
2319
|
+
"renew-before-days",
|
|
2320
|
+
"maximum-payment-atomic",
|
|
2321
|
+
"maximum-renewals",
|
|
2322
|
+
"renew-through",
|
|
2323
|
+
"maximum-total-spend-atomic",
|
|
2324
|
+
"allowed-network",
|
|
2325
|
+
"allowed-asset",
|
|
2326
|
+
"refill-target-atomic",
|
|
2327
|
+
"email",
|
|
2328
|
+
"disabled",
|
|
2329
|
+
]);
|
|
2330
|
+
return handleRenewalConfigure(client, store, options, context);
|
|
2331
|
+
case "renewal status":
|
|
2332
|
+
rejectUnknownOptions(options, [
|
|
2333
|
+
...COMMON_OPTIONS,
|
|
2334
|
+
"server",
|
|
2335
|
+
"token-file",
|
|
2336
|
+
]);
|
|
2337
|
+
return handleRenewalStatus(client, store, options, context);
|
|
2338
|
+
case "renewal due":
|
|
2339
|
+
rejectUnknownOptions(options, [
|
|
2340
|
+
...COMMON_OPTIONS,
|
|
2341
|
+
"server",
|
|
2342
|
+
"all",
|
|
2343
|
+
"token-file",
|
|
2344
|
+
]);
|
|
2345
|
+
return handleRenewalDue(client, store, options, context);
|
|
2346
|
+
case "renewal prepare":
|
|
2347
|
+
rejectUnknownOptions(options, [
|
|
2348
|
+
...COMMON_OPTIONS,
|
|
2349
|
+
"server",
|
|
2350
|
+
"token-file",
|
|
2351
|
+
"request-envelope-out",
|
|
2352
|
+
]);
|
|
2353
|
+
return handleRenewalPrepare(client, store, options, context);
|
|
2354
|
+
case "renewal submit":
|
|
2355
|
+
rejectUnknownOptions(options, [
|
|
2356
|
+
...COMMON_OPTIONS,
|
|
2357
|
+
"server",
|
|
2358
|
+
"token-file",
|
|
2359
|
+
"payment-artifact",
|
|
2360
|
+
"wait",
|
|
2361
|
+
"timeout-seconds",
|
|
2362
|
+
]);
|
|
2363
|
+
return handleRenewalSubmit(client, store, options, context);
|
|
2364
|
+
case "renewal run":
|
|
2365
|
+
rejectUnknownOptions(options, [
|
|
2366
|
+
...COMMON_OPTIONS,
|
|
2367
|
+
"server",
|
|
2368
|
+
"all-due",
|
|
2369
|
+
"token-file",
|
|
2370
|
+
"request-envelope-out",
|
|
2371
|
+
]);
|
|
2372
|
+
return handleRenewalRun(client, store, options, context);
|
|
2373
|
+
case "notifications configure":
|
|
2374
|
+
rejectUnknownOptions(options, [
|
|
2375
|
+
...COMMON_OPTIONS,
|
|
2376
|
+
"server",
|
|
2377
|
+
"token-file",
|
|
2378
|
+
"email",
|
|
2379
|
+
"events",
|
|
2380
|
+
]);
|
|
2381
|
+
return handleNotificationsConfigure(client, store, options, context);
|
|
2382
|
+
case "notifications status":
|
|
2383
|
+
rejectUnknownOptions(options, [
|
|
2384
|
+
...COMMON_OPTIONS,
|
|
2385
|
+
"server",
|
|
2386
|
+
"token-file",
|
|
2387
|
+
]);
|
|
2388
|
+
return handleNotificationsStatus(client, store, options, context);
|
|
1524
2389
|
case "server login":
|
|
1525
2390
|
rejectUnknownOptions(options, [...COMMON_OPTIONS, "server", "identity"]);
|
|
1526
2391
|
return handleServerLogin(client, store, options, context);
|
|
@@ -1554,6 +2419,8 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
1554
2419
|
"hostname",
|
|
1555
2420
|
"os",
|
|
1556
2421
|
"ssh-public-key-file",
|
|
2422
|
+
"generate-ssh-key",
|
|
2423
|
+
"ssh-key-name",
|
|
1557
2424
|
"idempotency-key",
|
|
1558
2425
|
"wait",
|
|
1559
2426
|
"timeout-seconds",
|
|
@@ -1728,7 +2595,7 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
1728
2595
|
context.stdout,
|
|
1729
2596
|
summary,
|
|
1730
2597
|
context.json,
|
|
1731
|
-
`State: ${summary.stateFile}\nOrders: ${summary.orders.length}\nServers: ${summary.servers.length}\nOperations: ${summary.operations.length}\nRuntimes: ${summary.runtimes.length}\nSandboxes: ${summary.sandboxes.length}\nAccess grants: ${summary.accessGrants.length}`,
|
|
2598
|
+
`State: ${summary.stateFile}\nOrders: ${summary.orders.length}\nServers: ${summary.servers.length}\nSSH identities: ${summary.identities.length}\nRenewal policies: ${summary.renewals.length}\nOperations: ${summary.operations.length}\nRuntimes: ${summary.runtimes.length}\nSandboxes: ${summary.sandboxes.length}\nAccess grants: ${summary.accessGrants.length}`,
|
|
1732
2599
|
);
|
|
1733
2600
|
return 0;
|
|
1734
2601
|
}
|