@h-ai/core 1.0.0-beta.2 → 1.0.0-beta.20

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-WU6HUTBO.js';
2
+ export { AccessConditionSchema, CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, RequestAccessConfigSchema, RequestAccessPolicySchema, TrustedProxiesSchema, err, ok } from './chunk-WU6HUTBO.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';
@@ -36,13 +36,14 @@ function addAddress(list, entry) {
36
36
  return;
37
37
  }
38
38
  const bits = Number(prefix);
39
- if (!/^\d+$/.test(prefix) || bits < 0 || bits > (family === 4 ? 32 : 128))
39
+ if (!/^\d+$/.test(prefix) || bits < 0 || bits > (family === 4 ? 32 : 128)) {
40
40
  throw new Error("Invalid CIDR prefix");
41
+ }
41
42
  list.addSubnet(address, bits, type);
42
43
  }
43
44
  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)
45
+ 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;
46
+ if (trustedProxies === "auto" && process4.env.HAI_DEPLOY_PRIVATE_PROXY === "1" && !entries.length)
46
47
  throw new Error("Private proxy mode requires an isolated container network");
47
48
  const proxies = new BlockList();
48
49
  for (const entry of entries)
@@ -73,8 +74,22 @@ function createClientIpResolver(trustedProxies = "auto") {
73
74
  };
74
75
  }
