@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/README.en.md +820 -0
- package/README.ja.md +820 -0
- package/README.md +715 -317
- 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-MYNB4HIZ.js} +9786 -3163
- package/dist/chunk-MYNB4HIZ.js.map +1 -0
- package/dist/core-types-DM_AiIaS.d.ts +2885 -0
- package/dist/node.d.ts +13 -70
- package/dist/node.js +750 -88
- 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,61 +1,533 @@
|
|
|
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, 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
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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 (
|
|
433
|
-
|
|
1013
|
+
if (process4.stdout.setDefaultEncoding) {
|
|
1014
|
+
process4.stdout.setDefaultEncoding("utf8");
|
|
434
1015
|
}
|
|
435
|
-
if (
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1067
|
+
write("trace", message, ctx);
|
|
470
1068
|
},
|
|
471
1069
|
debug(message, ctx) {
|
|
472
|
-
|
|
1070
|
+
write("debug", message, ctx);
|
|
473
1071
|
},
|
|
474
1072
|
info(message, ctx) {
|
|
475
|
-
|
|
1073
|
+
write("info", message, ctx);
|
|
476
1074
|
},
|
|
477
1075
|
warn(message, ctx) {
|
|
478
|
-
|
|
1076
|
+
write("warn", message, ctx);
|
|
479
1077
|
},
|
|
480
1078
|
error(message, ctx) {
|
|
481
|
-
|
|
1079
|
+
write("error", message, ctx);
|
|
482
1080
|
},
|
|
483
1081
|
fatal(message, ctx) {
|
|
484
|
-
|
|
1082
|
+
write("fatal", message, ctx);
|
|
485
1083
|
},
|
|
486
1084
|
child(childContext) {
|
|
487
|
-
return wrapPino(
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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 {
|