@h-ai/core 0.1.0-alpha9 → 1.0.0-beta.1

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,43 +1,358 @@
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, 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';
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 } from 'path';
8
+ import { BlockList, isIP } from 'net';
9
+ import { networkInterfaces } from 'os';
10
+ import process3 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
 
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" ? 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)
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 = process3.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)
139
+ break;
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 segment of path2.split("/").filter(Boolean)) {
211
+ let child = node.children.get(segment);
212
+ if (!child) {
213
+ child = { children: /* @__PURE__ */ new Map(), rules: [] };
214
+ node.children.set(segment, 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 segment of pattern.slice(0, -3).split("/").filter(Boolean)) {
237
+ let child = node.children.get(segment);
238
+ if (!child) {
239
+ child = { children: /* @__PURE__ */ new Map(), rules: [] };
240
+ node.children.set(segment, 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);
250
+ }
251
+ }
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 segment of path2.split("/").filter(Boolean)) {
277
+ node = node.children.get(segment);
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 segment of path2.split("/").filter(Boolean)) {
298
+ node = node.children.get(segment);
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 };
10
308
  var ENV_VAR_PATTERN = /\$\{([^}:]+)(?::([^}]*))?\}/g;
11
309
  var FULL_VAR_PATTERN = /^\$\{[^}:]+(?::[^}]*)?\}$/;
