@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.
- package/dist/index.d.mts +539 -15
- package/dist/index.mjs +1541 -187
- 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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
22031
|
+
log$2.warn(s, e);
|
|
22032
22032
|
},
|
|
22033
22033
|
error: (s, e) => {
|
|
22034
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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,
|
|
22704
|
+
const finish = (message, logLine) => {
|
|
22597
22705
|
if (!active) return;
|
|
22598
22706
|
active = false;
|
|
22599
22707
|
const finalMessage = message || latestMessage;
|
|
22600
|
-
if (finalMessage)
|
|
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$
|
|
22714
|
+
if (message) log$2.step(message, logOptions);
|
|
22607
22715
|
},
|
|
22608
|
-
stop: (message) => finish(message, log$
|
|
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$
|
|
22720
|
+
finish(message, log$2.warn);
|
|
22613
22721
|
if (wasActive) onCancel?.();
|
|
22614
22722
|
},
|
|
22615
|
-
error: (message) => finish(message, log$
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23020
|
+
const summaryInput = new PassThrough();
|
|
23021
|
+
(display ?? input).pipe(progressInput);
|
|
22830
23022
|
input.pipe(logInput);
|
|
22831
|
-
|
|
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)
|
|
22859
|
-
|
|
22860
|
-
|
|
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
|
|
25769
|
+
const parsePrivateKey = (pem) => {
|
|
24931
25770
|
try {
|
|
24932
|
-
const privateKey = crypto$1.createPrivateKey(
|
|
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
|
|
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
|
|
24943
|
-
const cached = privateKeys.get(
|
|
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 =
|
|
24946
|
-
privateKeys.delete(
|
|
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(
|
|
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
|
|
24973
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
25076
|
-
if (!
|
|
25077
|
-
|
|
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
|
|
25083
|
-
if (!
|
|
25084
|
-
|
|
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 `
|
|
44401
|
-
const
|
|
44402
|
-
const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && getObjectPropertyName(candidate) ===
|
|
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
|
|
45136
|
-
return
|
|
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
|
|
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:
|
|
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
|
|
45683
|
-
|
|
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
|
|
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
|
|
47020
|
+
const file = await loadConfigFile();
|
|
45695
47021
|
const configs = [];
|
|
45696
47022
|
for (const platform of platforms) configs.push({
|
|
45697
47023
|
platform,
|
|
45698
|
-
config:
|
|
47024
|
+
config: await configFor(file, {
|
|
45699
47025
|
channel,
|
|
45700
47026
|
platform
|
|
45701
|
-
})
|
|
47027
|
+
})
|
|
45702
47028
|
});
|
|
45703
47029
|
return configs;
|
|
45704
47030
|
};
|
|
45705
|
-
|
|
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
|
|
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,
|
|
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 };
|