turnzero 0.43.0 → 0.45.0-next.1
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 +3 -2
- package/dist/turnzero-cloud.mjs +638 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ Build with AI. Turn Zero Cloud runs your backend—with hosting, PostgreSQL, fil
|
|
|
4
4
|
|
|
5
5
|
## Get started
|
|
6
6
|
|
|
7
|
-
Paste
|
|
7
|
+
Paste the setup prompt into your AI coding tool.
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
10
|
Set me up with Turn Zero Cloud and a small demo app. Read https://turnzero.ai/cloud/getting-started/set-up/index.md in full and follow the steps.
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Follow the setup steps for your AI coding tool to connect it to Turn Zero, sign in when asked, and paste the build prompt to build and deploy a small demo app.
|
|
14
14
|
|
|
15
15
|
[Read the setup guide](https://turnzero.ai/cloud/getting-started/set-up/) · [Explore Turn Zero Cloud](https://turnzero.ai/cloud/)
|
|
16
16
|
|
|
@@ -24,6 +24,7 @@ To install it yourself, run `npm install -g turnzero`. It needs Node.js 24 or la
|
|
|
24
24
|
|
|
25
25
|
- `deploy` zips your application, uploads it, deploys it, and waits for the result.
|
|
26
26
|
- `check` tells you whether a deploy would be refused, without uploading anything.
|
|
27
|
+
- `promote` promotes the version development serves, or a deployed version you name, to production, and prints the result.
|
|
27
28
|
- `hash` prints the hash of the zip a deploy would upload.
|
|
28
29
|
- `secret store` and `secret rotate` store a secret under a name, or replace its value.
|
|
29
30
|
- `put` uploads one file to a storage area.
|
package/dist/turnzero-cloud.mjs
CHANGED
|
@@ -53,7 +53,9 @@ var COMMAND_ACTION_SURFACE = Object.freeze({
|
|
|
53
53
|
create_environment: "tzcloud",
|
|
54
54
|
deploy: "tzcloud",
|
|
55
55
|
deploy_files: "tzcloud",
|
|
56
|
+
list_applications: "tzcloud",
|
|
56
57
|
list_secrets: "tzcloud",
|
|
58
|
+
list_versions: "tzcloud",
|
|
57
59
|
mint_upload_grant: "tzcloud",
|
|
58
60
|
promote: "tzcloud",
|
|
59
61
|
read_export: "tzcloud",
|
|
@@ -121,6 +123,12 @@ var CHECK_ACTION = "check_deploy";
|
|
|
121
123
|
var HASH_SUBCOMMAND = "hash";
|
|
122
124
|
var DEPLOY_OPTION = Object.freeze({ env: "--env", upload: "--upload", path: "--path", origin: "--origin", application: "--application" });
|
|
123
125
|
var CHECK_OPTION = { application: "--application", path: "--path", origin: "--origin" };
|
|
126
|
+
var PROMOTE_SUBCOMMAND = "promote";
|
|
127
|
+
var PROMOTE_OPTION = {
|
|
128
|
+
application: "--application",
|
|
129
|
+
version: "--version-number",
|
|
130
|
+
origin: "--origin"
|
|
131
|
+
};
|
|
124
132
|
var HASH_OPTION = Object.freeze({ path: DEPLOY_OPTION.path, list: "--list", origin: DEPLOY_OPTION.origin });
|
|
125
133
|
var HASH_LINE_HEAD = "sha256: ";
|
|
126
134
|
var LINE_PROGRAM = TURNZERO_CLOUD_PACKAGE;
|
|
@@ -352,7 +360,9 @@ var TZMESH_OPTION = {
|
|
|
352
360
|
description: "--description",
|
|
353
361
|
title: "--title",
|
|
354
362
|
join: "--join",
|
|
355
|
-
public: "--public"
|
|
363
|
+
public: "--public",
|
|
364
|
+
// A standing agent's key rotation (the tzmesh rotate plan's decision 1).
|
|
365
|
+
rotate: "--rotate"
|
|
356
366
|
};
|
|
357
367
|
var TZMESH_ESTATES = Object.freeze(["production", "development"]);
|
|
358
368
|
var ESTATE_ORIGINS = Object.freeze({
|
|
@@ -377,6 +387,13 @@ var TZMESH_REGISTER_PATH = "/v1/register";
|
|
|
377
387
|
var TZMESH_DELETE_AGENT_PATH = "/v1/delete_agent";
|
|
378
388
|
var TZMESH_WEBHOOK_SET_PATH = "/v1/set_webhook";
|
|
379
389
|
var TZMESH_JOIN_PATH = "/v1/join_channel";
|
|
390
|
+
var TZMESH_LIST_PARTICIPANTS_PATH = "/v1/list_participants";
|
|
391
|
+
var TZMESH_REMOVE_LISTENERS_PATH = "/v1/remove_listeners";
|
|
392
|
+
var TZMESH_LABEL_MAX = 80;
|
|
393
|
+
var tzmeshListenerLabelOf = (destination) => {
|
|
394
|
+
const where = "secret" in destination ? `secret ${destination.secret} ${destination.application === null ? "at the account" : `at ${destination.application} ${destination.environment ?? "production"}`}` : `file ${destination.toFile.split(/[\\/]/).pop() ?? destination.toFile}`;
|
|
395
|
+
return [...`${TZMESH_COMMAND_LABEL}: ${where}`].slice(0, TZMESH_LABEL_MAX).join("");
|
|
396
|
+
};
|
|
380
397
|
var TZMESH_WEBHOOK_ROUTE = "/tzmesh/webhook";
|
|
381
398
|
var TZMESH_WEBHOOK_SECRET_PREFIX = "turnzero_tzmesh_whk_";
|
|
382
399
|
var TZMESH_WEBHOOK_SECRET_FORM = new RegExp(`^${TZMESH_WEBHOOK_SECRET_PREFIX}[A-Za-z0-9_-]{43}$`);
|
|
@@ -392,6 +409,7 @@ var tzmeshMintMembers = (challenge) => [
|
|
|
392
409
|
];
|
|
393
410
|
var tzmeshRegisterOptionsOf = (call) => [
|
|
394
411
|
`${TZMESH_OPTION.name} ${call.name}`,
|
|
412
|
+
...call.rotate === true ? [TZMESH_OPTION.rotate] : [],
|
|
395
413
|
call.secret !== void 0 ? `${TZMESH_OPTION.secret} ${call.secret}` : `${TZMESH_OPTION.toFile} "${call.toFile ?? ""}"`,
|
|
396
414
|
...call.application !== void 0 ? [`${TZMESH_OPTION.application} ${call.application}`] : [],
|
|
397
415
|
...call.environment !== void 0 ? [`${TZMESH_OPTION.environment} ${call.environment}`] : [],
|
|
@@ -1064,7 +1082,8 @@ var variableReadSentence = (origin, standing) => {
|
|
|
1064
1082
|
}
|
|
1065
1083
|
};
|
|
1066
1084
|
var fileReadSentence = (origin) => `The credential this run read was this computer's token for ${origin}, from its credential file: where the platform no longer accepts it, run ${LOGIN_SUBCOMMAND}${originWordsOf(origin)} to authorize this computer again.`;
|
|
1067
|
-
var
|
|
1085
|
+
var NO_COMPUTER_SIGN_IN_SENTENCE = "Signing in to a Turn Zero connection authorizes no computer.";
|
|
1086
|
+
var noTokenSentence = (origin, expiredAt) => expiredAt === null ? `No credential was read, so nothing was sent: ${MINTED_TOKEN_VARIABLE} is not set, and this computer's credential file holds no token for ${origin}. ${NO_COMPUTER_SIGN_IN_SENTENCE}` : `No credential was read, so nothing was sent: ${MINTED_TOKEN_VARIABLE} is not set, and this computer's token for ${origin} in its credential file expired at ${expiredAt}. ${NO_COMPUTER_SIGN_IN_SENTENCE}`;
|
|
1068
1087
|
var VARIABLE_ROUTE_SENTENCES = `A tool connected by a minted token, or a job, sets ${MINTED_TOKEN_VARIABLE} from where its token is kept, never by writing the value in a command. It sets ${ORIGIN_VARIABLE} to the address that minted the token if that is not ${DEFAULT_COMMAND_ORIGIN}, then runs the command again. Such a tool cannot use ${MINT_TOKEN_ACTION}, which works only under a sign-in. A tool sets ${MINTED_TOKEN_VARIABLE} for one command at a time and unsets it once that command ends.`;
|
|
1069
1088
|
var authorizeRouteSentence = (origin) => `${AUTHORIZE_SUBCOMMAND}${originWordsOf(origin)} run with nothing piped prints the ${MINT_TOKEN_ACTION} call a signed-in tool makes to authorize this computer.`;
|
|
1070
1089
|
var LOGIN_ROUTE_OPENER = "A person signs in on this computer once, in a terminal of their own, and its token then stays on this computer for 30 days:";
|
|
@@ -1187,20 +1206,23 @@ var firstActNamed = (kind, origin = DEFAULT_COMMAND_ORIGIN) => kind === "tzmesh"
|
|
|
1187
1206
|
) : `${kind === "authorized computer" ? AUTHORIZE_SUBCOMMAND : `${TOKEN_SUBCOMMAND} ${TOKEN_REQUEST_ACT}`}${originWordsOf(origin)}`;
|
|
1188
1207
|
var noConnectionLine = (origin) => `Where your tool holds no connection signed in to ${origin}, ${MINT_TOKEN_ACTION} is not among its tools: a person connects the tool and signs in, in a browser, before the call.`;
|
|
1189
1208
|
var loginWaySentence = (loginLines) => `A person can instead sign in on this computer themselves, in a terminal of their own, by running ${loginLines.join(", then ")}, and approving the sign-in in the browser.`;
|
|
1190
|
-
var AUTHORIZE_HAND_OVER_SENTENCE =
|
|
1209
|
+
var AUTHORIZE_HAND_OVER_SENTENCE = `If the host refuses ${MINT_TOKEN_ACTION} or won't run its returned line, use a deploy route that needs no turnzero command. Use tzcloud_deploy_files within its caps or, past them where your tool can zip and upload the folder, use tzcloud_mint_upload_grant followed by tzcloud_deploy naming the uploaded zip as artifact. Tell the person which route you take and why. If neither route fits, ask the person to run the login command above in their own terminal.`;
|
|
1191
1210
|
var TZMESH_HAND_OVER_SENTENCE = "If the answered line can't run here, give the person that exact line to run once in their own terminal, never a rewritten one.";
|
|
1192
1211
|
var OPTIONS_LINE_HEAD = "options: ";
|
|
1193
|
-
var tzmeshFirstActLines = (origin, pending, completing2) =>
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1212
|
+
var tzmeshFirstActLines = (origin, pending, completing2) => {
|
|
1213
|
+
const rotating = completing2.split(" ").includes(TZMESH_OPTION.rotate);
|
|
1214
|
+
return [
|
|
1215
|
+
`${rotating ? "To add a new key to this tzMesh agent" : "To register a tzMesh agent"}, call ${MINT_TOKEN_ACTION} at ${origin} from your tool, through the connection you signed in with, naming the members below and no others. Then run the line it answers once, on this computer, with the options below after it.`,
|
|
1216
|
+
noConnectionLine(origin),
|
|
1217
|
+
`${CHALLENGE_LINE_HEAD}${pending.challenge}`,
|
|
1218
|
+
CALL_LINE_HEAD,
|
|
1219
|
+
...mintCallMembers("tzmesh", pending.challenge, TZMESH_COMMAND_LABEL).map(([name, value]) => ` ${name}: ${JSON.stringify(value)}`),
|
|
1220
|
+
`${OPTIONS_LINE_HEAD}${completing2}`,
|
|
1221
|
+
rotating ? `The line it answers holds no code. The run presents this computer's verifier, uses the minted token to add the new key, saves the new key's secret where the options say, and revokes the minted token. No line prints the secret.` : `The line it answers holds no code. The run presents this computer's verifier, registers the agent with the token it mints, keeps the agent's secret where the options say, and revokes the token. No line prints the secret.`,
|
|
1222
|
+
`The request is pending until ${pending.expires_at}: ${firstActNamed("tzmesh", origin)} run again with the same options before then prints this challenge again.`,
|
|
1223
|
+
TZMESH_HAND_OVER_SENTENCE
|
|
1224
|
+
];
|
|
1225
|
+
};
|
|
1204
1226
|
var firstActLines = (origin, kind, pending, label, loginLines = [], completing2 = "") => kind === "tzmesh" ? tzmeshFirstActLines(origin, pending, completing2) : [
|
|
1205
1227
|
kind === "authorized computer" ? `To authorize this computer at ${origin}, call ${MINT_TOKEN_ACTION} from your tool, through the connection you signed in with, naming the members below and no others. Then run the line it answers once, as given, on this computer.` : `To mint a token whose value never passes through the chat, call ${MINT_TOKEN_ACTION} at ${origin} from your tool, through the connection you signed in with. Name the member below beside the scope_kind, application, grants, label, and expires_in_days the task needs.`,
|
|
1206
1228
|
// The case this command cannot see (#4273): a tool with no signed-in
|
|
@@ -1423,6 +1445,14 @@ async function completeAct(io2, origin, kind, presentation, userAgent, say, deli
|
|
|
1423
1445
|
let waiting = false;
|
|
1424
1446
|
const release = io2.whenEnded(() => {
|
|
1425
1447
|
stage.ended = true;
|
|
1448
|
+
const settling = carried?.settle?.(stage.now) ?? null;
|
|
1449
|
+
if (settling !== null) {
|
|
1450
|
+
return settling.then((line) => {
|
|
1451
|
+
say(line);
|
|
1452
|
+
dropCredentialWriting(io2, place, tag);
|
|
1453
|
+
carried?.drop();
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1426
1456
|
say(
|
|
1427
1457
|
carried !== null ? carried.endedLine(stage.now) : device !== null && waiting ? device.stoppedLine : stage.now === "unsent" ? `The run was ended before its exchange. ${unread}` : stage.now === "sent" ? `The run was ended during its exchange. ${exchangeUnknownSentence(kind, origin, login2)}` : `The run was ended after its exchange minted a token, which may not have been ${kind === "authorized computer" ? "kept" : "printed"}: list_tokens from your tool shows it by its label, and revoke_token ends it.`
|
|
1428
1458
|
);
|
|
@@ -6256,7 +6286,7 @@ var LIBRARY_CREDENTIAL = Object.freeze({
|
|
|
6256
6286
|
var LIBRARY_READS = `no standard input. It reads a minted token from ${MINTED_TOKEN_VARIABLE} if that is set. Otherwise it reads this computer's token for the platform's address from its credential file, which ${LOGIN_SUBCOMMAND} and ${AUTHORIZE_SUBCOMMAND} write, and sends it only to that address. With neither, it prints the variable route first, then the sign-in route if the address is admitted`;
|
|
6257
6287
|
var TAKE_AFTER_LOGIN = `Once ${LOGIN_SUBCOMMAND} has printed authorized:, run this take line again. ${NOTHING_WRITTEN2}`;
|
|
6258
6288
|
var takeRedirectedLine = (status) => `${LIBRARY_READ_ACTION} was answered ${status >= 100 ? `${answeredHow(status)}, a redirect` : answeredHow(status)}, which the command never follows, so the credential was sent to no second address. ${NOTHING_WRITTEN2}`;
|
|
6259
|
-
async function readTakeCredential(io2, named, afterLogin, why) {
|
|
6289
|
+
async function readTakeCredential(io2, named, afterLogin, why, refused) {
|
|
6260
6290
|
const line = { named, closing: NOTHING_WRITTEN2 };
|
|
6261
6291
|
const read2 = await readCredential(io2, LIBRARY_CREDENTIAL, "", line);
|
|
6262
6292
|
if ("unauthorized" in read2) {
|
|
@@ -6266,7 +6296,7 @@ async function readTakeCredential(io2, named, afterLogin, why) {
|
|
|
6266
6296
|
const unwritable = loginRouteRefusalOf(io2, NOTHING_WRITTEN2);
|
|
6267
6297
|
if (unwritable !== null) return { lines: [...head, VARIABLE_ROUTE_SENTENCES, unwritable] };
|
|
6268
6298
|
const route = loginRouteLines(io2, origin.origin, false);
|
|
6269
|
-
return { lines: [...head, VARIABLE_ROUTE_SENTENCES, authorizeRouteSentence(origin.origin), ...route, afterLogin] };
|
|
6299
|
+
return { lines: [...head, VARIABLE_ROUTE_SENTENCES, authorizeRouteSentence(origin.origin), ...refused !== void 0 ? [refused] : [], ...route, afterLogin] };
|
|
6270
6300
|
}
|
|
6271
6301
|
if ("refusal" in read2) return { lines: [read2.refusal] };
|
|
6272
6302
|
io2.hide(read2.credential, REDACTED_TOKEN);
|
|
@@ -7565,6 +7595,164 @@ var startUpgradeCheck = (io2, subcommand, named, running) => {
|
|
|
7565
7595
|
};
|
|
7566
7596
|
};
|
|
7567
7597
|
|
|
7598
|
+
// src/turnzero_cloud_command/promote.ts
|
|
7599
|
+
var NOTHING_SENT_OR_PROMOTED = "Nothing was sent and nothing was promoted.";
|
|
7600
|
+
var PROMOTE_ACT = "promote";
|
|
7601
|
+
var PROMOTE_WAIT_SECONDS = 45;
|
|
7602
|
+
var VERSION_FORM3 = /^[1-9][0-9]{0,8}$/;
|
|
7603
|
+
var promoteOptionsOf = (named) => {
|
|
7604
|
+
const application = named[PROMOTE_OPTION.application];
|
|
7605
|
+
if (application === void 0 || !UPLOAD_ID.test(application)) {
|
|
7606
|
+
return { refusal: `${PROMOTE_OPTION.application} gives the application to promote, by its id, a lowercase UUID, as ${toolNameOf("list_applications")} returns it. ${NOTHING_SENT_OR_PROMOTED}` };
|
|
7607
|
+
}
|
|
7608
|
+
const version = named[PROMOTE_OPTION.version];
|
|
7609
|
+
if (version !== void 0 && !VERSION_FORM3.test(version)) {
|
|
7610
|
+
return { refusal: `${PROMOTE_OPTION.version} gives a version of the application's history by its number, as ${toolNameOf("list_versions")} returns it: a whole number from 1, at most nine digits, with no leading zero. ${NOTHING_SENT_OR_PROMOTED}` };
|
|
7611
|
+
}
|
|
7612
|
+
const origin = commandOriginOf(named[PROMOTE_OPTION.origin] ?? DEFAULT_COMMAND_ORIGIN);
|
|
7613
|
+
if ("refusal" in origin) return origin;
|
|
7614
|
+
return version === void 0 ? { application } : { application, version: Number(version) };
|
|
7615
|
+
};
|
|
7616
|
+
var PROMOTE_OPTIONS = Object.freeze([
|
|
7617
|
+
{
|
|
7618
|
+
name: PROMOTE_OPTION.application,
|
|
7619
|
+
value: "<application id>",
|
|
7620
|
+
required: true,
|
|
7621
|
+
description: `The application to promote, by its id, a lowercase UUID, as ${toolNameOf("list_applications")} returns it.`
|
|
7622
|
+
},
|
|
7623
|
+
{
|
|
7624
|
+
name: PROMOTE_OPTION.version,
|
|
7625
|
+
value: "<number>",
|
|
7626
|
+
required: false,
|
|
7627
|
+
description: `The number of a version in the application's history to promote, as ${toolNameOf("list_versions")} returns it: a whole number from 1, at most nine digits, with no leading zero. If omitted, the version the development environment serves.`
|
|
7628
|
+
},
|
|
7629
|
+
{
|
|
7630
|
+
name: PROMOTE_OPTION.origin,
|
|
7631
|
+
value: "<origin>",
|
|
7632
|
+
required: false,
|
|
7633
|
+
description: `The platform's address. If omitted, the address in ${ORIGIN_VARIABLE}, or ${DEFAULT_COMMAND_ORIGIN} if that is unset. Under ${MINTED_TOKEN_VARIABLE} the token is sent only to the address in ${ORIGIN_VARIABLE}, or to ${DEFAULT_COMMAND_ORIGIN} if that is unset. Otherwise it gives the address whose token the command reads from this computer's credential file, and that token is sent only to that address.`
|
|
7634
|
+
}
|
|
7635
|
+
]);
|
|
7636
|
+
var PROMOTE_CREDENTIAL = Object.freeze({
|
|
7637
|
+
grant: "minted token",
|
|
7638
|
+
aGrant: "a minted token",
|
|
7639
|
+
call: `the ${toolNameOf("promote")} call`,
|
|
7640
|
+
mintedToken: true,
|
|
7641
|
+
piped: false,
|
|
7642
|
+
places: NO_INPUT_PLACES
|
|
7643
|
+
});
|
|
7644
|
+
var PROMOTE_READS = `no standard input. It reads a minted token from ${MINTED_TOKEN_VARIABLE} if that is set. Otherwise it reads this computer's token for the platform's address from its credential file, which ${LOGIN_SUBCOMMAND} and ${AUTHORIZE_SUBCOMMAND} write, and sends it only to that address. With neither, it prints the variable route first, then the sign-in route if the address is admitted`;
|
|
7645
|
+
var PROMOTE_AFTER_LOGIN = `Once login has printed authorized:, run this promote line again. ${NOTHING_SENT_OR_PROMOTED}`;
|
|
7646
|
+
var NEXT_LINE_HEAD = "next: ";
|
|
7647
|
+
var nextLineOf = (application, waitSeconds) => `${NEXT_LINE_HEAD}call ${toolNameOf("read_status")} with application ${application}, environment production, and wait_seconds ${waitSeconds}, to read the promote's end.`;
|
|
7648
|
+
var unknownStartLineOf = (application) => `Whether the promote started is unknown: call ${toolNameOf("read_status")} with application ${application} and environment production, and run this line again only if that read shows no promote started after this call.`;
|
|
7649
|
+
var ANSWER_STATES = Object.freeze(["deployed", "failed", "deploying"]);
|
|
7650
|
+
var promoteAnswerOf = (answer) => {
|
|
7651
|
+
const { state, summary, detail, next } = answer;
|
|
7652
|
+
if (typeof state !== "string" || !ANSWER_STATES.includes(state)) return null;
|
|
7653
|
+
let nextWait = null;
|
|
7654
|
+
if (next !== null && typeof next === "object" && !Array.isArray(next)) {
|
|
7655
|
+
const { action, arguments: args } = next;
|
|
7656
|
+
const wait = args !== null && typeof args === "object" ? args.wait_seconds : void 0;
|
|
7657
|
+
if (action === "read_status" && typeof wait === "number" && Number.isInteger(wait) && wait > 0) nextWait = wait;
|
|
7658
|
+
}
|
|
7659
|
+
return { state, summary: typeof summary === "string" ? summary : null, detail: typeof detail === "string" ? detail : null, nextWait };
|
|
7660
|
+
};
|
|
7661
|
+
async function promote(named, io2) {
|
|
7662
|
+
const options = promoteOptionsOf(named);
|
|
7663
|
+
if ("refusal" in options) {
|
|
7664
|
+
io2.print(options.refusal);
|
|
7665
|
+
return COMMAND_STATUS.nothing_started;
|
|
7666
|
+
}
|
|
7667
|
+
const agent = userAgentOf(io2.env);
|
|
7668
|
+
if ("refusal" in agent) {
|
|
7669
|
+
io2.print(agent.refusal);
|
|
7670
|
+
return COMMAND_STATUS.nothing_started;
|
|
7671
|
+
}
|
|
7672
|
+
const line = { named: named[PROMOTE_OPTION.origin], closing: NOTHING_SENT_OR_PROMOTED };
|
|
7673
|
+
const credential = await readCredential(io2, PROMOTE_CREDENTIAL, "", line);
|
|
7674
|
+
if ("unauthorized" in credential) {
|
|
7675
|
+
io2.print(noTokenSentence(credential.unauthorized.origin, credential.unauthorized.expiredAt));
|
|
7676
|
+
io2.print(VARIABLE_ROUTE_SENTENCES);
|
|
7677
|
+
const reached = authorizationOriginOf(io2.env, named[PROMOTE_OPTION.origin], NOTHING_SENT_OR_PROMOTED);
|
|
7678
|
+
if ("refusal" in reached) {
|
|
7679
|
+
io2.print(reached.refusal);
|
|
7680
|
+
return COMMAND_STATUS.nothing_started;
|
|
7681
|
+
}
|
|
7682
|
+
const unwritable = loginRouteRefusalOf(io2, NOTHING_SENT_OR_PROMOTED);
|
|
7683
|
+
if (unwritable !== null) {
|
|
7684
|
+
io2.print(unwritable);
|
|
7685
|
+
return COMMAND_STATUS.nothing_started;
|
|
7686
|
+
}
|
|
7687
|
+
for (const routeLine of loginRouteLines(io2, reached.origin)) io2.print(routeLine);
|
|
7688
|
+
io2.print(PROMOTE_AFTER_LOGIN);
|
|
7689
|
+
return COMMAND_STATUS.nothing_started;
|
|
7690
|
+
}
|
|
7691
|
+
if ("refusal" in credential) {
|
|
7692
|
+
io2.print(credential.refusal);
|
|
7693
|
+
return COMMAND_STATUS.nothing_started;
|
|
7694
|
+
}
|
|
7695
|
+
io2.hide(credential.credential, REDACTED_TOKEN);
|
|
7696
|
+
const sent = credentialOriginOf(io2.env, credential, line);
|
|
7697
|
+
if ("refusal" in sent) {
|
|
7698
|
+
io2.print(sent.refusal);
|
|
7699
|
+
return COMMAND_STATUS.nothing_started;
|
|
7700
|
+
}
|
|
7701
|
+
const origin = sent.origin;
|
|
7702
|
+
const older = await planeWireRefusal(io2, origin);
|
|
7703
|
+
if (older !== null) {
|
|
7704
|
+
io2.print(older);
|
|
7705
|
+
return COMMAND_STATUS.nothing_started;
|
|
7706
|
+
}
|
|
7707
|
+
const unknown = (lines) => {
|
|
7708
|
+
for (const printed of lines) io2.print(printed);
|
|
7709
|
+
io2.print(unknownStartLineOf(options.application));
|
|
7710
|
+
return COMMAND_STATUS.unknown;
|
|
7711
|
+
};
|
|
7712
|
+
const body = { application: options.application, ...options.version !== void 0 ? { version: options.version } : {}, wait_seconds: PROMOTE_WAIT_SECONDS };
|
|
7713
|
+
const answered = await postAction(io2, origin, "promote", credential.credential, agent.userAgent, body);
|
|
7714
|
+
if (answered === null) return unknown([`The ${PROMOTE_ACT} call drew no answer.`]);
|
|
7715
|
+
if (answered.redirected) return unknown([`The ${PROMOTE_ACT} call was answered ${answeredHow(answered.status)}, which the command never follows, so the credential was sent to no second address.`]);
|
|
7716
|
+
if (answered.status >= PLATFORM_FAILURE_STATUS) return unknown(failureLines(PROMOTE_ACT, answered.status, planeRefusalOf(answered.text)));
|
|
7717
|
+
if (answered.status < 200 || answered.status > 299) {
|
|
7718
|
+
const refusal = markedRefusalOf(answered.status, answered.text);
|
|
7719
|
+
if (refusal === null) return unknown([notThePlatformLine(PROMOTE_ACT, answered.status)]);
|
|
7720
|
+
for (const printed of refusalLines(PROMOTE_ACT, answered.status, refusal)) io2.print(printed);
|
|
7721
|
+
io2.print(credential.source === "credential_file" ? fileReadSentence(origin) : variableReadSentence(origin, fileStandingOf(io2, origin, io2.print)));
|
|
7722
|
+
return COMMAND_STATUS.nothing_started;
|
|
7723
|
+
}
|
|
7724
|
+
const object = planeObjectOf(answered.text);
|
|
7725
|
+
const answer = object === null ? null : promoteAnswerOf(object);
|
|
7726
|
+
if (answer === null) return unknown([`The ${PROMOTE_ACT} call was answered HTTP ${answered.status} with no answer the command can read.`]);
|
|
7727
|
+
if (answer.summary !== null) io2.print(oneLine(answer.summary));
|
|
7728
|
+
if (answer.detail !== null) io2.print(oneLine(answer.detail));
|
|
7729
|
+
if (answer.state === "deployed") return COMMAND_STATUS.done;
|
|
7730
|
+
if (answer.state === "failed") return COMMAND_STATUS.failed;
|
|
7731
|
+
io2.print(nextLineOf(options.application, answer.nextWait ?? PROMOTE_WAIT_SECONDS));
|
|
7732
|
+
return COMMAND_STATUS.unknown;
|
|
7733
|
+
}
|
|
7734
|
+
var PROMOTE_AFTER_OWN_ERROR = `the promote may have started, so call ${toolNameOf("read_status")} for the application's production environment, and run the line again only if that read shows no promote started after this call`;
|
|
7735
|
+
var PROMOTE_ROW = Object.freeze({
|
|
7736
|
+
name: PROMOTE_SUBCOMMAND,
|
|
7737
|
+
summary: `Promotes a version to production through the platform's ${toolNameOf("promote")} action: the version the development environment serves, or the one ${PROMOTE_OPTION.version} gives. It makes one ${toolNameOf("promote")} call under the same credential as check and waits up to ${PROMOTE_WAIT_SECONDS} seconds for its end. It prints the platform's own summary and detail. With no credential, it sends nothing and prints the two routes to get one.`,
|
|
7738
|
+
example: `${PROMOTE_SUBCOMMAND} ${PROMOTE_OPTION.application} <application id>`,
|
|
7739
|
+
options: PROMOTE_OPTIONS,
|
|
7740
|
+
credential: PROMOTE_CREDENTIAL,
|
|
7741
|
+
reads: PROMOTE_READS,
|
|
7742
|
+
statuses: {
|
|
7743
|
+
done: "the promoted version serves production",
|
|
7744
|
+
failed: "the promote ended failed, as the printed detail says",
|
|
7745
|
+
unknown: `the promote is still running, or its start is unknown: the printed line gives the ${toolNameOf("read_status")} call that reads it`,
|
|
7746
|
+
nothing_started: "nothing was promoted. An option or the credential was refused, or no credential was read and the two routes to get one were printed. Or the platform was too old, or it refused the call"
|
|
7747
|
+
},
|
|
7748
|
+
note: [
|
|
7749
|
+
`A promote reuses the image built at deploy and gives the version production's own settings and credentials, as the ${toolNameOf("promote")} call does. If you omit ${PROMOTE_OPTION.version}, check that development serves the version you want in production. To promote a specific version, use ${PROMOTE_OPTION.version} with the number of a deployed version that has not been retired. It never re-mints the production database's password, which stays the tool's own ${toolNameOf("promote")} call with rotate_database_credential.`,
|
|
7750
|
+
`Where the call drew no answer, or the answer was not the platform's or could not be read, the promote may have started. Call ${toolNameOf("read_status")} for the application's production environment, and run the line again only if that read shows no promote started after the call.`
|
|
7751
|
+
],
|
|
7752
|
+
afterOwnError: PROMOTE_AFTER_OWN_ERROR,
|
|
7753
|
+
run: (call, io2) => promote(call.named, io2)
|
|
7754
|
+
});
|
|
7755
|
+
|
|
7568
7756
|
// src/turnzero_cloud_command/provision.ts
|
|
7569
7757
|
import { basename as basename3, dirname as dirname6, join as join10 } from "node:path";
|
|
7570
7758
|
var PROVISION_OPTIONS = Object.freeze([
|
|
@@ -9413,9 +9601,11 @@ var RESOURCE_TEMPLATE_CHOICES = RESOURCE_TEMPLATES.map((template) => `${SCAFFOLD
|
|
|
9413
9601
|
var perResourceTemplate = (of) => listedAnd(RESOURCE_TEMPLATES.map((template) => `${orListed(of(keptOf(template)))} under ${template}`));
|
|
9414
9602
|
var orListed = (names) => names.length < 3 ? names.join(" or ") : `${names.slice(0, -1).join(", ")}, or ${names[names.length - 1]}`;
|
|
9415
9603
|
var andListed = (names) => names.length < 3 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
|
|
9604
|
+
var PUBLIC_GALLERY_STARTER_SENTENCE = "For a public gallery with no sign-in, follow the complete starter in Store files, under A public upload service with no sign-in (read_documentation, page /cloud/guides/store-files/).";
|
|
9416
9605
|
var templatesDescribed = () => [
|
|
9417
9606
|
`The template to write, one of ${andListed(SCAFFOLD_TEMPLATES)}, ${SCAFFOLD_DEFAULT_TEMPLATE} if omitted.`,
|
|
9418
|
-
...SCAFFOLD_TEMPLATES.map((template) => `The ${template} template is ${TEMPLATE_FACTS[template].is}.`)
|
|
9607
|
+
...SCAFFOLD_TEMPLATES.map((template) => `The ${template} template is ${TEMPLATE_FACTS[template].is}.`),
|
|
9608
|
+
PUBLIC_GALLERY_STARTER_SENTENCE
|
|
9419
9609
|
].join(" ");
|
|
9420
9610
|
var OTHER_TYPE_WAY_ON = "For a type outside these, name the nearest kind, then change the written migration and routes: the files are your service's own.";
|
|
9421
9611
|
var SCAFFOLD_OPTIONS = Object.freeze([
|
|
@@ -10307,7 +10497,7 @@ async function scaffold(call, io2) {
|
|
|
10307
10497
|
if ("lines" in found) return refuse(...found.lines);
|
|
10308
10498
|
held = found;
|
|
10309
10499
|
} else {
|
|
10310
|
-
const credential = await readTakeCredential(io2, call.named[SCAFFOLD_OPTION.origin], SCAFFOLD_AFTER_LOGIN, SCAFFOLD_SIGN_IN_WHY);
|
|
10500
|
+
const credential = await readTakeCredential(io2, call.named[SCAFFOLD_OPTION.origin], SCAFFOLD_AFTER_LOGIN, SCAFFOLD_SIGN_IN_WHY, SCAFFOLD_REFUSED_MINT_SENTENCE);
|
|
10311
10501
|
if ("lines" in credential) return refuse(...credential.lines);
|
|
10312
10502
|
lineOrigin = credential.origin;
|
|
10313
10503
|
takeLines = takeLinesFor(lineOrigin);
|
|
@@ -10403,6 +10593,7 @@ var SCAFFOLD_CREDENTIAL = LIBRARY_CREDENTIAL;
|
|
|
10403
10593
|
var SCAFFOLD_READS = `what library take reads under a template that takes library entries: ${LIBRARY_READS}. Under a template that takes none, it reads no credential`;
|
|
10404
10594
|
var SCAFFOLD_AFTER_LOGIN = `Once ${LOGIN_SUBCOMMAND} has printed authorized:, run this ${SCAFFOLD_SUBCOMMAND} line again. ${NOTHING_WRITTEN2}`;
|
|
10405
10595
|
var SCAFFOLD_SIGN_IN_WHY = `A template that takes library entries needs a sign-in, since the library's reads are under a credential. The ${SCAFFOLD_DEFAULT_TEMPLATE} template, which takes none, runs without one.`;
|
|
10596
|
+
var SCAFFOLD_REFUSED_MINT_SENTENCE = `If the host refuses ${MINT_TOKEN_ACTION} or its returned line, ask the person to run the login line below in their own terminal, or build without ${SCAFFOLD_SUBCOMMAND} using signed-in tzcloud_read_library and tzcloud_deploy_files within its caps.`;
|
|
10406
10597
|
var SCAFFOLD_ROW = Object.freeze({
|
|
10407
10598
|
name: SCAFFOLD_SUBCOMMAND,
|
|
10408
10599
|
summary: `Writes a new service from a template into a project, ${SCAFFOLD_DEFAULT_TEMPLATE} if the command has no ${SCAFFOLD_OPTION.template}. It writes the template's files under app/, runs npm install and npm test there, and prints the next steps. It starts a new application, and writes over no file. Some templates take library entries: database-service takes the Database entry, and web-app takes three. Such a template takes each entry as library take does, under the same credential and with the same checks, before it writes anything. It fills the manifest's packages member from the library folder's rows. With no credential, it writes nothing, and prints the sign-in route and this command to run again once you are signed in. A template that takes no entry reads no credential.`,
|
|
@@ -10511,6 +10702,13 @@ var TZMESH_OPTIONS = Object.freeze([
|
|
|
10511
10702
|
required: true,
|
|
10512
10703
|
description: "The agent's name, 1 to 32 letters, digits, hyphens, and underscores. Its handle is this name under your account's handle."
|
|
10513
10704
|
},
|
|
10705
|
+
{
|
|
10706
|
+
name: TZMESH_OPTION.rotate,
|
|
10707
|
+
value: "",
|
|
10708
|
+
required: false,
|
|
10709
|
+
flag: true,
|
|
10710
|
+
description: `Adds a program key to the standing agent named by ${TZMESH_OPTION.name} and saves it to the chosen destination. For webhook rotations, it sets the webhook on the new key. After the command succeeds, restart your program, then end the old key.`
|
|
10711
|
+
},
|
|
10514
10712
|
{
|
|
10515
10713
|
name: TZMESH_OPTION.secret,
|
|
10516
10714
|
value: "<secret name>",
|
|
@@ -10545,7 +10743,7 @@ var TZMESH_OPTIONS = Object.freeze([
|
|
|
10545
10743
|
name: TZMESH_OPTION.webhookSecret,
|
|
10546
10744
|
value: "<secret name>",
|
|
10547
10745
|
required: false,
|
|
10548
|
-
description: `With ${TZMESH_OPTION.webhookOrigin}, the name the webhook's signing secret is stored under, in the scope ${TZMESH_OPTION.secret} is stored in. It names the secret and never holds it.`
|
|
10746
|
+
description: `With ${TZMESH_OPTION.webhookOrigin}, or alone under ${TZMESH_OPTION.rotate}, the name the webhook's signing secret is stored under, in the scope ${TZMESH_OPTION.secret} is stored in. It names the secret and never holds it.`
|
|
10549
10747
|
},
|
|
10550
10748
|
{
|
|
10551
10749
|
name: TZMESH_OPTION.description,
|
|
@@ -10598,6 +10796,35 @@ var TZMESH_OPTIONS = Object.freeze([
|
|
|
10598
10796
|
description: `The mint the line names, by its id, as the line a ${MINT_TOKEN_ACTION} call answers names it. The id is no credential. Under it, the command presents this computer's verifier for that mint.`
|
|
10599
10797
|
}
|
|
10600
10798
|
]);
|
|
10799
|
+
var ROTATE_REFUSED = [TZMESH_OPTION.description, TZMESH_OPTION.title, TZMESH_OPTION.join];
|
|
10800
|
+
var rotateOptionsOf = (call, toFile, closing) => {
|
|
10801
|
+
const named = (option) => call.named[option] ?? null;
|
|
10802
|
+
const refused = [...ROTATE_REFUSED.filter((option) => named(option) !== null), ...call.flags.has(TZMESH_OPTION.public) ? [TZMESH_OPTION.public] : []];
|
|
10803
|
+
if (refused.length > 0) {
|
|
10804
|
+
return { refusal: `${refused[0]} sets up a new agent, and ${TZMESH_OPTION.rotate} keeps the agent as it stands: leave ${refused[0]} out. ${closing}` };
|
|
10805
|
+
}
|
|
10806
|
+
const webhookOrigin = named(TZMESH_OPTION.webhookOrigin);
|
|
10807
|
+
const webhookSecret = named(TZMESH_OPTION.webhookSecret);
|
|
10808
|
+
if (toFile && (webhookOrigin !== null || webhookSecret !== null)) {
|
|
10809
|
+
return { refusal: `${webhookOrigin !== null ? TZMESH_OPTION.webhookOrigin : TZMESH_OPTION.webhookSecret} goes with ${TZMESH_OPTION.secret} alone: a file holds one value, and a webhook needs two. ${closing}` };
|
|
10810
|
+
}
|
|
10811
|
+
if (webhookOrigin !== null && webhookSecret === null) {
|
|
10812
|
+
return { refusal: `${TZMESH_OPTION.webhookOrigin} moves the webhook, and its signing secret goes under the name ${TZMESH_OPTION.webhookSecret} gives: name it too. ${closing}` };
|
|
10813
|
+
}
|
|
10814
|
+
if (webhookOrigin !== null && !isHttpsOrigin(webhookOrigin)) {
|
|
10815
|
+
return {
|
|
10816
|
+
refusal: `${TZMESH_OPTION.webhookOrigin} names the https origin of the application that hosts the agent, such as https://<name>.ai.host: its host in lowercase letters, digits, hyphens, and dots, with no path. ${closing}`
|
|
10817
|
+
};
|
|
10818
|
+
}
|
|
10819
|
+
if (webhookSecret !== null && !SECRET_NAME_FORM.test(webhookSecret)) {
|
|
10820
|
+
return { refusal: `${TZMESH_OPTION.webhookSecret} names the secret the webhook's signing secret is stored under: a letter or digit first, then letters, digits, _ or -, up to 64 characters. ${closing}` };
|
|
10821
|
+
}
|
|
10822
|
+
const secret2 = named(TZMESH_OPTION.secret);
|
|
10823
|
+
if (webhookSecret !== null && secret2 !== null && webhookSecret.toLowerCase() === secret2.toLowerCase()) {
|
|
10824
|
+
return { refusal: `${TZMESH_OPTION.webhookSecret} names a secret other than the one ${TZMESH_OPTION.secret} names: the agent's secret and the webhook's signing secret are stored under two names. ${closing}` };
|
|
10825
|
+
}
|
|
10826
|
+
return { rotateWebhook: webhookSecret === null ? null : { origin: webhookOrigin, secretName: webhookSecret } };
|
|
10827
|
+
};
|
|
10601
10828
|
var SETUP_OPTIONS = [TZMESH_OPTION.webhookOrigin, TZMESH_OPTION.webhookSecret, TZMESH_OPTION.description, TZMESH_OPTION.title, TZMESH_OPTION.join];
|
|
10602
10829
|
var UNCARRIED_WORDS = "a double quote, $, a backtick, %, !, &, |, <, >, ^, a curly double quote, a doubled or trailing backslash, or a control character";
|
|
10603
10830
|
var JOIN_CHANNEL_FORM = /^[^\s,]{1,200}$/;
|
|
@@ -10696,7 +10923,10 @@ var tzmeshOptionsOf = (call, io2) => {
|
|
|
10696
10923
|
if (path !== null && !lineValueCarries(path)) {
|
|
10697
10924
|
return { refusal: `${TZMESH_OPTION.toFile} names a path holding a character a line cannot carry alike in every shell between double quotes: ${UNCARRIED_WORDS}. Name a path without one. ${closing}` };
|
|
10698
10925
|
}
|
|
10699
|
-
const
|
|
10926
|
+
const rotate = call.flags.has(TZMESH_OPTION.rotate);
|
|
10927
|
+
const rotation = rotate ? rotateOptionsOf(call, toFile !== void 0, closing) : { rotateWebhook: null };
|
|
10928
|
+
if ("refusal" in rotation) return rotation;
|
|
10929
|
+
const setup = rotate ? { webhook: null, description: null, title: null, join: [], public: false } : setupOptionsOf(call, toFile !== void 0, closing);
|
|
10700
10930
|
if ("refusal" in setup) return setup;
|
|
10701
10931
|
const origin = entryOriginOf(io2.env, call.named[TZMESH_OPTION.origin], closing);
|
|
10702
10932
|
if ("refusal" in origin) return origin;
|
|
@@ -10713,16 +10943,21 @@ var tzmeshOptionsOf = (call, io2) => {
|
|
|
10713
10943
|
origin: origin.origin,
|
|
10714
10944
|
estate,
|
|
10715
10945
|
mint,
|
|
10716
|
-
...setup
|
|
10946
|
+
...setup,
|
|
10947
|
+
rotate,
|
|
10948
|
+
rotateWebhook: rotation.rotateWebhook
|
|
10717
10949
|
};
|
|
10718
10950
|
};
|
|
10951
|
+
var destinationOf = (options) => "secret" in options.route ? { secret: options.route.secret, application: options.application, environment: options.environment } : { toFile: options.route.toFile };
|
|
10719
10952
|
var completingOptionsOf = (options) => {
|
|
10720
10953
|
const call = {
|
|
10721
10954
|
name: options.name,
|
|
10955
|
+
...options.rotate ? { rotate: true } : {},
|
|
10722
10956
|
..."secret" in options.route ? { secret: options.route.secret } : { toFile: options.route.toFile },
|
|
10723
10957
|
...options.application !== null ? { application: options.application } : {},
|
|
10724
10958
|
...options.environment !== null ? { environment: options.environment } : {},
|
|
10725
10959
|
...options.webhook !== null ? { webhookOrigin: options.webhook.origin, webhookSecret: options.webhook.secretName } : {},
|
|
10960
|
+
...options.rotateWebhook !== null ? { ...options.rotateWebhook.origin !== null ? { webhookOrigin: options.rotateWebhook.origin } : {}, webhookSecret: options.rotateWebhook.secretName } : {},
|
|
10726
10961
|
...options.description !== null ? { description: options.description } : {},
|
|
10727
10962
|
...options.title !== null ? { title: options.title } : {},
|
|
10728
10963
|
...options.join.length > 0 ? { join: options.join } : {},
|
|
@@ -10816,7 +11051,9 @@ var register = async (io2, wire, token2, userAgent, options) => {
|
|
|
10816
11051
|
const answered = await postWire(io2, wire, TZMESH_REGISTER_PATH, token2, userAgent, {
|
|
10817
11052
|
kind: "standing",
|
|
10818
11053
|
name: options.name,
|
|
10819
|
-
label
|
|
11054
|
+
// The listener's label names where its secret went (the tzmesh rotate
|
|
11055
|
+
// plan's decision 2), so a rotation for the same place finds it.
|
|
11056
|
+
label: tzmeshListenerLabelOf(destinationOf(options)),
|
|
10820
11057
|
...options.title !== null ? { title: options.title } : {},
|
|
10821
11058
|
...options.description !== null ? { description: options.description } : {},
|
|
10822
11059
|
...options.public ? { visibility: "public" } : {}
|
|
@@ -10918,6 +11155,105 @@ var revokeRunToken = async (io2, plane, bearer, id, userAgent) => {
|
|
|
10918
11155
|
}
|
|
10919
11156
|
return [...lines, tokenStandsSentence(id)];
|
|
10920
11157
|
};
|
|
11158
|
+
var READ_ACT = "list_participants";
|
|
11159
|
+
var ATTACH_ACT = "attach";
|
|
11160
|
+
var REMOVE_ACT = "remove_listeners";
|
|
11161
|
+
var LIST_SECRETS_ACTION = "list_secrets";
|
|
11162
|
+
var ATTACHED_LINE_HEAD = "attached: ";
|
|
11163
|
+
var PRINTED_INSTANT_FORM = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
11164
|
+
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
11165
|
+
var isWebhookUrl = (value) => {
|
|
11166
|
+
try {
|
|
11167
|
+
const url = new URL(value);
|
|
11168
|
+
return url.protocol === "https:" && url.hostname !== "" && url.username === "" && url.password === "" && url.hash === "";
|
|
11169
|
+
} catch {
|
|
11170
|
+
return false;
|
|
11171
|
+
}
|
|
11172
|
+
};
|
|
11173
|
+
var rotatedAgentOf = (answer, name) => {
|
|
11174
|
+
const participants = Array.isArray(answer.participants) ? answer.participants : [];
|
|
11175
|
+
for (const entry of participants) {
|
|
11176
|
+
const record = recordOf(entry);
|
|
11177
|
+
if (record === null || record.kind !== "standing") continue;
|
|
11178
|
+
const handle = typeof record.handle === "string" ? record.handle : null;
|
|
11179
|
+
const named = record.name === name || handle !== null && handle.includes("/") && handle.slice(handle.lastIndexOf("/") + 1) === name;
|
|
11180
|
+
if (!named) continue;
|
|
11181
|
+
if (typeof record.id !== "string" || !PRINTED_NAME_FORM.test(record.id)) return null;
|
|
11182
|
+
const webhook = recordOf(record.webhook);
|
|
11183
|
+
const webhookUrl = typeof webhook?.url === "string" && isWebhookUrl(webhook.url) ? webhook.url : null;
|
|
11184
|
+
const rows = Array.isArray(record.served_by) ? record.served_by : [];
|
|
11185
|
+
const programListeners = rows.map(recordOf).filter((row) => row !== null && row.kind === "program" && typeof row.id === "string" && PRINTED_NAME_FORM.test(row.id)).map((row) => ({
|
|
11186
|
+
id: row.id,
|
|
11187
|
+
label: typeof row.label === "string" ? row.label : null,
|
|
11188
|
+
madeAt: typeof row.created_at === "string" && PRINTED_INSTANT_FORM.test(row.created_at) ? row.created_at : null
|
|
11189
|
+
}));
|
|
11190
|
+
return { id: record.id, handle: handle !== null && PRINTED_NAME_FORM.test(handle) ? handle : null, webhookUrl, programListeners };
|
|
11191
|
+
}
|
|
11192
|
+
return null;
|
|
11193
|
+
};
|
|
11194
|
+
var storedAtScope = (answer, name, application, environment) => {
|
|
11195
|
+
const secrets = Array.isArray(answer.secrets) ? answer.secrets : [];
|
|
11196
|
+
return secrets.some((entry) => {
|
|
11197
|
+
const row = recordOf(entry);
|
|
11198
|
+
if (row === null || row.name !== name) return false;
|
|
11199
|
+
if (application === null) return (row.application ?? null) === null;
|
|
11200
|
+
return row.application === application && row.environment === (environment ?? "production");
|
|
11201
|
+
});
|
|
11202
|
+
};
|
|
11203
|
+
var attachListener = async (io2, wire, token2, userAgent, agent, label) => {
|
|
11204
|
+
const answered = await postWire(io2, wire, TZMESH_REGISTER_PATH, token2, userAgent, { attach: agent.id, kind: "standing", session_secret: false, label });
|
|
11205
|
+
if (answered === null) return { unknown: [`The ${ATTACH_ACT} drew no answer.`] };
|
|
11206
|
+
if (answered.redirected) return { unknown: [`The ${ATTACH_ACT} was answered ${answeredHow(answered.status)}, which the command never follows, so the token was sent to no second address.`] };
|
|
11207
|
+
const answer = planeObjectOf(answered.text);
|
|
11208
|
+
if (answer !== null && typeof answer.secret === "string") io2.hide(answer.secret, REDACTED_PROGRAM_SECRET);
|
|
11209
|
+
if (answered.status >= PLATFORM_FAILURE_STATUS) return { unknown: failureLines(ATTACH_ACT, answered.status, planeRefusalOf(answered.text)) };
|
|
11210
|
+
if (answered.status < 200 || answered.status > 299) {
|
|
11211
|
+
const refusal = planeRefusalOf(answered.text);
|
|
11212
|
+
if (refusal === null) return { unknown: [`The ${ATTACH_ACT} was answered HTTP ${answered.status}, not by the tzMesh service.`] };
|
|
11213
|
+
return { refused: [...refusalLines(ATTACH_ACT, answered.status, refusal), "No key was added."] };
|
|
11214
|
+
}
|
|
11215
|
+
const listener = typeof answer?.listener === "string" && PRINTED_NAME_FORM.test(answer.listener) ? answer.listener : null;
|
|
11216
|
+
const secret2 = answer?.secret;
|
|
11217
|
+
if (listener === null || typeof secret2 !== "string" || !TZMESH_SECRET_FORM.test(secret2)) {
|
|
11218
|
+
return { unknown: [`The ${ATTACH_ACT} was answered HTTP ${answered.status} with no new key the command can read.`] };
|
|
11219
|
+
}
|
|
11220
|
+
return { secret: secret2, listener };
|
|
11221
|
+
};
|
|
11222
|
+
var removeListener = async (io2, wire, token2, userAgent, agent, listener) => {
|
|
11223
|
+
const answered = await postWire(io2, wire, TZMESH_REMOVE_LISTENERS_PATH, token2, userAgent, { participant: agent.id, listener });
|
|
11224
|
+
const removed = answered !== null && !answered.redirected && answered.status >= 200 && answered.status <= 299 && planeObjectOf(answered.text) !== null;
|
|
11225
|
+
if (removed) return { removed: true, lines: [`The new key ${listener} was removed, so the stored names still hold the old key, which stays live.`], unknown: false };
|
|
11226
|
+
const refusal = answered === null || answered.redirected || answered.status >= PLATFORM_FAILURE_STATUS ? null : planeRefusalOf(answered.text);
|
|
11227
|
+
const why = answered === null ? [`The ${REMOVE_ACT} drew no answer.`] : answered.redirected ? [`The ${REMOVE_ACT} was answered ${answeredHow(answered.status)}, which the command never follows.`] : answered.status >= PLATFORM_FAILURE_STATUS ? failureLines(REMOVE_ACT, answered.status, planeRefusalOf(answered.text)) : refusal !== null ? refusalLines(REMOVE_ACT, answered.status, refusal) : [`The ${REMOVE_ACT} was answered HTTP ${answered.status}, not by the tzMesh service.`];
|
|
11228
|
+
return { removed: false, lines: why, unknown: refusal === null };
|
|
11229
|
+
};
|
|
11230
|
+
var personEndsKeyAt = (estate) => `${ESTATE_ORIGINS[estate].site}/me under Hosted agents, open the agent's Manage keys, and choose End key`;
|
|
11231
|
+
var noStandingAgentSentence = (name) => `The account holds no standing agent named ${name}, so ${TZMESH_OPTION.rotate} has no key to replace.`;
|
|
11232
|
+
var webhookNameOwedSentence = () => `The agent answers a webhook, so its new key needs a new signing secret too: name the signing secret's stored name with ${TZMESH_OPTION.webhookSecret}, beside ${TZMESH_OPTION.secret}.`;
|
|
11233
|
+
var webhookNotAnsweredSentence = () => `The agent answers no webhook, so ${TZMESH_OPTION.webhookSecret} names nothing to replace: leave it out, or add ${TZMESH_OPTION.webhookOrigin} to set one.`;
|
|
11234
|
+
var notStoredSentence = (option, name) => `${option} names ${name}, which is not stored at that scope, and ${TZMESH_OPTION.rotate} replaces only a name its setup stored: name the stored name, or set the agent up again.`;
|
|
11235
|
+
var NOTHING_CHANGED = "Nothing changed.";
|
|
11236
|
+
var attachedLineOf = (agent, listener, label) => `${ATTACHED_LINE_HEAD}a new key ${listener} for agent ${agent.handle ?? `with id ${agent.id}`}, labelled "${label}", its secret printed nowhere`;
|
|
11237
|
+
var attachUnknownSentence = (label, estate) => `The command could not tell whether it added a key, so it removed none. Use tzmesh_list_participants to look for the destination label "${label}". More than one key can have that label. End a key at ${personEndsKeyAt(estate)} only if you can identify it as the new key and verify it is unused.`;
|
|
11238
|
+
var crossingSentence = () => `Another run added a key to the agent while this one ran, so this run stops: run ${TZMESH_OPTION.rotate} once more, alone.`;
|
|
11239
|
+
var unremovedSentence = (listener, estate) => `The new key ${listener} stands, and no stored name holds its secret, so it serves nothing: your person can end it at ${personEndsKeyAt(estate)}.`;
|
|
11240
|
+
var storeUnknownRotateSentence = (name, listener) => `Whether ${name} now holds the new key ${listener} is unknown: tzcloud_list_secrets from your tool shows when it was last written. Either way the key it holds stays live. Run ${TZMESH_OPTION.rotate} again before restarting your program.`;
|
|
11241
|
+
var WEBHOOK_UNSET_SENTENCE = `The new key has no webhook. Run ${TZMESH_OPTION.rotate} again before restarting your program.`;
|
|
11242
|
+
var webhookClearedSentence = (programName, signingName) => `The program secret ${programName} holds the new key. The signing secret ${signingName} was not updated, and the new key's webhook was cleared. Run ${TZMESH_OPTION.rotate} again before restarting.`;
|
|
11243
|
+
var signingUnknownRotateSentence = (signingName) => `The command could not confirm whether ${signingName} was updated. Use tzcloud_list_secrets to see when it was last written. The new key's webhook was cleared. Run ${TZMESH_OPTION.rotate} again before restarting.`;
|
|
11244
|
+
var webhookUnclearedSentence = (listener, estate) => `The command could not confirm that the webhook was cleared on the new key ${listener}. Run ${TZMESH_OPTION.rotate} again and restart your program before ending key ${listener} at ${personEndsKeyAt(estate)}.`;
|
|
11245
|
+
var crossingAfterSentence = () => `Another run added a key to the agent while this one ran, so the stored names may hold a mixed pair: run ${TZMESH_OPTION.rotate} once more, alone, before restarting your program.`;
|
|
11246
|
+
var crossingUnreadSentence = () => `Whether another run added a key meanwhile is unknown: tzmesh_list_participants from your tool shows the agent's keys. Where only this run's new key is new, restart your program as below.`;
|
|
11247
|
+
var keyMadeWordsOf = (instant) => `${new Date(instant).toLocaleString("en-GB", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", timeZone: "UTC" })} UTC`;
|
|
11248
|
+
var rotateNextLines = (options, others, label, newMadeAt) => {
|
|
11249
|
+
const restart = "secret" in options.route && options.application !== null ? `1. Restart the application so it reads the new key: tzcloud_restart_application from your tool for ${options.application} in ${options.environment ?? "production"}.` : `1. Restart your program so it reads the new key from ${"secret" in options.route ? options.route.secret : "the file"}.`;
|
|
11250
|
+
const unlabelled = (entry) => entry.label === null || entry.label === "" || entry.label === TZMESH_COMMAND_LABEL;
|
|
11251
|
+
const matching = others.filter((entry) => entry.label === label);
|
|
11252
|
+
const old = others.length === 1 && (unlabelled(others[0]) || others[0].label === label) ? others[0] : matching.length === 1 ? matching[0] : null;
|
|
11253
|
+
const made = old !== null && old.madeAt !== null ? keyMadeWordsOf(old.madeAt) : null;
|
|
11254
|
+
const end = old !== null && made !== null && newMadeAt !== null && made !== keyMadeWordsOf(newMadeAt) ? `2. At ${ESTATE_ORIGINS[options.estate].site}/me, under Hosted agents, open the agent's Manage keys. Find the old key${old.label !== null && old.label !== "" ? ` labelled "${old.label}" and` : ""} created at ${made} (its id is ${old.id}), then choose End key for that entry. Until then, conversations the old key holds wait for it.` : `2. Then end the old key of the copy the restart replaced, and no other, at ${personEndsKeyAt(options.estate)}. Until then, conversations the old key holds wait for it.`;
|
|
11255
|
+
return [restart, end];
|
|
11256
|
+
};
|
|
10921
11257
|
var endedLineOf = (stage, plane, agent, options) => {
|
|
10922
11258
|
const route = options.route;
|
|
10923
11259
|
switch (stage) {
|
|
@@ -10937,6 +11273,14 @@ var endedLineOf = (stage, plane, agent, options) => {
|
|
|
10937
11273
|
return agent === null || options.webhook === null ? `The run was ended after the register. ${registerUnknownSentence(options.estate)} ${TOKEN_LEFT}` : `The run was ended while the webhook's signing secret was being stored. ${signingUnknownSentence(options.webhook.secretName, agent, options.estate)} ${TOKEN_LEFT}`;
|
|
10938
11274
|
case "stored":
|
|
10939
11275
|
return `The run was ended after the agent's secret was kept, before the run's token was revoked. ${TOKEN_LEFT}`;
|
|
11276
|
+
// The rotation's stages, whose lines its run names with what it holds
|
|
11277
|
+
// (rotateEndedLineOf); this one stands where no run supplies them.
|
|
11278
|
+
case "rotate reading":
|
|
11279
|
+
case "rotate attached":
|
|
11280
|
+
case "rotate storing":
|
|
11281
|
+
case "rotate signing":
|
|
11282
|
+
case "rotate stored":
|
|
11283
|
+
return `The run was ended during its rotation. tzmesh_list_participants from your tool shows the agent's keys. ${TOKEN_LEFT}`;
|
|
10940
11284
|
}
|
|
10941
11285
|
};
|
|
10942
11286
|
var completing = async (io2, options, mint, tokens, userAgent) => {
|
|
@@ -10964,15 +11308,18 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
|
|
|
10964
11308
|
} else io2.print(kept2 === null ? agentStandsSentence(standing, options.estate) : unfinishedStandsSentence(standing, kept2, options.estate));
|
|
10965
11309
|
return revokedThen(minted, deleted.unknown ? whenUnknown : COMMAND_STATUS.failed);
|
|
10966
11310
|
};
|
|
10967
|
-
const storeNamed = async (name, value) => {
|
|
11311
|
+
const storeNamed = async (name, value, replace = false, heedEnd = true) => {
|
|
10968
11312
|
const action = SECRET_ACTION.store;
|
|
10969
11313
|
const answered = await postAction(io2, options.origin, action, (tokens.store ?? tokens.mint).token, userAgent, {
|
|
10970
11314
|
name,
|
|
10971
11315
|
value,
|
|
10972
11316
|
...options.application !== null ? { application: options.application } : {},
|
|
10973
|
-
...options.environment !== null ? { environment: options.environment } : {}
|
|
11317
|
+
...options.environment !== null ? { environment: options.environment } : {},
|
|
11318
|
+
// A rotation replaces a name its setup stored (the tzmesh rotate
|
|
11319
|
+
// plan's decision 3; store_secret's `replace`).
|
|
11320
|
+
...replace ? { replace: true } : {}
|
|
10974
11321
|
});
|
|
10975
|
-
if (stage.ended) return "ended";
|
|
11322
|
+
if (heedEnd && stage.ended) return "ended";
|
|
10976
11323
|
if (answered === null) return { unknown: [`${action} drew no answer.`] };
|
|
10977
11324
|
if (answered.redirected) return { unknown: [`${action} was answered ${answeredHow(answered.status)}, which the command never follows, so the value was sent to no second address.`] };
|
|
10978
11325
|
if (answered.status >= PLATFORM_FAILURE_STATUS) return { unknown: failureLines(action, answered.status, planeRefusalOf(answered.text)) };
|
|
@@ -10980,6 +11327,7 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
|
|
|
10980
11327
|
if (answered.status < 200 || answered.status > 299) {
|
|
10981
11328
|
const refusal = markedRefusalOf(answered.status, answered.text);
|
|
10982
11329
|
if (refusal === null) return { unknown: [`${action} was answered HTTP ${answered.status}, not by the platform.`] };
|
|
11330
|
+
if (replace && answered.status === 404 && refusal.error === "not_found") return "gone";
|
|
10983
11331
|
for (const line of refusalLines(action, answered.status, refusal)) io2.print(line);
|
|
10984
11332
|
return "refused";
|
|
10985
11333
|
}
|
|
@@ -10989,9 +11337,231 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
|
|
|
10989
11337
|
if ("value" in answer) io2.print("The answer carried a value, which the command never prints.");
|
|
10990
11338
|
return "stored";
|
|
10991
11339
|
};
|
|
11340
|
+
const rotationLabel = tzmeshListenerLabelOf(destinationOf(options));
|
|
11341
|
+
let rotated = null;
|
|
11342
|
+
let newKey = null;
|
|
11343
|
+
let others = [];
|
|
11344
|
+
let answerWindow = null;
|
|
11345
|
+
const windowDone = new Promise((resolve14) => {
|
|
11346
|
+
answerWindow = resolve14;
|
|
11347
|
+
});
|
|
11348
|
+
const closeWindow = async (minted, status) => {
|
|
11349
|
+
if (stage.ended) {
|
|
11350
|
+
answerWindow?.(`The run was ended during its stores, and its calls answered first: the lines above say what stands. ${TOKEN_LEFT}`);
|
|
11351
|
+
return COMMAND_STATUS.unknown;
|
|
11352
|
+
}
|
|
11353
|
+
return revokedThen(minted, status);
|
|
11354
|
+
};
|
|
11355
|
+
const unchanged = async (minted, lines) => {
|
|
11356
|
+
for (const line of lines) io2.print(line);
|
|
11357
|
+
io2.print(NOTHING_CHANGED);
|
|
11358
|
+
const revoked = await revokeRunToken(io2, plane, tokens.mint.token, minted.id, userAgent);
|
|
11359
|
+
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
11360
|
+
if (revoked === null) {
|
|
11361
|
+
io2.print(`The run's token, ${minted.id}, is revoked.`);
|
|
11362
|
+
return COMMAND_STATUS.nothing_started;
|
|
11363
|
+
}
|
|
11364
|
+
for (const line of revoked) io2.print(line);
|
|
11365
|
+
return COMMAND_STATUS.failed;
|
|
11366
|
+
};
|
|
11367
|
+
let removing = null;
|
|
11368
|
+
const removeOnce = (token2) => removing ??= Promise.resolve().then(() => removeListener(io2, wire, token2, userAgent, rotated, newKey));
|
|
11369
|
+
const removeNewKey = async (minted) => {
|
|
11370
|
+
const removal = await removeOnce(minted.value);
|
|
11371
|
+
for (const line of removal.lines) io2.print(line);
|
|
11372
|
+
if (!removal.removed) io2.print(unremovedSentence(newKey, options.estate));
|
|
11373
|
+
return removal;
|
|
11374
|
+
};
|
|
11375
|
+
const removedThen = async (minted, status) => {
|
|
11376
|
+
const removal = await removeNewKey(minted);
|
|
11377
|
+
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
11378
|
+
return revokedThen(minted, removal.unknown ? COMMAND_STATUS.unknown : removal.removed ? status : Math.max(status, COMMAND_STATUS.failed));
|
|
11379
|
+
};
|
|
11380
|
+
const storeRotated = async (name, value) => {
|
|
11381
|
+
let answered = await storeNamed(name, value, true, false);
|
|
11382
|
+
let uncertain = false;
|
|
11383
|
+
if (typeof answered === "object" && !stage.ended) {
|
|
11384
|
+
for (const line of answered.unknown) io2.print(line);
|
|
11385
|
+
uncertain = true;
|
|
11386
|
+
answered = await storeNamed(name, value, true, false);
|
|
11387
|
+
}
|
|
11388
|
+
if (answered === "gone") answered = await storeNamed(name, value, false, false);
|
|
11389
|
+
const settled = answered === "ended" || answered === "gone" ? "refused" : answered;
|
|
11390
|
+
if (settled === "refused" && uncertain) return { unknown: [`${SECRET_ACTION.store} was refused when sent once more, which does not show whether its first send wrote ${name}.`] };
|
|
11391
|
+
return settled;
|
|
11392
|
+
};
|
|
11393
|
+
const setWebhook = (secret2, url) => setupCall(io2, wire, TZMESH_WEBHOOK_SET_PATH, secret2, userAgent, WEBHOOK_ACT, { url }, (answer) => {
|
|
11394
|
+
if (typeof answer?.signing_secret === "string") io2.hide(answer.signing_secret, REDACTED_WEBHOOK_SECRET);
|
|
11395
|
+
});
|
|
11396
|
+
let tokensRun = null;
|
|
11397
|
+
const readAgents = () => setupCall(io2, wire, TZMESH_LIST_PARTICIPANTS_PATH, tokensRun, userAgent, READ_ACT, {}, () => void 0);
|
|
11398
|
+
const rotating = async (minted) => {
|
|
11399
|
+
tokensRun = minted.value;
|
|
11400
|
+
stage.now = "rotate reading";
|
|
11401
|
+
const first = await readAgents();
|
|
11402
|
+
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
11403
|
+
if (!("answer" in first)) return unchanged(minted, "refused" in first ? first.refused : first.unknown);
|
|
11404
|
+
const read2 = rotatedAgentOf(first.answer, options.name);
|
|
11405
|
+
if (read2 === null) return unchanged(minted, [noStandingAgentSentence(options.name)]);
|
|
11406
|
+
rotated = read2;
|
|
11407
|
+
others = read2.programListeners;
|
|
11408
|
+
const movedTo = options.rotateWebhook?.origin ?? null;
|
|
11409
|
+
const webhookUrl = movedTo !== null ? `${movedTo}${TZMESH_WEBHOOK_ROUTE}` : read2.webhookUrl;
|
|
11410
|
+
if (webhookUrl !== null && options.rotateWebhook === null) return unchanged(minted, [webhookNameOwedSentence()]);
|
|
11411
|
+
if (webhookUrl === null && options.rotateWebhook !== null) return unchanged(minted, [webhookNotAnsweredSentence()]);
|
|
11412
|
+
if ("secret" in route) {
|
|
11413
|
+
const named = [[TZMESH_OPTION.secret, route.secret], ...options.rotateWebhook !== null ? [[TZMESH_OPTION.webhookSecret, options.rotateWebhook.secretName]] : []];
|
|
11414
|
+
const listed2 = await postAction(io2, options.origin, LIST_SECRETS_ACTION, (tokens.store ?? tokens.mint).token, userAgent, {
|
|
11415
|
+
...options.application !== null ? { application: options.application } : {},
|
|
11416
|
+
...options.application !== null && options.environment !== null ? { environment: options.environment } : {}
|
|
11417
|
+
});
|
|
11418
|
+
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
11419
|
+
const answer = listed2 !== null && !listed2.redirected && listed2.status >= 200 && listed2.status <= 299 ? planeObjectOf(listed2.text) : null;
|
|
11420
|
+
if (answer === null) {
|
|
11421
|
+
const refusal = listed2 === null || listed2.redirected ? null : markedRefusalOf(listed2.status, listed2.text);
|
|
11422
|
+
return unchanged(minted, [
|
|
11423
|
+
listed2 === null ? `${LIST_SECRETS_ACTION} drew no answer.` : listed2.redirected ? `${LIST_SECRETS_ACTION} was answered ${answeredHow(listed2.status)}, which the command never follows.` : refusal !== null ? refusalLines(LIST_SECRETS_ACTION, listed2.status, refusal).join(" ") : `${LIST_SECRETS_ACTION} was answered HTTP ${listed2.status}, not by the platform.`
|
|
11424
|
+
]);
|
|
11425
|
+
}
|
|
11426
|
+
const missing = named.find(([, name]) => !storedAtScope(answer, name, options.application, options.environment));
|
|
11427
|
+
if (missing !== void 0) return unchanged(minted, [notStoredSentence(missing[0], missing[1])]);
|
|
11428
|
+
}
|
|
11429
|
+
const attached = await attachListener(io2, wire, minted.value, userAgent, read2, rotationLabel);
|
|
11430
|
+
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
11431
|
+
if ("refused" in attached) {
|
|
11432
|
+
for (const line of attached.refused) io2.print(line);
|
|
11433
|
+
return revokedThen(minted, COMMAND_STATUS.failed);
|
|
11434
|
+
}
|
|
11435
|
+
if ("unknown" in attached) {
|
|
11436
|
+
for (const line of attached.unknown) io2.print(line);
|
|
11437
|
+
io2.print(attachUnknownSentence(rotationLabel, options.estate));
|
|
11438
|
+
return revokedThen(minted, COMMAND_STATUS.unknown);
|
|
11439
|
+
}
|
|
11440
|
+
newKey = attached.listener;
|
|
11441
|
+
const secret2 = attached.secret;
|
|
11442
|
+
stage.now = "rotate attached";
|
|
11443
|
+
io2.print(attachedLineOf(read2, newKey, rotationLabel));
|
|
11444
|
+
const known = new Set(read2.programListeners.map((entry) => entry.id));
|
|
11445
|
+
const strangerIn = (answer) => {
|
|
11446
|
+
const now = rotatedAgentOf(answer, options.name);
|
|
11447
|
+
return now === null || now.programListeners.some((entry) => !known.has(entry.id) && entry.id !== newKey);
|
|
11448
|
+
};
|
|
11449
|
+
const second = await readAgents();
|
|
11450
|
+
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
11451
|
+
if (!("answer" in second)) {
|
|
11452
|
+
for (const line of "refused" in second ? second.refused : second.unknown) io2.print(line);
|
|
11453
|
+
return removedThen(minted, "refused" in second ? COMMAND_STATUS.failed : COMMAND_STATUS.unknown);
|
|
11454
|
+
}
|
|
11455
|
+
if (strangerIn(second.answer)) {
|
|
11456
|
+
io2.print(crossingSentence());
|
|
11457
|
+
return removedThen(minted, COMMAND_STATUS.failed);
|
|
11458
|
+
}
|
|
11459
|
+
stage.now = "rotate storing";
|
|
11460
|
+
if ("toFile" in route) {
|
|
11461
|
+
const written = writeThenRename(io2, route.toFile, `${secret2}
|
|
11462
|
+
`, "owner only", tag);
|
|
11463
|
+
if ("unwritten" in written) {
|
|
11464
|
+
io2.print(`The file ${TZMESH_OPTION.toFile} names could not be written (${written.unwritten}), so it still holds the old key.`);
|
|
11465
|
+
await removeNewKey(minted);
|
|
11466
|
+
return closeWindow(minted, COMMAND_STATUS.failed);
|
|
11467
|
+
}
|
|
11468
|
+
io2.print(WRITTEN_LINE);
|
|
11469
|
+
} else {
|
|
11470
|
+
const stored = await storeRotated(route.secret, secret2);
|
|
11471
|
+
if (stored === "refused") {
|
|
11472
|
+
await removeNewKey(minted);
|
|
11473
|
+
return closeWindow(minted, COMMAND_STATUS.failed);
|
|
11474
|
+
}
|
|
11475
|
+
if (stored !== "stored") {
|
|
11476
|
+
for (const line of stored.unknown) io2.print(line);
|
|
11477
|
+
io2.print(storeUnknownRotateSentence(route.secret, newKey));
|
|
11478
|
+
return closeWindow(minted, COMMAND_STATUS.unknown);
|
|
11479
|
+
}
|
|
11480
|
+
}
|
|
11481
|
+
if (webhookUrl !== null && options.rotateWebhook !== null && "secret" in route) {
|
|
11482
|
+
let set = await setWebhook(secret2, webhookUrl);
|
|
11483
|
+
if ("unknown" in set && !stage.ended) {
|
|
11484
|
+
for (const line of set.unknown) io2.print(line);
|
|
11485
|
+
set = await setWebhook(secret2, webhookUrl);
|
|
11486
|
+
if ("refused" in set) set = { unknown: [...set.refused, `The ${WEBHOOK_ACT} was refused when sent once more, which does not show whether its first send set the webhook.`] };
|
|
11487
|
+
}
|
|
11488
|
+
stage.now = "rotate signing";
|
|
11489
|
+
const signing = "answer" in set ? set.answer.signing_secret : null;
|
|
11490
|
+
if (!("answer" in set) || typeof signing !== "string" || !TZMESH_WEBHOOK_SECRET_FORM.test(signing)) {
|
|
11491
|
+
if ("refused" in set) {
|
|
11492
|
+
for (const line of set.refused) io2.print(line);
|
|
11493
|
+
io2.print(WEBHOOK_UNSET_SENTENCE);
|
|
11494
|
+
return closeWindow(minted, COMMAND_STATUS.failed);
|
|
11495
|
+
}
|
|
11496
|
+
for (const line of "unknown" in set ? set.unknown : [`The ${WEBHOOK_ACT} answered no signing secret of its form, so the command keeps none.`]) io2.print(line);
|
|
11497
|
+
const cleared = await setWebhook(secret2, null);
|
|
11498
|
+
if (!("answer" in cleared)) {
|
|
11499
|
+
for (const line of "refused" in cleared ? cleared.refused : cleared.unknown) io2.print(line);
|
|
11500
|
+
io2.print(webhookUnclearedSentence(newKey, options.estate));
|
|
11501
|
+
return closeWindow(minted, COMMAND_STATUS.unknown);
|
|
11502
|
+
}
|
|
11503
|
+
io2.print(WEBHOOK_UNSET_SENTENCE);
|
|
11504
|
+
return closeWindow(minted, "unknown" in set ? COMMAND_STATUS.unknown : COMMAND_STATUS.failed);
|
|
11505
|
+
}
|
|
11506
|
+
io2.print(webhookLineOf(webhookUrl));
|
|
11507
|
+
const kept2 = await storeRotated(options.rotateWebhook.secretName, signing);
|
|
11508
|
+
if (kept2 !== "stored") {
|
|
11509
|
+
if (typeof kept2 === "object") for (const line of kept2.unknown) io2.print(line);
|
|
11510
|
+
const cleared = await setWebhook(secret2, null);
|
|
11511
|
+
if (!("answer" in cleared)) {
|
|
11512
|
+
for (const line of "refused" in cleared ? cleared.refused : cleared.unknown) io2.print(line);
|
|
11513
|
+
io2.print(webhookUnclearedSentence(newKey, options.estate));
|
|
11514
|
+
return closeWindow(minted, COMMAND_STATUS.unknown);
|
|
11515
|
+
}
|
|
11516
|
+
io2.print(kept2 === "refused" ? webhookClearedSentence(route.secret, options.rotateWebhook.secretName) : signingUnknownRotateSentence(options.rotateWebhook.secretName));
|
|
11517
|
+
return closeWindow(minted, kept2 === "refused" ? COMMAND_STATUS.failed : COMMAND_STATUS.unknown);
|
|
11518
|
+
}
|
|
11519
|
+
}
|
|
11520
|
+
stage.now = "rotate stored";
|
|
11521
|
+
if (stage.ended) {
|
|
11522
|
+
for (const line of rotateNextLines(options, others, rotationLabel, null)) io2.print(line);
|
|
11523
|
+
return closeWindow(minted, COMMAND_STATUS.unknown);
|
|
11524
|
+
}
|
|
11525
|
+
const third = await readAgents();
|
|
11526
|
+
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
11527
|
+
if (!("answer" in third)) {
|
|
11528
|
+
for (const line of "refused" in third ? third.refused : third.unknown) io2.print(line);
|
|
11529
|
+
io2.print(crossingUnreadSentence());
|
|
11530
|
+
for (const line of rotateNextLines(options, others, rotationLabel, null)) io2.print(line);
|
|
11531
|
+
return revokedThen(minted, COMMAND_STATUS.unknown);
|
|
11532
|
+
}
|
|
11533
|
+
if (strangerIn(third.answer)) {
|
|
11534
|
+
io2.print(crossingAfterSentence());
|
|
11535
|
+
return revokedThen(minted, COMMAND_STATUS.failed);
|
|
11536
|
+
}
|
|
11537
|
+
const newMadeAt = rotatedAgentOf(third.answer, options.name)?.programListeners.find((entry) => entry.id === newKey)?.madeAt ?? null;
|
|
11538
|
+
for (const line of rotateNextLines(options, others, rotationLabel, newMadeAt)) io2.print(line);
|
|
11539
|
+
return revokedThen(minted, COMMAND_STATUS.done);
|
|
11540
|
+
};
|
|
11541
|
+
const rotateEndedLineOf = (now) => {
|
|
11542
|
+
switch (now) {
|
|
11543
|
+
case "rotate reading":
|
|
11544
|
+
return `The run was ended before its new key was known, so no stored name holds a key this run made: tzmesh_list_participants from your tool shows the agent's keys, and one labelled "${rotationLabel}" that no name holds can be ended at ${personEndsKeyAt(options.estate)}. ${TOKEN_LEFT}`;
|
|
11545
|
+
case "rotate stored":
|
|
11546
|
+
return `The run was ended after the new secret values were saved. ${rotateNextLines(options, others, rotationLabel, null).join(" ")} ${TOKEN_LEFT}`;
|
|
11547
|
+
default:
|
|
11548
|
+
return endedLineOf(now, plane, agent, options);
|
|
11549
|
+
}
|
|
11550
|
+
};
|
|
11551
|
+
const rotateSettle = (now) => {
|
|
11552
|
+
if (now === "rotate attached" && rotated !== null && newKey !== null && tokensRun !== null) {
|
|
11553
|
+
const key = newKey;
|
|
11554
|
+
return removeOnce(tokensRun).then(
|
|
11555
|
+
(removal) => removal.removed ? `The run was ended before its first store, so the new key ${key} was removed, and the stored names still hold the old key. ${TOKEN_LEFT}` : `The run was ended before its first store. ${unremovedSentence(key, options.estate)} ${TOKEN_LEFT}`
|
|
11556
|
+
);
|
|
11557
|
+
}
|
|
11558
|
+
if (now === "rotate storing" || now === "rotate signing") return windowDone;
|
|
11559
|
+
return null;
|
|
11560
|
+
};
|
|
10992
11561
|
const carried = {
|
|
10993
11562
|
stage,
|
|
10994
|
-
endedLine: (now) => endedLineOf(now, plane, agent, options),
|
|
11563
|
+
endedLine: (now) => options.rotate ? rotateEndedLineOf(now) : endedLineOf(now, plane, agent, options),
|
|
11564
|
+
...options.rotate ? { settle: rotateSettle } : {},
|
|
10995
11565
|
// The file's try, by an empty file beside it under the run's tag,
|
|
10996
11566
|
// owner-only, before any request (decision 6).
|
|
10997
11567
|
prepare: () => {
|
|
@@ -11010,6 +11580,7 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
|
|
|
11010
11580
|
return revokedThen(minted, COMMAND_STATUS.failed);
|
|
11011
11581
|
},
|
|
11012
11582
|
deliver: async (minted) => {
|
|
11583
|
+
if (options.rotate) return rotating(minted);
|
|
11013
11584
|
stage.now = "registering";
|
|
11014
11585
|
const registered = await register(io2, wire, minted.value, userAgent, options);
|
|
11015
11586
|
if (stage.ended) return COMMAND_STATUS.unknown;
|
|
@@ -11062,7 +11633,7 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
|
|
|
11062
11633
|
}
|
|
11063
11634
|
const stored = await storeNamed(route.secret, secret2);
|
|
11064
11635
|
if (stored === "ended" || stage.ended) return COMMAND_STATUS.unknown;
|
|
11065
|
-
if (stored === "refused") return deletedThen(minted, agent);
|
|
11636
|
+
if (stored === "refused" || stored === "gone") return deletedThen(minted, agent);
|
|
11066
11637
|
if (stored !== "stored") {
|
|
11067
11638
|
for (const line of stored.unknown) io2.print(line);
|
|
11068
11639
|
if (options.webhook !== null) return deletedThen(minted, agent, COMMAND_STATUS.unknown, { name: route.secret, known: false });
|
|
@@ -11073,7 +11644,7 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
|
|
|
11073
11644
|
stage.now = "storing signing";
|
|
11074
11645
|
const kept2 = await storeNamed(options.webhook.secretName, signing);
|
|
11075
11646
|
if (kept2 === "ended" || stage.ended) return COMMAND_STATUS.unknown;
|
|
11076
|
-
if (kept2 === "refused") return deletedThen(minted, agent, COMMAND_STATUS.failed, { name: route.secret, known: true });
|
|
11647
|
+
if (kept2 === "refused" || kept2 === "gone") return deletedThen(minted, agent, COMMAND_STATUS.failed, { name: route.secret, known: true });
|
|
11077
11648
|
if (kept2 !== "stored") {
|
|
11078
11649
|
for (const line of kept2.unknown) io2.print(line);
|
|
11079
11650
|
io2.print(signingUnknownSentence(options.webhook.secretName, agent, options.estate));
|
|
@@ -11145,23 +11716,24 @@ var TZMESH_FIRST_ACT_AFTER_OWN_ERROR = "a request may now be pending in this com
|
|
|
11145
11716
|
var TZMESH_ROW = Object.freeze({
|
|
11146
11717
|
name: TZMESH_SUBCOMMAND,
|
|
11147
11718
|
acts: Object.freeze([TZMESH_REGISTER_ACT]),
|
|
11148
|
-
summary: `Registers a tzMesh standing agent,
|
|
11719
|
+
summary: `Registers a tzMesh standing agent, or rotates its program key with ${TZMESH_OPTION.rotate}. It saves the secret in your account's secret store or in a file, and prints no secret value. It runs in two acts around one ${MINT_TOKEN_ACTION} call your tool makes.`,
|
|
11149
11720
|
example: `${TZMESH_ACT_WORDS} ${TZMESH_OPTION.name} <name> ${TZMESH_OPTION.secret} <secret name> ${TZMESH_OPTION.application} <application id> ${TZMESH_OPTION.environment} production`,
|
|
11150
11721
|
options: TZMESH_OPTIONS,
|
|
11151
11722
|
credential: null,
|
|
11152
11723
|
reads: TZMESH_READS,
|
|
11153
11724
|
statuses: {
|
|
11154
|
-
done: "the challenge printed,
|
|
11155
|
-
failed: "a
|
|
11156
|
-
unknown: "an outcome is unknown: the printed lines name list_tokens, tzmesh_list_participants, or tzcloud_list_secrets",
|
|
11157
|
-
nothing_started: `
|
|
11725
|
+
done: "the challenge printed. On completion, setup registered the agent and saved its secret, or rotation added a key and saved the required values. The run token was revoked",
|
|
11726
|
+
failed: "a step failed, or the run token could not be revoked. Read the printed lines to see what changed and how to recover",
|
|
11727
|
+
unknown: "an outcome is unknown: the printed lines name list_tokens, tzmesh_list_participants, or tzcloud_list_secrets. Follow the printed recovery before restarting or ending a key",
|
|
11728
|
+
nothing_started: `the command stopped before changing the agent or destination. If the command minted a run token, it revoked it. An option, the credential file, the file ${TZMESH_OPTION.toFile} names, or this computer's token was refused. Or no request was pending, the platform was too old, or the exchange was refused. Or, under ${TZMESH_OPTION.rotate}, the agent, a stored name, or the signing secret's name was missing`
|
|
11158
11729
|
},
|
|
11159
11730
|
variables: [[ORIGIN_VARIABLE, `the platform's address the secret is stored at where the line names no ${TZMESH_OPTION.origin}. It must be ${DEFAULT_COMMAND_ORIGIN} or ${DEVELOPMENT_COMMAND_ORIGIN}.`]],
|
|
11160
11731
|
note: [
|
|
11161
|
-
`
|
|
11162
|
-
`The completing line presents this computer's verifier and receives a tzMesh token minted for this run alone.
|
|
11732
|
+
`Setup and rotation take two acts around one ${MINT_TOKEN_ACTION} call, which your signed-in tool makes itself. ${TZMESH_ACT_WORDS} run without ${TZMESH_OPTION.mint} prints a challenge, the call to make, and the options its completing line takes. Your tool makes that call, and runs the line it answers once, on this computer, with those options after it. That line holds no code.`,
|
|
11733
|
+
`The completing line presents this computer's verifier and receives a tzMesh token minted for this run alone. In setup, it registers the agent with that token, keeps the agent's secret, and revokes the token. Where the secret could not be kept, it deletes the agent.`,
|
|
11163
11734
|
`With ${TZMESH_OPTION.secret}, the secret goes from the register's answer to your account's secret store, and never reaches your tool. With ${TZMESH_OPTION.toFile}, it goes into that file, which anything that runs as you can read, your tool among them. Keep that file outside any application's folder.`,
|
|
11164
|
-
`
|
|
11735
|
+
`In setup, for an agent a Turn Zero Cloud application hosts, the same run sets it up. ${TZMESH_OPTION.webhookOrigin} sets its webhook at that application's origin, and ${TZMESH_OPTION.webhookSecret} names where the webhook's signing secret is stored. ${TZMESH_OPTION.join} joins its channels. Its description, title, and ${TZMESH_OPTION.public} go into the register. Where a step fails, follow the recovery the run prints before running the line again.`,
|
|
11736
|
+
`With ${TZMESH_OPTION.rotate}, the completing line adds a key to the named standing agent and saves the program secret in the chosen store or file. For a webhook rotation it also sets the webhook on the new key and stores its signing secret. After success, restart your program and check the new key before ending the old one. If a step fails, follow the printed recovery before restarting or ending a key.`,
|
|
11165
11737
|
`The run reads this computer's token for each address it sends to: the ${TZMESH_OPTION.tzmeshEstate} platform's, to revoke the run's token, and under ${TZMESH_OPTION.secret} the ${TZMESH_OPTION.origin} platform's, to store the secret. Authorize this computer at each first.`
|
|
11166
11738
|
],
|
|
11167
11739
|
afterOwnError: (call) => call.named[TZMESH_OPTION.mint] !== void 0 ? tzmeshAfterOwnError(call) : TZMESH_FIRST_ACT_AFTER_OWN_ERROR,
|
|
@@ -11587,7 +12159,8 @@ var childRunnerOf = (signals, inputIsTerminal, passesOn = SIGNALS_PASS_ON, group
|
|
|
11587
12159
|
}
|
|
11588
12160
|
});
|
|
11589
12161
|
var runChild = childRunnerOf(process, () => isatty(0));
|
|
11590
|
-
var
|
|
12162
|
+
var END_WAIT_BOUND_MS = 25e4;
|
|
12163
|
+
var endHoldOf = (signals, end, schedule = scheduleUnref) => (act) => {
|
|
11591
12164
|
let held = [];
|
|
11592
12165
|
const release = () => {
|
|
11593
12166
|
for (const [signal, handler] of held) signals.off(signal, handler);
|
|
@@ -11596,17 +12169,41 @@ var endHoldOf = (signals, end) => (act) => {
|
|
|
11596
12169
|
held = HELD_SIGNALS.map((signal) => {
|
|
11597
12170
|
const handler = () => {
|
|
11598
12171
|
release();
|
|
12172
|
+
let answered = void 0;
|
|
11599
12173
|
try {
|
|
11600
|
-
act();
|
|
12174
|
+
answered = act();
|
|
11601
12175
|
} catch {
|
|
11602
12176
|
}
|
|
11603
|
-
|
|
12177
|
+
if (answered === void 0 || typeof answered.then !== "function") {
|
|
12178
|
+
end(endedStatusOf(signal));
|
|
12179
|
+
return;
|
|
12180
|
+
}
|
|
12181
|
+
let ended = false;
|
|
12182
|
+
const again = [];
|
|
12183
|
+
const finish = () => {
|
|
12184
|
+
if (ended) return;
|
|
12185
|
+
ended = true;
|
|
12186
|
+
for (const [other, onAgain] of again) signals.off(other, onAgain);
|
|
12187
|
+
cancel();
|
|
12188
|
+
end(endedStatusOf(signal));
|
|
12189
|
+
};
|
|
12190
|
+
for (const other of HELD_SIGNALS) {
|
|
12191
|
+
signals.on(other, finish);
|
|
12192
|
+
again.push([other, finish]);
|
|
12193
|
+
}
|
|
12194
|
+
const cancel = schedule(END_WAIT_BOUND_MS, finish);
|
|
12195
|
+
answered.then(finish, finish);
|
|
11604
12196
|
};
|
|
11605
12197
|
signals.on(signal, handler);
|
|
11606
12198
|
return [signal, handler];
|
|
11607
12199
|
});
|
|
11608
12200
|
return release;
|
|
11609
12201
|
};
|
|
12202
|
+
var scheduleUnref = (ms, act) => {
|
|
12203
|
+
const timer = setTimeout(act, ms);
|
|
12204
|
+
timer.unref?.();
|
|
12205
|
+
return () => clearTimeout(timer);
|
|
12206
|
+
};
|
|
11610
12207
|
var OUTPUT_LEAVE_BOUND_MS = 2e3;
|
|
11611
12208
|
var endAfterOutput = (status) => {
|
|
11612
12209
|
process.exitCode = status;
|
|
@@ -11737,6 +12334,7 @@ var SUBCOMMANDS = Object.freeze([
|
|
|
11737
12334
|
HASH_ROW,
|
|
11738
12335
|
DEPLOY_ROW,
|
|
11739
12336
|
CHECK_ROW,
|
|
12337
|
+
PROMOTE_ROW,
|
|
11740
12338
|
SECRET_ROW,
|
|
11741
12339
|
PUT_ROW,
|
|
11742
12340
|
EXPORT_DOWNLOAD_ROW,
|
|
@@ -11988,7 +12586,7 @@ New to Turn Zero Cloud? Start here: ${SETUP_GUIDE}`);
|
|
|
11988
12586
|
if (argv2.length === 0 && showOnboarding(io2)) {
|
|
11989
12587
|
say(`
|
|
11990
12588
|
Get started: ${SETUP_GUIDE}`);
|
|
11991
|
-
say(`Paste
|
|
12589
|
+
say(`Paste the setup prompt into your AI coding tool.
|
|
11992
12590
|
${SETUP_PROMPT}`);
|
|
11993
12591
|
}
|
|
11994
12592
|
if (jsonDeployOf(argv2)) printDeployResultOnce(io2, jsonResultHoldOf(io2), deployResultOf(COMMAND_STATUS.nothing_started, deployApplicationOf(argv2), []));
|