75
76
  function timeReader(timezone) {
76
- const formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
77
- const weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
77
+ const formatter = new Intl.DateTimeFormat("en-US", {
78
+ timeZone: timezone,
79
+ weekday: "short",
80
+ hour: "2-digit",
81
+ minute: "2-digit",
82
+ hourCycle: "h23"
83
+ });
84
+ const weekdays = [
85
+ "Mon",
86
+ "Tue",
87
+ "Wed",
88
+ "Thu",
89
+ "Fri",
90
+ "Sat",
91
+ "Sun"
92
+ ];
78
93
  let lastMinute = Number.NaN;
79
94
  let local;
80
95
  return (now) => {
@@ -120,7 +135,7 @@ function createConditionCheck(input) {
120
135
  const condition = AccessConditionSchema.parse(input);
121
136
  return compileCondition(condition, condition.windows.length ? timeReader(condition.timezone) : void 0);
122
137
  }
123
- function loadPolicy(input = false, baseDirectory = process3.cwd()) {
138
+ function loadPolicy(input = false, baseDirectory = process4.cwd()) {
124
139
  const config2 = RequestAccessConfigSchema.parse(input);
125
140
  if (config2 === false || !("file" in config2))
126
141
  return config2;
@@ -180,8 +195,12 @@ function normalizeRequestPath(value) {
180
195
  }
181
196
  function createRequestCheck(input = false) {
182
197
  const policy = loadPolicy(input);
183
- if (policy === false)
184
- return Object.assign(() => allowed, { encryption: () => void 0, requiresEncryption: false });
198
+ if (policy === false) {
199
+ return Object.assign(() => allowed, {
200
+ encryption: () => void 0,
201
+ requiresEncryption: false
202
+ });
203
+ }
185
204
  const clocks = /* @__PURE__ */ new Map();
186
205
  const compile = (condition, id) => {
187
206
  let readTime;
@@ -193,12 +212,29 @@ function createRequestCheck(input = false) {
193
212
  };
194
213
  const globalCheck = policy.global ? compile(policy.global) : () => allowed;
195
214
  const exact = /* @__PURE__ */ new Map();
196
- const prefixes = { children: /* @__PURE__ */ new Map(), rules: [] };
197
- const allMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "CONNECT", "TRACE", "*"];
215
+ const prefixes = {
216
+ children: /* @__PURE__ */ new Map(),
217
+ rules: []
218
+ };
219
+ const allMethods = [
220
+ "GET",
221
+ "HEAD",
222
+ "POST",
223
+ "PUT",
224
+ "PATCH",
225
+ "DELETE",
226
+ "OPTIONS",
227
+ "CONNECT",
228
+ "TRACE",
229
+ "*"
230
+ ];
198
231
  let requiresEncryption = false;
199
232
  let hasEncryption = false;
200
233
  for (const rule of policy.rules) {
201
- const compiled = { methods: new Set(rule.methods), check: compile(rule, rule.id) };
234
+ const compiled = {
235
+ methods: new Set(rule.methods),
236
+ check: compile(rule, rule.id)
237
+ };
202
238
  for (const pattern of new Set(rule.paths)) {
203
239
  if (rule.encryption) {
204
240
  hasEncryption = true;
@@ -207,11 +243,14 @@ function createRequestCheck(input = false) {
207
243
  const nodes = [prefixes];
208
244
  let node = prefixes;
209
245
  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);
246
+ for (const segment2 of path2.split("/").filter(Boolean)) {
247
+ let child = node.children.get(segment2);
212
248
  if (!child) {
213
- child = { children: /* @__PURE__ */ new Map(), rules: [] };
214
- node.children.set(segment, child);
249
+ child = {
250
+ children: /* @__PURE__ */ new Map(),
251
+ rules: []
252
+ };
253
+ node.children.set(segment2, child);
215
254
  }
216
255
  node = child;
217
256
  nodes.push(node);
@@ -233,11 +272,14 @@ function createRequestCheck(input = false) {
233
272
  }
234
273
  if (pattern.endsWith("/**")) {
235
274
  let node = prefixes;
236
- for (const segment of pattern.slice(0, -3).split("/").filter(Boolean)) {
237
- let child = node.children.get(segment);
275
+ for (const segment2 of pattern.slice(0, -3).split("/").filter(Boolean)) {
276
+ let child = node.children.get(segment2);
238
277
  if (!child) {
239
- child = { children: /* @__PURE__ */ new Map(), rules: [] };
240
- node.children.set(segment, child);
278
+ child = {
279
+ children: /* @__PURE__ */ new Map(),
280
+ rules: []
281
+ };
282
+ node.children.set(segment2, child);
241
283
  }
242
284
  node = child;
243
285
  }
@@ -273,8 +315,8 @@ function createRequestCheck(input = false) {
273
315
  }
274
316
  }
275
317
  if (node.children.size) {
276
- for (const segment of path2.split("/").filter(Boolean)) {
277
- node = node.children.get(segment);
318
+ for (const segment2 of path2.split("/").filter(Boolean)) {
319
+ node = node.children.get(segment2);
278
320
  if (!node)
279
321
  break;
280
322
  for (const rule of node.rules) {
@@ -294,87 +336,303 @@ function createRequestCheck(input = false) {
294
336
  return void 0;
295
337
  let node = prefixes;
296
338
  let mode = node.encryption?.get(method) ?? node.encryption?.get("*");
297
- for (const segment of path2.split("/").filter(Boolean)) {
298
- node = node.children.get(segment);
339
+ for (const segment2 of path2.split("/").filter(Boolean)) {
340
+ node = node.children.get(segment2);
299
341
  if (!node)
300
342
  return mode;
301
343
  mode ??= node.encryption?.get(method) ?? node.encryption?.get("*");
302
344
  }
303
345
  return mode ?? node?.exactEncryption?.get(method) ?? node?.exactEncryption?.get("*");
304
346
  };
305
- return Object.assign(check, { encryption: hasEncryption ? encryption : () => void 0, requiresEncryption });
347
+ return Object.assign(check, {
348
+ encryption: hasEncryption ? encryption : () => void 0,
349
+ requiresEncryption
350
+ });
351
+ }
352
+ var access = {
353
+ normalizeClientIp,
354
+ createClientIpResolver,
355
+ createConditionCheck,
356
+ createRequestCheck,
357
+ loadPolicy
358
+ };
359
+ function object(value) {
360
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
361
+ }
362
+ function segment(value) {
363
+ return value.replace(/[^a-z0-9]+/gi, "_").replace(/^_|_$/g, "").toUpperCase();
364
+ }
365
+ function shapes(node, root, value, depth = 0) {
366
+ if (depth > 32)
367
+ return [];
368
+ const result = [node];
369
+ if (typeof node.$ref === "string" && node.$ref.startsWith("#/")) {
370
+ let target = root;
371
+ for (const key of node.$ref.slice(2).split("/").map((item) => item.replaceAll("~1", "/").replaceAll("~0", "~")))
372
+ target = object(target)?.[key];
373
+ if (object(target))
374
+ result.push(...shapes(object(target), root, value, depth + 1));
375
+ }
376
+ for (const keyword of ["anyOf", "oneOf", "allOf"]) {
377
+ if (!Array.isArray(node[keyword]))
378
+ continue;
379
+ for (const item of node[keyword]) {
380
+ const branch = object(item);
381
+ if (!branch)
382
+ continue;
383
+ const fields = object(branch.properties);
384
+ const input = object(value);
385
+ if (fields && input && Object.entries(fields).some(([key, field]) => input[key] !== void 0 && object(field)?.const !== void 0 && object(field).const !== input[key]))
386
+ continue;
387
+ result.push(...shapes(branch, root, value, depth + 1));
388
+ }
389
+ }
390
+ return result;
306
391
  }
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 === "")
392
+ function combined(nodes) {
393
+ const entries = nodes.map(object).filter((item) => item !== void 0);
394
+ return entries.length === 1 ? entries[0] : { anyOf: entries };
395
+ }
396
+ function scalar(text, node, root) {
397
+ const types = new Set(shapes(node, root).flatMap((item) => Array.isArray(item.type) ? item.type : typeof item.type === "string" ? [item.type] : []));
398
+ if (types.has("string") && [...types].every((type) => type === "string" || type === "null") && !(text === "null" && types.has("null")))
399
+ return text;
400
+ if (text === "")
312
401
  return "";
313
402
  try {
314
- const parsed = parse(value);
315
- return parsed === void 0 ? value : parsed;
403
+ return parse(text) ?? (text === "null" ? null : text);
316
404
  } catch {
317
- return value;
405
+ return text;
406
+ }
407
+ }
408
+ function mergeEnv(layers) {
409
+ const values = {};
410
+ for (const layer of layers) {
411
+ for (const name of Object.keys(layer).sort((left, right) => left.length - right.length || left.localeCompare(right))) {
412
+ if (layer[name] === void 0)
413
+ continue;
414
+ if (name.startsWith("HAI_")) {
415
+ for (const previous of Object.keys(values)) {
416
+ if (previous !== "HAI_SERV_REFRESH_COOKIE_SECURE" && previous.startsWith(`${name}_`))
417
+ delete values[previous];
418
+ }
419
+ }
420
+ values[name] = layer[name];
421
+ }
318
422
  }
423
+ return values;
319
424
  }
320
- function normalizeEnvSegment(segment) {
321
- return segment.replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
425
+ function parseEnvironment(text) {
426
+ const result = parseEnv(text);
427
+ const names = /* @__PURE__ */ new Set();
428
+ const assignments = /^[ \t]*(?:export[ \t]+)?([A-Z_]\w*)[ \t]*=[ \t]*('[^']*'|"(?:[^"\\]|\\.)*"|`[^`]*`|[^\r\n]*)/gim;
429
+ for (const match of text.matchAll(assignments)) {
430
+ const name = match[1];
431
+ if (names.has(name))
432
+ throw new Error(`Duplicate environment variable: ${name}`);
433
+ names.add(name);
434
+ if (match[2].startsWith('"') && (match[2].includes('\\"') || match[2].includes("\\u0027"))) {
435
+ try {
436
+ result[name] = JSON.parse(match[2]);
437
+ } catch {
438
+ }
439
+ }
440
+ }
441
+ return Object.fromEntries(Object.entries(result).filter((entry) => entry[1] !== void 0));
322
442
  }
323
- function buildConventionEnvName(name, path2) {
324
- return ["HAI", name, ...path2].map(normalizeEnvSegment).filter(Boolean).join("_");
443
+ function stringifyEnv(values, options = {}) {
444
+ return `${Object.keys(values).sort().map((name) => {
445
+ if (!/^[A-Z_]\w*$/i.test(name))
446
+ throw new Error("Invalid environment variable name");
447
+ const value = values[name];
448
+ let literal;
449
+ if (!value.includes("'"))
450
+ literal = `'${value}'`;
451
+ else if (options.target === "compose")
452
+ literal = JSON.stringify(value).replaceAll("$", () => "$$");
453
+ else literal = JSON.stringify(value).replaceAll("'", "\\u0027");
454
+ return `${name}=${literal}`;
455
+ }).join("\n")}
456
+ `;
325
457
  }
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)
458
+ function resolveEnvironment(options, literal = false) {
459
+ const environment = options.environment ?? process4.env;
460
+ const missingNames = /* @__PURE__ */ new Set();
461
+ let root;
462
+ try {
463
+ root = options.schema ? options.schema.toJSONSchema({ io: "input", unrepresentable: "any" }) : options.shape ?? {};
464
+ } catch {
465
+ root = {};
466
+ }
467
+ const prefix = `HAI_${segment(options.name)}`;
468
+ function visit(original, name, node, inheritedLiteral, depth, declaration = original) {
469
+ if (depth > 32)
470
+ throw new Error(name);
471
+ let value = original;
472
+ let fromEnvironment = inheritedLiteral;
473
+ if (environment[name] !== void 0) {
474
+ value = scalar(environment[name], node, root);
475
+ fromEnvironment = true;
476
+ }
477
+ const alternatives = shapes(node, root);
478
+ const discriminators = /* @__PURE__ */ new Map();
479
+ for (const candidate of alternatives) {
480
+ for (const [key, field] of Object.entries(object(candidate.properties) ?? {})) {
481
+ if (shapes(object(field) ?? {}, root).some((item) => Object.hasOwn(item, "const"))) {
482
+ discriminators.set(key, [
483
+ ...discriminators.get(key) ?? [],
484
+ field
485
+ ]);
486
+ }
487
+ }
488
+ }
489
+ const selection = {
490
+ ...object(value)
491
+ };
492
+ for (const [key, fields] of discriminators) {
493
+ const text = environment[`${name}_${segment(key)}`];
494
+ if (text !== void 0)
495
+ selection[key] = scalar(text, combined(fields), root);
496
+ }
497
+ const candidates = shapes(node, root, Object.keys(selection).length ? selection : value);
498
+ const synthesized = value === void 0;
499
+ const valueType = typeof value;
500
+ const primitiveOverride = value === void 0 || candidates.some((candidate) => value !== null && candidate.type === valueType || candidate.const === value || value === null && candidate.type === "null");
501
+ if (primitiveOverride && !object(value) && !Array.isArray(value)) {
502
+ const declared = /* @__PURE__ */ new Set([
503
+ ...Object.keys(object(declaration) ?? {}),
504
+ ...candidates.flatMap((candidate) => Object.keys(object(candidate.properties) ?? {}))
505
+ ]);
506
+ const specified = [...declared].filter((key) => Object.keys(environment).some((candidate) => environment[candidate] !== void 0 && (candidate === `${name}_${segment(key)}` || candidate.startsWith(`${name}_${segment(key)}_`))));
507
+ if (specified.length)
508
+ value = Object.fromEntries(specified.map((key) => [key, void 0]));
509
+ }
510
+ if (value === void 0 && Object.keys(environment).some((key) => environment[key] !== void 0 && key.startsWith(`${name}_`))) {
511
+ if (candidates.some((item) => item.type === "object" || object(item.properties)))
512
+ value = {};
513
+ }
514
+ const indexed = Object.keys(environment).filter((key) => environment[key] !== void 0 && key.startsWith(`${name}_`) && /^\d+(?:_|$)/.test(key.slice(name.length + 1)));
515
+ if (value === void 0 && indexed.length && candidates.some((candidate) => candidate.type === "array")) {
516
+ const defaults = candidates.find((candidate) => Array.isArray(candidate.default))?.default;
517
+ if (!Array.isArray(defaults))
518
+ throw new TypeError(indexed[0]);
519
+ value = structuredClone(defaults);
520
+ }
521
+ if (Array.isArray(value)) {
522
+ const itemShape = combined(candidates.map((item) => item.items));
523
+ for (const key of indexed) {
524
+ const index = key.slice(name.length + 1).split("_")[0];
525
+ if (/^\d+$/.test(index) && (String(Number(index)) !== index || Number(index) >= value.length))
526
+ throw new Error(key);
527
+ }
528
+ return value.map((item, index) => visit(item, `${name}_${index}`, itemShape, fromEnvironment, depth + 1, Array.isArray(declaration) ? declaration[index] : void 0));
529
+ }
530
+ const input = object(value);
531
+ if (input) {
532
+ const properties = /* @__PURE__ */ new Map();
533
+ for (const candidate of candidates) {
534
+ for (const [key, field] of Object.entries(object(candidate.properties) ?? {})) {
535
+ properties.set(key, [
536
+ ...properties.get(key) ?? [],
537
+ field
538
+ ]);
539
+ }
540
+ }
541
+ const declaredKeys = /* @__PURE__ */ new Set([
542
+ ...Object.keys(object(declaration) ?? {}),
543
+ ...properties.keys()
544
+ ]);
545
+ const keys = /* @__PURE__ */ new Set([
546
+ ...Object.keys(input),
547
+ ...[...declaredKeys].filter((key) => Object.keys(environment).some((nameInEnv) => environment[nameInEnv] !== void 0 && (nameInEnv === `${name}_${segment(key)}` || nameInEnv.startsWith(`${name}_${segment(key)}_`))))
548
+ ]);
549
+ const entries = [...keys].map((key) => {
550
+ if (["__proto__", "prototype", "constructor"].includes(key))
551
+ throw new Error(name);
552
+ return [
553
+ key,
554
+ visit(input[key], `${name}_${segment(key)}`, combined(properties.get(key) ?? candidates.map((candidate) => candidate.additionalProperties)), fromEnvironment, depth + 1, object(declaration)?.[key])
555
+ ];
556
+ }).filter(([, child]) => child !== void 0);
557
+ return synthesized && !entries.length ? void 0 : Object.fromEntries(entries);
558
+ }
559
+ if (typeof value !== "string" || fromEnvironment)
560
+ return value;
561
+ const parts = [];
562
+ let consumed = 0;
563
+ let search = 0;
564
+ let full = false;
565
+ while (search < value.length) {
566
+ const start = value.indexOf("${", search);
567
+ if (start < 0)
568
+ break;
569
+ if (value[start + 2] === ":" || value[start + 2] === "}") {
570
+ search = start + 2;
571
+ continue;
572
+ }
573
+ const end = value.indexOf("}", start + 2);
574
+ if (end < 0)
340
575
  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 } }));
576
+ const expression = value.slice(start + 2, end);
577
+ const separator = expression.indexOf(":");
578
+ const key = separator < 0 ? expression : expression.slice(0, separator);
579
+ const fallback = separator < 0 ? void 0 : expression.slice(separator + 1);
580
+ if (environment[key] === void 0 && fallback === void 0 && options.missing !== "empty") {
581
+ missingNames.add(key);
582
+ throw new Error(key);
345
583
  }
346
- result = result.replace(fullMatch, () => envValue ?? defaultValue ?? "");
584
+ parts.push(value.slice(consumed, start), environment[key] ?? fallback ?? "");
585
+ consumed = end + 1;
586
+ search = consumed;
587
+ full = start === 0 && consumed === value.length;
347
588
  }
348
- if (FULL_VAR_PATTERN.test(value))
349
- return ok(parseEnvValue(result));
350
- return ok(result);
589
+ const resolved = parts.join("") + value.slice(consumed);
590
+ return full ? scalar(resolved, node, root) : resolved;
351
591
  }
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);
592
+ try {
593
+ return ok(visit(options.value, prefix, root, literal, 0));
594
+ } catch (failure) {
595
+ const name = failure instanceof Error && /^[A-Z_]\w*$/i.test(failure.message) ? failure.message : prefix;
596
+ if (!missingNames.has(name)) {
597
+ return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"), {
598
+ fields: [name]
599
+ });
359
600
  }
360
- return ok(results);
601
+ return err(HaiConfigError.CONFIG_ENV_VAR_MISSING, i18n.coreM("core_configEnvVarMissing", {
602
+ params: { varName: name }
603
+ }));
361
604
  }
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;
605
+ }
606
+ function toEnv(options) {
607
+ const result = {};
608
+ const root = options.schema ? options.schema.toJSONSchema({ io: "input", unrepresentable: "any" }) : options.shape ?? {};
609
+ function visit(value, name, node) {
610
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name))
611
+ throw new Error("Invalid environment variable name");
612
+ if (value === void 0)
613
+ return;
614
+ const input = object(value);
615
+ const candidates = shapes(node, root, value);
616
+ const properties = Object.assign({}, ...candidates.map((candidate) => object(candidate.properties) ?? {}));
617
+ const dynamic = candidates.some((candidate) => candidate.additionalProperties === true || object(candidate.additionalProperties));
618
+ if (input && Object.keys(input).length && !dynamic && Object.keys(input).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key))) {
619
+ for (const [key, item] of Object.entries(input))
620
+ visit(item, `${name}_${segment(key)}`, object(properties[key]) ?? {});
621
+ } else {
622
+ result[name] = value === null || typeof value === "object" ? JSON.stringify(value) : String(value);
369
623
  }
370
- return ok(results);
371
624
  }
372
- return ok(value);
625
+ visit(options.value, options.prefix, root);
626
+ return result;
373
627
  }
628
+
629
+ // src/functions/core-function-config.node.ts
374
630
  var configCache = /* @__PURE__ */ new Map();
375
631
  var watchEntries = /* @__PURE__ */ new Map();
376
632
  function createNotLoadedError(name) {
377
- const result = err(HaiConfigError.CONFIG_NOT_LOADED, i18n.coreM("core_configNotLoaded", { params: { name } }));
633
+ const result = err(HaiConfigError.CONFIG_NOT_LOADED, i18n.coreM("core_configNotLoaded", {
634
+ params: { name }
635
+ }));
378
636
  if (!result.success)
379
637
  return result.error;
380
638
  throw new Error("unreachable");
@@ -399,20 +657,24 @@ function notifyWatchCallbacks(name, result) {
399
657
  }
400
658
  }
401
659
  }
402
- function loadYaml(name, filePath) {
660
+ function loadYaml(name, filePath, schema) {
403
661
  if (!existsSync(filePath)) {
404
- return err(HaiConfigError.CONFIG_FILE_NOT_FOUND, i18n.coreM("core_configFileNotExist", { params: { filePath } }));
662
+ return err(HaiConfigError.CONFIG_FILE_NOT_FOUND, i18n.coreM("core_configFileNotExist", {
663
+ params: { filePath }
664
+ }));
405
665
  }
406
666
  try {
407
667
  const content = readFileSync(filePath, "utf-8");
408
668
  const parsed = parse(content);
409
- return resolveEnv(parsed, name);
669
+ return resolveEnvironment({ name, value: parsed, schema });
410
670
  } catch (error) {
411
- return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath } }), error);
671
+ return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", {
672
+ params: { filePath }
673
+ }), error);
412
674
  }
