@hot-updater/cli-tools 1.1.4 → 1.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.mts +539 -15
  2. package/dist/index.mjs +1541 -187
  3. package/package.json +5 -5
package/dist/index.mjs CHANGED
@@ -9,18 +9,19 @@ import $r, { EventEmitter } from "events";
9
9
  import Pi from "assert";
10
10
  import crypto from "crypto";
11
11
  import { Buffer as Buffer$1 } from "buffer";
12
- import { aborted, callbackify, debuglog, inspect, promisify, stripVTControlCharacters, styleText } from "node:util";
12
+ import { aborted, callbackify, debuglog, inspect, parseEnv, promisify, stripVTControlCharacters, styleText } from "node:util";
13
13
  import process$1, { execArgv, execPath, hrtime, platform, stdin, stdout } from "node:process";
14
14
  import * as l from "node:readline";
15
15
  import l__default from "node:readline";
16
16
  import tty, { ReadStream } from "node:tty";
17
- import Ei, { appendFileSync, createReadStream, createWriteStream, existsSync, lstatSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
17
+ import Ei, { appendFileSync, createReadStream, createWriteStream, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
18
18
  import path$1, { basename, dirname as dirname$1, join, posix, win32 } from "node:path";
19
19
  import { createMemoryAdapter, createStorageAdapter, getContentType, isRemoteDatabase, toolingTargetOf } from "@hot-updater/plugin-core";
20
20
  import { createHotUpdater } from "@hot-updater/server";
21
21
  import os, { constants } from "node:os";
22
22
  import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
23
23
  import fs$2 from "node:fs/promises";
24
+ import { ChildProcess, execFile, execFileSync, spawn, spawnSync } from "node:child_process";
24
25
  import As, { Duplex, PassThrough as PassThrough$1, Readable as Readable$1, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
25
26
  import { finished, pipeline } from "node:stream/promises";
26
27
  import { constants as constants$1, createBrotliCompress } from "node:zlib";
@@ -30,7 +31,6 @@ import * as Ms from "zlib";
30
31
  import Jr from "zlib";
31
32
  import no from "node:assert";
32
33
  import { fileURLToPath } from "node:url";
33
- import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
34
34
  import { scheduler, setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
35
35
  import { serialize } from "node:v8";
36
36
  import { Buffer as Buffer$2 } from "node:buffer";
@@ -21393,11 +21393,11 @@ var dist_exports = /* @__PURE__ */ __exportAll({
21393
21393
  group: () => group,
21394
21394
  groupMultiselect: () => groupMultiselect,
21395
21395
  intro: () => intro,
21396
- isCI: () => isCI,
21396
+ isCI: () => isCI$1,
21397
21397
  isCancel: () => isCancel,
21398
21398
  isTTY: () => isTTY,
21399
21399
  limitOptions: () => limitOptions,
21400
- log: () => log$1,
21400
+ log: () => log$2,
21401
21401
  multiline: () => multiline,
21402
21402
  multiselect: () => multiselect,
21403
21403
  note: () => note,
@@ -21424,7 +21424,7 @@ function isUnicodeSupported$1() {
21424
21424
  return Boolean(process$1.env.CI) || Boolean(process$1.env.WT_SESSION) || Boolean(process$1.env.TERMINUS_SUBLIME) || process$1.env.ConEmuTask === "{cmd::Cmder}" || process$1.env.TERM_PROGRAM === "Terminus-Sublime" || process$1.env.TERM_PROGRAM === "vscode" || process$1.env.TERM === "xterm-256color" || process$1.env.TERM === "alacritty" || process$1.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
21425
21425
  }
21426
21426
  const unicode = isUnicodeSupported$1();
21427
- const isCI = () => process.env.CI === "true";
21427
+ const isCI$1 = () => process.env.CI === "true";
21428
21428
  const isTTY = (o) => o.isTTY === true;
21429
21429
  const unicodeOr = (o, e) => unicode ? o : e;
21430
21430
  const S_STEP_ACTIVE = unicodeOr("◆", "*");
@@ -21987,7 +21987,7 @@ ${r}
21987
21987
  }
21988
21988
  }).prompt();
21989
21989
  };
21990
- const log$1 = {
21990
+ const log$2 = {
21991
21991
  message: (s = [], { symbol: e = styleText("gray", S_BAR), secondarySymbol: r = styleText("gray", S_BAR), output: m = process.stdout, spacing: l = 1, withGuide: c } = {}) => {
21992
21992
  const t = [], o = c ?? settings.withGuide, f = o ? r : "", O = o ? `${e} ` : "", u = o ? `${r} ` : "";
21993
21993
  for (let i = 0; i < l; i++) t.push(f);
@@ -22003,35 +22003,35 @@ const log$1 = {
22003
22003
  `);
22004
22004
  },
22005
22005
  info: (s, e) => {
22006
- log$1.message(s, {
22006
+ log$2.message(s, {
22007
22007
  ...e,
22008
22008
  symbol: styleText("blue", S_INFO)
22009
22009
  });
22010
22010
  },
22011
22011
  success: (s, e) => {
22012
- log$1.message(s, {
22012
+ log$2.message(s, {
22013
22013
  ...e,
22014
22014
  symbol: styleText("green", S_SUCCESS)
22015
22015
  });
22016
22016
  },
22017
22017
  step: (s, e) => {
22018
- log$1.message(s, {
22018
+ log$2.message(s, {
22019
22019
  ...e,
22020
22020
  symbol: styleText("green", S_STEP_SUBMIT)
22021
22021
  });
22022
22022
  },
22023
22023
  warn: (s, e) => {
22024
- log$1.message(s, {
22024
+ log$2.message(s, {
22025
22025
  ...e,
22026
22026
  symbol: styleText("yellow", S_WARN)
22027
22027
  });
22028
22028
  },
22029
22029
  /** alias for `log.warn()`. */
22030
22030
  warning: (s, e) => {
22031
- log$1.warn(s, e);
22031
+ log$2.warn(s, e);
22032
22032
  },
22033
22033
  error: (s, e) => {
22034
- log$1.message(s, {
22034
+ log$2.message(s, {
22035
22035
  ...e,
22036
22036
  symbol: styleText("red", S_ERROR)
22037
22037
  });
@@ -22191,7 +22191,7 @@ const spinner$1 = ({ indicator: l = "dots", onCancel: h, output: n = process.std
22191
22191
  "O",
22192
22192
  "0"
22193
22193
  ], delay: F = unicode ? 80 : 120, signal: m, ...I } = {}) => {
22194
- const u = isCI();
22194
+ const u = isCI$1();
22195
22195
  let M, T, d = false, S = false, s = "", p, w = performance.now();
22196
22196
  const x = getColumns(n), k = I?.styleFrame ?? W$2, g = (e) => {
22197
22197
  const r = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel;
@@ -22429,7 +22429,7 @@ const tasks$1 = async (o, e) => {
22429
22429
  };
22430
22430
  const A$2 = (l) => l.replace(/\x1b\[(?:\d+;)*\d*[ABCDEFGHfJKSTsu]|\x1b\[(s|u)/g, "");
22431
22431
  const taskLog = (l) => {
22432
- const r = l.output ?? process.stdout, O = getColumns(r), i = styleText("gray", S_BAR), p = l.spacing ?? 1, k = 3, m = l.retainLog === true, d = !isCI() && isTTY(r);
22432
+ const r = l.output ?? process.stdout, O = getColumns(r), i = styleText("gray", S_BAR), p = l.spacing ?? 1, k = 3, m = l.retainLog === true, d = !isCI$1() && isTTY(r);
22433
22433
  r.write(`${i}
22434
22434
  `), r.write(`${styleText("green", S_STEP_SUBMIT)} ${l.title}
22435
22435
  `);
@@ -22458,13 +22458,13 @@ ${t.header}`);
22458
22458
  }, h = (e, s, t) => {
22459
22459
  const o = t ? `${e.full}
22460
22460
  ${e.value}` : e.value;
22461
- e.header !== void 0 && e.header !== "" && log$1.message(e.header.split(`
22461
+ e.header !== void 0 && e.header !== "" && log$2.message(e.header.split(`
22462
22462
  `).map((a) => styleText("bold", a)), {
22463
22463
  output: r,
22464
22464
  secondarySymbol: i,
22465
22465
  symbol: i,
22466
22466
  spacing: 0
22467
- }), log$1.message(o.split(`
22467
+ }), log$2.message(o.split(`
22468
22468
  `).map((a) => styleText("dim", a)), {
22469
22469
  output: r,
22470
22470
  secondarySymbol: i,
@@ -22492,11 +22492,11 @@ ${e.value}` : e.value;
22492
22492
  }
22493
22493
  d && y();
22494
22494
  }, y = () => {
22495
- for (const e of n) e.result ? e.result.status === "error" ? log$1.error(e.result.message, {
22495
+ for (const e of n) e.result ? e.result.status === "error" ? log$2.error(e.result.message, {
22496
22496
  output: r,
22497
22497
  secondarySymbol: i,
22498
22498
  spacing: 0
22499
- }) : log$1.success(e.result.message, {
22499
+ }) : log$2.success(e.result.message, {
22500
22500
  output: r,
22501
22501
  secondarySymbol: i,
22502
22502
  spacing: 0
@@ -22533,14 +22533,14 @@ ${e.value}` : e.value;
22533
22533
  };
22534
22534
  },
22535
22535
  error(e, s) {
22536
- f(true), log$1.error(e, {
22536
+ f(true), log$2.error(e, {
22537
22537
  output: r,
22538
22538
  secondarySymbol: i,
22539
22539
  spacing: 1
22540
22540
  }), s?.showLog !== false && T(), n.splice(1, n.length - 1), n[0].value = "", n[0].full = "";
22541
22541
  },
22542
22542
  success(e, s) {
22543
- f(true), log$1.success(e, {
22543
+ f(true), log$2.success(e, {
22544
22544
  output: r,
22545
22545
  secondarySymbol: i,
22546
22546
  spacing: 1
@@ -22584,7 +22584,115 @@ ${i ? styleText("cyan", S_BAR_END) : ""}
22584
22584
  }
22585
22585
  }).prompt();
22586
22586
  //#endregion
22587
+ //#region src/interactive.ts
22588
+ /**
22589
+ * Whether the environment is a CI run: `CI` is set to anything but empty,
22590
+ * `0`, or `false`. GitHub Actions, GitLab CI, CircleCI, Buildkite, Bitrise,
22591
+ * and Codemagic set it.
22592
+ */
22593
+ const isCI = (env = process.env) => {
22594
+ const value = env["CI"]?.trim().toLowerCase();
22595
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false";
22596
+ };
22597
+ /**
22598
+ * Why the CLI can't ask anything here, or undefined when it can: in CI,
22599
+ * where a runner may give the job a terminal nobody answers in, and without
22600
+ * a terminal on stdin. Prompts and sign-ins both check it.
22601
+ */
22602
+ const nonInteractiveReason = ({ env = process.env, input = process.stdin } = {}) => {
22603
+ if (isCI(env)) return "in CI (CI is set)";
22604
+ if (!input.isTTY) return "without a terminal";
22605
+ };
22606
+ //#endregion
22607
+ //#region src/secretMasking.ts
22608
+ const SECRET_MASK = "********";
22609
+ /**
22610
+ * A value shorter than this is masked only where it is a whole token, so a
22611
+ * keystore password `a` masks `-Ppassword=a` but leaves `Gradle failed`
22612
+ * readable, and a value `1` leaves `http://127.0.0.1:3941` whole. The lines
22613
+ * of a multi-line value are registered only from this length: masking the
22614
+ * `{` line of a JSON file would redact every brace.
22615
+ */
22616
+ const MIN_SUBSTRING_SECRET_LENGTH = 6;
22617
+ /**
22618
+ * What separates the tokens a short value must be whole: whitespace, quotes,
22619
+ * `=`, `&`, `|`, brackets, commas, and semicolons. Characters such as `.`,
22620
+ * `:`, `/`, `-`, and `@` join a run into one token, as in a URL or a version,
22621
+ * except a `.`, `:`, `!`, or `?` that ends a sentence.
22622
+ */
22623
+ const TOKEN_SEPARATOR = "\\s\"'`=&|,;()[\\]{}<>";
22624
+ const SHORT_SECRET_BEFORE = `(?<=^|[${TOKEN_SEPARATOR}])`;
22625
+ const SHORT_SECRET_AFTER = `(?=$|[${TOKEN_SEPARATOR}]|[.:!?](?:$|[${TOKEN_SEPARATOR}]))`;
22626
+ const PEM_ARMOR_LINE = /^-----(BEGIN|END) [A-Z0-9 ]+-----$/;
22627
+ const MASKING_REGISTRY = Symbol.for("@hot-updater/cli-tools/secret-masking");
22628
+ /**
22629
+ * The values to mask, kept on `globalThis`: the config file loads its own
22630
+ * copy of Hot Updater's modules, and a value it resolves, such as through
22631
+ * `secretEnv`, must be masked by the CLI's log sinks too.
22632
+ */
22633
+ const registry = globalThis[MASKING_REGISTRY] ??= {
22634
+ secretValues: /* @__PURE__ */ new Set(),
22635
+ shortSecretValues: /* @__PURE__ */ new Set(),
22636
+ secretValuesLongestFirst: [],
22637
+ shortSecretPattern: void 0
22638
+ };
22639
+ const { secretValues, shortSecretValues } = registry;
22640
+ const longestFirst = (values) => [...values].sort((left, right) => right.length - left.length);
22641
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22642
+ /**
22643
+ * Registers a credential value so every CLI log sink (`p`, `log`, and the
22644
+ * build log files) prints it masked, however short it is. A multi-line value
22645
+ * is also registered line by line, so a PEM printed one line at a time stays
22646
+ * masked.
22647
+ */
22648
+ const registerSecretValue = (value) => {
22649
+ const whole = value.trim();
22650
+ if (whole.length >= MIN_SUBSTRING_SECRET_LENGTH) secretValues.add(whole);
22651
+ else if (whole) shortSecretValues.add(whole);
22652
+ for (const line of value.split(/\r?\n/)) {
22653
+ const trimmed = line.trim();
22654
+ if (trimmed.length >= MIN_SUBSTRING_SECRET_LENGTH && !PEM_ARMOR_LINE.test(trimmed)) secretValues.add(trimmed);
22655
+ }
22656
+ registry.secretValuesLongestFirst = longestFirst(secretValues);
22657
+ registry.shortSecretPattern = shortSecretValues.size > 0 ? new RegExp(`${SHORT_SECRET_BEFORE}(?:${longestFirst(shortSecretValues).map(escapeRegExp).join("|")})${SHORT_SECRET_AFTER}`, "g") : void 0;
22658
+ };
22659
+ /** Replaces every registered credential value in `text` with a mask. */
22660
+ const maskSecretValues = (text) => {
22661
+ let masked = text;
22662
+ for (const value of registry.secretValuesLongestFirst) if (masked.includes(value)) masked = masked.split(value).join(SECRET_MASK);
22663
+ const { shortSecretPattern } = registry;
22664
+ return shortSecretPattern ? masked.replace(shortSecretPattern, SECRET_MASK) : masked;
22665
+ };
22666
+ //#endregion
22587
22667
  //#region src/prompts.ts
22668
+ const maskMessage = (message) => message === void 0 ? message : maskSecretValues(message);
22669
+ const log$1 = {
22670
+ message: (message, options) => log$2.message(Array.isArray(message) ? message.map(maskSecretValues) : maskMessage(message), options),
22671
+ info: (message, options) => log$2.info(maskSecretValues(message), options),
22672
+ success: (message, options) => log$2.success(maskSecretValues(message), options),
22673
+ step: (message, options) => log$2.step(maskSecretValues(message), options),
22674
+ warn: (message, options) => log$2.warn(maskSecretValues(message), options),
22675
+ warning: (message, options) => log$2.warning(maskSecretValues(message), options),
22676
+ error: (message, options) => log$2.error(maskSecretValues(message), options)
22677
+ };
22678
+ const maskSpinner = (target) => ({
22679
+ start: (message) => target.start(maskMessage(message)),
22680
+ stop: (message) => target.stop(maskMessage(message)),
22681
+ cancel: (message) => target.cancel(maskMessage(message)),
22682
+ error: (message) => target.error(maskMessage(message)),
22683
+ message: (message) => target.message(maskMessage(message)),
22684
+ clear: () => target.clear(),
22685
+ get isCancelled() {
22686
+ return target.isCancelled;
22687
+ }
22688
+ });
22689
+ const maskProgress = (target) => ({
22690
+ ...maskSpinner(target),
22691
+ advance: (step, message) => target.advance(step, maskMessage(message)),
22692
+ get isCancelled() {
22693
+ return target.isCancelled;
22694
+ }
22695
+ });
22588
22696
  const createStaticSpinner = ({ onCancel, output = process.stdout, withGuide } = {}) => {
22589
22697
  let active = false;
22590
22698
  let cancelled = false;
@@ -22593,26 +22701,26 @@ const createStaticSpinner = ({ onCancel, output = process.stdout, withGuide } =
22593
22701
  output,
22594
22702
  withGuide
22595
22703
  };
22596
- const finish = (message, log) => {
22704
+ const finish = (message, logLine) => {
22597
22705
  if (!active) return;
22598
22706
  active = false;
22599
22707
  const finalMessage = message || latestMessage;
22600
- if (finalMessage) log(finalMessage, logOptions);
22708
+ if (finalMessage) logLine(finalMessage, logOptions);
22601
22709
  };
22602
22710
  return {
22603
22711
  start: (message = "") => {
22604
22712
  active = true;
22605
22713
  latestMessage = message;
22606
- if (message) log$1.step(message, logOptions);
22714
+ if (message) log$2.step(message, logOptions);
22607
22715
  },
22608
- stop: (message) => finish(message, log$1.success),
22716
+ stop: (message) => finish(message, log$2.success),
22609
22717
  cancel: (message) => {
22610
22718
  const wasActive = active;
22611
22719
  cancelled = wasActive;
22612
- finish(message, log$1.warn);
22720
+ finish(message, log$2.warn);
22613
22721
  if (wasActive) onCancel?.();
22614
22722
  },
22615
- error: (message) => finish(message, log$1.error),
22723
+ error: (message) => finish(message, log$2.error),
22616
22724
  message: (message = "") => {
22617
22725
  latestMessage = message;
22618
22726
  },
@@ -22626,12 +22734,13 @@ const createStaticSpinner = ({ onCancel, output = process.stdout, withGuide } =
22626
22734
  };
22627
22735
  const spinner = (options = {}) => {
22628
22736
  const output = options.output ?? process.stdout;
22629
- return isTTY(output) && !isCI() ? spinner$1(options) : createStaticSpinner(options);
22737
+ const isInteractive = isTTY(output) && !isCI$1();
22738
+ return maskSpinner(isInteractive ? spinner$1(options) : createStaticSpinner(options));
22630
22739
  };
22631
22740
  const progress = (options = {}) => {
22632
22741
  const output = options.output ?? process.stdout;
22633
- if (isTTY(output) && !isCI()) return progress$1(options);
22634
- const staticSpinner = createStaticSpinner(options);
22742
+ if (isTTY(output) && !isCI$1()) return maskProgress(progress$1(options));
22743
+ const staticSpinner = maskSpinner(createStaticSpinner(options));
22635
22744
  return {
22636
22745
  ...staticSpinner,
22637
22746
  advance: (_step, message) => staticSpinner.message(message),
@@ -22654,8 +22763,46 @@ const tasks = async (taskList, options) => {
22654
22763
  }
22655
22764
  }
22656
22765
  };
22766
+ /**
22767
+ * The prompt, refusing to ask in CI or without a terminal: a prompt on a
22768
+ * closed or piped stdin would end as a cancel or wait forever, and one in CI
22769
+ * waits on a terminal nobody answers in. A command that can skip a prompt
22770
+ * says which option does before it asks.
22771
+ */
22772
+ const requireTerminal = (prompt) => {
22773
+ return (options) => {
22774
+ const reason = nonInteractiveReason({ input: options.input ?? process.stdin });
22775
+ if (reason !== void 0) {
22776
+ const message = options.message;
22777
+ return Promise.reject(/* @__PURE__ */ new Error(`${typeof message === "string" ? `${message} ` : ""}The CLI asks nothing ${reason}: pass the command's options instead (see --help).`));
22778
+ }
22779
+ return prompt(options);
22780
+ };
22781
+ };
22782
+ /**
22783
+ * `@clack/prompts` with CI-safe spinners, with every message a registered
22784
+ * credential value appears in masked (`registerSecretValue`), and with
22785
+ * prompts that refuse to ask without a terminal.
22786
+ */
22657
22787
  const p = {
22658
22788
  ...dist_exports,
22789
+ autocomplete: requireTerminal(autocomplete),
22790
+ autocompleteMultiselect: requireTerminal(autocompleteMultiselect),
22791
+ confirm: requireTerminal(confirm),
22792
+ date: requireTerminal(date),
22793
+ groupMultiselect: requireTerminal(groupMultiselect),
22794
+ multiline: requireTerminal(multiline),
22795
+ multiselect: requireTerminal(multiselect),
22796
+ password: requireTerminal(password),
22797
+ path: requireTerminal(path$5),
22798
+ select: requireTerminal(select),
22799
+ selectKey: requireTerminal(selectKey),
22800
+ text: requireTerminal(text),
22801
+ log: log$1,
22802
+ cancel: (message, options) => cancel(maskMessage(message), options),
22803
+ intro: (title, options) => intro(maskMessage(title), options),
22804
+ note: (message, title, options) => note(maskMessage(message), maskMessage(title), options),
22805
+ outro: (message, options) => outro(maskMessage(message), options),
22659
22806
  progress,
22660
22807
  spinner,
22661
22808
  tasks
@@ -22710,11 +22857,18 @@ const createLogWriter = async ({ prefix, logFilePath }) => {
22710
22857
  });
22711
22858
  const writeChunk = (value) => {
22712
22859
  if (!stream.writable || stream.destroyed) return;
22713
- const plainText = stripAnsi(normalizeChunk({ chunk: value })).replace(/\r/g, "");
22860
+ const plainText = maskSecretValues(stripAnsi(normalizeChunk({ chunk: value })).replace(/\r/g, ""));
22714
22861
  stream.write(plainText);
22715
22862
  };
22716
22863
  const writeStream = async (input) => {
22717
- for await (const chunk of input) writeChunk(normalizeChunk({ chunk }));
22864
+ let partialLine = "";
22865
+ for await (const chunk of input) {
22866
+ const text = partialLine + normalizeChunk({ chunk });
22867
+ const lineEnd = Math.max(text.lastIndexOf("\n"), text.lastIndexOf("\r")) + 1;
22868
+ partialLine = text.slice(lineEnd);
22869
+ if (lineEnd > 0) writeChunk(text.slice(0, lineEnd));
22870
+ }
22871
+ if (partialLine) writeChunk(partialLine);
22718
22872
  };
22719
22873
  const writeError = (error) => {
22720
22874
  const message = error instanceof Error ? error.stack ?? error.message : String(error);
@@ -22799,10 +22953,39 @@ const resolveNextCompletedStage = ({ line, progressStages, completedStages }) =>
22799
22953
  }
22800
22954
  return nextCompletedStages;
22801
22955
  };
22956
+ /** The most lines a failure summary prints, and the tail it falls back to. */
22957
+ const MAX_SUMMARY_LINES = 60;
22958
+ const TAIL_LINES = 40;
22959
+ const matchesSummaryPattern = (line, pattern) => {
22960
+ if (pattern instanceof RegExp) {
22961
+ pattern.lastIndex = 0;
22962
+ return pattern.test(line);
22963
+ }
22964
+ return line.includes(pattern);
22965
+ };
22966
+ /**
22967
+ * A native build that failed, after its logger printed the summary of the
22968
+ * tool's output and the log file's path, so its message is one line.
22969
+ */
22970
+ var BuildFailedError = class extends Error {
22971
+ name = "BuildFailedError";
22972
+ /** The build log, relative to the project, or null without one. */
22973
+ logPath;
22974
+ /** The lines of the tool's output the summary printed. */
22975
+ summary;
22976
+ constructor(message, { logPath, summary, cause }) {
22977
+ super(message, { cause });
22978
+ this.logPath = logPath;
22979
+ this.summary = summary;
22980
+ }
22981
+ };
22802
22982
  var BuildLogger = class {
22803
22983
  config;
22804
22984
  state = createInitialState();
22805
22985
  logWriter;
22986
+ summaryLines = [];
22987
+ tailLines = [];
22988
+ openBlock;
22806
22989
  promptProgress;
22807
22990
  stageCount;
22808
22991
  constructor(config) {
@@ -22819,16 +23002,76 @@ var BuildLogger = class {
22819
23002
  ...createInitialState(),
22820
23003
  started: true
22821
23004
  };
23005
+ this.summaryLines = [];
23006
+ this.tailLines = [];
23007
+ this.openBlock = void 0;
22822
23008
  this.logWriter = await createLogWriter({ prefix: this.config.logPrefix });
22823
23009
  this.promptProgress.start();
22824
23010
  }
22825
- async processStream(input) {
23011
+ /**
23012
+ * Writes the tool's raw output to the log file and reads its failure
23013
+ * summary from it, and shows progress from `display`, a formatter's
23014
+ * output of it such as xcbeautify's, or from the raw output without one.
23015
+ */
23016
+ async processStream(input, { display } = {}) {
22826
23017
  if (!this.logWriter) throw new Error("BuildLogger has not been started. Call start() first.");
22827
23018
  const progressInput = new PassThrough();
22828
23019
  const logInput = new PassThrough();
22829
- input.pipe(progressInput);
23020
+ const summaryInput = new PassThrough();
23021
+ (display ?? input).pipe(progressInput);
22830
23022
  input.pipe(logInput);
22831
- await Promise.all([this.consumeProgressStream(progressInput), this.logWriter.writeStream(logInput)]);
23023
+ input.pipe(summaryInput);
23024
+ await Promise.all([
23025
+ this.consumeProgressStream(progressInput),
23026
+ this.logWriter.writeStream(logInput),
23027
+ this.consumeSummaryStream(summaryInput)
23028
+ ]);
23029
+ }
23030
+ /** The build log, relative to the project, or null without one. */
23031
+ get logPath() {
23032
+ return this.logWriter?.logFilePath ? path.relative(getCwd(), this.logWriter.logFilePath) : null;
23033
+ }
23034
+ /**
23035
+ * The lines of the raw output a failure prints: the ones the config's
23036
+ * `failureSummary` keeps, deduplicated, or the last lines without any.
23037
+ */
23038
+ get failureSummary() {
23039
+ const lines = this.summaryLines.length ? this.summaryLines : this.tailLines.filter((line) => line.trim());
23040
+ const first = lines.findIndex((line) => line.trim());
23041
+ const last = lines.findLastIndex((line) => line.trim());
23042
+ return first === -1 ? [] : lines.slice(first, last + 1);
23043
+ }
23044
+ async consumeSummaryStream(input) {
23045
+ const lineReader = createInterface({
23046
+ input,
23047
+ crlfDelay: Infinity,
23048
+ terminal: false
23049
+ });
23050
+ for await (const rawLine of lineReader) this.processSummaryLine(stripAnsi(rawLine).replace(/\r/g, "").trimEnd());
23051
+ }
23052
+ processSummaryLine(line) {
23053
+ this.tailLines.push(line);
23054
+ if (this.tailLines.length > TAIL_LINES) this.tailLines.shift();
23055
+ const { lines, blocks } = this.config.failureSummary;
23056
+ const trimmedLine = line.trim();
23057
+ if (this.openBlock) {
23058
+ if (matchesSummaryPattern(trimmedLine, this.openBlock.end)) {
23059
+ if (this.openBlock.includeEnd) this.keepSummaryLine(line);
23060
+ this.openBlock = void 0;
23061
+ } else this.keepSummaryLine(line);
23062
+ return;
23063
+ }
23064
+ const block = blocks.find(({ start }) => matchesSummaryPattern(trimmedLine, start));
23065
+ if (block) {
23066
+ this.openBlock = block;
23067
+ this.keepSummaryLine(line);
23068
+ return;
23069
+ }
23070
+ if (lines.some((pattern) => matchesSummaryPattern(trimmedLine, pattern))) this.keepSummaryLine(line);
23071
+ }
23072
+ keepSummaryLine(line) {
23073
+ if (this.summaryLines.length >= MAX_SUMMARY_LINES || line.trim() && this.summaryLines.includes(line)) return;
23074
+ this.summaryLines.push(line);
22832
23075
  }
22833
23076
  async consumeProgressStream(input) {
22834
23077
  const lineReader = createInterface({
@@ -22855,9 +23098,13 @@ var BuildLogger = class {
22855
23098
  if (!this.state.started || this.state.stopped) return;
22856
23099
  if (success) this.finishRemainingProgress();
22857
23100
  const finalMessage = message || (success ? "Build completed successfully" : "Build failed");
22858
- if (success) this.promptProgress.stop(finalMessage);
22859
- else this.promptProgress.error(finalMessage);
22860
- this.showLogFileLocation();
23101
+ if (success) {
23102
+ this.promptProgress.stop(finalMessage);
23103
+ this.showLogFileLocation();
23104
+ } else {
23105
+ this.promptProgress.error(finalMessage);
23106
+ this.showFailureSummary();
23107
+ }
22861
23108
  this.state = {
22862
23109
  ...this.state,
22863
23110
  stopped: true
@@ -22897,6 +23144,16 @@ var BuildLogger = class {
22897
23144
  patterns: this.config.importantLogPatterns
22898
23145
  });
22899
23146
  }
23147
+ /**
23148
+ * Prints the failure summary and the log's path to stderr, so a `--json`
23149
+ * command's stdout stays its JSON.
23150
+ */
23151
+ showFailureSummary() {
23152
+ const summary = this.failureSummary;
23153
+ const logPath = this.logPath;
23154
+ const message = [...summary, ...logPath ? [...summary.length ? [""] : [], `Build log: ${logPath}`] : []].join("\n");
23155
+ if (message) p.log.error(message, { output: process.stderr });
23156
+ }
22900
23157
  showLogFileLocation() {
22901
23158
  if (!this.logWriter?.logFilePath) return;
22902
23159
  const relativePath = path.relative(getCwd(), this.logWriter.logFilePath);
@@ -24925,28 +25182,651 @@ const printBanner = (version) => {
24925
25182
  console.log(banner(version));
24926
25183
  };
24927
25184
  //#endregion
25185
+ //#region src/initOptions.ts
25186
+ var InitError = class extends Error {
25187
+ name = "InitError";
25188
+ };
25189
+ var MissingInitInputsError = class extends InitError {
25190
+ missingInputs;
25191
+ name = "MissingInitInputsError";
25192
+ constructor(missingInputs) {
25193
+ super(["Init is missing required inputs:", ...missingInputs.map((input) => `- ${input}`)].join("\n"));
25194
+ this.missingInputs = missingInputs;
25195
+ }
25196
+ };
25197
+ var InitEnvFileError = class extends InitError {
25198
+ name = "InitEnvFileError";
25199
+ };
25200
+ var LegacyInfrastructureError = class extends InitError {
25201
+ name = "LegacyInfrastructureError";
25202
+ constructor(provider, resource) {
25203
+ super(`${provider} v0 infrastructure was detected at ${resource}. Hot Updater v1 cannot upgrade it in place. Run init with new provider resources and ship the new endpoint in a new native build. The existing infrastructure was not changed.`);
25204
+ }
25205
+ };
25206
+ const assertInitInputs = ({ inputs, strict }) => {
25207
+ if (!strict) return;
25208
+ const missingInputs = getMissingInitInputs(inputs);
25209
+ if (missingInputs.length > 0) throw new MissingInitInputsError(missingInputs);
25210
+ };
25211
+ const getMissingInitInputs = (inputs) => Object.entries(inputs).filter(([, value]) => !value?.trim()).map(([key]) => key);
25212
+ //#endregion
25213
+ //#region src/hotUpdaterEnv.ts
25214
+ const HOT_UPDATER_ENV_PATH = ".env.hotupdater";
25215
+ const unquoteEnvValue = (value) => {
25216
+ const trimmed = value.trim();
25217
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) try {
25218
+ const parsed = JSON.parse(trimmed);
25219
+ return typeof parsed === "string" ? parsed : trimmed.slice(1, -1);
25220
+ } catch {
25221
+ return trimmed.slice(1, -1);
25222
+ }
25223
+ return trimmed.startsWith("'") && trimmed.endsWith("'") ? trimmed.slice(1, -1) : trimmed;
25224
+ };
25225
+ const parseEnv$1 = (content) => {
25226
+ const env = {};
25227
+ for (const line of content.split("\n")) {
25228
+ const trimmed = line.trim();
25229
+ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
25230
+ const separatorIndex = trimmed.indexOf("=");
25231
+ const key = trimmed.slice(0, separatorIndex).trim();
25232
+ if (key) env[key] = unquoteEnvValue(trimmed.slice(separatorIndex + 1));
25233
+ }
25234
+ return env;
25235
+ };
25236
+ const readEnvFile = async (filePath, allowMissing) => {
25237
+ let content;
25238
+ try {
25239
+ content = await fs$1.readFile(filePath, "utf-8");
25240
+ } catch (error) {
25241
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
25242
+ if (allowMissing) return {};
25243
+ throw new InitEnvFileError(`Init environment file not found: ${filePath}`);
25244
+ }
25245
+ throw error;
25246
+ }
25247
+ return parseEnv$1(content);
25248
+ };
25249
+ const getHotUpdaterInitInputEnv = ({ env, managedEnv }, nonInteractive) => nonInteractive ? env : managedEnv;
25250
+ const readHotUpdaterInitEnv = async (cwd, envFile) => {
25251
+ const savedEnvPath = path.resolve(cwd, HOT_UPDATER_ENV_PATH);
25252
+ const savedEnv = await readEnvFile(savedEnvPath, true);
25253
+ if (!envFile) return {
25254
+ env: {},
25255
+ managedEnv: savedEnv
25256
+ };
25257
+ const inputEnvPath = path.resolve(cwd, envFile);
25258
+ if (inputEnvPath === savedEnvPath) return {
25259
+ env: savedEnv,
25260
+ managedEnv: savedEnv
25261
+ };
25262
+ const inputEnv = await readEnvFile(inputEnvPath, false);
25263
+ return {
25264
+ env: {
25265
+ ...savedEnv,
25266
+ ...inputEnv
25267
+ },
25268
+ inputEnv,
25269
+ managedEnv: savedEnv
25270
+ };
25271
+ };
25272
+ /** `key`'s own value in `record`, never a member such as `__proto__`. */
25273
+ const ownValue = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
25274
+ const getHotUpdaterEnvValue = (env, key) => {
25275
+ const fromProcess = ownValue(process.env, key);
25276
+ if (fromProcess !== void 0) return fromProcess.trim() || void 0;
25277
+ return ownValue(env, key)?.trim() || void 0;
25278
+ };
25279
+ /**
25280
+ * An env file's variables as `process.loadEnvFile` sets them, the way
25281
+ * `hot-updater.config.ts` loads `.env.hotupdater`: inline comments, quotes,
25282
+ * and `export` prefixes included.
25283
+ */
25284
+ const parseLoadedEnv = (content) => Object.fromEntries(Object.entries(parseEnv(content)).filter((entry) => entry[1] !== void 0));
25285
+ /** `cwd`'s `.env.hotupdater` as the config loads it; empty when there is none. */
25286
+ const readLoadedHotUpdaterEnv = async (cwd) => {
25287
+ try {
25288
+ return parseLoadedEnv(await fs$1.readFile(path.resolve(cwd, HOT_UPDATER_ENV_PATH), "utf-8"));
25289
+ } catch (error) {
25290
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
25291
+ throw error;
25292
+ }
25293
+ };
25294
+ //#endregion
25295
+ //#region src/localKeyStore.ts
25296
+ /**
25297
+ * Relocates Hot Updater's per-user directory, `~/.hot-updater` by default,
25298
+ * such as for CI caches or tests.
25299
+ */
25300
+ const HOT_UPDATER_HOME_ENV = "HOT_UPDATER_HOME";
25301
+ /** Hot Updater's per-user directory: `$HOT_UPDATER_HOME`, else `~/.hot-updater`. */
25302
+ const getHotUpdaterHome = () => {
25303
+ const home = process.env[HOT_UPDATER_HOME_ENV]?.trim();
25304
+ return home ? path$1.resolve(home) : path$1.join(os.homedir(), ".hot-updater");
25305
+ };
25306
+ const git = (cwd, args) => {
25307
+ try {
25308
+ return execFileSync("git", args, {
25309
+ cwd,
25310
+ encoding: "utf-8",
25311
+ stdio: [
25312
+ "ignore",
25313
+ "pipe",
25314
+ "ignore"
25315
+ ],
25316
+ timeout: 1e4
25317
+ }).trim();
25318
+ } catch {
25319
+ return;
25320
+ }
25321
+ };
25322
+ const realpath = (directory) => {
25323
+ try {
25324
+ return realpathSync(directory);
25325
+ } catch {
25326
+ return path$1.resolve(directory);
25327
+ }
25328
+ };
25329
+ /**
25330
+ * The project's directory name in the store: the project directory's name
25331
+ * and the first 12 hex digits of the SHA-256 of its real path, such as
25332
+ * `my-app-3f2a9c1d04be`. Each checkout of an app has its own.
25333
+ */
25334
+ const getProjectKey = (cwd) => {
25335
+ const directory = realpath(cwd);
25336
+ return `${path$1.basename(directory).replace(/[^A-Za-z0-9._-]/gu, "-").replace(/^[.-]+/u, "").slice(0, 40) || "project"}-${createHash("sha256").update(directory).digest("hex").slice(0, 12)}`;
25337
+ };
25338
+ /**
25339
+ * The directory `localSecrets()` keeps a project's secrets in without a
25340
+ * `path`: `<home>/secrets/<project-key>` (`getHotUpdaterHome`,
25341
+ * `getProjectKey`).
25342
+ */
25343
+ const getLocalKeyStoreDir = (cwd) => path$1.join(getHotUpdaterHome(), "secrets", getProjectKey(cwd));
25344
+ /**
25345
+ * `localSecrets({ path })`'s directory: `~` is the home directory, and a
25346
+ * relative path is the project's (`cwd`).
25347
+ */
25348
+ const resolveLocalSecretsPath = (value, cwd) => {
25349
+ if (value === "~") return os.homedir();
25350
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path$1.join(os.homedir(), value.slice(2));
25351
+ return path$1.resolve(cwd, value);
25352
+ };
25353
+ const isMissing = (error) => error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
25354
+ /** `target`'s real path, through its nearest parent that exists. */
25355
+ const realpathThroughParents = (target) => {
25356
+ const missing = [];
25357
+ let current = target;
25358
+ for (;;) try {
25359
+ return path$1.join(realpathSync(current), ...missing.reverse());
25360
+ } catch {
25361
+ const parent = path$1.dirname(current);
25362
+ if (parent === current) return target;
25363
+ missing.push(path$1.basename(current));
25364
+ current = parent;
25365
+ }
25366
+ };
25367
+ /** Whether git ignores `directory`, a path in the work tree at `root`. */
25368
+ const isIgnoredByGit = (root, directory) => {
25369
+ const relative = path$1.relative(root, directory).split(path$1.sep).join("/");
25370
+ return spawnSync("git", [
25371
+ "check-ignore",
25372
+ "--quiet",
25373
+ "--",
25374
+ `${relative}/`
25375
+ ], {
25376
+ cwd: root,
25377
+ stdio: "ignore",
25378
+ timeout: 1e4
25379
+ }).status === 0;
25380
+ };
25381
+ /**
25382
+ * Refuses a directory secrets must not go in: a path that exists and is no
25383
+ * directory, or one in a git work tree that git does not ignore, whose
25384
+ * secrets could be committed.
25385
+ */
25386
+ const assertPrivateDirectory = async (directory) => {
25387
+ const stats = await fs$2.stat(directory).catch((error) => {
25388
+ if (isMissing(error)) return void 0;
25389
+ throw error;
25390
+ });
25391
+ if (stats && !stats.isDirectory()) throw new Error(`localSecrets() keeps secrets in a directory, and ${directory} is not one. Set its path to a directory.`);
25392
+ const real = realpathThroughParents(directory);
25393
+ let probe = real;
25394
+ while (!existsSync(probe) && path$1.dirname(probe) !== probe) probe = path$1.dirname(probe);
25395
+ const root = git(probe, ["rev-parse", "--show-toplevel"]);
25396
+ if (!root) return;
25397
+ const top = realpath(root);
25398
+ if (isIgnoredByGit(top, real)) return;
25399
+ throw new Error(`localSecrets() refuses ${directory}: it is in the git repository ${top}, which does not ignore it, so the secrets in it could be committed. Add ${path$1.relative(top, real).split(path$1.sep).join("/")}/ to that repository's .gitignore, or keep the secrets outside it with localSecrets({ path }).`);
25400
+ };
25401
+ /** Makes `directory` 0700, and every directory it creates on the way. */
25402
+ const makePrivateDirectory = async (directory) => {
25403
+ await fs$2.mkdir(directory, {
25404
+ recursive: true,
25405
+ mode: 448
25406
+ });
25407
+ if (process.platform !== "win32") await fs$2.chmod(directory, 448);
25408
+ };
25409
+ /**
25410
+ * The store `localSecrets({ path })` reads and writes: `path`, or
25411
+ * `getLocalKeyStoreDir` without one. Its first use refuses a directory
25412
+ * secrets must not go in (`assertPrivateDirectory`).
25413
+ */
25414
+ const createLocalSecretsStore = ({ path: configured } = {}) => {
25415
+ const checked = /* @__PURE__ */ new Map();
25416
+ const directory = (cwd) => configured === void 0 ? getLocalKeyStoreDir(cwd) : resolveLocalSecretsPath(configured, cwd);
25417
+ const check = (target) => {
25418
+ let pending = checked.get(target);
25419
+ if (!pending) {
25420
+ pending = assertPrivateDirectory(target);
25421
+ checked.set(target, pending);
25422
+ pending.catch(() => checked.delete(target));
25423
+ }
25424
+ return pending;
25425
+ };
25426
+ const file = (name, cwd) => path$1.join(directory(cwd), `${name}.json`);
25427
+ return {
25428
+ directory,
25429
+ file,
25430
+ read: async (name, cwd) => {
25431
+ await check(directory(cwd));
25432
+ try {
25433
+ return await fs$2.readFile(file(name, cwd), "utf-8") || void 0;
25434
+ } catch (error) {
25435
+ if (isMissing(error)) return void 0;
25436
+ throw error;
25437
+ }
25438
+ },
25439
+ write: async (name, value, cwd) => {
25440
+ const target = directory(cwd);
25441
+ await check(target);
25442
+ await makePrivateDirectory(target);
25443
+ const destination = file(name, cwd);
25444
+ const temporary = `${destination}.${randomUUID()}.tmp`;
25445
+ try {
25446
+ await fs$2.writeFile(temporary, value, {
25447
+ mode: 384,
25448
+ flag: "wx"
25449
+ });
25450
+ await fs$2.rename(temporary, destination);
25451
+ } catch (error) {
25452
+ await fs$2.rm(temporary, { force: true });
25453
+ throw error;
25454
+ }
25455
+ return destination;
25456
+ }
25457
+ };
25458
+ };
25459
+ const defaultStore = createLocalSecretsStore();
25460
+ /** The file `localSecrets()` without a `path` keeps `name` in. */
25461
+ const getLocalKeyFile = (name, cwd) => defaultStore.file(name, cwd);
25462
+ /** `name`'s value in `localSecrets()`'s default store, or undefined. */
25463
+ const readLocalKey = (name, cwd) => defaultStore.read(name, cwd);
25464
+ /** Stores `value` as `name` in `localSecrets()`'s default store. */
25465
+ const writeLocalKey = (name, value, cwd) => defaultStore.write(name, value, cwd);
25466
+ //#endregion
25467
+ //#region src/credentialEnvNames.ts
25468
+ /**
25469
+ * The environment variables that override each kind, one entry per field.
25470
+ * A field with two names takes a file path or the file's contents; the path
25471
+ * wins when both are set.
25472
+ */
25473
+ const CREDENTIAL_ENV_NAMES = {
25474
+ "asc-api-key": [
25475
+ ["HOT_UPDATER_ASC_KEY_PATH", "HOT_UPDATER_ASC_KEY"],
25476
+ ["HOT_UPDATER_ASC_KEY_ID"],
25477
+ ["HOT_UPDATER_ASC_ISSUER_ID"]
25478
+ ],
25479
+ "android-keystore": [
25480
+ ["HOT_UPDATER_ANDROID_KEYSTORE_PATH", "HOT_UPDATER_ANDROID_KEYSTORE_BASE64"],
25481
+ ["HOT_UPDATER_ANDROID_KEYSTORE_PASSWORD"],
25482
+ ["HOT_UPDATER_ANDROID_KEY_ALIAS"],
25483
+ ["HOT_UPDATER_ANDROID_KEY_PASSWORD"]
25484
+ ],
25485
+ "play-service-account": [["HOT_UPDATER_PLAY_SERVICE_ACCOUNT_PATH", "HOT_UPDATER_PLAY_SERVICE_ACCOUNT_JSON"]]
25486
+ };
25487
+ //#endregion
25488
+ //#region src/reservedVariables.ts
25489
+ /**
25490
+ * Variables that change how a process loads code or which credentials Hot
25491
+ * Updater uses, compared case-insensitively, as Windows and npm read them.
25492
+ */
25493
+ const RESERVED_NAMES = /* @__PURE__ */ new Set([
25494
+ "PATH",
25495
+ "BASH_ENV",
25496
+ "ENV",
25497
+ "JAVA_TOOL_OPTIONS",
25498
+ "_JAVA_OPTIONS",
25499
+ "JDK_JAVA_OPTIONS",
25500
+ "GRADLE_OPTS",
25501
+ "RUBYOPT",
25502
+ "RUBYLIB",
25503
+ "PYTHONPATH",
25504
+ "PYTHONSTARTUP",
25505
+ "PERL5OPT",
25506
+ ...Object.values(CREDENTIAL_ENV_NAMES).flat(2),
25507
+ "HOT_UPDATER_SIGNING_TOKEN",
25508
+ "HOT_UPDATER_HOME"
25509
+ ]);
25510
+ const RESERVED_PREFIXES = [
25511
+ "LD_",
25512
+ "DYLD_",
25513
+ "GIT_",
25514
+ "NPM_CONFIG_",
25515
+ "HOT_UPDATER_GOOGLE_OAUTH_CLIENT_"
25516
+ ];
25517
+ /**
25518
+ * Whether `name` is a variable no secret may set: one a loader or
25519
+ * interpreter reads (`NODE_*` but `NODE_ENV`, `PATH`, `LD_*`, `DYLD_*`,
25520
+ * `BASH_ENV`, `ENV`, `GIT_*`, `npm_config_*`, the JVM's, Gradle's, Ruby's,
25521
+ * Python's, and Perl's options), or one Hot Updater reads its credentials
25522
+ * from. `secretEnv` refuses these names, and so does every environment the
25523
+ * CLI gives a child process from resolved secrets.
25524
+ */
25525
+ const isReservedVariableName = (name) => {
25526
+ const upper = name.toUpperCase();
25527
+ return RESERVED_NAMES.has(upper) || upper.startsWith("NODE_") && upper !== "NODE_ENV" || RESERVED_PREFIXES.some((prefix) => upper.startsWith(prefix));
25528
+ };
25529
+ /**
25530
+ * Throws when `env`, an environment the CLI gives a child process from
25531
+ * resolved secrets, sets a reserved variable, naming it and `what` sets it.
25532
+ * Returns `env`.
25533
+ */
25534
+ const assertUnreservedEnv = (env, what) => {
25535
+ const reserved = Object.keys(env).filter(isReservedVariableName);
25536
+ if (reserved.length > 0) throw new Error(`${what} would set ${reserved.join(", ")}, which no secret may set: ${reserved.length === 1 ? "it changes" : "they change"} how a process loads code or which credentials Hot Updater uses.`);
25537
+ return env;
25538
+ };
25539
+ //#endregion
25540
+ //#region src/secrets.ts
25541
+ const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
25542
+ /** A secret's name, as `secret:<name>` takes one. */
25543
+ const SECRET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
25544
+ const SECRET_REFERENCE_PREFIX = "secret:";
25545
+ /** Whether `name` is a secret's name, as `secret:<name>` takes one. */
25546
+ const isSecretName = (name) => SECRET_NAME_PATTERN.test(name);
25547
+ /**
25548
+ * The secret name a config's `secret:<name>` reference names, such as
25549
+ * `release-keystore` for `"secret:release-keystore"`. `field` is the config
25550
+ * field the reference is in, which the error names.
25551
+ */
25552
+ const parseSecretReference = (reference, field) => {
25553
+ const name = reference.startsWith(SECRET_REFERENCE_PREFIX) ? reference.slice(7) : void 0;
25554
+ if (name === void 0 || !SECRET_NAME_PATTERN.test(name)) throw new Error(`${field} names a secret as secret:<name>, such as "secret:release-keystore", and ${JSON.stringify(reference)} is not one.`);
25555
+ return name;
25556
+ };
25557
+ /**
25558
+ * The environment variable the default store reads a secret from: a name
25559
+ * that is a variable name already, such as `CLOUDFLARE_API_TOKEN`, as it
25560
+ * is, and any other, such as `asc-api-key`, upper-cased with `HOT_UPDATER_`
25561
+ * before it and every character but letters and digits as `_`
25562
+ * (`HOT_UPDATER_ASC_API_KEY`).
25563
+ */
25564
+ const secretVariableName = (name) => ENV_NAME_PATTERN.test(name) ? name : `HOT_UPDATER_${name.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`;
25565
+ /** `name` from its variable in `process.env` or the project's `.env.hotupdater`. */
25566
+ const readSecretVariables = async (names, cwd) => {
25567
+ const fileEnv = await readLoadedHotUpdaterEnv(cwd);
25568
+ const values = Object.create(null);
25569
+ for (const name of names) values[name] = getHotUpdaterEnvValue(fileEnv, secretVariableName(name));
25570
+ return values;
25571
+ };
25572
+ /** `localSecrets()`'s name, the `secrets` adapter when the config sets none. */
25573
+ const LOCAL_SECRETS_NAME = "localSecrets";
25574
+ const LOCAL_KEY_STORE = Symbol.for("@hot-updater/cli-tools/local-key-store");
25575
+ /** `localSecrets()`'s store without a `path`, the one after any adapter. */
25576
+ const defaultLocalSecretsStore = createLocalSecretsStore();
25577
+ const isLocalKeyStoreSecrets = (secrets) => LOCAL_KEY_STORE in secrets;
25578
+ /**
25579
+ * Secrets in JSON files on this machine, the `secrets` adapter when the
25580
+ * config sets none: `get` reads each name from its environment variable
25581
+ * (`secretVariableName`, in `process.env` or `.env.hotupdater`), then from
25582
+ * `~/.hot-updater/secrets/<project-key>/<name>.json`; `set` writes that
25583
+ * file, 0600 in a 0700 directory, outside the project, so it survives
25584
+ * `node_modules` and is never committed. `HOT_UPDATER_HOME` moves
25585
+ * `~/.hot-updater`. The project key is the same for every clone of one app
25586
+ * on a machine (its `origin` remote and its directory in the repository),
25587
+ * so another machine signs in and issues a key of its own.
25588
+ *
25589
+ * @example
25590
+ * export default defineConfig({ secrets: localSecrets(), ... });
25591
+ */
25592
+ const localSecrets = (options = {}) => createLocalKeyStoreSecrets(void 0, createLocalSecretsStore(options));
25593
+ /**
25594
+ * The config's `secrets` adapter with `localSecrets()`'s store after it:
25595
+ * `get` reads each name the adapter does not hold from
25596
+ * `~/.hot-updater/secrets/<project-key>/<name>.json`, and `set` stores there
25597
+ * when the adapter stores nothing. Without an adapter, it is
25598
+ * `localSecrets()` itself. Keys Hot Updater issues, and credentials
25599
+ * `secrets put` imports, are reused by the next build on this machine with
25600
+ * no step in between.
25601
+ */
25602
+ const withLocalKeyStore = (configured) => configured && isLocalKeyStoreSecrets(configured) ? configured : createLocalKeyStoreSecrets(configured, defaultLocalSecretsStore);
25603
+ const createLocalKeyStoreSecrets = (configured, store) => {
25604
+ const describeFile = (name) => store.file(name, getCwd());
25605
+ return {
25606
+ name: configured?.name ?? "localSecrets",
25607
+ [LOCAL_KEY_STORE]: configured ? {
25608
+ configured,
25609
+ store
25610
+ } : { store },
25611
+ get: async (names, context) => {
25612
+ const values = configured ? { ...await configured.get(names, context) } : await readSecretVariables(names, context.cwd);
25613
+ const read = Object.create(null);
25614
+ for (const name of names) {
25615
+ const value = Object.hasOwn(values, name) ? values[name] : void 0;
25616
+ read[name] = typeof value === "string" && value ? value : await store.read(name, context.cwd);
25617
+ }
25618
+ return read;
25619
+ },
25620
+ set: async (name, value, context) => {
25621
+ if (configured?.set) {
25622
+ await configured.set(name, value, context);
25623
+ if (await store.read(name, context.cwd) === void 0) return;
25624
+ try {
25625
+ await store.write(name, value, context.cwd);
25626
+ } catch (error) {
25627
+ p.log.warn(`The ${configured.name} secrets adapter stored ${name}, and the copy on this machine at ${store.file(name, context.cwd)} still holds the value it replaced: ${error instanceof Error ? error.message : String(error)}. Delete that file.`);
25628
+ }
25629
+ return;
25630
+ }
25631
+ await store.write(name, value, context.cwd);
25632
+ },
25633
+ describe: (name) => configured ? `${describeSecret(configured, name)}, or ${describeFile(name)} on this machine` : `${secretVariableName(name)} in the environment, or ${describeFile(name)} on this machine`
25634
+ };
25635
+ };
25636
+ /**
25637
+ * The `localSecrets()` store `secrets` reads after its adapter, and stores
25638
+ * in when the adapter stores nothing: `localSecrets({ path })`'s, else the
25639
+ * default one.
25640
+ */
25641
+ const getLocalSecretsStore = (secrets) => isLocalKeyStoreSecrets(secrets) ? secrets[LOCAL_KEY_STORE].store : defaultLocalSecretsStore;
25642
+ /**
25643
+ * The config's own `secrets` adapter, without `localSecrets()`'s store
25644
+ * after it: undefined for `localSecrets()`, and when the config sets none.
25645
+ * `secretEnv` reads deploy credentials from it.
25646
+ */
25647
+ const getConfiguredAdapter = (secrets) => isLocalKeyStoreSecrets(secrets) ? secrets[LOCAL_KEY_STORE].configured : secrets;
25648
+ /**
25649
+ * Where `secrets.set` stores `name`, for messages: the config's adapter
25650
+ * when it stores values, else `localSecrets()`'s file.
25651
+ */
25652
+ const describeStoredSecret = (secrets, name) => {
25653
+ const configured = getConfiguredAdapter(secrets);
25654
+ return configured?.set ? describeSecret(configured, name) : getLocalSecretsStore(secrets).file(name, getCwd());
25655
+ };
25656
+ /**
25657
+ * Whether a key `secrets` stores stays on this machine: it is `localSecrets()`,
25658
+ * whose files no other machine reads.
25659
+ */
25660
+ const keepsKeysOnThisMachine = (secrets) => getConfiguredAdapter(secrets) === void 0;
25661
+ const CONFIGURED_SECRETS = Symbol.for("@hot-updater/cli-tools/secrets");
25662
+ const LOADING_SCOPE = Symbol.for("@hot-updater/cli-tools/secrets-scope");
25663
+ /**
25664
+ * Evaluates a config, the file or a config function, with `scope` as the
25665
+ * scope of every `secretEnv` it creates, so each reads the `secrets` of the
25666
+ * config that created it, whatever config loads after. It is kept on
25667
+ * `globalThis`: the config file loads its own copy of Hot Updater's modules.
25668
+ */
25669
+ const withSecretsScope = async (scope, evaluate) => {
25670
+ const slot = globalThis;
25671
+ const previous = slot[LOADING_SCOPE];
25672
+ slot[LOADING_SCOPE] = scope;
25673
+ try {
25674
+ return await evaluate();
25675
+ } finally {
25676
+ slot[LOADING_SCOPE] = previous;
25677
+ }
25678
+ };
25679
+ /**
25680
+ * Makes `secrets` the adapter a credential resolver reads from by default,
25681
+ * and a `secretEnv` created outside any config load, as `loadConfig` does
25682
+ * with the config's. It is kept on `globalThis`: the config file loads Hot
25683
+ * Updater's modules again, as its own copies.
25684
+ */
25685
+ const setConfiguredSecrets = (secrets) => {
25686
+ globalThis[CONFIGURED_SECRETS] = secrets;
25687
+ };
25688
+ /**
25689
+ * The config's `secrets` adapter with `localSecrets()`'s store after it
25690
+ * (`withLocalKeyStore`), `localSecrets()` until a config loads.
25691
+ */
25692
+ const getConfiguredSecrets = () => globalThis[CONFIGURED_SECRETS] ?? localSecrets();
25693
+ /** Where an adapter keeps `name`, for messages. */
25694
+ const describeSecret = (secrets, name) => secrets.describe?.(name) ?? `${name} in ${secrets.name}`;
25695
+ const list = (names) => names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
25696
+ /**
25697
+ * Environment variables from the `secrets` adapter of the config that
25698
+ * creates it, as a function a config calls when an adapter needs them, so loading
25699
+ * `hot-updater.config.ts` reads no secret. Each name set in the environment
25700
+ * or `.env.hotupdater` wins; the others are read from the config's
25701
+ * `secrets` adapter in one call, and without one they must be set. The values go only to the caller: they are never set in
25702
+ * `process.env`, and are masked in the CLI's logs. Names a loader or
25703
+ * interpreter reads, such as `NODE_OPTIONS`, are refused.
25704
+ *
25705
+ * @example
25706
+ * const deploy = secretEnv(["S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"]);
25707
+ * s3Storage({
25708
+ * bucketName: "bundles",
25709
+ * region: "us-east-1",
25710
+ * credentials: async () => {
25711
+ * const env = await deploy();
25712
+ * return {
25713
+ * accessKeyId: env.S3_ACCESS_KEY_ID,
25714
+ * secretAccessKey: env.S3_SECRET_ACCESS_KEY,
25715
+ * };
25716
+ * },
25717
+ * });
25718
+ */
25719
+ const secretEnv = (names) => {
25720
+ const wanted = [...new Set(names)];
25721
+ const invalid = wanted.filter((name) => !ENV_NAME_PATTERN.test(name));
25722
+ if (invalid.length > 0) throw new Error(`secretEnv takes environment variable names: letters, digits, and underscores, not starting with a digit, and ${invalid.map((name) => JSON.stringify(name)).join(", ")} ${invalid.length === 1 ? "is" : "are"} not.`);
25723
+ const reserved = wanted.filter(isReservedVariableName);
25724
+ if (reserved.length > 0) throw new Error(`secretEnv refuses ${list(reserved)}: ${reserved.length === 1 ? "it changes" : "they change"} how a process loads code or which credentials Hot Updater uses.`);
25725
+ const scope = globalThis[LOADING_SCOPE];
25726
+ return async () => {
25727
+ const cwd = getCwd();
25728
+ const fileEnv = await readLoadedHotUpdaterEnv(cwd);
25729
+ const variables = Object.create(null);
25730
+ const fromStore = [];
25731
+ for (const name of wanted) {
25732
+ const value = getHotUpdaterEnvValue(fileEnv, name);
25733
+ if (value === void 0) fromStore.push(name);
25734
+ else variables[name] = value;
25735
+ }
25736
+ if (fromStore.length > 0) {
25737
+ const secrets = getConfiguredAdapter(scope?.secrets ?? getConfiguredSecrets());
25738
+ const read = secrets ? await secrets.get(fromStore, { cwd }) : {};
25739
+ const missing = fromStore.filter((name) => {
25740
+ const value = Object.hasOwn(read, name) ? read[name] : void 0;
25741
+ if (typeof value !== "string" || value === "") return true;
25742
+ variables[name] = value;
25743
+ return false;
25744
+ });
25745
+ if (missing.length > 0) {
25746
+ const them = missing.length === 1 ? "it" : "them";
25747
+ throw new Error(secrets === void 0 ? `secretEnv found no value for ${list(missing)}: set ${them} in the environment or .env.hotupdater, or read ${them} from a secrets store with the config's secrets.` : `secretEnv found no value for ${list(missing)}: ${missing.length === 1 ? "it is" : "they are"} not set in the environment or .env.hotupdater, and the ${secrets.name} secrets adapter does not hold ${them} (${missing.map((name) => describeSecret(secrets, name)).join("; ")}).`);
25748
+ }
25749
+ }
25750
+ for (const value of Object.values(variables)) registerSecretValue(value);
25751
+ return Object.freeze(variables);
25752
+ };
25753
+ };
25754
+ //#endregion
24928
25755
  //#region src/localBundleSigning.ts
25756
+ /** The secret `keys generate --secret` stores the private key as by default. */
25757
+ const DEFAULT_SIGNING_KEY_SECRET = "bundle-signing-key";
25758
+ /**
25759
+ * A local signing failure whose message the CLI prints as it is: it names
25760
+ * the secret and how to store it, and quotes no key material.
25761
+ */
25762
+ var LocalSigningKeyError = class extends Error {
25763
+ constructor(message, options) {
25764
+ super(message, options);
25765
+ this.name = "HotUpdaterLocalSigningKeyError";
25766
+ }
25767
+ };
24929
25768
  const resolvePath$1 = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.resolve(cwd, filePath);
24930
- const loadPrivateKey = async (privateKeyPath) => {
25769
+ const parsePrivateKey = (pem) => {
24931
25770
  try {
24932
- const privateKey = crypto$1.createPrivateKey(await fs$2.readFile(privateKeyPath, "utf8"));
25771
+ const privateKey = crypto$1.createPrivateKey(pem);
24933
25772
  if (privateKey.asymmetricKeyType !== "rsa" || (privateKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("not rsa");
24934
25773
  return privateKey;
24935
25774
  } catch {
24936
25775
  throw new Error("Failed to load the local bundle signing private key.");
24937
25776
  }
24938
25777
  };
24939
- const createLocalSigningAdapter = ({ privateKeyPath }) => {
25778
+ const loadPrivateKeyFile = async (privateKeyPath) => {
25779
+ let pem;
25780
+ try {
25781
+ pem = await fs$2.readFile(privateKeyPath, "utf8");
25782
+ } catch {
25783
+ throw new Error("Failed to load the local bundle signing private key.");
25784
+ }
25785
+ return parsePrivateKey(pem);
25786
+ };
25787
+ /**
25788
+ * The private key the `secrets` adapter holds as `name`: masked in the CLI's
25789
+ * output before it is parsed, and kept only as a key object. A PEM written
25790
+ * on one line, with `\n` for its line breaks, as a `.env.hotupdater` value
25791
+ * or a CI variable may hold it, reads as the PEM.
25792
+ */
25793
+ const loadPrivateKeySecret = async (secrets, name, cwd) => {
25794
+ let value;
25795
+ try {
25796
+ const read = await secrets.get([name], { cwd });
25797
+ value = Object.hasOwn(read, name) ? read[name] : void 0;
25798
+ } catch (error) {
25799
+ const reason = error instanceof Error ? error.message : String(error);
25800
+ throw new LocalSigningKeyError(`The ${secrets.name} secrets adapter could not read the bundle signing key ${name}: ${reason}`, { cause: error });
25801
+ }
25802
+ if (typeof value !== "string" || !value.trim()) throw new LocalSigningKeyError(`signing.privateKey reads the bundle signing key ${name}, which is not in the ${secrets.name} secrets adapter (${describeSecret(secrets, name)}). Set it to the RSA private key in PEM, or generate a key pair with \`npx hot-updater keys generate --secret ${name}\`.`);
25803
+ registerSecretValue(value);
25804
+ const pem = value.includes("\n") ? value : value.replaceAll("\\n", "\n");
25805
+ if (pem !== value) registerSecretValue(pem);
25806
+ try {
25807
+ return parsePrivateKey(pem);
25808
+ } catch {
25809
+ throw new LocalSigningKeyError(`The bundle signing key ${name} in the ${secrets.name} secrets adapter is not an RSA private key in PEM of at least 2048 bits.`);
25810
+ }
25811
+ };
25812
+ /**
25813
+ * The built-in local signer. Its private key is read on first use, once:
25814
+ * from `privateKeyPath`, or from the config's `secrets` adapter for
25815
+ * `privateKey: "secret:<name>"`, and is never written anywhere.
25816
+ */
25817
+ const createLocalSigningAdapter = (config, { secrets } = {}) => {
24940
25818
  const privateKeys = /* @__PURE__ */ new Map();
25819
+ const { privateKeyPath } = config;
25820
+ const secretName = privateKeyPath === void 0 ? parseSecretReference(config.privateKey, "signing.privateKey") : void 0;
24941
25821
  const getPrivateKey = (cwd = process.cwd()) => {
24942
- const resolvedPath = resolvePath$1(cwd, privateKeyPath);
24943
- const cached = privateKeys.get(resolvedPath);
25822
+ const cacheKey = privateKeyPath === void 0 ? "" : resolvePath$1(cwd, privateKeyPath);
25823
+ const cached = privateKeys.get(cacheKey);
24944
25824
  if (cached) return cached;
24945
- const pending = loadPrivateKey(resolvedPath).catch((error) => {
24946
- privateKeys.delete(resolvedPath);
25825
+ const pending = (secretName === void 0 ? loadPrivateKeyFile(cacheKey) : loadPrivateKeySecret(secrets ?? getConfiguredSecrets(), secretName, cwd)).catch((error) => {
25826
+ privateKeys.delete(cacheKey);
24947
25827
  throw error;
24948
25828
  });
24949
- privateKeys.set(resolvedPath, pending);
25829
+ privateKeys.set(cacheKey, pending);
24950
25830
  return pending;
24951
25831
  };
24952
25832
  return {
@@ -24965,25 +25845,48 @@ const createLocalSigningAdapter = ({ privateKeyPath }) => {
24965
25845
  }
24966
25846
  };
24967
25847
  };
25848
+ /**
25849
+ * The `secrets` adapter of the config each normalized `privateKey` config
25850
+ * came from, so a config function's per-platform configs each read their
25851
+ * own store, whichever config loaded last.
25852
+ */
25853
+ const signingSecrets = /* @__PURE__ */ new WeakMap();
25854
+ /** The `secrets` adapter `normalizeSigningConfig` bound `signing` to. */
25855
+ const getSigningSecrets = (signing) => signingSecrets.get(signing);
24968
25856
  const invalidSigningConfig = () => /* @__PURE__ */ new Error("Bundle signing must be a local key config or signing adapter. Omit signing to disable it.");
24969
- const normalizeSigningConfig = (signing) => {
25857
+ const normalizeSigningConfig = (signing, { secrets } = {}) => {
24970
25858
  if (signing === void 0) return void 0;
24971
25859
  if (typeof signing !== "object" || signing === null) throw invalidSigningConfig();
24972
- const hasPrivateKeyPath = Reflect.has(signing, "privateKeyPath");
24973
- const hasEnabled = Reflect.has(signing, "enabled");
25860
+ const hasLocalFields = [
25861
+ "enabled",
25862
+ "privateKeyPath",
25863
+ "privateKey"
25864
+ ].some((key) => Reflect.has(signing, key));
24974
25865
  const hasPluginMembers = [
24975
25866
  "name",
24976
25867
  "getPublicKey",
24977
25868
  "sign"
24978
25869
  ].some((key) => Reflect.has(signing, key));
24979
- if ((hasPrivateKeyPath || hasEnabled) && hasPluginMembers) throw new Error("Bundle signing config cannot combine local signing fields with signing adapter fields.");
25870
+ if (hasLocalFields && hasPluginMembers) throw new Error("Bundle signing config cannot combine local signing fields with signing adapter fields.");
24980
25871
  if (!hasPluginMembers) {
24981
- if (Object.keys(signing).some((key) => key !== "enabled" && key !== "privateKeyPath")) throw new Error("Local bundle signing accepts only enabled and privateKeyPath.");
25872
+ if (Object.keys(signing).some((key) => key !== "enabled" && key !== "privateKeyPath" && key !== "privateKey")) throw new Error("Local bundle signing accepts only enabled, and privateKeyPath or privateKey.");
24982
25873
  const enabled = Reflect.get(signing, "enabled");
24983
25874
  if (enabled === false) return void 0;
24984
25875
  if (enabled !== true) throw invalidSigningConfig();
24985
25876
  const privateKeyPath = Reflect.get(signing, "privateKeyPath");
24986
- if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) throw new Error("Enabled local bundle signing requires privateKeyPath.");
25877
+ const privateKey = Reflect.get(signing, "privateKey");
25878
+ if (privateKeyPath !== void 0 && privateKey !== void 0) throw new Error("Local bundle signing takes its private key from privateKeyPath or from privateKey, not both.");
25879
+ if (privateKey !== void 0) {
25880
+ if (typeof privateKey !== "string") throw new Error("signing.privateKey names a secret as secret:<name>, such as \"secret:bundle-signing-key\".");
25881
+ parseSecretReference(privateKey, "signing.privateKey");
25882
+ const normalized = {
25883
+ enabled: true,
25884
+ privateKey
25885
+ };
25886
+ if (secrets) signingSecrets.set(normalized, secrets);
25887
+ return normalized;
25888
+ }
25889
+ if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) throw new Error("Enabled local bundle signing requires privateKeyPath, or privateKey as \"secret:<name>\".");
24987
25890
  return {
24988
25891
  enabled: true,
24989
25892
  privateKeyPath
@@ -25020,13 +25923,39 @@ const readBundleSigningPublicKeyFile = async (publicKeyPath, options = {}) => {
25020
25923
  throw new Error("Failed to read the bundle signing public key file.");
25021
25924
  }
25022
25925
  };
25926
+ /**
25927
+ * `remoteSigning`'s errors with a reason it wrote itself, by name, as it may
25928
+ * come from another copy of plugin-core, and the local signer's errors that
25929
+ * name its secret. Any other signing adapter error is redacted: it may quote
25930
+ * key material.
25931
+ */
25932
+ const REMOTE_SIGNING_ERROR_NAME = "HotUpdaterRemoteSigningError";
25933
+ const signingFailure = (summary, error) => new Error(error instanceof Error && (error.name === REMOTE_SIGNING_ERROR_NAME || error instanceof LocalSigningKeyError) ? `${summary} ${error.message}` : summary);
25934
+ /**
25935
+ * The local signer of each config's `signing` that reads its private key
25936
+ * from a secret, so the commands of one run that sign and derive the public
25937
+ * key read the secret once.
25938
+ */
25939
+ const secretSigners = /* @__PURE__ */ new WeakMap();
25940
+ const getSigningProvider = (signing) => {
25941
+ const normalized = normalizeSigningConfig(signing);
25942
+ if (!normalized) return null;
25943
+ if (!("enabled" in normalized)) return normalized;
25944
+ if (normalized.privateKey === void 0) return createLocalSigningAdapter(normalized);
25945
+ const key = signing;
25946
+ const cached = secretSigners.get(key);
25947
+ if (cached) return cached;
25948
+ const local = createLocalSigningAdapter(normalized, { secrets: getSigningSecrets(key) });
25949
+ secretSigners.set(key, local);
25950
+ return local;
25951
+ };
25023
25952
  const getProviderPublicKey = async (provider, cwd) => {
25024
25953
  try {
25025
25954
  const result = await provider.getPublicKey({ cwd });
25026
25955
  if (!result || typeof result.publicKey !== "string") throw new Error("invalid result");
25027
25956
  return parseRsaPublicKey(result.publicKey);
25028
- } catch {
25029
- throw new Error("Failed to resolve the bundle signing provider public key.");
25957
+ } catch (error) {
25958
+ throw signingFailure("Failed to resolve the bundle signing provider public key.", error);
25030
25959
  }
25031
25960
  };
25032
25961
  const createMemoizedSigner = ({ publicKey, sign }) => {
@@ -25041,8 +25970,8 @@ const createMemoizedSigner = ({ publicKey, sign }) => {
25041
25970
  let signature;
25042
25971
  try {
25043
25972
  signature = await sign(new Uint8Array(message));
25044
- } catch {
25045
- throw new Error("Bundle signing provider failed to sign the file hash.");
25973
+ } catch (error) {
25974
+ throw signingFailure("Bundle signing provider failed to sign the file hash.", error);
25046
25975
  }
25047
25976
  if (!(signature instanceof Uint8Array) || signature.byteLength === 0) throw new Error("Bundle signing provider returned an invalid signature.");
25048
25977
  if (!crypto$1.verify("RSA-SHA256", message, publicKey, signature)) throw new Error("Bundle signing provider returned a signature that does not match the configured public key.");
@@ -25072,18 +26001,15 @@ const preparePluginSigning = async (signing, cwd) => {
25072
26001
  };
25073
26002
  };
25074
26003
  const prepareBundleSigning = async (signing, options = {}) => {
25075
- const normalized = normalizeSigningConfig(signing);
25076
- if (!normalized) return null;
25077
- const cwd = options.cwd ?? getCwd();
25078
- return preparePluginSigning("enabled" in normalized ? createLocalSigningAdapter(normalized) : normalized, cwd);
26004
+ const provider = getSigningProvider(signing);
26005
+ if (!provider) return null;
26006
+ return preparePluginSigning(provider, options.cwd ?? getCwd());
25079
26007
  };
25080
26008
  /** Resolves the public identity of the configured bundle signer. */
25081
26009
  const getBundleSigningPublicKey = async (signing, options = {}) => {
25082
- const normalized = normalizeSigningConfig(signing);
25083
- if (!normalized) return null;
25084
- const cwd = options.cwd ?? getCwd();
25085
- const provider = "enabled" in normalized ? createLocalSigningAdapter(normalized) : normalized;
25086
- return exportPublicKey(await getProviderPublicKey(provider, cwd));
26010
+ const provider = getSigningProvider(signing);
26011
+ if (!provider) return null;
26012
+ return exportPublicKey(await getProviderPublicKey(provider, options.cwd ?? getCwd()));
25087
26013
  };
25088
26014
  //#endregion
25089
26015
  //#region src/ConfigBuilder.ts
@@ -25144,7 +26070,7 @@ var ConfigBuilder = class {
25144
26070
  intermediateCode = "";
25145
26071
  imports = [{
25146
26072
  pkg: "hot-updater",
25147
- named: ["defineConfig"]
26073
+ named: ["defineConfig", "localSecrets"]
25148
26074
  }, {
25149
26075
  pkg: "node:fs",
25150
26076
  named: ["existsSync"]
@@ -25215,6 +26141,7 @@ ${this.intermediateCode ? `${this.intermediateCode}\n\n` : ""}export default def
25215
26141
  storage: ${this.storageInfo.configString},
25216
26142
  database: ${this.databaseInfo.configString},
25217
26143
  ${plugins === "plugins" ? "plugins" : `plugins: ${plugins}`},
26144
+ secrets: localSecrets(),
25218
26145
  updateStrategy: "appVersion", // or "fingerprint"
25219
26146
  });
25220
26147
  `.trim();
@@ -37678,6 +38605,421 @@ const createZip = async ({ outfile, targetDir, excludeExts = [] }) => {
37678
38605
  return outfile;
37679
38606
  };
37680
38607
  //#endregion
38608
+ //#region src/credentialFiles.ts
38609
+ const TERMINATION_SIGNALS = [
38610
+ "SIGHUP",
38611
+ "SIGINT",
38612
+ "SIGTERM"
38613
+ ];
38614
+ const liveDirectories = /* @__PURE__ */ new Set();
38615
+ const onSignal = (signal) => {
38616
+ removeLiveDirectories();
38617
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
38618
+ };
38619
+ const removeLiveDirectories = () => {
38620
+ for (const directory of liveDirectories) rmSync(directory, {
38621
+ force: true,
38622
+ recursive: true
38623
+ });
38624
+ liveDirectories.clear();
38625
+ stopListening();
38626
+ };
38627
+ const startListening = () => {
38628
+ process.on("exit", removeLiveDirectories);
38629
+ for (const signal of TERMINATION_SIGNALS) process.on(signal, onSignal);
38630
+ };
38631
+ const stopListening = () => {
38632
+ process.off("exit", removeLiveDirectories);
38633
+ for (const signal of TERMINATION_SIGNALS) process.off(signal, onSignal);
38634
+ };
38635
+ const trackDirectory = (directory) => {
38636
+ if (liveDirectories.size === 0) startListening();
38637
+ liveDirectories.add(directory);
38638
+ };
38639
+ const untrackDirectory = (directory) => {
38640
+ if (liveDirectories.delete(directory) && liveDirectories.size === 0) stopListening();
38641
+ };
38642
+ /**
38643
+ * Creates the temporary files of one credential resolver: a `mkdtemp`
38644
+ * directory, created on the first write and removed by `dispose` or, if the
38645
+ * process exits first, on `exit` or a termination signal.
38646
+ */
38647
+ const createCredentialFiles = () => {
38648
+ let directory;
38649
+ let disposal;
38650
+ const ensureDirectory = () => {
38651
+ directory ??= fs$2.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-credentials-")).then((created) => {
38652
+ trackDirectory(created);
38653
+ return created;
38654
+ });
38655
+ return directory;
38656
+ };
38657
+ return {
38658
+ write: async (fileName, contents) => {
38659
+ if (disposal) throw new Error("The credential files have been disposed.");
38660
+ if (typeof contents === "string") registerSecretValue(contents);
38661
+ const fileDirectory = await fs$2.mkdtemp(path$1.join(await ensureDirectory(), "item-"));
38662
+ const filePath = path$1.join(fileDirectory, path$1.basename(fileName));
38663
+ await fs$2.writeFile(filePath, contents, {
38664
+ flag: "wx",
38665
+ mode: 384
38666
+ });
38667
+ return filePath;
38668
+ },
38669
+ dispose: () => {
38670
+ disposal ??= (async () => {
38671
+ const created = await directory?.catch(() => void 0);
38672
+ if (!created) return;
38673
+ await fs$2.rm(created, {
38674
+ force: true,
38675
+ recursive: true
38676
+ });
38677
+ untrackDirectory(created);
38678
+ })();
38679
+ return disposal;
38680
+ }
38681
+ };
38682
+ };
38683
+ //#endregion
38684
+ //#region src/credentialResolver.ts
38685
+ /** Passwords are read as set: trimming would change them. */
38686
+ const UNTRIMMED_ENV_NAMES = /* @__PURE__ */ new Set(["HOT_UPDATER_ANDROID_KEYSTORE_PASSWORD", "HOT_UPDATER_ANDROID_KEY_PASSWORD"]);
38687
+ const describeField = ([name, alternative]) => alternative ? `${name} (or ${alternative})` : name;
38688
+ const describeFields = (fields) => {
38689
+ const described = fields.map(describeField);
38690
+ if (described.length <= 2) return described.join(" and ");
38691
+ return `${described.slice(0, -1).join(", ")}, and ${described.at(-1)}`;
38692
+ };
38693
+ /**
38694
+ * The sentence every "credential unavailable" message ends with: the
38695
+ * environment variables that provide `kind` whatever the secrets adapter.
38696
+ */
38697
+ const credentialOverrideHint = (kind) => `Or set ${describeFields(CREDENTIAL_ENV_NAMES[kind])} to provide ${kind} from the environment.`;
38698
+ /**
38699
+ * The command that stores a credential of `kind` you have as `name` through
38700
+ * the config's `secrets` adapter, from its files.
38701
+ */
38702
+ const credentialPutCommand = (kind, name = kind) => {
38703
+ const put = `npx hot-updater secrets put ${name} --kind ${kind}`;
38704
+ switch (kind) {
38705
+ case "android-keystore": return `${put} --keystore <release.jks> --key-alias <alias>`;
38706
+ case "asc-api-key": return `${put} --p8 <AuthKey.p8> --key-id <id> --issuer-id <id>`;
38707
+ case "play-service-account": return `${put} --service-account <key.json>`;
38708
+ }
38709
+ };
38710
+ /**
38711
+ * The command that signs in once, at a terminal, and stores the key Hot
38712
+ * Updater issues as `name`: an App Store Connect API key after an Apple
38713
+ * Account sign-in, a Play service account after a Google one.
38714
+ */
38715
+ const credentialIssueCommand = (kind, name = kind) => `npx hot-updater secrets issue ${kind}${name === kind ? "" : ` ${name}`}`;
38716
+ /** The command group of each issued kind's shortcut, `<group> login`. */
38717
+ const CREDENTIAL_LOGIN_GROUPS = {
38718
+ "asc-api-key": "apple",
38719
+ "play-service-account": "google-play"
38720
+ };
38721
+ /**
38722
+ * The command people meet first that issues `kind` as `name`: its shortcut,
38723
+ * `apple login` for an App Store Connect API key or `google-play login`
38724
+ * for a Play service account, which takes nothing and stores the kind's
38725
+ * own name; for another name, `credentialIssueCommand`.
38726
+ */
38727
+ const credentialLoginCommand = (kind, name = kind) => name === kind ? `npx hot-updater ${CREDENTIAL_LOGIN_GROUPS[kind]} login` : credentialIssueCommand(kind, name);
38728
+ /**
38729
+ * The command that stores a missing credential of `kind` as `name`: the
38730
+ * keystore you have, or the key Hot Updater issues after a sign-in.
38731
+ */
38732
+ const credentialStoreCommand = (kind, name = kind) => kind === "android-keystore" ? credentialPutCommand(kind, name) : credentialLoginCommand(kind, name);
38733
+ var CredentialUnavailableError = class extends Error {
38734
+ kind;
38735
+ /** The environment variables that can provide this credential. */
38736
+ envNames;
38737
+ constructor(kind, message, options) {
38738
+ super(message, options);
38739
+ this.name = "CredentialUnavailableError";
38740
+ this.kind = kind;
38741
+ this.envNames = CREDENTIAL_ENV_NAMES[kind].flat();
38742
+ }
38743
+ };
38744
+ const readEnvField = (env, field) => {
38745
+ for (const name of field) {
38746
+ const value = UNTRIMMED_ENV_NAMES.has(name) ? (process.env[name] ?? env[name]) || void 0 : getHotUpdaterEnvValue(env, name);
38747
+ if (value !== void 0) return {
38748
+ name,
38749
+ value
38750
+ };
38751
+ }
38752
+ };
38753
+ const readFileField = async (kind, { name, value }) => {
38754
+ const filePath = path$1.resolve(value);
38755
+ if (!await fs$2.stat(filePath).then(async (stats) => {
38756
+ await fs$2.access(filePath, fs$2.constants.R_OK);
38757
+ return stats.isFile();
38758
+ }).catch(() => false)) throw new CredentialUnavailableError(kind, `${name} does not point to a readable file.`);
38759
+ return filePath;
38760
+ };
38761
+ const parseServiceAccount = (json) => {
38762
+ try {
38763
+ return JSON.parse(json);
38764
+ } catch {
38765
+ return;
38766
+ }
38767
+ };
38768
+ const fromEnv = {
38769
+ "asc-api-key": async ([key, keyId, issuerId], { files }) => ({
38770
+ keyPath: key.name === "HOT_UPDATER_ASC_KEY_PATH" ? await readFileField("asc-api-key", key) : await files.write(`AuthKey_${keyId.value}.p8`, key.value.includes("\n") ? key.value : key.value.replaceAll("\\n", "\n")),
38771
+ keyId: keyId.value,
38772
+ issuerId: issuerId.value
38773
+ }),
38774
+ "android-keystore": async ([keystore, storePassword, keyAlias, keyPassword], { files, name }) => {
38775
+ let storeFile;
38776
+ if (keystore.name === "HOT_UPDATER_ANDROID_KEYSTORE_PATH") storeFile = await readFileField("android-keystore", keystore);
38777
+ else {
38778
+ registerSecretValue(keystore.value);
38779
+ const contents = decodeBase64(keystore.value);
38780
+ if (!contents) throw new CredentialUnavailableError("android-keystore", `${keystore.name} is not a base64-encoded keystore.`);
38781
+ storeFile = await files.write(`${name}.keystore`, contents);
38782
+ }
38783
+ return {
38784
+ storeFile,
38785
+ storePassword: storePassword.value,
38786
+ keyAlias: keyAlias.value,
38787
+ keyPassword: keyPassword.value
38788
+ };
38789
+ },
38790
+ "play-service-account": async ([serviceAccount]) => {
38791
+ const json = serviceAccount.name === "HOT_UPDATER_PLAY_SERVICE_ACCOUNT_PATH" ? await fs$2.readFile(await readFileField("play-service-account", serviceAccount), "utf-8") : serviceAccount.value;
38792
+ const parsed = parseServiceAccount(json);
38793
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CredentialUnavailableError("play-service-account", `${serviceAccount.name} is not a service account JSON object.`);
38794
+ return { json };
38795
+ }
38796
+ };
38797
+ /** Fields of a service account that are no secret: its type and Google's endpoints. */
38798
+ /**
38799
+ * Fields of a JSON credential that identify it rather than grant access:
38800
+ * its IDs, key alias, and a service account's email, project, type, and
38801
+ * Google's endpoints. They stay readable, as messages name them, and a
38802
+ * short one, such as the alias `release`, would mask every word it is in.
38803
+ */
38804
+ const isIdentifierField = (field) => [
38805
+ "keyId",
38806
+ "issuerId",
38807
+ "keyAlias",
38808
+ "type",
38809
+ "project_id",
38810
+ "client_email",
38811
+ "client_id",
38812
+ "private_key_id",
38813
+ "universe_domain"
38814
+ ].includes(field) || field.endsWith("_uri") || field.endsWith("_url");
38815
+ const registerJsonSecrets = (value, fields) => {
38816
+ registerSecretValue(value);
38817
+ for (const [field, entry] of Object.entries(fields)) if (typeof entry === "string" && !isIdentifierField(field)) registerSecretValue(entry);
38818
+ };
38819
+ /**
38820
+ * Masks a credential's JSON value in the CLI's output: the whole value, and
38821
+ * each field but those that identify it, such as `keyId` and `keyAlias`,
38822
+ * so a password or a key a log prints alone stays masked. A value that is
38823
+ * no JSON object is masked whole.
38824
+ */
38825
+ const registerCredentialValue = (value) => {
38826
+ const parsed = parseObject(value);
38827
+ if (parsed) registerJsonSecrets(value, parsed);
38828
+ else registerSecretValue(value);
38829
+ };
38830
+ const parseObject = (value) => {
38831
+ const parsed = parseServiceAccount(value);
38832
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
38833
+ };
38834
+ /**
38835
+ * The fields of a JSON credential, each a non-empty string, or an error
38836
+ * naming the shape `kind` takes.
38837
+ */
38838
+ const readJsonFields = (kind, item, value, fields) => {
38839
+ const parsed = parseObject(value);
38840
+ const shape = `{${fields.map((field) => `"${field}"`).join(", ")}}`;
38841
+ if (!parsed) throw new CredentialUnavailableError(kind, `${item} is not a JSON object: ${aKind(kind)} is one JSON value, ${shape}.`);
38842
+ registerJsonSecrets(value, parsed);
38843
+ const missing = fields.filter((field) => typeof parsed[field] !== "string" || !parsed[field]);
38844
+ if (missing.length > 0) throw new CredentialUnavailableError(kind, `${item} has no ${missing.join(", ")}: ${aKind(kind)} is one JSON value, ${shape}.`);
38845
+ return parsed;
38846
+ };
38847
+ const aKind = (kind) => `${/^[aeiou]/u.test(kind) ? "an" : "a"} ${kind}`;
38848
+ /** A base64 value's bytes, or undefined when it is not base64. */
38849
+ const decodeBase64 = (value) => {
38850
+ const base64 = value.replace(/\s/g, "");
38851
+ const contents = Buffer.from(base64, "base64");
38852
+ return contents.length > 0 && contents.toString("base64").replace(/=+$/, "") === base64.replace(/=+$/, "") ? contents : void 0;
38853
+ };
38854
+ /**
38855
+ * Each kind from the one string a secrets adapter holds for it: a JSON
38856
+ * object for the App Store Connect key (`p8` is the PEM) and the keystore
38857
+ * (`keystore` is base64), and the service account's own JSON. File values
38858
+ * are written with `files`; every value is masked.
38859
+ */
38860
+ const fromSecretValue = {
38861
+ "asc-api-key": async (value, { files, item }) => {
38862
+ const { keyId, issuerId, p8 } = readJsonFields("asc-api-key", item, value, [
38863
+ "keyId",
38864
+ "issuerId",
38865
+ "p8"
38866
+ ]);
38867
+ return {
38868
+ keyPath: await files.write(`AuthKey_${keyId}.p8`, p8.includes("\n") ? p8 : p8.replaceAll("\\n", "\n")),
38869
+ keyId,
38870
+ issuerId
38871
+ };
38872
+ },
38873
+ "android-keystore": async (value, { files, name, item }) => {
38874
+ const { keystore, storePassword, keyAlias, keyPassword } = readJsonFields("android-keystore", item, value, [
38875
+ "keystore",
38876
+ "storePassword",
38877
+ "keyAlias",
38878
+ "keyPassword"
38879
+ ]);
38880
+ const contents = decodeBase64(keystore);
38881
+ if (!contents) throw new CredentialUnavailableError("android-keystore", `${item}'s keystore is not a base64-encoded keystore.`);
38882
+ return {
38883
+ storeFile: await files.write(`${name}.keystore`, contents),
38884
+ storePassword,
38885
+ keyAlias,
38886
+ keyPassword
38887
+ };
38888
+ },
38889
+ "play-service-account": async (value, { item }) => {
38890
+ const parsed = parseObject(value);
38891
+ if (!parsed) throw new CredentialUnavailableError("play-service-account", `${item} is not a service account JSON object.`);
38892
+ registerJsonSecrets(value, parsed);
38893
+ return { json: value };
38894
+ }
38895
+ };
38896
+ const registerCredentialSecrets = (kind, credential) => {
38897
+ if (kind === "android-keystore") {
38898
+ const keystore = credential;
38899
+ registerSecretValue(keystore.storePassword);
38900
+ registerSecretValue(keystore.keyPassword);
38901
+ }
38902
+ if (kind === "play-service-account") {
38903
+ const { json } = credential;
38904
+ registerSecretValue(json);
38905
+ const privateKey = parseServiceAccount(json)?.private_key;
38906
+ if (typeof privateKey === "string") registerSecretValue(privateKey);
38907
+ }
38908
+ };
38909
+ /**
38910
+ * Reads names from `secrets`, the names asked for in one turn of the event
38911
+ * loop, such as a build's credentials, in one `get`. Nothing is cached.
38912
+ */
38913
+ const createSecretLoader = (secrets, cwd) => {
38914
+ let batch;
38915
+ const flush = async (names) => {
38916
+ try {
38917
+ const read = await secrets().get([...names.keys()], { cwd: cwd() });
38918
+ for (const [name, waiters] of names) {
38919
+ const value = Object.hasOwn(read, name) ? read[name] : void 0;
38920
+ for (const waiter of waiters) waiter.resolve(typeof value === "string" && value ? value : void 0);
38921
+ }
38922
+ } catch (error) {
38923
+ for (const waiters of names.values()) for (const waiter of waiters) waiter.reject(error);
38924
+ }
38925
+ };
38926
+ return (name) => new Promise((resolve, reject) => {
38927
+ if (batch === void 0) {
38928
+ const names = /* @__PURE__ */ new Map();
38929
+ batch = names;
38930
+ setTimeout(() => {
38931
+ batch = void 0;
38932
+ flush(names);
38933
+ }, 0);
38934
+ }
38935
+ const waiters = batch.get(name) ?? [];
38936
+ waiters.push({
38937
+ resolve,
38938
+ reject
38939
+ });
38940
+ batch.set(name, waiters);
38941
+ });
38942
+ };
38943
+ /**
38944
+ * Resolves each item from its environment overrides when any is set, and
38945
+ * otherwise from the config's `secrets` adapter as one value named by the
38946
+ * item, so overrides never reach the store. When the adapter fails or holds
38947
+ * no such value, the error names the environment variables that can
38948
+ * provide the item instead. Nothing is cached, values are never written to
38949
+ * disk except as 0600 temporary files, are never set in `process.env`, and
38950
+ * every secret is masked in logs.
38951
+ */
38952
+ const createCredentialResolver = ({ env = {}, secrets, cwd } = {}) => {
38953
+ const files = createCredentialFiles();
38954
+ const resolved = /* @__PURE__ */ new Map();
38955
+ let disposal;
38956
+ const adapter = () => typeof secrets === "function" ? secrets() : secrets ?? getConfiguredSecrets();
38957
+ const load = createSecretLoader(adapter, () => cwd ?? getCwd());
38958
+ const resolveItem = async (kind, name) => {
38959
+ const fields = CREDENTIAL_ENV_NAMES[kind];
38960
+ const values = fields.map((field) => readEnvField(env, field));
38961
+ if (values.some((value) => value !== void 0)) {
38962
+ const missing = fields.filter((_, index) => !values[index]);
38963
+ if (missing.length > 0) throw new CredentialUnavailableError(kind, `The environment overrides ${kind} but ${describeFields(missing)} ${missing.length === 1 ? "is" : "are"} not set.`);
38964
+ const credential = await fromEnv[kind](values, {
38965
+ files,
38966
+ name
38967
+ });
38968
+ registerCredentialSecrets(kind, credential);
38969
+ return credential;
38970
+ }
38971
+ const store = adapter();
38972
+ const item = name === kind ? kind : `${name} (${kind})`;
38973
+ let value;
38974
+ try {
38975
+ value = await load(name);
38976
+ } catch (error) {
38977
+ const reason = error instanceof Error ? error.message : String(error);
38978
+ throw new CredentialUnavailableError(kind, `The ${store.name} secrets adapter could not read ${item}: ${reason} ${credentialOverrideHint(kind)}`, { cause: error });
38979
+ }
38980
+ if (value === void 0) throw new CredentialUnavailableError(kind, `${item} is not in the ${store.name} secrets adapter (${describeSecret(store, name)}). Store it once with \`${credentialStoreCommand(kind, name)}\`. ${credentialOverrideHint(kind)}`);
38981
+ registerSecretValue(value);
38982
+ return fromSecretValue[kind](value, {
38983
+ files,
38984
+ name,
38985
+ item: `The ${store.name} secrets adapter's ${item}`
38986
+ });
38987
+ };
38988
+ return {
38989
+ resolve: ({ kind, name = kind }) => {
38990
+ if (disposal) return Promise.reject(/* @__PURE__ */ new Error("The credential resolver has been disposed."));
38991
+ const key = `${kind}\u0000${name}`;
38992
+ let pending = resolved.get(key);
38993
+ if (!pending) {
38994
+ pending = resolveItem(kind, name);
38995
+ resolved.set(key, pending);
38996
+ pending.catch(() => resolved.delete(key));
38997
+ }
38998
+ return pending;
38999
+ },
39000
+ dispose: () => {
39001
+ disposal ??= (async () => {
39002
+ await Promise.allSettled(resolved.values());
39003
+ resolved.clear();
39004
+ await files.dispose();
39005
+ })();
39006
+ return disposal;
39007
+ }
39008
+ };
39009
+ };
39010
+ /**
39011
+ * Runs `run` with a credential resolver and deletes its temporary files
39012
+ * when `run` settles.
39013
+ */
39014
+ const withCredentialResolver = async (run, options) => {
39015
+ const resolver = createCredentialResolver(options);
39016
+ try {
39017
+ return await run(resolver);
39018
+ } finally {
39019
+ await resolver.dispose();
39020
+ }
39021
+ };
39022
+ //#endregion
37681
39023
  //#region src/isObject.ts
37682
39024
  const isObject$2 = (value) => {
37683
39025
  return !!value && value.constructor === Object;
@@ -44397,11 +45739,13 @@ const findManagedProperty = (objectExpression, propertyName) => {
44397
45739
  const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && !candidate.shorthand && getObjectPropertyName(candidate) === propertyName);
44398
45740
  return property?.type === "Property" ? property : null;
44399
45741
  };
44400
- /** The `plugins` property, written either way: `plugins: [...]` or `plugins`. */
44401
- const findPluginsProperty = (objectExpression) => {
44402
- const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && getObjectPropertyName(candidate) === "plugins");
45742
+ /** The `name` property, written either way: `name: value` or `name`. */
45743
+ const findAnyProperty = (objectExpression, name) => {
45744
+ const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && getObjectPropertyName(candidate) === name);
44403
45745
  return property?.type === "Property" ? property : null;
44404
45746
  };
45747
+ /** The `plugins` property, written either way: `plugins: [...]` or `plugins`. */
45748
+ const findPluginsProperty = (objectExpression) => findAnyProperty(objectExpression, "plugins");
44405
45749
  const getCallCallee = (expression) => {
44406
45750
  if (expression.type !== "CallExpression" || expression.callee.type !== "Identifier") return null;
44407
45751
  return expression.callee.name;
@@ -44492,6 +45836,8 @@ const updateManagedObject = (existing, next, droppedHelpers) => {
44492
45836
  text: nextPluginsText
44493
45837
  });
44494
45838
  }
45839
+ const nextSecrets = findAnyProperty(next.objectExpression, "secrets");
45840
+ if (nextSecrets && !findAnyProperty(existing.objectExpression, "secrets")) missingPropertyTexts.push(getPropertyBlockText(next.source, nextSecrets));
44495
45841
  return {
44496
45842
  text: applyTextEdits(objectText, [...propertyEdits, ...appendPropertiesEdits(existing.source, existing.objectExpression, missingPropertyTexts)]),
44497
45843
  keptBuild
@@ -44570,9 +45916,13 @@ const keptManagedImports = (declarations, scaffoldImports, usedText) => {
44570
45916
  };
44571
45917
  };
44572
45918
  const rebuildImportBlock = (source, scaffold, { keptBuild, usedText }) => {
45919
+ const references = usedIdentifiers(usedText);
44573
45920
  const imports = scaffold.imports.filter((info) => !(keptBuild && BUILD_IMPORT_PACKAGES.has(info.pkg))).map((info) => info.pkg === "node:fs" ? {
44574
45921
  ...info,
44575
45922
  named: info.named?.filter((name) => name !== "existsSync")
45923
+ } : info.pkg === "hot-updater" && references && !references.has("localSecrets") ? {
45924
+ ...info,
45925
+ named: info.named?.filter((name) => name !== "localSecrets")
44576
45926
  } : info);
44577
45927
  const importDeclarations = importDeclarationsOf(source);
44578
45928
  const firstImport = importDeclarations[0];
@@ -44818,102 +46168,6 @@ const writeHotUpdaterFiles = async (scaffold, { cwd = process.cwd(), settings })
44818
46168
  return { config };
44819
46169
  };
44820
46170
  //#endregion
44821
- //#region src/initOptions.ts
44822
- var InitError = class extends Error {
44823
- name = "InitError";
44824
- };
44825
- var MissingInitInputsError = class extends InitError {
44826
- missingInputs;
44827
- name = "MissingInitInputsError";
44828
- constructor(missingInputs) {
44829
- super(["Init is missing required inputs:", ...missingInputs.map((input) => `- ${input}`)].join("\n"));
44830
- this.missingInputs = missingInputs;
44831
- }
44832
- };
44833
- var InitEnvFileError = class extends InitError {
44834
- name = "InitEnvFileError";
44835
- };
44836
- var LegacyInfrastructureError = class extends InitError {
44837
- name = "LegacyInfrastructureError";
44838
- constructor(provider, resource) {
44839
- super(`${provider} v0 infrastructure was detected at ${resource}. Hot Updater v1 cannot upgrade it in place. Run init with new provider resources and ship the new endpoint in a new native build. The existing infrastructure was not changed.`);
44840
- }
44841
- };
44842
- const assertInitInputs = ({ inputs, strict }) => {
44843
- if (!strict) return;
44844
- const missingInputs = getMissingInitInputs(inputs);
44845
- if (missingInputs.length > 0) throw new MissingInitInputsError(missingInputs);
44846
- };
44847
- const getMissingInitInputs = (inputs) => Object.entries(inputs).filter(([, value]) => !value?.trim()).map(([key]) => key);
44848
- //#endregion
44849
- //#region src/hotUpdaterEnv.ts
44850
- const HOT_UPDATER_ENV_PATH = ".env.hotupdater";
44851
- const unquoteEnvValue = (value) => {
44852
- const trimmed = value.trim();
44853
- if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) try {
44854
- const parsed = JSON.parse(trimmed);
44855
- return typeof parsed === "string" ? parsed : trimmed.slice(1, -1);
44856
- } catch {
44857
- return trimmed.slice(1, -1);
44858
- }
44859
- return trimmed.startsWith("'") && trimmed.endsWith("'") ? trimmed.slice(1, -1) : trimmed;
44860
- };
44861
- const parseEnv = (content) => {
44862
- const env = {};
44863
- for (const line of content.split("\n")) {
44864
- const trimmed = line.trim();
44865
- if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
44866
- const separatorIndex = trimmed.indexOf("=");
44867
- const key = trimmed.slice(0, separatorIndex).trim();
44868
- if (key) env[key] = unquoteEnvValue(trimmed.slice(separatorIndex + 1));
44869
- }
44870
- return env;
44871
- };
44872
- const readEnvFile = async (filePath, allowMissing) => {
44873
- let content;
44874
- try {
44875
- content = await fs$1.readFile(filePath, "utf-8");
44876
- } catch (error) {
44877
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
44878
- if (allowMissing) return {};
44879
- throw new InitEnvFileError(`Init environment file not found: ${filePath}`);
44880
- }
44881
- throw error;
44882
- }
44883
- return parseEnv(content);
44884
- };
44885
- const getHotUpdaterInitInputEnv = ({ env, managedEnv }, nonInteractive) => nonInteractive ? env : managedEnv;
44886
- const readHotUpdaterInitEnv = async (cwd, envFile) => {
44887
- const savedEnvPath = path.resolve(cwd, HOT_UPDATER_ENV_PATH);
44888
- const savedEnv = await readEnvFile(savedEnvPath, true);
44889
- if (!envFile) return {
44890
- env: {},
44891
- managedEnv: savedEnv
44892
- };
44893
- const inputEnvPath = path.resolve(cwd, envFile);
44894
- if (inputEnvPath === savedEnvPath) return {
44895
- env: savedEnv,
44896
- managedEnv: savedEnv
44897
- };
44898
- const inputEnv = await readEnvFile(inputEnvPath, false);
44899
- return {
44900
- env: {
44901
- ...savedEnv,
44902
- ...inputEnv
44903
- },
44904
- inputEnv,
44905
- managedEnv: savedEnv
44906
- };
44907
- };
44908
- const readHotUpdaterEnv = async (cwd) => {
44909
- const { managedEnv } = await readHotUpdaterInitEnv(cwd);
44910
- return managedEnv;
44911
- };
44912
- const getHotUpdaterEnvValue = (env, key) => {
44913
- if (process.env[key] !== void 0) return process.env[key]?.trim() || void 0;
44914
- return env[key]?.trim() || void 0;
44915
- };
44916
- //#endregion
44917
46171
  //#region src/infrastructureGeneration.ts
44918
46172
  const assertInfrastructureGenerationPayload = ({ payload, provider, resource }) => {
44919
46173
  if (typeof payload !== "object" || payload === null || !("infrastructureGeneration" in payload) || payload.infrastructureGeneration !== 1) throw new LegacyInfrastructureError(provider, resource);
@@ -45090,6 +46344,51 @@ const atLeast = (version, minimum) => {
45090
46344
  }
45091
46345
  return true;
45092
46346
  };
46347
+ /** The arguments that run React Native CLI's `config` with Node.js. */
46348
+ const reactNativeCliConfig = (cwd) => [path$1.join(path$1.dirname(require$2.resolve("react-native/package.json", { paths: [cwd] })), "cli.js"), "config"];
46349
+ /**
46350
+ * The command, run with Node.js, that lists the React Native packages a
46351
+ * build links, chosen as Expo and @expo/fingerprint choose it: Expo's
46352
+ * autolinking from expo-modules-autolinking 1.12 (SDK 52), else React Native
46353
+ * CLI, as for an app without Expo. The native templates also fall back to
46354
+ * React Native CLI when `EXPO_USE_COMMUNITY_AUTOLINKING=1`, as Expo
46355
+ * documents.
46356
+ */
46357
+ const reactNativeConfigCommand = (cwd, platform, env = process.env) => {
46358
+ let autolinking = null;
46359
+ try {
46360
+ autolinking = resolveExpoAutolinking(cwd);
46361
+ } catch {}
46362
+ if (autolinking?.linksReactNative && env["EXPO_USE_COMMUNITY_AUTOLINKING"] !== "1") return [
46363
+ autolinking.bin,
46364
+ "react-native-config",
46365
+ "--platform",
46366
+ platform,
46367
+ "--json"
46368
+ ];
46369
+ return reactNativeCliConfig(cwd);
46370
+ };
46371
+ /**
46372
+ * The command that lists the React Native packages the native project of a
46373
+ * build adapter links, as the adapter's own fingerprint runs it: `rock config`
46374
+ * for Rock, React Native CLI's `config` for the bare adapter, and
46375
+ * {@link reactNativeConfigCommand} for Expo and any other adapter.
46376
+ */
46377
+ const autolinkingConfigCommand = (cwd, platform, { buildAdapter, env = process.env } = {}) => {
46378
+ if (buildAdapter === "rock") return {
46379
+ command: "npx",
46380
+ args: [
46381
+ "rock",
46382
+ "config",
46383
+ "-p",
46384
+ platform
46385
+ ]
46386
+ };
46387
+ return {
46388
+ command: process.execPath,
46389
+ args: buildAdapter === "bare" ? reactNativeCliConfig(cwd) : reactNativeConfigCommand(cwd, platform, env)
46390
+ };
46391
+ };
45093
46392
  /**
45094
46393
  * The expo-modules-autolinking an Expo app builds with, resolved from its
45095
46394
  * `expo` package as the native templates resolve it.
@@ -45132,8 +46431,11 @@ const readExpoModules = async (cwd, platform) => {
45132
46431
  reject: false
45133
46432
  });
45134
46433
  if (result.exitCode !== 0) throw new Error("`expo-modules-autolinking resolve` failed.");
45135
- const { modules = [] } = parseJsonOutput(result.stdout, "expo-modules-autolinking resolve");
45136
- return readExpoModuleDependencies(modules);
46434
+ const output = parseJsonOutput(result.stdout, "expo-modules-autolinking resolve");
46435
+ return {
46436
+ dependencies: await readExpoModuleDependencies(output.modules ?? []),
46437
+ output
46438
+ };
45137
46439
  };
45138
46440
  const readExpoModuleDependencies = (modules) => Promise.all(modules.flatMap((module) => {
45139
46441
  const name = module.packageName;
@@ -45218,13 +46520,21 @@ const readReactNativeInterface = async ({ cwd, command, args, platform, settings
45218
46520
  ...rest,
45219
46521
  sourceDirs: [sourceDir]
45220
46522
  } : rest) : []);
45221
- const expoModules = dependencies.some(({ name }) => name === "expo") ? await readExpoModules(cwd, platform) : [];
46523
+ const expo = dependencies.some(({ name }) => name === "expo") ? await readExpoModules(cwd, platform) : {
46524
+ dependencies: [],
46525
+ output: null
46526
+ };
46527
+ const expoModules = expo.dependencies;
45222
46528
  const names = new Set(dependencies.map(({ name }) => name));
45223
46529
  const settings = declaredSettings ?? (platform === "android" ? await readGradleFlags(config.project?.android?.sourceDir ?? path$1.join(cwd, "android")) : {});
45224
46530
  return {
45225
46531
  runtime: `react-native@${reactNative?.version ?? "unknown"}`,
45226
46532
  dependencies: [...dependencies, ...expoModules.filter(({ name }) => !names.has(name))],
45227
- settings
46533
+ settings,
46534
+ autolinking: {
46535
+ reactNative: config,
46536
+ expoModules: expo.output
46537
+ }
45228
46538
  };
45229
46539
  };
45230
46540
  const sha256 = (data) => createHash("sha256").update(data).digest("hex");
@@ -45447,7 +46757,7 @@ const createInterfaceFingerprint = async (cwd, options, nativeInterface) => {
45447
46757
  const reactNativeCliFingerprint = async (cwd, options) => createInterfaceFingerprint(cwd, options, await readReactNativeInterface({
45448
46758
  cwd,
45449
46759
  command: process.execPath,
45450
- args: [path$1.join(path$1.dirname(require$2.resolve("react-native/package.json", { paths: [cwd] })), "cli.js"), "config"],
46760
+ args: reactNativeCliConfig(cwd),
45451
46761
  platform: options.platform
45452
46762
  }));
45453
46763
  //#endregion
@@ -45648,12 +46958,14 @@ const mergeConfigSources = (...sources) => {
45648
46958
  const mergedConfig = sources.reduceRight((mergedConfig, source) => mergeWith(mergedConfig, source ?? {}, replaceArrays), {});
45649
46959
  const database = sources.find((source) => source?.database)?.database;
45650
46960
  const plugins = sources.find((source) => source?.plugins)?.plugins;
46961
+ const secrets = sources.find((source) => source?.secrets)?.secrets;
45651
46962
  const signing = sources.find((source) => source?.signing)?.signing;
45652
46963
  const storage = sources.find((source) => source?.storage)?.storage;
45653
46964
  return {
45654
46965
  ...mergedConfig,
45655
46966
  ...database ? { database } : {},
45656
46967
  ...plugins ? { plugins } : {},
46968
+ ...secrets ? { secrets } : {},
45657
46969
  ...signing ? { signing } : {},
45658
46970
  ...storage ? { storage } : {}
45659
46971
  };
@@ -45679,11 +46991,25 @@ const getConfigLoaderOptions = () => {
45679
46991
  };
45680
46992
  /** Each load runs the config file again, which creates its adapters again. */
45681
46993
  const loadConfigFile = async () => {
45682
- const { config } = await loadConfig$1(getConfigLoaderOptions());
45683
- return config;
46994
+ const scope = {};
46995
+ const { config } = await withSecretsScope(scope, () => loadConfig$1(getConfigLoaderOptions()));
46996
+ return {
46997
+ source: config,
46998
+ scope
46999
+ };
47000
+ };
47001
+ /**
47002
+ * The config `options` select, resolved: a config function runs in a scope
47003
+ * of its own, so the `secretEnv` it creates reads this config's `secrets`,
47004
+ * and the file's top-level `secretEnv` reads the first config's.
47005
+ */
47006
+ const configFor = async ({ source, scope: fileScope }, options) => {
47007
+ if (typeof source !== "function") return resolveConfig(source, [fileScope]);
47008
+ const scope = {};
47009
+ const config = await withSecretsScope(scope, () => source(options));
47010
+ return resolveConfig(config, [scope, fileScope]);
45684
47011
  };
45685
- const configFor = async (source, options) => typeof source === "function" ? await source(options) : source;
45686
- const loadConfig = async (options) => resolveConfig(await configFor(await loadConfigFile(), options));
47012
+ const loadConfig = async (options) => configFor(await loadConfigFile(), options);
45687
47013
  /**
45688
47014
  * hot-updater.config for each of `platforms`, with the file loaded once. A
45689
47015
  * config object gives every platform the same database, storage, and
@@ -45691,23 +47017,32 @@ const loadConfig = async (options) => resolveConfig(await configFor(await loadCo
45691
47017
  * creates are that platform's own.
45692
47018
  */
45693
47019
  const loadPlatformConfigs = async (platforms, { channel }) => {
45694
- const source = await loadConfigFile();
47020
+ const file = await loadConfigFile();
45695
47021
  const configs = [];
45696
47022
  for (const platform of platforms) configs.push({
45697
47023
  platform,
45698
- config: resolveConfig(await configFor(source, {
47024
+ config: await configFor(file, {
45699
47025
  channel,
45700
47026
  platform
45701
- }))
47027
+ })
45702
47028
  });
45703
47029
  return configs;
45704
47030
  };
45705
- const resolveConfig = (config) => {
47031
+ /**
47032
+ * The config with its defaults. Its `secrets` adapter fills `scopes` not yet
47033
+ * filled, which the `secretEnv` its evaluation created read, and becomes the
47034
+ * default of credential resolvers.
47035
+ */
47036
+ const resolveConfig = (config, scopes) => {
45706
47037
  const mergedConfig = mergeConfigSources(config, getDefaultConfig());
45707
- const signing = normalizeSigningConfig(mergedConfig.signing);
47038
+ const secrets = withLocalKeyStore(mergedConfig.secrets);
47039
+ const signing = normalizeSigningConfig(mergedConfig.signing, { secrets });
47040
+ for (const scope of scopes) scope.secrets ??= secrets;
47041
+ setConfiguredSecrets(secrets);
45708
47042
  return {
45709
47043
  ...mergedConfig,
45710
- signing
47044
+ signing,
47045
+ secrets
45711
47046
  };
45712
47047
  };
45713
47048
  //#endregion
@@ -45802,13 +47137,14 @@ const generateSchema = ({ database, plugins }, version, name) => {
45802
47137
  };
45803
47138
  //#endregion
45804
47139
  //#region src/log.ts
47140
+ const mask = (message) => typeof message === "string" ? maskSecretValues(message) : message;
45805
47141
  const log = {
45806
- normal: (message) => console.log(message),
45807
- success: (message) => console.log(import_picocolors.default.green(message)),
45808
- info: (message) => console.log(import_picocolors.default.blue(message)),
45809
- error: (message) => console.log(import_picocolors.default.red(message)),
45810
- warn: (message) => console.log(import_picocolors.default.yellow(message)),
45811
- debug: (message) => console.log(import_picocolors.default.gray(message))
47142
+ normal: (message) => console.log(mask(message)),
47143
+ success: (message) => console.log(import_picocolors.default.green(mask(message))),
47144
+ info: (message) => console.log(import_picocolors.default.blue(mask(message))),
47145
+ error: (message) => console.log(import_picocolors.default.red(mask(message))),
47146
+ warn: (message) => console.log(import_picocolors.default.yellow(mask(message))),
47147
+ debug: (message) => console.log(import_picocolors.default.gray(mask(message)))
45812
47148
  };
45813
47149
  //#endregion
45814
47150
  //#region src/makeEnv.ts
@@ -45860,6 +47196,7 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
45860
47196
  if (isFileSystemError(error) && error.code === "ENOENT") return "";
45861
47197
  throw error;
45862
47198
  });
47199
+ if (options?.expectedContent !== void 0 && existingContent !== options.expectedContent) throw new Error(`${filePath} changed since it was read, so it was not rewritten.`);
45863
47200
  const lines = existingContent ? existingContent.split("\n") : [];
45864
47201
  const processedKeys = /* @__PURE__ */ new Set();
45865
47202
  const updatedLines = [];
@@ -45915,6 +47252,23 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
45915
47252
  }
45916
47253
  };
45917
47254
  //#endregion
47255
+ //#region src/minBundleId.ts
47256
+ /**
47257
+ * The MIN_BUNDLE_ID a native build compiles in, which is also the ID of its
47258
+ * built-in bundle: a UUIDv7 of the build's millisecond whose random bits are
47259
+ * all zero, `<ts>-<ts>-7000-8000-000000000000`, as the SDK's podspec script
47260
+ * phase and Gradle task generate it. The bundles `hot-updater deploy` issued
47261
+ * before the build sort before it, and the ones it issues afterwards sort
47262
+ * after it. Bundle IDs share one space across platforms, so two builds that
47263
+ * start in the same millisecond get the same one: `native-build ios|android`
47264
+ * steps past an ID the registry holds before it builds, and the registry
47265
+ * refuses a second registration of one.
47266
+ */
47267
+ const createMinBundleId = (nowMs = Date.now()) => {
47268
+ const timestamp = nowMs.toString(16).padStart(12, "0");
47269
+ return `${timestamp.slice(0, 8)}-${timestamp.slice(8)}-7000-8000-000000000000`;
47270
+ };
47271
+ //#endregion
45918
47272
  //#region src/moduleSpecifiers.ts
45919
47273
  /**
45920
47274
  * The modules `source` imports or re-exports, statically or with an
@@ -46031,4 +47385,4 @@ function transformTemplate(templateString, values) {
46031
47385
  return result;
46032
47386
  }
46033
47387
  //#endregion
46034
- export { BuildLogger, ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, INTERFACE_FINGERPRINT_ENGINE, InitEnvFileError, InitError, LegacyInfrastructureError, MissingInitInputsError, ServerDefinitionError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, banner, clientAuthOf, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLogWriter, createMigrator, createTarBrTargetFiles, createZip, decryptJson, defineInitProvider, encryptJson, ensureInstallPackages, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getCwd, getHotUpdaterEnvValue, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getReactNativeMetadatas, getStorageFileByteSize, isCodegenSpecFile, isServerDefinition, link, loadConfig, loadPlatformConfigs, log, makeEnv, moduleSpecifiersOf, p, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, readBundleSigningPublicKeyFile, readHotUpdaterEnv, readHotUpdaterInitEnv, readPackageUp, readReactNativeInterface, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolvePackageVersion, serverDefinitionOf, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, writeHotUpdaterConfig, writeHotUpdaterFiles };
47388
+ export { BuildFailedError, BuildLogger, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError, DEFAULT_SIGNING_KEY_SECRET, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, INTERFACE_FINGERPRINT_ENGINE, InitEnvFileError, InitError, LOCAL_SECRETS_NAME, LegacyInfrastructureError, MissingInitInputsError, ServerDefinitionError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, clientAuthOf, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialOverrideHint, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeSecret, describeStoredSecret, encryptJson, ensureInstallPackages, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readHotUpdaterInitEnv, readLoadedHotUpdaterEnv, readLocalKey, readPackageUp, readReactNativeInterface, readSecretVariables, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, secretEnv, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, withCredentialResolver, withLocalKeyStore, withSecretsScope, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };