@hot-updater/cli-tools 1.2.0-rc.6 → 1.2.0-rc.8
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 +292 -97
- package/dist/index.mjs +743 -541
- 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,241 @@ 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
|
-
|
|
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/colors.ts
|
|
22890
|
+
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
22891
|
+
let p = process || {};
|
|
22892
|
+
let argv = p.argv || [];
|
|
22893
|
+
let env = p.env || {};
|
|
22894
|
+
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);
|
|
22895
|
+
let formatter = (open, close, replace = open) => (input) => {
|
|
22896
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
22897
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
22898
|
+
};
|
|
22899
|
+
let replaceClose = (string, close, replace, index) => {
|
|
22900
|
+
let result = "", cursor = 0;
|
|
22901
|
+
do {
|
|
22902
|
+
result += string.substring(cursor, index) + replace;
|
|
22903
|
+
cursor = index + close.length;
|
|
22904
|
+
index = string.indexOf(close, cursor);
|
|
22905
|
+
} while (~index);
|
|
22906
|
+
return result + string.substring(cursor);
|
|
22907
|
+
};
|
|
22908
|
+
let createColors = (enabled = isColorSupported) => {
|
|
22909
|
+
let f = enabled ? formatter : () => String;
|
|
22910
|
+
return {
|
|
22911
|
+
isColorSupported: enabled,
|
|
22912
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
22913
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
22914
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
22915
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
22916
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
22917
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
22918
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
22919
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
22920
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
22921
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
22922
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
22923
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
22924
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
22925
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
22926
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
22927
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
22928
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
22929
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
22930
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
22931
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
22932
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
22933
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
22934
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
22935
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
22936
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
22937
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
22938
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
22939
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
22940
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
22941
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
22942
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
22943
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
22944
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
22945
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
22946
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
22947
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
22948
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
22949
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
22950
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
22951
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
22952
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
22953
|
+
};
|
|
22954
|
+
};
|
|
22955
|
+
module.exports = createColors();
|
|
22956
|
+
module.exports.createColors = createColors;
|
|
22957
|
+
})))(), 1);
|
|
22958
|
+
const typedColors = import_picocolors.default;
|
|
22959
|
+
//#endregion
|
|
22960
|
+
//#region src/step.ts
|
|
22961
|
+
/** The widest a finished step's line is, after clack's 3-column gutter. */
|
|
22962
|
+
const STEP_WIDTH = 73;
|
|
22963
|
+
/** The room the spinner's symbol and its `[12m 34s]` timer take. */
|
|
22964
|
+
const LIVE_LINE_CHROME = 13;
|
|
22965
|
+
/** `4.2s`, `38s`, `6m41s`, `1h02m`. */
|
|
22966
|
+
const formatDuration = (ms) => {
|
|
22967
|
+
if (ms < 9950) return `${(Math.max(0, ms) / 1e3).toFixed(1)}s`;
|
|
22968
|
+
const seconds = Math.round(ms / 1e3);
|
|
22969
|
+
if (seconds < 60) return `${seconds}s`;
|
|
22970
|
+
const minutes = Math.floor(seconds / 60);
|
|
22971
|
+
if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
22972
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
22973
|
+
};
|
|
22974
|
+
const visibleLength = (text) => stripVTControlCharacters(text).length;
|
|
22975
|
+
/**
|
|
22976
|
+
* `message` with `duration` at the right edge of a step's line, the message
|
|
22977
|
+
* cut with `…` when the line would be wider.
|
|
22978
|
+
*/
|
|
22979
|
+
const withDuration = (message, ms, width = STEP_WIDTH) => {
|
|
22980
|
+
const duration = formatDuration(ms);
|
|
22981
|
+
const text = truncateLine(message, width - duration.length - 2);
|
|
22982
|
+
const gap = Math.max(2, width - visibleLength(text) - duration.length);
|
|
22983
|
+
return `${text}${" ".repeat(gap)}${typedColors.dim(duration)}`;
|
|
22984
|
+
};
|
|
22985
|
+
/** `text` cut to `width` visible columns, with `…` for the rest. */
|
|
22986
|
+
const truncateLine = (text, width) => {
|
|
22987
|
+
const plain = stripVTControlCharacters(text);
|
|
22988
|
+
if (plain.length <= width) return text;
|
|
22989
|
+
return `${plain.slice(0, Math.max(0, width - 1))}…`;
|
|
22990
|
+
};
|
|
22991
|
+
const isLive = (output) => isTTY(output) && !isCI$1();
|
|
22992
|
+
/**
|
|
22993
|
+
* A step that shows nothing until `start()`. A step created while another
|
|
22994
|
+
* runs is part of it: its title and detail become the running step's detail.
|
|
22995
|
+
*/
|
|
22996
|
+
const createStep = (title, { output = process.stdout } = {}) => {
|
|
22997
|
+
let startedAt;
|
|
22998
|
+
let ended = false;
|
|
22999
|
+
let detail;
|
|
23000
|
+
let spinner;
|
|
23001
|
+
let ticker;
|
|
23002
|
+
let held = [];
|
|
23003
|
+
let outer;
|
|
23004
|
+
const liveMessage = () => {
|
|
23005
|
+
const text = maskSecretValues(detail ? `${title} · ${detail}` : title);
|
|
23006
|
+
const columns = output.columns || 80;
|
|
23007
|
+
return truncateLine(text, Math.max(20, columns - LIVE_LINE_CHROME));
|
|
23008
|
+
};
|
|
23009
|
+
const finish = (write) => {
|
|
23010
|
+
if (startedAt === void 0 || ended) return;
|
|
23011
|
+
ended = true;
|
|
23012
|
+
if (ticker) clearInterval(ticker);
|
|
23013
|
+
if (outer) {
|
|
23014
|
+
outer.update(void 0);
|
|
23015
|
+
return;
|
|
23016
|
+
}
|
|
23017
|
+
setStepCapture(void 0);
|
|
23018
|
+
if (spinner) spinner.clear();
|
|
23019
|
+
write();
|
|
23020
|
+
const logs = held;
|
|
23021
|
+
held = [];
|
|
23022
|
+
for (const log of logs) log();
|
|
23023
|
+
};
|
|
23024
|
+
const logOptions = () => ({
|
|
23025
|
+
output,
|
|
23026
|
+
spacing: spinner ? 0 : 1
|
|
23027
|
+
});
|
|
23028
|
+
const step = {
|
|
23029
|
+
start() {
|
|
23030
|
+
if (startedAt !== void 0) return step;
|
|
23031
|
+
startedAt = performance.now();
|
|
23032
|
+
outer = stepCapture();
|
|
23033
|
+
if (outer) {
|
|
23034
|
+
outer.update(title);
|
|
23035
|
+
return step;
|
|
23036
|
+
}
|
|
23037
|
+
setStepCapture({
|
|
23038
|
+
update: (value) => step.update(value),
|
|
23039
|
+
hold: (log) => {
|
|
23040
|
+
held.push(log);
|
|
23041
|
+
},
|
|
23042
|
+
interrupt: (message) => step.fail(message)
|
|
23043
|
+
});
|
|
23044
|
+
if (isLive(output)) {
|
|
23045
|
+
spinner = spinner$1({
|
|
23046
|
+
indicator: "timer",
|
|
23047
|
+
output
|
|
23048
|
+
});
|
|
23049
|
+
spinner.start(liveMessage());
|
|
23050
|
+
ticker = setInterval(() => spinner?.message(liveMessage()), 1e3);
|
|
23051
|
+
ticker.unref?.();
|
|
23052
|
+
}
|
|
23053
|
+
return step;
|
|
23054
|
+
},
|
|
23055
|
+
update(value) {
|
|
23056
|
+
if (startedAt === void 0 || ended) return;
|
|
23057
|
+
if (outer) {
|
|
23058
|
+
outer.update(value ? `${title} · ${value}` : title);
|
|
23059
|
+
return;
|
|
23060
|
+
}
|
|
23061
|
+
detail = value;
|
|
23062
|
+
spinner?.message(liveMessage());
|
|
23063
|
+
},
|
|
23064
|
+
succeed(message = title, { level = "step" } = {}) {
|
|
23065
|
+
const elapsed = step.elapsedMs;
|
|
23066
|
+
finish(() => {
|
|
23067
|
+
const line = maskSecretValues(withDuration(message, elapsed));
|
|
23068
|
+
if (level === "info") log$2.info(line, logOptions());
|
|
23069
|
+
else if (level === "warn") log$2.warn(line, logOptions());
|
|
23070
|
+
else log$2.step(line, logOptions());
|
|
23071
|
+
});
|
|
23072
|
+
},
|
|
23073
|
+
fail(message = `${title} failed`) {
|
|
23074
|
+
const elapsed = step.elapsedMs;
|
|
23075
|
+
finish(() => {
|
|
23076
|
+
log$2.error(maskSecretValues(withDuration(message, elapsed)), logOptions());
|
|
23077
|
+
});
|
|
23078
|
+
},
|
|
23079
|
+
get elapsedMs() {
|
|
23080
|
+
return startedAt === void 0 ? 0 : performance.now() - startedAt;
|
|
23081
|
+
},
|
|
23082
|
+
get active() {
|
|
23083
|
+
return startedAt !== void 0 && !ended;
|
|
22952
23084
|
}
|
|
23085
|
+
};
|
|
23086
|
+
return step;
|
|
23087
|
+
};
|
|
23088
|
+
/**
|
|
23089
|
+
* Ends the running step as failed, if one runs, before a command prints
|
|
23090
|
+
* its error.
|
|
23091
|
+
*/
|
|
23092
|
+
const failActiveStep = () => {
|
|
23093
|
+
stepCapture()?.interrupt();
|
|
23094
|
+
};
|
|
23095
|
+
/** A step, started. */
|
|
23096
|
+
const startStep = (title, options) => createStep(title, options).start();
|
|
23097
|
+
/**
|
|
23098
|
+
* Runs `run` as a step titled `title`, which ends with `done(result)`, or as
|
|
23099
|
+
* failed when `run` throws.
|
|
23100
|
+
*/
|
|
23101
|
+
const withStep = async (title, run, done = () => title, options) => {
|
|
23102
|
+
const step = startStep(title, options);
|
|
23103
|
+
try {
|
|
23104
|
+
const result = await run(step);
|
|
23105
|
+
step.succeed(done(result));
|
|
23106
|
+
return result;
|
|
23107
|
+
} catch (error) {
|
|
23108
|
+
step.fail();
|
|
23109
|
+
throw error;
|
|
22953
23110
|
}
|
|
22954
|
-
return nextCompletedStages;
|
|
22955
23111
|
};
|
|
23112
|
+
//#endregion
|
|
23113
|
+
//#region src/BuildLogger.ts
|
|
22956
23114
|
/** The most lines a failure summary prints, and the tail it falls back to. */
|
|
22957
|
-
const MAX_SUMMARY_LINES =
|
|
22958
|
-
const TAIL_LINES =
|
|
23115
|
+
const MAX_SUMMARY_LINES = 20;
|
|
23116
|
+
const TAIL_LINES = 15;
|
|
22959
23117
|
const matchesSummaryPattern = (line, pattern) => {
|
|
22960
23118
|
if (pattern instanceof RegExp) {
|
|
22961
23119
|
pattern.lastIndex = 0;
|
|
@@ -22973,52 +23131,58 @@ var BuildFailedError = class extends Error {
|
|
|
22973
23131
|
logPath;
|
|
22974
23132
|
/** The lines of the tool's output the summary printed. */
|
|
22975
23133
|
summary;
|
|
22976
|
-
|
|
23134
|
+
/**
|
|
23135
|
+
* What to do about it, such as accepting an agreement, which the summary
|
|
23136
|
+
* doesn't say: the one line a command prints after it, or null.
|
|
23137
|
+
*/
|
|
23138
|
+
hint;
|
|
23139
|
+
constructor(message, { logPath, summary, hint = null, cause }) {
|
|
22977
23140
|
super(message, { cause });
|
|
22978
23141
|
this.logPath = logPath;
|
|
22979
23142
|
this.summary = summary;
|
|
23143
|
+
this.hint = hint;
|
|
22980
23144
|
}
|
|
22981
23145
|
};
|
|
23146
|
+
/**
|
|
23147
|
+
* Runs a native build tool's output through a step: the step's live line
|
|
23148
|
+
* shows the phase its reader finds, the log file keeps the raw output, and a
|
|
23149
|
+
* failure prints the summary of it on stderr.
|
|
23150
|
+
*/
|
|
22982
23151
|
var BuildLogger = class {
|
|
22983
23152
|
config;
|
|
22984
|
-
state = createInitialState();
|
|
22985
23153
|
logWriter;
|
|
23154
|
+
step;
|
|
22986
23155
|
summaryLines = [];
|
|
22987
23156
|
tailLines = [];
|
|
22988
23157
|
openBlock;
|
|
22989
|
-
promptProgress;
|
|
22990
|
-
stageCount;
|
|
22991
23158
|
constructor(config) {
|
|
22992
23159
|
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
23160
|
}
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23161
|
+
/**
|
|
23162
|
+
* Opens the log, writes `header` to it, such as the tool's command line,
|
|
23163
|
+
* which the terminal doesn't show, and starts the step titled `title`.
|
|
23164
|
+
*/
|
|
23165
|
+
async start({ title, header = [] }) {
|
|
23005
23166
|
this.summaryLines = [];
|
|
23006
23167
|
this.tailLines = [];
|
|
23007
23168
|
this.openBlock = void 0;
|
|
23008
|
-
this.logWriter = await createLogWriter({
|
|
23009
|
-
|
|
23169
|
+
this.logWriter = await createLogWriter({
|
|
23170
|
+
prefix: this.config.logPrefix,
|
|
23171
|
+
logFilePath: this.config.logFilePath
|
|
23172
|
+
});
|
|
23173
|
+
if (header.length) this.logWriter.writeLines(header);
|
|
23174
|
+
this.step = startStep(title);
|
|
23010
23175
|
}
|
|
23011
23176
|
/**
|
|
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.
|
|
23177
|
+
* Writes the tool's raw output to the log file, reads its failure summary
|
|
23178
|
+
* from it, and shows its phases on the step's line.
|
|
23015
23179
|
*/
|
|
23016
|
-
async processStream(input
|
|
23180
|
+
async processStream(input) {
|
|
23017
23181
|
if (!this.logWriter) throw new Error("BuildLogger has not been started. Call start() first.");
|
|
23018
23182
|
const progressInput = new PassThrough();
|
|
23019
23183
|
const logInput = new PassThrough();
|
|
23020
23184
|
const summaryInput = new PassThrough();
|
|
23021
|
-
|
|
23185
|
+
input.pipe(progressInput);
|
|
23022
23186
|
input.pipe(logInput);
|
|
23023
23187
|
input.pipe(summaryInput);
|
|
23024
23188
|
await Promise.all([
|
|
@@ -23031,6 +23195,10 @@ var BuildLogger = class {
|
|
|
23031
23195
|
get logPath() {
|
|
23032
23196
|
return this.logWriter?.logFilePath ? path.relative(getCwd(), this.logWriter.logFilePath) : null;
|
|
23033
23197
|
}
|
|
23198
|
+
/** The build log's absolute path, or null without one. */
|
|
23199
|
+
get logFilePath() {
|
|
23200
|
+
return this.logWriter?.logFilePath ?? null;
|
|
23201
|
+
}
|
|
23034
23202
|
/**
|
|
23035
23203
|
* The lines of the raw output a failure prints: the ones the config's
|
|
23036
23204
|
* `failureSummary` keeps, deduplicated, or the last lines without any.
|
|
@@ -23079,36 +23247,28 @@ var BuildLogger = class {
|
|
|
23079
23247
|
crlfDelay: Infinity,
|
|
23080
23248
|
terminal: false
|
|
23081
23249
|
});
|
|
23082
|
-
|
|
23083
|
-
|
|
23084
|
-
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
|
|
23091
|
-
});
|
|
23092
|
-
this.updateProgress({
|
|
23093
|
-
nextCompletedStages,
|
|
23094
|
-
latestMessage: normalizedLine
|
|
23095
|
-
});
|
|
23250
|
+
const readPhase = this.config.createPhaseReader();
|
|
23251
|
+
let phase;
|
|
23252
|
+
for await (const line of lineReader) {
|
|
23253
|
+
const next = readPhase(stripAnsi(line).replace(/\r/g, "").trim());
|
|
23254
|
+
if (next !== void 0 && next !== phase) {
|
|
23255
|
+
phase = next;
|
|
23256
|
+
this.step?.update(phase);
|
|
23257
|
+
}
|
|
23258
|
+
}
|
|
23096
23259
|
}
|
|
23260
|
+
/**
|
|
23261
|
+
* Ends the step with `message` and its duration, or, for a failure, as
|
|
23262
|
+
* failed, then prints the failure summary and the log's path.
|
|
23263
|
+
*/
|
|
23097
23264
|
stop(message, success = true) {
|
|
23098
|
-
|
|
23099
|
-
if (
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
this.showLogFileLocation();
|
|
23104
|
-
} else {
|
|
23105
|
-
this.promptProgress.error(finalMessage);
|
|
23265
|
+
const step = this.step;
|
|
23266
|
+
if (!step?.active) return;
|
|
23267
|
+
if (success) step.succeed(message);
|
|
23268
|
+
else {
|
|
23269
|
+
step.fail(message);
|
|
23106
23270
|
this.showFailureSummary();
|
|
23107
23271
|
}
|
|
23108
|
-
this.state = {
|
|
23109
|
-
...this.state,
|
|
23110
|
-
stopped: true
|
|
23111
|
-
};
|
|
23112
23272
|
}
|
|
23113
23273
|
writeError(error) {
|
|
23114
23274
|
this.logWriter?.writeError(error);
|
|
@@ -23118,32 +23278,6 @@ var BuildLogger = class {
|
|
|
23118
23278
|
await this.logWriter.close();
|
|
23119
23279
|
this.logWriter = void 0;
|
|
23120
23280
|
}
|
|
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
23281
|
/**
|
|
23148
23282
|
* Prints the failure summary and the log's path to stderr, so a `--json`
|
|
23149
23283
|
* command's stdout stays its JSON.
|
|
@@ -23154,12 +23288,6 @@ var BuildLogger = class {
|
|
|
23154
23288
|
const message = [...summary, ...logPath ? [...summary.length ? [""] : [], `Build log: ${logPath}`] : []].join("\n");
|
|
23155
23289
|
if (message) p.log.error(message, { output: process.stderr });
|
|
23156
23290
|
}
|
|
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
23291
|
};
|
|
23164
23292
|
//#endregion
|
|
23165
23293
|
//#region src/appSetup.ts
|
|
@@ -25299,6 +25427,17 @@ const readLoadedHotUpdaterEnv = async (cwd) => {
|
|
|
25299
25427
|
throw error;
|
|
25300
25428
|
}
|
|
25301
25429
|
};
|
|
25430
|
+
/**
|
|
25431
|
+
* `file`, an env file a command was given, relative to `cwd`, as
|
|
25432
|
+
* `parseLoadedEnv` reads it; a file that can't be read is refused by name.
|
|
25433
|
+
*/
|
|
25434
|
+
const readLoadedEnvFile = async (file, cwd) => {
|
|
25435
|
+
try {
|
|
25436
|
+
return parseLoadedEnv(await fs$1.readFile(path.resolve(cwd, file), "utf-8"));
|
|
25437
|
+
} catch (error) {
|
|
25438
|
+
throw new Error(`Can't read ${file}.\n${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
25439
|
+
}
|
|
25440
|
+
};
|
|
25302
25441
|
//#endregion
|
|
25303
25442
|
//#region src/localKeyStore.ts
|
|
25304
25443
|
/**
|
|
@@ -25512,21 +25651,21 @@ const RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
|
25512
25651
|
"PYTHONSTARTUP",
|
|
25513
25652
|
"PERL5OPT",
|
|
25514
25653
|
...Object.values(CREDENTIAL_ENV_NAMES).flat(2),
|
|
25515
|
-
"HOT_UPDATER_SIGNING_TOKEN",
|
|
25516
25654
|
"HOT_UPDATER_HOME"
|
|
25517
25655
|
]);
|
|
25518
25656
|
const RESERVED_PREFIXES = [
|
|
25519
25657
|
"LD_",
|
|
25520
25658
|
"DYLD_",
|
|
25521
25659
|
"GIT_",
|
|
25522
|
-
"NPM_CONFIG_"
|
|
25660
|
+
"NPM_CONFIG_",
|
|
25661
|
+
"INFISICAL_"
|
|
25523
25662
|
];
|
|
25524
25663
|
/**
|
|
25525
25664
|
* Whether `name` is a variable no secret may set: one a loader or
|
|
25526
25665
|
* interpreter reads (`NODE_*` but `NODE_ENV`, `PATH`, `LD_*`, `DYLD_*`,
|
|
25527
25666
|
* `BASH_ENV`, `ENV`, `GIT_*`, `npm_config_*`, the JVM's, Gradle's, Ruby's,
|
|
25528
25667
|
* Python's, and Perl's options), or one Hot Updater reads its credentials
|
|
25529
|
-
* from. `
|
|
25668
|
+
* from. `secret()` refuses these names, and so does every environment the
|
|
25530
25669
|
* CLI gives a child process from resolved secrets.
|
|
25531
25670
|
*/
|
|
25532
25671
|
const isReservedVariableName = (name) => {
|
|
@@ -25546,20 +25685,25 @@ const assertUnreservedEnv = (env, what) => {
|
|
|
25546
25685
|
//#endregion
|
|
25547
25686
|
//#region src/secrets.ts
|
|
25548
25687
|
const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
25549
|
-
/** A secret's name, as `secret
|
|
25688
|
+
/** A secret's name, as `secret("<name>")` takes one. */
|
|
25550
25689
|
const SECRET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
25551
|
-
|
|
25552
|
-
|
|
25690
|
+
/**
|
|
25691
|
+
* The brand `secret()` sets, which plugin-core's `SecretRef` types. It is a
|
|
25692
|
+
* `Symbol.for` key: the config file loads its own copy of this module.
|
|
25693
|
+
*/
|
|
25694
|
+
const SECRET_REF = Symbol.for("@hot-updater/secret-ref");
|
|
25695
|
+
/** Whether `name` is a secret's name, as `secret("<name>")` takes one. */
|
|
25553
25696
|
const isSecretName = (name) => SECRET_NAME_PATTERN.test(name);
|
|
25697
|
+
/** Whether `value` is a `secret("<name>")` reference. */
|
|
25698
|
+
const isSecretRef = (value) => typeof value === "function" && Reflect.get(value, SECRET_REF) === true && isSecretName(value.name);
|
|
25554
25699
|
/**
|
|
25555
|
-
* The secret name a config's `secret
|
|
25556
|
-
* `release-keystore` for `"
|
|
25557
|
-
* field
|
|
25700
|
+
* The secret name a config's `secret("<name>")` names, such as
|
|
25701
|
+
* `release-keystore` for `secret("release-keystore")`. `field` is the
|
|
25702
|
+
* config field it is in, which the error names.
|
|
25558
25703
|
*/
|
|
25559
25704
|
const parseSecretReference = (reference, field) => {
|
|
25560
|
-
|
|
25561
|
-
|
|
25562
|
-
return name;
|
|
25705
|
+
if (!isSecretRef(reference)) throw new Error(`${field} takes secret("<name>").`);
|
|
25706
|
+
return reference.name;
|
|
25563
25707
|
};
|
|
25564
25708
|
/**
|
|
25565
25709
|
* The environment variable the default store reads a secret from: a name
|
|
@@ -25624,8 +25768,8 @@ const createLocalSecrets = (store) => {
|
|
|
25624
25768
|
const getLocalSecretsStore = (secrets) => isLocalKeyStoreSecrets(secrets) ? secrets[LOCAL_KEY_STORE].store : void 0;
|
|
25625
25769
|
/**
|
|
25626
25770
|
* The config's own `secrets` adapter: undefined for `localSecrets()`,
|
|
25627
|
-
* which is the adapter when the config sets none. `
|
|
25628
|
-
*
|
|
25771
|
+
* which is the adapter when the config sets none. A called `secret()` reads
|
|
25772
|
+
* from it.
|
|
25629
25773
|
*/
|
|
25630
25774
|
const getConfiguredAdapter = (secrets) => isLocalKeyStoreSecrets(secrets) ? void 0 : secrets;
|
|
25631
25775
|
/**
|
|
@@ -25643,7 +25787,7 @@ const readSecretWithSource = async (secrets, name, cwd) => {
|
|
|
25643
25787
|
const held = (value) => typeof value === "string" && value.length > 0;
|
|
25644
25788
|
const store = getLocalSecretsStore(secrets);
|
|
25645
25789
|
if (!store) {
|
|
25646
|
-
const value = (await secrets
|
|
25790
|
+
const value = (await readSecrets(secrets, [name], cwd))[name];
|
|
25647
25791
|
return held(value) ? {
|
|
25648
25792
|
value,
|
|
25649
25793
|
source: describeSecret(secrets, name)
|
|
@@ -25654,7 +25798,9 @@ const readSecretWithSource = async (secrets, name, cwd) => {
|
|
|
25654
25798
|
value: variable,
|
|
25655
25799
|
source: `${secretVariableName(name)} (environment)`
|
|
25656
25800
|
};
|
|
25657
|
-
const value = await store.read(name, cwd)
|
|
25801
|
+
const value = await store.read(name, cwd).catch((error) => {
|
|
25802
|
+
throw secretReadError(secrets, [name], error);
|
|
25803
|
+
});
|
|
25658
25804
|
return held(value) ? {
|
|
25659
25805
|
value,
|
|
25660
25806
|
source: store.file(name, cwd)
|
|
@@ -25669,7 +25815,7 @@ const CONFIGURED_SECRETS = Symbol.for("@hot-updater/cli-tools/secrets");
|
|
|
25669
25815
|
const LOADING_SCOPE = Symbol.for("@hot-updater/cli-tools/secrets-scope");
|
|
25670
25816
|
/**
|
|
25671
25817
|
* Evaluates a config, the file or a config function, with `scope` as the
|
|
25672
|
-
* scope of every `
|
|
25818
|
+
* scope of every `secret()` it creates, so each reads the `secrets` of the
|
|
25673
25819
|
* config that created it, whatever config loads after. It is kept on
|
|
25674
25820
|
* `globalThis`: the config file loads its own copy of Hot Updater's modules.
|
|
25675
25821
|
*/
|
|
@@ -25685,7 +25831,7 @@ const withSecretsScope = async (scope, evaluate) => {
|
|
|
25685
25831
|
};
|
|
25686
25832
|
/**
|
|
25687
25833
|
* Makes `secrets` the adapter a credential resolver reads from by default,
|
|
25688
|
-
* and a `
|
|
25834
|
+
* and a `secret()` created outside any config load, as `loadConfig` does
|
|
25689
25835
|
* with the config's. It is kept on `globalThis`: the config file loads Hot
|
|
25690
25836
|
* Updater's modules again, as its own copies.
|
|
25691
25837
|
*/
|
|
@@ -25697,108 +25843,192 @@ const setConfiguredSecrets = (secrets) => {
|
|
|
25697
25843
|
* until a config loads.
|
|
25698
25844
|
*/
|
|
25699
25845
|
const getConfiguredSecrets = () => globalThis[CONFIGURED_SECRETS] ?? localSecrets();
|
|
25846
|
+
/**
|
|
25847
|
+
* `names` from `secrets` in one `get`. A read that fails names the secrets
|
|
25848
|
+
* and the adapter, with the adapter's own error on the next line, such as
|
|
25849
|
+
* `EACCES: permission denied, open '<file>'`.
|
|
25850
|
+
*/
|
|
25851
|
+
const readSecrets = async (secrets, names, cwd) => {
|
|
25852
|
+
try {
|
|
25853
|
+
return await secrets.get(names, { cwd });
|
|
25854
|
+
} catch (error) {
|
|
25855
|
+
throw secretReadError(secrets, names, error);
|
|
25856
|
+
}
|
|
25857
|
+
};
|
|
25858
|
+
const secretReadError = (secrets, names, error) => new Error(`Couldn't read ${list$1(names)} from the ${secrets.name} secrets adapter.\n${(error instanceof Error ? error.message : String(error)).split("\n")[0]}`, { cause: error });
|
|
25700
25859
|
/** Where an adapter keeps `name`, for messages. */
|
|
25701
25860
|
const describeSecret = (secrets, name) => secrets.describe?.(name) ?? `${name} in ${secrets.name}`;
|
|
25702
25861
|
const list$1 = (names) => names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
25703
25862
|
/**
|
|
25704
|
-
*
|
|
25705
|
-
*
|
|
25706
|
-
*
|
|
25707
|
-
*
|
|
25708
|
-
*
|
|
25709
|
-
*
|
|
25710
|
-
*
|
|
25863
|
+
* `secret("<name>")`: the secret `name` in `hot-updater.config.ts`, for a
|
|
25864
|
+
* build credential, such as `signing.keystore`, or any adapter credential,
|
|
25865
|
+
* which calls it on first use, so loading the config reads no secret.
|
|
25866
|
+
* Called, it reads `name`'s variable (`secretVariableName`) in the
|
|
25867
|
+
* environment or `.env.hotupdater`, else the config's `secrets` adapter:
|
|
25868
|
+
* the first call reads every name the config's `secret()`s reference and
|
|
25869
|
+
* the environment doesn't set, in one `get`. With `localSecrets()`, the
|
|
25870
|
+
* variable must be set: its files hold only the keys Hot Updater stores.
|
|
25871
|
+
* Values are masked in the CLI's logs and never set in `process.env`.
|
|
25872
|
+
* Names a loader or interpreter reads, such as `NODE_OPTIONS`, are refused.
|
|
25873
|
+
* A server passes its own environment's values instead.
|
|
25711
25874
|
*
|
|
25712
25875
|
* @example
|
|
25713
|
-
*
|
|
25714
|
-
* s3Storage({
|
|
25876
|
+
* r2Storage({
|
|
25715
25877
|
* bucketName: "bundles",
|
|
25716
|
-
*
|
|
25717
|
-
* credentials:
|
|
25718
|
-
*
|
|
25719
|
-
*
|
|
25720
|
-
* accessKeyId: env.S3_ACCESS_KEY_ID,
|
|
25721
|
-
* secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
|
25722
|
-
* };
|
|
25878
|
+
* accountId: "<account-id>",
|
|
25879
|
+
* credentials: {
|
|
25880
|
+
* accessKeyId: secret("HOT_UPDATER_CLOUDFLARE_R2_ACCESS_KEY_ID"),
|
|
25881
|
+
* secretAccessKey: secret("HOT_UPDATER_CLOUDFLARE_R2_SECRET_ACCESS_KEY"),
|
|
25723
25882
|
* },
|
|
25724
25883
|
* });
|
|
25725
25884
|
*/
|
|
25726
|
-
const
|
|
25727
|
-
|
|
25728
|
-
|
|
25729
|
-
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.`);
|
|
25730
|
-
const reserved = wanted.filter(isReservedVariableName);
|
|
25731
|
-
if (reserved.length > 0) throw new Error(`secretEnv refuses ${list$1(reserved)}: ${reserved.length === 1 ? "it changes" : "they change"} how a process loads code or which credentials Hot Updater uses.`);
|
|
25885
|
+
const secret = (name) => {
|
|
25886
|
+
if (typeof name !== "string" || !isSecretName(name)) throw new Error(`secret() takes up to 64 letters, digits, ".", "_", and "-", starting with a letter or digit, and ${JSON.stringify(name)} is not that.`);
|
|
25887
|
+
if (isReservedVariableName(name)) throw new Error(`secret() refuses ${name}: it changes how a process loads code or which credentials Hot Updater uses.`);
|
|
25732
25888
|
const scope = globalThis[LOADING_SCOPE];
|
|
25733
|
-
if (scope)
|
|
25734
|
-
scope.names ??= /* @__PURE__ */ new Set();
|
|
25735
|
-
for (const name of wanted) scope.names.add(name);
|
|
25736
|
-
}
|
|
25889
|
+
if (scope) (scope.names ??= /* @__PURE__ */ new Set()).add(name);
|
|
25737
25890
|
let read;
|
|
25738
|
-
|
|
25891
|
+
const ref = () => read ??= readSecretRef(name, scope).catch((error) => {
|
|
25739
25892
|
read = void 0;
|
|
25740
25893
|
throw error;
|
|
25741
25894
|
});
|
|
25895
|
+
Object.defineProperty(ref, "name", { value: name });
|
|
25896
|
+
Object.defineProperty(ref, SECRET_REF, { value: true });
|
|
25897
|
+
return Object.freeze(ref);
|
|
25742
25898
|
};
|
|
25743
|
-
|
|
25899
|
+
/**
|
|
25900
|
+
* The scope's names to read in one `get`: those no build credential takes
|
|
25901
|
+
* and the environment doesn't set, with `name`.
|
|
25902
|
+
*/
|
|
25903
|
+
const batchedNames = (scope, name, fileEnv) => {
|
|
25904
|
+
const names = [...scope.names ?? []].filter((candidate) => !scope.buildNames?.has(candidate) && getHotUpdaterEnvValue(fileEnv, secretVariableName(candidate)) === void 0);
|
|
25905
|
+
return names.includes(name) ? names : [...names, name];
|
|
25906
|
+
};
|
|
25907
|
+
const readSecretRef = async (name, scope) => {
|
|
25744
25908
|
const cwd = getCwd();
|
|
25745
25909
|
const fileEnv = await readLoadedHotUpdaterEnv(cwd);
|
|
25746
|
-
const
|
|
25747
|
-
const
|
|
25748
|
-
|
|
25749
|
-
|
|
25750
|
-
|
|
25751
|
-
|
|
25752
|
-
|
|
25753
|
-
if (
|
|
25754
|
-
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
25759
|
-
variables[name] = value;
|
|
25760
|
-
return false;
|
|
25910
|
+
const variable = secretVariableName(name);
|
|
25911
|
+
const fromEnv = getHotUpdaterEnvValue(fileEnv, variable);
|
|
25912
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
25913
|
+
registerSecretValue(fromEnv);
|
|
25914
|
+
return fromEnv;
|
|
25915
|
+
}
|
|
25916
|
+
const secrets = getConfiguredAdapter(scope?.secrets ?? getConfiguredSecrets());
|
|
25917
|
+
if (secrets === void 0) throw new Error(`secret("${name}") has no value: set ${variable} in the environment or .env.hotupdater, or set the config's secrets to a team store.`);
|
|
25918
|
+
let values;
|
|
25919
|
+
if (scope) {
|
|
25920
|
+
scope.read ??= readSecrets(secrets, batchedNames(scope, name, fileEnv), cwd).catch((error) => {
|
|
25921
|
+
scope.read = void 0;
|
|
25922
|
+
throw error;
|
|
25761
25923
|
});
|
|
25762
|
-
|
|
25763
|
-
const them = missing.length === 1 ? "it" : "them";
|
|
25764
|
-
throw new Error(secrets === void 0 ? `secretEnv found no value for ${list$1(missing)}: set ${them} in the environment or .env.hotupdater, or read ${them} from a secrets store with the config's secrets.` : `secretEnv found no value for ${list$1(missing)}: ${missing.length === 1 ? "it is" : "they are"} not set in the environment or .env.hotupdater, and the ${secrets.name} secrets adapter does not hold ${them} (${missing.map((name) => describeSecret(secrets, name)).join("; ")}).`);
|
|
25765
|
-
}
|
|
25924
|
+
values = await scope.read;
|
|
25766
25925
|
}
|
|
25767
|
-
|
|
25768
|
-
|
|
25926
|
+
if (!values || !Object.hasOwn(values, name)) values = await readSecrets(secrets, [name], cwd);
|
|
25927
|
+
const value = Object.hasOwn(values, name) ? values[name] : void 0;
|
|
25928
|
+
if (typeof value !== "string" || value === "") throw new Error(`secret("${name}") has no value: ${variable} is not set in the environment or .env.hotupdater, and the ${secrets.name} secrets adapter does not hold it (${describeSecret(secrets, name)}).`);
|
|
25929
|
+
registerSecretValue(value);
|
|
25930
|
+
return value;
|
|
25769
25931
|
};
|
|
25770
25932
|
//#endregion
|
|
25771
|
-
//#region src/
|
|
25772
|
-
/** The secret `
|
|
25773
|
-
const
|
|
25933
|
+
//#region src/bundleSigning.ts
|
|
25934
|
+
/** The secret `signing: secret("bundle-signing-key")` names, which `secrets issue` creates. */
|
|
25935
|
+
const BUNDLE_SIGNING_KEY_SECRET = "bundle-signing-key";
|
|
25936
|
+
/** The command that creates and stores a bundle signing key. */
|
|
25937
|
+
const ISSUE_SIGNING_KEY_COMMAND = `npx hot-updater secrets issue ${BUNDLE_SIGNING_KEY_SECRET}`;
|
|
25938
|
+
/** The command that stores a bundle signing key you have. */
|
|
25939
|
+
const PUT_SIGNING_KEY_COMMAND = `npx hot-updater secrets put ${BUNDLE_SIGNING_KEY_SECRET} <key.pem>`;
|
|
25940
|
+
/** What deploy and doctor say when the config sets no `signing`. */
|
|
25941
|
+
const UNSIGNED_BUNDLES_WARNING = "Bundles are unsigned.";
|
|
25942
|
+
/** The next step after `UNSIGNED_BUNDLES_WARNING`. */
|
|
25943
|
+
const UNSIGNED_BUNDLES_NEXT_STEP = "To sign them, run:";
|
|
25944
|
+
/** What deploy and doctor say when `signing` is no shape it takes. */
|
|
25945
|
+
const INVALID_SIGNING_MESSAGE = `signing takes secret("${BUNDLE_SIGNING_KEY_SECRET}").`;
|
|
25946
|
+
/** The next step after `INVALID_SIGNING_MESSAGE`. */
|
|
25947
|
+
const INVALID_SIGNING_NEXT_STEP = "Store your key:";
|
|
25948
|
+
const invalid = (message) => ({
|
|
25949
|
+
status: "invalid",
|
|
25950
|
+
message
|
|
25951
|
+
});
|
|
25952
|
+
const LOCAL_FIELDS = [
|
|
25953
|
+
"enabled",
|
|
25954
|
+
"privateKeyPath",
|
|
25955
|
+
"privateKey"
|
|
25956
|
+
];
|
|
25957
|
+
const readBundleSigningState = (signing) => {
|
|
25958
|
+
if (signing === void 0) return { status: "unsigned" };
|
|
25959
|
+
if (isSecretRef(signing)) return {
|
|
25960
|
+
status: "on",
|
|
25961
|
+
key: { secret: signing.name }
|
|
25962
|
+
};
|
|
25963
|
+
if (typeof signing !== "object" || signing === null || !Object.keys(signing).every((key) => LOCAL_FIELDS.includes(key))) return invalid(INVALID_SIGNING_MESSAGE);
|
|
25964
|
+
const { enabled, privateKey, privateKeyPath } = signing;
|
|
25965
|
+
if (enabled === false) return { status: "off" };
|
|
25966
|
+
if (enabled !== true) return invalid(INVALID_SIGNING_MESSAGE);
|
|
25967
|
+
if (privateKey !== void 0 && privateKeyPath !== void 0) return invalid("signing takes privateKey or privateKeyPath, not both.");
|
|
25968
|
+
if (privateKey !== void 0) return isSecretRef(privateKey) ? {
|
|
25969
|
+
status: "on",
|
|
25970
|
+
key: { secret: privateKey.name }
|
|
25971
|
+
} : invalid("signing.privateKey takes secret(\"<name>\").");
|
|
25972
|
+
if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) return invalid("signing takes privateKeyPath, or privateKey as secret(\"<name>\").");
|
|
25973
|
+
return {
|
|
25974
|
+
status: "on",
|
|
25975
|
+
key: { privateKeyPath }
|
|
25976
|
+
};
|
|
25977
|
+
};
|
|
25978
|
+
/** The `secret()` the config's `signing` reads its private key from, if any. */
|
|
25979
|
+
const bundleSigningSecretRef = (signing) => {
|
|
25980
|
+
if (isSecretRef(signing)) return signing;
|
|
25981
|
+
const privateKey = typeof signing === "object" && signing !== null ? Reflect.get(signing, "privateKey") : void 0;
|
|
25982
|
+
return isSecretRef(privateKey) ? privateKey : void 0;
|
|
25983
|
+
};
|
|
25774
25984
|
/**
|
|
25775
|
-
* A
|
|
25776
|
-
* the secret and
|
|
25985
|
+
* A bundle signing failure whose message the CLI prints as it is: it names
|
|
25986
|
+
* the secret and the next step, and quotes no key material.
|
|
25777
25987
|
*/
|
|
25778
|
-
var
|
|
25988
|
+
var BundleSigningKeyError = class extends Error {
|
|
25779
25989
|
constructor(message, options) {
|
|
25780
25990
|
super(message, options);
|
|
25781
|
-
this.name = "
|
|
25991
|
+
this.name = "HotUpdaterBundleSigningKeyError";
|
|
25782
25992
|
}
|
|
25783
25993
|
};
|
|
25784
|
-
const
|
|
25785
|
-
const
|
|
25994
|
+
const FILE_HASH_PATTERN = /^[a-f\d]{64}$/iu;
|
|
25995
|
+
const resolvePath = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.resolve(cwd, filePath);
|
|
25996
|
+
const parseRsaPublicKey = (publicKeyPEM) => {
|
|
25997
|
+
try {
|
|
25998
|
+
const normalizedPublicKey = publicKeyPEM.trim();
|
|
25999
|
+
if (!normalizedPublicKey.startsWith("-----BEGIN PUBLIC KEY-----") || !normalizedPublicKey.endsWith("-----END PUBLIC KEY-----")) throw new Error("not spki");
|
|
26000
|
+
const publicKey = crypto$1.createPublicKey(normalizedPublicKey);
|
|
26001
|
+
if (publicKey.asymmetricKeyType !== "rsa" || (publicKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("not rsa");
|
|
26002
|
+
return publicKey;
|
|
26003
|
+
} catch {
|
|
26004
|
+
throw new Error("Bundle signing public key must be a valid RSA SPKI PEM key with a modulus of at least 2048 bits.");
|
|
26005
|
+
}
|
|
26006
|
+
};
|
|
26007
|
+
/** The SPKI PEM of `key`, a public key or the private key it derives from. */
|
|
26008
|
+
const exportPublicKey = (key) => (key.type === "private" ? crypto$1.createPublicKey(key) : key).export({
|
|
26009
|
+
type: "spki",
|
|
26010
|
+
format: "pem"
|
|
26011
|
+
}).toString();
|
|
26012
|
+
const readBundleSigningPublicKeyFile = async (publicKeyPath, options = {}) => {
|
|
26013
|
+
if (!publicKeyPath.trim()) throw new Error("Bundle signing public key path is required.");
|
|
25786
26014
|
try {
|
|
25787
|
-
const
|
|
25788
|
-
|
|
25789
|
-
return privateKey;
|
|
26015
|
+
const publicKeyPEM = await fs$2.readFile(resolvePath(options.cwd ?? getCwd(), publicKeyPath), "utf8");
|
|
26016
|
+
return exportPublicKey(parseRsaPublicKey(publicKeyPEM));
|
|
25790
26017
|
} catch {
|
|
25791
|
-
throw new Error("Failed to
|
|
26018
|
+
throw new Error("Failed to read the bundle signing public key file.");
|
|
25792
26019
|
}
|
|
25793
26020
|
};
|
|
25794
|
-
const
|
|
25795
|
-
|
|
26021
|
+
const parsePrivateKey = (pem) => {
|
|
26022
|
+
const privateKey = crypto$1.createPrivateKey(pem);
|
|
26023
|
+
if (privateKey.asymmetricKeyType !== "rsa" || (privateKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("not rsa");
|
|
26024
|
+
return privateKey;
|
|
26025
|
+
};
|
|
26026
|
+
const loadPrivateKeyFile = async (file) => {
|
|
25796
26027
|
try {
|
|
25797
|
-
|
|
26028
|
+
return parsePrivateKey(await fs$2.readFile(file, "utf8"));
|
|
25798
26029
|
} catch {
|
|
25799
|
-
throw new
|
|
26030
|
+
throw new BundleSigningKeyError("Failed to load the bundle signing private key from privateKeyPath.");
|
|
25800
26031
|
}
|
|
25801
|
-
return parsePrivateKey(pem);
|
|
25802
26032
|
};
|
|
25803
26033
|
/**
|
|
25804
26034
|
* The private key the `secrets` adapter holds as `name`: masked in the CLI's
|
|
@@ -25813,219 +26043,90 @@ const loadPrivateKeySecret = async (secrets, name, cwd) => {
|
|
|
25813
26043
|
value = Object.hasOwn(read, name) ? read[name] : void 0;
|
|
25814
26044
|
} catch (error) {
|
|
25815
26045
|
const reason = error instanceof Error ? error.message : String(error);
|
|
25816
|
-
throw new
|
|
26046
|
+
throw new BundleSigningKeyError(`The ${secrets.name} secrets adapter could not read the bundle signing key ${name}: ${reason}`, { cause: error });
|
|
26047
|
+
}
|
|
26048
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
26049
|
+
const issue = `npx hot-updater secrets issue ${BUNDLE_SIGNING_KEY_SECRET}${name === "bundle-signing-key" ? "" : ` ${name}`}`;
|
|
26050
|
+
throw new BundleSigningKeyError(`signing reads the bundle signing key ${name}, which is not in the ${secrets.name} secrets adapter (${describeSecret(secrets, name)}).\nTo create one, run: ${issue}`);
|
|
25817
26051
|
}
|
|
25818
|
-
if (typeof value !== "string" || !value.trim()) throw new LocalSigningKeyError(`signing.privateKey reads the bundle signing key ${name}, which is not in the ${secrets.name} secrets adapter (${describeSecret(secrets, name)}). Set it to the RSA private key in PEM, or generate a key pair with \`npx hot-updater keys generate --secret ${name}\`.`);
|
|
25819
26052
|
registerSecretValue(value);
|
|
25820
26053
|
const pem = value.includes("\n") ? value : value.replaceAll("\\n", "\n");
|
|
25821
26054
|
if (pem !== value) registerSecretValue(pem);
|
|
25822
26055
|
try {
|
|
25823
26056
|
return parsePrivateKey(pem);
|
|
25824
26057
|
} catch {
|
|
25825
|
-
throw new
|
|
26058
|
+
throw new BundleSigningKeyError(`The bundle signing key ${name} in the ${secrets.name} secrets adapter is not an RSA private key in PEM of at least 2048 bits.`);
|
|
25826
26059
|
}
|
|
25827
26060
|
};
|
|
25828
26061
|
/**
|
|
25829
|
-
* The
|
|
25830
|
-
*
|
|
25831
|
-
*
|
|
26062
|
+
* The private keys each `signing` value has read, by file or by secrets
|
|
26063
|
+
* adapter, so the commands of one run that sign and derive the public key
|
|
26064
|
+
* read the key once.
|
|
25832
26065
|
*/
|
|
25833
|
-
const
|
|
25834
|
-
const privateKeys = /* @__PURE__ */ new Map();
|
|
25835
|
-
const { privateKeyPath } = config;
|
|
25836
|
-
const secretName = privateKeyPath === void 0 ? parseSecretReference(config.privateKey, "signing.privateKey") : void 0;
|
|
25837
|
-
const getPrivateKey = (cwd = process.cwd()) => {
|
|
25838
|
-
const cacheKey = privateKeyPath === void 0 ? "" : resolvePath$1(cwd, privateKeyPath);
|
|
25839
|
-
const cached = privateKeys.get(cacheKey);
|
|
25840
|
-
if (cached) return cached;
|
|
25841
|
-
const pending = (secretName === void 0 ? loadPrivateKeyFile(cacheKey) : loadPrivateKeySecret(secrets ?? getConfiguredSecrets(), secretName, cwd)).catch((error) => {
|
|
25842
|
-
privateKeys.delete(cacheKey);
|
|
25843
|
-
throw error;
|
|
25844
|
-
});
|
|
25845
|
-
privateKeys.set(cacheKey, pending);
|
|
25846
|
-
return pending;
|
|
25847
|
-
};
|
|
25848
|
-
return {
|
|
25849
|
-
name: "localSigning",
|
|
25850
|
-
async getPublicKey({ cwd } = {}) {
|
|
25851
|
-
const privateKey = await getPrivateKey(cwd);
|
|
25852
|
-
return { publicKey: crypto$1.createPublicKey(privateKey).export({
|
|
25853
|
-
format: "pem",
|
|
25854
|
-
type: "spki"
|
|
25855
|
-
}).toString() };
|
|
25856
|
-
},
|
|
25857
|
-
async sign({ message, cwd }) {
|
|
25858
|
-
if (!(message instanceof Uint8Array) || message.byteLength !== 32) throw new Error("Local bundle signing messages must be exactly 32 bytes.");
|
|
25859
|
-
const privateKey = await getPrivateKey(cwd);
|
|
25860
|
-
return { signature: crypto$1.sign("RSA-SHA256", message, privateKey) };
|
|
25861
|
-
}
|
|
25862
|
-
};
|
|
25863
|
-
};
|
|
26066
|
+
const privateKeys = /* @__PURE__ */ new WeakMap();
|
|
25864
26067
|
/**
|
|
25865
|
-
* The `
|
|
25866
|
-
*
|
|
25867
|
-
*
|
|
26068
|
+
* The private key `signing` signs with, or null when it signs nothing.
|
|
26069
|
+
* Throws `INVALID_SIGNING_MESSAGE`, with the next step, for a value
|
|
26070
|
+
* `signing` doesn't take.
|
|
25868
26071
|
*/
|
|
25869
|
-
const
|
|
25870
|
-
|
|
25871
|
-
|
|
25872
|
-
|
|
25873
|
-
const
|
|
25874
|
-
|
|
25875
|
-
|
|
25876
|
-
|
|
25877
|
-
|
|
25878
|
-
|
|
25879
|
-
|
|
25880
|
-
|
|
25881
|
-
|
|
25882
|
-
|
|
25883
|
-
|
|
25884
|
-
|
|
25885
|
-
].some((key) => Reflect.has(signing, key));
|
|
25886
|
-
if (hasLocalFields && hasPluginMembers) throw new Error("Bundle signing config cannot combine local signing fields with signing adapter fields.");
|
|
25887
|
-
if (!hasPluginMembers) {
|
|
25888
|
-
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.");
|
|
25889
|
-
const enabled = Reflect.get(signing, "enabled");
|
|
25890
|
-
if (enabled === false) return void 0;
|
|
25891
|
-
if (enabled !== true) throw invalidSigningConfig();
|
|
25892
|
-
const privateKeyPath = Reflect.get(signing, "privateKeyPath");
|
|
25893
|
-
const privateKey = Reflect.get(signing, "privateKey");
|
|
25894
|
-
if (privateKeyPath !== void 0 && privateKey !== void 0) throw new Error("Local bundle signing takes its private key from privateKeyPath or from privateKey, not both.");
|
|
25895
|
-
if (privateKey !== void 0) {
|
|
25896
|
-
if (typeof privateKey !== "string") throw new Error("signing.privateKey names a secret as secret:<name>, such as \"secret:bundle-signing-key\".");
|
|
25897
|
-
parseSecretReference(privateKey, "signing.privateKey");
|
|
25898
|
-
const normalized = {
|
|
25899
|
-
enabled: true,
|
|
25900
|
-
privateKey
|
|
25901
|
-
};
|
|
25902
|
-
if (secrets) signingSecrets.set(normalized, secrets);
|
|
25903
|
-
return normalized;
|
|
25904
|
-
}
|
|
25905
|
-
if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) throw new Error("Enabled local bundle signing requires privateKeyPath, or privateKey as \"secret:<name>\".");
|
|
25906
|
-
return {
|
|
25907
|
-
enabled: true,
|
|
25908
|
-
privateKeyPath
|
|
25909
|
-
};
|
|
25910
|
-
}
|
|
25911
|
-
if (typeof Reflect.get(signing, "name") !== "string" || typeof Reflect.get(signing, "getPublicKey") !== "function" || typeof Reflect.get(signing, "sign") !== "function") throw invalidSigningConfig();
|
|
25912
|
-
return signing;
|
|
25913
|
-
};
|
|
25914
|
-
//#endregion
|
|
25915
|
-
//#region src/bundleSigning.ts
|
|
25916
|
-
const FILE_HASH_PATTERN = /^[a-f\d]{64}$/iu;
|
|
25917
|
-
const resolvePath = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.resolve(cwd, filePath);
|
|
25918
|
-
const parseRsaPublicKey = (publicKeyPEM) => {
|
|
25919
|
-
try {
|
|
25920
|
-
const normalizedPublicKey = publicKeyPEM.trim();
|
|
25921
|
-
if (!normalizedPublicKey.startsWith("-----BEGIN PUBLIC KEY-----") || !normalizedPublicKey.endsWith("-----END PUBLIC KEY-----")) throw new Error("not spki");
|
|
25922
|
-
const publicKey = crypto$1.createPublicKey(normalizedPublicKey);
|
|
25923
|
-
if (publicKey.asymmetricKeyType !== "rsa" || (publicKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("not rsa");
|
|
25924
|
-
return publicKey;
|
|
25925
|
-
} catch {
|
|
25926
|
-
throw new Error("Bundle signing public key must be a valid RSA SPKI PEM key with a modulus of at least 2048 bits.");
|
|
25927
|
-
}
|
|
25928
|
-
};
|
|
25929
|
-
const exportPublicKey = (publicKey) => publicKey.export({
|
|
25930
|
-
type: "spki",
|
|
25931
|
-
format: "pem"
|
|
25932
|
-
}).toString();
|
|
25933
|
-
const readBundleSigningPublicKeyFile = async (publicKeyPath, options = {}) => {
|
|
25934
|
-
if (!publicKeyPath.trim()) throw new Error("Bundle signing public key path is required.");
|
|
25935
|
-
try {
|
|
25936
|
-
const publicKeyPEM = await fs$2.readFile(resolvePath(options.cwd ?? getCwd(), publicKeyPath), "utf8");
|
|
25937
|
-
return exportPublicKey(parseRsaPublicKey(publicKeyPEM));
|
|
25938
|
-
} catch {
|
|
25939
|
-
throw new Error("Failed to read the bundle signing public key file.");
|
|
25940
|
-
}
|
|
26072
|
+
const loadSigningKey = (signing, { cwd = getCwd(), secrets }) => {
|
|
26073
|
+
const state = readBundleSigningState(signing);
|
|
26074
|
+
if (state.status === "invalid") throw new BundleSigningKeyError(`${state.message}\n${INVALID_SIGNING_NEXT_STEP} ${PUT_SIGNING_KEY_COMMAND}`);
|
|
26075
|
+
if (state.status !== "on") return null;
|
|
26076
|
+
const store = secrets ?? getConfiguredSecrets();
|
|
26077
|
+
const cacheKey = "secret" in state.key ? store : resolvePath(cwd, state.key.privateKeyPath);
|
|
26078
|
+
const cache = privateKeys.get(signing) ?? /* @__PURE__ */ new Map();
|
|
26079
|
+
privateKeys.set(signing, cache);
|
|
26080
|
+
const cached = cache.get(cacheKey);
|
|
26081
|
+
if (cached) return cached;
|
|
26082
|
+
const pending = ("secret" in state.key ? loadPrivateKeySecret(store, state.key.secret, cwd) : loadPrivateKeyFile(cacheKey)).catch((error) => {
|
|
26083
|
+
cache.delete(cacheKey);
|
|
26084
|
+
throw error;
|
|
26085
|
+
});
|
|
26086
|
+
cache.set(cacheKey, pending);
|
|
26087
|
+
return pending;
|
|
25941
26088
|
};
|
|
26089
|
+
const signingFailure = (summary, error) => new Error(error instanceof BundleSigningKeyError ? error.message : `${summary} ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
25942
26090
|
/**
|
|
25943
|
-
*
|
|
25944
|
-
*
|
|
25945
|
-
*
|
|
25946
|
-
* key material.
|
|
26091
|
+
* Prepares the run's bundle signing: the public key the app must embed, and
|
|
26092
|
+
* a signer of SHA-256 file hashes that signs each hash once. Null when
|
|
26093
|
+
* `signing` is omitted or `{ enabled: false }`.
|
|
25947
26094
|
*/
|
|
25948
|
-
const
|
|
25949
|
-
const
|
|
25950
|
-
|
|
25951
|
-
|
|
25952
|
-
* from a secret, so the commands of one run that sign and derive the public
|
|
25953
|
-
* key read the secret once.
|
|
25954
|
-
*/
|
|
25955
|
-
const secretSigners = /* @__PURE__ */ new WeakMap();
|
|
25956
|
-
const getSigningProvider = (signing) => {
|
|
25957
|
-
const normalized = normalizeSigningConfig(signing);
|
|
25958
|
-
if (!normalized) return null;
|
|
25959
|
-
if (!("enabled" in normalized)) return normalized;
|
|
25960
|
-
if (normalized.privateKey === void 0) return createLocalSigningAdapter(normalized);
|
|
25961
|
-
const key = signing;
|
|
25962
|
-
const cached = secretSigners.get(key);
|
|
25963
|
-
if (cached) return cached;
|
|
25964
|
-
const local = createLocalSigningAdapter(normalized, { secrets: getSigningSecrets(key) });
|
|
25965
|
-
secretSigners.set(key, local);
|
|
25966
|
-
return local;
|
|
25967
|
-
};
|
|
25968
|
-
const getProviderPublicKey = async (provider, cwd) => {
|
|
26095
|
+
const prepareBundleSigning = async (signing, options = {}) => {
|
|
26096
|
+
const loading = loadSigningKey(signing, options);
|
|
26097
|
+
if (!loading) return null;
|
|
26098
|
+
let privateKey;
|
|
25969
26099
|
try {
|
|
25970
|
-
|
|
25971
|
-
if (!result || typeof result.publicKey !== "string") throw new Error("invalid result");
|
|
25972
|
-
return parseRsaPublicKey(result.publicKey);
|
|
26100
|
+
privateKey = await loading;
|
|
25973
26101
|
} catch (error) {
|
|
25974
|
-
throw signingFailure("Failed to
|
|
26102
|
+
throw signingFailure("Failed to read the bundle signing key.", error);
|
|
25975
26103
|
}
|
|
25976
|
-
};
|
|
25977
|
-
const createMemoizedSigner = ({ publicKey, sign }) => {
|
|
25978
26104
|
const signatures = /* @__PURE__ */ new Map();
|
|
25979
|
-
return (fileHash) => {
|
|
25980
|
-
if (!FILE_HASH_PATTERN.test(fileHash)) return Promise.reject(/* @__PURE__ */ new Error("Bundle signing requires a 64-character hexadecimal file hash."));
|
|
25981
|
-
const normalizedFileHash = fileHash.toLowerCase();
|
|
25982
|
-
const cached = signatures.get(normalizedFileHash);
|
|
25983
|
-
if (cached) return cached;
|
|
25984
|
-
const pending = (async () => {
|
|
25985
|
-
const message = Buffer.from(normalizedFileHash, "hex");
|
|
25986
|
-
let signature;
|
|
25987
|
-
try {
|
|
25988
|
-
signature = await sign(new Uint8Array(message));
|
|
25989
|
-
} catch (error) {
|
|
25990
|
-
throw signingFailure("Bundle signing provider failed to sign the file hash.", error);
|
|
25991
|
-
}
|
|
25992
|
-
if (!(signature instanceof Uint8Array) || signature.byteLength === 0) throw new Error("Bundle signing provider returned an invalid signature.");
|
|
25993
|
-
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.");
|
|
25994
|
-
return Buffer.from(signature).toString("base64");
|
|
25995
|
-
})().catch((error) => {
|
|
25996
|
-
signatures.delete(normalizedFileHash);
|
|
25997
|
-
throw error;
|
|
25998
|
-
});
|
|
25999
|
-
signatures.set(normalizedFileHash, pending);
|
|
26000
|
-
return pending;
|
|
26001
|
-
};
|
|
26002
|
-
};
|
|
26003
|
-
const preparePluginSigning = async (signing, cwd) => {
|
|
26004
|
-
const providerPublicKey = await getProviderPublicKey(signing, cwd);
|
|
26005
26105
|
return {
|
|
26006
|
-
|
|
26007
|
-
|
|
26008
|
-
|
|
26009
|
-
|
|
26010
|
-
|
|
26011
|
-
|
|
26012
|
-
|
|
26013
|
-
|
|
26014
|
-
|
|
26015
|
-
|
|
26016
|
-
})
|
|
26106
|
+
publicKey: exportPublicKey(privateKey),
|
|
26107
|
+
signFileHash: async (fileHash) => {
|
|
26108
|
+
if (!FILE_HASH_PATTERN.test(fileHash)) throw new Error("Bundle signing requires a 64-character hexadecimal file hash.");
|
|
26109
|
+
const normalized = fileHash.toLowerCase();
|
|
26110
|
+
const cached = signatures.get(normalized);
|
|
26111
|
+
if (cached) return cached;
|
|
26112
|
+
const signature = crypto$1.sign("RSA-SHA256", Buffer.from(normalized, "hex"), privateKey).toString("base64");
|
|
26113
|
+
signatures.set(normalized, signature);
|
|
26114
|
+
return signature;
|
|
26115
|
+
}
|
|
26017
26116
|
};
|
|
26018
26117
|
};
|
|
26019
|
-
|
|
26020
|
-
|
|
26021
|
-
|
|
26022
|
-
|
|
26023
|
-
};
|
|
26024
|
-
/** Resolves the public identity of the configured bundle signer. */
|
|
26118
|
+
/**
|
|
26119
|
+
* The public key of the configured bundle signing key, in SPKI PEM; null
|
|
26120
|
+
* when `signing` signs nothing.
|
|
26121
|
+
*/
|
|
26025
26122
|
const getBundleSigningPublicKey = async (signing, options = {}) => {
|
|
26026
|
-
const
|
|
26027
|
-
if (!
|
|
26028
|
-
|
|
26123
|
+
const loading = loadSigningKey(signing, options);
|
|
26124
|
+
if (!loading) return null;
|
|
26125
|
+
try {
|
|
26126
|
+
return exportPublicKey(await loading);
|
|
26127
|
+
} catch (error) {
|
|
26128
|
+
throw signingFailure("Failed to read the bundle signing key.", error);
|
|
26129
|
+
}
|
|
26029
26130
|
};
|
|
26030
26131
|
//#endregion
|
|
26031
26132
|
//#region src/ConfigBuilder.ts
|
|
@@ -38730,48 +38831,55 @@ const formatCommand = (command) => {
|
|
|
38730
38831
|
*/
|
|
38731
38832
|
const withLead = (lead, command) => lead.length + 1 + command.length <= 77 ? `${lead} ${command}` : `${lead}\n${formatCommand(command)}`;
|
|
38732
38833
|
/**
|
|
38733
|
-
* Why a build has no upload keystore,
|
|
38734
|
-
*
|
|
38735
|
-
*
|
|
38736
|
-
*
|
|
38834
|
+
* Why a build has no upload keystore, steering away from a second key:
|
|
38835
|
+
* Play rejects builds signed with a key it does not have. Under a team's
|
|
38836
|
+
* adapter the keystore is someone's already, so it is imported, never
|
|
38837
|
+
* created; only `localSecrets()` alone offers a new one, for an app with
|
|
38838
|
+
* no `.aab` on Play yet.
|
|
38737
38839
|
*/
|
|
38738
38840
|
const describeMissingKeystore = (store, item, name) => {
|
|
38739
38841
|
const configured = getConfiguredAdapter(store);
|
|
38740
38842
|
if (configured) return [
|
|
38741
38843
|
`No ${item} in ${describeSecret(configured, name)}.`,
|
|
38742
38844
|
"Ask whoever set it up to store it there, or import it with:",
|
|
38743
|
-
"
|
|
38845
|
+
formatCommand(credentialPutCommand("android-keystore", name))
|
|
38744
38846
|
].join("\n");
|
|
38745
38847
|
return [
|
|
38746
38848
|
`No ${item} in the ${store.name} secrets adapter.`,
|
|
38747
|
-
"
|
|
38748
|
-
withLead("
|
|
38849
|
+
withLead("Store the app's upload key:", credentialPutCommand("android-keystore", name)),
|
|
38850
|
+
withLead("No .aab on Play yet? Create one:", credentialIssueCommand("android-keystore", name))
|
|
38749
38851
|
].join("\n");
|
|
38750
38852
|
};
|
|
38853
|
+
/** The file each `secrets put <name> <file>` command names. */
|
|
38854
|
+
const PUT_FILES = {
|
|
38855
|
+
"android-keystore": "<upload.jks>",
|
|
38856
|
+
"play-service-account": "<key.json>"
|
|
38857
|
+
};
|
|
38858
|
+
/**
|
|
38859
|
+
* The command that stores the file you have as `name`: the service
|
|
38860
|
+
* account's JSON key, or the app's upload keystore. `--kind` is named only
|
|
38861
|
+
* for a name that is not the kind.
|
|
38862
|
+
*/
|
|
38863
|
+
const credentialPutCommand = (kind, name = kind) => `npx hot-updater secrets put ${name}${name === kind ? "" : ` --kind ${kind}`} ${PUT_FILES[kind]}`;
|
|
38751
38864
|
/**
|
|
38752
38865
|
* The command that stores the key Hot Updater issues as `name`: an App
|
|
38753
|
-
* Store Connect API key after an Apple Account sign-in,
|
|
38754
|
-
*
|
|
38866
|
+
* Store Connect API key after an Apple Account sign-in, and an upload
|
|
38867
|
+
* keystore it creates.
|
|
38755
38868
|
*/
|
|
38756
38869
|
const credentialIssueCommand = (kind, name = kind) => `npx hot-updater secrets issue ${kind}${name === kind ? "" : ` ${name}`}`;
|
|
38757
38870
|
/** The command group of each issued kind's shortcut, `<group> login`. */
|
|
38758
|
-
const CREDENTIAL_LOGIN_GROUPS = {
|
|
38759
|
-
"asc-api-key": "apple",
|
|
38760
|
-
"play-service-account": "google-play"
|
|
38761
|
-
};
|
|
38871
|
+
const CREDENTIAL_LOGIN_GROUPS = { "asc-api-key": "apple" };
|
|
38762
38872
|
/**
|
|
38763
38873
|
* The command people meet first that issues `kind` as `name`: its shortcut,
|
|
38764
|
-
* `apple login` for an App Store Connect API key
|
|
38765
|
-
*
|
|
38766
|
-
* own name; for another name, `credentialIssueCommand`.
|
|
38874
|
+
* `apple login` for an App Store Connect API key, which takes nothing and
|
|
38875
|
+
* stores the kind's own name; for another name, `credentialIssueCommand`.
|
|
38767
38876
|
*/
|
|
38768
38877
|
const credentialLoginCommand = (kind, name = kind) => name === kind ? `npx hot-updater ${CREDENTIAL_LOGIN_GROUPS[kind]} login` : credentialIssueCommand(kind, name);
|
|
38769
38878
|
/**
|
|
38770
38879
|
* The command that stores a missing credential of `kind` as `name`: the
|
|
38771
|
-
*
|
|
38772
|
-
* sign-in.
|
|
38880
|
+
* key Hot Updater issues after an Apple sign-in, or the file you have.
|
|
38773
38881
|
*/
|
|
38774
|
-
const credentialStoreCommand = (kind, name = kind) => kind === "
|
|
38882
|
+
const credentialStoreCommand = (kind, name = kind) => kind === "asc-api-key" ? credentialLoginCommand(kind, name) : credentialPutCommand(kind, name);
|
|
38775
38883
|
var CredentialUnavailableError = class extends Error {
|
|
38776
38884
|
kind;
|
|
38777
38885
|
/** The environment variables that can provide this credential. */
|
|
@@ -45531,19 +45639,6 @@ const MANAGED_HELPER_NAMES = /* @__PURE__ */ new Set([
|
|
|
45531
45639
|
"deploySecrets",
|
|
45532
45640
|
"storageSecrets"
|
|
45533
45641
|
]);
|
|
45534
|
-
/**
|
|
45535
|
-
* `const <name> = secretEnv([...names])`, a helper a managed config reads
|
|
45536
|
-
* credentials with: from the environment and `.env.hotupdater` first, then
|
|
45537
|
-
* from the config's `secrets` adapter. `storageSecrets` and
|
|
45538
|
-
* `databaseSecrets` read each adapter's own, so a command that reaches only
|
|
45539
|
-
* one needs only its credentials; `deploySecrets` reads what both share.
|
|
45540
|
-
* Its builder imports `secretEnv` from `hot-updater`.
|
|
45541
|
-
*/
|
|
45542
|
-
const secretEnvHelper = (name, names) => ({
|
|
45543
|
-
name,
|
|
45544
|
-
strategy: "replace",
|
|
45545
|
-
code: `const ${name} = secretEnv([\n${names.map((variable) => ` ${JSON.stringify(variable)},`).join("\n")}\n]);`
|
|
45546
|
-
});
|
|
45547
45642
|
const KNOWN_BUILD_CALLEES = /* @__PURE__ */ new Set([
|
|
45548
45643
|
"bare",
|
|
45549
45644
|
"expo",
|
|
@@ -45620,13 +45715,16 @@ const isHotUpdaterEnvRead = (expression) => {
|
|
|
45620
45715
|
const read = expression.type === "TSNonNullExpression" ? expression.expression : expression;
|
|
45621
45716
|
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";
|
|
45622
45717
|
};
|
|
45718
|
+
/** `secret("<name>")`, the credential init writes. */
|
|
45719
|
+
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";
|
|
45623
45720
|
/**
|
|
45624
45721
|
* A value init writes into a managed adapter's options: a string, a
|
|
45625
|
-
* `process.env.HOT_UPDATER_*` read, as configs of 1.1 read every value,
|
|
45626
|
-
* an object of only those, such as `credentials`.
|
|
45627
|
-
* what its run resolves; any other value is the
|
|
45722
|
+
* `process.env.HOT_UPDATER_*` read, as configs of 1.1 read every value, a
|
|
45723
|
+
* `secret("<name>")`, or an object of only those, such as `credentials`.
|
|
45724
|
+
* Init replaces it with what its run resolves; any other value is the
|
|
45725
|
+
* project's own and kept.
|
|
45628
45726
|
*/
|
|
45629
|
-
const isInitWrittenValue = (expression) => expression.type === "Literal" && typeof expression.value === "string" || isHotUpdaterEnvRead(expression) || expression.type === "ObjectExpression" && expression.properties.length > 0 && expression.properties.every((property) => isDataProperty(property) && isInitWrittenValue(property.value));
|
|
45727
|
+
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));
|
|
45630
45728
|
const dedentBlock = (text) => {
|
|
45631
45729
|
const lines = text.replace(/\s+$/, "").split("\n");
|
|
45632
45730
|
const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)?.[0].length ?? 0);
|
|
@@ -46191,6 +46289,22 @@ const readTextFile = (filePath) => fs$1.readFile(filePath, "utf-8").catch((error
|
|
|
46191
46289
|
* existing config with the scaffold's build, storage, database, plugins, and
|
|
46192
46290
|
* helpers merged in, keeping the project's own settings.
|
|
46193
46291
|
*/
|
|
46292
|
+
/**
|
|
46293
|
+
* Whether `hot-updater.config.ts` in `cwd` sets a team `secrets` adapter,
|
|
46294
|
+
* read from its text: a `secrets` property other than `localSecrets(...)`,
|
|
46295
|
+
* or a config whose shape init can't read. False without the file. Init
|
|
46296
|
+
* reads the store through the loaded config, so one that sets a store but
|
|
46297
|
+
* doesn't load must be fixed first.
|
|
46298
|
+
*/
|
|
46299
|
+
const configSetsTeamSecrets = async (cwd) => {
|
|
46300
|
+
const text = await readTextFile(path.join(cwd, HOT_UPDATER_CONFIG_PATH));
|
|
46301
|
+
if (text === null) return false;
|
|
46302
|
+
const source = parseConfigSource(text);
|
|
46303
|
+
const config = source && findDefineConfigObject(source);
|
|
46304
|
+
if (!config) return true;
|
|
46305
|
+
const secrets = findAnyProperty(config.objectExpression, "secrets");
|
|
46306
|
+
return secrets !== null && getCallCallee(secrets.value) !== "localSecrets";
|
|
46307
|
+
};
|
|
46194
46308
|
const writeHotUpdaterConfig = async (scaffold, filePath = HOT_UPDATER_CONFIG_PATH) => {
|
|
46195
46309
|
const existingText = await readTextFile(filePath);
|
|
46196
46310
|
if (existingText === null) {
|
|
@@ -46246,6 +46360,75 @@ const writeHotUpdaterFiles = async (scaffold, { cwd = process.cwd(), settings })
|
|
|
46246
46360
|
].join("\n"));
|
|
46247
46361
|
return { config };
|
|
46248
46362
|
};
|
|
46363
|
+
/**
|
|
46364
|
+
* The config text with `signing: secret("<name>")` added, and `secret` in
|
|
46365
|
+
* its `hot-updater` import, once. A config that sets `signing` in any form,
|
|
46366
|
+
* such as 1.1's `{ enabled: true, privateKeyPath }`, is kept.
|
|
46367
|
+
*/
|
|
46368
|
+
const addSigningSecretToConfigText = (text, name) => {
|
|
46369
|
+
const source = parseConfigSource(text);
|
|
46370
|
+
const config = source && findDefineConfigObject(source);
|
|
46371
|
+
if (!source || !config) return { status: "unsupported" };
|
|
46372
|
+
const { objectExpression } = config;
|
|
46373
|
+
if (objectExpression.properties.some((property) => property.type === "SpreadElement" || getObjectPropertyName(property) === "signing")) return { status: "kept" };
|
|
46374
|
+
const edits = [];
|
|
46375
|
+
const property = `signing: secret(${JSON.stringify(name)})`;
|
|
46376
|
+
const last = objectExpression.properties.at(-1);
|
|
46377
|
+
if (!last) edits.push({
|
|
46378
|
+
start: objectExpression.start,
|
|
46379
|
+
end: objectExpression.end,
|
|
46380
|
+
text: `{\n ${property},\n}`
|
|
46381
|
+
});
|
|
46382
|
+
else {
|
|
46383
|
+
const lineStart = text.lastIndexOf("\n", last.start) + 1;
|
|
46384
|
+
const indent = /^[ \t]*/u.exec(text.slice(lineStart, last.start))[0];
|
|
46385
|
+
const between = text.slice(last.end, objectExpression.end - 1);
|
|
46386
|
+
const comma = /^\s*,/u.exec(between);
|
|
46387
|
+
let at = last.end + (comma?.[0].length ?? 0);
|
|
46388
|
+
const lineEnd = text.indexOf("\n", at);
|
|
46389
|
+
if (lineEnd !== -1 && text.slice(at, lineEnd).trim().startsWith("//")) at = lineEnd;
|
|
46390
|
+
edits.push({
|
|
46391
|
+
start: at,
|
|
46392
|
+
end: at,
|
|
46393
|
+
text: `\n${indent}${property},`
|
|
46394
|
+
});
|
|
46395
|
+
if (!comma) edits.push({
|
|
46396
|
+
start: last.end,
|
|
46397
|
+
end: last.end,
|
|
46398
|
+
text: ","
|
|
46399
|
+
});
|
|
46400
|
+
}
|
|
46401
|
+
const hotUpdaterImports = importDeclarationsOf(source).filter((declaration) => declaration.source.value === "hot-updater" && declaration.importKind !== "type");
|
|
46402
|
+
if (!hotUpdaterImports.some((declaration) => declaration.specifiers.some((specifier) => specifier.type === "ImportSpecifier" && specifier.local.name === "secret"))) {
|
|
46403
|
+
const named = hotUpdaterImports.find((declaration) => declaration.specifiers.some((specifier) => specifier.type === "ImportSpecifier"));
|
|
46404
|
+
const lastSpecifier = named?.specifiers.at(-1);
|
|
46405
|
+
edits.push(named && lastSpecifier ? {
|
|
46406
|
+
start: lastSpecifier.end,
|
|
46407
|
+
end: lastSpecifier.end,
|
|
46408
|
+
text: ", secret"
|
|
46409
|
+
} : {
|
|
46410
|
+
start: 0,
|
|
46411
|
+
end: 0,
|
|
46412
|
+
text: "import { secret } from \"hot-updater\";\n"
|
|
46413
|
+
});
|
|
46414
|
+
}
|
|
46415
|
+
return {
|
|
46416
|
+
status: "added",
|
|
46417
|
+
text: applyTextEdits(text, edits)
|
|
46418
|
+
};
|
|
46419
|
+
};
|
|
46420
|
+
/**
|
|
46421
|
+
* Adds `signing: secret("<name>")` to `hot-updater.config.ts` in `cwd`, as
|
|
46422
|
+
* `addSigningSecretToConfigText` does. Without the file, nothing is written.
|
|
46423
|
+
*/
|
|
46424
|
+
const addSigningSecretToConfig = async (cwd, name) => {
|
|
46425
|
+
const filePath = path.join(cwd, HOT_UPDATER_CONFIG_PATH);
|
|
46426
|
+
const text = await readTextFile(filePath);
|
|
46427
|
+
if (text === null) return "missing";
|
|
46428
|
+
const result = addSigningSecretToConfigText(text, name);
|
|
46429
|
+
if (result.status === "added") await fs$1.writeFile(filePath, result.text, "utf-8");
|
|
46430
|
+
return result.status;
|
|
46431
|
+
};
|
|
46249
46432
|
//#endregion
|
|
46250
46433
|
//#region src/infrastructureGeneration.ts
|
|
46251
46434
|
const assertInfrastructureGenerationPayload = ({ payload, provider, resource }) => {
|
|
@@ -47079,8 +47262,8 @@ const loadConfigFile = async () => {
|
|
|
47079
47262
|
};
|
|
47080
47263
|
/**
|
|
47081
47264
|
* The config `options` select, resolved: a config function runs in a scope
|
|
47082
|
-
* of its own, so the `
|
|
47083
|
-
* and the file's top-level `
|
|
47265
|
+
* of its own, so the `secret()` it creates reads this config's `secrets`,
|
|
47266
|
+
* and the file's top-level `secret()` reads the first config's.
|
|
47084
47267
|
*/
|
|
47085
47268
|
const configFor = async ({ source, scope: fileScope }, options) => {
|
|
47086
47269
|
if (typeof source !== "function") return resolveConfig(source, [fileScope]);
|
|
@@ -47108,31 +47291,48 @@ const loadPlatformConfigs = async (platforms, { channel }) => {
|
|
|
47108
47291
|
return configs;
|
|
47109
47292
|
};
|
|
47110
47293
|
/**
|
|
47294
|
+
* The names the config's build credentials reference, which the CLI reads
|
|
47295
|
+
* for their kind: `signing`'s, and each scheme's
|
|
47296
|
+
* `signing.ascApiKey` and `signing.keystore`.
|
|
47297
|
+
*/
|
|
47298
|
+
const buildSecretNames = (config) => {
|
|
47299
|
+
return [
|
|
47300
|
+
bundleSigningSecretRef(config.signing),
|
|
47301
|
+
...Object.values(config.nativeBuild?.ios ?? {}).map((scheme) => scheme?.signing?.ascApiKey),
|
|
47302
|
+
...Object.values(config.nativeBuild?.android ?? {}).map((scheme) => scheme?.signing?.keystore)
|
|
47303
|
+
].filter(isSecretRef).map(({ name }) => name);
|
|
47304
|
+
};
|
|
47305
|
+
/**
|
|
47111
47306
|
* The config with its defaults. Its `secrets` adapter fills `scopes` not yet
|
|
47112
|
-
* filled, which the `
|
|
47307
|
+
* filled, which the `secret()` its evaluation created read, and becomes the
|
|
47113
47308
|
* default of credential resolvers.
|
|
47114
47309
|
*/
|
|
47115
47310
|
const resolveConfig = (config, scopes) => {
|
|
47116
47311
|
const mergedConfig = mergeConfigSources(config, getDefaultConfig());
|
|
47117
47312
|
const secrets = resolveSecretsAdapter(mergedConfig.secrets);
|
|
47118
|
-
const
|
|
47119
|
-
for (const scope of scopes)
|
|
47313
|
+
const buildNames = buildSecretNames(mergedConfig);
|
|
47314
|
+
for (const scope of scopes) {
|
|
47315
|
+
scope.secrets ??= secrets;
|
|
47316
|
+
scope.buildNames ??= /* @__PURE__ */ new Set();
|
|
47317
|
+
for (const name of buildNames) scope.buildNames.add(name);
|
|
47318
|
+
}
|
|
47120
47319
|
setConfiguredSecrets(secrets);
|
|
47121
47320
|
const resolved = {
|
|
47122
47321
|
...mergedConfig,
|
|
47123
|
-
signing,
|
|
47124
47322
|
secrets
|
|
47125
47323
|
};
|
|
47126
|
-
Object.defineProperty(resolved,
|
|
47324
|
+
Object.defineProperty(resolved, DEPLOY_SECRET_NAMES, { value: [...new Set(scopes.flatMap((scope) => [...scope.names ?? []]))].filter((name) => !buildNames.includes(name)) });
|
|
47127
47325
|
return resolved;
|
|
47128
47326
|
};
|
|
47129
|
-
const
|
|
47327
|
+
const DEPLOY_SECRET_NAMES = Symbol.for("@hot-updater/cli-tools/deploy-secret-names");
|
|
47130
47328
|
/**
|
|
47131
|
-
* The names
|
|
47132
|
-
*
|
|
47329
|
+
* The names the config's `secret()`s reference for its adapters, such as
|
|
47330
|
+
* `HOT_UPDATER_CLOUDFLARE_API_TOKEN`, in the order the config created
|
|
47331
|
+
* them: every name but those of build credentials, which `doctor` lists
|
|
47332
|
+
* by field.
|
|
47133
47333
|
*/
|
|
47134
|
-
const
|
|
47135
|
-
const names = Reflect.get(config,
|
|
47334
|
+
const getDeploySecretNames = (config) => {
|
|
47335
|
+
const names = Reflect.get(config, DEPLOY_SECRET_NAMES);
|
|
47136
47336
|
return Array.isArray(names) ? names : [];
|
|
47137
47337
|
};
|
|
47138
47338
|
//#endregion
|
|
@@ -47456,7 +47656,7 @@ const loadTeamSecrets = async () => {
|
|
|
47456
47656
|
const readMissingFromTeamSecrets = async (secrets, names, { env, cwd }) => {
|
|
47457
47657
|
const missing = names.filter((name) => getHotUpdaterEnvValue(env, name) === void 0);
|
|
47458
47658
|
if (missing.length === 0) return {};
|
|
47459
|
-
const held = await secrets
|
|
47659
|
+
const held = await readSecrets(secrets, missing, cwd);
|
|
47460
47660
|
const values = Object.create(null);
|
|
47461
47661
|
for (const name of missing) {
|
|
47462
47662
|
const value = Object.hasOwn(held, name) ? held[name] : void 0;
|
|
@@ -47468,16 +47668,18 @@ const readMissingFromTeamSecrets = async (secrets, names, { env, cwd }) => {
|
|
|
47468
47668
|
return values;
|
|
47469
47669
|
};
|
|
47470
47670
|
/**
|
|
47471
|
-
* Stores each of `names` that `.env.hotupdater`
|
|
47472
|
-
* then reads them back in one call.
|
|
47671
|
+
* Stores each of `names` that `envFile`, `.env.hotupdater` by default,
|
|
47672
|
+
* holds through `secrets`, then reads them back in one call. Only the file
|
|
47673
|
+
* is read, never the environment. The file is left as it is. Returns the
|
|
47473
47674
|
* names stored, in the order of `names`; throws when the adapter reads
|
|
47474
47675
|
* back another value.
|
|
47475
47676
|
*/
|
|
47476
|
-
const storeEnvFileSecrets = async (secrets, names, cwd) => {
|
|
47477
|
-
const file = await readLoadedHotUpdaterEnv(cwd);
|
|
47677
|
+
const storeEnvFileSecrets = async (secrets, names, cwd, envFile) => {
|
|
47678
|
+
const file = envFile === void 0 ? await readLoadedHotUpdaterEnv(cwd) : await readLoadedEnvFile(envFile, cwd);
|
|
47478
47679
|
const values = /* @__PURE__ */ new Map();
|
|
47479
47680
|
for (const name of names) {
|
|
47480
|
-
const
|
|
47681
|
+
const variable = secretVariableName(name);
|
|
47682
|
+
const value = Object.hasOwn(file, variable) ? file[variable]?.trim() : void 0;
|
|
47481
47683
|
if (value) values.set(name, value);
|
|
47482
47684
|
}
|
|
47483
47685
|
if (values.size === 0) return [];
|
|
@@ -47543,4 +47745,4 @@ function transformTemplate(templateString, values) {
|
|
|
47543
47745
|
return result;
|
|
47544
47746
|
}
|
|
47545
47747
|
//#endregion
|
|
47546
|
-
export { BuildFailedError, BuildLogger, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError,
|
|
47748
|
+
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, MissingInitInputsError, PUT_SIGNING_KEY_COMMAND, ServerDefinitionError, UNSIGNED_BUNDLES_NEXT_STEP, UNSIGNED_BUNDLES_WARNING, addSigningSecretToConfig, addSigningSecretToConfigText, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, 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, describeSecret, describeStoredSecret, encryptJson, ensureInstallPackages, failActiveStep, 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 };
|