@hot-updater/cli-tools 1.2.0-rc.1 → 1.2.0-rc.11
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 +429 -114
- package/dist/index.mjs +1436 -981
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -37,78 +37,6 @@ import { Buffer as Buffer$2 } from "node:buffer";
|
|
|
37
37
|
import { Visitor, parseSync } from "oxc-parser";
|
|
38
38
|
import { loadConfig as loadConfig$1 } from "unconfig";
|
|
39
39
|
import { transformSync } from "oxc-transform";
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region src/colors.ts
|
|
42
|
-
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
43
|
-
let p = process || {};
|
|
44
|
-
let argv = p.argv || [];
|
|
45
|
-
let env = p.env || {};
|
|
46
|
-
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
47
|
-
let formatter = (open, close, replace = open) => (input) => {
|
|
48
|
-
let string = "" + input, index = string.indexOf(close, open.length);
|
|
49
|
-
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
50
|
-
};
|
|
51
|
-
let replaceClose = (string, close, replace, index) => {
|
|
52
|
-
let result = "", cursor = 0;
|
|
53
|
-
do {
|
|
54
|
-
result += string.substring(cursor, index) + replace;
|
|
55
|
-
cursor = index + close.length;
|
|
56
|
-
index = string.indexOf(close, cursor);
|
|
57
|
-
} while (~index);
|
|
58
|
-
return result + string.substring(cursor);
|
|
59
|
-
};
|
|
60
|
-
let createColors = (enabled = isColorSupported) => {
|
|
61
|
-
let f = enabled ? formatter : () => String;
|
|
62
|
-
return {
|
|
63
|
-
isColorSupported: enabled,
|
|
64
|
-
reset: f("\x1B[0m", "\x1B[0m"),
|
|
65
|
-
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
66
|
-
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
67
|
-
italic: f("\x1B[3m", "\x1B[23m"),
|
|
68
|
-
underline: f("\x1B[4m", "\x1B[24m"),
|
|
69
|
-
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
70
|
-
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
71
|
-
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
72
|
-
black: f("\x1B[30m", "\x1B[39m"),
|
|
73
|
-
red: f("\x1B[31m", "\x1B[39m"),
|
|
74
|
-
green: f("\x1B[32m", "\x1B[39m"),
|
|
75
|
-
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
76
|
-
blue: f("\x1B[34m", "\x1B[39m"),
|
|
77
|
-
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
78
|
-
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
79
|
-
white: f("\x1B[37m", "\x1B[39m"),
|
|
80
|
-
gray: f("\x1B[90m", "\x1B[39m"),
|
|
81
|
-
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
82
|
-
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
83
|
-
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
84
|
-
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
85
|
-
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
86
|
-
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
87
|
-
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
88
|
-
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
89
|
-
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
90
|
-
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
91
|
-
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
92
|
-
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
93
|
-
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
94
|
-
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
95
|
-
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
96
|
-
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
97
|
-
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
98
|
-
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
99
|
-
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
100
|
-
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
101
|
-
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
102
|
-
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
103
|
-
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
104
|
-
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
105
|
-
};
|
|
106
|
-
};
|
|
107
|
-
module.exports = createColors();
|
|
108
|
-
module.exports.createColors = createColors;
|
|
109
|
-
})))(), 1);
|
|
110
|
-
const typedColors = import_picocolors.default;
|
|
111
|
-
//#endregion
|
|
112
40
|
//#region ../../node_modules/.pnpm/workspace-tools@0.41.7/node_modules/workspace-tools/lib/graph/getPackageDependencies.js
|
|
113
41
|
var require_getPackageDependencies = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
114
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -22628,7 +22556,7 @@ const MASKING_REGISTRY = Symbol.for("@hot-updater/cli-tools/secret-masking");
|
|
|
22628
22556
|
/**
|
|
22629
22557
|
* The values to mask, kept on `globalThis`: the config file loads its own
|
|
22630
22558
|
* copy of Hot Updater's modules, and a value it resolves, such as through
|
|
22631
|
-
* `
|
|
22559
|
+
* `secret()`, must be masked by the CLI's log sinks too.
|
|
22632
22560
|
*/
|
|
22633
22561
|
const registry = globalThis[MASKING_REGISTRY] ??= {
|
|
22634
22562
|
secretValues: /* @__PURE__ */ new Set(),
|
|
@@ -22664,17 +22592,57 @@ const maskSecretValues = (text) => {
|
|
|
22664
22592
|
return shortSecretPattern ? masked.replace(shortSecretPattern, SECRET_MASK) : masked;
|
|
22665
22593
|
};
|
|
22666
22594
|
//#endregion
|
|
22595
|
+
//#region src/stepCapture.ts
|
|
22596
|
+
let current;
|
|
22597
|
+
const stepCapture = () => current;
|
|
22598
|
+
const setStepCapture = (capture) => {
|
|
22599
|
+
current = capture;
|
|
22600
|
+
};
|
|
22601
|
+
//#endregion
|
|
22667
22602
|
//#region src/prompts.ts
|
|
22668
22603
|
const maskMessage = (message) => message === void 0 ? message : maskSecretValues(message);
|
|
22669
|
-
|
|
22670
|
-
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
|
|
22675
|
-
|
|
22676
|
-
|
|
22604
|
+
/**
|
|
22605
|
+
* Prints a log line, or, while a step runs, holds it until the step's line
|
|
22606
|
+
* prints. An info line says what the step is doing, so it shows as the
|
|
22607
|
+
* step's detail instead; an error ends the step first and prints at once,
|
|
22608
|
+
* since the process may exit right after it.
|
|
22609
|
+
*/
|
|
22610
|
+
const logThroughStep = (write, { inStep = "hold" } = {}) => (...args) => {
|
|
22611
|
+
const capture = stepCapture();
|
|
22612
|
+
if (!capture) return write(...args);
|
|
22613
|
+
if (inStep === "interrupt") {
|
|
22614
|
+
capture.interrupt();
|
|
22615
|
+
return write(...args);
|
|
22616
|
+
}
|
|
22617
|
+
if (inStep === "detail") {
|
|
22618
|
+
const message = args[0];
|
|
22619
|
+
const text = Array.isArray(message) ? message.join("\n") : message ?? "";
|
|
22620
|
+
capture.update(maskSecretValues(text.split("\n")[0] ?? ""));
|
|
22621
|
+
return;
|
|
22622
|
+
}
|
|
22623
|
+
capture.hold(() => write(...args));
|
|
22677
22624
|
};
|
|
22625
|
+
const log$1 = {
|
|
22626
|
+
message: logThroughStep((message, options) => log$2.message(Array.isArray(message) ? message.map(maskSecretValues) : maskMessage(message), options)),
|
|
22627
|
+
info: logThroughStep((message, options) => log$2.info(maskSecretValues(message), options), { inStep: "detail" }),
|
|
22628
|
+
success: logThroughStep((message, options) => log$2.success(maskSecretValues(message), options)),
|
|
22629
|
+
step: logThroughStep((message, options) => log$2.step(maskSecretValues(message), options)),
|
|
22630
|
+
warn: logThroughStep((message, options) => log$2.warn(maskSecretValues(message), options)),
|
|
22631
|
+
warning: logThroughStep((message, options) => log$2.warning(maskSecretValues(message), options)),
|
|
22632
|
+
error: logThroughStep((message, options) => log$2.error(maskSecretValues(message), options), { inStep: "interrupt" })
|
|
22633
|
+
};
|
|
22634
|
+
/** A spinner inside a running step, whose messages are the step's detail. */
|
|
22635
|
+
const spinnerInStep = (capture) => ({
|
|
22636
|
+
start: (message) => capture.update(message),
|
|
22637
|
+
message: (message) => capture.update(message),
|
|
22638
|
+
stop: () => capture.update(void 0),
|
|
22639
|
+
cancel: () => capture.update(void 0),
|
|
22640
|
+
error: (message) => capture.interrupt(message || void 0),
|
|
22641
|
+
clear: () => capture.update(void 0),
|
|
22642
|
+
get isCancelled() {
|
|
22643
|
+
return false;
|
|
22644
|
+
}
|
|
22645
|
+
});
|
|
22678
22646
|
const maskSpinner = (target) => ({
|
|
22679
22647
|
start: (message) => target.start(maskMessage(message)),
|
|
22680
22648
|
stop: (message) => target.stop(maskMessage(message)),
|
|
@@ -22733,11 +22701,23 @@ const createStaticSpinner = ({ onCancel, output = process.stdout, withGuide } =
|
|
|
22733
22701
|
};
|
|
22734
22702
|
};
|
|
22735
22703
|
const spinner = (options = {}) => {
|
|
22704
|
+
const capture = stepCapture();
|
|
22705
|
+
if (capture) return spinnerInStep(capture);
|
|
22736
22706
|
const output = options.output ?? process.stdout;
|
|
22737
22707
|
const isInteractive = isTTY(output) && !isCI$1();
|
|
22738
22708
|
return maskSpinner(isInteractive ? spinner$1(options) : createStaticSpinner(options));
|
|
22739
22709
|
};
|
|
22740
22710
|
const progress = (options = {}) => {
|
|
22711
|
+
const capture = stepCapture();
|
|
22712
|
+
if (capture) return {
|
|
22713
|
+
...spinnerInStep(capture),
|
|
22714
|
+
advance: (_step, message) => {
|
|
22715
|
+
if (message) capture.update(message);
|
|
22716
|
+
},
|
|
22717
|
+
get isCancelled() {
|
|
22718
|
+
return false;
|
|
22719
|
+
}
|
|
22720
|
+
};
|
|
22741
22721
|
const output = options.output ?? process.stdout;
|
|
22742
22722
|
if (isTTY(output) && !isCI$1()) return maskProgress(progress$1(options));
|
|
22743
22723
|
const staticSpinner = maskSpinner(createStaticSpinner(options));
|
|
@@ -22870,6 +22850,9 @@ const createLogWriter = async ({ prefix, logFilePath }) => {
|
|
|
22870
22850
|
}
|
|
22871
22851
|
if (partialLine) writeChunk(partialLine);
|
|
22872
22852
|
};
|
|
22853
|
+
const writeLines = (lines) => {
|
|
22854
|
+
writeChunk(`${lines.join("\n")}\n`);
|
|
22855
|
+
};
|
|
22873
22856
|
const writeError = (error) => {
|
|
22874
22857
|
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
22875
22858
|
writeChunk(`${message}\n`);
|
|
@@ -22886,6 +22869,7 @@ const createLogWriter = async ({ prefix, logFilePath }) => {
|
|
|
22886
22869
|
return {
|
|
22887
22870
|
logFilePath: resolvedLogFilePath,
|
|
22888
22871
|
writeStream,
|
|
22872
|
+
writeLines,
|
|
22889
22873
|
writeError,
|
|
22890
22874
|
close
|
|
22891
22875
|
};
|
|
@@ -22895,67 +22879,295 @@ const createLogWriter = async ({ prefix, logFilePath }) => {
|
|
|
22895
22879
|
return {
|
|
22896
22880
|
logFilePath: null,
|
|
22897
22881
|
writeStream: async () => {},
|
|
22882
|
+
writeLines: () => {},
|
|
22898
22883
|
writeError: () => {},
|
|
22899
22884
|
close: async () => {}
|
|
22900
22885
|
};
|
|
22901
22886
|
}
|
|
22902
22887
|
};
|
|
22903
22888
|
//#endregion
|
|
22904
|
-
//#region src/
|
|
22905
|
-
|
|
22906
|
-
|
|
22907
|
-
|
|
22908
|
-
|
|
22909
|
-
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
const
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
|
|
22918
|
-
|
|
22919
|
-
|
|
22920
|
-
|
|
22921
|
-
|
|
22922
|
-
|
|
22923
|
-
});
|
|
22924
|
-
|
|
22925
|
-
|
|
22926
|
-
|
|
22927
|
-
|
|
22928
|
-
|
|
22929
|
-
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
22935
|
-
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
22939
|
-
|
|
22940
|
-
|
|
22941
|
-
|
|
22942
|
-
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
22948
|
-
|
|
22949
|
-
|
|
22950
|
-
|
|
22951
|
-
|
|
22889
|
+
//#region src/messageLines.ts
|
|
22890
|
+
/**
|
|
22891
|
+
* The widest line of a message the CLI prints: clack puts it after its
|
|
22892
|
+
* 3-column gutter (`│ `), and the whole line stays within 76 columns.
|
|
22893
|
+
*/
|
|
22894
|
+
const MESSAGE_WIDTH = 73;
|
|
22895
|
+
const graphemes = new Intl.Segmenter();
|
|
22896
|
+
/** Combining marks, format and control characters: they take no column. */
|
|
22897
|
+
const ZERO_WIDTH = /^[\p{M}\p{Cf}\p{Cc}]+$/u;
|
|
22898
|
+
/** East Asian wide and fullwidth characters, and emoji: two columns each. */
|
|
22899
|
+
const WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{20000}-\u{3FFFD}]|\p{Emoji_Presentation}/u;
|
|
22900
|
+
/** The columns one grapheme takes. */
|
|
22901
|
+
const segmentWidth = (segment) => ZERO_WIDTH.test(segment) ? 0 : WIDE.test(segment) ? 2 : 1;
|
|
22902
|
+
/**
|
|
22903
|
+
* The columns `text` takes in a terminal, without its colors: `│`, `■`,
|
|
22904
|
+
* and `…` take one each, though each is several bytes.
|
|
22905
|
+
*/
|
|
22906
|
+
const displayWidth = (text) => {
|
|
22907
|
+
let width = 0;
|
|
22908
|
+
for (const { segment } of graphemes.segment(stripVTControlCharacters(text))) width += segmentWidth(segment);
|
|
22909
|
+
return width;
|
|
22910
|
+
};
|
|
22911
|
+
/**
|
|
22912
|
+
* `text` cut to `width` display columns, with `…` for the rest, never
|
|
22913
|
+
* inside a character: a cut line loses its colors.
|
|
22914
|
+
*/
|
|
22915
|
+
const truncateLine = (text, width) => {
|
|
22916
|
+
const plain = stripVTControlCharacters(text);
|
|
22917
|
+
if (displayWidth(plain) <= width) return text;
|
|
22918
|
+
let cut = "";
|
|
22919
|
+
let used = 0;
|
|
22920
|
+
for (const { segment } of graphemes.segment(plain)) {
|
|
22921
|
+
used += segmentWidth(segment);
|
|
22922
|
+
if (used > width - 1) break;
|
|
22923
|
+
cut += segment;
|
|
22924
|
+
}
|
|
22925
|
+
return `${cut.trimEnd()}…`;
|
|
22926
|
+
};
|
|
22927
|
+
/**
|
|
22928
|
+
* A message of `lines`, each at most `MESSAGE_WIDTH` columns, broken at
|
|
22929
|
+
* spaces, so a name, a path, or a list it embeds never widens a line. A
|
|
22930
|
+
* word longer than a line, such as a URL or a path, gets a line of its
|
|
22931
|
+
* own. Lines that fit, such as an indented command, stay as they are.
|
|
22932
|
+
*/
|
|
22933
|
+
const fitMessage = (lines) => lines.flatMap((line) => {
|
|
22934
|
+
if (displayWidth(line) <= 73) return [line];
|
|
22935
|
+
const fitted = [];
|
|
22936
|
+
let current = "";
|
|
22937
|
+
for (const word of line.split(" ").filter(Boolean)) if (current && displayWidth(current) + 1 + displayWidth(word) > 73) {
|
|
22938
|
+
fitted.push(current);
|
|
22939
|
+
current = word;
|
|
22940
|
+
} else current = current ? `${current} ${word}` : word;
|
|
22941
|
+
return [...fitted, current];
|
|
22942
|
+
}).join("\n");
|
|
22943
|
+
//#endregion
|
|
22944
|
+
//#region src/colors.ts
|
|
22945
|
+
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
22946
|
+
let p = process || {};
|
|
22947
|
+
let argv = p.argv || [];
|
|
22948
|
+
let env = p.env || {};
|
|
22949
|
+
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
22950
|
+
let formatter = (open, close, replace = open) => (input) => {
|
|
22951
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
22952
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
22953
|
+
};
|
|
22954
|
+
let replaceClose = (string, close, replace, index) => {
|
|
22955
|
+
let result = "", cursor = 0;
|
|
22956
|
+
do {
|
|
22957
|
+
result += string.substring(cursor, index) + replace;
|
|
22958
|
+
cursor = index + close.length;
|
|
22959
|
+
index = string.indexOf(close, cursor);
|
|
22960
|
+
} while (~index);
|
|
22961
|
+
return result + string.substring(cursor);
|
|
22962
|
+
};
|
|
22963
|
+
let createColors = (enabled = isColorSupported) => {
|
|
22964
|
+
let f = enabled ? formatter : () => String;
|
|
22965
|
+
return {
|
|
22966
|
+
isColorSupported: enabled,
|
|
22967
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
22968
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
22969
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
22970
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
22971
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
22972
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
22973
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
22974
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
22975
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
22976
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
22977
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
22978
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
22979
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
22980
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
22981
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
22982
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
22983
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
22984
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
22985
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
22986
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
22987
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
22988
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
22989
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
22990
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
22991
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
22992
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
22993
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
22994
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
22995
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
22996
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
22997
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
22998
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
22999
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
23000
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
23001
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
23002
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
23003
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
23004
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
23005
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
23006
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
23007
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
23008
|
+
};
|
|
23009
|
+
};
|
|
23010
|
+
module.exports = createColors();
|
|
23011
|
+
module.exports.createColors = createColors;
|
|
23012
|
+
})))(), 1);
|
|
23013
|
+
const typedColors = import_picocolors.default;
|
|
23014
|
+
//#endregion
|
|
23015
|
+
//#region src/step.ts
|
|
23016
|
+
/** The widest a finished step's line is, after clack's 3-column gutter. */
|
|
23017
|
+
const STEP_WIDTH = 73;
|
|
23018
|
+
/** The room the spinner's symbol and its `[12m 34s]` timer take. */
|
|
23019
|
+
const LIVE_LINE_CHROME = 13;
|
|
23020
|
+
/** `4.2s`, `38s`, `6m41s`, `1h02m`. */
|
|
23021
|
+
const formatDuration = (ms) => {
|
|
23022
|
+
if (ms < 9950) return `${(Math.max(0, ms) / 1e3).toFixed(1)}s`;
|
|
23023
|
+
const seconds = Math.round(ms / 1e3);
|
|
23024
|
+
if (seconds < 60) return `${seconds}s`;
|
|
23025
|
+
const minutes = Math.floor(seconds / 60);
|
|
23026
|
+
if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
23027
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
23028
|
+
};
|
|
23029
|
+
/**
|
|
23030
|
+
* `message` with `duration` at the right edge of a step's line, the message
|
|
23031
|
+
* cut with `…` when the line would be wider.
|
|
23032
|
+
*/
|
|
23033
|
+
const withDuration = (message, ms, width = STEP_WIDTH) => {
|
|
23034
|
+
const duration = formatDuration(ms);
|
|
23035
|
+
const text = truncateLine(message, width - duration.length - 2);
|
|
23036
|
+
const gap = Math.max(2, width - displayWidth(text) - duration.length);
|
|
23037
|
+
return `${text}${" ".repeat(gap)}${typedColors.dim(duration)}`;
|
|
23038
|
+
};
|
|
23039
|
+
const isLive = (output) => isTTY(output) && !isCI$1();
|
|
23040
|
+
/**
|
|
23041
|
+
* A step that shows nothing until `start()`. A step created while another
|
|
23042
|
+
* runs is part of it: its title and detail become the running step's detail,
|
|
23043
|
+
* and its failure fails the running step.
|
|
23044
|
+
*/
|
|
23045
|
+
const createStep = (title, { output = process.stdout } = {}) => {
|
|
23046
|
+
let startedAt;
|
|
23047
|
+
let ended = false;
|
|
23048
|
+
let detail;
|
|
23049
|
+
let spinner;
|
|
23050
|
+
let ticker;
|
|
23051
|
+
let held = [];
|
|
23052
|
+
let outer;
|
|
23053
|
+
const liveMessage = () => {
|
|
23054
|
+
const text = maskSecretValues(detail ? `${title} · ${detail}` : title);
|
|
23055
|
+
const columns = output.columns || 80;
|
|
23056
|
+
return truncateLine(text, Math.max(20, columns - LIVE_LINE_CHROME));
|
|
23057
|
+
};
|
|
23058
|
+
const finish = (write) => {
|
|
23059
|
+
if (startedAt === void 0 || ended) return;
|
|
23060
|
+
ended = true;
|
|
23061
|
+
if (ticker) clearInterval(ticker);
|
|
23062
|
+
if (outer) {
|
|
23063
|
+
outer.update(void 0);
|
|
23064
|
+
return;
|
|
22952
23065
|
}
|
|
23066
|
+
setStepCapture(void 0);
|
|
23067
|
+
if (spinner) spinner.clear();
|
|
23068
|
+
write();
|
|
23069
|
+
const logs = held;
|
|
23070
|
+
held = [];
|
|
23071
|
+
for (const log of logs) log();
|
|
23072
|
+
};
|
|
23073
|
+
const logOptions = () => ({
|
|
23074
|
+
output,
|
|
23075
|
+
spacing: spinner ? 0 : 1
|
|
23076
|
+
});
|
|
23077
|
+
const step = {
|
|
23078
|
+
start() {
|
|
23079
|
+
if (startedAt !== void 0) return step;
|
|
23080
|
+
startedAt = performance.now();
|
|
23081
|
+
outer = stepCapture();
|
|
23082
|
+
if (outer) {
|
|
23083
|
+
outer.update(title);
|
|
23084
|
+
return step;
|
|
23085
|
+
}
|
|
23086
|
+
setStepCapture({
|
|
23087
|
+
update: (value) => step.update(value),
|
|
23088
|
+
hold: (log) => {
|
|
23089
|
+
held.push(log);
|
|
23090
|
+
},
|
|
23091
|
+
interrupt: (message) => step.fail(message)
|
|
23092
|
+
});
|
|
23093
|
+
if (isLive(output)) {
|
|
23094
|
+
spinner = spinner$1({
|
|
23095
|
+
indicator: "timer",
|
|
23096
|
+
output
|
|
23097
|
+
});
|
|
23098
|
+
spinner.start(liveMessage());
|
|
23099
|
+
ticker = setInterval(() => spinner?.message(liveMessage()), 1e3);
|
|
23100
|
+
ticker.unref?.();
|
|
23101
|
+
}
|
|
23102
|
+
return step;
|
|
23103
|
+
},
|
|
23104
|
+
update(value) {
|
|
23105
|
+
if (startedAt === void 0 || ended) return;
|
|
23106
|
+
if (outer) {
|
|
23107
|
+
outer.update(value ? `${title} · ${value}` : title);
|
|
23108
|
+
return;
|
|
23109
|
+
}
|
|
23110
|
+
detail = value;
|
|
23111
|
+
spinner?.message(liveMessage());
|
|
23112
|
+
},
|
|
23113
|
+
succeed(message = title, { level = "step" } = {}) {
|
|
23114
|
+
const elapsed = step.elapsedMs;
|
|
23115
|
+
finish(() => {
|
|
23116
|
+
const line = maskSecretValues(withDuration(message, elapsed));
|
|
23117
|
+
if (level === "info") log$2.info(line, logOptions());
|
|
23118
|
+
else if (level === "warn") log$2.warn(line, logOptions());
|
|
23119
|
+
else log$2.step(line, logOptions());
|
|
23120
|
+
});
|
|
23121
|
+
},
|
|
23122
|
+
fail(message = `${title} failed`) {
|
|
23123
|
+
const elapsed = step.elapsedMs;
|
|
23124
|
+
if (outer && startedAt !== void 0 && !ended) {
|
|
23125
|
+
ended = true;
|
|
23126
|
+
outer.interrupt(message);
|
|
23127
|
+
return;
|
|
23128
|
+
}
|
|
23129
|
+
finish(() => {
|
|
23130
|
+
log$2.error(maskSecretValues(withDuration(message, elapsed)), logOptions());
|
|
23131
|
+
});
|
|
23132
|
+
},
|
|
23133
|
+
get elapsedMs() {
|
|
23134
|
+
return startedAt === void 0 ? 0 : performance.now() - startedAt;
|
|
23135
|
+
},
|
|
23136
|
+
get active() {
|
|
23137
|
+
return startedAt !== void 0 && !ended;
|
|
23138
|
+
}
|
|
23139
|
+
};
|
|
23140
|
+
return step;
|
|
23141
|
+
};
|
|
23142
|
+
/**
|
|
23143
|
+
* Ends the running step as failed, if one runs, before a command prints
|
|
23144
|
+
* its error.
|
|
23145
|
+
*/
|
|
23146
|
+
const failActiveStep = () => {
|
|
23147
|
+
stepCapture()?.interrupt();
|
|
23148
|
+
};
|
|
23149
|
+
/** A step, started. */
|
|
23150
|
+
const startStep = (title, options) => createStep(title, options).start();
|
|
23151
|
+
/**
|
|
23152
|
+
* Runs `run` as a step titled `title`, which ends with `done(result)`, or as
|
|
23153
|
+
* failed when `run` throws.
|
|
23154
|
+
*/
|
|
23155
|
+
const withStep = async (title, run, done = () => title, options) => {
|
|
23156
|
+
const step = startStep(title, options);
|
|
23157
|
+
try {
|
|
23158
|
+
const result = await run(step);
|
|
23159
|
+
step.succeed(done(result));
|
|
23160
|
+
return result;
|
|
23161
|
+
} catch (error) {
|
|
23162
|
+
step.fail();
|
|
23163
|
+
throw error;
|
|
22953
23164
|
}
|
|
22954
|
-
return nextCompletedStages;
|
|
22955
23165
|
};
|
|
23166
|
+
//#endregion
|
|
23167
|
+
//#region src/BuildLogger.ts
|
|
22956
23168
|
/** The most lines a failure summary prints, and the tail it falls back to. */
|
|
22957
|
-
const MAX_SUMMARY_LINES =
|
|
22958
|
-
const TAIL_LINES =
|
|
23169
|
+
const MAX_SUMMARY_LINES = 20;
|
|
23170
|
+
const TAIL_LINES = 15;
|
|
22959
23171
|
const matchesSummaryPattern = (line, pattern) => {
|
|
22960
23172
|
if (pattern instanceof RegExp) {
|
|
22961
23173
|
pattern.lastIndex = 0;
|
|
@@ -22973,52 +23185,57 @@ var BuildFailedError = class extends Error {
|
|
|
22973
23185
|
logPath;
|
|
22974
23186
|
/** The lines of the tool's output the summary printed. */
|
|
22975
23187
|
summary;
|
|
22976
|
-
|
|
23188
|
+
/**
|
|
23189
|
+
* What to do about it, such as accepting an agreement, which the summary
|
|
23190
|
+
* doesn't say: the one line a command prints after it, or null.
|
|
23191
|
+
*/
|
|
23192
|
+
hint;
|
|
23193
|
+
constructor(message, { logPath, summary, hint = null, cause }) {
|
|
22977
23194
|
super(message, { cause });
|
|
22978
23195
|
this.logPath = logPath;
|
|
22979
23196
|
this.summary = summary;
|
|
23197
|
+
this.hint = hint;
|
|
22980
23198
|
}
|
|
22981
23199
|
};
|
|
22982
23200
|
var BuildLogger = class {
|
|
22983
23201
|
config;
|
|
22984
|
-
state = createInitialState();
|
|
22985
23202
|
logWriter;
|
|
23203
|
+
step;
|
|
22986
23204
|
summaryLines = [];
|
|
22987
23205
|
tailLines = [];
|
|
23206
|
+
lineCount = 0;
|
|
23207
|
+
stopped = false;
|
|
22988
23208
|
openBlock;
|
|
22989
|
-
promptProgress;
|
|
22990
|
-
stageCount;
|
|
22991
23209
|
constructor(config) {
|
|
22992
23210
|
this.config = config;
|
|
22993
|
-
this.stageCount = config.progressStages.length;
|
|
22994
|
-
this.promptProgress = p.progress({
|
|
22995
|
-
indicator: "timer",
|
|
22996
|
-
max: config.progressStages.length,
|
|
22997
|
-
delay: 500
|
|
22998
|
-
});
|
|
22999
23211
|
}
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23212
|
+
/**
|
|
23213
|
+
* Opens the log, writes `header` to it, such as the tool's command line,
|
|
23214
|
+
* which the terminal doesn't show, and starts the step titled `title`.
|
|
23215
|
+
*/
|
|
23216
|
+
async start({ title, header = [] }) {
|
|
23005
23217
|
this.summaryLines = [];
|
|
23006
23218
|
this.tailLines = [];
|
|
23219
|
+
this.lineCount = 0;
|
|
23220
|
+
this.stopped = false;
|
|
23007
23221
|
this.openBlock = void 0;
|
|
23008
|
-
this.logWriter = await createLogWriter({
|
|
23009
|
-
|
|
23222
|
+
this.logWriter = await createLogWriter({
|
|
23223
|
+
prefix: this.config.logPrefix,
|
|
23224
|
+
logFilePath: this.config.logFilePath
|
|
23225
|
+
});
|
|
23226
|
+
if (header.length) this.logWriter.writeLines(header);
|
|
23227
|
+
this.step = startStep(title);
|
|
23010
23228
|
}
|
|
23011
23229
|
/**
|
|
23012
|
-
* Writes the tool's raw output to the log file
|
|
23013
|
-
*
|
|
23014
|
-
* output of it such as xcbeautify's, or from the raw output without one.
|
|
23230
|
+
* Writes the tool's raw output to the log file, reads its failure summary
|
|
23231
|
+
* from it, and shows its phases on the step's line.
|
|
23015
23232
|
*/
|
|
23016
|
-
async processStream(input
|
|
23233
|
+
async processStream(input) {
|
|
23017
23234
|
if (!this.logWriter) throw new Error("BuildLogger has not been started. Call start() first.");
|
|
23018
23235
|
const progressInput = new PassThrough();
|
|
23019
23236
|
const logInput = new PassThrough();
|
|
23020
23237
|
const summaryInput = new PassThrough();
|
|
23021
|
-
|
|
23238
|
+
input.pipe(progressInput);
|
|
23022
23239
|
input.pipe(logInput);
|
|
23023
23240
|
input.pipe(summaryInput);
|
|
23024
23241
|
await Promise.all([
|
|
@@ -23031,15 +23248,25 @@ var BuildLogger = class {
|
|
|
23031
23248
|
get logPath() {
|
|
23032
23249
|
return this.logWriter?.logFilePath ? path.relative(getCwd(), this.logWriter.logFilePath) : null;
|
|
23033
23250
|
}
|
|
23251
|
+
/** The build log's absolute path, or null without one. */
|
|
23252
|
+
get logFilePath() {
|
|
23253
|
+
return this.logWriter?.logFilePath ?? null;
|
|
23254
|
+
}
|
|
23034
23255
|
/**
|
|
23035
23256
|
* The lines of the raw output a failure prints: the ones the config's
|
|
23036
|
-
* `failureSummary` keeps, deduplicated,
|
|
23257
|
+
* `failureSummary` keeps, deduplicated, with its `tail` unless the output
|
|
23258
|
+
* reached `stopAfter`, or the last lines without any.
|
|
23037
23259
|
*/
|
|
23038
23260
|
get failureSummary() {
|
|
23039
|
-
const
|
|
23261
|
+
const { tail = 0, maxWidth } = this.config.failureSummary;
|
|
23262
|
+
const lastLines = this.tailLines.filter(({ line }) => line.trim());
|
|
23263
|
+
const kept = this.summaryLines;
|
|
23264
|
+
const keptText = new Set(kept.map(({ line }) => line));
|
|
23265
|
+
const lines = kept.length ? [...kept, ...(tail && !this.stopped ? lastLines.slice(-tail) : []).filter(({ line }) => !keptText.has(line))].sort((a, b) => a.index - b.index).map(({ line }) => line) : lastLines.slice(-15).map(({ line }) => line);
|
|
23040
23266
|
const first = lines.findIndex((line) => line.trim());
|
|
23041
23267
|
const last = lines.findLastIndex((line) => line.trim());
|
|
23042
|
-
|
|
23268
|
+
const summary = first === -1 ? [] : lines.slice(first, last + 1);
|
|
23269
|
+
return maxWidth ? summary.map((line) => truncateLine(line, maxWidth)) : summary;
|
|
23043
23270
|
}
|
|
23044
23271
|
async consumeSummaryStream(input) {
|
|
23045
23272
|
const lineReader = createInterface({
|
|
@@ -23047,31 +23274,48 @@ var BuildLogger = class {
|
|
|
23047
23274
|
crlfDelay: Infinity,
|
|
23048
23275
|
terminal: false
|
|
23049
23276
|
});
|
|
23050
|
-
for await (const rawLine of lineReader)
|
|
23277
|
+
for await (const rawLine of lineReader) {
|
|
23278
|
+
const parts = stripAnsi(rawLine).split("\r");
|
|
23279
|
+
const lines = parts.length > 1 ? parts.filter((part) => part.trim()) : parts;
|
|
23280
|
+
for (const line of lines) this.processSummaryLine(line.trimEnd());
|
|
23281
|
+
}
|
|
23051
23282
|
}
|
|
23052
23283
|
processSummaryLine(line) {
|
|
23053
|
-
this.
|
|
23054
|
-
if (this.tailLines.length > TAIL_LINES) this.tailLines.shift();
|
|
23055
|
-
const { lines, blocks } = this.config.failureSummary;
|
|
23284
|
+
const { lines, blocks, ignore = [], tail = 0, stopAfter } = this.config.failureSummary;
|
|
23056
23285
|
const trimmedLine = line.trim();
|
|
23286
|
+
if (this.stopped || ignore.some((pattern) => matchesSummaryPattern(trimmedLine, pattern))) return;
|
|
23287
|
+
if (stopAfter && matchesSummaryPattern(trimmedLine, stopAfter)) {
|
|
23288
|
+
this.stopped = true;
|
|
23289
|
+
this.openBlock = void 0;
|
|
23290
|
+
}
|
|
23291
|
+
const index = this.lineCount++;
|
|
23292
|
+
this.tailLines.push({
|
|
23293
|
+
index,
|
|
23294
|
+
line
|
|
23295
|
+
});
|
|
23296
|
+
if (this.tailLines.length > Math.max(TAIL_LINES, tail)) this.tailLines.shift();
|
|
23057
23297
|
if (this.openBlock) {
|
|
23058
23298
|
if (matchesSummaryPattern(trimmedLine, this.openBlock.end)) {
|
|
23059
|
-
if (this.openBlock.includeEnd) this.keepSummaryLine(line);
|
|
23299
|
+
if (this.openBlock.includeEnd) this.keepSummaryLine(index, line);
|
|
23060
23300
|
this.openBlock = void 0;
|
|
23061
|
-
} else this.keepSummaryLine(line);
|
|
23301
|
+
} else this.keepSummaryLine(index, line);
|
|
23062
23302
|
return;
|
|
23063
23303
|
}
|
|
23064
23304
|
const block = blocks.find(({ start }) => matchesSummaryPattern(trimmedLine, start));
|
|
23065
23305
|
if (block) {
|
|
23066
23306
|
this.openBlock = block;
|
|
23067
|
-
this.keepSummaryLine(line);
|
|
23307
|
+
this.keepSummaryLine(index, line);
|
|
23068
23308
|
return;
|
|
23069
23309
|
}
|
|
23070
|
-
if (lines.some((pattern) => matchesSummaryPattern(trimmedLine, pattern))) this.keepSummaryLine(line);
|
|
23310
|
+
if (lines.some((pattern) => matchesSummaryPattern(trimmedLine, pattern))) this.keepSummaryLine(index, line);
|
|
23071
23311
|
}
|
|
23072
|
-
keepSummaryLine(line) {
|
|
23073
|
-
|
|
23074
|
-
this.summaryLines.
|
|
23312
|
+
keepSummaryLine(index, line) {
|
|
23313
|
+
const { tail = 0 } = this.config.failureSummary;
|
|
23314
|
+
if (this.summaryLines.length >= MAX_SUMMARY_LINES - tail || line.trim() && this.summaryLines.some((kept) => kept.line === line)) return;
|
|
23315
|
+
this.summaryLines.push({
|
|
23316
|
+
index,
|
|
23317
|
+
line
|
|
23318
|
+
});
|
|
23075
23319
|
}
|
|
23076
23320
|
async consumeProgressStream(input) {
|
|
23077
23321
|
const lineReader = createInterface({
|
|
@@ -23079,36 +23323,28 @@ var BuildLogger = class {
|
|
|
23079
23323
|
crlfDelay: Infinity,
|
|
23080
23324
|
terminal: false
|
|
23081
23325
|
});
|
|
23082
|
-
|
|
23083
|
-
|
|
23084
|
-
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
|
|
23091
|
-
});
|
|
23092
|
-
this.updateProgress({
|
|
23093
|
-
nextCompletedStages,
|
|
23094
|
-
latestMessage: normalizedLine
|
|
23095
|
-
});
|
|
23326
|
+
const readPhase = this.config.createPhaseReader();
|
|
23327
|
+
let phase;
|
|
23328
|
+
for await (const line of lineReader) {
|
|
23329
|
+
const next = readPhase(stripAnsi(line).replace(/\r/g, "").trim());
|
|
23330
|
+
if (next !== void 0 && next !== phase) {
|
|
23331
|
+
phase = next;
|
|
23332
|
+
this.step?.update(phase);
|
|
23333
|
+
}
|
|
23334
|
+
}
|
|
23096
23335
|
}
|
|
23336
|
+
/**
|
|
23337
|
+
* Ends the step with `message` and its duration, or, for a failure, as
|
|
23338
|
+
* failed, then prints the failure summary and the log's path.
|
|
23339
|
+
*/
|
|
23097
23340
|
stop(message, success = true) {
|
|
23098
|
-
|
|
23099
|
-
if (
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
this.showLogFileLocation();
|
|
23104
|
-
} else {
|
|
23105
|
-
this.promptProgress.error(finalMessage);
|
|
23341
|
+
const step = this.step;
|
|
23342
|
+
if (!step?.active) return;
|
|
23343
|
+
if (success) step.succeed(message);
|
|
23344
|
+
else {
|
|
23345
|
+
step.fail(message);
|
|
23106
23346
|
this.showFailureSummary();
|
|
23107
23347
|
}
|
|
23108
|
-
this.state = {
|
|
23109
|
-
...this.state,
|
|
23110
|
-
stopped: true
|
|
23111
|
-
};
|
|
23112
23348
|
}
|
|
23113
23349
|
writeError(error) {
|
|
23114
23350
|
this.logWriter?.writeError(error);
|
|
@@ -23118,32 +23354,6 @@ var BuildLogger = class {
|
|
|
23118
23354
|
await this.logWriter.close();
|
|
23119
23355
|
this.logWriter = void 0;
|
|
23120
23356
|
}
|
|
23121
|
-
updateProgress({ nextCompletedStages, latestMessage }) {
|
|
23122
|
-
if (this.state.stopped) return;
|
|
23123
|
-
const progressDelta = nextCompletedStages - this.state.completedStages;
|
|
23124
|
-
if (progressDelta > 0) this.promptProgress.advance(progressDelta, latestMessage);
|
|
23125
|
-
else if (latestMessage && latestMessage !== this.state.latestMessage && this.shouldLogLine(latestMessage)) this.promptProgress.message(latestMessage);
|
|
23126
|
-
this.state = {
|
|
23127
|
-
...this.state,
|
|
23128
|
-
completedStages: Math.max(this.state.completedStages, nextCompletedStages),
|
|
23129
|
-
latestMessage
|
|
23130
|
-
};
|
|
23131
|
-
}
|
|
23132
|
-
finishRemainingProgress() {
|
|
23133
|
-
if (!this.promptProgress || this.state.completedStages >= this.stageCount) return;
|
|
23134
|
-
const remainingStages = this.stageCount - this.state.completedStages;
|
|
23135
|
-
this.promptProgress.advance(remainingStages, this.state.latestMessage);
|
|
23136
|
-
this.state = {
|
|
23137
|
-
...this.state,
|
|
23138
|
-
completedStages: this.stageCount
|
|
23139
|
-
};
|
|
23140
|
-
}
|
|
23141
|
-
shouldLogLine(line) {
|
|
23142
|
-
return matchesAnyPattern({
|
|
23143
|
-
line,
|
|
23144
|
-
patterns: this.config.importantLogPatterns
|
|
23145
|
-
});
|
|
23146
|
-
}
|
|
23147
23357
|
/**
|
|
23148
23358
|
* Prints the failure summary and the log's path to stderr, so a `--json`
|
|
23149
23359
|
* command's stdout stays its JSON.
|
|
@@ -23154,12 +23364,6 @@ var BuildLogger = class {
|
|
|
23154
23364
|
const message = [...summary, ...logPath ? [...summary.length ? [""] : [], `Build log: ${logPath}`] : []].join("\n");
|
|
23155
23365
|
if (message) p.log.error(message, { output: process.stderr });
|
|
23156
23366
|
}
|
|
23157
|
-
showLogFileLocation() {
|
|
23158
|
-
if (!this.logWriter?.logFilePath) return;
|
|
23159
|
-
const relativePath = path.relative(getCwd(), this.logWriter.logFilePath);
|
|
23160
|
-
const highlightedPath = typedColors.blueBright(relativePath);
|
|
23161
|
-
p.log.info(`Build log stored at ${highlightedPath}`);
|
|
23162
|
-
}
|
|
23163
23367
|
};
|
|
23164
23368
|
//#endregion
|
|
23165
23369
|
//#region src/appSetup.ts
|
|
@@ -25182,6 +25386,103 @@ const printBanner = (version) => {
|
|
|
25182
25386
|
console.log(banner(version));
|
|
25183
25387
|
};
|
|
25184
25388
|
//#endregion
|
|
25389
|
+
//#region src/credentialEnvNames.ts
|
|
25390
|
+
/**
|
|
25391
|
+
* The environment variables that override each kind, one entry per field.
|
|
25392
|
+
* A field with two names takes a file path or the file's contents; the path
|
|
25393
|
+
* wins when both are set.
|
|
25394
|
+
*/
|
|
25395
|
+
const CREDENTIAL_ENV_NAMES = {
|
|
25396
|
+
"asc-api-key": [
|
|
25397
|
+
["HOT_UPDATER_ASC_KEY_PATH", "HOT_UPDATER_ASC_KEY"],
|
|
25398
|
+
["HOT_UPDATER_ASC_KEY_ID"],
|
|
25399
|
+
["HOT_UPDATER_ASC_ISSUER_ID"]
|
|
25400
|
+
],
|
|
25401
|
+
"android-keystore": [
|
|
25402
|
+
["HOT_UPDATER_ANDROID_KEYSTORE_PATH", "HOT_UPDATER_ANDROID_KEYSTORE_BASE64"],
|
|
25403
|
+
["HOT_UPDATER_ANDROID_KEYSTORE_PASSWORD"],
|
|
25404
|
+
["HOT_UPDATER_ANDROID_KEY_ALIAS"],
|
|
25405
|
+
["HOT_UPDATER_ANDROID_KEY_PASSWORD"]
|
|
25406
|
+
],
|
|
25407
|
+
"play-service-account": [["HOT_UPDATER_PLAY_SERVICE_ACCOUNT_PATH", "HOT_UPDATER_PLAY_SERVICE_ACCOUNT_JSON"]]
|
|
25408
|
+
};
|
|
25409
|
+
//#endregion
|
|
25410
|
+
//#region src/credentialFiles.ts
|
|
25411
|
+
const TERMINATION_SIGNALS = [
|
|
25412
|
+
"SIGHUP",
|
|
25413
|
+
"SIGINT",
|
|
25414
|
+
"SIGTERM"
|
|
25415
|
+
];
|
|
25416
|
+
const liveDirectories = /* @__PURE__ */ new Set();
|
|
25417
|
+
const onSignal = (signal) => {
|
|
25418
|
+
removeLiveDirectories();
|
|
25419
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
25420
|
+
};
|
|
25421
|
+
const removeLiveDirectories = () => {
|
|
25422
|
+
for (const directory of liveDirectories) rmSync(directory, {
|
|
25423
|
+
force: true,
|
|
25424
|
+
recursive: true
|
|
25425
|
+
});
|
|
25426
|
+
liveDirectories.clear();
|
|
25427
|
+
stopListening();
|
|
25428
|
+
};
|
|
25429
|
+
const startListening = () => {
|
|
25430
|
+
process.on("exit", removeLiveDirectories);
|
|
25431
|
+
for (const signal of TERMINATION_SIGNALS) process.on(signal, onSignal);
|
|
25432
|
+
};
|
|
25433
|
+
const stopListening = () => {
|
|
25434
|
+
process.off("exit", removeLiveDirectories);
|
|
25435
|
+
for (const signal of TERMINATION_SIGNALS) process.off(signal, onSignal);
|
|
25436
|
+
};
|
|
25437
|
+
const trackDirectory = (directory) => {
|
|
25438
|
+
if (liveDirectories.size === 0) startListening();
|
|
25439
|
+
liveDirectories.add(directory);
|
|
25440
|
+
};
|
|
25441
|
+
const untrackDirectory = (directory) => {
|
|
25442
|
+
if (liveDirectories.delete(directory) && liveDirectories.size === 0) stopListening();
|
|
25443
|
+
};
|
|
25444
|
+
/**
|
|
25445
|
+
* Creates the temporary files of one credential resolver: a `mkdtemp`
|
|
25446
|
+
* directory, created on the first write and removed by `dispose` or, if the
|
|
25447
|
+
* process exits first, on `exit` or a termination signal.
|
|
25448
|
+
*/
|
|
25449
|
+
const createCredentialFiles = () => {
|
|
25450
|
+
let directory;
|
|
25451
|
+
let disposal;
|
|
25452
|
+
const ensureDirectory = () => {
|
|
25453
|
+
directory ??= fs$2.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-credentials-")).then((created) => {
|
|
25454
|
+
trackDirectory(created);
|
|
25455
|
+
return created;
|
|
25456
|
+
});
|
|
25457
|
+
return directory;
|
|
25458
|
+
};
|
|
25459
|
+
return {
|
|
25460
|
+
write: async (fileName, contents) => {
|
|
25461
|
+
if (disposal) throw new Error("The credential files have been disposed.");
|
|
25462
|
+
if (typeof contents === "string") registerSecretValue(contents);
|
|
25463
|
+
const fileDirectory = await fs$2.mkdtemp(path$1.join(await ensureDirectory(), "item-"));
|
|
25464
|
+
const filePath = path$1.join(fileDirectory, path$1.basename(fileName));
|
|
25465
|
+
await fs$2.writeFile(filePath, contents, {
|
|
25466
|
+
flag: "wx",
|
|
25467
|
+
mode: 384
|
|
25468
|
+
});
|
|
25469
|
+
return filePath;
|
|
25470
|
+
},
|
|
25471
|
+
dispose: () => {
|
|
25472
|
+
disposal ??= (async () => {
|
|
25473
|
+
const created = await directory?.catch(() => void 0);
|
|
25474
|
+
if (!created) return;
|
|
25475
|
+
await fs$2.rm(created, {
|
|
25476
|
+
force: true,
|
|
25477
|
+
recursive: true
|
|
25478
|
+
});
|
|
25479
|
+
untrackDirectory(created);
|
|
25480
|
+
})();
|
|
25481
|
+
return disposal;
|
|
25482
|
+
}
|
|
25483
|
+
};
|
|
25484
|
+
};
|
|
25485
|
+
//#endregion
|
|
25185
25486
|
//#region src/initOptions.ts
|
|
25186
25487
|
var InitError = class extends Error {
|
|
25187
25488
|
name = "InitError";
|
|
@@ -25247,9 +25548,17 @@ const readEnvFile = async (filePath, allowMissing) => {
|
|
|
25247
25548
|
return parseEnv$1(content);
|
|
25248
25549
|
};
|
|
25249
25550
|
const getHotUpdaterInitInputEnv = ({ env, managedEnv }, nonInteractive) => nonInteractive ? env : managedEnv;
|
|
25250
|
-
|
|
25551
|
+
/**
|
|
25552
|
+
* Init's inputs: `.env.hotupdater` (`managedEnv`), with `envFile`'s over
|
|
25553
|
+
* it (`env`). `secretsEnv`, the credentials init read from a team secrets
|
|
25554
|
+
* store, sits under both, as values the file would hold.
|
|
25555
|
+
*/
|
|
25556
|
+
const readHotUpdaterInitEnv = async (cwd, envFile, secretsEnv = {}) => {
|
|
25251
25557
|
const savedEnvPath = path.resolve(cwd, HOT_UPDATER_ENV_PATH);
|
|
25252
|
-
const savedEnv =
|
|
25558
|
+
const savedEnv = {
|
|
25559
|
+
...secretsEnv,
|
|
25560
|
+
...await readEnvFile(savedEnvPath, true)
|
|
25561
|
+
};
|
|
25253
25562
|
if (!envFile) return {
|
|
25254
25563
|
env: {},
|
|
25255
25564
|
managedEnv: savedEnv
|
|
@@ -25291,6 +25600,17 @@ const readLoadedHotUpdaterEnv = async (cwd) => {
|
|
|
25291
25600
|
throw error;
|
|
25292
25601
|
}
|
|
25293
25602
|
};
|
|
25603
|
+
/**
|
|
25604
|
+
* `file`, an env file a command was given, relative to `cwd`, as
|
|
25605
|
+
* `parseLoadedEnv` reads it; a file that can't be read is refused by name.
|
|
25606
|
+
*/
|
|
25607
|
+
const readLoadedEnvFile = async (file, cwd) => {
|
|
25608
|
+
try {
|
|
25609
|
+
return parseLoadedEnv(await fs$1.readFile(path.resolve(cwd, file), "utf-8"));
|
|
25610
|
+
} catch (error) {
|
|
25611
|
+
throw new Error(`Can't read ${file}.\n${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
25612
|
+
}
|
|
25613
|
+
};
|
|
25294
25614
|
//#endregion
|
|
25295
25615
|
//#region src/localKeyStore.ts
|
|
25296
25616
|
/**
|
|
@@ -25388,7 +25708,7 @@ const assertPrivateDirectory = async (directory) => {
|
|
|
25388
25708
|
if (isMissing(error)) return void 0;
|
|
25389
25709
|
throw error;
|
|
25390
25710
|
});
|
|
25391
|
-
if (stats && !stats.isDirectory()) throw new Error(`localSecrets()
|
|
25711
|
+
if (stats && !stats.isDirectory()) throw new Error(fitMessage([`localSecrets() can't keep secrets in ${directory}.`, "It is not a directory: set its path to one."]));
|
|
25392
25712
|
const real = realpathThroughParents(directory);
|
|
25393
25713
|
let probe = real;
|
|
25394
25714
|
while (!existsSync(probe) && path$1.dirname(probe) !== probe) probe = path$1.dirname(probe);
|
|
@@ -25396,7 +25716,12 @@ const assertPrivateDirectory = async (directory) => {
|
|
|
25396
25716
|
if (!root) return;
|
|
25397
25717
|
const top = realpath(root);
|
|
25398
25718
|
if (isIgnoredByGit(top, real)) return;
|
|
25399
|
-
throw new Error(
|
|
25719
|
+
throw new Error(fitMessage([
|
|
25720
|
+
`localSecrets() refuses ${directory}: its secrets could be committed.`,
|
|
25721
|
+
`The git repository ${top} doesn't ignore it.`,
|
|
25722
|
+
`Add ${path$1.relative(top, real).split(path$1.sep).join("/")}/ to its .gitignore,`,
|
|
25723
|
+
"or set localSecrets({ path })."
|
|
25724
|
+
]));
|
|
25400
25725
|
};
|
|
25401
25726
|
/** Makes `directory` 0700, and every directory it creates on the way. */
|
|
25402
25727
|
const makePrivateDirectory = async (directory) => {
|
|
@@ -25464,27 +25789,6 @@ const readLocalKey = (name, cwd) => defaultStore.read(name, cwd);
|
|
|
25464
25789
|
/** Stores `value` as `name` in `localSecrets()`'s default store. */
|
|
25465
25790
|
const writeLocalKey = (name, value, cwd) => defaultStore.write(name, value, cwd);
|
|
25466
25791
|
//#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
25792
|
//#region src/reservedVariables.ts
|
|
25489
25793
|
/**
|
|
25490
25794
|
* Variables that change how a process loads code or which credentials Hot
|
|
@@ -25504,7 +25808,6 @@ const RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
|
25504
25808
|
"PYTHONSTARTUP",
|
|
25505
25809
|
"PERL5OPT",
|
|
25506
25810
|
...Object.values(CREDENTIAL_ENV_NAMES).flat(2),
|
|
25507
|
-
"HOT_UPDATER_SIGNING_TOKEN",
|
|
25508
25811
|
"HOT_UPDATER_HOME"
|
|
25509
25812
|
]);
|
|
25510
25813
|
const RESERVED_PREFIXES = [
|
|
@@ -25512,14 +25815,14 @@ const RESERVED_PREFIXES = [
|
|
|
25512
25815
|
"DYLD_",
|
|
25513
25816
|
"GIT_",
|
|
25514
25817
|
"NPM_CONFIG_",
|
|
25515
|
-
"
|
|
25818
|
+
"INFISICAL_"
|
|
25516
25819
|
];
|
|
25517
25820
|
/**
|
|
25518
25821
|
* Whether `name` is a variable no secret may set: one a loader or
|
|
25519
25822
|
* interpreter reads (`NODE_*` but `NODE_ENV`, `PATH`, `LD_*`, `DYLD_*`,
|
|
25520
25823
|
* `BASH_ENV`, `ENV`, `GIT_*`, `npm_config_*`, the JVM's, Gradle's, Ruby's,
|
|
25521
25824
|
* Python's, and Perl's options), or one Hot Updater reads its credentials
|
|
25522
|
-
* from. `
|
|
25825
|
+
* from. `secret()` refuses these names, and so does every environment the
|
|
25523
25826
|
* CLI gives a child process from resolved secrets.
|
|
25524
25827
|
*/
|
|
25525
25828
|
const isReservedVariableName = (name) => {
|
|
@@ -25539,20 +25842,25 @@ const assertUnreservedEnv = (env, what) => {
|
|
|
25539
25842
|
//#endregion
|
|
25540
25843
|
//#region src/secrets.ts
|
|
25541
25844
|
const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
25542
|
-
/** A secret's name, as `secret
|
|
25845
|
+
/** A secret's name, as `secret("<name>")` takes one. */
|
|
25543
25846
|
const SECRET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
25544
|
-
|
|
25545
|
-
|
|
25847
|
+
/**
|
|
25848
|
+
* The brand `secret()` sets, which plugin-core's `SecretRef` types. It is a
|
|
25849
|
+
* `Symbol.for` key: the config file loads its own copy of this module.
|
|
25850
|
+
*/
|
|
25851
|
+
const SECRET_REF = Symbol.for("@hot-updater/secret-ref");
|
|
25852
|
+
/** Whether `name` is a secret's name, as `secret("<name>")` takes one. */
|
|
25546
25853
|
const isSecretName = (name) => SECRET_NAME_PATTERN.test(name);
|
|
25854
|
+
/** Whether `value` is a `secret("<name>")` reference. */
|
|
25855
|
+
const isSecretRef = (value) => typeof value === "function" && Reflect.get(value, SECRET_REF) === true && isSecretName(value.name);
|
|
25547
25856
|
/**
|
|
25548
|
-
* The secret name a config's `secret
|
|
25549
|
-
* `release-keystore` for `"
|
|
25550
|
-
* field
|
|
25857
|
+
* The secret name a config's `secret("<name>")` names, such as
|
|
25858
|
+
* `release-keystore` for `secret("release-keystore")`. `field` is the
|
|
25859
|
+
* config field it is in, which the error names.
|
|
25551
25860
|
*/
|
|
25552
25861
|
const parseSecretReference = (reference, field) => {
|
|
25553
|
-
|
|
25554
|
-
|
|
25555
|
-
return name;
|
|
25862
|
+
if (!isSecretRef(reference)) throw new Error(`${field} takes secret("<name>").`);
|
|
25863
|
+
return reference.name;
|
|
25556
25864
|
};
|
|
25557
25865
|
/**
|
|
25558
25866
|
* The environment variable the default store reads a secret from: a name
|
|
@@ -25572,8 +25880,6 @@ const readSecretVariables = async (names, cwd) => {
|
|
|
25572
25880
|
/** `localSecrets()`'s name, the `secrets` adapter when the config sets none. */
|
|
25573
25881
|
const LOCAL_SECRETS_NAME = "localSecrets";
|
|
25574
25882
|
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
25883
|
const isLocalKeyStoreSecrets = (secrets) => LOCAL_KEY_STORE in secrets;
|
|
25578
25884
|
/**
|
|
25579
25885
|
* Secrets in JSON files on this machine, the `secrets` adapter when the
|
|
@@ -25582,76 +25888,80 @@ const isLocalKeyStoreSecrets = (secrets) => LOCAL_KEY_STORE in secrets;
|
|
|
25582
25888
|
* `~/.hot-updater/secrets/<project-key>/<name>.json`; `set` writes that
|
|
25583
25889
|
* file, 0600 in a 0700 directory, outside the project, so it survives
|
|
25584
25890
|
* `node_modules` and is never committed. `HOT_UPDATER_HOME` moves
|
|
25585
|
-
* `~/.hot-updater`. The project key
|
|
25586
|
-
*
|
|
25587
|
-
*
|
|
25891
|
+
* `~/.hot-updater`. The project key hashes the checkout's real path
|
|
25892
|
+
* (`getProjectKey`), so each clone of an app has its own directory, and
|
|
25893
|
+
* another clone or machine signs in and issues a key of its own.
|
|
25588
25894
|
*
|
|
25589
25895
|
* @example
|
|
25590
25896
|
* export default defineConfig({ secrets: localSecrets(), ... });
|
|
25591
25897
|
*/
|
|
25592
|
-
const localSecrets = (options = {}) =>
|
|
25898
|
+
const localSecrets = (options = {}) => createLocalSecrets(createLocalSecretsStore(options));
|
|
25593
25899
|
/**
|
|
25594
|
-
* The
|
|
25595
|
-
*
|
|
25596
|
-
*
|
|
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.
|
|
25900
|
+
* The `secrets` adapter a config load uses: the config's own adapter as it
|
|
25901
|
+
* is, with no store on this machine under it, or `localSecrets()` when the
|
|
25902
|
+
* config sets none.
|
|
25601
25903
|
*/
|
|
25602
|
-
const
|
|
25603
|
-
const
|
|
25604
|
-
const describeFile = (name) => store.file(name, getCwd());
|
|
25904
|
+
const resolveSecretsAdapter = (configured) => configured ?? localSecrets();
|
|
25905
|
+
const createLocalSecrets = (store) => {
|
|
25605
25906
|
return {
|
|
25606
|
-
name:
|
|
25607
|
-
[LOCAL_KEY_STORE]:
|
|
25608
|
-
configured,
|
|
25609
|
-
store
|
|
25610
|
-
} : { store },
|
|
25907
|
+
name: LOCAL_SECRETS_NAME,
|
|
25908
|
+
[LOCAL_KEY_STORE]: { store },
|
|
25611
25909
|
get: async (names, context) => {
|
|
25612
|
-
const values =
|
|
25910
|
+
const values = await readSecretVariables(names, context.cwd);
|
|
25613
25911
|
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
|
-
}
|
|
25912
|
+
for (const name of names) read[name] = values[name] || await store.read(name, context.cwd);
|
|
25618
25913
|
return read;
|
|
25619
25914
|
},
|
|
25620
25915
|
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
25916
|
await store.write(name, value, context.cwd);
|
|
25632
25917
|
},
|
|
25633
|
-
describe: (name) =>
|
|
25918
|
+
describe: (name) => `${secretVariableName(name)} in the environment, or ${store.file(name, getCwd())} on this machine`
|
|
25634
25919
|
};
|
|
25635
25920
|
};
|
|
25636
25921
|
/**
|
|
25637
|
-
*
|
|
25638
|
-
*
|
|
25639
|
-
|
|
25922
|
+
* `localSecrets()`'s store, for `localSecrets()` itself; undefined for any
|
|
25923
|
+
* other adapter, which keeps nothing on this machine.
|
|
25924
|
+
*/
|
|
25925
|
+
const getLocalSecretsStore = (secrets) => isLocalKeyStoreSecrets(secrets) ? secrets[LOCAL_KEY_STORE].store : void 0;
|
|
25926
|
+
/**
|
|
25927
|
+
* The config's own `secrets` adapter: undefined for `localSecrets()`,
|
|
25928
|
+
* which is the adapter when the config sets none. A called `secret()` reads
|
|
25929
|
+
* from it.
|
|
25640
25930
|
*/
|
|
25641
|
-
const
|
|
25931
|
+
const getConfiguredAdapter = (secrets) => isLocalKeyStoreSecrets(secrets) ? void 0 : secrets;
|
|
25642
25932
|
/**
|
|
25643
|
-
*
|
|
25644
|
-
*
|
|
25645
|
-
* `secretEnv` reads deploy credentials from it.
|
|
25933
|
+
* Where `secrets.set` stores `name`, for messages: the config's adapter,
|
|
25934
|
+
* else `localSecrets()`'s file.
|
|
25646
25935
|
*/
|
|
25647
|
-
const
|
|
25936
|
+
const describeStoredSecret = (secrets, name) => getLocalSecretsStore(secrets)?.file(name, getCwd()) ?? describeSecret(secrets, name);
|
|
25648
25937
|
/**
|
|
25649
|
-
*
|
|
25650
|
-
*
|
|
25938
|
+
* `name`'s value, read in the order `secrets.get` reads it, with the one
|
|
25939
|
+
* place it came from, for messages: the config's adapter
|
|
25940
|
+
* (`describeSecret`), the environment variable `localSecrets()` reads
|
|
25941
|
+
* first, or `localSecrets()`'s file. Undefined when nothing holds it.
|
|
25651
25942
|
*/
|
|
25652
|
-
const
|
|
25653
|
-
const
|
|
25654
|
-
|
|
25943
|
+
const readSecretWithSource = async (secrets, name, cwd) => {
|
|
25944
|
+
const held = (value) => typeof value === "string" && value.length > 0;
|
|
25945
|
+
const store = getLocalSecretsStore(secrets);
|
|
25946
|
+
if (!store) {
|
|
25947
|
+
const value = (await readSecrets(secrets, [name], cwd))[name];
|
|
25948
|
+
return held(value) ? {
|
|
25949
|
+
value,
|
|
25950
|
+
source: describeSecret(secrets, name)
|
|
25951
|
+
} : void 0;
|
|
25952
|
+
}
|
|
25953
|
+
const variable = (await readSecretVariables([name], cwd))[name];
|
|
25954
|
+
if (held(variable)) return {
|
|
25955
|
+
value: variable,
|
|
25956
|
+
source: `${secretVariableName(name)} (environment)`
|
|
25957
|
+
};
|
|
25958
|
+
const value = await store.read(name, cwd).catch((error) => {
|
|
25959
|
+
throw secretReadError(secrets, [name], error);
|
|
25960
|
+
});
|
|
25961
|
+
return held(value) ? {
|
|
25962
|
+
value,
|
|
25963
|
+
source: store.file(name, cwd)
|
|
25964
|
+
} : void 0;
|
|
25655
25965
|
};
|
|
25656
25966
|
/**
|
|
25657
25967
|
* Whether a key `secrets` stores stays on this machine: it is `localSecrets()`,
|
|
@@ -25662,7 +25972,7 @@ const CONFIGURED_SECRETS = Symbol.for("@hot-updater/cli-tools/secrets");
|
|
|
25662
25972
|
const LOADING_SCOPE = Symbol.for("@hot-updater/cli-tools/secrets-scope");
|
|
25663
25973
|
/**
|
|
25664
25974
|
* Evaluates a config, the file or a config function, with `scope` as the
|
|
25665
|
-
* scope of every `
|
|
25975
|
+
* scope of every `secret()` it creates, so each reads the `secrets` of the
|
|
25666
25976
|
* config that created it, whatever config loads after. It is kept on
|
|
25667
25977
|
* `globalThis`: the config file loads its own copy of Hot Updater's modules.
|
|
25668
25978
|
*/
|
|
@@ -25678,7 +25988,7 @@ const withSecretsScope = async (scope, evaluate) => {
|
|
|
25678
25988
|
};
|
|
25679
25989
|
/**
|
|
25680
25990
|
* Makes `secrets` the adapter a credential resolver reads from by default,
|
|
25681
|
-
* and a `
|
|
25991
|
+
* and a `secret()` created outside any config load, as `loadConfig` does
|
|
25682
25992
|
* with the config's. It is kept on `globalThis`: the config file loads Hot
|
|
25683
25993
|
* Updater's modules again, as its own copies.
|
|
25684
25994
|
*/
|
|
@@ -25686,217 +25996,548 @@ const setConfiguredSecrets = (secrets) => {
|
|
|
25686
25996
|
globalThis[CONFIGURED_SECRETS] = secrets;
|
|
25687
25997
|
};
|
|
25688
25998
|
/**
|
|
25689
|
-
* The config's `secrets` adapter
|
|
25690
|
-
*
|
|
25999
|
+
* The config's `secrets` adapter (`resolveSecretsAdapter`), `localSecrets()`
|
|
26000
|
+
* until a config loads.
|
|
25691
26001
|
*/
|
|
25692
26002
|
const getConfiguredSecrets = () => globalThis[CONFIGURED_SECRETS] ?? localSecrets();
|
|
26003
|
+
/**
|
|
26004
|
+
* `names` from `secrets` in one `get`. A read that fails names the secrets
|
|
26005
|
+
* and the adapter, with the adapter's own error on the next line, such as
|
|
26006
|
+
* `EACCES: permission denied, open '<file>'`.
|
|
26007
|
+
*/
|
|
26008
|
+
const readSecrets = async (secrets, names, cwd) => {
|
|
26009
|
+
try {
|
|
26010
|
+
return await secrets.get(names, { cwd });
|
|
26011
|
+
} catch (error) {
|
|
26012
|
+
throw secretReadError(secrets, names, error);
|
|
26013
|
+
}
|
|
26014
|
+
};
|
|
26015
|
+
const secretReadError = (secrets, names, error) => new Error(fitMessage([`Couldn't read ${list$1(names)} from the ${secrets.name} secrets adapter.`, (error instanceof Error ? error.message : String(error)).split("\n")[0]]), { cause: error });
|
|
25693
26016
|
/** Where an adapter keeps `name`, for messages. */
|
|
25694
26017
|
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)}`;
|
|
26018
|
+
const list$1 = (names) => names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
25696
26019
|
/**
|
|
25697
|
-
*
|
|
25698
|
-
*
|
|
25699
|
-
*
|
|
25700
|
-
*
|
|
25701
|
-
*
|
|
25702
|
-
*
|
|
25703
|
-
*
|
|
26020
|
+
* `secret("<name>")`: the secret `name` in `hot-updater.config.ts`, for a
|
|
26021
|
+
* build credential, such as `signing.keystore`, or any adapter credential,
|
|
26022
|
+
* which calls it on first use, so loading the config reads no secret.
|
|
26023
|
+
* Called, it reads `name`'s variable (`secretVariableName`) in the
|
|
26024
|
+
* environment or `.env.hotupdater`, else the config's `secrets` adapter:
|
|
26025
|
+
* the first call reads every name the config's `secret()`s reference and
|
|
26026
|
+
* the environment doesn't set, in one `get`. With `localSecrets()`, the
|
|
26027
|
+
* variable must be set: its files hold only the keys Hot Updater stores.
|
|
26028
|
+
* Values are masked in the CLI's logs and never set in `process.env`.
|
|
26029
|
+
* Names a loader or interpreter reads, such as `NODE_OPTIONS`, are refused.
|
|
26030
|
+
* A server passes its own environment's values instead.
|
|
25704
26031
|
*
|
|
25705
26032
|
* @example
|
|
25706
|
-
*
|
|
25707
|
-
* s3Storage({
|
|
26033
|
+
* r2Storage({
|
|
25708
26034
|
* bucketName: "bundles",
|
|
25709
|
-
*
|
|
25710
|
-
* credentials:
|
|
25711
|
-
*
|
|
25712
|
-
*
|
|
25713
|
-
* accessKeyId: env.S3_ACCESS_KEY_ID,
|
|
25714
|
-
* secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
|
25715
|
-
* };
|
|
26035
|
+
* accountId: "<account-id>",
|
|
26036
|
+
* credentials: {
|
|
26037
|
+
* accessKeyId: secret("HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID"),
|
|
26038
|
+
* secretAccessKey: secret("HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"),
|
|
25716
26039
|
* },
|
|
25717
26040
|
* });
|
|
25718
26041
|
*/
|
|
25719
|
-
const
|
|
25720
|
-
|
|
25721
|
-
|
|
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.`);
|
|
26042
|
+
const secret = (name) => {
|
|
26043
|
+
if (typeof name !== "string" || !isSecretName(name)) throw new Error([`secret() can't take the name ${JSON.stringify(name)}.`, "Use up to 64 letters, digits, \".\", \"_\", \"-\"; start with a letter or digit."].join("\n"));
|
|
26044
|
+
if (isReservedVariableName(name)) throw new Error([`secret() refuses ${name}.`, "It changes how a process loads code, or the credentials Hot Updater uses."].join("\n"));
|
|
25725
26045
|
const scope = globalThis[LOADING_SCOPE];
|
|
25726
|
-
|
|
25727
|
-
|
|
25728
|
-
|
|
25729
|
-
|
|
25730
|
-
|
|
25731
|
-
|
|
25732
|
-
|
|
25733
|
-
|
|
25734
|
-
|
|
25735
|
-
|
|
25736
|
-
|
|
25737
|
-
|
|
25738
|
-
|
|
25739
|
-
|
|
25740
|
-
|
|
25741
|
-
|
|
25742
|
-
|
|
25743
|
-
|
|
25744
|
-
|
|
25745
|
-
|
|
25746
|
-
|
|
25747
|
-
|
|
25748
|
-
|
|
25749
|
-
|
|
25750
|
-
|
|
25751
|
-
return
|
|
25752
|
-
}
|
|
26046
|
+
if (scope) (scope.names ??= /* @__PURE__ */ new Set()).add(name);
|
|
26047
|
+
let read;
|
|
26048
|
+
const ref = () => read ??= readSecretRef(name, scope).catch((error) => {
|
|
26049
|
+
read = void 0;
|
|
26050
|
+
throw error;
|
|
26051
|
+
});
|
|
26052
|
+
Object.defineProperty(ref, "name", { value: name });
|
|
26053
|
+
Object.defineProperty(ref, SECRET_REF, { value: true });
|
|
26054
|
+
return Object.freeze(ref);
|
|
26055
|
+
};
|
|
26056
|
+
/**
|
|
26057
|
+
* The scope's names to read in one `get`: those no build credential takes
|
|
26058
|
+
* and the environment doesn't set, with `name`.
|
|
26059
|
+
*/
|
|
26060
|
+
const batchedNames = (scope, name, fileEnv) => {
|
|
26061
|
+
const names = [...scope.names ?? []].filter((candidate) => !scope.buildNames?.has(candidate) && getHotUpdaterEnvValue(fileEnv, secretVariableName(candidate)) === void 0);
|
|
26062
|
+
return names.includes(name) ? names : [...names, name];
|
|
26063
|
+
};
|
|
26064
|
+
const readSecretRef = async (name, scope) => {
|
|
26065
|
+
const cwd = getCwd();
|
|
26066
|
+
const fileEnv = await readLoadedHotUpdaterEnv(cwd);
|
|
26067
|
+
const variable = secretVariableName(name);
|
|
26068
|
+
const fromEnv = getHotUpdaterEnvValue(fileEnv, variable);
|
|
26069
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
26070
|
+
registerSecretValue(fromEnv);
|
|
26071
|
+
return fromEnv;
|
|
26072
|
+
}
|
|
26073
|
+
const secrets = getConfiguredAdapter(scope?.secrets ?? getConfiguredSecrets());
|
|
26074
|
+
if (secrets === void 0) throw new Error(fitMessage([
|
|
26075
|
+
`secret("${name}") has no value.`,
|
|
26076
|
+
`Set ${variable} in the environment or .env.hotupdater.`,
|
|
26077
|
+
"Or set the config's secrets to a team store."
|
|
26078
|
+
]));
|
|
26079
|
+
let values;
|
|
26080
|
+
if (scope) {
|
|
26081
|
+
scope.read ??= readSecrets(secrets, batchedNames(scope, name, fileEnv), cwd).catch((error) => {
|
|
26082
|
+
scope.read = void 0;
|
|
26083
|
+
throw error;
|
|
26084
|
+
});
|
|
26085
|
+
values = await scope.read;
|
|
26086
|
+
}
|
|
26087
|
+
if (!values || !Object.hasOwn(values, name)) values = await readSecrets(secrets, [name], cwd);
|
|
26088
|
+
const value = Object.hasOwn(values, name) ? values[name] : void 0;
|
|
26089
|
+
if (typeof value !== "string" || value === "") throw new Error(fitMessage([
|
|
26090
|
+
`secret("${name}") has no value.`,
|
|
26091
|
+
`It isn't in ${describeSecret(secrets, name)}.`,
|
|
26092
|
+
`Store it there, or set ${variable}.`
|
|
26093
|
+
]));
|
|
26094
|
+
registerSecretValue(value);
|
|
26095
|
+
return value;
|
|
25753
26096
|
};
|
|
25754
26097
|
//#endregion
|
|
25755
|
-
//#region src/
|
|
25756
|
-
/**
|
|
25757
|
-
const
|
|
26098
|
+
//#region src/credentialResolver.ts
|
|
26099
|
+
/** Passwords are read as set: trimming would change them. */
|
|
26100
|
+
const UNTRIMMED_ENV_NAMES = /* @__PURE__ */ new Set(["HOT_UPDATER_ANDROID_KEYSTORE_PASSWORD", "HOT_UPDATER_ANDROID_KEY_PASSWORD"]);
|
|
26101
|
+
const describeField = ([name, alternative]) => alternative ? `${name} (or ${alternative})` : name;
|
|
26102
|
+
const describeFields = (fields) => {
|
|
26103
|
+
const described = fields.map(describeField);
|
|
26104
|
+
if (described.length <= 2) return described.join(" and ");
|
|
26105
|
+
return `${described.slice(0, -1).join(", ")}, and ${described.at(-1)}`;
|
|
26106
|
+
};
|
|
25758
26107
|
/**
|
|
25759
|
-
* A
|
|
25760
|
-
*
|
|
26108
|
+
* A command to print, broken at spaces with ` \` once a line would pass
|
|
26109
|
+
* 74 columns, each continuation indented two spaces, so a terminal never
|
|
26110
|
+
* wraps it mid-word and it still pastes as one command.
|
|
25761
26111
|
*/
|
|
25762
|
-
|
|
25763
|
-
|
|
26112
|
+
const formatCommand = (command) => {
|
|
26113
|
+
const lines = [];
|
|
26114
|
+
let line = "";
|
|
26115
|
+
for (const word of command.split(" ")) {
|
|
26116
|
+
const next = line ? `${line} ${word}` : word;
|
|
26117
|
+
if (line && next.length > 72) {
|
|
26118
|
+
lines.push(`${line} \\`);
|
|
26119
|
+
line = ` ${word}`;
|
|
26120
|
+
} else line = next;
|
|
26121
|
+
}
|
|
26122
|
+
lines.push(line);
|
|
26123
|
+
return lines.join("\n");
|
|
26124
|
+
};
|
|
26125
|
+
/**
|
|
26126
|
+
* `lead` and the command it names on one line when they fit 77 columns,
|
|
26127
|
+
* else the command on the lines after it.
|
|
26128
|
+
*/
|
|
26129
|
+
const withLead = (lead, command) => lead.length + 1 + command.length <= 77 ? `${lead} ${command}` : `${lead}\n${formatCommand(command)}`;
|
|
26130
|
+
/**
|
|
26131
|
+
* Why a build has no upload keystore, steering away from a second key:
|
|
26132
|
+
* Play rejects builds signed with a key it does not have. Under a team's
|
|
26133
|
+
* adapter the keystore is someone's already, so it is imported, never
|
|
26134
|
+
* created; only `localSecrets()` alone offers a new one, for an app with
|
|
26135
|
+
* no `.aab` on Play yet.
|
|
26136
|
+
*/
|
|
26137
|
+
const describeMissingKeystore = (store, item, name) => {
|
|
26138
|
+
const configured = getConfiguredAdapter(store);
|
|
26139
|
+
if (configured) return [
|
|
26140
|
+
`No ${item} in ${describeSecret(configured, name)}.`,
|
|
26141
|
+
"Ask whoever set it up to store it there, or import it with:",
|
|
26142
|
+
formatCommand(credentialPutCommand("android-keystore", name))
|
|
26143
|
+
].join("\n");
|
|
26144
|
+
return [
|
|
26145
|
+
`No ${item} in the ${store.name} secrets adapter.`,
|
|
26146
|
+
withLead("Store the app's upload key:", credentialPutCommand("android-keystore", name)),
|
|
26147
|
+
withLead("No .aab on Play yet? Create one:", credentialIssueCommand("android-keystore", name))
|
|
26148
|
+
].join("\n");
|
|
26149
|
+
};
|
|
26150
|
+
/** The file each `secrets put <name> <file>` command names. */
|
|
26151
|
+
const PUT_FILES = {
|
|
26152
|
+
"android-keystore": "<upload.jks>",
|
|
26153
|
+
"play-service-account": "<key.json>"
|
|
26154
|
+
};
|
|
26155
|
+
/**
|
|
26156
|
+
* The command that stores the file you have as `name`: the service
|
|
26157
|
+
* account's JSON key, or the app's upload keystore. `--kind` is named only
|
|
26158
|
+
* for a name that is not the kind.
|
|
26159
|
+
*/
|
|
26160
|
+
const credentialPutCommand = (kind, name = kind) => `npx hot-updater secrets put ${name}${name === kind ? "" : ` --kind ${kind}`} ${PUT_FILES[kind]}`;
|
|
26161
|
+
/**
|
|
26162
|
+
* The command that stores the key Hot Updater issues as `name`: an App
|
|
26163
|
+
* Store Connect API key after an Apple Account sign-in, and an upload
|
|
26164
|
+
* keystore it creates.
|
|
26165
|
+
*/
|
|
26166
|
+
const credentialIssueCommand = (kind, name = kind) => `npx hot-updater secrets issue ${kind}${name === kind ? "" : ` ${name}`}`;
|
|
26167
|
+
/** The command group of each issued kind's shortcut, `<group> login`. */
|
|
26168
|
+
const CREDENTIAL_LOGIN_GROUPS = { "asc-api-key": "apple" };
|
|
26169
|
+
/**
|
|
26170
|
+
* The command people meet first that issues `kind` as `name`: its shortcut,
|
|
26171
|
+
* `apple login` for an App Store Connect API key, which takes nothing and
|
|
26172
|
+
* stores the kind's own name; for another name, `credentialIssueCommand`.
|
|
26173
|
+
*/
|
|
26174
|
+
const credentialLoginCommand = (kind, name = kind) => name === kind ? `npx hot-updater ${CREDENTIAL_LOGIN_GROUPS[kind]} login` : credentialIssueCommand(kind, name);
|
|
26175
|
+
/**
|
|
26176
|
+
* The command that stores a missing credential of `kind` as `name`: the
|
|
26177
|
+
* key Hot Updater issues after an Apple sign-in, or the file you have.
|
|
26178
|
+
*/
|
|
26179
|
+
const credentialStoreCommand = (kind, name = kind) => kind === "asc-api-key" ? credentialLoginCommand(kind, name) : credentialPutCommand(kind, name);
|
|
26180
|
+
/**
|
|
26181
|
+
* Why `secrets` has no `name`, and the command that brings the existing
|
|
26182
|
+
* one in, never one that creates another: a signing key may already be
|
|
26183
|
+
* trusted by a shipped app or a store. Under a team's adapter the name or
|
|
26184
|
+
* the access is wrong; under `localSecrets()` this machine lacks it.
|
|
26185
|
+
*/
|
|
26186
|
+
const describeMissingSecret = (secrets, name, { action = "import your key", command }) => [fitMessage(getConfiguredAdapter(secrets) ? [`${name} isn't in the ${secrets.name} secrets adapter.`, `Check the name and your access, or ${action}:`] : [`${name} isn't on this machine.`, `Check the name, set ${secretVariableName(name)}, or ${action}:`]), formatCommand(command)].join("\n");
|
|
26187
|
+
var CredentialUnavailableError = class extends Error {
|
|
26188
|
+
kind;
|
|
26189
|
+
/** The environment variables that can provide this credential. */
|
|
26190
|
+
envNames;
|
|
26191
|
+
constructor(kind, message, options) {
|
|
25764
26192
|
super(message, options);
|
|
25765
|
-
this.name = "
|
|
26193
|
+
this.name = "CredentialUnavailableError";
|
|
26194
|
+
this.kind = kind;
|
|
26195
|
+
this.envNames = CREDENTIAL_ENV_NAMES[kind].flat();
|
|
25766
26196
|
}
|
|
25767
26197
|
};
|
|
25768
|
-
const
|
|
25769
|
-
const
|
|
25770
|
-
|
|
25771
|
-
|
|
25772
|
-
|
|
25773
|
-
|
|
25774
|
-
|
|
25775
|
-
throw new Error("Failed to load the local bundle signing private key.");
|
|
26198
|
+
const readEnvField = (env, field) => {
|
|
26199
|
+
for (const name of field) {
|
|
26200
|
+
const value = UNTRIMMED_ENV_NAMES.has(name) ? (process.env[name] ?? env[name]) || void 0 : getHotUpdaterEnvValue(env, name);
|
|
26201
|
+
if (value !== void 0) return {
|
|
26202
|
+
name,
|
|
26203
|
+
value
|
|
26204
|
+
};
|
|
25776
26205
|
}
|
|
25777
26206
|
};
|
|
25778
|
-
const
|
|
25779
|
-
|
|
26207
|
+
const readFileField = async (kind, { name, value }) => {
|
|
26208
|
+
const filePath = path$1.resolve(value);
|
|
26209
|
+
if (!await fs$2.stat(filePath).then(async (stats) => {
|
|
26210
|
+
await fs$2.access(filePath, fs$2.constants.R_OK);
|
|
26211
|
+
return stats.isFile();
|
|
26212
|
+
}).catch(() => false)) throw new CredentialUnavailableError(kind, `${name} does not point to a readable file.`);
|
|
26213
|
+
return filePath;
|
|
26214
|
+
};
|
|
26215
|
+
const parseServiceAccount = (json) => {
|
|
25780
26216
|
try {
|
|
25781
|
-
|
|
26217
|
+
return JSON.parse(json);
|
|
25782
26218
|
} catch {
|
|
25783
|
-
|
|
26219
|
+
return;
|
|
25784
26220
|
}
|
|
25785
|
-
return parsePrivateKey(pem);
|
|
25786
26221
|
};
|
|
26222
|
+
const fromEnv = {
|
|
26223
|
+
"asc-api-key": async ([key, keyId, issuerId], { files }) => ({
|
|
26224
|
+
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")),
|
|
26225
|
+
keyId: keyId.value,
|
|
26226
|
+
issuerId: issuerId.value
|
|
26227
|
+
}),
|
|
26228
|
+
"android-keystore": async ([keystore, storePassword, keyAlias, keyPassword], { files, name }) => {
|
|
26229
|
+
let storeFile;
|
|
26230
|
+
if (keystore.name === "HOT_UPDATER_ANDROID_KEYSTORE_PATH") storeFile = await readFileField("android-keystore", keystore);
|
|
26231
|
+
else {
|
|
26232
|
+
registerSecretValue(keystore.value);
|
|
26233
|
+
const contents = decodeBase64(keystore.value);
|
|
26234
|
+
if (!contents) throw new CredentialUnavailableError("android-keystore", `${keystore.name} is not a base64-encoded keystore.`);
|
|
26235
|
+
storeFile = await files.write(`${name}.keystore`, contents);
|
|
26236
|
+
}
|
|
26237
|
+
return {
|
|
26238
|
+
storeFile,
|
|
26239
|
+
storePassword: storePassword.value,
|
|
26240
|
+
keyAlias: keyAlias.value,
|
|
26241
|
+
keyPassword: keyPassword.value
|
|
26242
|
+
};
|
|
26243
|
+
},
|
|
26244
|
+
"play-service-account": async ([serviceAccount]) => {
|
|
26245
|
+
const json = serviceAccount.name === "HOT_UPDATER_PLAY_SERVICE_ACCOUNT_PATH" ? await fs$2.readFile(await readFileField("play-service-account", serviceAccount), "utf-8") : serviceAccount.value;
|
|
26246
|
+
const parsed = parseServiceAccount(json);
|
|
26247
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CredentialUnavailableError("play-service-account", `${serviceAccount.name} is not a service account JSON object.`);
|
|
26248
|
+
return { json };
|
|
26249
|
+
}
|
|
26250
|
+
};
|
|
26251
|
+
/** Fields of a service account that are no secret: its type and Google's endpoints. */
|
|
25787
26252
|
/**
|
|
25788
|
-
*
|
|
25789
|
-
*
|
|
25790
|
-
*
|
|
25791
|
-
*
|
|
26253
|
+
* Fields of a JSON credential that identify it rather than grant access:
|
|
26254
|
+
* its IDs, key alias, and a service account's email, project, type, and
|
|
26255
|
+
* Google's endpoints. They stay readable, as messages name them, and a
|
|
26256
|
+
* short one, such as the alias `release`, would mask every word it is in.
|
|
25792
26257
|
*/
|
|
25793
|
-
const
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
25802
|
-
|
|
26258
|
+
const isIdentifierField = (field) => [
|
|
26259
|
+
"keyId",
|
|
26260
|
+
"issuerId",
|
|
26261
|
+
"keyAlias",
|
|
26262
|
+
"type",
|
|
26263
|
+
"project_id",
|
|
26264
|
+
"client_email",
|
|
26265
|
+
"client_id",
|
|
26266
|
+
"private_key_id",
|
|
26267
|
+
"universe_domain"
|
|
26268
|
+
].includes(field) || field.endsWith("_uri") || field.endsWith("_url");
|
|
26269
|
+
const registerJsonSecrets = (value, fields) => {
|
|
25803
26270
|
registerSecretValue(value);
|
|
25804
|
-
const
|
|
25805
|
-
|
|
25806
|
-
|
|
25807
|
-
|
|
25808
|
-
|
|
25809
|
-
|
|
26271
|
+
for (const [field, entry] of Object.entries(fields)) if (typeof entry === "string" && !isIdentifierField(field)) registerSecretValue(entry);
|
|
26272
|
+
};
|
|
26273
|
+
/**
|
|
26274
|
+
* Masks a credential's JSON value in the CLI's output: the whole value, and
|
|
26275
|
+
* each field but those that identify it, such as `keyId` and `keyAlias`,
|
|
26276
|
+
* so a password or a key a log prints alone stays masked. A value that is
|
|
26277
|
+
* no JSON object is masked whole.
|
|
26278
|
+
*/
|
|
26279
|
+
const registerCredentialValue = (value) => {
|
|
26280
|
+
const parsed = parseObject(value);
|
|
26281
|
+
if (parsed) registerJsonSecrets(value, parsed);
|
|
26282
|
+
else registerSecretValue(value);
|
|
26283
|
+
};
|
|
26284
|
+
const parseObject = (value) => {
|
|
26285
|
+
const parsed = parseServiceAccount(value);
|
|
26286
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
26287
|
+
};
|
|
26288
|
+
/**
|
|
26289
|
+
* The fields of a JSON credential, each a non-empty string, or an error
|
|
26290
|
+
* naming the shape `kind` takes.
|
|
26291
|
+
*/
|
|
26292
|
+
const readJsonFields = (kind, item, value, fields) => {
|
|
26293
|
+
const parsed = parseObject(value);
|
|
26294
|
+
const shape = `{${fields.map((field) => `"${field}"`).join(", ")}}`;
|
|
26295
|
+
if (!parsed) throw new CredentialUnavailableError(kind, `${item} is not a JSON object.\n${capitalize(aKind(kind))} is one JSON value: ${shape}`);
|
|
26296
|
+
registerJsonSecrets(value, parsed);
|
|
26297
|
+
const missing = fields.filter((field) => typeof parsed[field] !== "string" || !parsed[field]);
|
|
26298
|
+
if (missing.length > 0) throw new CredentialUnavailableError(kind, `${item} has no ${missing.join(", ")}.\n${capitalize(aKind(kind))} is one JSON value: ${shape}`);
|
|
26299
|
+
return parsed;
|
|
26300
|
+
};
|
|
26301
|
+
const aKind = (kind) => `${/^[aeiou]/u.test(kind) ? "an" : "a"} ${kind}`;
|
|
26302
|
+
const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
26303
|
+
/** A base64 value's bytes, or undefined when it is not base64. */
|
|
26304
|
+
const decodeBase64 = (value) => {
|
|
26305
|
+
const base64 = value.replace(/\s/g, "");
|
|
26306
|
+
const contents = Buffer.from(base64, "base64");
|
|
26307
|
+
return contents.length > 0 && contents.toString("base64").replace(/=+$/, "") === base64.replace(/=+$/, "") ? contents : void 0;
|
|
26308
|
+
};
|
|
26309
|
+
/**
|
|
26310
|
+
* Each kind from the one string a secrets adapter holds for it: a JSON
|
|
26311
|
+
* object for the App Store Connect key (`p8` is the PEM) and the keystore
|
|
26312
|
+
* (`keystore` is base64), and the service account's own JSON. File values
|
|
26313
|
+
* are written with `files`; every value is masked.
|
|
26314
|
+
*/
|
|
26315
|
+
const fromSecretValue = {
|
|
26316
|
+
"asc-api-key": async (value, { files, item }) => {
|
|
26317
|
+
const { keyId, issuerId, p8 } = readJsonFields("asc-api-key", item, value, [
|
|
26318
|
+
"keyId",
|
|
26319
|
+
"issuerId",
|
|
26320
|
+
"p8"
|
|
26321
|
+
]);
|
|
26322
|
+
return {
|
|
26323
|
+
keyPath: await files.write(`AuthKey_${keyId}.p8`, p8.includes("\n") ? p8 : p8.replaceAll("\\n", "\n")),
|
|
26324
|
+
keyId,
|
|
26325
|
+
issuerId
|
|
26326
|
+
};
|
|
26327
|
+
},
|
|
26328
|
+
"android-keystore": async (value, { files, name, item }) => {
|
|
26329
|
+
const { keystore, storePassword, keyAlias, keyPassword } = readJsonFields("android-keystore", item, value, [
|
|
26330
|
+
"keystore",
|
|
26331
|
+
"storePassword",
|
|
26332
|
+
"keyAlias",
|
|
26333
|
+
"keyPassword"
|
|
26334
|
+
]);
|
|
26335
|
+
const contents = decodeBase64(keystore);
|
|
26336
|
+
if (!contents) throw new CredentialUnavailableError("android-keystore", `${item}'s keystore is not a base64-encoded keystore.`);
|
|
26337
|
+
return {
|
|
26338
|
+
storeFile: await files.write(`${name}.keystore`, contents),
|
|
26339
|
+
storePassword,
|
|
26340
|
+
keyAlias,
|
|
26341
|
+
keyPassword
|
|
26342
|
+
};
|
|
26343
|
+
},
|
|
26344
|
+
"play-service-account": async (value, { item }) => {
|
|
26345
|
+
const parsed = parseObject(value);
|
|
26346
|
+
if (!parsed) throw new CredentialUnavailableError("play-service-account", `${item} is not a service account JSON object.`);
|
|
26347
|
+
registerJsonSecrets(value, parsed);
|
|
26348
|
+
return { json: value };
|
|
26349
|
+
}
|
|
26350
|
+
};
|
|
26351
|
+
const registerCredentialSecrets = (kind, credential) => {
|
|
26352
|
+
if (kind === "android-keystore") {
|
|
26353
|
+
const keystore = credential;
|
|
26354
|
+
registerSecretValue(keystore.storePassword);
|
|
26355
|
+
registerSecretValue(keystore.keyPassword);
|
|
26356
|
+
}
|
|
26357
|
+
if (kind === "play-service-account") {
|
|
26358
|
+
const { json } = credential;
|
|
26359
|
+
registerSecretValue(json);
|
|
26360
|
+
const privateKey = parseServiceAccount(json)?.private_key;
|
|
26361
|
+
if (typeof privateKey === "string") registerSecretValue(privateKey);
|
|
25810
26362
|
}
|
|
25811
26363
|
};
|
|
25812
26364
|
/**
|
|
25813
|
-
*
|
|
25814
|
-
*
|
|
25815
|
-
* `privateKey: "secret:<name>"`, and is never written anywhere.
|
|
26365
|
+
* Reads names from `secrets`, the names asked for in one turn of the event
|
|
26366
|
+
* loop, such as a build's credentials, in one `get`. Nothing is cached.
|
|
25816
26367
|
*/
|
|
25817
|
-
const
|
|
25818
|
-
|
|
25819
|
-
const
|
|
25820
|
-
|
|
25821
|
-
|
|
25822
|
-
|
|
25823
|
-
|
|
25824
|
-
|
|
25825
|
-
|
|
25826
|
-
|
|
25827
|
-
|
|
26368
|
+
const createSecretLoader = (secrets, cwd) => {
|
|
26369
|
+
let batch;
|
|
26370
|
+
const flush = async (names) => {
|
|
26371
|
+
try {
|
|
26372
|
+
const read = await secrets().get([...names.keys()], { cwd: cwd() });
|
|
26373
|
+
for (const [name, waiters] of names) {
|
|
26374
|
+
const value = Object.hasOwn(read, name) ? read[name] : void 0;
|
|
26375
|
+
for (const waiter of waiters) waiter.resolve(typeof value === "string" && value ? value : void 0);
|
|
26376
|
+
}
|
|
26377
|
+
} catch (error) {
|
|
26378
|
+
for (const waiters of names.values()) for (const waiter of waiters) waiter.reject(error);
|
|
26379
|
+
}
|
|
26380
|
+
};
|
|
26381
|
+
return (name) => new Promise((resolve, reject) => {
|
|
26382
|
+
if (batch === void 0) {
|
|
26383
|
+
const names = /* @__PURE__ */ new Map();
|
|
26384
|
+
batch = names;
|
|
26385
|
+
setTimeout(() => {
|
|
26386
|
+
batch = void 0;
|
|
26387
|
+
flush(names);
|
|
26388
|
+
}, 0);
|
|
26389
|
+
}
|
|
26390
|
+
const waiters = batch.get(name) ?? [];
|
|
26391
|
+
waiters.push({
|
|
26392
|
+
resolve,
|
|
26393
|
+
reject
|
|
26394
|
+
});
|
|
26395
|
+
batch.set(name, waiters);
|
|
26396
|
+
});
|
|
26397
|
+
};
|
|
26398
|
+
/**
|
|
26399
|
+
* Resolves each item from its environment overrides when any is set, and
|
|
26400
|
+
* otherwise from the config's `secrets` adapter as one value named by the
|
|
26401
|
+
* item, so overrides never reach the store. When the adapter fails or holds
|
|
26402
|
+
* no such value, the error names the environment variables that can
|
|
26403
|
+
* provide the item instead. Nothing is cached, values are never written to
|
|
26404
|
+
* disk except as 0600 temporary files, are never set in `process.env`, and
|
|
26405
|
+
* every secret is masked in logs.
|
|
26406
|
+
*/
|
|
26407
|
+
const createCredentialResolver = ({ env = {}, secrets, cwd } = {}) => {
|
|
26408
|
+
const files = createCredentialFiles();
|
|
26409
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
26410
|
+
let disposal;
|
|
26411
|
+
const adapter = () => typeof secrets === "function" ? secrets() : secrets ?? getConfiguredSecrets();
|
|
26412
|
+
const load = createSecretLoader(adapter, () => cwd ?? getCwd());
|
|
26413
|
+
const resolveItem = async (kind, name) => {
|
|
26414
|
+
const fields = CREDENTIAL_ENV_NAMES[kind];
|
|
26415
|
+
const values = fields.map((field) => readEnvField(env, field));
|
|
26416
|
+
if (values.some((value) => value !== void 0)) {
|
|
26417
|
+
const missing = fields.filter((_, index) => !values[index]);
|
|
26418
|
+
if (missing.length > 0) throw new CredentialUnavailableError(kind, `The environment overrides ${kind} but ${describeFields(missing)} ${missing.length === 1 ? "is" : "are"} not set.`);
|
|
26419
|
+
const credential = await fromEnv[kind](values, {
|
|
26420
|
+
files,
|
|
26421
|
+
name
|
|
26422
|
+
});
|
|
26423
|
+
registerCredentialSecrets(kind, credential);
|
|
26424
|
+
return credential;
|
|
26425
|
+
}
|
|
26426
|
+
const store = adapter();
|
|
26427
|
+
const item = name === kind ? kind : `${name} (${kind})`;
|
|
26428
|
+
let value;
|
|
26429
|
+
try {
|
|
26430
|
+
value = await load(name);
|
|
26431
|
+
} catch (error) {
|
|
26432
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
26433
|
+
throw new CredentialUnavailableError(kind, `Couldn't read ${item} from the ${store.name} secrets adapter.\n${reason}`, { cause: error });
|
|
26434
|
+
}
|
|
26435
|
+
if (value === void 0) throw new CredentialUnavailableError(kind, kind === "android-keystore" ? describeMissingKeystore(store, item, name) : `No ${item} in the ${store.name} secrets adapter.\nStore it once with:\n${formatCommand(credentialStoreCommand(kind, name))}`);
|
|
26436
|
+
registerSecretValue(value);
|
|
26437
|
+
return fromSecretValue[kind](value, {
|
|
26438
|
+
files,
|
|
26439
|
+
name,
|
|
26440
|
+
item: `The ${store.name} secrets adapter's ${item}`
|
|
25828
26441
|
});
|
|
25829
|
-
privateKeys.set(cacheKey, pending);
|
|
25830
|
-
return pending;
|
|
25831
26442
|
};
|
|
25832
26443
|
return {
|
|
25833
|
-
|
|
25834
|
-
|
|
25835
|
-
const
|
|
25836
|
-
|
|
25837
|
-
|
|
25838
|
-
|
|
25839
|
-
|
|
26444
|
+
resolve: ({ kind, name = kind }) => {
|
|
26445
|
+
if (disposal) return Promise.reject(/* @__PURE__ */ new Error("The credential resolver has been disposed."));
|
|
26446
|
+
const key = `${kind}\u0000${name}`;
|
|
26447
|
+
let pending = resolved.get(key);
|
|
26448
|
+
if (!pending) {
|
|
26449
|
+
pending = resolveItem(kind, name);
|
|
26450
|
+
resolved.set(key, pending);
|
|
26451
|
+
pending.catch(() => resolved.delete(key));
|
|
26452
|
+
}
|
|
26453
|
+
return pending;
|
|
25840
26454
|
},
|
|
25841
|
-
|
|
25842
|
-
|
|
25843
|
-
|
|
25844
|
-
|
|
26455
|
+
dispose: () => {
|
|
26456
|
+
disposal ??= (async () => {
|
|
26457
|
+
await Promise.allSettled(resolved.values());
|
|
26458
|
+
resolved.clear();
|
|
26459
|
+
await files.dispose();
|
|
26460
|
+
})();
|
|
26461
|
+
return disposal;
|
|
25845
26462
|
}
|
|
25846
26463
|
};
|
|
25847
26464
|
};
|
|
25848
26465
|
/**
|
|
25849
|
-
*
|
|
25850
|
-
*
|
|
25851
|
-
* own store, whichever config loaded last.
|
|
26466
|
+
* Runs `run` with a credential resolver and deletes its temporary files
|
|
26467
|
+
* when `run` settles.
|
|
25852
26468
|
*/
|
|
25853
|
-
const
|
|
25854
|
-
|
|
25855
|
-
|
|
25856
|
-
|
|
25857
|
-
|
|
25858
|
-
|
|
25859
|
-
if (typeof signing !== "object" || signing === null) throw invalidSigningConfig();
|
|
25860
|
-
const hasLocalFields = [
|
|
25861
|
-
"enabled",
|
|
25862
|
-
"privateKeyPath",
|
|
25863
|
-
"privateKey"
|
|
25864
|
-
].some((key) => Reflect.has(signing, key));
|
|
25865
|
-
const hasPluginMembers = [
|
|
25866
|
-
"name",
|
|
25867
|
-
"getPublicKey",
|
|
25868
|
-
"sign"
|
|
25869
|
-
].some((key) => Reflect.has(signing, key));
|
|
25870
|
-
if (hasLocalFields && hasPluginMembers) throw new Error("Bundle signing config cannot combine local signing fields with signing adapter fields.");
|
|
25871
|
-
if (!hasPluginMembers) {
|
|
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.");
|
|
25873
|
-
const enabled = Reflect.get(signing, "enabled");
|
|
25874
|
-
if (enabled === false) return void 0;
|
|
25875
|
-
if (enabled !== true) throw invalidSigningConfig();
|
|
25876
|
-
const privateKeyPath = Reflect.get(signing, "privateKeyPath");
|
|
25877
|
-
const privateKey = Reflect.get(signing, "privateKey");
|
|
25878
|
-
if (privateKeyPath !== void 0 && privateKey !== void 0) throw new Error("Local bundle signing takes its private key from privateKeyPath or from privateKey, not both.");
|
|
25879
|
-
if (privateKey !== void 0) {
|
|
25880
|
-
if (typeof privateKey !== "string") throw new Error("signing.privateKey names a secret as secret:<name>, such as \"secret:bundle-signing-key\".");
|
|
25881
|
-
parseSecretReference(privateKey, "signing.privateKey");
|
|
25882
|
-
const normalized = {
|
|
25883
|
-
enabled: true,
|
|
25884
|
-
privateKey
|
|
25885
|
-
};
|
|
25886
|
-
if (secrets) signingSecrets.set(normalized, secrets);
|
|
25887
|
-
return normalized;
|
|
25888
|
-
}
|
|
25889
|
-
if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) throw new Error("Enabled local bundle signing requires privateKeyPath, or privateKey as \"secret:<name>\".");
|
|
25890
|
-
return {
|
|
25891
|
-
enabled: true,
|
|
25892
|
-
privateKeyPath
|
|
25893
|
-
};
|
|
26469
|
+
const withCredentialResolver = async (run, options) => {
|
|
26470
|
+
const resolver = createCredentialResolver(options);
|
|
26471
|
+
try {
|
|
26472
|
+
return await run(resolver);
|
|
26473
|
+
} finally {
|
|
26474
|
+
await resolver.dispose();
|
|
25894
26475
|
}
|
|
25895
|
-
if (typeof Reflect.get(signing, "name") !== "string" || typeof Reflect.get(signing, "getPublicKey") !== "function" || typeof Reflect.get(signing, "sign") !== "function") throw invalidSigningConfig();
|
|
25896
|
-
return signing;
|
|
25897
26476
|
};
|
|
25898
26477
|
//#endregion
|
|
25899
26478
|
//#region src/bundleSigning.ts
|
|
26479
|
+
/** The secret `signing: secret("bundle-signing-key")` names, which `secrets issue` creates. */
|
|
26480
|
+
const BUNDLE_SIGNING_KEY_SECRET = "bundle-signing-key";
|
|
26481
|
+
/** The command that creates and stores a bundle signing key. */
|
|
26482
|
+
const ISSUE_SIGNING_KEY_COMMAND = `npx hot-updater secrets issue ${BUNDLE_SIGNING_KEY_SECRET}`;
|
|
26483
|
+
/** The command that stores a bundle signing key you have. */
|
|
26484
|
+
const PUT_SIGNING_KEY_COMMAND = `npx hot-updater secrets put ${BUNDLE_SIGNING_KEY_SECRET} <key.pem>`;
|
|
26485
|
+
/** The command that stores the bundle signing key you have as `name`. */
|
|
26486
|
+
const bundleSigningPutCommand = (name) => `npx hot-updater secrets put ${name}${name === "bundle-signing-key" ? "" : ` --kind ${BUNDLE_SIGNING_KEY_SECRET}`} <key.pem>`;
|
|
26487
|
+
/** What deploy and doctor say when the config sets no `signing`. */
|
|
26488
|
+
const UNSIGNED_BUNDLES_WARNING = "Bundles are unsigned.";
|
|
26489
|
+
/** The next step after `UNSIGNED_BUNDLES_WARNING`. */
|
|
26490
|
+
const UNSIGNED_BUNDLES_NEXT_STEP = "To sign them, run:";
|
|
26491
|
+
/** What deploy and doctor say when `signing` is no shape it takes. */
|
|
26492
|
+
const INVALID_SIGNING_MESSAGE = `signing takes secret("${BUNDLE_SIGNING_KEY_SECRET}").`;
|
|
26493
|
+
/** The next step after `INVALID_SIGNING_MESSAGE`. */
|
|
26494
|
+
const INVALID_SIGNING_NEXT_STEP = "Store your key:";
|
|
26495
|
+
const invalid = (message) => ({
|
|
26496
|
+
status: "invalid",
|
|
26497
|
+
message
|
|
26498
|
+
});
|
|
26499
|
+
const LOCAL_FIELDS = [
|
|
26500
|
+
"enabled",
|
|
26501
|
+
"privateKeyPath",
|
|
26502
|
+
"privateKey"
|
|
26503
|
+
];
|
|
26504
|
+
const readBundleSigningState = (signing) => {
|
|
26505
|
+
if (signing === void 0) return { status: "unsigned" };
|
|
26506
|
+
if (isSecretRef(signing)) return {
|
|
26507
|
+
status: "on",
|
|
26508
|
+
key: { secret: signing.name }
|
|
26509
|
+
};
|
|
26510
|
+
if (typeof signing !== "object" || signing === null || !Object.keys(signing).every((key) => LOCAL_FIELDS.includes(key))) return invalid(INVALID_SIGNING_MESSAGE);
|
|
26511
|
+
const { enabled, privateKey, privateKeyPath } = signing;
|
|
26512
|
+
if (enabled === false) return { status: "off" };
|
|
26513
|
+
if (enabled !== true) return invalid(INVALID_SIGNING_MESSAGE);
|
|
26514
|
+
if (privateKey !== void 0 && privateKeyPath !== void 0) return invalid("signing takes privateKey or privateKeyPath, not both.");
|
|
26515
|
+
if (privateKey !== void 0) return isSecretRef(privateKey) ? {
|
|
26516
|
+
status: "on",
|
|
26517
|
+
key: { secret: privateKey.name }
|
|
26518
|
+
} : invalid("signing.privateKey takes secret(\"<name>\").");
|
|
26519
|
+
if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) return invalid("signing takes privateKeyPath, or privateKey as secret(\"<name>\").");
|
|
26520
|
+
return {
|
|
26521
|
+
status: "on",
|
|
26522
|
+
key: { privateKeyPath }
|
|
26523
|
+
};
|
|
26524
|
+
};
|
|
26525
|
+
/** The `secret()` the config's `signing` reads its private key from, if any. */
|
|
26526
|
+
const bundleSigningSecretRef = (signing) => {
|
|
26527
|
+
if (isSecretRef(signing)) return signing;
|
|
26528
|
+
const privateKey = typeof signing === "object" && signing !== null ? Reflect.get(signing, "privateKey") : void 0;
|
|
26529
|
+
return isSecretRef(privateKey) ? privateKey : void 0;
|
|
26530
|
+
};
|
|
26531
|
+
/**
|
|
26532
|
+
* A bundle signing failure whose message the CLI prints as it is: it names
|
|
26533
|
+
* the secret and the next step, and quotes no key material.
|
|
26534
|
+
*/
|
|
26535
|
+
var BundleSigningKeyError = class extends Error {
|
|
26536
|
+
constructor(message, options) {
|
|
26537
|
+
super(message, options);
|
|
26538
|
+
this.name = "HotUpdaterBundleSigningKeyError";
|
|
26539
|
+
}
|
|
26540
|
+
};
|
|
25900
26541
|
const FILE_HASH_PATTERN = /^[a-f\d]{64}$/iu;
|
|
25901
26542
|
const resolvePath = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.resolve(cwd, filePath);
|
|
25902
26543
|
const parseRsaPublicKey = (publicKeyPEM) => {
|
|
@@ -25910,7 +26551,8 @@ const parseRsaPublicKey = (publicKeyPEM) => {
|
|
|
25910
26551
|
throw new Error("Bundle signing public key must be a valid RSA SPKI PEM key with a modulus of at least 2048 bits.");
|
|
25911
26552
|
}
|
|
25912
26553
|
};
|
|
25913
|
-
|
|
26554
|
+
/** The SPKI PEM of `key`, a public key or the private key it derives from. */
|
|
26555
|
+
const exportPublicKey = (key) => (key.type === "private" ? crypto$1.createPublicKey(key) : key).export({
|
|
25914
26556
|
type: "spki",
|
|
25915
26557
|
format: "pem"
|
|
25916
26558
|
}).toString();
|
|
@@ -25923,93 +26565,112 @@ const readBundleSigningPublicKeyFile = async (publicKeyPath, options = {}) => {
|
|
|
25923
26565
|
throw new Error("Failed to read the bundle signing public key file.");
|
|
25924
26566
|
}
|
|
25925
26567
|
};
|
|
26568
|
+
const parsePrivateKey = (pem) => {
|
|
26569
|
+
const privateKey = crypto$1.createPrivateKey(pem);
|
|
26570
|
+
if (privateKey.asymmetricKeyType !== "rsa" || (privateKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("not rsa");
|
|
26571
|
+
return privateKey;
|
|
26572
|
+
};
|
|
26573
|
+
const loadPrivateKeyFile = async (file) => {
|
|
26574
|
+
try {
|
|
26575
|
+
return parsePrivateKey(await fs$2.readFile(file, "utf8"));
|
|
26576
|
+
} catch {
|
|
26577
|
+
throw new BundleSigningKeyError("Failed to load the bundle signing private key from privateKeyPath.");
|
|
26578
|
+
}
|
|
26579
|
+
};
|
|
26580
|
+
/**
|
|
26581
|
+
* The private key the `secrets` adapter holds as `name`: masked in the CLI's
|
|
26582
|
+
* output before it is parsed, and kept only as a key object. A PEM written
|
|
26583
|
+
* on one line, with `\n` for its line breaks, as a `.env.hotupdater` value
|
|
26584
|
+
* or a CI variable may hold it, reads as the PEM.
|
|
26585
|
+
*/
|
|
26586
|
+
const loadPrivateKeySecret = async (secrets, name, cwd) => {
|
|
26587
|
+
let value;
|
|
26588
|
+
try {
|
|
26589
|
+
const read = await secrets.get([name], { cwd });
|
|
26590
|
+
value = Object.hasOwn(read, name) ? read[name] : void 0;
|
|
26591
|
+
} catch (error) {
|
|
26592
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
26593
|
+
throw new BundleSigningKeyError(`Couldn't read ${name} from the ${secrets.name} secrets adapter.\n${reason}`, { cause: error });
|
|
26594
|
+
}
|
|
26595
|
+
if (typeof value !== "string" || !value.trim()) throw new BundleSigningKeyError(describeMissingSecret(secrets, name, { command: bundleSigningPutCommand(name) }));
|
|
26596
|
+
registerSecretValue(value);
|
|
26597
|
+
const pem = value.includes("\n") ? value : value.replaceAll("\\n", "\n");
|
|
26598
|
+
if (pem !== value) registerSecretValue(pem);
|
|
26599
|
+
try {
|
|
26600
|
+
return parsePrivateKey(pem);
|
|
26601
|
+
} catch {
|
|
26602
|
+
throw new BundleSigningKeyError([`${name} in the ${secrets.name} secrets adapter is no signing key.`, "Store an RSA private key in PEM, of 2048 bits or more."].join("\n"));
|
|
26603
|
+
}
|
|
26604
|
+
};
|
|
25926
26605
|
/**
|
|
25927
|
-
*
|
|
25928
|
-
*
|
|
25929
|
-
*
|
|
25930
|
-
* key material.
|
|
26606
|
+
* The private keys each `signing` value has read, by file or by secrets
|
|
26607
|
+
* adapter, so the commands of one run that sign and derive the public key
|
|
26608
|
+
* read the key once.
|
|
25931
26609
|
*/
|
|
25932
|
-
const
|
|
25933
|
-
const signingFailure = (summary, error) => new Error(error instanceof Error && (error.name === REMOTE_SIGNING_ERROR_NAME || error instanceof LocalSigningKeyError) ? `${summary} ${error.message}` : summary);
|
|
26610
|
+
const privateKeys = /* @__PURE__ */ new WeakMap();
|
|
25934
26611
|
/**
|
|
25935
|
-
* The
|
|
25936
|
-
*
|
|
25937
|
-
*
|
|
26612
|
+
* The private key `signing` signs with, or null when it signs nothing.
|
|
26613
|
+
* Throws `INVALID_SIGNING_MESSAGE`, with the next step, for a value
|
|
26614
|
+
* `signing` doesn't take.
|
|
25938
26615
|
*/
|
|
25939
|
-
const
|
|
25940
|
-
const
|
|
25941
|
-
|
|
25942
|
-
if (
|
|
25943
|
-
|
|
25944
|
-
|
|
25945
|
-
const
|
|
25946
|
-
|
|
26616
|
+
const loadSigningKey = (signing, { cwd = getCwd(), secrets }) => {
|
|
26617
|
+
const state = readBundleSigningState(signing);
|
|
26618
|
+
if (state.status === "invalid") throw new BundleSigningKeyError(`${state.message}\n${INVALID_SIGNING_NEXT_STEP} ${PUT_SIGNING_KEY_COMMAND}`);
|
|
26619
|
+
if (state.status !== "on") return null;
|
|
26620
|
+
const store = secrets ?? getConfiguredSecrets();
|
|
26621
|
+
const cacheKey = "secret" in state.key ? store : resolvePath(cwd, state.key.privateKeyPath);
|
|
26622
|
+
const cache = privateKeys.get(signing) ?? /* @__PURE__ */ new Map();
|
|
26623
|
+
privateKeys.set(signing, cache);
|
|
26624
|
+
const cached = cache.get(cacheKey);
|
|
25947
26625
|
if (cached) return cached;
|
|
25948
|
-
const
|
|
25949
|
-
|
|
25950
|
-
|
|
26626
|
+
const pending = ("secret" in state.key ? loadPrivateKeySecret(store, state.key.secret, cwd) : loadPrivateKeyFile(cacheKey)).catch((error) => {
|
|
26627
|
+
cache.delete(cacheKey);
|
|
26628
|
+
throw error;
|
|
26629
|
+
});
|
|
26630
|
+
cache.set(cacheKey, pending);
|
|
26631
|
+
return pending;
|
|
25951
26632
|
};
|
|
25952
|
-
const
|
|
26633
|
+
const signingFailure = (summary, error) => new Error(error instanceof BundleSigningKeyError ? error.message : `${summary} ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
26634
|
+
/**
|
|
26635
|
+
* Prepares the run's bundle signing: the public key the app must embed, and
|
|
26636
|
+
* a signer of SHA-256 file hashes that signs each hash once. Null when
|
|
26637
|
+
* `signing` is omitted or `{ enabled: false }`.
|
|
26638
|
+
*/
|
|
26639
|
+
const prepareBundleSigning = async (signing, options = {}) => {
|
|
26640
|
+
const loading = loadSigningKey(signing, options);
|
|
26641
|
+
if (!loading) return null;
|
|
26642
|
+
let privateKey;
|
|
25953
26643
|
try {
|
|
25954
|
-
|
|
25955
|
-
if (!result || typeof result.publicKey !== "string") throw new Error("invalid result");
|
|
25956
|
-
return parseRsaPublicKey(result.publicKey);
|
|
26644
|
+
privateKey = await loading;
|
|
25957
26645
|
} catch (error) {
|
|
25958
|
-
throw signingFailure("Failed to
|
|
26646
|
+
throw signingFailure("Failed to read the bundle signing key.", error);
|
|
25959
26647
|
}
|
|
25960
|
-
};
|
|
25961
|
-
const createMemoizedSigner = ({ publicKey, sign }) => {
|
|
25962
26648
|
const signatures = /* @__PURE__ */ new Map();
|
|
25963
|
-
return (fileHash) => {
|
|
25964
|
-
if (!FILE_HASH_PATTERN.test(fileHash)) return Promise.reject(/* @__PURE__ */ new Error("Bundle signing requires a 64-character hexadecimal file hash."));
|
|
25965
|
-
const normalizedFileHash = fileHash.toLowerCase();
|
|
25966
|
-
const cached = signatures.get(normalizedFileHash);
|
|
25967
|
-
if (cached) return cached;
|
|
25968
|
-
const pending = (async () => {
|
|
25969
|
-
const message = Buffer.from(normalizedFileHash, "hex");
|
|
25970
|
-
let signature;
|
|
25971
|
-
try {
|
|
25972
|
-
signature = await sign(new Uint8Array(message));
|
|
25973
|
-
} catch (error) {
|
|
25974
|
-
throw signingFailure("Bundle signing provider failed to sign the file hash.", error);
|
|
25975
|
-
}
|
|
25976
|
-
if (!(signature instanceof Uint8Array) || signature.byteLength === 0) throw new Error("Bundle signing provider returned an invalid signature.");
|
|
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.");
|
|
25978
|
-
return Buffer.from(signature).toString("base64");
|
|
25979
|
-
})().catch((error) => {
|
|
25980
|
-
signatures.delete(normalizedFileHash);
|
|
25981
|
-
throw error;
|
|
25982
|
-
});
|
|
25983
|
-
signatures.set(normalizedFileHash, pending);
|
|
25984
|
-
return pending;
|
|
25985
|
-
};
|
|
25986
|
-
};
|
|
25987
|
-
const preparePluginSigning = async (signing, cwd) => {
|
|
25988
|
-
const providerPublicKey = await getProviderPublicKey(signing, cwd);
|
|
25989
26649
|
return {
|
|
25990
|
-
|
|
25991
|
-
|
|
25992
|
-
|
|
25993
|
-
|
|
25994
|
-
|
|
25995
|
-
|
|
25996
|
-
|
|
25997
|
-
|
|
25998
|
-
|
|
25999
|
-
|
|
26000
|
-
})
|
|
26650
|
+
publicKey: exportPublicKey(privateKey),
|
|
26651
|
+
signFileHash: async (fileHash) => {
|
|
26652
|
+
if (!FILE_HASH_PATTERN.test(fileHash)) throw new Error("Bundle signing requires a 64-character hexadecimal file hash.");
|
|
26653
|
+
const normalized = fileHash.toLowerCase();
|
|
26654
|
+
const cached = signatures.get(normalized);
|
|
26655
|
+
if (cached) return cached;
|
|
26656
|
+
const signature = crypto$1.sign("RSA-SHA256", Buffer.from(normalized, "hex"), privateKey).toString("base64");
|
|
26657
|
+
signatures.set(normalized, signature);
|
|
26658
|
+
return signature;
|
|
26659
|
+
}
|
|
26001
26660
|
};
|
|
26002
26661
|
};
|
|
26003
|
-
|
|
26004
|
-
|
|
26005
|
-
|
|
26006
|
-
|
|
26007
|
-
};
|
|
26008
|
-
/** Resolves the public identity of the configured bundle signer. */
|
|
26662
|
+
/**
|
|
26663
|
+
* The public key of the configured bundle signing key, in SPKI PEM; null
|
|
26664
|
+
* when `signing` signs nothing.
|
|
26665
|
+
*/
|
|
26009
26666
|
const getBundleSigningPublicKey = async (signing, options = {}) => {
|
|
26010
|
-
const
|
|
26011
|
-
if (!
|
|
26012
|
-
|
|
26667
|
+
const loading = loadSigningKey(signing, options);
|
|
26668
|
+
if (!loading) return null;
|
|
26669
|
+
try {
|
|
26670
|
+
return exportPublicKey(await loading);
|
|
26671
|
+
} catch (error) {
|
|
26672
|
+
throw signingFailure("Failed to read the bundle signing key.", error);
|
|
26673
|
+
}
|
|
26013
26674
|
};
|
|
26014
26675
|
//#endregion
|
|
26015
26676
|
//#region src/ConfigBuilder.ts
|
|
@@ -38605,421 +39266,6 @@ const createZip = async ({ outfile, targetDir, excludeExts = [] }) => {
|
|
|
38605
39266
|
return outfile;
|
|
38606
39267
|
};
|
|
38607
39268
|
//#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
|
|
39023
39269
|
//#region src/isObject.ts
|
|
39024
39270
|
const isObject$2 = (value) => {
|
|
39025
39271
|
return !!value && value.constructor === Object;
|
|
@@ -45483,7 +45729,10 @@ const MANAGED_IMPORT_PACKAGES = /* @__PURE__ */ new Set([
|
|
|
45483
45729
|
const MANAGED_HELPER_NAMES = /* @__PURE__ */ new Set([
|
|
45484
45730
|
"awsOptions",
|
|
45485
45731
|
"commonOptions",
|
|
45486
|
-
"credential"
|
|
45732
|
+
"credential",
|
|
45733
|
+
"databaseSecrets",
|
|
45734
|
+
"deploySecrets",
|
|
45735
|
+
"storageSecrets"
|
|
45487
45736
|
]);
|
|
45488
45737
|
const KNOWN_BUILD_CALLEES = /* @__PURE__ */ new Set([
|
|
45489
45738
|
"bare",
|
|
@@ -45556,6 +45805,21 @@ const getObjectPropertyName = (property) => {
|
|
|
45556
45805
|
return null;
|
|
45557
45806
|
};
|
|
45558
45807
|
const isDataProperty = (property) => property.type === "Property" && property.kind === "init" && !property.method && !property.shorthand;
|
|
45808
|
+
/** `process.env.HOT_UPDATER_*`, with or without `!`. */
|
|
45809
|
+
const isHotUpdaterEnvRead = (expression) => {
|
|
45810
|
+
const read = expression.type === "TSNonNullExpression" ? expression.expression : expression;
|
|
45811
|
+
return read.type === "MemberExpression" && !read.computed && read.property.type === "Identifier" && read.property.name.startsWith("HOT_UPDATER_") && read.object.type === "MemberExpression" && !read.object.computed && read.object.object.type === "Identifier" && read.object.object.name === "process" && read.object.property.type === "Identifier" && read.object.property.name === "env";
|
|
45812
|
+
};
|
|
45813
|
+
/** `secret("<name>")`, the credential init writes. */
|
|
45814
|
+
const isSecretCall = (expression) => expression.type === "CallExpression" && expression.callee.type === "Identifier" && expression.callee.name === "secret" && expression.arguments.length === 1 && expression.arguments[0]?.type === "Literal" && typeof expression.arguments[0].value === "string";
|
|
45815
|
+
/**
|
|
45816
|
+
* A value init writes into a managed adapter's options: a string, a
|
|
45817
|
+
* `process.env.HOT_UPDATER_*` read, as configs of 1.1 read every value, a
|
|
45818
|
+
* `secret("<name>")`, or an object of only those, such as `credentials`.
|
|
45819
|
+
* Init replaces it with what its run resolves; any other value is the
|
|
45820
|
+
* project's own and kept.
|
|
45821
|
+
*/
|
|
45822
|
+
const isInitWrittenValue = (expression) => expression.type === "Literal" && typeof expression.value === "string" || isHotUpdaterEnvRead(expression) || isSecretCall(expression) || expression.type === "ObjectExpression" && expression.properties.length > 0 && expression.properties.every((property) => isDataProperty(property) && isInitWrittenValue(property.value));
|
|
45559
45823
|
const dedentBlock = (text) => {
|
|
45560
45824
|
const lines = text.replace(/\s+$/, "").split("\n");
|
|
45561
45825
|
const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)?.[0].length ?? 0);
|
|
@@ -45628,9 +45892,17 @@ const mergeObjectLiteralText = (existingObject, newObject, replaceIncompatiblePr
|
|
|
45628
45892
|
existingPropertyNames.add(propertyName);
|
|
45629
45893
|
const nextProperty = newObject.objectExpression.properties.find((candidate) => getObjectPropertyName(candidate) === propertyName);
|
|
45630
45894
|
if (!nextProperty || !isDataProperty(property) || !isDataProperty(nextProperty)) continue;
|
|
45895
|
+
if (isInitWrittenValue(property.value)) {
|
|
45896
|
+
edits.push({
|
|
45897
|
+
start: property.value.start - existingObject.objectExpression.start,
|
|
45898
|
+
end: property.value.end - existingObject.objectExpression.start,
|
|
45899
|
+
text: getNodeText(newObject.source, nextProperty.value)
|
|
45900
|
+
});
|
|
45901
|
+
continue;
|
|
45902
|
+
}
|
|
45631
45903
|
const existingCallee = getCallCallee(property.value);
|
|
45632
45904
|
const nextCallee = getCallCallee(nextProperty.value);
|
|
45633
|
-
const hasIncompatibleValue = existingCallee !== null && nextCallee !== null && existingCallee !== nextCallee || property.value.type
|
|
45905
|
+
const hasIncompatibleValue = existingCallee !== null && nextCallee !== null && existingCallee !== nextCallee || property.value.type !== nextProperty.value.type;
|
|
45634
45906
|
if (replaceIncompatibleProperties.includes(propertyName) && hasIncompatibleValue) {
|
|
45635
45907
|
edits.push({
|
|
45636
45908
|
start: property.value.start - existingObject.objectExpression.start,
|
|
@@ -46112,6 +46384,22 @@ const readTextFile = (filePath) => fs$1.readFile(filePath, "utf-8").catch((error
|
|
|
46112
46384
|
* existing config with the scaffold's build, storage, database, plugins, and
|
|
46113
46385
|
* helpers merged in, keeping the project's own settings.
|
|
46114
46386
|
*/
|
|
46387
|
+
/**
|
|
46388
|
+
* Whether `hot-updater.config.ts` in `cwd` sets a team `secrets` adapter,
|
|
46389
|
+
* read from its text: a `secrets` property other than `localSecrets(...)`,
|
|
46390
|
+
* or a config whose shape init can't read. False without the file. Init
|
|
46391
|
+
* reads the store through the loaded config, so one that sets a store but
|
|
46392
|
+
* doesn't load must be fixed first.
|
|
46393
|
+
*/
|
|
46394
|
+
const configSetsTeamSecrets = async (cwd) => {
|
|
46395
|
+
const text = await readTextFile(path.join(cwd, HOT_UPDATER_CONFIG_PATH));
|
|
46396
|
+
if (text === null) return false;
|
|
46397
|
+
const source = parseConfigSource(text);
|
|
46398
|
+
const config = source && findDefineConfigObject(source);
|
|
46399
|
+
if (!config) return true;
|
|
46400
|
+
const secrets = findAnyProperty(config.objectExpression, "secrets");
|
|
46401
|
+
return secrets !== null && getCallCallee(secrets.value) !== "localSecrets";
|
|
46402
|
+
};
|
|
46115
46403
|
const writeHotUpdaterConfig = async (scaffold, filePath = HOT_UPDATER_CONFIG_PATH) => {
|
|
46116
46404
|
const existingText = await readTextFile(filePath);
|
|
46117
46405
|
if (existingText === null) {
|
|
@@ -46167,6 +46455,75 @@ const writeHotUpdaterFiles = async (scaffold, { cwd = process.cwd(), settings })
|
|
|
46167
46455
|
].join("\n"));
|
|
46168
46456
|
return { config };
|
|
46169
46457
|
};
|
|
46458
|
+
/**
|
|
46459
|
+
* The config text with `signing: secret("<name>")` added, and `secret` in
|
|
46460
|
+
* its `hot-updater` import, once. A config that sets `signing` in any form,
|
|
46461
|
+
* such as 1.1's `{ enabled: true, privateKeyPath }`, is kept.
|
|
46462
|
+
*/
|
|
46463
|
+
const addSigningSecretToConfigText = (text, name) => {
|
|
46464
|
+
const source = parseConfigSource(text);
|
|
46465
|
+
const config = source && findDefineConfigObject(source);
|
|
46466
|
+
if (!source || !config) return { status: "unsupported" };
|
|
46467
|
+
const { objectExpression } = config;
|
|
46468
|
+
if (objectExpression.properties.some((property) => property.type === "SpreadElement" || getObjectPropertyName(property) === "signing")) return { status: "kept" };
|
|
46469
|
+
const edits = [];
|
|
46470
|
+
const property = `signing: secret(${JSON.stringify(name)})`;
|
|
46471
|
+
const last = objectExpression.properties.at(-1);
|
|
46472
|
+
if (!last) edits.push({
|
|
46473
|
+
start: objectExpression.start,
|
|
46474
|
+
end: objectExpression.end,
|
|
46475
|
+
text: `{\n ${property},\n}`
|
|
46476
|
+
});
|
|
46477
|
+
else {
|
|
46478
|
+
const lineStart = text.lastIndexOf("\n", last.start) + 1;
|
|
46479
|
+
const indent = /^[ \t]*/u.exec(text.slice(lineStart, last.start))[0];
|
|
46480
|
+
const between = text.slice(last.end, objectExpression.end - 1);
|
|
46481
|
+
const comma = /^\s*,/u.exec(between);
|
|
46482
|
+
let at = last.end + (comma?.[0].length ?? 0);
|
|
46483
|
+
const lineEnd = text.indexOf("\n", at);
|
|
46484
|
+
if (lineEnd !== -1 && text.slice(at, lineEnd).trim().startsWith("//")) at = lineEnd;
|
|
46485
|
+
edits.push({
|
|
46486
|
+
start: at,
|
|
46487
|
+
end: at,
|
|
46488
|
+
text: `\n${indent}${property},`
|
|
46489
|
+
});
|
|
46490
|
+
if (!comma) edits.push({
|
|
46491
|
+
start: last.end,
|
|
46492
|
+
end: last.end,
|
|
46493
|
+
text: ","
|
|
46494
|
+
});
|
|
46495
|
+
}
|
|
46496
|
+
const hotUpdaterImports = importDeclarationsOf(source).filter((declaration) => declaration.source.value === "hot-updater" && declaration.importKind !== "type");
|
|
46497
|
+
if (!hotUpdaterImports.some((declaration) => declaration.specifiers.some((specifier) => specifier.type === "ImportSpecifier" && specifier.local.name === "secret"))) {
|
|
46498
|
+
const named = hotUpdaterImports.find((declaration) => declaration.specifiers.some((specifier) => specifier.type === "ImportSpecifier"));
|
|
46499
|
+
const lastSpecifier = named?.specifiers.at(-1);
|
|
46500
|
+
edits.push(named && lastSpecifier ? {
|
|
46501
|
+
start: lastSpecifier.end,
|
|
46502
|
+
end: lastSpecifier.end,
|
|
46503
|
+
text: ", secret"
|
|
46504
|
+
} : {
|
|
46505
|
+
start: 0,
|
|
46506
|
+
end: 0,
|
|
46507
|
+
text: "import { secret } from \"hot-updater\";\n"
|
|
46508
|
+
});
|
|
46509
|
+
}
|
|
46510
|
+
return {
|
|
46511
|
+
status: "added",
|
|
46512
|
+
text: applyTextEdits(text, edits)
|
|
46513
|
+
};
|
|
46514
|
+
};
|
|
46515
|
+
/**
|
|
46516
|
+
* Adds `signing: secret("<name>")` to `hot-updater.config.ts` in `cwd`, as
|
|
46517
|
+
* `addSigningSecretToConfigText` does. Without the file, nothing is written.
|
|
46518
|
+
*/
|
|
46519
|
+
const addSigningSecretToConfig = async (cwd, name) => {
|
|
46520
|
+
const filePath = path.join(cwd, HOT_UPDATER_CONFIG_PATH);
|
|
46521
|
+
const text = await readTextFile(filePath);
|
|
46522
|
+
if (text === null) return "missing";
|
|
46523
|
+
const result = addSigningSecretToConfigText(text, name);
|
|
46524
|
+
if (result.status === "added") await fs$1.writeFile(filePath, result.text, "utf-8");
|
|
46525
|
+
return result.status;
|
|
46526
|
+
};
|
|
46170
46527
|
//#endregion
|
|
46171
46528
|
//#region src/infrastructureGeneration.ts
|
|
46172
46529
|
const assertInfrastructureGenerationPayload = ({ payload, provider, resource }) => {
|
|
@@ -47000,8 +47357,8 @@ const loadConfigFile = async () => {
|
|
|
47000
47357
|
};
|
|
47001
47358
|
/**
|
|
47002
47359
|
* The config `options` select, resolved: a config function runs in a scope
|
|
47003
|
-
* of its own, so the `
|
|
47004
|
-
* and the file's top-level `
|
|
47360
|
+
* of its own, so the `secret()` it creates reads this config's `secrets`,
|
|
47361
|
+
* and the file's top-level `secret()` reads the first config's.
|
|
47005
47362
|
*/
|
|
47006
47363
|
const configFor = async ({ source, scope: fileScope }, options) => {
|
|
47007
47364
|
if (typeof source !== "function") return resolveConfig(source, [fileScope]);
|
|
@@ -47029,21 +47386,49 @@ const loadPlatformConfigs = async (platforms, { channel }) => {
|
|
|
47029
47386
|
return configs;
|
|
47030
47387
|
};
|
|
47031
47388
|
/**
|
|
47389
|
+
* The names the config's build credentials reference, which the CLI reads
|
|
47390
|
+
* for their kind: `signing`'s, and each scheme's
|
|
47391
|
+
* `signing.ascApiKey` and `signing.keystore`.
|
|
47392
|
+
*/
|
|
47393
|
+
const buildSecretNames = (config) => {
|
|
47394
|
+
return [
|
|
47395
|
+
bundleSigningSecretRef(config.signing),
|
|
47396
|
+
...Object.values(config.nativeBuild?.ios ?? {}).map((scheme) => scheme?.signing?.ascApiKey),
|
|
47397
|
+
...Object.values(config.nativeBuild?.android ?? {}).map((scheme) => scheme?.signing?.keystore)
|
|
47398
|
+
].filter(isSecretRef).map(({ name }) => name);
|
|
47399
|
+
};
|
|
47400
|
+
/**
|
|
47032
47401
|
* The config with its defaults. Its `secrets` adapter fills `scopes` not yet
|
|
47033
|
-
* filled, which the `
|
|
47402
|
+
* filled, which the `secret()` its evaluation created read, and becomes the
|
|
47034
47403
|
* default of credential resolvers.
|
|
47035
47404
|
*/
|
|
47036
47405
|
const resolveConfig = (config, scopes) => {
|
|
47037
47406
|
const mergedConfig = mergeConfigSources(config, getDefaultConfig());
|
|
47038
|
-
const secrets =
|
|
47039
|
-
const
|
|
47040
|
-
for (const scope of scopes)
|
|
47407
|
+
const secrets = resolveSecretsAdapter(mergedConfig.secrets);
|
|
47408
|
+
const buildNames = buildSecretNames(mergedConfig);
|
|
47409
|
+
for (const scope of scopes) {
|
|
47410
|
+
scope.secrets ??= secrets;
|
|
47411
|
+
scope.buildNames ??= /* @__PURE__ */ new Set();
|
|
47412
|
+
for (const name of buildNames) scope.buildNames.add(name);
|
|
47413
|
+
}
|
|
47041
47414
|
setConfiguredSecrets(secrets);
|
|
47042
|
-
|
|
47415
|
+
const resolved = {
|
|
47043
47416
|
...mergedConfig,
|
|
47044
|
-
signing,
|
|
47045
47417
|
secrets
|
|
47046
47418
|
};
|
|
47419
|
+
Object.defineProperty(resolved, DEPLOY_SECRET_NAMES, { value: [...new Set(scopes.flatMap((scope) => [...scope.names ?? []]))].filter((name) => !buildNames.includes(name)) });
|
|
47420
|
+
return resolved;
|
|
47421
|
+
};
|
|
47422
|
+
const DEPLOY_SECRET_NAMES = Symbol.for("@hot-updater/cli-tools/deploy-secret-names");
|
|
47423
|
+
/**
|
|
47424
|
+
* The names the config's `secret()`s reference for its adapters, such as
|
|
47425
|
+
* `HOT_UPDATER_CLOUDFLARE_API_TOKEN`, in the order the config created
|
|
47426
|
+
* them: every name but those of build credentials, which `doctor` lists
|
|
47427
|
+
* by field.
|
|
47428
|
+
*/
|
|
47429
|
+
const getDeploySecretNames = (config) => {
|
|
47430
|
+
const names = Reflect.get(config, DEPLOY_SECRET_NAMES);
|
|
47431
|
+
return Array.isArray(names) ? names : [];
|
|
47047
47432
|
};
|
|
47048
47433
|
//#endregion
|
|
47049
47434
|
//#region src/serverDefinition.ts
|
|
@@ -47335,6 +47720,76 @@ const resolveHotUpdaterServerVersion = (currentPackageName, options) => {
|
|
|
47335
47720
|
return process.env["HOT_UPDATER_SERVER_PACKAGE_VERSION"]?.trim() || resolvePackageVersion(currentPackageName, options);
|
|
47336
47721
|
};
|
|
47337
47722
|
//#endregion
|
|
47723
|
+
//#region src/teamSecrets.ts
|
|
47724
|
+
/**
|
|
47725
|
+
* The client API key the managed servers' `apiKeys()` gives an app, which
|
|
47726
|
+
* init saves (`API_KEY_ENV` in `@hot-updater/plugin-api-keys`). The config
|
|
47727
|
+
* never reads it; a team store shares it with the deploy credentials, so a
|
|
47728
|
+
* teammate's init registers the same key again rather than a new one.
|
|
47729
|
+
*/
|
|
47730
|
+
const MANAGED_CLIENT_CREDENTIAL_ENV = "HOT_UPDATER_API_KEY";
|
|
47731
|
+
/**
|
|
47732
|
+
* The config's own `secrets` adapter, the team store: undefined when the
|
|
47733
|
+
* config sets none, which is `localSecrets()`. Loading the config runs its
|
|
47734
|
+
* `process.loadEnvFile(".env.hotupdater")`, which init must not see, so
|
|
47735
|
+
* `process.env` is restored after it.
|
|
47736
|
+
*/
|
|
47737
|
+
const loadTeamSecrets = async () => {
|
|
47738
|
+
const before = { ...process.env };
|
|
47739
|
+
try {
|
|
47740
|
+
return getConfiguredAdapter((await loadConfig(null)).secrets);
|
|
47741
|
+
} finally {
|
|
47742
|
+
for (const key of Object.keys(process.env)) if (!Object.hasOwn(before, key)) delete process.env[key];
|
|
47743
|
+
Object.assign(process.env, before);
|
|
47744
|
+
}
|
|
47745
|
+
};
|
|
47746
|
+
/**
|
|
47747
|
+
* `names` neither the environment nor `env`, init's env files, sets, read
|
|
47748
|
+
* from `secrets` in one call: the values a teammate's init, or a run after
|
|
47749
|
+
* the file's lines were deleted, replays with.
|
|
47750
|
+
*/
|
|
47751
|
+
const readMissingFromTeamSecrets = async (secrets, names, { env, cwd }) => {
|
|
47752
|
+
const missing = names.filter((name) => getHotUpdaterEnvValue(env, name) === void 0);
|
|
47753
|
+
if (missing.length === 0) return {};
|
|
47754
|
+
const held = await readSecrets(secrets, missing, cwd);
|
|
47755
|
+
const values = Object.create(null);
|
|
47756
|
+
for (const name of missing) {
|
|
47757
|
+
const value = Object.hasOwn(held, name) ? held[name] : void 0;
|
|
47758
|
+
if (typeof value === "string" && value !== "") {
|
|
47759
|
+
registerSecretValue(value);
|
|
47760
|
+
values[name] = value;
|
|
47761
|
+
}
|
|
47762
|
+
}
|
|
47763
|
+
return values;
|
|
47764
|
+
};
|
|
47765
|
+
/**
|
|
47766
|
+
* Stores each of `names` that `envFile`, `.env.hotupdater` by default,
|
|
47767
|
+
* holds through `secrets`, then reads them back in one call. Only the file
|
|
47768
|
+
* is read, never the environment. The file is left as it is. Returns the
|
|
47769
|
+
* names stored, in the order of `names`; throws when the adapter reads
|
|
47770
|
+
* back another value.
|
|
47771
|
+
*/
|
|
47772
|
+
const storeEnvFileSecrets = async (secrets, names, cwd, envFile) => {
|
|
47773
|
+
const file = envFile === void 0 ? await readLoadedHotUpdaterEnv(cwd) : await readLoadedEnvFile(envFile, cwd);
|
|
47774
|
+
const values = /* @__PURE__ */ new Map();
|
|
47775
|
+
for (const name of names) {
|
|
47776
|
+
const variable = secretVariableName(name);
|
|
47777
|
+
const value = Object.hasOwn(file, variable) ? file[variable]?.trim() : void 0;
|
|
47778
|
+
if (value) values.set(name, value);
|
|
47779
|
+
}
|
|
47780
|
+
if (values.size === 0) return [];
|
|
47781
|
+
const stored = [...values.keys()];
|
|
47782
|
+
for (const [name, value] of values) {
|
|
47783
|
+
registerSecretValue(value);
|
|
47784
|
+
await secrets.set(name, value, { cwd });
|
|
47785
|
+
}
|
|
47786
|
+
const read = await secrets.get(stored, { cwd });
|
|
47787
|
+
const unread = stored.filter((name) => (Object.hasOwn(read, name) ? read[name] : void 0) !== values.get(name));
|
|
47788
|
+
if (unread.length > 0) throw new Error(`The ${secrets.name} secrets adapter did not read back ${list(unread)} (${unread.map((name) => describeSecret(secrets, name)).join("; ")}).`);
|
|
47789
|
+
return stored;
|
|
47790
|
+
};
|
|
47791
|
+
const list = (names) => names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
47792
|
+
//#endregion
|
|
47338
47793
|
//#region src/storageFiles.ts
|
|
47339
47794
|
const getStorageFileByteSize = async (filePath) => {
|
|
47340
47795
|
const { size } = await fs$2.stat(filePath, { bigint: true });
|
|
@@ -47385,4 +47840,4 @@ function transformTemplate(templateString, values) {
|
|
|
47385
47840
|
return result;
|
|
47386
47841
|
}
|
|
47387
47842
|
//#endregion
|
|
47388
|
-
export { BuildFailedError, BuildLogger, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError,
|
|
47843
|
+
export { BUNDLE_SIGNING_KEY_SECRET, BuildFailedError, BuildLogger, BundleSigningKeyError, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, INTERFACE_FINGERPRINT_ENGINE, INVALID_SIGNING_MESSAGE, INVALID_SIGNING_NEXT_STEP, ISSUE_SIGNING_KEY_COMMAND, InitEnvFileError, InitError, LOCAL_SECRETS_NAME, LegacyInfrastructureError, MANAGED_CLIENT_CREDENTIAL_ENV, MESSAGE_WIDTH, MissingInitInputsError, PUT_SIGNING_KEY_COMMAND, ServerDefinitionError, UNSIGNED_BUNDLES_NEXT_STEP, UNSIGNED_BUNDLES_WARNING, addSigningSecretToConfig, addSigningSecretToConfigText, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, bundleSigningPutCommand, bundleSigningSecretRef, clientAuthOf, typedColors as colors, configSetsTeamSecrets, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createStep, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeMissingSecret, describeSecret, describeStoredSecret, displayWidth, encryptJson, ensureInstallPackages, failActiveStep, fitMessage, formatCommand, formatDuration, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getDeploySecretNames, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isSecretRef, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, loadTeamSecrets, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readBundleSigningState, readHotUpdaterInitEnv, readLoadedEnvFile, readLoadedHotUpdaterEnv, readLocalKey, readMissingFromTeamSecrets, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, readSecrets, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, resolveSecretsAdapter, secret, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, startStep, storeEnvFileSecrets, stripAnsi, transformEnv, transformTemplate, truncateLine, withCredentialResolver, withDuration, withSecretsScope, withStep, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
|