@h-ai/core 1.0.0-beta.5 → 1.0.0-beta.6
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/README.en.md +6 -3
- package/README.ja.md +6 -3
- package/README.md +6 -3
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -2
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-PXODJDJ5.js → chunk-JJPUZUAY.js} +3 -3
- package/dist/{chunk-PXODJDJ5.js.map → chunk-JJPUZUAY.js.map} +1 -1
- package/dist/{core-types-_JqVAGGT.d.ts → core-types-Irg7caTy.d.ts} +142 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +310 -88
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { installRequestLocales, installRequestContext, createCore, i18n, err, HaiConfigError, ok, RequestAccessConfigSchema, RequestAccessPolicySchema, AccessConditionSchema, requestContext, sanitize, emitLogRecord, sanitizeLogFields,
|
|
2
|
-
export { AccessConditionSchema, CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, RequestAccessConfigSchema, RequestAccessPolicySchema, TrustedProxiesSchema, err, ok } from './chunk-
|
|
1
|
+
import { installRequestLocales, installRequestContext, createCore, i18n, err, HaiConfigError, ok, RequestAccessConfigSchema, RequestAccessPolicySchema, AccessConditionSchema, requestContext, sanitize, emitLogRecord, sanitizeLogFields, CoreConfigSchema } from './chunk-JJPUZUAY.js';
|
|
2
|
+
export { AccessConditionSchema, CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, RequestAccessConfigSchema, RequestAccessPolicySchema, TrustedProxiesSchema, err, ok } from './chunk-JJPUZUAY.js';
|
|
3
3
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
4
4
|
import { Buffer } from 'buffer';
|
|
5
5
|
import { timingSafeEqual } from 'crypto';
|
|
@@ -7,7 +7,7 @@ import { existsSync, readFileSync, openSync, constants, fstatSync, readSync, clo
|
|
|
7
7
|
import path, { join } from 'path';
|
|
8
8
|
import { BlockList, isIP } from 'net';
|
|
9
9
|
import { networkInterfaces } from 'os';
|
|
10
|
-
import
|
|
10
|
+
import process4 from 'process';
|
|
11
11
|
import { parse } from 'yaml';
|
|
12
12
|
import { parseEnv } from 'util';
|
|
13
13
|
import { execSync } from 'child_process';
|
|
@@ -41,8 +41,8 @@ function addAddress(list, entry) {
|
|
|
41
41
|
list.addSubnet(address, bits, type);
|
|
42
42
|
}
|
|
43
43
|
function createClientIpResolver(trustedProxies = "auto") {
|
|
44
|
-
const entries = trustedProxies === "auto" ?
|
|
45
|
-
if (trustedProxies === "auto" &&
|
|
44
|
+
const entries = trustedProxies === "auto" ? process4.env.HAI_DEPLOY_PRIVATE_PROXY === "1" ? Object.values(networkInterfaces()).flatMap((interfaces) => interfaces?.filter((value) => !value.internal && value.cidr && !value.cidr.includes("%")).map((value) => value.cidr) ?? []) : [] : trustedProxies;
|
|
45
|
+
if (trustedProxies === "auto" && process4.env.HAI_DEPLOY_PRIVATE_PROXY === "1" && !entries.length)
|
|
46
46
|
throw new Error("Private proxy mode requires an isolated container network");
|
|
47
47
|
const proxies = new BlockList();
|
|
48
48
|
for (const entry of entries)
|
|
@@ -120,7 +120,7 @@ function createConditionCheck(input) {
|
|
|
120
120
|
const condition = AccessConditionSchema.parse(input);
|
|
121
121
|
return compileCondition(condition, condition.windows.length ? timeReader(condition.timezone) : void 0);
|
|
122
122
|
}
|
|
123
|
-
function loadPolicy(input = false, baseDirectory =
|
|
123
|
+
function loadPolicy(input = false, baseDirectory = process4.cwd()) {
|
|
124
124
|
const config2 = RequestAccessConfigSchema.parse(input);
|
|
125
125
|
if (config2 === false || !("file" in config2))
|
|
126
126
|
return config2;
|
|
@@ -207,11 +207,11 @@ function createRequestCheck(input = false) {
|
|
|
207
207
|
const nodes = [prefixes];
|
|
208
208
|
let node = prefixes;
|
|
209
209
|
const path2 = normalizeRequestPath(prefix ? pattern.slice(0, -3) || "/" : pattern);
|
|
210
|
-
for (const
|
|
211
|
-
let child = node.children.get(
|
|
210
|
+
for (const segment2 of path2.split("/").filter(Boolean)) {
|
|
211
|
+
let child = node.children.get(segment2);
|
|
212
212
|
if (!child) {
|
|
213
213
|
child = { children: /* @__PURE__ */ new Map(), rules: [] };
|
|
214
|
-
node.children.set(
|
|
214
|
+
node.children.set(segment2, child);
|
|
215
215
|
}
|
|
216
216
|
node = child;
|
|
217
217
|
nodes.push(node);
|
|
@@ -233,11 +233,11 @@ function createRequestCheck(input = false) {
|
|
|
233
233
|
}
|
|
234
234
|
if (pattern.endsWith("/**")) {
|
|
235
235
|
let node = prefixes;
|
|
236
|
-
for (const
|
|
237
|
-
let child = node.children.get(
|
|
236
|
+
for (const segment2 of pattern.slice(0, -3).split("/").filter(Boolean)) {
|
|
237
|
+
let child = node.children.get(segment2);
|
|
238
238
|
if (!child) {
|
|
239
239
|
child = { children: /* @__PURE__ */ new Map(), rules: [] };
|
|
240
|
-
node.children.set(
|
|
240
|
+
node.children.set(segment2, child);
|
|
241
241
|
}
|
|
242
242
|
node = child;
|
|
243
243
|
}
|
|
@@ -273,8 +273,8 @@ function createRequestCheck(input = false) {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
if (node.children.size) {
|
|
276
|
-
for (const
|
|
277
|
-
node = node.children.get(
|
|
276
|
+
for (const segment2 of path2.split("/").filter(Boolean)) {
|
|
277
|
+
node = node.children.get(segment2);
|
|
278
278
|
if (!node)
|
|
279
279
|
break;
|
|
280
280
|
for (const rule of node.rules) {
|
|
@@ -294,8 +294,8 @@ function createRequestCheck(input = false) {
|
|
|
294
294
|
return void 0;
|
|
295
295
|
let node = prefixes;
|
|
296
296
|
let mode = node.encryption?.get(method) ?? node.encryption?.get("*");
|
|
297
|
-
for (const
|
|
298
|
-
node = node.children.get(
|
|
297
|
+
for (const segment2 of path2.split("/").filter(Boolean)) {
|
|
298
|
+
node = node.children.get(segment2);
|
|
299
299
|
if (!node)
|
|
300
300
|
return mode;
|
|
301
301
|
mode ??= node.encryption?.get(method) ?? node.encryption?.get("*");
|
|
@@ -305,72 +305,229 @@ function createRequestCheck(input = false) {
|
|
|
305
305
|
return Object.assign(check, { encryption: hasEncryption ? encryption : () => void 0, requiresEncryption });
|
|
306
306
|
}
|
|
307
307
|
var access = { normalizeClientIp, createClientIpResolver, createConditionCheck, createRequestCheck, loadPolicy };
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
308
|
+
function object(value) {
|
|
309
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
310
|
+
}
|
|
311
|
+
function segment(value) {
|
|
312
|
+
return value.replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
313
|
+
}
|
|
314
|
+
function shapes(node, root, value, depth = 0) {
|
|
315
|
+
if (depth > 32)
|
|
316
|
+
return [];
|
|
317
|
+
const result = [node];
|
|
318
|
+
if (typeof node.$ref === "string" && node.$ref.startsWith("#/")) {
|
|
319
|
+
let target = root;
|
|
320
|
+
for (const key of node.$ref.slice(2).split("/").map((item) => item.replaceAll("~1", "/").replaceAll("~0", "~")))
|
|
321
|
+
target = object(target)?.[key];
|
|
322
|
+
if (object(target))
|
|
323
|
+
result.push(...shapes(object(target), root, value, depth + 1));
|
|
324
|
+
}
|
|
325
|
+
for (const keyword of ["anyOf", "oneOf", "allOf"]) {
|
|
326
|
+
if (!Array.isArray(node[keyword]))
|
|
327
|
+
continue;
|
|
328
|
+
for (const item of node[keyword]) {
|
|
329
|
+
const branch = object(item);
|
|
330
|
+
if (!branch)
|
|
331
|
+
continue;
|
|
332
|
+
const fields = object(branch.properties);
|
|
333
|
+
const input = object(value);
|
|
334
|
+
if (fields && input && Object.entries(fields).some(([key, field]) => input[key] !== void 0 && object(field)?.const !== void 0 && object(field).const !== input[key]))
|
|
335
|
+
continue;
|
|
336
|
+
result.push(...shapes(branch, root, value, depth + 1));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
function combined(nodes) {
|
|
342
|
+
const entries = nodes.map(object).filter((item) => item !== void 0);
|
|
343
|
+
return entries.length === 1 ? entries[0] : { anyOf: entries };
|
|
344
|
+
}
|
|
345
|
+
function scalar(text, node, root) {
|
|
346
|
+
const types = new Set(shapes(node, root).flatMap((item) => Array.isArray(item.type) ? item.type : typeof item.type === "string" ? [item.type] : []));
|
|
347
|
+
if (types.has("string") && [...types].every((type) => type === "string" || type === "null") && !(text === "null" && types.has("null")))
|
|
348
|
+
return text;
|
|
349
|
+
if (text === "")
|
|
312
350
|
return "";
|
|
313
351
|
try {
|
|
314
|
-
|
|
315
|
-
return parsed === void 0 ? value : parsed;
|
|
352
|
+
return parse(text) ?? (text === "null" ? null : text);
|
|
316
353
|
} catch {
|
|
317
|
-
return
|
|
354
|
+
return text;
|
|
318
355
|
}
|
|
319
356
|
}
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
357
|
+
function mergeEnv(layers) {
|
|
358
|
+
const values = {};
|
|
359
|
+
for (const layer of layers) {
|
|
360
|
+
for (const name of Object.keys(layer).sort((left, right) => left.length - right.length || left.localeCompare(right))) {
|
|
361
|
+
if (layer[name] === void 0)
|
|
362
|
+
continue;
|
|
363
|
+
if (name.startsWith("HAI_")) {
|
|
364
|
+
for (const previous of Object.keys(values)) {
|
|
365
|
+
if (previous.startsWith(`${name}_`))
|
|
366
|
+
delete values[previous];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
values[name] = layer[name];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return values;
|
|
325
373
|
}
|
|
326
|
-
function
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (!match)
|
|
340
|
-
break;
|
|
341
|
-
const [fullMatch, varName, defaultValue] = match;
|
|
342
|
-
const envValue = process3.env[varName];
|
|
343
|
-
if (envValue === void 0 && defaultValue === void 0) {
|
|
344
|
-
return err(HaiConfigError.CONFIG_ENV_VAR_MISSING, i18n.coreM("core_configEnvVarMissing", { params: { varName } }));
|
|
374
|
+
function parseEnvironment(text) {
|
|
375
|
+
const result = parseEnv(text);
|
|
376
|
+
const names = /* @__PURE__ */ new Set();
|
|
377
|
+
const assignments = /^(?:export\s+)?([A-Z_]\w*)\s*=\s*('[^']*'|"(?:[^"\\]|\\.)*"|`[^`]*`|[^\r\n]*)/gim;
|
|
378
|
+
for (const match of text.matchAll(assignments)) {
|
|
379
|
+
const name = match[1];
|
|
380
|
+
if (names.has(name))
|
|
381
|
+
throw new Error(`Duplicate environment variable: ${name}`);
|
|
382
|
+
names.add(name);
|
|
383
|
+
if (match[2].startsWith('"') && (match[2].includes('\\"') || match[2].includes("\\u0027"))) {
|
|
384
|
+
try {
|
|
385
|
+
result[name] = JSON.parse(match[2]);
|
|
386
|
+
} catch {
|
|
345
387
|
}
|
|
346
|
-
result = result.replace(fullMatch, () => envValue ?? defaultValue ?? "");
|
|
347
388
|
}
|
|
348
|
-
if (FULL_VAR_PATTERN.test(value))
|
|
349
|
-
return ok(parseEnvValue(result));
|
|
350
|
-
return ok(result);
|
|
351
389
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
390
|
+
return Object.fromEntries(Object.entries(result).filter((entry) => entry[1] !== void 0));
|
|
391
|
+
}
|
|
392
|
+
function stringifyEnv(values, options = {}) {
|
|
393
|
+
return `${Object.keys(values).sort().map((name) => {
|
|
394
|
+
if (!/^[A-Z_]\w*$/i.test(name))
|
|
395
|
+
throw new Error("Invalid environment variable name");
|
|
396
|
+
const value = values[name];
|
|
397
|
+
let literal;
|
|
398
|
+
if (!value.includes("'"))
|
|
399
|
+
literal = `'${value}'`;
|
|
400
|
+
else if (options.target === "compose")
|
|
401
|
+
literal = JSON.stringify(value).replaceAll("$", () => "$$");
|
|
402
|
+
else literal = JSON.stringify(value).replaceAll("'", "\\u0027");
|
|
403
|
+
return `${name}=${literal}`;
|
|
404
|
+
}).join("\n")}
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
407
|
+
function resolveEnvironment(options, literal = false) {
|
|
408
|
+
const environment = options.environment ?? process4.env;
|
|
409
|
+
const missingNames = /* @__PURE__ */ new Set();
|
|
410
|
+
let root;
|
|
411
|
+
try {
|
|
412
|
+
root = options.schema ? options.schema.toJSONSchema({ io: "input", unrepresentable: "any" }) : options.shape ?? {};
|
|
413
|
+
} catch {
|
|
414
|
+
root = {};
|
|
415
|
+
}
|
|
416
|
+
const prefix = `HAI_${segment(options.name)}`;
|
|
417
|
+
function visit(original, name, node, inheritedLiteral, depth, declaration = original) {
|
|
418
|
+
if (depth > 32)
|
|
419
|
+
throw new Error(name);
|
|
420
|
+
let value = original;
|
|
421
|
+
let fromEnvironment = inheritedLiteral;
|
|
422
|
+
if (environment[name] !== void 0) {
|
|
423
|
+
value = scalar(environment[name], node, root);
|
|
424
|
+
fromEnvironment = true;
|
|
425
|
+
}
|
|
426
|
+
const alternatives = shapes(node, root);
|
|
427
|
+
const discriminators = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const candidate of alternatives) {
|
|
429
|
+
for (const [key, field] of Object.entries(object(candidate.properties) ?? {})) {
|
|
430
|
+
if (shapes(object(field) ?? {}, root).some((item) => Object.hasOwn(item, "const")))
|
|
431
|
+
discriminators.set(key, [...discriminators.get(key) ?? [], field]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const selection = { ...object(value) };
|
|
435
|
+
for (const [key, fields] of discriminators) {
|
|
436
|
+
const text = environment[`${name}_${segment(key)}`];
|
|
437
|
+
if (text !== void 0)
|
|
438
|
+
selection[key] = scalar(text, combined(fields), root);
|
|
439
|
+
}
|
|
440
|
+
const candidates = shapes(node, root, Object.keys(selection).length ? selection : value);
|
|
441
|
+
const synthesized = value === void 0;
|
|
442
|
+
const valueType = typeof value;
|
|
443
|
+
const primitiveOverride = value === void 0 || candidates.some((candidate) => value !== null && candidate.type === valueType || candidate.const === value || value === null && candidate.type === "null");
|
|
444
|
+
if (primitiveOverride && !object(value) && !Array.isArray(value)) {
|
|
445
|
+
const declared = /* @__PURE__ */ new Set([...Object.keys(object(declaration) ?? {}), ...candidates.flatMap((candidate) => Object.keys(object(candidate.properties) ?? {}))]);
|
|
446
|
+
const specified = [...declared].filter((key) => Object.keys(environment).some((candidate) => environment[candidate] !== void 0 && (candidate === `${name}_${segment(key)}` || candidate.startsWith(`${name}_${segment(key)}_`))));
|
|
447
|
+
if (specified.length)
|
|
448
|
+
value = Object.fromEntries(specified.map((key) => [key, void 0]));
|
|
449
|
+
}
|
|
450
|
+
if (value === void 0 && Object.keys(environment).some((key) => environment[key] !== void 0 && key.startsWith(`${name}_`))) {
|
|
451
|
+
if (candidates.some((item) => item.type === "object" || object(item.properties)))
|
|
452
|
+
value = {};
|
|
359
453
|
}
|
|
360
|
-
|
|
454
|
+
const indexed = Object.keys(environment).filter((key) => environment[key] !== void 0 && key.startsWith(`${name}_`) && /^\d+(?:_|$)/.test(key.slice(name.length + 1)));
|
|
455
|
+
if (value === void 0 && indexed.length && candidates.some((candidate) => candidate.type === "array")) {
|
|
456
|
+
const defaults = candidates.find((candidate) => Array.isArray(candidate.default))?.default;
|
|
457
|
+
if (!Array.isArray(defaults))
|
|
458
|
+
throw new Error(indexed[0]);
|
|
459
|
+
value = structuredClone(defaults);
|
|
460
|
+
}
|
|
461
|
+
if (Array.isArray(value)) {
|
|
462
|
+
const itemShape = combined(candidates.map((item) => item.items));
|
|
463
|
+
for (const key of indexed) {
|
|
464
|
+
const index = key.slice(name.length + 1).split("_")[0];
|
|
465
|
+
if (/^\d+$/.test(index) && (String(Number(index)) !== index || Number(index) >= value.length))
|
|
466
|
+
throw new Error(key);
|
|
467
|
+
}
|
|
468
|
+
return value.map((item, index) => visit(item, `${name}_${index}`, itemShape, fromEnvironment, depth + 1, Array.isArray(declaration) ? declaration[index] : void 0));
|
|
469
|
+
}
|
|
470
|
+
const input = object(value);
|
|
471
|
+
if (input) {
|
|
472
|
+
const properties = /* @__PURE__ */ new Map();
|
|
473
|
+
for (const candidate of candidates) {
|
|
474
|
+
for (const [key, field] of Object.entries(object(candidate.properties) ?? {}))
|
|
475
|
+
properties.set(key, [...properties.get(key) ?? [], field]);
|
|
476
|
+
}
|
|
477
|
+
const declaredKeys = /* @__PURE__ */ new Set([...Object.keys(object(declaration) ?? {}), ...properties.keys()]);
|
|
478
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(input), ...[...declaredKeys].filter((key) => Object.keys(environment).some((nameInEnv) => environment[nameInEnv] !== void 0 && (nameInEnv === `${name}_${segment(key)}` || nameInEnv.startsWith(`${name}_${segment(key)}_`))))]);
|
|
479
|
+
const entries = [...keys].map((key) => {
|
|
480
|
+
if (["__proto__", "prototype", "constructor"].includes(key))
|
|
481
|
+
throw new Error(name);
|
|
482
|
+
return [key, visit(input[key], `${name}_${segment(key)}`, combined(properties.get(key) ?? candidates.map((candidate) => candidate.additionalProperties)), fromEnvironment, depth + 1, object(declaration)?.[key])];
|
|
483
|
+
}).filter(([, child]) => child !== void 0);
|
|
484
|
+
return synthesized && !entries.length ? void 0 : Object.fromEntries(entries);
|
|
485
|
+
}
|
|
486
|
+
if (typeof value !== "string" || fromEnvironment)
|
|
487
|
+
return value;
|
|
488
|
+
const full = /^\$\{[^}:]+(?::[^}]*)?\}$/.test(value);
|
|
489
|
+
const resolved = value.replace(/\$\{([^}:]+)(?::([^}]*))?\}/g, (_match, key, fallback) => {
|
|
490
|
+
if (environment[key] === void 0 && fallback === void 0 && options.missing !== "empty") {
|
|
491
|
+
missingNames.add(key);
|
|
492
|
+
throw new Error(key);
|
|
493
|
+
}
|
|
494
|
+
return environment[key] ?? fallback ?? "";
|
|
495
|
+
});
|
|
496
|
+
return full ? scalar(resolved, node, root) : resolved;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
return ok(visit(options.value, prefix, root, literal, 0));
|
|
500
|
+
} catch (failure) {
|
|
501
|
+
const name = failure instanceof Error && /^[A-Z_]\w*$/i.test(failure.message) ? failure.message : prefix;
|
|
502
|
+
if (!missingNames.has(name))
|
|
503
|
+
return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"), { fields: [name] });
|
|
504
|
+
return err(HaiConfigError.CONFIG_ENV_VAR_MISSING, i18n.coreM("core_configEnvVarMissing", { params: { varName: name } }));
|
|
361
505
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
506
|
+
}
|
|
507
|
+
function toEnv(options) {
|
|
508
|
+
const result = {};
|
|
509
|
+
const root = options.schema ? options.schema.toJSONSchema({ io: "input", unrepresentable: "any" }) : options.shape ?? {};
|
|
510
|
+
function visit(value, name, node) {
|
|
511
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
512
|
+
throw new Error("Invalid environment variable name");
|
|
513
|
+
if (value === void 0)
|
|
514
|
+
return;
|
|
515
|
+
const input = object(value);
|
|
516
|
+
const candidates = shapes(node, root, value);
|
|
517
|
+
const properties = Object.assign({}, ...candidates.map((candidate) => object(candidate.properties) ?? {}));
|
|
518
|
+
const dynamic = candidates.some((candidate) => candidate.additionalProperties === true || object(candidate.additionalProperties));
|
|
519
|
+
if (input && Object.keys(input).length && !dynamic && Object.keys(input).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key))) {
|
|
520
|
+
for (const [key, item] of Object.entries(input))
|
|
521
|
+
visit(item, `${name}_${segment(key)}`, object(properties[key]) ?? {});
|
|
522
|
+
} else {
|
|
523
|
+
result[name] = value === null || typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
369
524
|
}
|
|
370
|
-
return ok(results);
|
|
371
525
|
}
|
|
372
|
-
|
|
526
|
+
visit(options.value, options.prefix, root);
|
|
527
|
+
return result;
|
|
373
528
|
}
|
|
529
|
+
|
|
530
|
+
// src/functions/core-function-config.node.ts
|
|
374
531
|
var configCache = /* @__PURE__ */ new Map();
|
|
375
532
|
var watchEntries = /* @__PURE__ */ new Map();
|
|
376
533
|
function createNotLoadedError(name) {
|
|
@@ -399,20 +556,20 @@ function notifyWatchCallbacks(name, result) {
|
|
|
399
556
|
}
|
|
400
557
|
}
|
|
401
558
|
}
|
|
402
|
-
function loadYaml(name, filePath) {
|
|
559
|
+
function loadYaml(name, filePath, schema) {
|
|
403
560
|
if (!existsSync(filePath)) {
|
|
404
561
|
return err(HaiConfigError.CONFIG_FILE_NOT_FOUND, i18n.coreM("core_configFileNotExist", { params: { filePath } }));
|
|
405
562
|
}
|
|
406
563
|
try {
|
|
407
564
|
const content = readFileSync(filePath, "utf-8");
|
|
408
565
|
const parsed = parse(content);
|
|
409
|
-
return
|
|
566
|
+
return resolveEnvironment({ name, value: parsed, schema });
|
|
410
567
|
} catch (error) {
|
|
411
568
|
return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath } }), error);
|
|
412
569
|
}
|
|
413
570
|
}
|
|
414
571
|
function loadConfig(name, filePath, schema) {
|
|
415
|
-
const yamlResult = loadYaml(name, filePath);
|
|
572
|
+
const yamlResult = loadYaml(name, filePath, schema);
|
|
416
573
|
if (!yamlResult.success)
|
|
417
574
|
return yamlResult;
|
|
418
575
|
const parseResult = schema.safeParse(yamlResult.data);
|
|
@@ -433,12 +590,21 @@ function loadAndCache(name, filePath, schema) {
|
|
|
433
590
|
}
|
|
434
591
|
return result;
|
|
435
592
|
}
|
|
593
|
+
function loadConfigStructure(name, filePath, schemaHint) {
|
|
594
|
+
const result = loadYaml(name, filePath, schemaHint);
|
|
595
|
+
if (result.success)
|
|
596
|
+
configCache.set(name, { data: result.data, filePath, schemaHint, loadedAt: Date.now() });
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
436
599
|
function validateLoadedConfig(name, schema) {
|
|
437
600
|
const entry = configCache.get(name);
|
|
438
601
|
if (!entry) {
|
|
439
602
|
return err(createNotLoadedError(name));
|
|
440
603
|
}
|
|
441
|
-
const
|
|
604
|
+
const resolved = resolveEnvironment({ name, value: entry.data, schema }, true);
|
|
605
|
+
if (!resolved.success)
|
|
606
|
+
return resolved;
|
|
607
|
+
const parseResult = schema.safeParse(resolved.data);
|
|
442
608
|
if (!parseResult.success) {
|
|
443
609
|
return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"), parseResult.error.issues);
|
|
444
610
|
}
|
|
@@ -458,7 +624,7 @@ function reloadAndNotify(name) {
|
|
|
458
624
|
notifyWatchCallbacks(name, result2);
|
|
459
625
|
return result2;
|
|
460
626
|
}
|
|
461
|
-
const result = entry.schema ? loadConfig(name, entry.filePath, entry.schema) : loadYaml(name, entry.filePath);
|
|
627
|
+
const result = entry.schema ? loadConfig(name, entry.filePath, entry.schema) : loadYaml(name, entry.filePath, entry.schemaHint);
|
|
462
628
|
if (result.success) {
|
|
463
629
|
configCache.set(name, {
|
|
464
630
|
...entry,
|
|
@@ -548,10 +714,67 @@ function clearCache(name) {
|
|
|
548
714
|
}
|
|
549
715
|
}
|
|
550
716
|
var config = {
|
|
717
|
+
/**
|
|
718
|
+
* 合并从低到高的环境来源;高来源父项替换旧节点,同来源细项覆盖父 JSON。
|
|
719
|
+
* @param layers - 按优先级排列的字面变量。
|
|
720
|
+
* @returns 合并后的变量,可能含秘密。
|
|
721
|
+
* @example
|
|
722
|
+
* ```ts
|
|
723
|
+
* import { core } from '@h-ai/core'
|
|
724
|
+
* const value = core.config.mergeEnv([{ HAI_APP_NAME: 'base' }, { HAI_APP_NAME: 'test' }])
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
mergeEnv,
|
|
728
|
+
/**
|
|
729
|
+
* 解析 dotenv 字面文本,拒绝重复键,不执行 shell 或变量展开。
|
|
730
|
+
* @param text - dotenv 文本,可能包含秘密。
|
|
731
|
+
* @returns 字面变量;重复键抛出不含变量值的错误。
|
|
732
|
+
* @example
|
|
733
|
+
* ```ts
|
|
734
|
+
* import { core } from '@h-ai/core'
|
|
735
|
+
* const value = core.config.parseEnv('HAI_APP_NAME=Example\n')
|
|
736
|
+
* ```
|
|
737
|
+
*/
|
|
738
|
+
parseEnv: parseEnvironment,
|
|
739
|
+
/**
|
|
740
|
+
* 无损编码 dotenv,父变量排在细项前;含秘密时只能写入受保护文件。
|
|
741
|
+
* @param values - 字面变量,不进行脱敏。
|
|
742
|
+
* @param options - 输出语法目标,默认 Node dotenv。
|
|
743
|
+
* @param options.target - Compose 目标同时处理其美元符号插值规则。
|
|
744
|
+
* @returns dotenv 文本;非法变量名抛错。
|
|
745
|
+
* @example
|
|
746
|
+
* ```ts
|
|
747
|
+
* import { core } from '@h-ai/core'
|
|
748
|
+
* const value = core.config.stringifyEnv({ HAI_APP_NAME: 'Example' })
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
stringifyEnv,
|
|
752
|
+
/**
|
|
753
|
+
* 按已有结构或 Schema 解析父 JSON 和细项,不修改缓存或进程环境。
|
|
754
|
+
* @param options - 配置名、原始结构、变量、可选 Schema 和缺失插值规则。
|
|
755
|
+
* @returns 解析结果;调用方仍须执行最终 Schema 校验。
|
|
756
|
+
* @example
|
|
757
|
+
* ```ts
|
|
758
|
+
* import { core } from '@h-ai/core'
|
|
759
|
+
* const value = core.config.resolve({ name: 'app', value: { name: 'Example' }, environment: { HAI_APP_NAME: 'Test' } })
|
|
760
|
+
* ```
|
|
761
|
+
*/
|
|
762
|
+
resolve: (options) => resolveEnvironment(options),
|
|
763
|
+
/**
|
|
764
|
+
* 默认将对象展开为细项;数组、动态字典和空对象保留 JSON。
|
|
765
|
+
* @param options - 完整变量前缀、配置值和可选 Schema。
|
|
766
|
+
* @returns 字面环境变量,不脱敏;非法名称抛错。
|
|
767
|
+
* @example
|
|
768
|
+
* ```ts
|
|
769
|
+
* import { core } from '@h-ai/core'
|
|
770
|
+
* const value = core.config.toEnv({ prefix: 'HAI_APP', value: { name: 'Example', tags: ['demo'] } })
|
|
771
|
+
* ```
|
|
772
|
+
*/
|
|
773
|
+
toEnv,
|
|
551
774
|
/**
|
|
552
775
|
* 读取 dotenv 文件并按统一顺序合并,不修改进程环境。
|
|
553
776
|
*
|
|
554
|
-
* 优先级:已有进程环境 > .env.local > .env.<mode> > .env。
|
|
777
|
+
* 优先级:已有进程环境 > .env.<mode>.local > .env.local > .env.<mode> > .env。
|
|
555
778
|
* development/production 分别映射到 dev/prod;不存在的文件跳过。
|
|
556
779
|
* 返回值可能包含私有配置,仅在 Node 启动/构建边界注入,不下发浏览器。
|
|
557
780
|
* @param options - 文件所在目录及环境模式
|
|
@@ -570,18 +793,17 @@ var config = {
|
|
|
570
793
|
const mode = options.mode === "development" ? "dev" : options.mode === "production" ? "prod" : options.mode;
|
|
571
794
|
if (!/^[a-z0-9][\w-]*$/i.test(mode))
|
|
572
795
|
return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"));
|
|
573
|
-
const envDir = options.envDir ??
|
|
796
|
+
const envDir = options.envDir ?? process4.cwd();
|
|
574
797
|
try {
|
|
575
|
-
const
|
|
576
|
-
for (const name of [".env", `.env.${mode}`, ".env.local"]) {
|
|
798
|
+
const layers = [];
|
|
799
|
+
for (const name of [".env", `.env.${mode}`, ".env.local", `.env.${mode}.local`]) {
|
|
577
800
|
const file = join(envDir, name);
|
|
578
801
|
if (existsSync(file))
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
for (const name of Object.keys(merged)) {
|
|
582
|
-
if (process3.env[name] !== void 0)
|
|
583
|
-
merged[name] = process3.env[name];
|
|
802
|
+
layers.push(parseEnvironment(readFileSync(file, "utf8")));
|
|
584
803
|
}
|
|
804
|
+
const declared = new Set(layers.flatMap((layer) => Object.keys(layer)));
|
|
805
|
+
layers.push(Object.fromEntries(Object.entries(process4.env).filter(([name]) => declared.has(name) || name.startsWith("HAI_"))));
|
|
806
|
+
const merged = mergeEnv(layers);
|
|
585
807
|
return ok(merged);
|
|
586
808
|
} catch {
|
|
587
809
|
return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath: envDir } }));
|
|
@@ -781,14 +1003,14 @@ var config = {
|
|
|
781
1003
|
}
|
|
782
1004
|
};
|
|
783
1005
|
var nodeRequire = createRequire(import.meta.url);
|
|
784
|
-
if (
|
|
1006
|
+
if (process4.platform === "win32" && process4.stdout.isTTY) {
|
|
785
1007
|
try {
|
|
786
1008
|
execSync("chcp 65001 > nul", { stdio: "ignore" });
|
|
787
|
-
if (
|
|
788
|
-
|
|
1009
|
+
if (process4.stdout.setDefaultEncoding) {
|
|
1010
|
+
process4.stdout.setDefaultEncoding("utf8");
|
|
789
1011
|
}
|
|
790
|
-
if (
|
|
791
|
-
|
|
1012
|
+
if (process4.stderr.setDefaultEncoding) {
|
|
1013
|
+
process4.stderr.setDefaultEncoding("utf8");
|
|
792
1014
|
}
|
|
793
1015
|
} catch (_e) {
|
|
794
1016
|
}
|
|
@@ -1044,7 +1266,7 @@ function initCore(options = {}) {
|
|
|
1044
1266
|
});
|
|
1045
1267
|
}
|
|
1046
1268
|
} else {
|
|
1047
|
-
const result =
|
|
1269
|
+
const result = loadConfigStructure(item.name, item.filePath, options.configSchemas?.[item.name]);
|
|
1048
1270
|
if (result.success) {
|
|
1049
1271
|
logger2.info(`[core] Config loaded: ${item.name} <- ${item.filePath}`);
|
|
1050
1272
|
} else {
|