@chidchanun/bcp 0.2.1 → 0.2.3

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.
@@ -0,0 +1,226 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ resolveBcpConfig,
5
+ type ResolveBcpConfigOverrides,
6
+ type ResolvedBcpConfig,
7
+ } from "./index.js";
8
+ import {
9
+ loadBcpEnvironmentSchema,
10
+ } from "./environment-loader.js";
11
+ import {
12
+ validateEnvironment,
13
+ type BcpEnvironmentValidationResult,
14
+ } from "./environment-schema.js";
15
+
16
+ export type BcpConfigurationDiagnosticSeverity =
17
+ | "error"
18
+ | "warning";
19
+
20
+ export interface BcpConfigurationDiagnostic {
21
+ severity: BcpConfigurationDiagnosticSeverity;
22
+ code: string;
23
+ message: string;
24
+ }
25
+
26
+ export interface BcpConfigurationDiagnosticsReport {
27
+ ok: boolean;
28
+ mode: string;
29
+ configFile: string | null;
30
+ environmentSchemaFile: string | null;
31
+ resolvedConfig: ResolvedBcpConfig;
32
+ environment: {
33
+ checked: number;
34
+ present: number;
35
+ defaults: number;
36
+ };
37
+ diagnostics: BcpConfigurationDiagnostic[];
38
+ }
39
+
40
+ export interface DiagnoseBcpConfigurationOptions {
41
+ rootDirectory: string;
42
+ mode: string;
43
+ environment?: NodeJS.ProcessEnv;
44
+ overrides?: ResolveBcpConfigOverrides;
45
+ }
46
+
47
+ export async function diagnoseBcpConfiguration(
48
+ options: DiagnoseBcpConfigurationOptions
49
+ ): Promise<BcpConfigurationDiagnosticsReport> {
50
+ const environment =
51
+ options.environment ??
52
+ process.env;
53
+ const resolved =
54
+ await resolveBcpConfig(
55
+ options.rootDirectory,
56
+ options.overrides ?? {},
57
+ environment
58
+ );
59
+ const loadedSchema =
60
+ await loadBcpEnvironmentSchema(
61
+ options.rootDirectory
62
+ );
63
+ const environmentValidation =
64
+ validateEnvironment(
65
+ loadedSchema.schema,
66
+ environment
67
+ );
68
+ const diagnostics:
69
+ BcpConfigurationDiagnostic[] = [];
70
+
71
+ appendEnvironmentDiagnostics(
72
+ diagnostics,
73
+ environmentValidation
74
+ );
75
+ appendRuntimeDiagnostics(
76
+ diagnostics,
77
+ options.mode,
78
+ resolved.config,
79
+ environment
80
+ );
81
+
82
+ return {
83
+ ok:
84
+ !diagnostics.some(
85
+ (diagnostic) =>
86
+ diagnostic.severity ===
87
+ "error"
88
+ ),
89
+ mode:
90
+ options.mode,
91
+ configFile:
92
+ resolved.file
93
+ ? path.basename(
94
+ resolved.file
95
+ )
96
+ : null,
97
+ environmentSchemaFile:
98
+ loadedSchema.file
99
+ ? path.basename(
100
+ loadedSchema.file
101
+ )
102
+ : null,
103
+ resolvedConfig:
104
+ resolved.config,
105
+ environment: {
106
+ checked:
107
+ environmentValidation.checked,
108
+ present:
109
+ environmentValidation.present,
110
+ defaults:
111
+ environmentValidation.defaults,
112
+ },
113
+ diagnostics,
114
+ };
115
+ }
116
+
117
+ export function assertConfigurationDiagnostics(
118
+ report: BcpConfigurationDiagnosticsReport
119
+ ): void {
120
+ if (
121
+ report.ok
122
+ ) {
123
+ return;
124
+ }
125
+
126
+ const errors =
127
+ report.diagnostics
128
+ .filter(
129
+ (diagnostic) =>
130
+ diagnostic.severity ===
131
+ "error"
132
+ )
133
+ .map(
134
+ (diagnostic) =>
135
+ diagnostic.message
136
+ );
137
+
138
+ throw new Error(
139
+ `BCP Configuration Error:\n${errors.map((message) => `- ${message}`).join("\n")}`
140
+ );
141
+ }
142
+
143
+ function appendEnvironmentDiagnostics(
144
+ diagnostics: BcpConfigurationDiagnostic[],
145
+ validation: BcpEnvironmentValidationResult
146
+ ): void {
147
+ for (
148
+ const issue
149
+ of validation.issues
150
+ ) {
151
+ diagnostics.push({
152
+ severity:
153
+ issue.severity,
154
+ code:
155
+ `environment.${issue.code}`,
156
+ message:
157
+ issue.message,
158
+ });
159
+ }
160
+ }
161
+
162
+ function appendRuntimeDiagnostics(
163
+ diagnostics: BcpConfigurationDiagnostic[],
164
+ mode: string,
165
+ config: ResolvedBcpConfig,
166
+ environment: NodeJS.ProcessEnv
167
+ ): void {
168
+ if (
169
+ mode !== "production"
170
+ ) {
171
+ return;
172
+ }
173
+
174
+ if (
175
+ config.build.sourceMaps
176
+ ) {
177
+ diagnostics.push({
178
+ severity: "warning",
179
+ code: "production.source_maps",
180
+ message:
181
+ "Production source maps are enabled. Confirm that exposing source information is intentional.",
182
+ });
183
+ }
184
+
185
+ if (
186
+ config.security.poweredByHeader
187
+ ) {
188
+ diagnostics.push({
189
+ severity: "warning",
190
+ code: "production.powered_by",
191
+ message:
192
+ "security.poweredByHeader is enabled in production.",
193
+ });
194
+ }
195
+
196
+ if (
197
+ config.security.contentSecurityPolicy ===
198
+ false
199
+ ) {
200
+ diagnostics.push({
201
+ severity: "warning",
202
+ code: "production.csp_disabled",
203
+ message:
204
+ "Content-Security-Policy is disabled. Configure a policy when the application can support one.",
205
+ });
206
+ }
207
+
208
+ const trustProxy =
209
+ environment.BCP_TRUST_PROXY
210
+ ?.trim()
211
+ .toLowerCase();
212
+
213
+ if (
214
+ trustProxy === "true" ||
215
+ trustProxy === "1" ||
216
+ trustProxy === "yes" ||
217
+ trustProxy === "on"
218
+ ) {
219
+ diagnostics.push({
220
+ severity: "warning",
221
+ code: "production.trust_proxy",
222
+ message:
223
+ "BCP_TRUST_PROXY is enabled. Ensure untrusted clients cannot bypass the trusted reverse proxy/load balancer.",
224
+ });
225
+ }
226
+ }
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ pathToFileURL,
5
+ } from "node:url";
6
+
7
+ import {
8
+ assertEnvironmentSchema,
9
+ type BcpEnvironmentSchema,
10
+ } from "./environment-schema.js";
11
+
12
+ const ENVIRONMENT_SCHEMA_FILES = [
13
+ "bcp.environment.ts",
14
+ "bcp.environment.mts",
15
+ "bcp.environment.js",
16
+ "bcp.environment.mjs",
17
+ ] as const;
18
+
19
+ export interface LoadedBcpEnvironmentSchema {
20
+ file: string | null;
21
+ schema: BcpEnvironmentSchema;
22
+ }
23
+
24
+ export function getEnvironmentSchemaFileNames(): string[] {
25
+ return [
26
+ ...ENVIRONMENT_SCHEMA_FILES,
27
+ ];
28
+ }
29
+
30
+ export async function loadBcpEnvironmentSchema(
31
+ rootDirectory: string
32
+ ): Promise<LoadedBcpEnvironmentSchema> {
33
+ const matches =
34
+ ENVIRONMENT_SCHEMA_FILES
35
+ .map(
36
+ (fileName) =>
37
+ path.join(
38
+ rootDirectory,
39
+ fileName
40
+ )
41
+ )
42
+ .filter(
43
+ (filePath) =>
44
+ fs.existsSync(
45
+ filePath
46
+ )
47
+ );
48
+
49
+ if (
50
+ matches.length > 1
51
+ ) {
52
+ throw new Error(
53
+ `BCP Framework: multiple environment schema files found: ${matches.map((file) => path.basename(file)).join(", ")}. Keep only one bcp.environment file.`
54
+ );
55
+ }
56
+
57
+ if (
58
+ matches.length === 0
59
+ ) {
60
+ return {
61
+ file: null,
62
+ schema: {},
63
+ };
64
+ }
65
+
66
+ const file =
67
+ matches[0];
68
+ const url =
69
+ pathToFileURL(
70
+ file
71
+ );
72
+
73
+ url.searchParams.set(
74
+ "bcp-environment",
75
+ `${Date.now()}-${Math.random()}`
76
+ );
77
+
78
+ const module =
79
+ await import(
80
+ url.href
81
+ );
82
+ const schema =
83
+ module.default;
84
+
85
+ if (
86
+ schema === undefined
87
+ ) {
88
+ throw new Error(
89
+ `BCP Framework: ${path.basename(file)} must export a default environment schema.`
90
+ );
91
+ }
92
+
93
+ assertEnvironmentSchema(
94
+ schema
95
+ );
96
+
97
+ return {
98
+ file,
99
+ schema,
100
+ };
101
+ }