@ckirg/corelib 0.1.22
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/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/RequestUnlimited-CLB36IJX.js +27 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +75 -0
- package/dist/bun-LDGTNQBK.js +41 -0
- package/dist/chunk-2DF4ADYX.js +9 -0
- package/dist/chunk-BAVE2JXI.js +207 -0
- package/dist/chunk-HOOAMOFY.js +120 -0
- package/dist/chunk-HPE2XSTW.js +202 -0
- package/dist/chunk-PESRDNPD.js +876 -0
- package/dist/cloudflare-IOVZ3QEK.js +102 -0
- package/dist/deno-MVIUW5GX.js +41 -0
- package/dist/flight-recorder-BCSVZvWQ.d.ts +92 -0
- package/dist/gcp-TRX5BADQ.js +39 -0
- package/dist/index.d.ts +879 -0
- package/dist/index.js +975 -0
- package/dist/lambda-CQXJKGN5.js +32 -0
- package/dist/node-6V4FDE5Z.js +41 -0
- package/dist/utils-Q4C2EEPD.js +42 -0
- package/package.json +62 -0
- package/scripts/postinstall.js +74 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
getAllEnv,
|
|
4
|
+
getCwd,
|
|
5
|
+
getDirname,
|
|
6
|
+
getMode,
|
|
7
|
+
getPlatform,
|
|
8
|
+
getRequire,
|
|
9
|
+
loggers_default,
|
|
10
|
+
readTextFileSync
|
|
11
|
+
} from "./chunk-BAVE2JXI.js";
|
|
12
|
+
import {
|
|
13
|
+
nextCid
|
|
14
|
+
} from "./chunk-2DF4ADYX.js";
|
|
15
|
+
import {
|
|
16
|
+
detectRuntime
|
|
17
|
+
} from "./chunk-HPE2XSTW.js";
|
|
18
|
+
|
|
19
|
+
// src/retrieve/RequestUnlimited.ts
|
|
20
|
+
import { deepmergeCustom as deepmergeCustom2 } from "deepmerge-ts";
|
|
21
|
+
import ky, { HTTPError } from "ky";
|
|
22
|
+
import { serializeError as serializeError3 } from "serialize-error";
|
|
23
|
+
|
|
24
|
+
// src/configs/ConfigManager.ts
|
|
25
|
+
import { EventEmitter } from "events";
|
|
26
|
+
import { deepmergeCustom } from "deepmerge-ts";
|
|
27
|
+
import { serializeError } from "serialize-error";
|
|
28
|
+
|
|
29
|
+
// src/configs/ConfigManager.json
|
|
30
|
+
var ConfigManager_default = {
|
|
31
|
+
markets: {
|
|
32
|
+
chromeVersion: "146",
|
|
33
|
+
nasdaq: {
|
|
34
|
+
statusEndpoint: "https://api.nasdaq.com/api/market-info",
|
|
35
|
+
monitor: {
|
|
36
|
+
liveIntervalSec: 10,
|
|
37
|
+
closedIntervalSec: 3600,
|
|
38
|
+
warnIntervalSec: 60
|
|
39
|
+
},
|
|
40
|
+
symbols: {
|
|
41
|
+
nasdaqListedUrl: "https://www.nasdaqtrader.com/dynamic/symdir/nasdaqlisted.txt",
|
|
42
|
+
otherListedUrl: "https://www.nasdaqtrader.com/dynamic/symdir/otherlisted.txt",
|
|
43
|
+
initialBackoffMs: 1e3,
|
|
44
|
+
maxRetryBackoffMs: 36e5,
|
|
45
|
+
maxFetchRetries: 10
|
|
46
|
+
},
|
|
47
|
+
quotes: {
|
|
48
|
+
concurrencyLimit: 5
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
retrieve: {
|
|
53
|
+
timeout: 5e4,
|
|
54
|
+
retry: {
|
|
55
|
+
limit: 5,
|
|
56
|
+
backoffLimit: 3e3
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/configs/ConfigUtils.ts
|
|
62
|
+
async function decryptConfig(encryptedData) {
|
|
63
|
+
const { getEnv } = await import("./utils-Q4C2EEPD.js");
|
|
64
|
+
const crypto = (await import("crypto")).default;
|
|
65
|
+
const password = getEnv("CORELIB_AES_PASSWORD");
|
|
66
|
+
if (!password) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"Decryption failed: CORELIB_AES_PASSWORD environment variable is not set."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const lines = encryptedData.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
72
|
+
if (lines.length < 2) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Invalid .enc file format. Expected IV on line 1 and Ciphertext on line 2."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const iv = Buffer.from(lines[0], "base64");
|
|
78
|
+
const ciphertext = Buffer.from(lines[1], "base64");
|
|
79
|
+
const key = Buffer.from(password, "hex");
|
|
80
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
|
81
|
+
let decrypted = decipher.update(ciphertext);
|
|
82
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
83
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/configs/ConfigManager.ts
|
|
87
|
+
var leafMerger = deepmergeCustom({
|
|
88
|
+
mergeArrays: false
|
|
89
|
+
});
|
|
90
|
+
var isSafeKey = (key) => !key.split(/[.-]/).some((p) => p === "__proto__" || p === "constructor" || p === "prototype");
|
|
91
|
+
function clearAndFill(target, source) {
|
|
92
|
+
for (const key of Object.keys(target)) {
|
|
93
|
+
if (!(key in source)) delete target[key];
|
|
94
|
+
}
|
|
95
|
+
for (const key of Object.keys(source)) {
|
|
96
|
+
const val = source[key];
|
|
97
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
98
|
+
if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
|
|
99
|
+
target[key] = {};
|
|
100
|
+
}
|
|
101
|
+
clearAndFill(
|
|
102
|
+
target[key],
|
|
103
|
+
val
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
target[key] = val;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function resolveAppName(cwd, pathMod, fileExists, readFile) {
|
|
111
|
+
let dir = cwd;
|
|
112
|
+
for (; ; ) {
|
|
113
|
+
if (fileExists(pathMod.join(dir, "pnpm-workspace.yaml")))
|
|
114
|
+
return pathMod.basename(dir);
|
|
115
|
+
const pkgPath = pathMod.join(dir, "package.json");
|
|
116
|
+
if (fileExists(pkgPath)) {
|
|
117
|
+
try {
|
|
118
|
+
const pkg = JSON.parse(readFile(pkgPath));
|
|
119
|
+
if (pkg.workspaces) return pathMod.basename(dir);
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const parent = pathMod.dirname(dir);
|
|
124
|
+
if (parent === dir) break;
|
|
125
|
+
dir = parent;
|
|
126
|
+
}
|
|
127
|
+
return pathMod.basename(cwd);
|
|
128
|
+
}
|
|
129
|
+
var ConfigManager = class _ConfigManager extends EventEmitter {
|
|
130
|
+
static instance;
|
|
131
|
+
_config = {};
|
|
132
|
+
_defaultsPath;
|
|
133
|
+
static _logger = loggers_default.child({ section: "ConfigManager" });
|
|
134
|
+
/** Single-flight guard for initialize(); null until first call, evicted on failure. */
|
|
135
|
+
_initPromise = null;
|
|
136
|
+
_isInitialized = false;
|
|
137
|
+
/** Async mutex: every async mutator chains onto this so they never interleave. */
|
|
138
|
+
_mutationChain = Promise.resolve();
|
|
139
|
+
/** The staging object of an in-flight staged build, or null. Lets a synchronous
|
|
140
|
+
* updateValue() between awaits survive the final clearAndFill swap. */
|
|
141
|
+
_inFlightTempConfig = null;
|
|
142
|
+
/** Throttle: emit the premature-read warning at most once per process (get()
|
|
143
|
+
* is called pervasively, so a per-call warn would flood dev logs). */
|
|
144
|
+
static _prematureWarnEmitted = false;
|
|
145
|
+
constructor() {
|
|
146
|
+
super();
|
|
147
|
+
const __dirname = getDirname();
|
|
148
|
+
let defaultsPath = __dirname;
|
|
149
|
+
try {
|
|
150
|
+
const { join } = getRequire()("node:path");
|
|
151
|
+
defaultsPath = join(__dirname, "ConfigManager.json");
|
|
152
|
+
} catch (_e) {
|
|
153
|
+
defaultsPath = `${__dirname}/ConfigManager.json`;
|
|
154
|
+
}
|
|
155
|
+
this._defaultsPath = defaultsPath;
|
|
156
|
+
clearAndFill(
|
|
157
|
+
this._config,
|
|
158
|
+
ConfigManager_default
|
|
159
|
+
);
|
|
160
|
+
globalThis.sysconfig = this._config;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Singleton accessor for the ConfigManager
|
|
164
|
+
*/
|
|
165
|
+
static getInstance() {
|
|
166
|
+
if (!_ConfigManager.instance) {
|
|
167
|
+
_ConfigManager.instance = new _ConfigManager();
|
|
168
|
+
}
|
|
169
|
+
return _ConfigManager.instance;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Retrieves a nested configuration value by string path (e.g., "db.mysql.port").
|
|
173
|
+
* @param {string} path - The dot-notation path to the configuration value.
|
|
174
|
+
* @returns The value at the specified path, or undefined if not found.
|
|
175
|
+
*/
|
|
176
|
+
get(path) {
|
|
177
|
+
if (!this._isInitialized && !_ConfigManager._prematureWarnEmitted && getMode() !== "production") {
|
|
178
|
+
_ConfigManager._prematureWarnEmitted = true;
|
|
179
|
+
_ConfigManager._logger.warn(
|
|
180
|
+
`ConfigManager.get("${path}") called before initialize() resolved; returning seeded default`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const keys = path.split(".");
|
|
184
|
+
let current = this._config;
|
|
185
|
+
for (const key of keys) {
|
|
186
|
+
if (current === null || typeof current !== "object" || !(key in current)) {
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
current = current[key];
|
|
190
|
+
}
|
|
191
|
+
return current;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Static helper to retrieve a configuration value from the singleton instance.
|
|
195
|
+
*/
|
|
196
|
+
static get(path) {
|
|
197
|
+
return _ConfigManager.getInstance().get(path);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Idempotent under concurrency: simultaneous calls share one in-flight
|
|
201
|
+
* promise (-01). On failure the promise is evicted so a transient error can
|
|
202
|
+
* self-heal on a later call. Signature unchanged (still awaitable Promise<void>).
|
|
203
|
+
*/
|
|
204
|
+
initialize(args) {
|
|
205
|
+
if (this._initPromise) return this._initPromise;
|
|
206
|
+
this._initPromise = this._enqueue(() => this.runInitSequence(args)).then(
|
|
207
|
+
() => {
|
|
208
|
+
this._isInitialized = true;
|
|
209
|
+
this._initPromise = null;
|
|
210
|
+
},
|
|
211
|
+
(error) => {
|
|
212
|
+
this._initPromise = null;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
return this._initPromise;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Main initialization sequence (formerly the body of initialize()).
|
|
220
|
+
* 1. Load Defaults 2. Detect CLI -C 3. Process Hierarchy 4. Env 5. CLI overrides.
|
|
221
|
+
*/
|
|
222
|
+
async runInitSequence(args) {
|
|
223
|
+
_ConfigManager._logger.trace("initialize: start");
|
|
224
|
+
const argv = args ?? (typeof process !== "undefined" && Array.isArray(process.argv) ? process.argv.slice(2) : []);
|
|
225
|
+
let configPath;
|
|
226
|
+
const overrides = {};
|
|
227
|
+
for (let i = 0; i < argv.length; i++) {
|
|
228
|
+
const tok = argv[i];
|
|
229
|
+
if (tok === "-C" || tok === "--config") {
|
|
230
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("-"))
|
|
231
|
+
configPath = argv[++i];
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (tok.startsWith("--config=")) {
|
|
235
|
+
configPath = tok.slice("--config=".length);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!tok.startsWith("--")) continue;
|
|
239
|
+
let key = tok.slice(2);
|
|
240
|
+
if (key === "" || key.startsWith("=")) continue;
|
|
241
|
+
let value;
|
|
242
|
+
const eq = key.indexOf("=");
|
|
243
|
+
if (eq > -1) {
|
|
244
|
+
value = key.slice(eq + 1);
|
|
245
|
+
key = key.slice(0, eq);
|
|
246
|
+
} else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
247
|
+
value = argv[++i];
|
|
248
|
+
} else {
|
|
249
|
+
value = true;
|
|
250
|
+
}
|
|
251
|
+
overrides[key] = value;
|
|
252
|
+
}
|
|
253
|
+
_ConfigManager._logger.trace("initialize: resolved", {
|
|
254
|
+
hasConfigPath: configPath != null,
|
|
255
|
+
overrideCount: Object.keys(overrides).length
|
|
256
|
+
});
|
|
257
|
+
const tempConfig = {};
|
|
258
|
+
this._inFlightTempConfig = tempConfig;
|
|
259
|
+
try {
|
|
260
|
+
this.loadDefaults(tempConfig);
|
|
261
|
+
if (configPath) {
|
|
262
|
+
const externalData = await this.fetchExternalConfig(configPath);
|
|
263
|
+
this.processHierarchy(externalData, tempConfig);
|
|
264
|
+
}
|
|
265
|
+
this.applyEnvOverrides(tempConfig);
|
|
266
|
+
this.applyCliOverrides(overrides, tempConfig);
|
|
267
|
+
clearAndFill(this._config, tempConfig);
|
|
268
|
+
_ConfigManager._logger.trace("initialize: committed", {
|
|
269
|
+
keys: Object.keys(this._config ?? {}).length
|
|
270
|
+
});
|
|
271
|
+
this.emit("initialized", this._config);
|
|
272
|
+
} finally {
|
|
273
|
+
this._inFlightTempConfig = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Serializes an async mutator against all other mutators (initialize +
|
|
278
|
+
* loadExternalConfig) via _mutationChain. The work runs after the previous
|
|
279
|
+
* op settles (success OR failure — `.then(work, work)` so a prior rejection
|
|
280
|
+
* never wedges the chain). The chain itself swallows results/errors so it
|
|
281
|
+
* stays resolved; the caller still receives this op's real result/error.
|
|
282
|
+
*
|
|
283
|
+
* Note: the chain's swallow-handler means a `loadExternalConfig` rejection is
|
|
284
|
+
* absorbed if the caller discards the returned promise. `initialize()` is
|
|
285
|
+
* different — it wraps this result in `_initPromise`, which is NOT on the
|
|
286
|
+
* chain, so an un-awaited failed `initialize()` surfaces as an unhandled
|
|
287
|
+
* rejection in Node strict mode. Callers should await/catch both.
|
|
288
|
+
*/
|
|
289
|
+
_enqueue(work) {
|
|
290
|
+
const run = this._mutationChain.then(work, work);
|
|
291
|
+
this._mutationChain = run.then(
|
|
292
|
+
() => void 0,
|
|
293
|
+
() => void 0
|
|
294
|
+
);
|
|
295
|
+
return run;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Retrieves the current active configuration object.
|
|
299
|
+
*/
|
|
300
|
+
getConfig() {
|
|
301
|
+
return this._config;
|
|
302
|
+
}
|
|
303
|
+
/** True once a call to initialize() has settled successfully. */
|
|
304
|
+
get isInitialized() {
|
|
305
|
+
return this._isInitialized;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Resolves when the first initialize() settles successfully (rejects if that
|
|
309
|
+
* in-flight initialize fails). If initialize() was never started — OR a prior
|
|
310
|
+
* attempt failed and was evicted — this resolves immediately and does NOT
|
|
311
|
+
* imply the manager is initialized; callers needing guaranteed initialization
|
|
312
|
+
* must call initialize() first and catch its rejection.
|
|
313
|
+
*/
|
|
314
|
+
whenReady() {
|
|
315
|
+
if (this._isInitialized) return Promise.resolve();
|
|
316
|
+
return this._initPromise ?? Promise.resolve();
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Public method to load and merge a new configuration from a URL or file path on demand.
|
|
320
|
+
* Respects the established configuration hierarchy and maintains Env overrides.
|
|
321
|
+
* @param source - The URL or local file path to the configuration.
|
|
322
|
+
*/
|
|
323
|
+
loadExternalConfig(source) {
|
|
324
|
+
return this._enqueue(() => this.loadExternalConfigInner(source));
|
|
325
|
+
}
|
|
326
|
+
async loadExternalConfigInner(source) {
|
|
327
|
+
try {
|
|
328
|
+
const externalData = await this.fetchExternalConfig(source);
|
|
329
|
+
const tempConfig = structuredClone(this._config);
|
|
330
|
+
this._inFlightTempConfig = tempConfig;
|
|
331
|
+
try {
|
|
332
|
+
this.processHierarchy(externalData, tempConfig);
|
|
333
|
+
this.applyEnvOverrides(tempConfig);
|
|
334
|
+
clearAndFill(this._config, tempConfig);
|
|
335
|
+
this.emit("configLoaded", this._config);
|
|
336
|
+
} finally {
|
|
337
|
+
this._inFlightTempConfig = null;
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
this.logError(
|
|
341
|
+
`Failed to load external config dynamically from ${source}`,
|
|
342
|
+
error
|
|
343
|
+
);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Loads the base ConfigManager.json from the local directory.
|
|
349
|
+
* Always seeds from the bundled JSON (available in all runtimes, including edge).
|
|
350
|
+
* If the JSON file is also found on disk, it replaces the bundled defaults.
|
|
351
|
+
*/
|
|
352
|
+
loadDefaults(target) {
|
|
353
|
+
let defaults = {
|
|
354
|
+
...ConfigManager_default
|
|
355
|
+
};
|
|
356
|
+
if (existsSync(this._defaultsPath)) {
|
|
357
|
+
try {
|
|
358
|
+
defaults = JSON.parse(readTextFileSync(this._defaultsPath));
|
|
359
|
+
} catch (e) {
|
|
360
|
+
this.logError("Failed to load defaults", e);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
clearAndFill(target, defaults);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Fetches and parses configuration from a URL or Local Path.
|
|
367
|
+
* Supports .enc decryption and dynamic confbox parsing by extension.
|
|
368
|
+
*/
|
|
369
|
+
async fetchExternalConfig(source) {
|
|
370
|
+
_ConfigManager._logger.trace("external config", {
|
|
371
|
+
source: source.startsWith("http") ? "http" : "file"
|
|
372
|
+
});
|
|
373
|
+
let content;
|
|
374
|
+
if (source.startsWith("http")) {
|
|
375
|
+
const { endPoint: endPoint2 } = await import("./RequestUnlimited-CLB36IJX.js");
|
|
376
|
+
const result = await endPoint2(source);
|
|
377
|
+
if (result.status === "error") {
|
|
378
|
+
throw new Error("Failed to fetch external config");
|
|
379
|
+
}
|
|
380
|
+
content = result.value.body;
|
|
381
|
+
} else {
|
|
382
|
+
content = readTextFileSync(source);
|
|
383
|
+
}
|
|
384
|
+
const lowerSource = source.toLowerCase();
|
|
385
|
+
if (lowerSource.endsWith(".enc")) {
|
|
386
|
+
_ConfigManager._logger.trace("decrypting .enc config");
|
|
387
|
+
const decrypted = this.validateConfigObject(
|
|
388
|
+
await decryptConfig(content),
|
|
389
|
+
source
|
|
390
|
+
);
|
|
391
|
+
_ConfigManager._logger.trace("decryption ok");
|
|
392
|
+
return decrypted;
|
|
393
|
+
}
|
|
394
|
+
const confbox = await import("confbox");
|
|
395
|
+
if (lowerSource.endsWith(".yaml") || lowerSource.endsWith(".yml")) {
|
|
396
|
+
return this.validateConfigObject(confbox.parseYAML(content), source);
|
|
397
|
+
}
|
|
398
|
+
if (lowerSource.endsWith(".toml")) {
|
|
399
|
+
return this.validateConfigObject(confbox.parseTOML(content), source);
|
|
400
|
+
}
|
|
401
|
+
if (lowerSource.endsWith(".json5")) {
|
|
402
|
+
return this.validateConfigObject(confbox.parseJSON5(content), source);
|
|
403
|
+
}
|
|
404
|
+
if (lowerSource.endsWith(".jsonc")) {
|
|
405
|
+
return this.validateConfigObject(confbox.parseJSONC(content), source);
|
|
406
|
+
}
|
|
407
|
+
if (lowerSource.endsWith(".ini")) {
|
|
408
|
+
return this.validateConfigObject(confbox.parseINI(content), source);
|
|
409
|
+
}
|
|
410
|
+
return this.validateConfigObject(confbox.parseJSON(content), source);
|
|
411
|
+
}
|
|
412
|
+
validateConfigObject(parsed, source) {
|
|
413
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Config from "${source}" must be a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return parsed;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Processes the specific hierarchy:
|
|
422
|
+
* commonAll -> [AppName].common -> [AppName].[platform] -> [AppName].[platform].[mode]
|
|
423
|
+
*/
|
|
424
|
+
processHierarchy(data, target) {
|
|
425
|
+
if (!data) return;
|
|
426
|
+
const appName = this.getAppName();
|
|
427
|
+
const platform = getPlatform();
|
|
428
|
+
const mode = getMode();
|
|
429
|
+
let layeredConfig = data.commonAll || {};
|
|
430
|
+
const appKey = Object.keys(data).find(
|
|
431
|
+
(k) => k.toLowerCase() === appName.toLowerCase()
|
|
432
|
+
);
|
|
433
|
+
const appSection = appKey ? data[appKey] : null;
|
|
434
|
+
if (appSection) {
|
|
435
|
+
if (appSection.common) {
|
|
436
|
+
layeredConfig = leafMerger(
|
|
437
|
+
layeredConfig,
|
|
438
|
+
appSection.common
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const platformSection = appSection[platform];
|
|
442
|
+
if (platformSection) {
|
|
443
|
+
const modeSection = platformSection[mode];
|
|
444
|
+
if (modeSection) {
|
|
445
|
+
layeredConfig = leafMerger(layeredConfig, modeSection);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const merged = leafMerger(target, layeredConfig);
|
|
450
|
+
clearAndFill(target, merged);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Maps CORELIB_ prefixed environment variables to config keys.
|
|
454
|
+
* Example: CORELIB_DB_PORT -> config.db.port
|
|
455
|
+
*/
|
|
456
|
+
applyEnvOverrides(target) {
|
|
457
|
+
const prefix = "CORELIB_";
|
|
458
|
+
const env = getAllEnv();
|
|
459
|
+
Object.keys(env).forEach((envKey) => {
|
|
460
|
+
if (envKey.startsWith(prefix)) {
|
|
461
|
+
const configPath = envKey.slice(prefix.length).toLowerCase().replace(/_/g, ".");
|
|
462
|
+
_ConfigManager._logger.trace("env override", { key: configPath });
|
|
463
|
+
const value = this.parseValue(env[envKey]);
|
|
464
|
+
this.setPath(target, configPath, value);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Maps the parsed Kebab-case CLI overrides to the config structure.
|
|
470
|
+
* Unsafe keys (__proto__/constructor/prototype segments) are dropped.
|
|
471
|
+
*/
|
|
472
|
+
applyCliOverrides(overrides, target) {
|
|
473
|
+
Object.keys(overrides).forEach((key) => {
|
|
474
|
+
if (key === "config") return;
|
|
475
|
+
if (!isSafeKey(key)) {
|
|
476
|
+
_ConfigManager._logger.warn(
|
|
477
|
+
`Dropped unsafe CLI override key "${key}" (prototype-pollution guard)`
|
|
478
|
+
);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const configPath = key.replace(/-/g, ".");
|
|
482
|
+
_ConfigManager._logger.trace("cli override", { key: configPath });
|
|
483
|
+
const value = this.parseValue(overrides[key]);
|
|
484
|
+
this.setPath(target, configPath, value);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Core update method that updates both the local object
|
|
489
|
+
* and the active globalThis object, then emits events.
|
|
490
|
+
*/
|
|
491
|
+
updateValue(path, value) {
|
|
492
|
+
this.setPath(this._config, path, value);
|
|
493
|
+
if (this._inFlightTempConfig !== null) {
|
|
494
|
+
this.setPath(this._inFlightTempConfig, path, value);
|
|
495
|
+
}
|
|
496
|
+
this.emit("change", { path, value });
|
|
497
|
+
this.emit(`change:${path}`, value);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Helper to set nested object values by string path (e.g., "db.mysql.port")
|
|
501
|
+
*/
|
|
502
|
+
setPath(obj, path, value) {
|
|
503
|
+
const keys = path.split(".");
|
|
504
|
+
let current = obj;
|
|
505
|
+
while (keys.length > 1) {
|
|
506
|
+
const key = keys.shift();
|
|
507
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
|
|
508
|
+
current[key] = {};
|
|
509
|
+
}
|
|
510
|
+
current = current[key];
|
|
511
|
+
}
|
|
512
|
+
current[keys[0]] = value;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Parses values from Env/CLI, automatically handling JSON strings for arrays/objects.
|
|
516
|
+
*/
|
|
517
|
+
parseValue(val) {
|
|
518
|
+
if (typeof val !== "string") return val;
|
|
519
|
+
if (val.startsWith("[") && val.endsWith("]") || val.startsWith("{") && val.endsWith("}")) {
|
|
520
|
+
try {
|
|
521
|
+
return JSON.parse(val);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
this.logError("Failed to parse complex JSON from CLI/Env flag", e);
|
|
524
|
+
return val;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (val.toLowerCase() === "true") return true;
|
|
528
|
+
if (val.toLowerCase() === "false") return false;
|
|
529
|
+
if (!Number.isNaN(Number(val)) && val.trim() !== "") return Number(val);
|
|
530
|
+
return val;
|
|
531
|
+
}
|
|
532
|
+
getAppName() {
|
|
533
|
+
try {
|
|
534
|
+
const runtime = detectRuntime();
|
|
535
|
+
if (runtime === "node" || runtime === "bun" || typeof import.meta !== "undefined" && import.meta.url && import.meta.url.startsWith("file:")) {
|
|
536
|
+
const path = getRequire()("node:path");
|
|
537
|
+
return resolveAppName(
|
|
538
|
+
getCwd(),
|
|
539
|
+
path,
|
|
540
|
+
(p) => existsSync(p),
|
|
541
|
+
(p) => readTextFileSync(p)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return "edge-app";
|
|
545
|
+
} catch (e) {
|
|
546
|
+
this.logError(
|
|
547
|
+
"Failed to get app name from cwd. Falling back to default-app",
|
|
548
|
+
e
|
|
549
|
+
);
|
|
550
|
+
return "default-app";
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Logs errors internally. If the global pino logger is available, it uses it
|
|
555
|
+
* along with `serialize-error` to structure the error object for Vector sidecars.
|
|
556
|
+
*/
|
|
557
|
+
logError(msg, error) {
|
|
558
|
+
const serialized = error ? serializeError(error) : void 0;
|
|
559
|
+
_ConfigManager._logger.error(msg, { error: serialized });
|
|
560
|
+
}
|
|
561
|
+
// --- Rust Integration Helpers ---
|
|
562
|
+
toJsonString() {
|
|
563
|
+
return JSON.stringify(this._config);
|
|
564
|
+
}
|
|
565
|
+
toBuffer() {
|
|
566
|
+
return Buffer.from(this.toJsonString());
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Test-only: reset init/concurrency state so single-flight and failed-init
|
|
570
|
+
* eviction can be exercised on the singleton. Does NOT clear _config.
|
|
571
|
+
*/
|
|
572
|
+
__resetForTests() {
|
|
573
|
+
this._initPromise = null;
|
|
574
|
+
this._isInitialized = false;
|
|
575
|
+
this._inFlightTempConfig = null;
|
|
576
|
+
this._mutationChain = Promise.resolve();
|
|
577
|
+
_ConfigManager._prematureWarnEmitted = false;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/retrieve/RequestResponseSerialize.ts
|
|
582
|
+
import { serializeError as serializeError2 } from "serialize-error";
|
|
583
|
+
var requestResponseSerializeLogger = loggers_default.child({
|
|
584
|
+
section: "RequestResponseSerialize"
|
|
585
|
+
});
|
|
586
|
+
async function serializeResponse(response) {
|
|
587
|
+
if (!response) return null;
|
|
588
|
+
const headers = {};
|
|
589
|
+
response.headers.forEach((v, k) => {
|
|
590
|
+
headers[k.toLowerCase()] = v;
|
|
591
|
+
});
|
|
592
|
+
let body;
|
|
593
|
+
const contentType = response.headers.get("content-type") || "";
|
|
594
|
+
try {
|
|
595
|
+
if (response.bodyUsed) {
|
|
596
|
+
body = "[Body already consumed]";
|
|
597
|
+
} else {
|
|
598
|
+
const rawText = await response.clone().text();
|
|
599
|
+
if (contentType.includes("application/json")) {
|
|
600
|
+
try {
|
|
601
|
+
body = JSON.parse(rawText);
|
|
602
|
+
} catch {
|
|
603
|
+
body = rawText;
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
body = rawText;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} catch (error) {
|
|
610
|
+
requestResponseSerializeLogger.warn("Failed to read response body", {
|
|
611
|
+
status: response.status,
|
|
612
|
+
url: response.url,
|
|
613
|
+
bodyUsed: response.bodyUsed,
|
|
614
|
+
error: serializeError2(error)
|
|
615
|
+
});
|
|
616
|
+
body = "[Error reading body]";
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
ok: response.ok,
|
|
620
|
+
status: response.status,
|
|
621
|
+
statusText: response.statusText,
|
|
622
|
+
headers,
|
|
623
|
+
url: response.url,
|
|
624
|
+
redirected: response.redirected,
|
|
625
|
+
type: response.type,
|
|
626
|
+
body
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
var RequestResponseSerialize = {
|
|
630
|
+
serialize: serializeResponse
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// src/retrieve/RequestUnlimited.ts
|
|
634
|
+
var requestUnlimitedLogger = loggers_default.child({ section: "RequestUnlimited" });
|
|
635
|
+
var customDeepmerge = deepmergeCustom2({
|
|
636
|
+
mergeArrays: false
|
|
637
|
+
});
|
|
638
|
+
var MAX_RETRY_LIMIT = 10;
|
|
639
|
+
var MIN_TIMEOUT_MS = 1e3;
|
|
640
|
+
var MAX_TIMEOUT_MS = 12e4;
|
|
641
|
+
var MAX_BACKOFF_LIMIT_MS = 6e4;
|
|
642
|
+
function clampNumber(value, min, max, fallback) {
|
|
643
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
644
|
+
return Math.min(max, Math.max(min, value));
|
|
645
|
+
}
|
|
646
|
+
function safeUrl(u) {
|
|
647
|
+
try {
|
|
648
|
+
const parsed = u instanceof URL ? u : new URL(u instanceof Request ? u.url : u);
|
|
649
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
650
|
+
} catch {
|
|
651
|
+
return "[unparseable url]";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function fullJitterDelay(attempt, backoffLimit) {
|
|
655
|
+
const base = Math.min(backoffLimit, 300 * 2 ** (attempt - 1));
|
|
656
|
+
const delayMs = Math.round(Math.random() * base);
|
|
657
|
+
requestUnlimitedLogger.trace("retry delay computed", {
|
|
658
|
+
attempt,
|
|
659
|
+
base,
|
|
660
|
+
cap: backoffLimit,
|
|
661
|
+
delayMs
|
|
662
|
+
});
|
|
663
|
+
return delayMs;
|
|
664
|
+
}
|
|
665
|
+
var DEFAULT_REQUEST_OPTIONS = {
|
|
666
|
+
timeout: 5e4,
|
|
667
|
+
throwHttpErrors: true,
|
|
668
|
+
retry: {
|
|
669
|
+
limit: 5,
|
|
670
|
+
methods: ["get", "post", "put", "delete", "patch"],
|
|
671
|
+
backoffLimit: 3e3,
|
|
672
|
+
shouldRetry: ({ error, retryCount }) => {
|
|
673
|
+
if (error instanceof HTTPError && error.response) {
|
|
674
|
+
const status = error.response.status;
|
|
675
|
+
if (status === 429 && retryCount <= 5) {
|
|
676
|
+
requestUnlimitedLogger.trace("retry decision", {
|
|
677
|
+
status,
|
|
678
|
+
willRetry: true
|
|
679
|
+
});
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
if (status >= 400 && status < 500) {
|
|
683
|
+
requestUnlimitedLogger.trace("retry decision: skip", {
|
|
684
|
+
status,
|
|
685
|
+
willRetry: false
|
|
686
|
+
});
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const willRetry = status >= 500;
|
|
690
|
+
requestUnlimitedLogger.trace(
|
|
691
|
+
willRetry ? "retry decision" : "retry decision: skip",
|
|
692
|
+
{ status, willRetry }
|
|
693
|
+
);
|
|
694
|
+
return willRetry;
|
|
695
|
+
}
|
|
696
|
+
requestUnlimitedLogger.trace("retry decision", { willRetry: true });
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
method: "get",
|
|
701
|
+
headers: {
|
|
702
|
+
"content-type": "application/json",
|
|
703
|
+
accept: "application/json"
|
|
704
|
+
},
|
|
705
|
+
hooks: {
|
|
706
|
+
// Retry logging is done per-call in endPoint() so it can carry the request's cid;
|
|
707
|
+
// a module-level default hook here has no access to that per-call correlation id.
|
|
708
|
+
beforeRetry: []
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
function toLowercaseKeys(obj) {
|
|
712
|
+
const newObj = {};
|
|
713
|
+
for (const key in obj) {
|
|
714
|
+
if (Object.hasOwn(obj, key) && obj[key] !== void 0) {
|
|
715
|
+
newObj[key.toLowerCase()] = obj[key];
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return newObj;
|
|
719
|
+
}
|
|
720
|
+
async function endPoint(url, options = {}) {
|
|
721
|
+
const normalizedDefaultHeaders = toLowercaseKeys(
|
|
722
|
+
DEFAULT_REQUEST_OPTIONS.headers || {}
|
|
723
|
+
);
|
|
724
|
+
const normalizedInputHeaders = options.headers ? toLowercaseKeys(options.headers) : {};
|
|
725
|
+
const { headers, hooks, ...remainingOptions } = options;
|
|
726
|
+
const cfgTimeout = clampNumber(
|
|
727
|
+
ConfigManager.get("retrieve.timeout"),
|
|
728
|
+
MIN_TIMEOUT_MS,
|
|
729
|
+
MAX_TIMEOUT_MS,
|
|
730
|
+
5e4
|
|
731
|
+
);
|
|
732
|
+
const cfgRetryLimit = clampNumber(
|
|
733
|
+
ConfigManager.get("retrieve.retry.limit"),
|
|
734
|
+
0,
|
|
735
|
+
MAX_RETRY_LIMIT,
|
|
736
|
+
5
|
|
737
|
+
);
|
|
738
|
+
const cfgBackoffLimit = clampNumber(
|
|
739
|
+
ConfigManager.get("retrieve.retry.backoffLimit"),
|
|
740
|
+
0,
|
|
741
|
+
MAX_BACKOFF_LIMIT_MS,
|
|
742
|
+
3e3
|
|
743
|
+
);
|
|
744
|
+
const cid = nextCid();
|
|
745
|
+
const startedAt = performance.now();
|
|
746
|
+
requestUnlimitedLogger.trace("endPoint: request", {
|
|
747
|
+
cid,
|
|
748
|
+
url: safeUrl(url),
|
|
749
|
+
timeout: cfgTimeout,
|
|
750
|
+
retryLimit: cfgRetryLimit,
|
|
751
|
+
backoffLimit: cfgBackoffLimit
|
|
752
|
+
});
|
|
753
|
+
const defaultRetry = DEFAULT_REQUEST_OPTIONS.retry;
|
|
754
|
+
const kyOptions = customDeepmerge(
|
|
755
|
+
DEFAULT_REQUEST_OPTIONS,
|
|
756
|
+
{
|
|
757
|
+
timeout: cfgTimeout,
|
|
758
|
+
retry: {
|
|
759
|
+
...defaultRetry,
|
|
760
|
+
limit: cfgRetryLimit,
|
|
761
|
+
backoffLimit: cfgBackoffLimit,
|
|
762
|
+
delay: (attempt) => fullJitterDelay(attempt, cfgBackoffLimit)
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
remainingOptions,
|
|
766
|
+
{
|
|
767
|
+
headers: { ...normalizedDefaultHeaders, ...normalizedInputHeaders },
|
|
768
|
+
hooks: {
|
|
769
|
+
beforeRetry: [
|
|
770
|
+
...DEFAULT_REQUEST_OPTIONS.hooks?.beforeRetry || [],
|
|
771
|
+
// Per-call so the retry trace correlates to this request's cid.
|
|
772
|
+
async ({ retryCount }) => {
|
|
773
|
+
requestUnlimitedLogger.trace("endPoint: retry", {
|
|
774
|
+
cid,
|
|
775
|
+
retryCount,
|
|
776
|
+
durationMs: performance.now() - startedAt
|
|
777
|
+
});
|
|
778
|
+
},
|
|
779
|
+
...hooks?.beforeRetry || []
|
|
780
|
+
]
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
);
|
|
784
|
+
try {
|
|
785
|
+
const responseObject = await ky(url, kyOptions);
|
|
786
|
+
const response = await serializeResponse(responseObject);
|
|
787
|
+
requestUnlimitedLogger.trace("endPoint: ok", {
|
|
788
|
+
cid,
|
|
789
|
+
durationMs: performance.now() - startedAt,
|
|
790
|
+
url: safeUrl(url),
|
|
791
|
+
status: responseObject.status
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
status: "success",
|
|
795
|
+
value: response
|
|
796
|
+
};
|
|
797
|
+
} catch (error) {
|
|
798
|
+
if (error instanceof HTTPError || error.response) {
|
|
799
|
+
const errorResponse = await serializeResponse(
|
|
800
|
+
// @ts-expect-error - ky error property
|
|
801
|
+
error.response
|
|
802
|
+
);
|
|
803
|
+
requestUnlimitedLogger.trace("endPoint: error", {
|
|
804
|
+
cid,
|
|
805
|
+
durationMs: performance.now() - startedAt,
|
|
806
|
+
status: errorResponse?.status,
|
|
807
|
+
errorMsg: "HTTP error"
|
|
808
|
+
});
|
|
809
|
+
requestUnlimitedLogger.warn("HTTP Error", {
|
|
810
|
+
cid,
|
|
811
|
+
status: errorResponse?.status,
|
|
812
|
+
url: url.toString()
|
|
813
|
+
});
|
|
814
|
+
return {
|
|
815
|
+
status: "error",
|
|
816
|
+
reason: errorResponse
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const serializedError = serializeError3(error);
|
|
820
|
+
requestUnlimitedLogger.trace("endPoint: error", {
|
|
821
|
+
cid,
|
|
822
|
+
durationMs: performance.now() - startedAt,
|
|
823
|
+
errorMsg: serializedError?.message ?? "internal/network error"
|
|
824
|
+
});
|
|
825
|
+
requestUnlimitedLogger.error("Internal/Network Error", {
|
|
826
|
+
cid,
|
|
827
|
+
error: serializedError
|
|
828
|
+
});
|
|
829
|
+
return {
|
|
830
|
+
status: "error",
|
|
831
|
+
reason: serializedError
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async function endPoints(urls, options = {}) {
|
|
836
|
+
const cid = nextCid();
|
|
837
|
+
const startedAt = performance.now();
|
|
838
|
+
requestUnlimitedLogger.trace("endPoints: batch", { cid, count: urls.length });
|
|
839
|
+
const promises = urls.map((url) => endPoint(url, options));
|
|
840
|
+
const results = await Promise.allSettled(promises);
|
|
841
|
+
const mapped = results.map((result) => {
|
|
842
|
+
if (result.status === "fulfilled") return result.value;
|
|
843
|
+
return {
|
|
844
|
+
status: "error",
|
|
845
|
+
reason: serializeError3(result.reason)
|
|
846
|
+
};
|
|
847
|
+
});
|
|
848
|
+
const ok = mapped.filter((r) => r.status === "success").length;
|
|
849
|
+
requestUnlimitedLogger.trace("endPoints: done", {
|
|
850
|
+
cid,
|
|
851
|
+
durationMs: performance.now() - startedAt,
|
|
852
|
+
ok,
|
|
853
|
+
failed: mapped.length - ok
|
|
854
|
+
});
|
|
855
|
+
return mapped;
|
|
856
|
+
}
|
|
857
|
+
var RequestUnlimited = {
|
|
858
|
+
defaults: DEFAULT_REQUEST_OPTIONS,
|
|
859
|
+
endPoint,
|
|
860
|
+
endPoints
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
export {
|
|
864
|
+
RequestResponseSerialize,
|
|
865
|
+
MAX_RETRY_LIMIT,
|
|
866
|
+
MIN_TIMEOUT_MS,
|
|
867
|
+
MAX_TIMEOUT_MS,
|
|
868
|
+
MAX_BACKOFF_LIMIT_MS,
|
|
869
|
+
clampNumber,
|
|
870
|
+
fullJitterDelay,
|
|
871
|
+
DEFAULT_REQUEST_OPTIONS,
|
|
872
|
+
endPoint,
|
|
873
|
+
endPoints,
|
|
874
|
+
RequestUnlimited,
|
|
875
|
+
ConfigManager
|
|
876
|
+
};
|