413
675
  }
414
676
  function loadConfig(name, filePath, schema) {
415
- const yamlResult = loadYaml(name, filePath);
677
+ const yamlResult = loadYaml(name, filePath, schema);
416
678
  if (!yamlResult.success)
417
679
  return yamlResult;
418
680
  const parseResult = schema.safeParse(yamlResult.data);
@@ -433,12 +695,27 @@ function loadAndCache(name, filePath, schema) {
433
695
  }
434
696
  return result;
435
697
  }
698
+ function loadConfigStructure(name, filePath, schemaHint) {
699
+ const result = loadYaml(name, filePath, schemaHint);
700
+ if (result.success) {
701
+ configCache.set(name, {
702
+ data: result.data,
703
+ filePath,
704
+ schemaHint,
705
+ loadedAt: Date.now()
706
+ });
707
+ }
708
+ return result;
709
+ }
436
710
  function validateLoadedConfig(name, schema) {
437
711
  const entry = configCache.get(name);
438
712
  if (!entry) {
439
713
  return err(createNotLoadedError(name));
440
714
  }
441
- const parseResult = schema.safeParse(entry.data);
715
+ const resolved = resolveEnvironment({ name, value: entry.data, schema }, true);
716
+ if (!resolved.success)
717
+ return resolved;
718
+ const parseResult = schema.safeParse(resolved.data);
442
719
  if (!parseResult.success) {
443
720
  return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"), parseResult.error.issues);
444
721
  }
@@ -458,7 +735,7 @@ function reloadAndNotify(name) {
458
735
  notifyWatchCallbacks(name, result2);
459
736
  return result2;
460
737
  }
461
- const result = entry.schema ? loadConfig(name, entry.filePath, entry.schema) : loadYaml(name, entry.filePath);
738
+ const result = entry.schema ? loadConfig(name, entry.filePath, entry.schema) : loadYaml(name, entry.filePath, entry.schemaHint);
462
739
  if (result.success) {
463
740
  configCache.set(name, {
464
741
  ...entry,
@@ -482,15 +759,18 @@ function startFileWatcher(name) {
482
759
  callbacks: /* @__PURE__ */ new Set(),
483
760
  debounceTimer: null
484
761
  };
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;
762
+ const filePath = resolve(entry.filePath);
763
+ const fileName = basename(filePath);
764
+ const watcher = watch(dirname(filePath), (_eventType, changedName) => {
765
+ if (changedName !== null && changedName.toString() !== fileName)
766
+ return;
767
+ if (watchEntry.debounceTimer)
768
+ clearTimeout(watchEntry.debounceTimer);
769
+ watchEntry.debounceTimer = setTimeout(() => {
770
+ watchEntry.debounceTimer = null;
771
+ if (watchEntries.get(name) === watchEntry)
491
772
  reloadAndNotify(name);
492
- }, 100);
493
- }
773
+ }, 100);
494
774
  });
495
775
  watchEntry.watcher = watcher;
496
776
  watchEntries.set(name, watchEntry);
@@ -509,7 +789,7 @@ function registerWatch(name, callback) {
509
789
  entry.callbacks.add(callback);
510
790
  return () => {
511
791
  const current = watchEntries.get(name);
512
- if (!current)
792
+ if (current !== entry)
513
793
  return;
514
794
  current.callbacks.delete(callback);
515
795
  if (current.callbacks.size === 0) {
@@ -548,10 +828,67 @@ function clearCache(name) {
548
828
  }
549
829
  }
550
830
  var config = {
831
+ /**
832
+ * 合并从低到高的环境来源;高来源父项替换旧节点,同来源细项覆盖父 JSON。
833
+ * @param layers - 按优先级排列的字面变量。
834
+ * @returns 合并后的变量,可能含秘密。
835
+ * @example
836
+ * ```ts
837
+ * import { core } from '@h-ai/core'
838
+ * const value = core.config.mergeEnv([{ HAI_APP_NAME: 'base' }, { HAI_APP_NAME: 'test' }])
839
+ * ```
840
+ */
841
+ mergeEnv,
842
+ /**
843
+ * 解析 dotenv 字面文本,拒绝重复键,不执行 shell 或变量展开。
844
+ * @param text - dotenv 文本,可能包含秘密。
845
+ * @returns 字面变量;重复键抛出不含变量值的错误。
846
+ * @example
847
+ * ```ts
848
+ * import { core } from '@h-ai/core'
849
+ * const value = core.config.parseEnv('HAI_APP_NAME=Example\n')
850
+ * ```
851
+ */
852
+ parseEnv: parseEnvironment,
853
+ /**
854
+ * 无损编码 dotenv,父变量排在细项前;含秘密时只能写入受保护文件。
855
+ * @param values - 字面变量,不进行脱敏。
856
+ * @param options - 输出语法目标,默认 Node dotenv。
857
+ * @param options.target - Compose 目标同时处理其美元符号插值规则。
858
+ * @returns dotenv 文本;非法变量名抛错。
859
+ * @example
860
+ * ```ts
861
+ * import { core } from '@h-ai/core'
862
+ * const value = core.config.stringifyEnv({ HAI_APP_NAME: 'Example' })
863
+ * ```
864
+ */
865
+ stringifyEnv,
866
+ /**
867
+ * 按已有结构或 Schema 解析父 JSON 和细项,不修改缓存或进程环境。
868
+ * @param options - 配置名、原始结构、变量、可选 Schema 和缺失插值规则。
869
+ * @returns 解析结果;调用方仍须执行最终 Schema 校验。
870
+ * @example
871
+ * ```ts
872
+ * import { core } from '@h-ai/core'
873
+ * const value = core.config.resolve({ name: 'app', value: { name: 'Example' }, environment: { HAI_APP_NAME: 'Test' } })
874
+ * ```
875
+ */
876
+ resolve: (options) => resolveEnvironment(options),
877
+ /**
878
+ * 默认将对象展开为细项;数组、动态字典和空对象保留 JSON。
879
+ * @param options - 完整变量前缀、配置值和可选 Schema。
880
+ * @returns 字面环境变量,不脱敏;非法名称抛错。
881
+ * @example
882
+ * ```ts
883
+ * import { core } from '@h-ai/core'
884
+ * const value = core.config.toEnv({ prefix: 'HAI_APP', value: { name: 'Example', tags: ['demo'] } })
885
+ * ```
886
+ */
887
+ toEnv,
551
888
  /**
552
889
  * 读取 dotenv 文件并按统一顺序合并,不修改进程环境。
553
890
  *
554
- * 优先级:已有进程环境 > .env.local > .env.<mode> > .env。
891
+ * 优先级:已有进程环境 > .env.<mode>.local > .env.local > .env.<mode> > .env。
555
892
  * development/production 分别映射到 dev/prod;不存在的文件跳过。
556
893
  * 返回值可能包含私有配置,仅在 Node 启动/构建边界注入,不下发浏览器。
557
894
  * @param options - 文件所在目录及环境模式
@@ -570,21 +907,27 @@ var config = {
570
907
  const mode = options.mode === "development" ? "dev" : options.mode === "production" ? "prod" : options.mode;
571
908
  if (!/^[a-z0-9][\w-]*$/i.test(mode))
572
909
  return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"));
573
- const envDir = options.envDir ?? process3.cwd();
910
+ const envDir = options.envDir ?? process4.cwd();
574
911
  try {
575
- const merged = {};
576
- for (const name of [".env", `.env.${mode}`, ".env.local"]) {
912
+ const layers = [];
913
+ for (const name of [
914
+ ".env",
915
+ `.env.${mode}`,
916
+ ".env.local",
917
+ `.env.${mode}.local`
918
+ ]) {
577
919
  const file = join(envDir, name);
578
920
  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];
921
+ layers.push(parseEnvironment(readFileSync(file, "utf8")));
584
922
  }
923
+ const declared = new Set(layers.flatMap((layer) => Object.keys(layer)));
924
+ layers.push(Object.fromEntries(Object.entries(process4.env).filter(([name]) => declared.has(name) || name.startsWith("HAI_"))));
925
+ const merged = mergeEnv(layers);
585
926
  return ok(merged);
586
927
  } catch {
587
- return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath: envDir } }));
928
+ return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", {
929
+ params: { filePath: envDir }
930
+ }));
588
931
  }
589
932
  },
590
933
  /**
@@ -664,7 +1007,9 @@ var config = {
664
1007
  getOrThrow(name) {
665
1008
  const data = this.get(name);
666
1009
  if (data === void 0) {
667
- throw new Error(i18n.coreM("core_configNotLoaded", { params: { name } }));
1010
+ throw new Error(i18n.coreM("core_configNotLoaded", {
1011
+ params: { name }
1012
+ }));
668
1013
  }
669
1014
  return data;
670
1015
  },
@@ -730,7 +1075,8 @@ var config = {
730
1075
  /**
731
1076
  * 监听配置文件变更并自动重载。
732
1077
  *
733
- * 文件变更时自动重新加载并调用回调。配置未加载时立即回调 NOT_LOADED 错误。
1078
+ * 普通写入、原子替换及删除重建均触发重载;失败保留最近成功的缓存并通知错误。
1079
+ * 配置未加载时立即回调 NOT_LOADED 错误。
734
1080
  *
735
1081
  * @param name - 配置名称
736
1082
  * @param callback - 配置变更回调,接收新配置或错误
@@ -781,14 +1127,14 @@ var config = {
781
1127
  }
782
1128
  };
783
1129
  var nodeRequire = createRequire(import.meta.url);
784
- if (process3.platform === "win32" && process3.stdout.isTTY) {
1130
+ if (process4.platform === "win32" && process4.stdout.isTTY) {
785
1131
  try {
786
1132
  execSync("chcp 65001 > nul", { stdio: "ignore" });
787
- if (process3.stdout.setDefaultEncoding) {
788
- process3.stdout.setDefaultEncoding("utf8");
1133
+ if (process4.stdout.setDefaultEncoding) {
1134
+ process4.stdout.setDefaultEncoding("utf8");
789
1135
  }
790
- if (process3.stderr.setDefaultEncoding) {
791
- process3.stderr.setDefaultEncoding("utf8");
1136
+ if (process4.stderr.setDefaultEncoding) {
1137
+ process4.stderr.setDefaultEncoding("utf8");
792
1138
  }
793
1139
  } catch (_e) {
794
1140
  }
@@ -806,8 +1152,12 @@ function configureLogger(config2) {
806
1152
  globalFormat = config2.format;
807
1153
  outputRevision++;
808
1154
  }
809
- if (config2.context)
810
- globalContext = { ...globalContext, ...config2.context };
1155
+ if (config2.context) {
1156
+ globalContext = {
1157
+ ...globalContext,
1158
+ ...config2.context
1159
+ };
1160
+ }
811
1161
  if (config2.redact) {
812
1162
  globalRedact = [...config2.redact];
813
1163
  outputRevision++;
@@ -830,11 +1180,21 @@ function wrapPino(resolvePino, resolveContext) {
830
1180
  const output = resolvePino();
831
1181
  if (!output.isLevelEnabled(level))
832
1182
  return;
833
- const context = sanitizeLogContext({ ...resolveContext(), ...ctx, ...requestContext.get() }) ?? {};
1183
+ const context = sanitizeLogContext({
1184
+ ...resolveContext(),
1185
+ ...ctx,
1186
+ ...requestContext.get()
1187
+ }) ?? {};
834
1188
  message = sanitize.sanitizeText(message);
835
1189
  output[level](context, message);
836
- if (!requestContext.get().suppressLogObservers)
837
- emitLogRecord({ timestamp: Date.now(), level, message, context });
1190
+ if (!requestContext.get().suppressLogObservers) {
1191
+ emitLogRecord({
1192
+ timestamp: Date.now(),
1193
+ level,
1194
+ message,
1195
+ context
1196
+ });
1197
+ }
838
1198
  };
839
1199
  return {
840
1200
  trace(message, ctx) {
@@ -856,12 +1216,18 @@ function wrapPino(resolvePino, resolveContext) {
856
1216
  write("fatal", message, ctx);
857
1217
  },
858
1218
  child(childContext) {
859
- return wrapPino(resolvePino, () => ({ ...resolveContext(), ...childContext }));
1219
+ return wrapPino(resolvePino, () => ({
1220
+ ...resolveContext(),
1221
+ ...childContext
1222
+ }));
860
1223
  }
861
1224
  };
862
1225
  }
863
1226
  function createLogger(options = {}) {
864
- const localOptions = { ...options, context: { ...options.context } };
1227
+ const localOptions = {
1228
+ ...options,
1229
+ context: { ...options.context }
1230
+ };
865
1231
  let revision = outputRevision;
866
1232
  let output = createPinoOutput(localOptions);
867
1233
  const resolvePino = () => {
@@ -876,7 +1242,10 @@ function createLogger(options = {}) {
876
1242
  output.instance.level = level;
877
1243
  return output.instance;
878
1244
  };
879
- return wrapPino(resolvePino, () => ({ ...globalContext, ...localOptions.context }));
1245
+ return wrapPino(resolvePino, () => ({
1246
+ ...globalContext,
1247
+ ...localOptions.context
1248
+ }));
880
1249
  }
881
1250
  function createPinoOutput(options) {
882
1251
  const level = options.level ?? globalLevel;
@@ -919,7 +1288,10 @@ function createPinoOutput(options) {
919
1288
  } else {
920
1289
  pinoInstance = pino(pinoOptions);
921
1290
  }
922
- return { instance: pinoInstance, close: () => transport?.end() };
1291
+ return {
1292
+ instance: pinoInstance,
1293
+ close: () => transport?.end()
1294
+ };
923
1295
  }
924
1296
  function isPrettyTransportAvailable() {
925
1297
  try {
@@ -1044,7 +1416,7 @@ function initCore(options = {}) {
1044
1416
  });
1045
1417
  }
1046
1418
  } else {
1047
- const result = config.load(item.name, item.filePath);
1419
+ const result = loadConfigStructure(item.name, item.filePath, options.configSchemas?.[item.name]);
1048
1420
  if (result.success) {
1049
1421
  logger2.info(`[core] Config loaded: ${item.name} <- ${item.filePath}`);
1050
1422
  } else {