@h-ai/core 0.1.0-alpha9 → 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,61 +1,533 @@
1
- import { createCore, CoreConfigSchema, i18n, err, HaiConfigError, ok, typeUtils } from './chunk-FVCFFZP6.js';
2
- export { CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError, IdConfigSchema, LogFormatSchema, LogLevelSchema, LoggingConfigSchema, err, ok } from './chunk-FVCFFZP6.js';
3
- import { existsSync, readdirSync, watch, readFileSync } from 'fs';
4
- import { join } from 'path';
5
- import process2 from 'process';
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
+ import { AsyncLocalStorage } from 'async_hooks';
4
+ import { Buffer } from 'buffer';
5
+ import { timingSafeEqual } from 'crypto';
6
+ import { existsSync, readFileSync, openSync, constants, fstatSync, readSync, closeSync, readdirSync, watch } from 'fs';
7
+ import path, { join, resolve, basename, dirname } from 'path';
8
+ import { BlockList, isIP } from 'net';
9
+ import { networkInterfaces } from 'os';
10
+ import process4 from 'process';
6
11
  import { parse } from 'yaml';
12
+ import { parseEnv } from 'util';
7
13
  import { execSync } from 'child_process';
14
+ import { createRequire } from 'module';
8
15
  import pino from 'pino';
9
16
 
