turnzero 0.31.1 → 0.33.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.
@@ -64,7 +64,7 @@ var TURNZERO_CLOUD_PACKAGE = "turnzero";
64
64
  var LOGIN_SUBCOMMAND = "login";
65
65
  var LOGIN_OPTION = Object.freeze({ origin: "--origin", noBrowser: "--no-browser" });
66
66
  var loginLineOf = (origin) => commandLineOf(origin, LOGIN_SUBCOMMAND, origin !== DEFAULT_COMMAND_ORIGIN ? { origin } : {});
67
- var HAND_OVER_SENTENCE = "If your tool's own safety check refuses a line, give the person that exact line to run once in their own terminal, never a rewritten one.";
67
+ var HAND_OVER_SENTENCE = "If your tool's own safety check refuses a line, first take any route named above that your tool can still use, telling the person which and why. Only where none works, give the person that exact line to run once in their own terminal, never a rewritten one.";
68
68
  var COMPUTER_LOGIN_PATH = "/approve/computer";
69
69
  var COMPUTER_LOGIN_QUERY = Object.freeze({ challenge: "challenge", state: "state", port: "port", label: "label" });
70
70
  var LOGIN_STATE_FORM = /^[A-Za-z0-9_-]{43}$/;
@@ -169,6 +169,9 @@ var SECRET_NAME_FORM = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
169
169
  var PUT_SUBCOMMAND = "put";
170
170
  var PUT_OPTION = Object.freeze({ area: "--area", name: "--name", path: DEPLOY_OPTION.path, origin: DEPLOY_OPTION.origin });
171
171
  var PUT_AREA_FORM = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
