@chidchanun/bcp 0.2.16 → 0.2.18
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.md +150 -408
- package/docs/README.md +36 -38
- package/docs/api-manifest.json +27 -19
- package/docs/api-reference.md +257 -305
- package/docs/deployment-platform-v2.md +449 -0
- package/docs/docs-web-manifest.json +7 -3
- package/docs/observability-v3.md +402 -0
- package/docs/platform-manifest.json +30 -4
- package/docs/releases/0.2.17.md +166 -0
- package/docs/releases/0.2.18.md +136 -0
- package/package.json +11 -6
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/auth.mjs +1391 -0
- package/packages/client/src/config.mjs +1132 -0
- package/packages/client/src/deployment.mjs +609 -0
- package/packages/client/src/deployment.ts +20 -0
- package/packages/client/src/observability.mjs +1251 -0
- package/packages/client/src/observability.ts +37 -0
- package/packages/client/src/server.mjs +5615 -0
- package/packages/server/src/deployment.ts +936 -0
- package/packages/server/src/middleware.mjs +631 -0
- package/packages/server/src/observability-v3.ts +878 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
// packages/config/src/index.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
pathToFileURL
|
|
6
|
+
} from "node:url";
|
|
7
|
+
var CONFIG_FILES = [
|
|
8
|
+
"bcp.config.ts",
|
|
9
|
+
"bcp.config.mts",
|
|
10
|
+
"bcp.config.js",
|
|
11
|
+
"bcp.config.mjs"
|
|
12
|
+
];
|
|
13
|
+
var defaultBcpConfig = {
|
|
14
|
+
server: {
|
|
15
|
+
port: 3e3,
|
|
16
|
+
hostname: "localhost",
|
|
17
|
+
bodyLimit: 1024 * 1024
|
|
18
|
+
},
|
|
19
|
+
compression: true,
|
|
20
|
+
build: {
|
|
21
|
+
minify: true,
|
|
22
|
+
sourceMaps: false
|
|
23
|
+
},
|
|
24
|
+
cache: {
|
|
25
|
+
response: true
|
|
26
|
+
},
|
|
27
|
+
experimental: {
|
|
28
|
+
partialHydration: true,
|
|
29
|
+
islands: true
|
|
30
|
+
},
|
|
31
|
+
security: {
|
|
32
|
+
poweredByHeader: false,
|
|
33
|
+
contentSecurityPolicy: false,
|
|
34
|
+
frameOptions: "SAMEORIGIN",
|
|
35
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
36
|
+
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function defineConfig(config) {
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
function getConfigFileNames() {
|
|
43
|
+
return [
|
|
44
|
+
...CONFIG_FILES
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
async function loadBcpConfig(rootDirectory) {
|
|
48
|
+
const matches = CONFIG_FILES.map(
|
|
49
|
+
(fileName) => path.join(
|
|
50
|
+
rootDirectory,
|
|
51
|
+
fileName
|
|
52
|
+
)
|
|
53
|
+
).filter(
|
|
54
|
+
(filePath) => fs.existsSync(
|
|
55
|
+
filePath
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
if (matches.length > 1) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`BCP Framework: multiple config files found: ${matches.map((file2) => path.basename(file2)).join(", ")}. Keep only one bcp.config file.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (matches.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
file: null,
|
|
66
|
+
config: {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const file = matches[0];
|
|
70
|
+
const url = pathToFileURL(
|
|
71
|
+
file
|
|
72
|
+
);
|
|
73
|
+
url.searchParams.set(
|
|
74
|
+
"bcp-config",
|
|
75
|
+
`${Date.now()}-${Math.random()}`
|
|
76
|
+
);
|
|
77
|
+
const module = await import(url.href);
|
|
78
|
+
const config = module.default;
|
|
79
|
+
if (config === void 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`BCP Framework: ${path.basename(file)} must export a default config object.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
assertConfig(
|
|
85
|
+
config,
|
|
86
|
+
path.basename(file)
|
|
87
|
+
);
|
|
88
|
+
return {
|
|
89
|
+
file,
|
|
90
|
+
config
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function resolveBcpConfig(rootDirectory, overrides = {}, environment = process.env) {
|
|
94
|
+
const loaded = await loadBcpConfig(
|
|
95
|
+
rootDirectory
|
|
96
|
+
);
|
|
97
|
+
const config = loaded.config;
|
|
98
|
+
const resolved = {
|
|
99
|
+
server: {
|
|
100
|
+
port: overrides.port ?? parseEnvironmentPort(
|
|
101
|
+
environment.BCP_PORT
|
|
102
|
+
) ?? config.server?.port ?? defaultBcpConfig.server.port,
|
|
103
|
+
hostname: overrides.hostname ?? nonEmptyEnvironmentValue(
|
|
104
|
+
environment.BCP_HOSTNAME
|
|
105
|
+
) ?? config.server?.hostname ?? defaultBcpConfig.server.hostname,
|
|
106
|
+
bodyLimit: parsePositiveInteger(
|
|
107
|
+
environment.BCP_BODY_LIMIT,
|
|
108
|
+
"BCP_BODY_LIMIT"
|
|
109
|
+
) ?? config.server?.bodyLimit ?? defaultBcpConfig.server.bodyLimit
|
|
110
|
+
},
|
|
111
|
+
compression: parseEnvironmentBoolean(
|
|
112
|
+
environment.BCP_COMPRESSION,
|
|
113
|
+
"BCP_COMPRESSION"
|
|
114
|
+
) ?? config.compression ?? defaultBcpConfig.compression,
|
|
115
|
+
build: {
|
|
116
|
+
minify: parseEnvironmentBoolean(
|
|
117
|
+
environment.BCP_BUILD_MINIFY,
|
|
118
|
+
"BCP_BUILD_MINIFY"
|
|
119
|
+
) ?? config.build?.minify ?? defaultBcpConfig.build.minify,
|
|
120
|
+
sourceMaps: parseEnvironmentBoolean(
|
|
121
|
+
environment.BCP_BUILD_SOURCE_MAPS,
|
|
122
|
+
"BCP_BUILD_SOURCE_MAPS"
|
|
123
|
+
) ?? config.build?.sourceMaps ?? defaultBcpConfig.build.sourceMaps
|
|
124
|
+
},
|
|
125
|
+
cache: {
|
|
126
|
+
response: parseEnvironmentBoolean(
|
|
127
|
+
environment.BCP_RESPONSE_CACHE,
|
|
128
|
+
"BCP_RESPONSE_CACHE"
|
|
129
|
+
) ?? config.cache?.response ?? defaultBcpConfig.cache.response
|
|
130
|
+
},
|
|
131
|
+
experimental: {
|
|
132
|
+
partialHydration: parseEnvironmentBoolean(
|
|
133
|
+
environment.BCP_EXPERIMENTAL_PARTIAL_HYDRATION,
|
|
134
|
+
"BCP_EXPERIMENTAL_PARTIAL_HYDRATION"
|
|
135
|
+
) ?? config.experimental?.partialHydration ?? defaultBcpConfig.experimental.partialHydration,
|
|
136
|
+
islands: parseEnvironmentBoolean(
|
|
137
|
+
environment.BCP_EXPERIMENTAL_ISLANDS,
|
|
138
|
+
"BCP_EXPERIMENTAL_ISLANDS"
|
|
139
|
+
) ?? config.experimental?.islands ?? defaultBcpConfig.experimental.islands
|
|
140
|
+
},
|
|
141
|
+
security: {
|
|
142
|
+
poweredByHeader: parseEnvironmentBoolean(
|
|
143
|
+
environment.BCP_POWERED_BY_HEADER,
|
|
144
|
+
"BCP_POWERED_BY_HEADER"
|
|
145
|
+
) ?? config.security?.poweredByHeader ?? defaultBcpConfig.security.poweredByHeader,
|
|
146
|
+
contentSecurityPolicy: parseOptionalHeaderEnvironment(
|
|
147
|
+
environment.BCP_SECURITY_CSP,
|
|
148
|
+
"BCP_SECURITY_CSP"
|
|
149
|
+
) ?? config.security?.contentSecurityPolicy ?? defaultBcpConfig.security.contentSecurityPolicy,
|
|
150
|
+
frameOptions: parseFrameOptionsEnvironment(
|
|
151
|
+
environment.BCP_SECURITY_FRAME_OPTIONS
|
|
152
|
+
) ?? config.security?.frameOptions ?? defaultBcpConfig.security.frameOptions,
|
|
153
|
+
referrerPolicy: nonEmptyEnvironmentValue(
|
|
154
|
+
environment.BCP_SECURITY_REFERRER_POLICY
|
|
155
|
+
) ?? config.security?.referrerPolicy ?? defaultBcpConfig.security.referrerPolicy,
|
|
156
|
+
permissionsPolicy: parseOptionalHeaderEnvironment(
|
|
157
|
+
environment.BCP_SECURITY_PERMISSIONS_POLICY,
|
|
158
|
+
"BCP_SECURITY_PERMISSIONS_POLICY"
|
|
159
|
+
) ?? config.security?.permissionsPolicy ?? defaultBcpConfig.security.permissionsPolicy
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
validateResolvedConfig(
|
|
163
|
+
resolved
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
file: loaded.file,
|
|
167
|
+
config: resolved
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function applyResolvedBcpConfig(config) {
|
|
171
|
+
process.env.BCP_RESOLVED_CONFIG = JSON.stringify(
|
|
172
|
+
config
|
|
173
|
+
);
|
|
174
|
+
process.env.BCP_COMPRESSION = config.compression ? "1" : "0";
|
|
175
|
+
process.env.BCP_BODY_LIMIT = String(
|
|
176
|
+
config.server.bodyLimit
|
|
177
|
+
);
|
|
178
|
+
process.env.BCP_BUILD_MINIFY = config.build.minify ? "1" : "0";
|
|
179
|
+
process.env.BCP_BUILD_SOURCE_MAPS = config.build.sourceMaps ? "1" : "0";
|
|
180
|
+
process.env.BCP_RESPONSE_CACHE = config.cache.response ? "1" : "0";
|
|
181
|
+
process.env.BCP_EXPERIMENTAL_PARTIAL_HYDRATION = config.experimental.partialHydration ? "1" : "0";
|
|
182
|
+
process.env.BCP_EXPERIMENTAL_ISLANDS = config.experimental.islands ? "1" : "0";
|
|
183
|
+
process.env.BCP_POWERED_BY_HEADER = config.security.poweredByHeader ? "1" : "0";
|
|
184
|
+
process.env.BCP_SECURITY_CSP = config.security.contentSecurityPolicy === false ? "0" : config.security.contentSecurityPolicy;
|
|
185
|
+
process.env.BCP_SECURITY_FRAME_OPTIONS = config.security.frameOptions === false ? "0" : config.security.frameOptions;
|
|
186
|
+
process.env.BCP_SECURITY_REFERRER_POLICY = config.security.referrerPolicy;
|
|
187
|
+
process.env.BCP_SECURITY_PERMISSIONS_POLICY = config.security.permissionsPolicy === false ? "0" : config.security.permissionsPolicy;
|
|
188
|
+
}
|
|
189
|
+
function readResolvedBcpConfig() {
|
|
190
|
+
const serialized = process.env.BCP_RESOLVED_CONFIG;
|
|
191
|
+
if (!serialized) {
|
|
192
|
+
return structuredClone(
|
|
193
|
+
defaultBcpConfig
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const value = JSON.parse(
|
|
198
|
+
serialized
|
|
199
|
+
);
|
|
200
|
+
validateResolvedConfig(
|
|
201
|
+
value
|
|
202
|
+
);
|
|
203
|
+
return value;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`BCP Framework: invalid resolved configuration state. ${error instanceof Error ? error.message : String(error)}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function assertConfig(value, label) {
|
|
211
|
+
if (!isPlainObject(
|
|
212
|
+
value
|
|
213
|
+
)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`BCP Framework: ${label} must export an object.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
219
|
+
"server",
|
|
220
|
+
"compression",
|
|
221
|
+
"build",
|
|
222
|
+
"cache",
|
|
223
|
+
"experimental",
|
|
224
|
+
"security"
|
|
225
|
+
]);
|
|
226
|
+
for (const key of Object.keys(
|
|
227
|
+
value
|
|
228
|
+
)) {
|
|
229
|
+
if (!allowed.has(
|
|
230
|
+
key
|
|
231
|
+
)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`BCP Framework: unknown config option "${key}" in ${label}.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (value.server !== void 0) {
|
|
238
|
+
assertObjectKeys(
|
|
239
|
+
value.server,
|
|
240
|
+
"server",
|
|
241
|
+
[
|
|
242
|
+
"port",
|
|
243
|
+
"hostname",
|
|
244
|
+
"bodyLimit"
|
|
245
|
+
]
|
|
246
|
+
);
|
|
247
|
+
if (value.server.port !== void 0) {
|
|
248
|
+
assertPort(
|
|
249
|
+
value.server.port,
|
|
250
|
+
"server.port"
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (value.server.hostname !== void 0 && (typeof value.server.hostname !== "string" || value.server.hostname.trim() === "")) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
"BCP Framework: server.hostname must be a non-empty string."
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (value.server.bodyLimit !== void 0) {
|
|
259
|
+
assertPositiveInteger(
|
|
260
|
+
value.server.bodyLimit,
|
|
261
|
+
"server.bodyLimit"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
assertOptionalBoolean(
|
|
266
|
+
value.compression,
|
|
267
|
+
"compression"
|
|
268
|
+
);
|
|
269
|
+
if (value.build !== void 0) {
|
|
270
|
+
assertObjectKeys(
|
|
271
|
+
value.build,
|
|
272
|
+
"build",
|
|
273
|
+
[
|
|
274
|
+
"minify",
|
|
275
|
+
"sourceMaps"
|
|
276
|
+
]
|
|
277
|
+
);
|
|
278
|
+
assertOptionalBoolean(
|
|
279
|
+
value.build.minify,
|
|
280
|
+
"build.minify"
|
|
281
|
+
);
|
|
282
|
+
assertOptionalBoolean(
|
|
283
|
+
value.build.sourceMaps,
|
|
284
|
+
"build.sourceMaps"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (value.cache !== void 0) {
|
|
288
|
+
assertObjectKeys(
|
|
289
|
+
value.cache,
|
|
290
|
+
"cache",
|
|
291
|
+
[
|
|
292
|
+
"response"
|
|
293
|
+
]
|
|
294
|
+
);
|
|
295
|
+
assertOptionalBoolean(
|
|
296
|
+
value.cache.response,
|
|
297
|
+
"cache.response"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (value.experimental !== void 0) {
|
|
301
|
+
assertObjectKeys(
|
|
302
|
+
value.experimental,
|
|
303
|
+
"experimental",
|
|
304
|
+
[
|
|
305
|
+
"partialHydration",
|
|
306
|
+
"islands"
|
|
307
|
+
]
|
|
308
|
+
);
|
|
309
|
+
assertOptionalBoolean(
|
|
310
|
+
value.experimental.partialHydration,
|
|
311
|
+
"experimental.partialHydration"
|
|
312
|
+
);
|
|
313
|
+
assertOptionalBoolean(
|
|
314
|
+
value.experimental.islands,
|
|
315
|
+
"experimental.islands"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (value.security !== void 0) {
|
|
319
|
+
assertObjectKeys(
|
|
320
|
+
value.security,
|
|
321
|
+
"security",
|
|
322
|
+
[
|
|
323
|
+
"poweredByHeader",
|
|
324
|
+
"contentSecurityPolicy",
|
|
325
|
+
"frameOptions",
|
|
326
|
+
"referrerPolicy",
|
|
327
|
+
"permissionsPolicy"
|
|
328
|
+
]
|
|
329
|
+
);
|
|
330
|
+
assertOptionalBoolean(
|
|
331
|
+
value.security.poweredByHeader,
|
|
332
|
+
"security.poweredByHeader"
|
|
333
|
+
);
|
|
334
|
+
assertOptionalHeaderOrFalse(
|
|
335
|
+
value.security.contentSecurityPolicy,
|
|
336
|
+
"security.contentSecurityPolicy"
|
|
337
|
+
);
|
|
338
|
+
assertFrameOptions(
|
|
339
|
+
value.security.frameOptions,
|
|
340
|
+
"security.frameOptions"
|
|
341
|
+
);
|
|
342
|
+
assertOptionalHeaderString(
|
|
343
|
+
value.security.referrerPolicy,
|
|
344
|
+
"security.referrerPolicy"
|
|
345
|
+
);
|
|
346
|
+
assertOptionalHeaderOrFalse(
|
|
347
|
+
value.security.permissionsPolicy,
|
|
348
|
+
"security.permissionsPolicy"
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function validateResolvedConfig(value) {
|
|
353
|
+
if (!value || !value.server || !value.build || !value.cache || !value.experimental || !value.security) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
"Resolved config is incomplete."
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
assertPort(
|
|
359
|
+
value.server.port,
|
|
360
|
+
"server.port"
|
|
361
|
+
);
|
|
362
|
+
assertPositiveInteger(
|
|
363
|
+
value.server.bodyLimit,
|
|
364
|
+
"server.bodyLimit"
|
|
365
|
+
);
|
|
366
|
+
if (typeof value.server.hostname !== "string" || value.server.hostname.trim() === "") {
|
|
367
|
+
throw new Error(
|
|
368
|
+
"server.hostname must be a non-empty string."
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
for (const [
|
|
372
|
+
label,
|
|
373
|
+
item
|
|
374
|
+
] of [
|
|
375
|
+
["compression", value.compression],
|
|
376
|
+
["build.minify", value.build.minify],
|
|
377
|
+
["build.sourceMaps", value.build.sourceMaps],
|
|
378
|
+
["cache.response", value.cache.response],
|
|
379
|
+
["experimental.partialHydration", value.experimental.partialHydration],
|
|
380
|
+
["experimental.islands", value.experimental.islands],
|
|
381
|
+
["security.poweredByHeader", value.security.poweredByHeader]
|
|
382
|
+
]) {
|
|
383
|
+
if (typeof item !== "boolean") {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`${label} must be boolean.`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
assertHeaderOrFalse(
|
|
390
|
+
value.security.contentSecurityPolicy,
|
|
391
|
+
"security.contentSecurityPolicy"
|
|
392
|
+
);
|
|
393
|
+
assertFrameOptions(
|
|
394
|
+
value.security.frameOptions,
|
|
395
|
+
"security.frameOptions"
|
|
396
|
+
);
|
|
397
|
+
assertHeaderString(
|
|
398
|
+
value.security.referrerPolicy,
|
|
399
|
+
"security.referrerPolicy"
|
|
400
|
+
);
|
|
401
|
+
assertHeaderOrFalse(
|
|
402
|
+
value.security.permissionsPolicy,
|
|
403
|
+
"security.permissionsPolicy"
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
function assertObjectKeys(value, label, keys) {
|
|
407
|
+
if (!isPlainObject(
|
|
408
|
+
value
|
|
409
|
+
)) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`BCP Framework: ${label} must be an object.`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
const allowed = new Set(
|
|
415
|
+
keys
|
|
416
|
+
);
|
|
417
|
+
for (const key of Object.keys(
|
|
418
|
+
value
|
|
419
|
+
)) {
|
|
420
|
+
if (!allowed.has(
|
|
421
|
+
key
|
|
422
|
+
)) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`BCP Framework: unknown config option "${label}.${key}".`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function assertOptionalBoolean(value, label) {
|
|
430
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`BCP Framework: ${label} must be boolean.`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function assertPort(value, label) {
|
|
437
|
+
if (!Number.isInteger(
|
|
438
|
+
value
|
|
439
|
+
) || Number(value) < 1 || Number(value) > 65535) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`BCP Framework: ${label} must be an integer between 1 and 65535.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function assertPositiveInteger(value, label) {
|
|
446
|
+
if (!Number.isInteger(
|
|
447
|
+
value
|
|
448
|
+
) || Number(value) < 1) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`BCP Framework: ${label} must be a positive integer.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function assertOptionalHeaderOrFalse(value, label) {
|
|
455
|
+
if (value === void 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
assertHeaderOrFalse(
|
|
459
|
+
value,
|
|
460
|
+
label
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function assertHeaderOrFalse(value, label) {
|
|
464
|
+
if (value === false) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
assertHeaderString(
|
|
468
|
+
value,
|
|
469
|
+
label
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
function assertOptionalHeaderString(value, label) {
|
|
473
|
+
if (value === void 0) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
assertHeaderString(
|
|
477
|
+
value,
|
|
478
|
+
label
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
function assertHeaderString(value, label) {
|
|
482
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`BCP Framework: ${label} must be a non-empty string.`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (value.includes("\r") || value.includes("\n") || value.includes("\0")) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`BCP Framework: ${label} must not contain control characters.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function assertFrameOptions(value, label) {
|
|
494
|
+
if (value === void 0 || value === false || value === "DENY" || value === "SAMEORIGIN") {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
throw new Error(
|
|
498
|
+
`BCP Framework: ${label} must be "DENY", "SAMEORIGIN", or false.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
function parseEnvironmentPort(value) {
|
|
502
|
+
if (value === void 0 || value.trim() === "") {
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
const port = Number(
|
|
506
|
+
value
|
|
507
|
+
);
|
|
508
|
+
assertPort(
|
|
509
|
+
port,
|
|
510
|
+
"BCP_PORT"
|
|
511
|
+
);
|
|
512
|
+
return port;
|
|
513
|
+
}
|
|
514
|
+
function parsePositiveInteger(value, label) {
|
|
515
|
+
if (value === void 0 || value.trim() === "") {
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
const parsed = Number(
|
|
519
|
+
value
|
|
520
|
+
);
|
|
521
|
+
assertPositiveInteger(
|
|
522
|
+
parsed,
|
|
523
|
+
label
|
|
524
|
+
);
|
|
525
|
+
return parsed;
|
|
526
|
+
}
|
|
527
|
+
function parseEnvironmentBoolean(value, label) {
|
|
528
|
+
if (value === void 0 || value.trim() === "") {
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
const normalized = value.trim().toLowerCase();
|
|
532
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
throw new Error(
|
|
539
|
+
`BCP Framework: ${label} must be a boolean value (true/false, 1/0, yes/no, on/off).`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
function parseOptionalHeaderEnvironment(value, label) {
|
|
543
|
+
if (value === void 0 || value.trim() === "") {
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
const normalized = value.trim();
|
|
547
|
+
const lowered = normalized.toLowerCase();
|
|
548
|
+
if (lowered === "0" || lowered === "false" || lowered === "off") {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
assertHeaderString(
|
|
552
|
+
normalized,
|
|
553
|
+
label
|
|
554
|
+
);
|
|
555
|
+
return normalized;
|
|
556
|
+
}
|
|
557
|
+
function parseFrameOptionsEnvironment(value) {
|
|
558
|
+
if (value === void 0 || value.trim() === "") {
|
|
559
|
+
return void 0;
|
|
560
|
+
}
|
|
561
|
+
const normalized = value.trim().toUpperCase();
|
|
562
|
+
if (normalized === "0" || normalized === "FALSE" || normalized === "OFF") {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
if (normalized === "DENY" || normalized === "SAMEORIGIN") {
|
|
566
|
+
return normalized;
|
|
567
|
+
}
|
|
568
|
+
throw new Error(
|
|
569
|
+
"BCP Framework: BCP_SECURITY_FRAME_OPTIONS must be DENY, SAMEORIGIN, or off."
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
function nonEmptyEnvironmentValue(value) {
|
|
573
|
+
const normalized = value?.trim();
|
|
574
|
+
return normalized ? normalized : void 0;
|
|
575
|
+
}
|
|
576
|
+
function isPlainObject(value) {
|
|
577
|
+
return typeof value === "object" && value !== null && !Array.isArray(
|
|
578
|
+
value
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// packages/config/src/environment-schema.ts
|
|
583
|
+
function defineEnvironment(schema) {
|
|
584
|
+
assertEnvironmentSchema(
|
|
585
|
+
schema
|
|
586
|
+
);
|
|
587
|
+
return schema;
|
|
588
|
+
}
|
|
589
|
+
function applyEnvironmentDefaults(schema, target = process.env) {
|
|
590
|
+
assertEnvironmentSchema(
|
|
591
|
+
schema
|
|
592
|
+
);
|
|
593
|
+
let applied = 0;
|
|
594
|
+
for (const [
|
|
595
|
+
key,
|
|
596
|
+
rule
|
|
597
|
+
] of Object.entries(
|
|
598
|
+
schema
|
|
599
|
+
)) {
|
|
600
|
+
if (rule.default === void 0 || target[key] !== void 0 && target[key] !== "") {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
target[key] = serializeEnvironmentValue(
|
|
604
|
+
rule.default
|
|
605
|
+
);
|
|
606
|
+
applied++;
|
|
607
|
+
}
|
|
608
|
+
return applied;
|
|
609
|
+
}
|
|
610
|
+
function validateEnvironment(schema, source = process.env) {
|
|
611
|
+
assertEnvironmentSchema(
|
|
612
|
+
schema
|
|
613
|
+
);
|
|
614
|
+
const issues = [];
|
|
615
|
+
const values = {};
|
|
616
|
+
let present = 0;
|
|
617
|
+
let defaults = 0;
|
|
618
|
+
for (const [
|
|
619
|
+
key,
|
|
620
|
+
rule
|
|
621
|
+
] of Object.entries(
|
|
622
|
+
schema
|
|
623
|
+
)) {
|
|
624
|
+
if (rule.secret && key.startsWith(
|
|
625
|
+
"BCP_PUBLIC_"
|
|
626
|
+
)) {
|
|
627
|
+
issues.push({
|
|
628
|
+
key,
|
|
629
|
+
severity: "error",
|
|
630
|
+
code: "public_secret",
|
|
631
|
+
message: `${key} is marked secret but uses the BCP_PUBLIC_ prefix. Public variables are embedded in client bundles.`
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
const raw = source[key];
|
|
635
|
+
const hasSourceValue = raw !== void 0 && raw !== "";
|
|
636
|
+
if (hasSourceValue) {
|
|
637
|
+
present++;
|
|
638
|
+
}
|
|
639
|
+
const input = hasSourceValue ? raw : rule.default;
|
|
640
|
+
if (input === void 0) {
|
|
641
|
+
if (rule.required) {
|
|
642
|
+
issues.push({
|
|
643
|
+
key,
|
|
644
|
+
severity: "error",
|
|
645
|
+
code: "missing",
|
|
646
|
+
message: `${key} is required but was not provided.`
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (!hasSourceValue) {
|
|
652
|
+
defaults++;
|
|
653
|
+
}
|
|
654
|
+
const parsed = parseEnvironmentRuleValue(
|
|
655
|
+
key,
|
|
656
|
+
input,
|
|
657
|
+
rule,
|
|
658
|
+
issues
|
|
659
|
+
);
|
|
660
|
+
if (parsed !== void 0) {
|
|
661
|
+
values[key] = parsed;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
ok: !issues.some(
|
|
666
|
+
(issue) => issue.severity === "error"
|
|
667
|
+
),
|
|
668
|
+
checked: Object.keys(
|
|
669
|
+
schema
|
|
670
|
+
).length,
|
|
671
|
+
present,
|
|
672
|
+
defaults,
|
|
673
|
+
issues,
|
|
674
|
+
values
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function assertEnvironmentSchema(schema) {
|
|
678
|
+
if (!isPlainObject2(
|
|
679
|
+
schema
|
|
680
|
+
)) {
|
|
681
|
+
throw new Error(
|
|
682
|
+
"BCP Framework: environment config must be an object."
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
for (const [
|
|
686
|
+
key,
|
|
687
|
+
value
|
|
688
|
+
] of Object.entries(
|
|
689
|
+
schema
|
|
690
|
+
)) {
|
|
691
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(
|
|
692
|
+
key
|
|
693
|
+
)) {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`BCP Framework: invalid environment schema key "${key}".`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
assertEnvironmentRule(
|
|
699
|
+
key,
|
|
700
|
+
value
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function assertEnvironmentRule(key, value) {
|
|
705
|
+
if (!isPlainObject2(
|
|
706
|
+
value
|
|
707
|
+
)) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`BCP Framework: environment.${key} must be an object.`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
713
|
+
"type",
|
|
714
|
+
"required",
|
|
715
|
+
"secret",
|
|
716
|
+
"minLength",
|
|
717
|
+
"maxLength",
|
|
718
|
+
"min",
|
|
719
|
+
"max",
|
|
720
|
+
"default",
|
|
721
|
+
"description"
|
|
722
|
+
]);
|
|
723
|
+
for (const property of Object.keys(
|
|
724
|
+
value
|
|
725
|
+
)) {
|
|
726
|
+
if (!allowed.has(
|
|
727
|
+
property
|
|
728
|
+
)) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
`BCP Framework: unknown config option "environment.${key}.${property}".`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (value.type !== "string" && value.type !== "number" && value.type !== "boolean" && value.type !== "url") {
|
|
735
|
+
throw new Error(
|
|
736
|
+
`BCP Framework: environment.${key}.type must be string, number, boolean, or url.`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
assertOptionalBoolean2(
|
|
740
|
+
value.required,
|
|
741
|
+
`environment.${key}.required`
|
|
742
|
+
);
|
|
743
|
+
assertOptionalBoolean2(
|
|
744
|
+
value.secret,
|
|
745
|
+
`environment.${key}.secret`
|
|
746
|
+
);
|
|
747
|
+
assertOptionalNonNegativeInteger(
|
|
748
|
+
value.minLength,
|
|
749
|
+
`environment.${key}.minLength`
|
|
750
|
+
);
|
|
751
|
+
assertOptionalNonNegativeInteger(
|
|
752
|
+
value.maxLength,
|
|
753
|
+
`environment.${key}.maxLength`
|
|
754
|
+
);
|
|
755
|
+
assertOptionalFiniteNumber(
|
|
756
|
+
value.min,
|
|
757
|
+
`environment.${key}.min`
|
|
758
|
+
);
|
|
759
|
+
assertOptionalFiniteNumber(
|
|
760
|
+
value.max,
|
|
761
|
+
`environment.${key}.max`
|
|
762
|
+
);
|
|
763
|
+
if (value.description !== void 0 && (typeof value.description !== "string" || value.description.trim() === "")) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`BCP Framework: environment.${key}.description must be a non-empty string.`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
if (value.minLength !== void 0 && value.maxLength !== void 0 && value.minLength > value.maxLength) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
`BCP Framework: environment.${key}.minLength cannot be greater than maxLength.`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
if (value.min !== void 0 && value.max !== void 0 && value.min > value.max) {
|
|
774
|
+
throw new Error(
|
|
775
|
+
`BCP Framework: environment.${key}.min cannot be greater than max.`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (value.default !== void 0) {
|
|
779
|
+
const issues = [];
|
|
780
|
+
const parsed = parseEnvironmentRuleValue(
|
|
781
|
+
key,
|
|
782
|
+
value.default,
|
|
783
|
+
value,
|
|
784
|
+
issues
|
|
785
|
+
);
|
|
786
|
+
if (parsed === void 0 || issues.some(
|
|
787
|
+
(issue) => issue.severity === "error"
|
|
788
|
+
)) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`BCP Framework: environment.${key}.default does not satisfy its declared rule.`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function parseEnvironmentRuleValue(key, input, rule, issues) {
|
|
796
|
+
if (rule.type === "string" || rule.type === "url") {
|
|
797
|
+
if (typeof input !== "string") {
|
|
798
|
+
issues.push({
|
|
799
|
+
key,
|
|
800
|
+
severity: "error",
|
|
801
|
+
code: "invalid_type",
|
|
802
|
+
message: `${key} must be a ${rule.type}.`
|
|
803
|
+
});
|
|
804
|
+
return void 0;
|
|
805
|
+
}
|
|
806
|
+
if (rule.minLength !== void 0 && input.length < rule.minLength) {
|
|
807
|
+
issues.push({
|
|
808
|
+
key,
|
|
809
|
+
severity: "error",
|
|
810
|
+
code: "too_short",
|
|
811
|
+
message: `${key} must contain at least ${rule.minLength} character(s).`
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (rule.maxLength !== void 0 && input.length > rule.maxLength) {
|
|
815
|
+
issues.push({
|
|
816
|
+
key,
|
|
817
|
+
severity: "error",
|
|
818
|
+
code: "too_long",
|
|
819
|
+
message: `${key} must contain at most ${rule.maxLength} character(s).`
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (rule.type === "url") {
|
|
823
|
+
try {
|
|
824
|
+
const url = new URL(
|
|
825
|
+
input
|
|
826
|
+
);
|
|
827
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
828
|
+
throw new Error(
|
|
829
|
+
"unsupported protocol"
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
} catch {
|
|
833
|
+
issues.push({
|
|
834
|
+
key,
|
|
835
|
+
severity: "error",
|
|
836
|
+
code: "invalid_type",
|
|
837
|
+
message: `${key} must be an absolute http(s) URL.`
|
|
838
|
+
});
|
|
839
|
+
return void 0;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return input;
|
|
843
|
+
}
|
|
844
|
+
if (rule.type === "number") {
|
|
845
|
+
const parsed = typeof input === "number" ? input : typeof input === "string" && input.trim() !== "" ? Number(
|
|
846
|
+
input
|
|
847
|
+
) : Number.NaN;
|
|
848
|
+
if (!Number.isFinite(
|
|
849
|
+
parsed
|
|
850
|
+
)) {
|
|
851
|
+
issues.push({
|
|
852
|
+
key,
|
|
853
|
+
severity: "error",
|
|
854
|
+
code: "invalid_type",
|
|
855
|
+
message: `${key} must be a finite number.`
|
|
856
|
+
});
|
|
857
|
+
return void 0;
|
|
858
|
+
}
|
|
859
|
+
if (rule.min !== void 0 && parsed < rule.min) {
|
|
860
|
+
issues.push({
|
|
861
|
+
key,
|
|
862
|
+
severity: "error",
|
|
863
|
+
code: "too_small",
|
|
864
|
+
message: `${key} must be greater than or equal to ${rule.min}.`
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
if (rule.max !== void 0 && parsed > rule.max) {
|
|
868
|
+
issues.push({
|
|
869
|
+
key,
|
|
870
|
+
severity: "error",
|
|
871
|
+
code: "too_large",
|
|
872
|
+
message: `${key} must be less than or equal to ${rule.max}.`
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return parsed;
|
|
876
|
+
}
|
|
877
|
+
if (typeof input === "boolean") {
|
|
878
|
+
return input;
|
|
879
|
+
}
|
|
880
|
+
if (typeof input === "string") {
|
|
881
|
+
const normalized = input.trim().toLowerCase();
|
|
882
|
+
if ([
|
|
883
|
+
"1",
|
|
884
|
+
"true",
|
|
885
|
+
"yes",
|
|
886
|
+
"on"
|
|
887
|
+
].includes(
|
|
888
|
+
normalized
|
|
889
|
+
)) {
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
if ([
|
|
893
|
+
"0",
|
|
894
|
+
"false",
|
|
895
|
+
"no",
|
|
896
|
+
"off"
|
|
897
|
+
].includes(
|
|
898
|
+
normalized
|
|
899
|
+
)) {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
issues.push({
|
|
904
|
+
key,
|
|
905
|
+
severity: "error",
|
|
906
|
+
code: "invalid_type",
|
|
907
|
+
message: `${key} must be a boolean value (true/false, 1/0, yes/no, on/off).`
|
|
908
|
+
});
|
|
909
|
+
return void 0;
|
|
910
|
+
}
|
|
911
|
+
function serializeEnvironmentValue(value) {
|
|
912
|
+
if (typeof value === "boolean") {
|
|
913
|
+
return value ? "true" : "false";
|
|
914
|
+
}
|
|
915
|
+
return String(
|
|
916
|
+
value
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
function assertOptionalBoolean2(value, label) {
|
|
920
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
921
|
+
throw new Error(
|
|
922
|
+
`BCP Framework: ${label} must be boolean.`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function assertOptionalNonNegativeInteger(value, label) {
|
|
927
|
+
if (value !== void 0 && (!Number.isInteger(
|
|
928
|
+
value
|
|
929
|
+
) || Number(value) < 0)) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
`BCP Framework: ${label} must be a non-negative integer.`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function assertOptionalFiniteNumber(value, label) {
|
|
936
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(
|
|
937
|
+
value
|
|
938
|
+
))) {
|
|
939
|
+
throw new Error(
|
|
940
|
+
`BCP Framework: ${label} must be a finite number.`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function isPlainObject2(value) {
|
|
945
|
+
return typeof value === "object" && value !== null && !Array.isArray(
|
|
946
|
+
value
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// packages/config/src/environment-loader.ts
|
|
951
|
+
import fs2 from "node:fs";
|
|
952
|
+
import path2 from "node:path";
|
|
953
|
+
import {
|
|
954
|
+
pathToFileURL as pathToFileURL2
|
|
955
|
+
} from "node:url";
|
|
956
|
+
var ENVIRONMENT_SCHEMA_FILES = [
|
|
957
|
+
"bcp.environment.ts",
|
|
958
|
+
"bcp.environment.mts",
|
|
959
|
+
"bcp.environment.js",
|
|
960
|
+
"bcp.environment.mjs"
|
|
961
|
+
];
|
|
962
|
+
function getEnvironmentSchemaFileNames() {
|
|
963
|
+
return [
|
|
964
|
+
...ENVIRONMENT_SCHEMA_FILES
|
|
965
|
+
];
|
|
966
|
+
}
|
|
967
|
+
async function loadBcpEnvironmentSchema(rootDirectory) {
|
|
968
|
+
const matches = ENVIRONMENT_SCHEMA_FILES.map(
|
|
969
|
+
(fileName) => path2.join(
|
|
970
|
+
rootDirectory,
|
|
971
|
+
fileName
|
|
972
|
+
)
|
|
973
|
+
).filter(
|
|
974
|
+
(filePath) => fs2.existsSync(
|
|
975
|
+
filePath
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
if (matches.length > 1) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`BCP Framework: multiple environment schema files found: ${matches.map((file2) => path2.basename(file2)).join(", ")}. Keep only one bcp.environment file.`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (matches.length === 0) {
|
|
984
|
+
return {
|
|
985
|
+
file: null,
|
|
986
|
+
schema: {}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const file = matches[0];
|
|
990
|
+
const url = pathToFileURL2(
|
|
991
|
+
file
|
|
992
|
+
);
|
|
993
|
+
url.searchParams.set(
|
|
994
|
+
"bcp-environment",
|
|
995
|
+
`${Date.now()}-${Math.random()}`
|
|
996
|
+
);
|
|
997
|
+
const module = await import(url.href);
|
|
998
|
+
const schema = module.default;
|
|
999
|
+
if (schema === void 0) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`BCP Framework: ${path2.basename(file)} must export a default environment schema.`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
assertEnvironmentSchema(
|
|
1005
|
+
schema
|
|
1006
|
+
);
|
|
1007
|
+
return {
|
|
1008
|
+
file,
|
|
1009
|
+
schema
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// packages/config/src/diagnostics.ts
|
|
1014
|
+
import path3 from "node:path";
|
|
1015
|
+
async function diagnoseBcpConfiguration(options) {
|
|
1016
|
+
const environment = options.environment ?? process.env;
|
|
1017
|
+
const resolved = await resolveBcpConfig(
|
|
1018
|
+
options.rootDirectory,
|
|
1019
|
+
options.overrides ?? {},
|
|
1020
|
+
environment
|
|
1021
|
+
);
|
|
1022
|
+
const loadedSchema = await loadBcpEnvironmentSchema(
|
|
1023
|
+
options.rootDirectory
|
|
1024
|
+
);
|
|
1025
|
+
const environmentValidation = validateEnvironment(
|
|
1026
|
+
loadedSchema.schema,
|
|
1027
|
+
environment
|
|
1028
|
+
);
|
|
1029
|
+
const diagnostics = [];
|
|
1030
|
+
appendEnvironmentDiagnostics(
|
|
1031
|
+
diagnostics,
|
|
1032
|
+
environmentValidation
|
|
1033
|
+
);
|
|
1034
|
+
appendRuntimeDiagnostics(
|
|
1035
|
+
diagnostics,
|
|
1036
|
+
options.mode,
|
|
1037
|
+
resolved.config,
|
|
1038
|
+
environment
|
|
1039
|
+
);
|
|
1040
|
+
return {
|
|
1041
|
+
ok: !diagnostics.some(
|
|
1042
|
+
(diagnostic) => diagnostic.severity === "error"
|
|
1043
|
+
),
|
|
1044
|
+
mode: options.mode,
|
|
1045
|
+
configFile: resolved.file ? path3.basename(
|
|
1046
|
+
resolved.file
|
|
1047
|
+
) : null,
|
|
1048
|
+
environmentSchemaFile: loadedSchema.file ? path3.basename(
|
|
1049
|
+
loadedSchema.file
|
|
1050
|
+
) : null,
|
|
1051
|
+
resolvedConfig: resolved.config,
|
|
1052
|
+
environment: {
|
|
1053
|
+
checked: environmentValidation.checked,
|
|
1054
|
+
present: environmentValidation.present,
|
|
1055
|
+
defaults: environmentValidation.defaults
|
|
1056
|
+
},
|
|
1057
|
+
diagnostics
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function assertConfigurationDiagnostics(report) {
|
|
1061
|
+
if (report.ok) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const errors = report.diagnostics.filter(
|
|
1065
|
+
(diagnostic) => diagnostic.severity === "error"
|
|
1066
|
+
).map(
|
|
1067
|
+
(diagnostic) => diagnostic.message
|
|
1068
|
+
);
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
`BCP Configuration Error:
|
|
1071
|
+
${errors.map((message) => `- ${message}`).join("\n")}`
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
function appendEnvironmentDiagnostics(diagnostics, validation) {
|
|
1075
|
+
for (const issue of validation.issues) {
|
|
1076
|
+
diagnostics.push({
|
|
1077
|
+
severity: issue.severity,
|
|
1078
|
+
code: `environment.${issue.code}`,
|
|
1079
|
+
message: issue.message
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function appendRuntimeDiagnostics(diagnostics, mode, config, environment) {
|
|
1084
|
+
if (mode !== "production") {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (config.build.sourceMaps) {
|
|
1088
|
+
diagnostics.push({
|
|
1089
|
+
severity: "warning",
|
|
1090
|
+
code: "production.source_maps",
|
|
1091
|
+
message: "Production source maps are enabled. Confirm that exposing source information is intentional."
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
if (config.security.poweredByHeader) {
|
|
1095
|
+
diagnostics.push({
|
|
1096
|
+
severity: "warning",
|
|
1097
|
+
code: "production.powered_by",
|
|
1098
|
+
message: "security.poweredByHeader is enabled in production."
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
if (config.security.contentSecurityPolicy === false) {
|
|
1102
|
+
diagnostics.push({
|
|
1103
|
+
severity: "warning",
|
|
1104
|
+
code: "production.csp_disabled",
|
|
1105
|
+
message: "Content-Security-Policy is disabled. Configure a policy when the application can support one."
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
const trustProxy = environment.BCP_TRUST_PROXY?.trim().toLowerCase();
|
|
1109
|
+
if (trustProxy === "true" || trustProxy === "1" || trustProxy === "yes" || trustProxy === "on") {
|
|
1110
|
+
diagnostics.push({
|
|
1111
|
+
severity: "warning",
|
|
1112
|
+
code: "production.trust_proxy",
|
|
1113
|
+
message: "BCP_TRUST_PROXY is enabled. Ensure untrusted clients cannot bypass the trusted reverse proxy/load balancer."
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
export {
|
|
1118
|
+
applyEnvironmentDefaults,
|
|
1119
|
+
applyResolvedBcpConfig,
|
|
1120
|
+
assertConfigurationDiagnostics,
|
|
1121
|
+
defaultBcpConfig,
|
|
1122
|
+
defineConfig,
|
|
1123
|
+
defineEnvironment,
|
|
1124
|
+
diagnoseBcpConfiguration,
|
|
1125
|
+
getConfigFileNames,
|
|
1126
|
+
getEnvironmentSchemaFileNames,
|
|
1127
|
+
loadBcpConfig,
|
|
1128
|
+
loadBcpEnvironmentSchema,
|
|
1129
|
+
readResolvedBcpConfig,
|
|
1130
|
+
resolveBcpConfig,
|
|
1131
|
+
validateEnvironment
|
|
1132
|
+
};
|