@h-ai/core 0.1.0-alpha9 → 1.0.0-beta.2
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/README.en.md +653 -0
- package/README.ja.md +653 -0
- package/README.md +264 -33
- package/dist/browser.d.ts +11 -31
- package/dist/browser.js +114 -26
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-FVCFFZP6.js → chunk-BEGTB6GL.js} +9760 -3160
- package/dist/chunk-BEGTB6GL.js.map +1 -0
- package/dist/core-types-_JqVAGGT.d.ts +2742 -0
- package/dist/node.d.ts +13 -70
- package/dist/node.js +491 -55
- package/dist/node.js.map +1 -1
- package/package.json +23 -16
- package/dist/chunk-FVCFFZP6.js.map +0 -1
- package/dist/core-util-module-B-wH5_ZT.d.ts +0 -1626
package/dist/node.js
CHANGED
|
@@ -1,43 +1,358 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { CoreConfigSchema, EnvSchema, HaiCommonError, HaiConfigError,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 (
|
|
433
|
-
|
|
787
|
+
if (process3.stdout.setDefaultEncoding) {
|
|
788
|
+
process3.stdout.setDefaultEncoding("utf8");
|
|
434
789
|
}
|
|
435
|
-
if (
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
841
|
+
write("trace", message, ctx);
|
|
470
842
|
},
|
|
471
843
|
debug(message, ctx) {
|
|
472
|
-
|
|
844
|
+
write("debug", message, ctx);
|
|
473
845
|
},
|
|
474
846
|
info(message, ctx) {
|
|
475
|
-
|
|
847
|
+
write("info", message, ctx);
|
|
476
848
|
},
|
|
477
849
|
warn(message, ctx) {
|
|
478
|
-
|
|
850
|
+
write("warn", message, ctx);
|
|
479
851
|
},
|
|
480
852
|
error(message, ctx) {
|
|
481
|
-
|
|
853
|
+
write("error", message, ctx);
|
|
482
854
|
},
|
|
483
855
|
fatal(message, ctx) {
|
|
484
|
-
|
|
856
|
+
write("fatal", message, ctx);
|
|
485
857
|
},
|
|
486
858
|
child(childContext) {
|
|
487
|
-
return wrapPino(
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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(
|
|
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);
|