@h-ai/core 1.0.0-beta.4 → 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/dist/node.js CHANGED
@@ -1,5 +1,5 @@
1
- import { installRequestLocales, installRequestContext, createCore, i18n, err, HaiConfigError, ok, RequestAccessConfigSchema, RequestAccessPolicySchema, AccessConditionSchema, requestContext, sanitize, emitLogRecord, sanitizeLogFields, typeUtils, CoreConfigSchema } from './chunk-PXODJDJ5.js';
2
- export { AccessConditionSchema, CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, RequestAccessConfigSchema, RequestAccessPolicySchema, TrustedProxiesSchema, err, ok } from './chunk-PXODJDJ5.js';
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 process3 from 'process';
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" ? process3.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" && process3.env.HAI_DEPLOY_PRIVATE_PROXY === "1" && !entries.length)
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 = process3.cwd()) {
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 segment of path2.split("/").filter(Boolean)) {
211
- let child = node.children.get(segment);
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(segment, child);
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 segment of pattern.slice(0, -3).split("/").filter(Boolean)) {
237
- let child = node.children.get(segment);
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(segment, child);
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 segment of path2.split("/").filter(Boolean)) {
277
- node = node.children.get(segment);
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 segment of path2.split("/").filter(Boolean)) {
298
- node = node.children.get(segment);
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
- var ENV_VAR_PATTERN = /\$\{([^}:]+)(?::([^}]*))?\}/g;
309
- var FULL_VAR_PATTERN = /^\$\{[^}:]+(?::[^}]*)?\}$/;
310
- function parseEnvValue(value) {
311
- if (value === "")
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
- const parsed = parse(value);
315
- return parsed === void 0 ? value : parsed;
352
+ return parse(text) ?? (text === "null" ? null : text);
316
353
  } catch {
317
- return value;
354
+ return text;
318
355
  }
319
356
  }
320
- function normalizeEnvSegment(segment) {
321
- return segment.replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
322
- }
323
- function buildConventionEnvName(name, path2) {
324
- return ["HAI", name, ...path2].map(normalizeEnvSegment).filter(Boolean).join("_");
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 resolveEnv(value, name, path2 = []) {
327
- const conventionEnvValue = process3.env[buildConventionEnvName(name, path2)];
328
- if (conventionEnvValue !== void 0)
329
- return ok(parseEnvValue(conventionEnvValue));
330
- if (typeof value === "string") {
331
- ENV_VAR_PATTERN.lastIndex = 0;
332
- const hasExplicitEnv = ENV_VAR_PATTERN.test(value);
333
- ENV_VAR_PATTERN.lastIndex = 0;
334
- if (!hasExplicitEnv)
335
- return ok(value);
336
- let result = value;
337
- while (true) {
338
- const match = ENV_VAR_PATTERN.exec(value);
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
- if (Array.isArray(value)) {
353
- const results = [];
354
- for (const [index, item] of value.entries()) {
355
- const r = resolveEnv(item, name, [...path2, String(index)]);
356
- if (!r.success)
357
- return r;
358
- results.push(r.data);
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
- return ok(results);
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
- if (typeUtils.isObject(value)) {
363
- const results = {};
364
- for (const [k, v] of Object.entries(value)) {
365
- const r = resolveEnv(v, name, [...path2, k]);
366
- if (!r.success)
367
- return r;
368
- results[k] = r.data;
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
- return ok(value);
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 resolveEnv(parsed, name);
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 parseResult = schema.safeParse(entry.data);
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 ?? process3.cwd();
796
+ const envDir = options.envDir ?? process4.cwd();
574
797
  try {
575
- const merged = {};
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
- Object.assign(merged, parseEnv(readFileSync(file, "utf8")));
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 (process3.platform === "win32" && process3.stdout.isTTY) {
1006
+ if (process4.platform === "win32" && process4.stdout.isTTY) {
785
1007
  try {
786
1008
  execSync("chcp 65001 > nul", { stdio: "ignore" });
787
- if (process3.stdout.setDefaultEncoding) {
788
- process3.stdout.setDefaultEncoding("utf8");
1009
+ if (process4.stdout.setDefaultEncoding) {
1010
+ process4.stdout.setDefaultEncoding("utf8");
789
1011
  }
790
- if (process3.stderr.setDefaultEncoding) {
791
- process3.stderr.setDefaultEncoding("utf8");
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 = config.load(item.name, item.filePath);
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 {