@h-ai/core 1.0.0-beta.1 → 1.0.0-beta.10

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,13 +1,13 @@
1
- import { installRequestLocales, installRequestContext, createCore, i18n, err, HaiConfigError, ok, RequestAccessConfigSchema, RequestAccessPolicySchema, AccessConditionSchema, requestContext, sanitize, emitLogRecord, sanitizeLogFields, typeUtils, CoreConfigSchema } from './chunk-BEGTB6GL.js';
2
- export { AccessConditionSchema, CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, RequestAccessConfigSchema, RequestAccessPolicySchema, TrustedProxiesSchema, err, ok } from './chunk-BEGTB6GL.js';
1
+ import { installRequestLocales, installRequestContext, createCore, i18n, err, HaiConfigError, ok, RequestAccessConfigSchema, RequestAccessPolicySchema, AccessConditionSchema, requestContext, sanitize, emitLogRecord, sanitizeLogFields, CoreConfigSchema } from './chunk-MYNB4HIZ.js';
2
+ export { AccessConditionSchema, CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, RequestAccessConfigSchema, RequestAccessPolicySchema, TrustedProxiesSchema, err, ok } from './chunk-MYNB4HIZ.js';
3
3
  import { AsyncLocalStorage } from 'async_hooks';
4
4
  import { Buffer } from 'buffer';
5
5
  import { timingSafeEqual } from 'crypto';
6
6
  import { existsSync, readFileSync, openSync, constants, fstatSync, readSync, closeSync, readdirSync, watch } from 'fs';
7
- import path, { join } from 'path';
7
+ import path, { join, resolve, basename, dirname } 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 = {};
453
+ }
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);
359
460
  }
360
- return ok(results);
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,
@@ -482,15 +648,18 @@ function startFileWatcher(name) {
482
648
  callbacks: /* @__PURE__ */ new Set(),
483
649
  debounceTimer: null
484
650
  };