10
- var ENV_VAR_PATTERN = /\$\{([^}:]+)(?::([^}]*))?\}/g;
11
- var FULL_VAR_PATTERN = /^\$\{[^}:]+(?::[^}]*)?\}$/;
12
- function interpolateEnv(value) {
13
- if (typeof value === "string") {
14
- let result = value;
15
- ENV_VAR_PATTERN.lastIndex = 0;
16
- while (true) {
17
- const match = ENV_VAR_PATTERN.exec(value);
18
- if (!match)
17
+ var allowed = Object.freeze({ allowed: true });
18
+ var invalidPath = Object.freeze({ allowed: false, reason: "path" });
19
+ var maximumPolicyBytes = 1024 * 1024;
20
+ function normalizeClientIp(value) {
21
+ if (!value)
22
+ return void 0;
23
+ const address = value.trim();
24
+ if (address.includes("%") || !isIP(address))
25
+ return void 0;
26
+ return address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1] ?? address;
27
+ }
28
+ function addAddress(list, entry) {
29
+ const [address, prefix, extra] = entry.split("/");
30
+ const family = address ? isIP(address) : 0;
31
+ if (!family || address?.includes("%") || extra !== void 0)
32
+ throw new Error("Expected an IP address or CIDR");
33
+ const type = family === 4 ? "ipv4" : "ipv6";
34
+ if (prefix === void 0) {
35
+ list.addAddress(address, type);
36
+ return;
37
+ }
38
+ const bits = Number(prefix);
39
+ if (!/^\d+$/.test(prefix) || bits < 0 || bits > (family === 4 ? 32 : 128))
40
+ throw new Error("Invalid CIDR prefix");
41
+ list.addSubnet(address, bits, type);
42
+ }
43
+ function createClientIpResolver(trustedProxies = "auto") {
44
+ const entries = trustedProxies === "auto" ? process4.env.HAI_DEPLOY_PRIVATE_PROXY === "1" ? Object.values(networkInterfaces()).flatMap((interfaces) => interfaces?.filter((value) => !value.internal && value.cidr && !value.cidr.includes("%")).map((value) => value.cidr) ?? []) : [] : trustedProxies;
45
+ if (trustedProxies === "auto" && process4.env.HAI_DEPLOY_PRIVATE_PROXY === "1" && !entries.length)
46
+ throw new Error("Private proxy mode requires an isolated container network");
47
+ const proxies = new BlockList();
48
+ for (const entry of entries)
49
+ addAddress(proxies, entry);
50
+ const trusted = (address) => proxies.check(address, isIP(address) === 4 ? "ipv4" : "ipv6");
51
+ return (request, remoteAddress) => {
52
+ const peer = normalizeClientIp(remoteAddress);
53
+ if (!peer || !entries.length || !trusted(peer))
54
+ return peer;
55
+ const forwarded = request.headers.get("x-forwarded-for");
56
+ if (forwarded === null) {
57
+ const realIp = request.headers.get("x-real-ip");
58
+ return realIp === null ? peer : normalizeClientIp(realIp);
59
+ }
60
+ if (forwarded.length > 2048)
61
+ return void 0;
62
+ const chain = forwarded.split(",");
63
+ if (chain.length > 32)
64
+ return void 0;
65
+ let current = peer;
66
+ for (let index = chain.length - 1; index >= 0 && trusted(current); index--) {
67
+ const address = normalizeClientIp(chain[index]);
68
+ if (!address)
69
+ return void 0;
70
+ current = address;
71
+ }
72
+ return current;
73
+ };
74
+ }
75
+ 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"];
78
+ let lastMinute = Number.NaN;
79
+ let local;
80
+ return (now) => {
81
+ const minute = Math.floor(now / 6e4);
82
+ if (minute !== lastMinute) {
83
+ const parts = Object.fromEntries(formatter.formatToParts(now).map((part) => [part.type, part.value]));
84
+ local = { day: weekdays.indexOf(parts.weekday) + 1, time: `${parts.hour}:${parts.minute}` };
85
+ lastMinute = minute;
86
+ }
87
+ return local;
88
+ };
89
+ }
90
+ function compileCondition(condition, readTime, ruleId) {
91
+ const allow = new BlockList();
92
+ const deny = new BlockList();
93
+ for (const entry of condition.allowIps)
94
+ addAddress(allow, entry);
95
+ for (const entry of condition.denyIps)
96
+ addAddress(deny, entry);
97
+ const addressDenied = Object.freeze({ allowed: false, reason: "address", ruleId });
98
+ const timeDenied = Object.freeze({ allowed: false, reason: "time", ruleId });
99
+ const checksAddress = condition.allowIps.length > 0 || condition.denyIps.length > 0;
100
+ return (ip) => {
101
+ if (checksAddress) {
102
+ const address = normalizeClientIp(ip);
103
+ if (!address)
104
+ return addressDenied;
105
+ const family = isIP(address) === 4 ? "ipv4" : "ipv6";
106
+ if (deny.check(address, family) || condition.allowIps.length && !allow.check(address, family))
107
+ return addressDenied;
108
+ }
109
+ if (readTime) {
110
+ const { day, time } = readTime(Date.now());
111
+ const previous = day === 1 ? 7 : day - 1;
112
+ if (!condition.windows.some((window) => window.start < window.end ? window.days.includes(day) && time >= window.start && time < window.end : window.days.includes(day) && time >= window.start || window.days.includes(previous) && time < window.end)) {
113
+ return timeDenied;
114
+ }
115
+ }
116
+ return allowed;
117
+ };
118
+ }
119
+ function createConditionCheck(input) {
120
+ const condition = AccessConditionSchema.parse(input);
121
+ return compileCondition(condition, condition.windows.length ? timeReader(condition.timezone) : void 0);
122
+ }
123
+ function loadPolicy(input = false, baseDirectory = process4.cwd()) {
124
+ const config2 = RequestAccessConfigSchema.parse(input);
125
+ if (config2 === false || !("file" in config2))
126
+ return config2;
127
+ let descriptor;
128
+ let content;
129
+ try {
130
+ descriptor = openSync(path.resolve(baseDirectory, config2.file), constants.O_RDONLY | constants.O_NONBLOCK);
131
+ const stat = fstatSync(descriptor);
132
+ if (!stat.isFile() || stat.size > maximumPolicyBytes)
133
+ throw new Error("Invalid policy file size or type");
134
+ const bytes = Buffer.alloc(Math.min(stat.size + 1, maximumPolicyBytes + 1));
135
+ let length = 0;
136
+ while (length < bytes.length) {
137
+ const read = readSync(descriptor, bytes, length, bytes.length - length, null);
138
+ if (!read)
19
139
  break;
20
- const [fullMatch, varName, defaultValue] = match;
21
- const envValue = process2.env[varName];
22
- if (envValue === void 0 && defaultValue === void 0) {
23
- return err(HaiConfigError.CONFIG_ENV_VAR_MISSING, i18n.coreM("core_configEnvVarMissing", { params: { varName } }));
140
+ length += read;
141
+ }
142
+ if (length > maximumPolicyBytes || length !== stat.size)
143
+ throw new Error("Policy file changed while reading");
144
+ content = bytes.subarray(0, length).toString("utf8");
145
+ } catch {
146
+ throw new Error("Request access policy file could not be read; use a regular YAML file no larger than 1 MiB");
147
+ } finally {
148
+ if (descriptor !== void 0)
149
+ closeSync(descriptor);
150
+ }
151
+ let parsed;
152
+ try {
153
+ parsed = parse(content, { maxAliasCount: 0, uniqueKeys: true });
154
+ } catch {
155
+ throw new Error("Request access policy YAML is invalid; aliases and duplicate keys are not allowed");
156
+ }
157
+ const validated = RequestAccessPolicySchema.safeParse(parsed);
158
+ if (!validated.success)
159
+ throw new Error(`Request access policy is invalid at ${validated.error.issues.map((issue) => issue.path.join(".") || "root").join(", ")}`);
160
+ return validated.data;
161
+ }
162
+ function normalizeRequestPath(value) {
163
+ if (value.length > 8192)
164
+ return void 0;
165
+ let decoded = value;
166
+ if (value.includes("%")) {
167
+ if (/%(?:2f|5c|00)/i.test(value))
168
+ return void 0;
169
+ try {
170
+ decoded = decodeURIComponent(value);
171
+ } catch {
172
+ return void 0;
173
+ }
174
+ }
175
+ if (!decoded.startsWith("/") || /[\\%?#\u0000-\u0020\u007F]/.test(decoded) || decoded.includes("//"))
176
+ return void 0;
177
+ if (/(?:^|\/)\.\.?(?:\/|$)/.test(decoded))
178
+ return void 0;
179
+ return decoded.length > 1 && decoded.endsWith("/") ? decoded.slice(0, -1) : decoded;
180
+ }
181
+ function createRequestCheck(input = false) {
182
+ const policy = loadPolicy(input);
183
+ if (policy === false)
184
+ return Object.assign(() => allowed, { encryption: () => void 0, requiresEncryption: false });
185
+ const clocks = /* @__PURE__ */ new Map();
186
+ const compile = (condition, id) => {
187
+ let readTime;
188
+ if (condition.windows.length) {
189
+ readTime = clocks.get(condition.timezone) ?? timeReader(condition.timezone);
190
+ clocks.set(condition.timezone, readTime);
191
+ }
192
+ return compileCondition(condition, readTime, id);
193
+ };
194
+ const globalCheck = policy.global ? compile(policy.global) : () => allowed;
195
+ 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", "*"];
198
+ let requiresEncryption = false;
199
+ let hasEncryption = false;
200
+ for (const rule of policy.rules) {
201
+ const compiled = { methods: new Set(rule.methods), check: compile(rule, rule.id) };
202
+ for (const pattern of new Set(rule.paths)) {
203
+ if (rule.encryption) {
204
+ hasEncryption = true;
205
+ requiresEncryption ||= rule.encryption === "required";
206
+ const prefix = pattern.endsWith("/**");
207
+ const nodes = [prefixes];
208
+ let node = prefixes;
209
+ const path2 = normalizeRequestPath(prefix ? pattern.slice(0, -3) || "/" : pattern);
210
+ for (const segment2 of path2.split("/").filter(Boolean)) {
211
+ let child = node.children.get(segment2);
212
+ if (!child) {
213
+ child = { children: /* @__PURE__ */ new Map(), rules: [] };
214
+ node.children.set(segment2, child);
215
+ }
216
+ node = child;
217
+ nodes.push(node);
218
+ }
219
+ const bit = rule.encryption === "required" ? 1 : 2;
220
+ const opposite = bit === 1 ? 2 : 1;
221
+ const methods = rule.methods.length ? rule.methods : allMethods;
222
+ for (const method of methods) {
223
+ if (nodes.some((parent) => parent.encryption?.has(method) && parent.encryption.get(method) !== rule.encryption) || (prefix ? (node.encryptionBelow?.get(method) ?? 0) & opposite : node.exactEncryption?.has(method) && node.exactEncryption.get(method) !== rule.encryption)) {
224
+ throw new Error(`Request access encryption conflicts at rule ${rule.id}`);
225
+ }
226
+ const modes = prefix ? node.encryption ??= /* @__PURE__ */ new Map() : node.exactEncryption ??= /* @__PURE__ */ new Map();
227
+ modes.set(method, rule.encryption);
228
+ for (const parent of nodes) {
229
+ parent.encryptionBelow ??= /* @__PURE__ */ new Map();
230
+ parent.encryptionBelow.set(method, (parent.encryptionBelow.get(method) ?? 0) | bit);
231
+ }
232
+ }
233
+ }
234
+ if (pattern.endsWith("/**")) {
235
+ let node = prefixes;
236
+ for (const segment2 of pattern.slice(0, -3).split("/").filter(Boolean)) {
237
+ let child = node.children.get(segment2);
238
+ if (!child) {
239
+ child = { children: /* @__PURE__ */ new Map(), rules: [] };
240
+ node.children.set(segment2, child);
241
+ }
242
+ node = child;
243
+ }
244
+ node.rules.push(compiled);
245
+ } else {
246
+ const path2 = normalizeRequestPath(pattern);
247
+ const rules = exact.get(path2) ?? [];
248
+ rules.push(compiled);
249
+ exact.set(path2, rules);
24
250
  }
25
- result = result.replace(fullMatch, envValue ?? defaultValue ?? "");
26
251
  }
27
- if (FULL_VAR_PATTERN.test(value)) {
252
+ }
253
+ const check = (pathname, method, ip) => {
254
+ const global = globalCheck(ip);
255
+ if (!global.allowed)
256
+ return global;
257
+ const path2 = normalizeRequestPath(pathname);
258
+ if (!path2)
259
+ return invalidPath;
260
+ for (const rule of exact.get(path2) ?? []) {
261
+ if (!rule.methods.size || rule.methods.has(method)) {
262
+ const decision = rule.check(ip);
263
+ if (!decision.allowed)
264
+ return decision;
265
+ }
266
+ }
267
+ let node = prefixes;
268
+ for (const rule of node.rules) {
269
+ if (!rule.methods.size || rule.methods.has(method)) {
270
+ const decision = rule.check(ip);
271
+ if (!decision.allowed)
272
+ return decision;
273
+ }
274
+ }
275
+ if (node.children.size) {
276
+ for (const segment2 of path2.split("/").filter(Boolean)) {
277
+ node = node.children.get(segment2);
278
+ if (!node)
279
+ break;
280
+ for (const rule of node.rules) {
281
+ if (!rule.methods.size || rule.methods.has(method)) {
282
+ const decision = rule.check(ip);
283
+ if (!decision.allowed)
284
+ return decision;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ return allowed;
290
+ };
291
+ const encryption = (pathname, method) => {
292
+ const path2 = normalizeRequestPath(pathname);
293
+ if (!path2)
294
+ return void 0;
295
+ let node = prefixes;
296
+ let mode = node.encryption?.get(method) ?? node.encryption?.get("*");
297
+ for (const segment2 of path2.split("/").filter(Boolean)) {
298
+ node = node.children.get(segment2);
299
+ if (!node)
300
+ return mode;
301
+ mode ??= node.encryption?.get(method) ?? node.encryption?.get("*");
302
+ }
303
+ return mode ?? node?.exactEncryption?.get(method) ?? node?.exactEncryption?.get("*");
304
+ };
305
+ return Object.assign(check, { encryption: hasEncryption ? encryption : () => void 0, requiresEncryption });
306
+ }
307
+ var access = { normalizeClientIp, createClientIpResolver, createConditionCheck, createRequestCheck, loadPolicy };
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 === "")
350
+ return "";
351
+ try {
352
+ return parse(text) ?? (text === "null" ? null : text);
353
+ } catch {
354
+ return text;
355
+ }
356
+ }
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;
373
+ }
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"))) {
28
384
  try {
29
- const coerced = parse(result);
30
- if (coerced !== void 0)
31
- return ok(coerced);
385
+ result[name] = JSON.parse(match[2]);
32
386
  } catch {
33
387
  }
34
388
  }
35
- return ok(result);
36
389
  }
37
- if (Array.isArray(value)) {
38
- const results = [];
39
- for (const item of value) {
40
- const r = interpolateEnv(item);
41
- if (!r.success)
42
- return r;
43
- 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]));
44
449
  }
45
- return ok(results);
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);
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 } }));
46
505
  }