172
+ var LINE_PATH_SHELL_CLASS = /["$`%!&|<>^\u201c\u201d\u201e]|\\\\|\\$/;
173
+ var LINE_CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
174
+ var lineValueCarries = (value) => !LINE_CONTROL_CHARACTER.test(value) && !LINE_PATH_SHELL_CLASS.test(value);
172
175
  var EXPORT_AREA_PREFIX = "export-";
173
176
  var exportAreaName = (application) => `${EXPORT_AREA_PREFIX}${application}`;
174
177
  var EXPORT_MANIFEST_FILE = "manifest.json";
@@ -292,7 +295,14 @@ var TZMESH_OPTION = {
292
295
  force: "--force",
293
296
  origin: "--origin",
294
297
  tzmeshEstate: "--tzmesh-estate",
295
- mint: "--mint"
298
+ mint: "--mint",
299
+ // The hosted setup's options (the tzmesh hosted setup plan's decision 1).
300
+ webhookOrigin: "--webhook-origin",
301
+ webhookSecret: "--webhook-secret",
302
+ description: "--description",
303
+ title: "--title",
304
+ join: "--join",
305
+ public: "--public"
296
306
  };
297
307
  var TZMESH_ESTATES = Object.freeze(["production", "development"]);
298
308
  var TZMESH_ESTATE_ORIGINS = Object.freeze({
@@ -315,6 +325,13 @@ var TZMESH_SECRET_FORM = new RegExp(`^${TZMESH_SECRET_PREFIX}[A-Za-z0-9_-]{16,25
315
325
  var TZMESH_NAME_FORM = /^[A-Za-z0-9_-]{1,32}$/;
316
326
  var TZMESH_REGISTER_PATH = "/v1/register";
317
327
  var TZMESH_END_PATH = "/v1/end";
328
+ var TZMESH_WEBHOOK_SET_PATH = "/v1/webhook_set";
329
+ var TZMESH_JOIN_PATH = "/v1/join";
330
+ var TZMESH_WEBHOOK_ROUTE = "/tzmesh/webhook";
331
+ var TZMESH_WEBHOOK_SECRET_PREFIX = "turnzero_tzmesh_whk_";
332
+ var TZMESH_WEBHOOK_SECRET_FORM = new RegExp(`^${TZMESH_WEBHOOK_SECRET_PREFIX}[A-Za-z0-9_-]{43}$`);
333
+ var TZMESH_TITLE_MAX = 80;
334
+ var TZMESH_DESCRIPTION_MAX = 120;
318
335
  var tzmeshMintMembers = (challenge) => [
319
336
  [CODE_CHALLENGE_MEMBER, challenge],
320
337
  ["scope_kind", "account"],
@@ -328,6 +345,12 @@ var tzmeshRegisterOptionsOf = (call) => [
328
345
  call.secret !== void 0 ? `${TZMESH_OPTION.secret} ${call.secret}` : `${TZMESH_OPTION.toFile} "${call.toFile ?? ""}"`,
329
346
  ...call.application !== void 0 ? [`${TZMESH_OPTION.application} ${call.application}`] : [],
330
347
  ...call.environment !== void 0 ? [`${TZMESH_OPTION.environment} ${call.environment}`] : [],
348
+ ...call.webhookOrigin !== void 0 ? [`${TZMESH_OPTION.webhookOrigin} ${call.webhookOrigin}`] : [],
349
+ ...call.webhookSecret !== void 0 ? [`${TZMESH_OPTION.webhookSecret} ${call.webhookSecret}`] : [],
350
+ ...call.description !== void 0 ? [`${TZMESH_OPTION.description} "${call.description}"`] : [],
351
+ ...call.title !== void 0 ? [`${TZMESH_OPTION.title} "${call.title}"`] : [],
352
+ ...call.join !== void 0 && call.join.length > 0 ? [`${TZMESH_OPTION.join} "${call.join.join(",")}"`] : [],
353
+ ...call.public === true ? [TZMESH_OPTION.public] : [],
331
354
  ...call.force === true ? [TZMESH_OPTION.force] : [],
332
355
  `${TZMESH_OPTION.origin} ${originWord(call.origin)}`,
333
356
  `${TZMESH_OPTION.tzmeshEstate} ${call.tzmeshEstate}`
@@ -995,11 +1018,11 @@ var noTokenSentence = (origin, expiredAt) => expiredAt === null ? `No credential
995
1018
  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 line. It sets ${ORIGIN_VARIABLE} to the address that minted the token where that is not ${DEFAULT_COMMAND_ORIGIN}, then runs the line again. Such a tool cannot use ${MINT_TOKEN_ACTION}, which takes a sign-in alone. A tool sets ${MINTED_TOKEN_VARIABLE} for one line at a time and unsets it once that line ends.`;
996
1019
  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.`;
997
1020
  var LOGIN_ROUTE_OPENER = "A person logs this computer in once, in a terminal of their own, and its token then stays on this computer for 30 days:";
998
- var DEPLOY_FILES_ROUTE_SENTENCE = "A folder within the caps of your tool's deploy_files call deploys now through deploy_files, with no login; past them, through mint_upload_grant and deploy's artifact.";
1021
+ var DEPLOY_ROUTES_SENTENCE = "For regular use, prefer this command wherever your tool can install and run it and reach the platform. A first simple test within the caps of your tool's deploy_files call may deploy through deploy_files, as may any folder within them where this command cannot work. Past the caps, where your tool may not install or run this command, upload a zip with mint_upload_grant, then name it as deploy's artifact.";
999
1022
  var loginRouteLines = (io2, origin, deployFiles = true) => [
1000
1023
  LOGIN_ROUTE_OPENER,
1001
1024
  ...loginLinesFor(io2, origin),
1002
- ...deployFiles ? [DEPLOY_FILES_ROUTE_SENTENCE] : [],
1025
+ ...deployFiles ? [DEPLOY_ROUTES_SENTENCE] : [],
1003
1026
  HAND_OVER_SENTENCE
1004
1027
  ];
1005
1028
  var loginRouteRefusalOf = (io2, closing) => {
@@ -1018,6 +1041,8 @@ var GRANT_TEXT = new RegExp(`${UPLOAD_GRANT_PREFIX}[A-Za-z0-9_-]*(?:\\.[A-Za-z0-
1018
1041
  var TOKEN_TEXT = new RegExp(`${MINTED_TOKEN_PREFIX}[A-Za-z0-9_-]*(?:\\.[A-Za-z0-9_-]+)*`, "g");
1019
1042
  var PROGRAM_SECRET_TEXT = new RegExp(`${TZMESH_SECRET_PREFIX}[A-Za-z0-9_-]*`, "g");
1020
1043
  var REDACTED_PROGRAM_SECRET = "<program secret>";
1044
+ var WEBHOOK_SECRET_TEXT = new RegExp(`${TZMESH_WEBHOOK_SECRET_PREFIX}[A-Za-z0-9_-]*`, "g");
1045
+ var REDACTED_WEBHOOK_SECRET = "<webhook signing secret>";
1021
1046
  var REDACTED_GRANT = "<grant>";
1022
1047
  var REDACTED_VALUE = "<value>";
1023
1048
  var REDACTED_TOKEN_CODE = "<token code>";
@@ -1025,7 +1050,7 @@ var REDACTED_VERIFIER = "<verifier>";
1025
1050
  var REDACTED_CHALLENGE = "<challenge>";
1026
1051
  var HIDDEN_TEXT_MIN = 8;
1027
1052
  var redactGrants = (line) => line.replace(GRANT_TEXT, REDACTED_GRANT);
1028
- var redactCredentials = (line) => redactGrants(line).replace(TOKEN_TEXT, REDACTED_TOKEN).replace(PROGRAM_SECRET_TEXT, REDACTED_PROGRAM_SECRET);
1053
+ var redactCredentials = (line) => redactGrants(line).replace(TOKEN_TEXT, REDACTED_TOKEN).replace(PROGRAM_SECRET_TEXT, REDACTED_PROGRAM_SECRET).replace(WEBHOOK_SECRET_TEXT, REDACTED_WEBHOOK_SECRET);
1029
1054
  var VALUE_LINE_REFUSED = Object.freeze({
1030
1055
  twice: "the value line is printed once in a run",
1031
1056
  form: "only a value of a form token claim prints is printed whole",
@@ -1110,6 +1135,8 @@ var firstActNamed = (kind, origin = DEFAULT_COMMAND_ORIGIN) => kind === "tzmesh"
1110
1135
  ) : `${kind === "authorized computer" ? AUTHORIZE_SUBCOMMAND : `${TOKEN_SUBCOMMAND} ${TOKEN_REQUEST_ACT}`}${originWordsOf(origin)}`;
1111
1136
  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.`;
1112
1137
  var loginWaySentence = (loginLines) => `A person can instead log this computer in themselves, in a terminal of their own, by running ${loginLines.join(", then ")}, and approving the login in the browser.`;
1138
+ var AUTHORIZE_HAND_OVER_SENTENCE = "If your tool's own safety check refuses the answered line, a deploy can still go through a route that does not require the turnzero command. That is deploy_files within its caps or, past them where your tool can zip and upload the folder, mint_upload_grant followed by 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.";
1139
+ var TZMESH_HAND_OVER_SENTENCE = "If your tool's own safety check refuses the answered line, give the person that exact line to run once in their own terminal, never a rewritten one.";
1113
1140
  var OPTIONS_LINE_HEAD = "options: ";
1114
1141
  var tzmeshFirstActLines = (origin, pending, completing2) => [
1115
1142
  `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.`,
@@ -1120,7 +1147,7 @@ var tzmeshFirstActLines = (origin, pending, completing2) => [
1120
1147
  `${OPTIONS_LINE_HEAD}${completing2}`,
1121
1148
  `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.`,
1122
1149
  `The request is pending until ${pending.expires_at}: ${firstActNamed("tzmesh", origin)} run again with the same options before then prints this challenge again.`,
1123
- HAND_OVER_SENTENCE
1150
+ TZMESH_HAND_OVER_SENTENCE
1124
1151
  ];
1125
1152
  var firstActLines = (origin, kind, pending, label, loginLines = [], completing2 = "") => kind === "tzmesh" ? tzmeshFirstActLines(origin, pending, completing2) : [
1126
1153
  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.`,
@@ -1132,7 +1159,7 @@ var firstActLines = (origin, kind, pending, label, loginLines = [], completing2
1132
1159
  ...mintCallMembers(kind, pending.challenge, label).map(([name, value]) => ` ${name}: ${JSON.stringify(value)}`),
1133
1160
  kind === "authorized computer" ? `The line it answers holds no code. It keeps the token it mints in this computer's credential file and revokes the one this computer held there before. The token is account-scoped and works from any computer until it expires or revoke_token ends it, and list_tokens shows it by its label. The request is pending until ${pending.expires_at}: ${firstActNamed(kind, origin)} run again before then prints this challenge again.` : `It answers a ${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} line, which is yours to run in a terminal outside the AI coding tool, on this computer as this user, and never through the tool. That line prints the token's value once to standard output, so pipe it into your secret store's command where the token is a job's. The request is pending until ${pending.expires_at}: ${firstActNamed(kind, origin)} run again before then prints this challenge again.`,
1134
1161
  ...kind === "authorized computer" && loginLines.length > 0 ? [loginWaySentence(loginLines)] : [],
1135
- ...kind === "authorized computer" ? [HAND_OVER_SENTENCE] : []
1162
+ ...kind === "authorized computer" ? [AUTHORIZE_HAND_OVER_SENTENCE] : []
1136
1163
  ];
1137
1164
  var reusedRequestSentence = (origin, pending) => `A request for ${origin} is pending until ${pending.expires_at}, so its challenge is printed again and no new verifier was made.`;
1138
1165
  var replacedRequestSentence = (origin, expired2) => `The request pending for ${origin} expired at ${expired2.expires_at}, so a new one replaces it. A line minted against the old challenge is refused at its exchange, and a ${MINT_TOKEN_ACTION} call naming the new challenge below answers a fresh one.`;
@@ -1257,6 +1284,16 @@ var mintedTokenOf = (answer, kind = "authorized computer") => {
1257
1284
  var mintAgainSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN, login2 = null) => login2 !== null ? `Run ${login2} again to log this computer in.` : `Call ${MINT_TOKEN_ACTION} from your tool again with the challenge ${firstActNamed(kind, origin)} prints, and run the line it answers.`;
1258
1285
  var exchangeUnknownSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN, login2 = null) => `Whether a token was minted is unknown: list_tokens from your tool shows each token by its label, and revoke_token ends one nothing holds. ${mintAgainSentence(kind, origin, login2)}`;
1259
1286
  var noPendingSentence = (origin, kind, presented = "code") => `No request is pending for ${origin} on this computer, so ${presented === "mint" ? "nothing was sent: the verifier the mint needs" : "the token code was not presented and nothing was sent: the verifier it needs"} is kept only on the computer whose ${firstActNamed(kind)} printed its challenge. Run ${firstActNamed(kind, origin)} on this computer, call ${MINT_TOKEN_ACTION} from your tool with the challenge it prints, and run the line it answers.`;
1287
+ var otherLivingRequestOf = (pending, origin, now) => {
1288
+ let found = null;
1289
+ for (const [other, entry] of pending) {
1290
+ const expires = Date.parse(entry.expires_at);
1291
+ if (other === origin || !Number.isFinite(expires) || expires <= now.getTime()) continue;
1292
+ if (found === null || expires > found.expires) found = { origin: other, expires };
1293
+ }
1294
+ return found?.origin ?? null;
1295
+ };
1296
+ var elsewhereRequestSentence = (origin, other) => `A request is pending on this computer for ${other} instead, so the first act ran against ${other}. Where the mint was made at ${origin}, run the first act above. Where it was made at ${other}, run the line its ${MINT_TOKEN_ACTION} call answered exactly as given.`;
1260
1297
  var markerDisagreesSentence = (kind, origin = DEFAULT_COMMAND_ORIGIN) => kind === "authorized computer" ? `The exchange minted a token that is not an authorized computer's, and ${AUTHORIZE_SUBCOMMAND} keeps only an authorized computer's, so nothing of its value was printed or kept. Call ${MINT_TOKEN_ACTION} from your tool again naming ${AUTHORIZED_COMPUTER_MEMBER}: true, as ${AUTHORIZE_SUBCOMMAND} prints it, and run the line it answers.` : `The exchange minted an authorized computer's token, and ${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} prints only a token that is not one, so nothing of its value was printed or kept. Call ${MINT_TOKEN_ACTION} from your tool again without ${AUTHORIZED_COMPUTER_MEMBER}, as ${TOKEN_SUBCOMMAND} ${TOKEN_REQUEST_ACT} prints it, and run the line it answers; to authorize this computer, run ${AUTHORIZE_SUBCOMMAND}${originWordsOf(origin)}.`;
1261
1298
  var unclaimableFormSentence = (origin = DEFAULT_COMMAND_ORIGIN) => `The exchange minted a token whose value is of no form ${TOKEN_SUBCOMMAND} ${TOKEN_CLAIM_ACT} prints, so nothing of its value was printed or kept. ${mintAgainSentence("general", origin)}`;
1262
1299
  var mintedRevocationLines = (revocation, minted) => "lines" in revocation ? [...revocation.lines, standsSentence(`The token it minted, ${minted.id},`, minted.id, minted.expiresAt)] : [`The token it minted, ${minted.id}, is revoked.`];
@@ -1301,6 +1338,8 @@ async function completeAct(io2, origin, kind, presentation, userAgent, say, deli
1301
1338
  const pending = read2.state.pending.get(origin);
1302
1339
  if (pending === void 0) {
1303
1340
  say(noPendingSentence(origin, kind, held ? "mint" : "code"));
1341
+ const other = otherLivingRequestOf(read2.state.pending, origin, io2.clock());
1342
+ if (other !== null) say(elsewhereRequestSentence(origin, other));
1304
1343
  return COMMAND_STATUS.nothing_started;
1305
1344
  }
1306
1345
  io2.hide(pending.verifier, REDACTED_VERIFIER);
@@ -2626,7 +2665,14 @@ var localWriteLineIn = (data) => {
2626
2665
  var localWriteLineOf = (found) => found === null ? null : `${WARNING_LINE_HEAD}${shownNoteName(found.name)}, line ${found.line}, writes to a local file inside the application's folder. A running copy's disk is lost whenever the copy is replaced or stops: keep files in a storage area and records in a database, as read_documentation's pages /cloud/guides/store-files/ and /cloud/guides/add-a-database/ describe.`;
2627
2666
  var LIST_LINE_HEAD = "file: ";
2628
2667
  var listLineOf = (file) => `${LIST_LINE_HEAD}${file.size} ${shownWhole(file.name)}`;
2629
- var NO_NOTES = Object.freeze({ leftOut: Object.freeze([]), rootZips: Object.freeze([]), rootRunOutput: Object.freeze([]), credentialFiles: Object.freeze([]), localWrite: null, unservedHealthPath: null, nearestHealthPath: null });
2668
+ var NO_NOTES = Object.freeze({ leftOut: Object.freeze([]), rootZips: Object.freeze([]), rootRunOutput: Object.freeze([]), credentialFiles: Object.freeze([]), localWrite: null, unservedHealthPath: null, nearestHealthPath: null, gitignored: Object.freeze([]) });
2669
+ var gitignoredLineOf = (entries) => entries.length === 0 ? null : `note: .gitignore lists ${entries.length === 1 ? "a file" : `${entries.length} files`} the upload includes, since the zip never honours .gitignore: a build's output is often listed there, and the image build runs no build script. ${listedPairs(entries)}. To leave a path out, list it in .deployignore at the folder's root.`;
2670
+ var listedPairs = (entries) => {
2671
+ const sorted = [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2672
+ const shown2 = sorted.slice(0, NOTE_NAMES_MAX).map((entry) => `${shownNoteName(entry.name)} (${shownNoteName(entry.pattern)})`).join(", ");
2673
+ const rest = sorted.length - NOTE_NAMES_MAX;
2674
+ return rest > 0 ? `${shown2}, and ${rest} more` : shown2;
2675
+ };
2630
2676
  var noteLinesOf = (notes, health, zipped = null) => {
2631
2677
  const lines = [];
2632
2678
  const files = zipped === null ? null : filesLineOf(zipped.map((file) => file.name));
@@ -2641,6 +2687,8 @@ var noteLinesOf = (notes, health, zipped = null) => {
2641
2687
  if (credential !== null) lines.push(credential);
2642
2688
  const localWrite = localWriteLineOf(notes.localWrite);
2643
2689
  if (localWrite !== null) lines.push(localWrite);
2690
+ const gitignored = gitignoredLineOf(notes.gitignored ?? []);
2691
+ if (gitignored !== null) lines.push(gitignored);
2644
2692
  if (health && notes.unservedHealthPath !== null) {
2645
2693
  lines.push(healthLineOf(notes.unservedHealthPath));
2646
2694
  if (notes.nearestHealthPath !== null) lines.push(nearestHealthLineOf(notes.nearestHealthPath));
@@ -3512,6 +3560,96 @@ import { readdirSync, readFileSync as readFileSync3, statSync as statSync2 } fro
3512
3560
  import { join as join5 } from "node:path";
3513
3561
  import * as zlib from "node:zlib";
3514
3562
  import { deflateRawSync, inflateRawSync as inflateRawSync2 } from "node:zlib";
3563
+
3564
+ // src/turnzero_cloud_command/ignore_rules.ts
3565
+ var DEPLOY_IGNORE_FILE = ".deployignore";
3566
+ var GIT_IGNORE_FILE = ".gitignore";
3567
+ var matcherOf = (body, anchored) => {
3568
+ let source = "";
3569
+ for (let i2 = 0; i2 < body.length; i2 += 1) {
3570
+ const c = body[i2];
3571
+ if (c === "\\" && i2 + 1 < body.length) {
3572
+ source += body[i2 + 1].replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
3573
+ i2 += 1;
3574
+ } else if (c === "*" && body[i2 + 1] === "*") {
3575
+ const before = i2 === 0 || body[i2 - 1] === "/";
3576
+ const after = i2 + 2 === body.length || body[i2 + 2] === "/";
3577
+ if (before && after) {
3578
+ if (i2 + 2 === body.length) source += ".*";
3579
+ else {
3580
+ source += "(?:.*/)?";
3581
+ i2 += 1;
3582
+ }
3583
+ i2 += 1;
3584
+ } else {
3585
+ source += "[^/]*";
3586
+ i2 += 1;
3587
+ }
3588
+ } else if (c === "*") source += "[^/]*";
3589
+ else if (c === "?") source += "[^/]";
3590
+ else if (c === "[") {
3591
+ const found = classOf(body, i2);
3592
+ if (found === null) source += "\\[";
3593
+ else {
3594
+ source += found.source;
3595
+ i2 = found.end;
3596
+ }
3597
+ } else source += c.replace(/[.+^${}()|\\]/g, "\\$&");
3598
+ }
3599
+ return new RegExp(anchored ? `^${source}$` : `^(?:.*/)?${source}$`);
3600
+ };
3601
+ var classOf = (body, open) => {
3602
+ let i2 = open + 1;
3603
+ const negated = body[i2] === "!" || body[i2] === "^";
3604
+ if (negated) i2 += 1;
3605
+ let inner = "";
3606
+ for (let first = true; i2 < body.length; i2 += 1, first = false) {
3607
+ const c = body[i2];
3608
+ if (c === "]" && !first) return { source: negated ? `[^/${inner}]` : `(?!/)[${inner}]`, end: i2 };
3609
+ if (c === "\\" && i2 + 1 < body.length) {
3610
+ i2 += 1;
3611
+ inner += inClass(body[i2]);
3612
+ } else inner += c === "-" ? c : inClass(c);
3613
+ }
3614
+ return null;
3615
+ };
3616
+ var inClass = (c) => c.replace(/[\\\]\[^-]/g, "\\$&");
3617
+ function ignoreRulesOf(text) {
3618
+ const rules = [];
3619
+ for (const [index, raw] of text.split(/\r?\n/).entries()) {
3620
+ let line = raw.replace(/(?<!\\) +$/, "");
3621
+ if (line === "" || line.startsWith("#") || /(?:^|[^\\])(?:\\\\)*\\$/.test(line)) continue;
3622
+ const negated = line.startsWith("!");
3623
+ if (negated) line = line.slice(1);
3624
+ else if (line.startsWith("\\!") || line.startsWith("\\#")) line = line.slice(1);
3625
+ const folderOnly = line.endsWith("/");
3626
+ let body = folderOnly ? line.slice(0, -1) : line;
3627
+ const anchored = body.includes("/");
3628
+ if (body.startsWith("/")) body = body.slice(1);
3629
+ if (body === "") continue;
3630
+ rules.push({ pattern: raw.trim(), line: index + 1, negated, folderOnly, matcher: matcherOf(body, anchored) });
3631
+ }
3632
+ return rules;
3633
+ }
3634
+ function ignoringRuleOf(rules, path, folder) {
3635
+ if (rules.length === 0) return null;
3636
+ const segments = path.split("/");
3637
+ for (let depth = 1; depth < segments.length; depth += 1) {
3638
+ const ancestor = ruleAt(rules, segments.slice(0, depth).join("/"), true);
3639
+ if (ancestor !== null) return ancestor;
3640
+ }
3641
+ return ruleAt(rules, path, folder);
3642
+ }
3643
+ var ruleAt = (rules, path, folder) => {
3644
+ let decided = null;
3645
+ for (const rule of rules) {
3646
+ if (rule.folderOnly && !folder) continue;
3647
+ if (rule.matcher.test(path)) decided = rule.negated ? null : rule;
3648
+ }
3649
+ return decided;
3650
+ };
3651
+
3652
+ // src/turnzero_cloud_command/zip.ts
3515
3653
  var AI_TOOL_NAMES = Object.freeze([
3516
3654
  ".claude",
3517
3655
  ".mcp.json",
@@ -3537,14 +3675,23 @@ var ZIP_VERSION = 20;
3537
3675
  var ZIP32_MAX = 4294967295;
3538
3676
  var ZipRefusal = class extends Error {
3539
3677
  };
3540
- var walkFolder = (root, limit, leftOut) => {
3678
+ var rootIgnoreRulesOf = (root, name) => {
3679
+ try {
3680
+ const path = join5(root, name);
3681
+ return statSync2(path).isFile() ? ignoreRulesOf(readFileSync3(path, "utf8")) : [];
3682
+ } catch {
3683
+ return [];
3684
+ }
3685
+ };
3686
+ var walkFolder = (root, limit, leftOut, ignore = []) => {
3541
3687
  const found = [];
3542
3688
  const pending = [{ path: root, name: "" }];
3543
3689
  while (pending.length > 0) {
3544
3690
  const folder = pending.pop();
3545
3691
  for (const entry of readdirSync(folder.path, { withFileTypes: true })) {
3546
3692
  const name = folder.name === "" ? entry.name : `${folder.name}/${entry.name}`;
3547
- if (leftOutName(entry.name)) {
3693
+ const kept2 = folder.name === "" && (entry.name === "package.json" || entry.name === MANIFEST_FILE);
3694
+ if (leftOutName(entry.name) || !kept2 && ignoringRuleOf(ignore, name, entry.isDirectory()) !== null) {
3548
3695
  leftOut?.push(entry.isDirectory() ? `${name}/` : name);
3549
3696
  continue;
3550
3697
  }
@@ -3644,7 +3791,9 @@ function zipFolder(root, seams = {}) {
3644
3791
  }
3645
3792
  if (!rootFile) throw new ZipRefusal(NO_PACKAGE_SENTENCE);
3646
3793
  const leftOut = [];
3647
- const files = (seams.walk ?? walkFolder)(root, ARTIFACT_ENTRIES_MAX + 1, leftOut);
3794
+ const deployIgnore = rootIgnoreRulesOf(root, DEPLOY_IGNORE_FILE);
3795
+ const gitIgnore = rootIgnoreRulesOf(root, GIT_IGNORE_FILE);
3796
+ const files = (seams.walk ?? walkFolder)(root, ARTIFACT_ENTRIES_MAX + 1, leftOut, deployIgnore);
3648
3797
  if (files.length > ARTIFACT_ENTRIES_MAX) throw new ZipRefusal(entryBoundSentence());
3649
3798
  const manifest = files.find((file) => file.name === MANIFEST_FILE);
3650
3799
  let healthPath = null;
@@ -3692,7 +3841,11 @@ function zipFolder(root, seams = {}) {
3692
3841
  credentialFiles,
3693
3842
  localWrite,
3694
3843
  unservedHealthPath: segment !== null && !held ? healthPath : null,
3695
- nearestHealthPath: segment !== null && !held && healthPath !== null ? nearestHealthPathOf(healthLike, healthPath) : null
3844
+ nearestHealthPath: segment !== null && !held && healthPath !== null ? nearestHealthPathOf(healthLike, healthPath) : null,
3845
+ gitignored: files.flatMap((file) => {
3846
+ const rule = ignoringRuleOf(gitIgnore, file.name, false);
3847
+ return rule === null ? [] : [{ name: file.name, pattern: rule.pattern }];
3848
+ })
3696
3849
  }
3697
3850
  };
3698
3851
  }
@@ -4139,8 +4292,9 @@ var DEPLOY_ROW = Object.freeze({
4139
4292
  note: [
4140
4293
  `A deploy is one line, which carries no credential: run the line your tool's deploy call answers, or ${DEPLOY_SUBCOMMAND} ${DEPLOY_OPTION.application} <application id>, from the application's folder. It reads no standard input. On an authorized computer it runs under this computer's token, which ${LOGIN_SUBCOMMAND} or ${AUTHORIZE_SUBCOMMAND} keeps in the credential file for each platform address. It sends that token only to the address it is kept under: ${DEPLOY_OPTION.origin} where the line names one, else the address ${ORIGIN_VARIABLE} holds, else ${DEFAULT_COMMAND_ORIGIN}.`,
4141
4294
  `On a computer with no token for that address, or one that has expired, it sends nothing, ends 3, and prints two routes, in this order. A tool connected by a minted token, or a job with no tool, such as a CI job's, sets ${MINTED_TOKEN_VARIABLE} from where its token is kept, never by writing the value in a line. It sets ${ORIGIN_VARIABLE} to the address that minted the token where that is not ${DEFAULT_COMMAND_ORIGIN}, then runs the deploy line again. Such a tool cannot use ${MINT_TOKEN_ACTION}, which takes a sign-in alone.`,
4142
- `Otherwise a person logs this computer in once with ${LOGIN_SUBCOMMAND}, in a terminal of their own, then runs the deploy line again. The credential file is ${CREDENTIAL_FILE_WORDS}.`,
4143
- `${DEPLOY_FILES_ROUTE_SENTENCE} ${HAND_OVER_SENTENCE}`,
4295
+ `The second route authorizes this computer, and the deploy line then runs again. A tool signed in to the platform may do it itself with ${AUTHORIZE_SUBCOMMAND}, and the run prints the person's way, ${LOGIN_SUBCOMMAND}, run once in a terminal of their own. The credential file is ${CREDENTIAL_FILE_WORDS}.`,
4296
+ DEPLOY_ROUTES_SENTENCE,
4297
+ HAND_OVER_SENTENCE,
4144
4298
  `A tool sets ${MINTED_TOKEN_VARIABLE} for one line at a time and unsets it once that line ends, since the command cannot unset a variable of the shell that started it. A value a job's environment already supplies stays as it stands. A job's token is minted for the application, with an expiry. The variable's token wins over the credential file: the command makes the preparing call under that token, and sends the token in no other request.`,
4145
4299
  `Under the variable, the command sends its token only to the address ${ORIGIN_VARIABLE} holds, or to ${DEFAULT_COMMAND_ORIGIN} where that variable is unset. Under the token, ${DEPLOY_OPTION.origin} may name that address alone, and where the line names none the command addresses it. ${DEPLOY_OPTION.env} names the environment where a line names one, ${COMMAND_ENVIRONMENTS.join(" or ")}. Where none is named, the platform chooses, and the command deploys to the environment the platform answers.`,
4146
4300
  `A progress read answered 502, 503, or 504, which is often temporary, is made again, at most three more times. An upload answered 503 credential_lookup_busy, which wrote nothing, is made again after the wait it names, at most twice.`,
@@ -6242,7 +6396,7 @@ var whoamiLinesOf = (origin, account, entry) => [
6242
6396
  `${WHOAMI_LINE_HEADS.token}label ${entry.label !== null ? JSON.stringify(entry.label) : "none"}, expiring ${entry.expires_at !== null ? `at ${entry.expires_at}` : "never"}`
6243
6397
  ];
6244
6398
  var noComputerTokenSentence = (origin, expiredAt) => expiredAt === null ? `This computer's credential file holds no token for ${origin}, so nothing was sent.` : `This computer's token for ${origin} in its credential file expired at ${expiredAt}, so nothing was sent.`;
6245
- var noLoginRoutesSentence = (origin) => `Your AI coding tool's own sign-in is not this computer's credential. With no login, deploy_files deploys a folder within its caps; past them, upload a zip with mint_upload_grant, then name it as deploy's artifact. ${PROVISION_SUBCOMMAND}${originWordsOf(origin)} run with nothing piped prints the call that answers its line.`;
6399
+ var noLoginRoutesSentence = (origin) => `Your AI coding tool's own sign-in is not this computer's credential. For regular use, prefer this command where it works. A signed-in tool can authorize this computer with ${AUTHORIZE_SUBCOMMAND}${originWordsOf(origin)}; the login route below is the person's alternative. A first simple test within the caps of your tool's deploy_files call may deploy through deploy_files with no login, as may any folder within them where this command cannot work. Past the caps, or for a file deploy_files cannot carry, where this command cannot run, upload a zip with mint_upload_grant, then name it as deploy's artifact. ${PROVISION_SUBCOMMAND}${originWordsOf(origin)} run with nothing piped prints the call that answers its line.`;
6246
6400
  var whoamiAfterLoginSentence = (whoami2) => `Once login has printed authorized:, run ${whoami2} again.`;
6247
6401
  async function whoami(call, io2) {
6248
6402
  const origin = entryOriginOf(io2.env, call.named[DEPLOY_OPTION.origin], NOTHING_SENT_OR_READ);
@@ -6298,7 +6452,7 @@ async function whoami(call, io2) {
6298
6452
  }
6299
6453
  var WHOAMI_ROW = Object.freeze({
6300
6454
  name: WHOAMI_SUBCOMMAND,
6301
- summary: `Names the account this computer's token acts as at the platform's address. It reads the token the credential file keeps for the address, and makes one ${READ_ACCOUNT_ACTION} call under it. It prints the address, the account's sign-in address and id, and the token's label and expiry. With no token for the address, it sends nothing. It names the ways on with no login (deploy_files, the upload route, and ${PROVISION_SUBCOMMAND}), then prints the login route. It prints no token.`,
6455
+ summary: `Names the account this computer's token acts as at the platform's address. It reads the token the credential file keeps for the address, and makes one ${READ_ACCOUNT_ACTION} call under it. It prints the address, the account's sign-in address and id, and the token's label and expiry. With no token for the address, it sends nothing. It names the routes a tool takes itself (${AUTHORIZE_SUBCOMMAND}, deploy_files, the upload route, and ${PROVISION_SUBCOMMAND}), then the person's login route. It prints no token.`,
6302
6456
  example: WHOAMI_SUBCOMMAND,
6303
6457
  options: WHOAMI_OPTIONS,
6304
6458
  credential: null,
@@ -9761,6 +9915,43 @@ var TZMESH_OPTIONS = Object.freeze([
9761
9915
  required: false,
9762
9916
  description: `Under ${TZMESH_OPTION.secret}, the environment of that application whose scope holds the secret, ${COMMAND_ENVIRONMENTS.join(" or ")}. It goes with ${TZMESH_OPTION.application}.`
9763
9917
  },
9918
+ {
9919
+ name: TZMESH_OPTION.webhookOrigin,
9920
+ value: "<origin>",
9921
+ required: false,
9922
+ description: `Under ${TZMESH_OPTION.secret}, the https origin of the application that hosts the agent. The run sets the agent's webhook to ${TZMESH_WEBHOOK_ROUTE} at that origin, and stores its signing secret under ${TZMESH_OPTION.webhookSecret}.`
9923
+ },
9924
+ {
9925
+ name: TZMESH_OPTION.webhookSecret,
9926
+ value: "<secret name>",
9927
+ required: false,
9928
+ 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.`
9929
+ },
9930
+ {
9931
+ name: TZMESH_OPTION.description,
9932
+ value: "<text>",
9933
+ required: false,
9934
+ description: `Under ${TZMESH_OPTION.secret}, the agent's description, at most ${TZMESH_DESCRIPTION_MAX} characters.`
9935
+ },
9936
+ {
9937
+ name: TZMESH_OPTION.title,
9938
+ value: "<text>",
9939
+ required: false,
9940
+ description: `Under ${TZMESH_OPTION.secret}, the agent's title, at most ${TZMESH_TITLE_MAX} characters.`
9941
+ },
9942
+ {
9943
+ name: TZMESH_OPTION.join,
9944
+ value: "<channels>",
9945
+ required: false,
9946
+ description: `Under ${TZMESH_OPTION.secret}, the channels the agent joins, as one value: channel ids, #names, or account/#names, separated by commas.`
9947
+ },
9948
+ {
9949
+ name: TZMESH_OPTION.public,
9950
+ value: "",
9951
+ required: false,
9952
+ flag: true,
9953
+ description: `Under ${TZMESH_OPTION.secret}, makes the agent public at its register. Whether it is public is your choice: a tool passes this only after you answer.`
9954
+ },
9764
9955
  {
9765
9956
  name: TZMESH_OPTION.force,
9766
9957
  value: "",
@@ -9787,7 +9978,69 @@ var TZMESH_OPTIONS = Object.freeze([
9787
9978
  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.`
9788
9979
  }
9789
9980
  ]);
9790
- var UNPRINTABLE_PATH = /["$`%\u0000-\u001f\u007f]/;
9981
+ var SETUP_OPTIONS = [TZMESH_OPTION.webhookOrigin, TZMESH_OPTION.webhookSecret, TZMESH_OPTION.description, TZMESH_OPTION.title, TZMESH_OPTION.join];
9982
+ var UNCARRIED_WORDS = "a double quote, $, a backtick, %, !, &, |, <, >, ^, a curly double quote, a doubled or trailing backslash, or a control character";
9983
+ var JOIN_CHANNEL_FORM = /^[^\s,]{1,200}$/;
9984
+ var WEBHOOK_ORIGIN_FORM = /^https:\/\/[a-z0-9.-]+(?::[0-9]{1,5})?$/;
9985
+ var isHttpsOrigin = (value) => {
9986
+ if (!WEBHOOK_ORIGIN_FORM.test(value)) return false;
9987
+ try {
9988
+ const url = new URL(value);
9989
+ return url.protocol === "https:" && url.origin === value && url.username === "" && url.password === "";
9990
+ } catch {
9991
+ return false;
9992
+ }
9993
+ };
9994
+ var setupOptionsOf = (call, toFile, closing) => {
9995
+ const named = (option) => call.named[option] ?? null;
9996
+ const isPublic = call.flags.has(TZMESH_OPTION.public);
9997
+ if (toFile) {
9998
+ const given = [...SETUP_OPTIONS.filter((option) => named(option) !== null), ...isPublic ? [TZMESH_OPTION.public] : []];
9999
+ if (given.length > 0) return { refusal: `${given[0]} goes with ${TZMESH_OPTION.secret} alone: an agent whose secret goes into a file is set up by its own program. ${closing}` };
10000
+ }
10001
+ const webhookOrigin = named(TZMESH_OPTION.webhookOrigin);
10002
+ const webhookSecret = named(TZMESH_OPTION.webhookSecret);
10003
+ if (webhookOrigin === null !== (webhookSecret === null)) {
10004
+ return { refusal: `${TZMESH_OPTION.webhookOrigin} and ${TZMESH_OPTION.webhookSecret} go together: the webhook's address, and the name its signing secret is stored under. ${closing}` };
10005
+ }
10006
+ if (webhookOrigin !== null && !isHttpsOrigin(webhookOrigin)) {
10007
+ return {
10008
+ 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}`
10009
+ };
10010
+ }
10011
+ if (webhookSecret !== null && !SECRET_NAME_FORM.test(webhookSecret)) {
10012
+ 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}` };
10013
+ }
10014
+ const secret2 = named(TZMESH_OPTION.secret);
10015
+ if (webhookSecret !== null && secret2 !== null && webhookSecret.toLowerCase() === secret2.toLowerCase()) {
10016
+ 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}` };
10017
+ }
10018
+ const bounded = (option, max) => {
10019
+ const value = named(option);
10020
+ if (value === null) return null;
10021
+ if (value === "" || [...value].length > max) return { refusal: `${option} takes 1 to ${max} characters. ${closing}` };
10022
+ if (!lineValueCarries(value)) return { refusal: `${option} holds a character a line cannot carry alike in every shell between double quotes: ${UNCARRIED_WORDS}. ${closing}` };
10023
+ return value;
10024
+ };
10025
+ const description = bounded(TZMESH_OPTION.description, TZMESH_DESCRIPTION_MAX);
10026
+ if (description !== null && typeof description === "object") return description;
10027
+ const title = bounded(TZMESH_OPTION.title, TZMESH_TITLE_MAX);
10028
+ if (title !== null && typeof title === "object") return title;
10029
+ const joinValue = named(TZMESH_OPTION.join);
10030
+ const join12 = joinValue === null ? [] : joinValue.split(",");
10031
+ if (joinValue !== null && (join12.some((channel) => !JOIN_CHANNEL_FORM.test(channel)) || !lineValueCarries(joinValue))) {
10032
+ return {
10033
+ refusal: `${TZMESH_OPTION.join} lists the channels the agent joins, as one value: ids, #names, or account/#names, separated by commas, with no space and nothing a line cannot carry between double quotes: ${UNCARRIED_WORDS}. ${closing}`
10034
+ };
10035
+ }
10036
+ return {
10037
+ webhook: webhookOrigin !== null && webhookSecret !== null ? { origin: webhookOrigin, secretName: webhookSecret } : null,
10038
+ description,
10039
+ title,
10040
+ join: join12,
10041
+ public: isPublic
10042
+ };
10043
+ };
9791
10044
  var closingOf = (call) => call.named[TZMESH_OPTION.mint] !== void 0 ? MINT_UNPRESENTED : NOTHING_SENT;
9792
10045
  var TZMESH_MINT_ID_REFUSAL = `${TZMESH_OPTION.mint} names the mint by its id, a lowercase UUID, as the line the ${MINT_TOKEN_ACTION} call answered names it: run that line as given, with the options the first act printed after it. ${NOTHING_SENT}`;
9793
10046
  var tzmeshOriginRefusal = (closing) => `${TZMESH_OPTION.origin} names the platform's address the secret is stored at, and ${TZMESH_ACT_WORDS} reaches only ${DEFAULT_COMMAND_ORIGIN} or ${DEVELOPMENT_COMMAND_ORIGIN}. Where the line names none, ${ORIGIN_VARIABLE} must hold one of those two, or be unset. ${closing}`;
@@ -9820,9 +10073,11 @@ var tzmeshOptionsOf = (call, io2) => {
9820
10073
  const force = call.flags.has(TZMESH_OPTION.force);
9821
10074
  if (force && toFile === void 0) return { refusal: `${TZMESH_OPTION.force} goes with ${TZMESH_OPTION.toFile} alone: it replaces a file that stands there. ${closing}` };
9822
10075
  const path = toFile !== void 0 ? resolve13(io2.cwd, toFile) : null;
9823
- if (path !== null && UNPRINTABLE_PATH.test(path)) {
9824
- return { refusal: `${TZMESH_OPTION.toFile} names a path holding a character a line cannot carry between double quotes: a double quote, $, a backtick, %, or a control character. Name a path without one. ${closing}` };
10076
+ if (path !== null && !lineValueCarries(path)) {
10077
+ 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}` };
9825
10078
  }
10079
+ const setup = setupOptionsOf(call, toFile !== void 0, closing);
10080
+ if ("refusal" in setup) return setup;
9826
10081
  const origin = entryOriginOf(io2.env, call.named[TZMESH_OPTION.origin], closing);
9827
10082
  if ("refusal" in origin) return origin;
9828
10083
  if (!FIRST_PARTY_COMMAND_ORIGINS.includes(origin.origin)) return { refusal: tzmeshOriginRefusal(closing) };
@@ -9837,7 +10092,8 @@ var tzmeshOptionsOf = (call, io2) => {
9837
10092
  force,
9838
10093
  origin: origin.origin,
9839
10094
  estate,
9840
- mint
10095
+ mint,
10096
+ ...setup
9841
10097
  };
9842
10098
  };
9843
10099
  var completingOptionsOf = (options) => {
@@ -9846,6 +10102,11 @@ var completingOptionsOf = (options) => {
9846
10102
  ..."secret" in options.route ? { secret: options.route.secret } : { toFile: options.route.toFile },
9847
10103
  ...options.application !== null ? { application: options.application } : {},
9848
10104
  ...options.environment !== null ? { environment: options.environment } : {},
10105
+ ...options.webhook !== null ? { webhookOrigin: options.webhook.origin, webhookSecret: options.webhook.secretName } : {},
10106
+ ...options.description !== null ? { description: options.description } : {},
10107
+ ...options.title !== null ? { title: options.title } : {},
10108
+ ...options.join.length > 0 ? { join: options.join } : {},
10109
+ ...options.public ? { public: true } : {},
9849
10110
  ...options.force ? { force: true } : {},
9850
10111
  origin: options.origin,
9851
10112
  tzmeshEstate: options.estate
@@ -9930,13 +10191,20 @@ var postWire = async (io2, wire, path, bearer, userAgent, body) => {
9930
10191
  cancel();
9931
10192
  }
9932
10193
  };
9933
- var register = async (io2, wire, token2, userAgent, name) => {
9934
- const answered = await postWire(io2, wire, TZMESH_REGISTER_PATH, token2, userAgent, { kind: "standing", name, label: TZMESH_COMMAND_LABEL });
10194
+ var register = async (io2, wire, token2, userAgent, options) => {
10195
+ const answered = await postWire(io2, wire, TZMESH_REGISTER_PATH, token2, userAgent, {
10196
+ kind: "standing",
10197
+ name: options.name,
10198
+ label: TZMESH_COMMAND_LABEL,
10199
+ ...options.title !== null ? { title: options.title } : {},
10200
+ ...options.description !== null ? { description: options.description } : {},
10201
+ ...options.public ? { visibility: "public" } : {}
10202
+ });
9935
10203
  if (answered === null) return { unknown: [`The ${REGISTER_ACT} drew no answer.`] };
9936
10204
  if (answered.redirected) return { unknown: [`The ${REGISTER_ACT} was answered ${answeredHow(answered.status)}, which the command never follows, so the token was sent to no second address.`] };
9937
- if (answered.status >= PLATFORM_FAILURE_STATUS) return { unknown: failureLines(REGISTER_ACT, answered.status, planeRefusalOf(answered.text)) };
9938
10205
  const answer = planeObjectOf(answered.text);
9939
10206
  if (answer !== null && typeof answer.secret === "string") io2.hide(answer.secret, REDACTED_PROGRAM_SECRET);
10207
+ if (answered.status >= PLATFORM_FAILURE_STATUS) return { unknown: failureLines(REGISTER_ACT, answered.status, planeRefusalOf(answered.text)) };
9940
10208
  if (answered.status < 200 || answered.status > 299) {
9941
10209
  const refusal = planeRefusalOf(answered.text);
9942
10210
  if (refusal === null) return { unknown: [`The ${REGISTER_ACT} was answered HTTP ${answered.status}, not by the tzMesh service.`] };
@@ -9956,10 +10224,60 @@ var register = async (io2, wire, token2, userAgent, name) => {
9956
10224
  var deleteAgent = async (io2, wire, token2, userAgent, agent) => {
9957
10225
  const answered = await postWire(io2, wire, TZMESH_END_PATH, token2, userAgent, { participant: agent.id, delete: true });
9958
10226
  const deleted = answered !== null && !answered.redirected && answered.status >= 200 && answered.status <= 299 && planeObjectOf(answered.text) !== null;
9959
- if (deleted) return [`The agent ${agentWords(agent)} was deleted, and its name is free again.`];
9960
- const why = answered === null ? [`The ${END_ACT} drew no answer.`] : answered.redirected ? [`The ${END_ACT} was answered ${answeredHow(answered.status)}, which the command never follows.`] : answered.status >= PLATFORM_FAILURE_STATUS ? failureLines(END_ACT, answered.status, planeRefusalOf(answered.text)) : planeRefusalOf(answered.text) !== null ? refusalLines(END_ACT, answered.status, planeRefusalOf(answered.text)) : [`The ${END_ACT} was answered HTTP ${answered.status}, not by the tzMesh service.`];
9961
- return [...why, agentStandsSentence(agent)];
9962
- };
10227
+ if (deleted) return { deleted: true, lines: [`The agent ${agentWords(agent)} was deleted, and its name is free again.`], unknown: false };
10228
+ const refusal = answered === null || answered.redirected || answered.status >= PLATFORM_FAILURE_STATUS ? null : planeRefusalOf(answered.text);
10229
+ const why = answered === null ? [`The ${END_ACT} drew no answer.`] : answered.redirected ? [`The ${END_ACT} was answered ${answeredHow(answered.status)}, which the command never follows.`] : answered.status >= PLATFORM_FAILURE_STATUS ? failureLines(END_ACT, answered.status, planeRefusalOf(answered.text)) : refusal !== null ? refusalLines(END_ACT, answered.status, refusal) : [`The ${END_ACT} was answered HTTP ${answered.status}, not by the tzMesh service.`];
10230
+ return { deleted: false, lines: why, unknown: refusal === null };
10231
+ };
10232
+ var WEBHOOK_ACT = "webhook_set";
10233
+ var JOIN_ACT = "join";
10234
+ var WEBHOOK_LINE_HEAD = "webhook: ";
10235
+ var JOINED_LINE_HEAD = "joined: ";
10236
+ var webhookLineOf = (url) => `${WEBHOOK_LINE_HEAD}set to ${url}, its signing secret printed nowhere`;
10237
+ var setupCall = async (io2, wire, path, secret2, userAgent, act, body, hide) => {
10238
+ const answered = await postWire(io2, wire, path, secret2, userAgent, body);
10239
+ if (answered === null) return { unknown: [`The ${act} drew no answer.`] };
10240
+ if (answered.redirected) return { unknown: [`The ${act} was answered ${answeredHow(answered.status)}, which the command never follows, so the secret was sent to no second address.`] };
10241
+ const answer = planeObjectOf(answered.text);
10242
+ hide(answer);
10243
+ if (answered.status >= PLATFORM_FAILURE_STATUS) return { unknown: failureLines(act, answered.status, planeRefusalOf(answered.text)) };
10244
+ if (answered.status < 200 || answered.status > 299) {
10245
+ const refusal = planeRefusalOf(answered.text);
10246
+ if (refusal === null) return { unknown: [`The ${act} was answered HTTP ${answered.status}, not by the tzMesh service.`] };
10247
+ return { refused: refusalLines(act, answered.status, refusal) };
10248
+ }
10249
+ if (answer === null) return { unknown: [`The ${act} was answered HTTP ${answered.status}, not by the tzMesh service.`] };
10250
+ return { answer };
10251
+ };
10252
+ var setUpAgent = async (io2, wire, secret2, userAgent, options, ended) => {
10253
+ let signing = null;
10254
+ if (options.webhook !== null) {
10255
+ const url = `${options.webhook.origin}${TZMESH_WEBHOOK_ROUTE}`;
10256
+ const set = await setupCall(io2, wire, TZMESH_WEBHOOK_SET_PATH, secret2, userAgent, WEBHOOK_ACT, { url }, (answer) => {
10257
+ if (typeof answer?.signing_secret === "string") io2.hide(answer.signing_secret, REDACTED_WEBHOOK_SECRET);
10258
+ });
10259
+ if (ended()) return "ended";
10260
+ if (!("answer" in set)) return "refused" in set ? { refused: [...set.refused, "The webhook was not set."] } : set;
10261
+ const value = set.answer.signing_secret;
10262
+ if (typeof value !== "string" || !TZMESH_WEBHOOK_SECRET_FORM.test(value)) {
10263
+ return { refused: [`The ${WEBHOOK_ACT} answered no signing secret of its form, so the command keeps none.`] };
10264
+ }
10265
+ signing = value;
10266
+ io2.print(webhookLineOf(url));
10267
+ }
10268
+ for (const channel of options.join) {
10269
+ const joined = await setupCall(io2, wire, TZMESH_JOIN_PATH, secret2, userAgent, JOIN_ACT, { channel }, () => void 0);
10270
+ if (ended()) return "ended";
10271
+ if (!("answer" in joined)) return "refused" in joined ? { refused: [...joined.refused, `The agent did not join ${channel}.`] } : joined;
10272
+ io2.print(`${JOINED_LINE_HEAD}${channel}`);
10273
+ }
10274
+ return { signing };
10275
+ };
10276
+ var firstStoreStandsSentence = (secretName) => `${secretName} now holds the deleted agent's secret, which works nowhere, until the line runs again.`;
10277
+ var signingUnknownSentence = (signingName, agent) => `Whether the webhook's signing secret was stored is unknown: list_secrets from your tool shows when ${signingName} was last written. Where it was not written, end from your tool, naming participant ${agent.id} and delete true, deletes the agent ${agentWords(agent)}, and the line runs again.`;
10278
+ var unfinishableSentence = (agent) => `The agent ${agentWords(agent)} cannot be finished, since its webhook's signing secret ended with the run: end from your tool, naming participant ${agent.id} and delete true, deletes it, and the line runs again.`;
10279
+ var firstStoreUnknownSentence = (secretName) => `Whether ${secretName} was written is unknown: list_secrets from your tool shows when it was last written. Where it was, it holds the deleted agent's secret, which works nowhere, until the line runs again.`;
10280
+ var unfinishedStandsSentence = (agent, kept2) => `${kept2.known ? `${kept2.name} holds the secret of the agent ${agentWords(agent)}.` : `Whether ${kept2.name} holds the secret of the agent ${agentWords(agent)} is unknown: list_secrets from your tool shows when it was last written.`} ${unfinishableSentence(agent)}`;
9963
10281
  var revokeRunToken = async (io2, plane, bearer, id, userAgent) => {
9964
10282
  const answered = await postAction(io2, plane, REVOKE_TOKEN_ACTION, bearer, userAgent, revocationBodyOf(id));
9965
10283
  let lines;
@@ -9979,7 +10297,8 @@ var revokeRunToken = async (io2, plane, bearer, id, userAgent) => {
9979
10297
  }
9980
10298
  return [...lines, tokenStandsSentence(id)];
9981
10299
  };
9982
- var endedLineOf = (stage, plane, agent, route) => {
10300
+ var endedLineOf = (stage, plane, agent, options) => {
10301
+ const route = options.route;
9983
10302
  switch (stage) {
9984
10303
  case "unsent":
9985
10304
  return `The run was ended before its exchange. ${MINT_UNPRESENTED}`;
@@ -9990,7 +10309,11 @@ var endedLineOf = (stage, plane, agent, route) => {
9990
10309
  case "registering":
9991
10310
  return `The run was ended during the register. ${REGISTER_UNKNOWN} ${TOKEN_LEFT}`;
9992
10311
  case "registered":
9993
- return agent === null ? `The run was ended after the register. ${REGISTER_UNKNOWN} ${TOKEN_LEFT}` : "secret" in route ? `The run was ended after the register, while its secret was being stored. ${storeUnknownSentence(route.secret, agent)} ${TOKEN_LEFT}` : `The run was ended after the register, before its secret was written. ${agentStandsSentence(agent)} ${TOKEN_LEFT}`;
10312
+ return agent === null ? `The run was ended after the register. ${REGISTER_UNKNOWN} ${TOKEN_LEFT}` : "secret" in route ? options.webhook !== null ? `The run was ended after the register, while its secret was being stored. ${unfinishableSentence(agent)} ${TOKEN_LEFT}` : `The run was ended after the register, while its secret was being stored. ${storeUnknownSentence(route.secret, agent)} ${TOKEN_LEFT}` : `The run was ended after the register, before its secret was written. ${agentStandsSentence(agent)} ${TOKEN_LEFT}`;
10313
+ case "setting up":
10314
+ return agent === null ? `The run was ended after the register. ${REGISTER_UNKNOWN} ${TOKEN_LEFT}` : `The run was ended while the agent's webhook or channels were being set. tzmesh_participants from your tool shows the agent and its webhook. ${agentStandsSentence(agent)} ${TOKEN_LEFT}`;
10315
+ case "storing signing":
10316
+ return agent === null || options.webhook === null ? `The run was ended after the register. ${REGISTER_UNKNOWN} ${TOKEN_LEFT}` : `The run was ended while the webhook's signing secret was being stored. ${signingUnknownSentence(options.webhook.secretName, agent)} ${TOKEN_LEFT}`;
9994
10317
  case "stored":
9995
10318
  return `The run was ended after the agent's secret was kept, before the run's token was revoked. ${TOKEN_LEFT}`;
9996
10319
  }
@@ -10011,15 +10334,43 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
10011
10334
  for (const line of lines) io2.print(line);
10012
10335
  return Math.max(status, COMMAND_STATUS.failed);
10013
10336
  };
10014
- const deletedThen = async (minted, standing) => {
10015
- const lines = await deleteAgent(io2, wire, minted.value, userAgent, standing);
10337
+ const deletedThen = async (minted, standing, whenUnknown = COMMAND_STATUS.failed, kept2 = null) => {
10338
+ const deleted = await deleteAgent(io2, wire, minted.value, userAgent, standing);
10016
10339
  if (stage.ended) return COMMAND_STATUS.unknown;
10017
- for (const line of lines) io2.print(line);
10018
- return revokedThen(minted, COMMAND_STATUS.failed);
10340
+ for (const line of deleted.lines) io2.print(line);
10341
+ if (deleted.deleted) {
10342
+ if (kept2 !== null) io2.print(kept2.known ? firstStoreStandsSentence(kept2.name) : firstStoreUnknownSentence(kept2.name));
10343
+ } else io2.print(kept2 === null ? agentStandsSentence(standing) : unfinishedStandsSentence(standing, kept2));
10344
+ return revokedThen(minted, deleted.unknown ? whenUnknown : COMMAND_STATUS.failed);
10345
+ };
10346
+ const storeNamed = async (name, value) => {
10347
+ const action = SECRET_ACTION.store;
10348
+ const answered = await postAction(io2, options.origin, action, (tokens.store ?? tokens.mint).token, userAgent, {
10349
+ name,
10350
+ value,
10351
+ ...options.application !== null ? { application: options.application } : {},
10352
+ ...options.environment !== null ? { environment: options.environment } : {}
10353
+ });
10354
+ if (stage.ended) return "ended";
10355
+ if (answered === null) return { unknown: [`${action} drew no answer.`] };
10356
+ 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.`] };
10357
+ if (answered.status >= PLATFORM_FAILURE_STATUS) return { unknown: failureLines(action, answered.status, planeRefusalOf(answered.text)) };
10358
+ const answer = planeObjectOf(answered.text);
10359
+ if (answered.status < 200 || answered.status > 299) {
10360
+ const refusal = markedRefusalOf(answered.status, answered.text);
10361
+ if (refusal === null) return { unknown: [`${action} was answered HTTP ${answered.status}, not by the platform.`] };
10362
+ for (const line of refusalLines(action, answered.status, refusal)) io2.print(line);
10363
+ return "refused";
10364
+ }
10365
+ if (answer === null) return { unknown: [`${action} was answered HTTP ${answered.status}, not by the platform.`] };
10366
+ io2.print(`${SECRET_LINE_HEAD.store}${JSON.stringify(Object.fromEntries(SECRET_SHOWN.filter((member) => member in answer).map((member) => [member, answer[member]])))}`);
10367
+ if (typeof answer.detail === "string") io2.print(oneLine(answer.detail));
10368
+ if ("value" in answer) io2.print("The answer carried a value, which the command never prints.");
10369
+ return "stored";
10019
10370
  };
10020
10371
  const carried = {
10021
10372
  stage,
10022
- endedLine: (now) => endedLineOf(now, plane, agent, route),
10373
+ endedLine: (now) => endedLineOf(now, plane, agent, options),
10023
10374
  // The file's try, by an empty file beside it under the run's tag,
10024
10375
  // owner-only, before any request (decision 6).
10025
10376
  prepare: () => {
@@ -10039,7 +10390,7 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
10039
10390
  },
10040
10391
  deliver: async (minted) => {
10041
10392
  stage.now = "registering";
10042
- const registered = await register(io2, wire, minted.value, userAgent, options.name);
10393
+ const registered = await register(io2, wire, minted.value, userAgent, options);
10043
10394
  if (stage.ended) return COMMAND_STATUS.unknown;
10044
10395
  if ("refused" in registered) {
10045
10396
  for (const line of registered.refused) io2.print(line);
@@ -10072,34 +10423,43 @@ var completing = async (io2, options, mint, tokens, userAgent) => {
10072
10423
  io2.print(WRITTEN_LINE);
10073
10424
  return revokedThen(minted, COMMAND_STATUS.done);
10074
10425
  }
10075
- const action = SECRET_ACTION.store;
10076
- const answered = await postAction(io2, options.origin, action, (tokens.store ?? tokens.mint).token, userAgent, {
10077
- name: route.secret,
10078
- value: secret2,
10079
- ...options.application !== null ? { application: options.application } : {},
10080
- ...options.environment !== null ? { environment: options.environment } : {}
10081
- });
10082
- if (stage.ended) return COMMAND_STATUS.unknown;
10083
- const unknownStore = async (lines) => {
10084
- for (const line of lines) io2.print(line);
10426
+ let signing = null;
10427
+ if (options.webhook !== null || options.join.length > 0) {
10428
+ stage.now = "setting up";
10429
+ const set = await setUpAgent(io2, wire, secret2, userAgent, options, () => stage.ended);
10430
+ if (set === "ended" || stage.ended) return COMMAND_STATUS.unknown;
10431
+ if ("refused" in set) {
10432
+ for (const line of set.refused) io2.print(line);
10433
+ return deletedThen(minted, agent);
10434
+ }
10435
+ if ("unknown" in set) {
10436
+ for (const line of set.unknown) io2.print(line);
10437
+ return deletedThen(minted, agent, COMMAND_STATUS.unknown);
10438
+ }
10439
+ signing = set.signing;
10440
+ stage.now = "registered";
10441
+ }
10442
+ const stored = await storeNamed(route.secret, secret2);
10443
+ if (stored === "ended" || stage.ended) return COMMAND_STATUS.unknown;
10444
+ if (stored === "refused") return deletedThen(minted, agent);
10445
+ if (stored !== "stored") {
10446
+ for (const line of stored.unknown) io2.print(line);
10447
+ if (options.webhook !== null) return deletedThen(minted, agent, COMMAND_STATUS.unknown, { name: route.secret, known: false });
10085
10448
  io2.print(storeUnknownSentence(route.secret, agent));
10086
10449
  return revokedThen(minted, COMMAND_STATUS.unknown);
10087
- };
10088
- if (answered === null) return unknownStore([`${action} drew no answer.`]);
10089
- if (answered.redirected) return unknownStore([`${action} was answered ${answeredHow(answered.status)}, which the command never follows, so the value was sent to no second address.`]);
10090
- if (answered.status >= PLATFORM_FAILURE_STATUS) return unknownStore(failureLines(action, answered.status, planeRefusalOf(answered.text)));
10091
- const answer = planeObjectOf(answered.text);
10092
- if (answered.status < 200 || answered.status > 299) {
10093
- const refusal = markedRefusalOf(answered.status, answered.text);
10094
- if (refusal === null) return unknownStore([`${action} was answered HTTP ${answered.status}, not by the platform.`]);
10095
- for (const line of refusalLines(action, answered.status, refusal)) io2.print(line);
10096
- return deletedThen(minted, agent);
10097
10450
  }
10098
- if (answer === null) return unknownStore([`${action} was answered HTTP ${answered.status}, not by the platform.`]);
10451
+ if (options.webhook !== null && signing !== null) {
10452
+ stage.now = "storing signing";
10453
+ const kept2 = await storeNamed(options.webhook.secretName, signing);
10454
+ if (kept2 === "ended" || stage.ended) return COMMAND_STATUS.unknown;
10455
+ if (kept2 === "refused") return deletedThen(minted, agent, COMMAND_STATUS.failed, { name: route.secret, known: true });
10456
+ if (kept2 !== "stored") {
10457
+ for (const line of kept2.unknown) io2.print(line);
10458
+ io2.print(signingUnknownSentence(options.webhook.secretName, agent));
10459
+ return revokedThen(minted, COMMAND_STATUS.unknown);
10460
+ }
10461
+ }
10099
10462
  stage.now = "stored";
10100
- io2.print(`${SECRET_LINE_HEAD.store}${JSON.stringify(Object.fromEntries(SECRET_SHOWN.filter((member) => member in answer).map((member) => [member, answer[member]])))}`);
10101
- if (typeof answer.detail === "string") io2.print(oneLine(answer.detail));
10102
- if ("value" in answer) io2.print("The answer carried a value, which the command never prints.");
10103
10463
  return revokedThen(minted, COMMAND_STATUS.done);
10104
10464
  }
10105
10465
  };
@@ -10173,6 +10533,7 @@ var TZMESH_ROW = Object.freeze({
10173
10533
  `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.`,
10174
10534
  `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.`,
10175
10535
  `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.`,
10536
+ `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.`,
10176
10537
  `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.`
10177
10538
  ],
10178
10539
  afterOwnError: (call) => call.named[TZMESH_OPTION.mint] !== void 0 ? TZMESH_AFTER_OWN_ERROR : TZMESH_FIRST_ACT_AFTER_OWN_ERROR,
@@ -10519,7 +10880,7 @@ var SUBCOMMANDS = Object.freeze([
10519
10880
  ]);
10520
10881
  var listedSubcommands = () => SUBCOMMANDS.filter((subcommand) => subcommand.listed !== false);
10521
10882
  var SERVED = LINE_RUNNER;
10522
- var NPX_FORM_SENTENCE = `Where the command is not installed, install it with ${installLineOf(DEFAULT_COMMAND_ORIGIN)}. A person or a job may instead run the same line through npx: write ${commandRunnerOf("latest")} for ${LINE_RUNNER}. A tool where the command is not installed deploys through deploy_files within its caps and the upload route past them. For any other line it takes the deploy skill's setup steps, never the npx line.`;
10883
+ var NPX_FORM_SENTENCE = `Where the command is not installed, install it with ${installLineOf(DEFAULT_COMMAND_ORIGIN)}. A person or a job may instead run the same line through npx: write ${commandRunnerOf("latest")} for ${LINE_RUNNER}. A tool where the command is not installed chooses its deploy route. It installs the command for regular use where it can install and run it and reach the platform. It uses deploy_files within its caps for a first simple test or where the command cannot work. Past the caps, where it may not install or run the command, it uploads a zip with mint_upload_grant and names it as deploy's artifact. For any other line it takes the deploy skill's setup steps, never the npx line.`;
10523
10884
  var namedWithActs = (subcommand) => subcommand.acts ? `${subcommand.name} ${subcommand.acts.join("|")}` : subcommand.name;
10524
10885
  var shown = (option) => option.flag ? option.name : `${option.name} ${option.value}`;
10525
10886
  var listed = (names) => names.length < 3 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turnzero",
3
- "version": "0.31.1",
3
+ "version": "0.33.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",