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 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 this into Claude, ChatGPT, or Codex:
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
- Your AI connects Turn Zero Cloud and signs you in, or creates a free account. Then it gives you a build prompt to paste, which builds a demo you can open in your browser.
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.
@@ -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 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 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}.`;
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 = "If the answered line can't run here, a deploy can still go through a route that does not require the turnzero command. That is tzcloud_deploy_files within its caps or, past them where your tool can zip and upload the folder, tzcloud_mint_upload_grant followed by tzcloud_deploy naming the uploaded zip as artifact. Tell the person which route you take and why. Only where no such route serves, give the person that exact line to run once in their own terminal, never a rewritten one.";
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
- `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.`,
1195
- noConnectionLine(origin),
1196
- `${CHALLENGE_LINE_HEAD}${pending.challenge}`,
1197
- CALL_LINE_HEAD,
1198
- ...mintCallMembers("tzmesh", pending.challenge, TZMESH_COMMAND_LABEL).map(([name, value]) => ` ${name}: ${JSON.stringify(value)}`),
1199
- `${OPTIONS_LINE_HEAD}${completing2}`,
1200
- `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.`,
1201
- `The request is pending until ${pending.expires_at}: ${firstActNamed("tzmesh", origin)} run again with the same options before then prints this challenge again.`,
1202
- TZMESH_HAND_OVER_SENTENCE
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 setup = setupOptionsOf(call, toFile !== void 0, closing);
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: TZMESH_COMMAND_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, and keeps its program secret where a program reads it, never printing it. It keeps the secret in your account's secret store by name, or in a file. It runs in two acts around one ${MINT_TOKEN_ACTION} call your tool makes.`,
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, or the agent registered, its secret kept, and the run's token revoked",
11155
- failed: "a part failed: the agent was not registered, its secret was not kept, or the run's token was not revoked. The printed lines name what stands and the call that ends it",
11156
- unknown: "an outcome is unknown: the printed lines name list_tokens, tzmesh_list_participants, or tzcloud_list_secrets",
11157
- nothing_started: `nothing was minted. 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`
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
- `Registering an agent takes 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.`,
11162
- `The completing line presents this computer's verifier and receives a tzMesh token minted for this run alone. 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.`,
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
- `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.`,
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 endHoldOf = (signals, end) => (act) => {
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
- end(endedStatusOf(signal));
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 this into Claude, ChatGPT, or Codex:
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), []));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turnzero",
3
- "version": "0.43.0",
3
+ "version": "0.45.0-next.1",
4
4
  "type": "module",
5
5
  "description": "Deploy and run AI-built apps with Turn Zero Cloud hosting and backend services.",
6
6
  "license": "Apache-2.0",