12
- function interpolateEnv(value) {
310
+ function parseEnvValue(value) {
311
+ if (value === "")
312
+ return "";
313
+ try {
314
+ const parsed = parse(value);
315
+ return parsed === void 0 ? value : parsed;
316
+ } catch {
317
+ return value;
318
+ }
319
+ }
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("_");
325
+ }
326
+ function resolveEnv(value, name, path2 = []) {
327
+ const conventionEnvValue = process3.env[buildConventionEnvName(name, path2)];
328
+ if (conventionEnvValue !== void 0)
329
+ return ok(parseEnvValue(conventionEnvValue));
13
330
  if (typeof value === "string") {
14
- let result = value;
15
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;
16
337
  while (true) {
17
338
  const match = ENV_VAR_PATTERN.exec(value);
18
339
  if (!match)
19
340
  break;
20
341
  const [fullMatch, varName, defaultValue] = match;
21
- const envValue = process2.env[varName];
342
+ const envValue = process3.env[varName];
22
343
  if (envValue === void 0 && defaultValue === void 0) {
23
344
  return err(HaiConfigError.CONFIG_ENV_VAR_MISSING, i18n.coreM("core_configEnvVarMissing", { params: { varName } }));
24
345
  }
25
- result = result.replace(fullMatch, envValue ?? defaultValue ?? "");
26
- }
27
- if (FULL_VAR_PATTERN.test(value)) {
28
- try {
29
- const coerced = parse(result);
30
- if (coerced !== void 0)
31
- return ok(coerced);
32
- } catch {
33
- }
346
+ result = result.replace(fullMatch, () => envValue ?? defaultValue ?? "");
34
347
  }
348
+ if (FULL_VAR_PATTERN.test(value))
349
+ return ok(parseEnvValue(result));
35
350
  return ok(result);
36
351
  }
37
352
  if (Array.isArray(value)) {
38
353
  const results = [];
39
- for (const item of value) {
40
- const r = interpolateEnv(item);
354
+ for (const [index, item] of value.entries()) {
355
+ const r = resolveEnv(item, name, [...path2, String(index)]);
41
356
  if (!r.success)
42
357
  return r;
43
358
  results.push(r.data);
@@ -47,7 +362,7 @@ function interpolateEnv(value) {
47
362
  if (typeUtils.isObject(value)) {
48
363
  const results = {};
49
364
  for (const [k, v] of Object.entries(value)) {
50
- const r = interpolateEnv(v);
365
+ const r = resolveEnv(v, name, [...path2, k]);
51
366
  if (!r.success)
52
367
  return r;
53
368
  results[k] = r.data;
@@ -84,20 +399,20 @@ function notifyWatchCallbacks(name, result) {
84
399
  }
85
400
  }
86
401
  }
87
- function loadYaml(filePath) {
402
+ function loadYaml(name, filePath) {
88
403
  if (!existsSync(filePath)) {
89
404
  return err(HaiConfigError.CONFIG_FILE_NOT_FOUND, i18n.coreM("core_configFileNotExist", { params: { filePath } }));
90
405
  }
91
406
  try {
92
407
  const content = readFileSync(filePath, "utf-8");
93
408
  const parsed = parse(content);
94
- return interpolateEnv(parsed);
409
+ return resolveEnv(parsed, name);
95
410
  } catch (error) {
96
411
  return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath } }), error);
97
412
  }
98
413
  }
99
- function loadConfig(filePath, schema) {
100
- const yamlResult = loadYaml(filePath);
414
+ function loadConfig(name, filePath, schema) {
415
+ const yamlResult = loadYaml(name, filePath);
101
416
  if (!yamlResult.success)
102
417
  return yamlResult;
103
418
  const parseResult = schema.safeParse(yamlResult.data);
@@ -107,7 +422,7 @@ function loadConfig(filePath, schema) {
107
422
  return ok(parseResult.data);
108
423
  }
109
424
  function loadAndCache(name, filePath, schema) {
110
- const result = schema ? loadConfig(filePath, schema) : loadYaml(filePath);
425
+ const result = schema ? loadConfig(name, filePath, schema) : loadYaml(name, filePath);
111
426
  if (result.success) {
112
427
  configCache.set(name, {
113
428
  data: result.data,
@@ -143,7 +458,7 @@ function reloadAndNotify(name) {
143
458
  notifyWatchCallbacks(name, result2);
144
459
  return result2;
145
460
  }
146
- const result = entry.schema ? loadConfig(entry.filePath, entry.schema) : loadYaml(entry.filePath);
461
+ const result = entry.schema ? loadConfig(name, entry.filePath, entry.schema) : loadYaml(name, entry.filePath);
147
462
  if (result.success) {
148
463
  configCache.set(name, {
149
464
  ...entry,
@@ -233,6 +548,45 @@ function clearCache(name) {
233
548
  }
234
549
  }
235
550
  var config = {
551
+ /**
552
+ * 读取 dotenv 文件并按统一顺序合并,不修改进程环境。
553
+ *
554
+ * 优先级:已有进程环境 > .env.local > .env.<mode> > .env。
555
+ * development/production 分别映射到 dev/prod;不存在的文件跳过。
556
+ * 返回值可能包含私有配置,仅在 Node 启动/构建边界注入,不下发浏览器。
557
+ * @param options - 文件所在目录及环境模式
558
+ * @param options.mode - dev/test/prod 或 Vite 的 development/production
559
+ * @param options.envDir - dotenv 所在目录,默认进程工作目录
560
+ * @returns 已合并的变量;非法模式或读取失败返回配置错误,不包含文件内容
561
+ * @example
562
+ * ```ts
563
+ * const env = core.config.loadEnvFiles({ mode: 'dev', envDir: '.' })
564
+ * if (!env.success) throw new Error(env.error.message)
565
+ * Object.assign(process.env, env.data)
566
+ * core.init({ configDir: './config' })
567
+ * ```
568
+ */
569
+ loadEnvFiles(options) {
570
+ const mode = options.mode === "development" ? "dev" : options.mode === "production" ? "prod" : options.mode;
571
+ if (!/^[a-z0-9][\w-]*$/i.test(mode))
572
+ return err(HaiConfigError.CONFIG_VALIDATION_ERROR, i18n.coreM("core_configValidationFailed"));
573
+ const envDir = options.envDir ?? process3.cwd();
574
+ try {
575
+ const merged = {};
576
+ for (const name of [".env", `.env.${mode}`, ".env.local"]) {
577
+ const file = join(envDir, name);
578
+ 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];
584
+ }
585
+ return ok(merged);
586
+ } catch {
587
+ return err(HaiConfigError.CONFIG_PARSE_ERROR, i18n.coreM("core_configParseFailed", { params: { filePath: envDir } }));
588
+ }
589
+ },
236
590
  /**
237
591
  * 加载配置到缓存。
238
592
  *
@@ -426,18 +780,16 @@ var config = {
426
780
  return watchEntries.has(name);
427
781
  }
428
782
  };
429
- if (process2.platform === "win32" && process2.stdout.isTTY) {
783
+ var nodeRequire = createRequire(import.meta.url);
784
+ if (process3.platform === "win32" && process3.stdout.isTTY) {
430
785
  try {
431
786
  execSync("chcp 65001 > nul", { stdio: "ignore" });
432
- if (process2.stdout.setDefaultEncoding) {
433
- process2.stdout.setDefaultEncoding("utf8");
787
+ if (process3.stdout.setDefaultEncoding) {
788
+ process3.stdout.setDefaultEncoding("utf8");
434
789
  }
435
- if (process2.stderr.setDefaultEncoding) {
436
- process2.stderr.setDefaultEncoding("utf8");
790
+ if (process3.stderr.setDefaultEncoding) {
791
+ process3.stderr.setDefaultEncoding("utf8");
437
792
  }
438
- process2.env.LANG = "zh_CN.UTF-8";
439
- process2.env.LC_ALL = "zh_CN.UTF-8";
440
- process2.env.PYTHONIOENCODING = "utf-8";
441
793
  } catch (_e) {
442
794
  }
443
795
  }
@@ -445,17 +797,21 @@ var globalLevel = "info";
445
797
  var globalFormat = "pretty";
446
798
  var globalContext = {};
447
799
  var globalRedact = [];
800
+ var outputRevision = 0;
448
801
  var defaultLogger = null;
449
802
  function configureLogger(config2) {
450
803
  if (config2.level)
451
804
  globalLevel = config2.level;
452
- if (config2.format)
805
+ if (config2.format) {
453
806
  globalFormat = config2.format;
807
+ outputRevision++;
808
+ }
454
809
  if (config2.context)
455
810
  globalContext = { ...globalContext, ...config2.context };
456
- if (config2.redact)
457
- globalRedact = config2.redact;
458
- defaultLogger = null;
811
+ if (config2.redact) {
812
+ globalRedact = [...config2.redact];
813
+ outputRevision++;
814
+ }
459
815
  }
460
816
  function setLogLevel(level) {
461
817
  globalLevel = level;
@@ -463,35 +819,68 @@ function setLogLevel(level) {
463
819
  function getLogLevel() {
464
820
  return globalLevel;
465
821
  }
466
- function wrapPino(pinoLogger, context) {
822
+ function sanitizeLogContext(ctx) {
823
+ if (!ctx || Object.keys(ctx).length === 0) {
824
+ return ctx;
825
+ }
826
+ return sanitizeLogFields(ctx, globalRedact);
827
+ }
828
+ function wrapPino(resolvePino, resolveContext) {
829
+ const write = (level, message, ctx) => {
830
+ const output = resolvePino();
831
+ if (!output.isLevelEnabled(level))
832
+ return;
833
+ const context = sanitizeLogContext({ ...resolveContext(), ...ctx, ...requestContext.get() }) ?? {};
834
+ message = sanitize.sanitizeText(message);
835
+ output[level](context, message);
836
+ if (!requestContext.get().suppressLogObservers)
837
+ emitLogRecord({ timestamp: Date.now(), level, message, context });
838
+ };
467
839
  return {
468
840
  trace(message, ctx) {
469
- pinoLogger.trace({ ...context, ...ctx }, message);
841
+ write("trace", message, ctx);
470
842
  },
471
843
  debug(message, ctx) {
472
- pinoLogger.debug({ ...context, ...ctx }, message);
844
+ write("debug", message, ctx);
473
845
  },
474
846
  info(message, ctx) {
475
- pinoLogger.info({ ...context, ...ctx }, message);
847
+ write("info", message, ctx);
476
848
  },
477
849
  warn(message, ctx) {
478
- pinoLogger.warn({ ...context, ...ctx }, message);
850
+ write("warn", message, ctx);
479
851
  },
480
852
  error(message, ctx) {
481
- pinoLogger.error({ ...context, ...ctx }, message);
853
+ write("error", message, ctx);
482
854
  },
483
855
  fatal(message, ctx) {
484
- pinoLogger.fatal({ ...context, ...ctx }, message);
856
+ write("fatal", message, ctx);
485
857
  },
486
858
  child(childContext) {
487
- return wrapPino(pinoLogger.child(childContext), { ...context, ...childContext });
859
+ return wrapPino(resolvePino, () => ({ ...resolveContext(), ...childContext }));
488
860
  }
489
861
  };
490
862
  }
491
863
  function createLogger(options = {}) {
864
+ const localOptions = { ...options, context: { ...options.context } };
865
+ let revision = outputRevision;
866
+ let output = createPinoOutput(localOptions);
867
+ const resolvePino = () => {
868
+ if (revision !== outputRevision) {
869
+ const updated = createPinoOutput(localOptions);
870
+ output.close();
871
+ output = updated;
872
+ revision = outputRevision;
873
+ }
874
+ const level = localOptions.level ?? globalLevel;
875
+ if (output.instance.level !== level)
876
+ output.instance.level = level;
877
+ return output.instance;
878
+ };
879
+ return wrapPino(resolvePino, () => ({ ...globalContext, ...localOptions.context }));
880
+ }
881
+ function createPinoOutput(options) {
492
882
  const level = options.level ?? globalLevel;
493
883
  const format = options.format ?? globalFormat;
494
- const context = { ...globalContext, ...options.context };
495
884
  const pinoOptions = {
496
885
  level,
497
886
  name: options.name,
@@ -506,22 +895,44 @@ function createLogger(options = {}) {
506
895
  };
507
896
  }
508
897
  let pinoInstance;
509
- if (format === "pretty") {
510
- pinoInstance = pino({
511
- ...pinoOptions,
512
- transport: {
898
+ let transport;
899
+ if (format === "pretty" && isPrettyTransportAvailable()) {
900
+ try {
901
+ transport = pino.transport({
513
902
  target: "pino-pretty",
514
903
  options: {
515
904
  colorize: true,
516
905
  translateTime: "SYS:standard",
517
906
  ignore: "pid,hostname"
518
907
  }
908
+ });
909
+ pinoInstance = pino(pinoOptions, transport);
910
+ if ("unref" in transport && typeof transport.unref === "function")
911
+ transport.unref();
912
+ } catch (error) {
913
+ if (isPrettyTransportUnavailable(error)) {
914
+ pinoInstance = pino(pinoOptions);
915
+ } else {
916
+ throw error;
519
917
  }
520
- });
918
+ }
521
919
  } else {
522
920
  pinoInstance = pino(pinoOptions);
523
921
  }
524
- return wrapPino(pinoInstance, context);
922
+ return { instance: pinoInstance, close: () => transport?.end() };
923
+ }
924
+ function isPrettyTransportAvailable() {
925
+ try {
926
+ nodeRequire.resolve("pino-pretty");
927
+ return true;
928
+ } catch {
929
+ return false;
930
+ }
931
+ }
932
+ function isPrettyTransportUnavailable(error) {
933
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
934
+ const normalized = message.toLowerCase();
935
+ 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
936
  }
526
937
  function getLogger() {
527
938
  if (!defaultLogger) {
@@ -538,6 +949,9 @@ var logger = {
538
949
  };
539
950
 
540
951
  // src/core-main.node.ts
952
+ installRequestLocales(new AsyncLocalStorage());
953
+ installRequestContext(new AsyncLocalStorage());
954
+ var YAML_EXTENSION_REGEX = /\.ya?ml$/;
541
955
  function createNodeCore() {
542
956
  const baseCore = createCore({
543
957
  createLogger: logger.createLogger,
@@ -548,6 +962,28 @@ function createNodeCore() {
548
962
  });
549
963
  return {
550
964
  ...baseCore,
965
+ /** Node 专属的可信地址、策略文件读取与已编译访问检查。 */
966
+ access,
967
+ string: {
968
+ ...baseCore.string,
969
+ /**
970
+ * Node 凭证比较使用原生恒时原语;不同长度直接失败,长度本身不受保护。
971
+ * UTF-16LE 保留孤立代理项,避免 UTF-8 替换字符把两个不同字符串误判为相等。
972
+ * @param a - 第一个原始字符串,不做 Unicode 归一化。
973
+ * @param b - 第二个原始字符串,不做 Unicode 归一化。
974
+ * @returns 字符串的 UTF-16 单元是否完全一致。
975
+ * @example
976
+ * ```ts
977
+ * import { core } from '@h-ai/core'
978
+ * const matches = core.string.constantTimeEqual('example-only', 'example-only')
979
+ * ```
980
+ */
981
+ constantTimeEqual(a, b) {
982
+ return a.length === b.length && timingSafeEqual(Buffer.from(a, "utf16le"), Buffer.from(b, "utf16le"));
983
+ }
984
+ },
985
+ /** Zod 校验错误 → i18n 消息映射工具(kit / serv / api-client 共享)。 */
986
+ zodValidation: baseCore.zodValidation,
551
987
  /** 配置管理 */
552
988
  config,
553
989
  /** 初始化 Core */
@@ -567,7 +1003,7 @@ function scanConfigDir(configDir) {
567
1003
  );
568
1004
  for (const file of files) {
569
1005
  const filePath = join(configDir, file);
570
- const baseName = file.replace(/\.ya?ml$/, "");
1006
+ const baseName = file.replace(YAML_EXTENSION_REGEX, "");
571
1007
  if (baseName.startsWith("_")) {
572
1008
  const moduleName = baseName.slice(1);
573
1009
  items.push({
@@ -589,11 +1025,11 @@ function initCore(options = {}) {
589
1025
  if (options.logging) {
590
1026
  core.logger.configure(options.logging);
591
1027
  }
592
- logger2.info("[core] Initializing...");
593
1028
  let configItems = [];
594
1029
  if (options.configDir) {
595
1030
  configItems = scanConfigDir(options.configDir);
596
1031
  }
1032
+ configItems.sort((left, right) => Number(right.name === "core") - Number(left.name === "core"));
597
1033
  for (const item of configItems) {
598
1034
  if (item.name === "core") {
599
1035
  const result = config.load(item.name, item.filePath, CoreConfigSchema);