47
- if (typeUtils.isObject(value)) {
48
- const results = {};
49
- for (const [k, v] of Object.entries(value)) {
50
- const r = interpolateEnv(v);
51
- if (!r.success)
52
- return r;
53
- 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);
54
524
  }
55
- return ok(results);
56
525
  }
57
- return ok(value);
526
+ visit(options.value, options.prefix, root);
527
+ return result;
58
528
  }
529
+
530
+ // src/functions/core-function-config.node.ts
59
531
  var configCache = /* @__PURE__ */ new Map();
60
532
  var watchEntries = /* @__PURE__ */ new Map();
61
533
  function createNotLoadedError(name) {
@@ -84,20 +556,20 @@ function notifyWatchCallbacks(name, result) {
84
556
  }
85
557
  }
86
558
  }
87
- function loadYaml(filePath) {
559
+ function loadYaml(name, filePath, schema) {
88
560
  if (!existsSync(filePath)) {
89
561
  return err(HaiConfigError.CONFIG_FILE_NOT_FOUND, i18n.coreM("core_configFileNotExist", { params: { filePath } }));
90
562
  }
91
563
  try {
92
564
  const content = readFileSync(filePath, "utf-8");
93
565
  const parsed = parse(content);
94
- return interpolateEnv(parsed);
566
+ return resolveEnvironment({ name, value: parsed, schema });
95
567
  } catch (error) {
96
568
  return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath } }), error);