485
- const watcher = watch(entry.filePath, (eventType) => {
486
- if (eventType === "change") {
487
- if (watchEntry.debounceTimer)
488
- clearTimeout(watchEntry.debounceTimer);
489
- watchEntry.debounceTimer = setTimeout(() => {
490
- watchEntry.debounceTimer = null;
651
+ const filePath = resolve(entry.filePath);
652
+ const fileName = basename(filePath);
653
+ const watcher = watch(dirname(filePath), (_eventType, changedName) => {
654
+ if (changedName !== null && changedName.toString() !== fileName)
655
+ return;
656
+ if (watchEntry.debounceTimer)
657
+ clearTimeout(watchEntry.debounceTimer);
658
+ watchEntry.debounceTimer = setTimeout(() => {
659
+ watchEntry.debounceTimer = null;
660
+ if (watchEntries.get(name) === watchEntry)
491
661
  reloadAndNotify(name);
492
- }, 100);
493
- }
662
+ }, 100);
494
663
  });
495
664
  watchEntry.watcher = watcher;
496
665
  watchEntries.set(name, watchEntry);
@@ -509,7 +678,7 @@ function registerWatch(name, callback) {
509
678
  entry.callbacks.add(callback);
510
679
  return () => {
511
680
  const current = watchEntries.get(name);
512
- if (!current)
681
+ if (current !== entry)
513
682
  return;
514
683
  current.callbacks.delete(callback);
515
684
  if (current.callbacks.size === 0) {
@@ -548,10 +717,67 @@ function clearCache(name) {
548
717
  }
549
718
  }
550
719
  var config = {
720
+ /**
721
+ * 合并从低到高的环境来源;高来源父项替换旧节点,同来源细项覆盖父 JSON。
722
+ * @param layers - 按优先级排列的字面变量。
723
+ * @returns 合并后的变量,可能含秘密。
724
+ * @example
725
+ * ```ts
726
+ * import { core } from '@h-ai/core'
727
+ * const value = core.config.mergeEnv([{ HAI_APP_NAME: 'base' }, { HAI_APP_NAME: 'test' }])
728
+ * ```
729
+ */
730
+ mergeEnv,
731
+ /**
732
+ * 解析 dotenv 字面文本,拒绝重复键,不执行 shell 或变量展开。
733
+ * @param text - dotenv 文本,可能包含秘密。
734
+ * @returns 字面变量;重复键抛出不含变量值的错误。
735
+ * @example
736
+ * ```ts
737
+ * import { core } from '@h-ai/core'
738
+ * const value = core.config.parseEnv('HAI_APP_NAME=Example\n')
739
+ * ```
740
+ */
741
+ parseEnv: parseEnvironment,
742
+ /**
743
+ * 无损编码 dotenv,父变量排在细项前;含秘密时只能写入受保护文件。
744
+ * @param values - 字面变量,不进行脱敏。
745
+ * @param options - 输出语法目标,默认 Node dotenv。
746
+ * @param options.target - Compose 目标同时处理其美元符号插值规则。
747
+ * @returns dotenv 文本;非法变量名抛错。
748
+ * @example
749
+ * ```ts
750
+ * import { core } from '@h-ai/core'
751
+ * const value = core.config.stringifyEnv({ HAI_APP_NAME: 'Example' })
752
+ * ```
753
+ */
754
+ stringifyEnv,
755
+ /**
756
+ * 按已有结构或 Schema 解析父 JSON 和细项,不修改缓存或进程环境。
757
+ * @param options - 配置名、原始结构、变量、可选 Schema 和缺失插值规则。
758
+ * @returns 解析结果;调用方仍须执行最终 Schema 校验。
759
+ * @example
760
+ * ```ts
761
+ * import { core } from '@h-ai/core'
762
+ * const value = core.config.resolve({ name: 'app', value: { name: 'Example' }, environment: { HAI_APP_NAME: 'Test' } })
763
+ * ```
764
+ */
765
+ resolve: (options) => resolveEnvironment(options),
766
+ /**
767
+ * 默认将对象展开为细项;数组、动态字典和空对象保留 JSON。
768
+ * @param options - 完整变量前缀、配置值和可选 Schema。
769
+ * @returns 字面环境变量,不脱敏;非法名称抛错。
770
+ * @example
771
+ * ```ts
772
+ * import { core } from '@h-ai/core'
773
+ * const value = core.config.toEnv({ prefix: 'HAI_APP', value: { name: 'Example', tags: ['demo'] } })
774
+ * ```
775
+ */
776
+ toEnv,
551
777
  /**
552
778
  * 读取 dotenv 文件并按统一顺序合并,不修改进程环境。
553
779
  *
554
- * 优先级:已有进程环境 > .env.local > .env.<mode> > .env。
780
+ * 优先级:已有进程环境 > .env.<mode>.local > .env.local > .env.<mode> > .env。
555
781
  * development/production 分别映射到 dev/prod;不存在的文件跳过。
556
782
  * 返回值可能包含私有配置,仅在 Node 启动/构建边界注入,不下发浏览器。
557
783
  * @param options - 文件所在目录及环境模式
@@ -570,18 +796,17 @@ var config = {
570
796
  const mode = options.mode === "development" ? "dev" : options.mode === "production" ? "prod" : options.mode;
571
797
  if (!/^[a-z0-9][\w-]*$/i.test(mode))
572
798
  return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"));
573
- const envDir = options.envDir ?? process3.cwd();
799
+ const envDir = options.envDir ?? process4.cwd();
574
800
  try {
575
- const merged = {};
576
- for (const name of [".env", `.env.${mode}`, ".env.local"]) {
801
+ const layers = [];
802
+ for (const name of [".env", `.env.${mode}`, ".env.local", `.env.${mode}.local`]) {
577
803
  const file = join(envDir, name);
578
804
  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];
805
+ layers.push(parseEnvironment(readFileSync(file, "utf8")));
584
806
  }
807
+ const declared = new Set(layers.flatMap((layer) => Object.keys(layer)));
808
+ layers.push(Object.fromEntries(Object.entries(process4.env).filter(([name]) => declared.has(name) || name.startsWith("HAI_"))));
809
+ const merged = mergeEnv(layers);
585
810
  return ok(merged);
586
811
  } catch {
587
812
  return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath: envDir } }));
@@ -730,7 +955,8 @@ var config = {
730
955
  /**
731
956
  * 监听配置文件变更并自动重载。
732
957
  *
733
- * 文件变更时自动重新加载并调用回调。配置未加载时立即回调 NOT_LOADED 错误。
958
+ * 普通写入、原子替换及删除重建均触发重载;失败保留最近成功的缓存并通知错误。
959
+ * 配置未加载时立即回调 NOT_LOADED 错误。
734
960
  *
735
961
  * @param name - 配置名称
736
962
  * @param callback - 配置变更回调,接收新配置或错误
@@ -781,14 +1007,14 @@ var config = {
781
1007
  }
782
1008
  };
783
1009
  var nodeRequire = createRequire(import.meta.url);
784
- if (process3.platform === "win32" && process3.stdout.isTTY) {
1010
+ if (process4.platform === "win32" && process4.stdout.isTTY) {
785
1011
  try {
786
1012
  execSync("chcp 65001 > nul", { stdio: "ignore" });
787
- if (process3.stdout.setDefaultEncoding) {
788
- process3.stdout.setDefaultEncoding("utf8");
1013
+ if (process4.stdout.setDefaultEncoding) {
1014
+ process4.stdout.setDefaultEncoding("utf8");
789
1015
  }
790
- if (process3.stderr.setDefaultEncoding) {
791
- process3.stderr.setDefaultEncoding("utf8");
1016
+ if (process4.stderr.setDefaultEncoding) {
1017
+ process4.stderr.setDefaultEncoding("utf8");
792
1018
  }
793
1019
  } catch (_e) {
794
1020
  }
@@ -1044,7 +1270,7 @@ function initCore(options = {}) {
1044
1270
  });
1045
1271
  }
1046
1272
  } else {
1047
- const result = config.load(item.name, item.filePath);
1273
+ const result = loadConfigStructure(item.name, item.filePath, options.configSchemas?.[item.name]);
1048
1274
  if (result.success) {
1049
1275
  logger2.info(`[core] Config loaded: ${item.name} <- ${item.filePath}`);
1050
1276
  } else {