97
569
  }
98
570
  }
99
- function loadConfig(filePath, schema) {
100
- const yamlResult = loadYaml(filePath);
571
+ function loadConfig(name, filePath, schema) {
572
+ const yamlResult = loadYaml(name, filePath, schema);
101
573
  if (!yamlResult.success)
102
574
  return yamlResult;
103
575
  const parseResult = schema.safeParse(yamlResult.data);
@@ -107,7 +579,7 @@ function loadConfig(filePath, schema) {
107
579
  return ok(parseResult.data);
108
580
  }
109
581
  function loadAndCache(name, filePath, schema) {
110
- const result = schema ? loadConfig(filePath, schema) : loadYaml(filePath);
582
+ const result = schema ? loadConfig(name, filePath, schema) : loadYaml(name, filePath);
111
583
  if (result.success) {
112
584
  configCache.set(name, {
113
585
  data: result.data,
@@ -118,12 +590,21 @@ function loadAndCache(name, filePath, schema) {
118
590
  }
119
591
  return result;
120
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
+ }
121
599
  function validateLoadedConfig(name, schema) {
122
600
  const entry = configCache.get(name);
123
601
  if (!entry) {
124
602
  return err(createNotLoadedError(name));
125
603
  }
126
- 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);
127
608
  if (!parseResult.success) {
128
609
  return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"), parseResult.error.issues);
129
610
  }
@@ -143,7 +624,7 @@ function reloadAndNotify(name) {
143
624
  notifyWatchCallbacks(name, result2);
144
625
  return result2;
145
626
  }
146
- const result = entry.schema ? loadConfig(entry.filePath, entry.schema) : loadYaml(entry.filePath);
627
+ const result = entry.schema ? loadConfig(name, entry.filePath, entry.schema) : loadYaml(name, entry.filePath, entry.schemaHint);
147
628
  if (result.success) {
148
629
  configCache.set(name, {
149
630
  ...entry,
@@ -167,15 +648,18 @@ function startFileWatcher(name) {
167
648
  callbacks: /* @__PURE__ */ new Set(),
168
649
  debounceTimer: null
169
650
  };
170
- const watcher = watch(entry.filePath, (eventType) => {
171
- if (eventType === "change") {
172
- if (watchEntry.debounceTimer)
173
- clearTimeout(watchEntry.debounceTimer);
174
- watchEntry.debounceTimer = setTimeout(() => {
175
- 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)
176
661
  reloadAndNotify(name);
177
- }, 100);
178
- }
662
+ }, 100);
179
663
  });
180
664
  watchEntry.watcher = watcher;
181
665
  watchEntries.set(name, watchEntry);
@@ -194,7 +678,7 @@ function registerWatch(name, callback) {
194
678
  entry.callbacks.add(callback);
195
679
  return () => {
196
680
  const current = watchEntries.get(name);
197
- if (!current)
681
+ if (current !== entry)
198
682
  return;
199
683
  current.callbacks.delete(callback);
200
684
  if (current.callbacks.size === 0) {
@@ -233,6 +717,101 @@ function clearCache(name) {
233
717
  }
234
718
  }
235
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,
777
+ /**
778
+ * 读取 dotenv 文件并按统一顺序合并,不修改进程环境。
779
+ *
780
+ * 优先级:已有进程环境 > .env.<mode>.local > .env.local > .env.<mode> > .env。
781
+ * development/production 分别映射到 dev/prod;不存在的文件跳过。
782
+ * 返回值可能包含私有配置,仅在 Node 启动/构建边界注入,不下发浏览器。
783
+ * @param options - 文件所在目录及环境模式
784
+ * @param options.mode - dev/test/prod 或 Vite 的 development/production
785
+ * @param options.envDir - dotenv 所在目录,默认进程工作目录
786
+ * @returns 已合并的变量;非法模式或读取失败返回配置错误,不包含文件内容
787
+ * @example
788
+ * ```ts
789
+ * const env = core.config.loadEnvFiles({ mode: 'dev', envDir: '.' })
790
+ * if (!env.success) throw new Error(env.error.message)
791
+ * Object.assign(process.env, env.data)
792
+ * core.init({ configDir: './config' })
793
+ * ```
794
+ */
795
+ loadEnvFiles(options) {
796
+ const mode = options.mode === "development" ? "dev" : options.mode === "production" ? "prod" : options.mode;
797
+ if (!/^[a-z0-9][\w-]*$/i.test(mode))
798
+ return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"));
799
+ const envDir = options.envDir ?? process4.cwd();
800
+ try {
801
+ const layers = [];
802
+ for (const name of [".env", `.env.${mode}`, ".env.local", `.env.${mode}.local`]) {
803
+ const file = join(envDir, name);
804
+ if (existsSync(file))
805
+ layers.push(parseEnvironment(readFileSync(file, "utf8")));
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);
810
+ return ok(merged);
811
+ } catch {
812
+ return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath: envDir } }));
813
+ }
814
+ },
236
815
  /**
237
816
  * 加载配置到缓存。
238
817
  *
@@ -376,7 +955,8 @@ var config = {
376
955
  /**
377
956
  * 监听配置文件变更并自动重载。
378
957
  *
379
- * 文件变更时自动重新加载并调用回调。配置未加载时立即回调 NOT_LOADED 错误。
958
+ * 普通写入、原子替换及删除重建均触发重载;失败保留最近成功的缓存并通知错误。
959
+ * 配置未加载时立即回调 NOT_LOADED 错误。
380
960
  *
381
961
  * @param name - 配置名称
382
962
  * @param callback - 配置变更回调,接收新配置或错误
@@ -426,18 +1006,16 @@ var config = {
426
1006
  return watchEntries.has(name);
427
1007
  }
428
1008
  };
429
- if (process2.platform === "win32" && process2.stdout.isTTY) {
1009
+ var nodeRequire = createRequire(import.meta.url);
1010
+ if (process4.platform === "win32" && process4.stdout.isTTY) {
430
1011
  try {
431
1012
  execSync("chcp 65001 > nul", { stdio: "ignore" });
432
- if (process2.stdout.setDefaultEncoding) {
433
- process2.stdout.setDefaultEncoding("utf8");
1013
+ if (process4.stdout.setDefaultEncoding) {
1014
+ process4.stdout.setDefaultEncoding("utf8");
434
1015
  }
435
- if (process2.stderr.setDefaultEncoding) {
436
- process2.stderr.setDefaultEncoding("utf8");
1016
+ if (process4.stderr.setDefaultEncoding) {
1017
+ process4.stderr.setDefaultEncoding("utf8");
437
1018
  }
438
- process2.env.LANG = "zh_CN.UTF-8";
439
- process2.env.LC_ALL = "zh_CN.UTF-8";
440
- process2.env.PYTHONIOENCODING = "utf-8";
441
1019
  } catch (_e) {
442
1020
  }
443
1021
  }
@@ -445,17 +1023,21 @@ var globalLevel = "info";
445
1023
  var globalFormat = "pretty";
446
1024
  var globalContext = {};
447
1025
  var globalRedact = [];
1026
+ var outputRevision = 0;
448
1027
  var defaultLogger = null;
449
1028
  function configureLogger(config2) {
450
1029
  if (config2.level)
451
1030
  globalLevel = config2.level;
452
- if (config2.format)
1031
+ if (config2.format) {
453
1032
  globalFormat = config2.format;
1033
+ outputRevision++;
1034
+ }
454
1035
  if (config2.context)
455
1036
  globalContext = { ...globalContext, ...config2.context };
456
- if (config2.redact)
457
- globalRedact = config2.redact;
458
- defaultLogger = null;
1037
+ if (config2.redact) {
1038
+ globalRedact = [...config2.redact];
1039
+ outputRevision++;
1040
+ }
459
1041
  }
460
1042
  function setLogLevel(level) {
461
1043
  globalLevel = level;
@@ -463,35 +1045,68 @@ function setLogLevel(level) {
463
1045
  function getLogLevel() {
464
1046
  return globalLevel;
465
1047
  }
466
- function wrapPino(pinoLogger, context) {
1048
+ function sanitizeLogContext(ctx) {
1049
+ if (!ctx || Object.keys(ctx).length === 0) {
1050
+ return ctx;
1051
+ }
1052
+ return sanitizeLogFields(ctx, globalRedact);
1053
+ }
1054
+ function wrapPino(resolvePino, resolveContext) {
1055
+ const write = (level, message, ctx) => {
1056
+ const output = resolvePino();
1057
+ if (!output.isLevelEnabled(level))
1058
+ return;
1059
+ const context = sanitizeLogContext({ ...resolveContext(), ...ctx, ...requestContext.get() }) ?? {};
1060
+ message = sanitize.sanitizeText(message);
1061
+ output[level](context, message);
1062
+ if (!requestContext.get().suppressLogObservers)
1063
+ emitLogRecord({ timestamp: Date.now(), level, message, context });
1064
+ };
467
1065
  return {
468
1066
  trace(message, ctx) {
469
- pinoLogger.trace({ ...context, ...ctx }, message);
1067
+ write("trace", message, ctx);
470
1068
  },
471
1069
  debug(message, ctx) {
472
- pinoLogger.debug({ ...context, ...ctx }, message);
1070
+ write("debug", message, ctx);
473
1071
  },
474
1072
  info(message, ctx) {
475
- pinoLogger.info({ ...context, ...ctx }, message);
1073
+ write("info", message, ctx);
476
1074
  },
477
1075
  warn(message, ctx) {
478
- pinoLogger.warn({ ...context, ...ctx }, message);
1076
+ write("warn", message, ctx);
479
1077
  },
480
1078
  error(message, ctx) {
481
- pinoLogger.error({ ...context, ...ctx }, message);
1079
+ write("error", message, ctx);
482
1080
  },
483
1081
  fatal(message, ctx) {
484
- pinoLogger.fatal({ ...context, ...ctx }, message);
1082
+ write("fatal", message, ctx);
485
1083
  },
486
1084
  child(childContext) {
487
- return wrapPino(pinoLogger.child(childContext), { ...context, ...childContext });
1085
+ return wrapPino(resolvePino, () => ({ ...resolveContext(), ...childContext }));
488
1086
  }
489
1087
  };
490
1088
  }
491
1089
  function createLogger(options = {}) {
1090
+ const localOptions = { ...options, context: { ...options.context } };
1091
+ let revision = outputRevision;
1092
+ let output = createPinoOutput(localOptions);
1093
+ const resolvePino = () => {
1094
+ if (revision !== outputRevision) {
1095
+ const updated = createPinoOutput(localOptions);
1096
+ output.close();
1097
+ output = updated;
1098
+ revision = outputRevision;
1099
+ }
1100
+ const level = localOptions.level ?? globalLevel;
1101
+ if (output.instance.level !== level)
1102
+ output.instance.level = level;
1103
+ return output.instance;
1104
+ };
1105
+ return wrapPino(resolvePino, () => ({ ...globalContext, ...localOptions.context }));
1106
+ }
1107
+ function createPinoOutput(options) {
492
1108
  const level = options.level ?? globalLevel;
493
1109
  const format = options.format ?? globalFormat;
494
- const context = { ...globalContext, ...options.context };
495
1110
  const pinoOptions = {
496
1111
  level,
497
1112
  name: options.name,
@@ -506,22 +1121,44 @@ function createLogger(options = {}) {
506
1121
  };
507
1122
  }
508
1123
  let pinoInstance;
509
- if (format === "pretty") {
510
- pinoInstance = pino({
511
- ...pinoOptions,
512
- transport: {
1124
+ let transport;
1125
+ if (format === "pretty" && isPrettyTransportAvailable()) {
1126
+ try {
1127
+ transport = pino.transport({
513
1128
  target: "pino-pretty",
514
1129
  options: {
515
1130
  colorize: true,
516
1131
  translateTime: "SYS:standard",
517
1132
  ignore: "pid,hostname"
518
1133
  }
1134
+ });
1135
+ pinoInstance = pino(pinoOptions, transport);
1136
+ if ("unref" in transport && typeof transport.unref === "function")
1137
+ transport.unref();
1138
+ } catch (error) {
1139
+ if (isPrettyTransportUnavailable(error)) {
1140
+ pinoInstance = pino(pinoOptions);
1141
+ } else {
1142
+ throw error;
519
1143
  }
520
- });
1144
+ }
521
1145
  } else {
522
1146
  pinoInstance = pino(pinoOptions);
523
1147
  }
524
- return wrapPino(pinoInstance, context);
1148
+ return { instance: pinoInstance, close: () => transport?.end() };
1149
+ }
1150
+ function isPrettyTransportAvailable() {
1151
+ try {
1152
+ nodeRequire.resolve("pino-pretty");
1153
+ return true;
1154
+ } catch {
1155
+ return false;
1156
+ }
1157
+ }
1158
+ function isPrettyTransportUnavailable(error) {
1159
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
1160
+ const normalized = message.toLowerCase();
1161
+ return normalized.includes("pino-pretty") && (normalized.includes("cannot find module") || normalized.includes("unable to determine transport target") || normalized.includes("cannot load") || normalized.includes("not found"));
525
1162
  }
526
1163
  function getLogger() {
527
1164
  if (!defaultLogger) {
@@ -538,6 +1175,9 @@ var logger = {
538
1175
  };
539
1176
 
540
1177
  // src/core-main.node.ts
1178
+ installRequestLocales(new AsyncLocalStorage());
1179
+ installRequestContext(new AsyncLocalStorage());
1180
+ var YAML_EXTENSION_REGEX = /\.ya?ml$/;
541
1181
  function createNodeCore() {
542
1182
  const baseCore = createCore({
543
1183
  createLogger: logger.createLogger,
@@ -548,6 +1188,28 @@ function createNodeCore() {
548
1188
  });
549
1189
  return {
550
1190
  ...baseCore,
1191
+ /** Node 专属的可信地址、策略文件读取与已编译访问检查。 */
1192
+ access,
1193
+ string: {
1194
+ ...baseCore.string,
1195
+ /**
1196
+ * Node 凭证比较使用原生恒时原语;不同长度直接失败,长度本身不受保护。
1197
+ * UTF-16LE 保留孤立代理项,避免 UTF-8 替换字符把两个不同字符串误判为相等。
1198
+ * @param a - 第一个原始字符串,不做 Unicode 归一化。
1199
+ * @param b - 第二个原始字符串,不做 Unicode 归一化。
1200
+ * @returns 字符串的 UTF-16 单元是否完全一致。
1201
+ * @example
1202
+ * ```ts
1203
+ * import { core } from '@h-ai/core'
1204
+ * const matches = core.string.constantTimeEqual('example-only', 'example-only')
1205
+ * ```
1206
+ */
1207
+ constantTimeEqual(a, b) {
1208
+ return a.length === b.length && timingSafeEqual(Buffer.from(a, "utf16le"), Buffer.from(b, "utf16le"));
1209
+ }
1210
+ },
1211
+ /** Zod 校验错误 → i18n 消息映射工具(kit / serv / api-client 共享)。 */
1212
+ zodValidation: baseCore.zodValidation,
551
1213
  /** 配置管理 */
552
1214
  config,
553
1215
  /** 初始化 Core */
@@ -567,7 +1229,7 @@ function scanConfigDir(configDir) {
567
1229
  );
568
1230
  for (const file of files) {
569
1231
  const filePath = join(configDir, file);
570
- const baseName = file.replace(/\.ya?ml$/, "");
1232
+ const baseName = file.replace(YAML_EXTENSION_REGEX, "");
571
1233
  if (baseName.startsWith("_")) {
572
1234
  const moduleName = baseName.slice(1);
573
1235
  items.push({
@@ -589,11 +1251,11 @@ function initCore(options = {}) {
589
1251
  if (options.logging) {
590
1252
  core.logger.configure(options.logging);
591
1253
  }
592
- logger2.info("[core] Initializing...");
593
1254
  let configItems = [];
594
1255
  if (options.configDir) {
595
1256
  configItems = scanConfigDir(options.configDir);
596
1257
  }
1258
+ configItems.sort((left, right) => Number(right.name === "core") - Number(left.name === "core"));
597
1259
  for (const item of configItems) {
598
1260
  if (item.name === "core") {
599
1261
  const result = config.load(item.name, item.filePath, CoreConfigSchema);
@@ -608,7 +1270,7 @@ function initCore(options = {}) {
608
1270
  });
609
1271
  }
610
1272
  } else {
611
- const result = config.load(item.name, item.filePath);
1273
+ const result = loadConfigStructure(item.name, item.filePath, options.configSchemas?.[item.name]);
612
1274
  if (result.success) {
613
1275
  logger2.info(`[core] Config loaded: ${item.name} <- ${item.filePath}`);
614
1276
  } else {