bazis 0.98.28 → 0.98.29
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/cli/build.ts +17 -3
- package/cli/generateProject.ts +2 -1
- package/cli/main.ts +2 -1
- package/cli/parseCli.ts +7 -6
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/kernel/config/README.md +10 -0
- package/core/kernel/config/defineConfig.ts +15 -10
- package/core/kernel/config/sources.ts +58 -0
- package/core/kernel/index.ts +1 -1
- package/core/kernel/logging/ConsoleLogger.ts +7 -3
- package/library/redaction/index.ts +5 -1
- package/package.json +1 -1
package/cli/build.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface BuildOptions {
|
|
|
10
10
|
readonly bin: boolean;
|
|
11
11
|
/** Executable path relative to the project; default `bin/<package name>`. */
|
|
12
12
|
readonly outfile?: string;
|
|
13
|
+
/** Let the executable read `.env` files from its working directory (off by default). */
|
|
14
|
+
readonly dotenv?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CompileOptions {
|
|
18
|
+
/** Let the executable read `.env`, `.env.local`, `.env.<NODE_ENV>` from its working directory. */
|
|
19
|
+
readonly dotenv?: boolean;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
/**
|
|
@@ -103,7 +110,7 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
|
|
|
103
110
|
if (typecheck !== 0 || !options.bin) return typecheck;
|
|
104
111
|
const outfile = path.resolve(cwd, options.outfile ?? path.join("bin", await binaryName(cwd)));
|
|
105
112
|
log(`[bazis] compile ${path.relative(cwd, outfile)}...`);
|
|
106
|
-
return compileBinary(bun, await projectEntry(cwd), outfile);
|
|
113
|
+
return compileBinary(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv });
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/**
|
|
@@ -111,11 +118,18 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
|
|
|
111
118
|
* `.<hash>.bun-build` file in its working directory and cannot remove it when
|
|
112
119
|
* that executable is read-only or carries `uchg` (scripts/bazis-bun). Compile
|
|
113
120
|
* from a private directory and remove it afterwards.
|
|
121
|
+
*
|
|
122
|
+
* By default a Bun executable reads `bunfig.toml` and `.env` files from the
|
|
123
|
+
* directory it is started in: a `preload` in such a bunfig runs foreign code
|
|
124
|
+
* inside the application, and a stray `.env` silently changes its settings.
|
|
125
|
+
* The executable gets its settings from the real environment instead;
|
|
126
|
+
* `dotenv: true` restores reading `.env`. A bunfig is never read.
|
|
114
127
|
*/
|
|
115
|
-
export function compileBinary(bun: string, entry: string, outfile: string): number {
|
|
128
|
+
export function compileBinary(bun: string, entry: string, outfile: string, options: CompileOptions = {}): number {
|
|
116
129
|
const workDir = mkdtempSync(path.join(tmpdir(), "bazis-compile-"));
|
|
130
|
+
const autoload = ["--no-compile-autoload-bunfig", options.dotenv === true ? "--compile-autoload-dotenv" : "--no-compile-autoload-dotenv"];
|
|
117
131
|
try {
|
|
118
|
-
const result = Bun.spawnSync([bun, "build", "--compile", path.resolve(entry), "--outfile", path.resolve(outfile)],
|
|
132
|
+
const result = Bun.spawnSync([bun, "build", "--compile", ...autoload, path.resolve(entry), "--outfile", path.resolve(outfile)],
|
|
119
133
|
{ cwd: workDir, stdout: "inherit", stderr: "inherit" });
|
|
120
134
|
return result.exitCode ?? 1;
|
|
121
135
|
} finally {
|
package/cli/generateProject.ts
CHANGED
|
@@ -164,7 +164,8 @@ function buildProjectFiles(name: string, dependency: string, frameworkMode: Fram
|
|
|
164
164
|
["package.json", `${JSON.stringify(manifest, null, 2)}\n`],
|
|
165
165
|
["tsconfig.json", `${JSON.stringify(tsconfig, null, 2)}\n`],
|
|
166
166
|
["bazis.config.json", `${JSON.stringify(codegen, null, 2)}\n`],
|
|
167
|
-
|
|
167
|
+
// Bun reads .env, .env.local and .env.<NODE_ENV>: none of them belongs in git.
|
|
168
|
+
[".gitignore", "node_modules/\nbin/\nsrc/generated/\n.env\n.env.*\n!.env.example\n"],
|
|
168
169
|
[".env.example", "# Copy to .env (Bun loads it automatically).\nBAZIS_ENV=development\nHOST=127.0.0.1\nPORT=3000\n"],
|
|
169
170
|
["src/app/test/health.test.ts", HEALTH_TEST],
|
|
170
171
|
["AGENTS.md", "# Working on this bazis project\n\nBefore changing the application, read the [module architecture](docs/architecture/MODULE_ARCHITECTURE.md). Create new modules only with `bunx bazis g module` or `bunx bazis g pack`; fill in the generated `MODULE.md` afterwards. Only codegen updates `src/generated/`.\n"],
|
package/cli/main.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const USAGE = `Usage:
|
|
|
16
16
|
bazis test [<bun test args>] Codegen, then bun test
|
|
17
17
|
bazis build Codegen and typecheck
|
|
18
18
|
bazis build --bin [--outfile <path>] Also compile a standalone executable (default bin/<name>)
|
|
19
|
+
bazis build --bin --dotenv The executable also reads .env files from its working directory
|
|
19
20
|
bazis --help
|
|
20
21
|
g can also be written as generate.
|
|
21
22
|
|
|
@@ -85,7 +86,7 @@ export async function runCli(argv: readonly string[], runtime: CliRuntime = defa
|
|
|
85
86
|
if (parsed.kind === "codegen") return await runtime.codegen(process.cwd(), parsed.target);
|
|
86
87
|
if (parsed.kind === "dev") return await runDev(process.cwd(), runtime.codegen, runtime.log, { watch: parsed.watch });
|
|
87
88
|
if (parsed.kind === "test") return await runTest(process.cwd(), parsed.args, runtime.codegen);
|
|
88
|
-
if (parsed.kind === "build") return await runBuild(process.cwd(), { bin: parsed.bin, outfile: parsed.outfile }, runtime.codegen, runtime.log);
|
|
89
|
+
if (parsed.kind === "build") return await runBuild(process.cwd(), { bin: parsed.bin, outfile: parsed.outfile, dotenv: parsed.dotenv }, runtime.codegen, runtime.log);
|
|
89
90
|
if (parsed.kind === "new") {
|
|
90
91
|
const result = await generateProject({ name: parsed.name, outputPath: parsed.outputPath, frameworkPath: parsed.frameworkPath, linkFramework: parsed.linkFramework, vendor: parsed.vendor, dryRun: parsed.dryRun });
|
|
91
92
|
runtime.log(`[bazis] ${result.dryRun ? "planned" : "created"} project: ${result.projectDir}`);
|
package/cli/parseCli.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type ParseCliResult =
|
|
|
25
25
|
| { readonly kind: "codegen"; readonly target?: string }
|
|
26
26
|
| { readonly kind: "dev"; readonly watch: boolean }
|
|
27
27
|
| { readonly kind: "test"; readonly args: readonly string[] }
|
|
28
|
-
| { readonly kind: "build"; readonly bin: boolean; readonly outfile?: string }
|
|
28
|
+
| { readonly kind: "build"; readonly bin: boolean; readonly outfile?: string; readonly dotenv: boolean }
|
|
29
29
|
| { readonly kind: "error"; readonly message: string };
|
|
30
30
|
|
|
31
31
|
const GENERATOR_ALIASES = new Map<string, "module" | "pack">([
|
|
@@ -33,7 +33,7 @@ const GENERATOR_ALIASES = new Map<string, "module" | "pack">([
|
|
|
33
33
|
["pack", "pack"], ["p", "pack"], ["module-pack", "pack"],
|
|
34
34
|
]);
|
|
35
35
|
const VALUE_OPTIONS = new Set(["--modules-root", "--app-module", "--pack", "--parts", "--target", "--path", "--framework", "--outfile"]);
|
|
36
|
-
const FLAG_OPTIONS = new Set(["--no-register", "--force", "--full", "--enterprise", "--minimal", "--empty", "--dry-run", "--no-codegen", "--link-framework", "--vendor", "--bin", "--watch"]);
|
|
36
|
+
const FLAG_OPTIONS = new Set(["--no-register", "--force", "--full", "--enterprise", "--minimal", "--empty", "--dry-run", "--no-codegen", "--link-framework", "--vendor", "--bin", "--watch", "--dotenv"]);
|
|
37
37
|
|
|
38
38
|
/** Parsing is pure: help and invalid input can never start generation. */
|
|
39
39
|
export function parseCliArgs(argv: readonly string[]): ParseCliResult {
|
|
@@ -89,14 +89,15 @@ export function parseCliArgs(argv: readonly string[]): ParseCliResult {
|
|
|
89
89
|
}
|
|
90
90
|
if (options.has("--watch")) return error("--watch is only supported by dev.");
|
|
91
91
|
if (command === "build") {
|
|
92
|
-
if (positional.length !== 1) return error("Use: bazis build [--bin [--outfile <path>]]");
|
|
92
|
+
if (positional.length !== 1) return error("Use: bazis build [--bin [--outfile <path>] [--dotenv]]");
|
|
93
93
|
for (const option of options.keys()) {
|
|
94
|
-
if (option !== "--bin" && option !== "--outfile") return error(`Option ${option} is not supported by build.`);
|
|
94
|
+
if (option !== "--bin" && option !== "--outfile" && option !== "--dotenv") return error(`Option ${option} is not supported by build.`);
|
|
95
95
|
}
|
|
96
96
|
if (options.has("--outfile") && !options.has("--bin")) return error("--outfile requires --bin.");
|
|
97
|
-
|
|
97
|
+
if (options.has("--dotenv") && !options.has("--bin")) return error("--dotenv requires --bin.");
|
|
98
|
+
return { kind: "build", bin: options.has("--bin"), outfile: options.get("--outfile"), dotenv: options.has("--dotenv") };
|
|
98
99
|
}
|
|
99
|
-
if (options.has("--bin") || options.has("--outfile")) return error("--bin and --
|
|
100
|
+
if (options.has("--bin") || options.has("--outfile") || options.has("--dotenv")) return error("--bin, --outfile and --dotenv are only supported by build.");
|
|
100
101
|
if (command === "codegen") {
|
|
101
102
|
if (positional.length !== 1) return error("Use: bazis codegen [--target <name|all>]");
|
|
102
103
|
for (const option of options.keys()) {
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.29" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -26,6 +26,16 @@ Flat Configuration key names are case-insensitive. A later source also wins
|
|
|
26
26
|
between the canonical name and an explicit env alias.
|
|
27
27
|
Numbers are finite; an empty string never becomes zero; a Secret does not allow an
|
|
28
28
|
empty or whitespace value. TypeScript forbids unsupported object types.
|
|
29
|
+
In `production` a Secret whose value would come from the `default` section (no source
|
|
30
|
+
supplied it and the `production` section does not declare it) is an error: a secret
|
|
31
|
+
default in the code is a development value. `production: { key: secret("…") }`
|
|
32
|
+
declares a production value explicitly.
|
|
33
|
+
|
|
34
|
+
`secretFilesSource(directory, { optional })` reads secret files as Docker and Kubernetes
|
|
35
|
+
mount them (`/run/secrets`): one file per value, the file name is the key (`db.password`)
|
|
36
|
+
or its variable (`BAZIS_DB__PASSWORD`), trailing line breaks are removed, hidden entries
|
|
37
|
+
(Kubernetes `..data`) and directories are skipped, a file over 64 KiB is an error.
|
|
38
|
+
Errors name the file, never its content. A missing directory is an error unless `optional`.
|
|
29
39
|
|
|
30
40
|
A declaration stores no chosen environment, values or cache. `get/has/ensureValid`
|
|
31
41
|
stay for standalone reading of the process env, but `ensureValid` no longer
|
|
@@ -156,7 +156,8 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
|
|
|
156
156
|
const issues: string[] = [];
|
|
157
157
|
// A wrong value names what came and where to change it: the variable that
|
|
158
158
|
// supplied it, or the variables that can override the value from the code.
|
|
159
|
-
|
|
159
|
+
// A key with a sensitive name never shows its value.
|
|
160
|
+
const got = (key: string, raw: unknown) => raw === undefined || isSensitiveKey(fullKey(key)) ? "" : `, got ${JSON.stringify(String(raw)).slice(0, 80)}`;
|
|
160
161
|
const source = (key: string, selected: { readonly name: string } | undefined) => ` (${selected ? selected.name : names.get(key)!.join(", ")})`;
|
|
161
162
|
for (const key of keys) {
|
|
162
163
|
const base = schema.default[key]!;
|
|
@@ -179,23 +180,31 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
|
|
|
179
180
|
let value: ConfigValueType | undefined;
|
|
180
181
|
if (type === "secret") {
|
|
181
182
|
if (typeof raw !== "string" || !raw.trim()) issues.push(`${fullKey(key)} — required non-empty secret is not set (${names.get(key)!.join(", ")})`);
|
|
183
|
+
// A secret default in the code is a development value; production must
|
|
184
|
+
// get the secret from a source or declare its own value explicitly.
|
|
185
|
+
// No "apiKey: …" in the hint: the console redaction would hide it.
|
|
186
|
+
else if (environment === "production" && selected === undefined && override === undefined) issues.push(`${fullKey(key)} — production would use the development default secret from the code; set ${names.get(key)!.join(" or ")}, or declare ${key} in the production section`);
|
|
182
187
|
else value = new Secret(raw);
|
|
183
188
|
} else if (type === "number") {
|
|
184
189
|
const parsed = Number(raw);
|
|
185
|
-
if (raw === undefined || (typeof raw === "string" && !raw.trim()) || !Number.isFinite(parsed)) issues.push(`${fullKey(key)} — expected a finite number${got(raw)}${source(key, selected)}`);
|
|
190
|
+
if (raw === undefined || (typeof raw === "string" && !raw.trim()) || !Number.isFinite(parsed)) issues.push(`${fullKey(key)} — expected a finite number${got(key, raw)}${source(key, selected)}`);
|
|
186
191
|
else value = parsed;
|
|
187
192
|
} else if (type === "boolean") {
|
|
188
193
|
const normalized = String(raw).toLowerCase();
|
|
189
194
|
if (normalized === "true" || normalized === "1") value = true;
|
|
190
195
|
else if (normalized === "false" || normalized === "0") value = false;
|
|
191
|
-
else issues.push(`${fullKey(key)} — expected a boolean (true, false, 1, 0)${got(raw)}${source(key, selected)}`);
|
|
196
|
+
else issues.push(`${fullKey(key)} — expected a boolean (true, false, 1, 0)${got(key, raw)}${source(key, selected)}`);
|
|
192
197
|
} else if (typeof raw === "string") value = raw;
|
|
193
198
|
else issues.push(`${fullKey(key)} — expected a string`);
|
|
194
199
|
if (value === undefined) continue;
|
|
195
|
-
if (isEnum(base) && !base.values.includes(value as string | number)) issues.push(`${fullKey(key)} — ${JSON.stringify(value)} is not allowed, use one of: ${base.values.join(", ")}${source(key, selected)}`);
|
|
200
|
+
if (isEnum(base) && !base.values.includes(value as string | number)) issues.push(`${fullKey(key)} — ${isSensitiveKey(fullKey(key)) ? "the value" : JSON.stringify(value)} is not allowed, use one of: ${base.values.join(", ")}${source(key, selected)}`);
|
|
196
201
|
try {
|
|
197
202
|
const issue = validators[key]?.(value);
|
|
198
|
-
|
|
203
|
+
// A validator may quote its own value; hide it in that message only.
|
|
204
|
+
// Replacing secrets across the whole text turned a one-letter secret
|
|
205
|
+
// into "m***il.***piKey".
|
|
206
|
+
const sensitive = value instanceof Secret ? value.reveal() : typeof value === "string" && isSensitiveKey(fullKey(key)) ? value : "";
|
|
207
|
+
if (issue) issues.push(`${fullKey(key)} — ${sensitive.trim() ? issue.replaceAll(sensitive, "***") : issue}`);
|
|
199
208
|
} catch { issues.push(`${fullKey(key)} — validator failed`); }
|
|
200
209
|
values.set(key, value);
|
|
201
210
|
inspection.push(Object.freeze({ key: fullKey(key), type: type as ConfigInspection["type"], env: names.get(key)!, source: selected?.source ?? (override === undefined ? "default" : environment), value: safeValue(fullKey(key), value) }));
|
|
@@ -203,11 +212,7 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
|
|
|
203
212
|
if (issues.length) {
|
|
204
213
|
// "key — text", not "key: text": the console redaction would read
|
|
205
214
|
// "db.password: required" as a secret value and hide the word "required".
|
|
206
|
-
|
|
207
|
-
for (const [key, value] of values) {
|
|
208
|
-
const sensitive = value instanceof Secret ? value.reveal() : typeof value === "string" && isSensitiveKey(key) ? value : undefined;
|
|
209
|
-
if (sensitive) message = message.replaceAll(sensitive, "***");
|
|
210
|
-
}
|
|
215
|
+
const message = issues.join("; ");
|
|
211
216
|
throw new KernelError(`Invalid configuration (environment "${environment}"): ${message}.`);
|
|
212
217
|
}
|
|
213
218
|
const rows = Object.freeze(inspection);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { KernelError } from "../errors";
|
|
2
4
|
import type { ConfigSource } from "../types";
|
|
3
5
|
|
|
@@ -96,6 +98,62 @@ export function jsonFileSource(path: string, options?: { readonly optional?: boo
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/** Largest secret file read by {@link secretFilesSource}: keys and certificates are a few KiB. */
|
|
102
|
+
const SECRET_FILE_MAX_BYTES = 64 * 1024;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Secret files of a directory, as Docker and Kubernetes mount them
|
|
106
|
+
* (`/run/secrets`). Each file is one value; its name is the key, written as
|
|
107
|
+
* the key (`db.password`) or as its variable (`BAZIS_DB__PASSWORD`). Trailing
|
|
108
|
+
* line breaks are removed. Hidden entries (Kubernetes `..data`) and
|
|
109
|
+
* directories are skipped. Errors name the file, never its content.
|
|
110
|
+
*/
|
|
111
|
+
export function secretFilesSource(directory: string, options?: { readonly optional?: boolean }): ConfigSource {
|
|
112
|
+
return {
|
|
113
|
+
description: `files(${directory})`,
|
|
114
|
+
load: async () => {
|
|
115
|
+
let names: string[];
|
|
116
|
+
try {
|
|
117
|
+
names = await readdir(directory);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (options?.optional === true && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
throw new KernelError(`Secret directory "${directory}" cannot be read (${(error as NodeJS.ErrnoException).code ?? "error"}).`);
|
|
123
|
+
}
|
|
124
|
+
const result: Record<string, string> = {};
|
|
125
|
+
for (const name of names.sort()) {
|
|
126
|
+
if (name.startsWith(".")) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const file = path.join(directory, name);
|
|
130
|
+
let text: string;
|
|
131
|
+
try {
|
|
132
|
+
// stat follows the symlinks Kubernetes uses for each key.
|
|
133
|
+
const info = await stat(file);
|
|
134
|
+
if (!info.isFile()) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (info.size > SECRET_FILE_MAX_BYTES) {
|
|
138
|
+
throw new KernelError(`Secret file "${file}" is larger than ${SECRET_FILE_MAX_BYTES} bytes.`);
|
|
139
|
+
}
|
|
140
|
+
text = await Bun.file(file).text();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof KernelError) throw error;
|
|
143
|
+
throw new KernelError(`Secret file "${file}" cannot be read (${(error as NodeJS.ErrnoException).code ?? "error"}).`);
|
|
144
|
+
}
|
|
145
|
+
const key = name.toUpperCase().startsWith(ENV_PREFIX)
|
|
146
|
+
? name.slice(ENV_PREFIX.length).toLowerCase().replaceAll("__", ".")
|
|
147
|
+
: name.toLowerCase();
|
|
148
|
+
if (key.length > 0) {
|
|
149
|
+
result[key] = text.replace(/[\r\n]+$/, "");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
99
157
|
function flatten(tree: ConfigTree, prefix = "", into: Record<string, string> = {}): Record<string, string> {
|
|
100
158
|
for (const [key, value] of Object.entries(tree)) {
|
|
101
159
|
const fullKey = prefix.length > 0 ? `${prefix}.${key}` : key;
|
package/core/kernel/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
type ValidatableConfig,
|
|
30
30
|
} from "./config/defineConfig";
|
|
31
31
|
export { addConfigOptions, configOptions, type ConfigOptionsBinding } from "./config/addConfigOptions";
|
|
32
|
-
export { argsSource, envSource, jsonFileSource, memorySource } from "./config/sources";
|
|
32
|
+
export { argsSource, envSource, jsonFileSource, memorySource, secretFilesSource } from "./config/sources";
|
|
33
33
|
|
|
34
34
|
export {
|
|
35
35
|
EVENT_HANDLER,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactSensitive, type SensitiveRedactionOptions } from "../../../library/redaction";
|
|
1
|
+
import { redactSensitive, redactSensitiveText, type SensitiveRedactionOptions } from "../../../library/redaction";
|
|
2
2
|
import type { LogFields, LogLevel, Logger } from "./Logger";
|
|
3
3
|
import { getRequestId, getTraceparent } from "../correlation/requestContext";
|
|
4
4
|
|
|
@@ -16,7 +16,10 @@ export interface ConsoleLoggerOptions {
|
|
|
16
16
|
readonly minLevel?: LogLevel;
|
|
17
17
|
/** Prefix tag, e.g. the app name. */
|
|
18
18
|
readonly name?: string;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The message and structured fields are redacted by default (secret keys, `password=…`,
|
|
21
|
+
* bearer tokens, URL credentials); pass false only for trusted local debugging.
|
|
22
|
+
*/
|
|
20
23
|
readonly redaction?: SensitiveRedactionOptions | false;
|
|
21
24
|
/**
|
|
22
25
|
* Inside a request, add `requestId` (and `traceparent`, when the request
|
|
@@ -83,6 +86,7 @@ export class ConsoleLogger implements Logger {
|
|
|
83
86
|
return;
|
|
84
87
|
}
|
|
85
88
|
const line = this.requestContext ? withRequestContext(fields) : fields;
|
|
86
|
-
|
|
89
|
+
const text = this.redaction === false ? message : redactSensitiveText(message, this.redaction);
|
|
90
|
+
LEVEL_SINK[level](`${this.prefix}${level}: ${text}${formatFields(line, this.redaction)}`);
|
|
87
91
|
}
|
|
88
92
|
}
|
|
@@ -19,6 +19,8 @@ const DEFAULT_SENSITIVE_FRAGMENTS = [
|
|
|
19
19
|
const SECRET_TEXT_PATTERN =
|
|
20
20
|
/\b(password|passwd|passphrase|secret|token|api[-_]?key|cookie|set-cookie|private[-_]?key|client[-_]?secret)\b(\s*[:=]\s*)("[^"]*"|'[^']*'|[^,\s}]+)/gi;
|
|
21
21
|
const BEARER_TEXT_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
22
|
+
// Credentials in a URL: postgres://user:password@host, https://token@host.
|
|
23
|
+
const URL_USERINFO_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/?#@]+)@/gi;
|
|
22
24
|
|
|
23
25
|
export interface SensitiveRedactionOptions {
|
|
24
26
|
readonly replacement?: string;
|
|
@@ -162,5 +164,7 @@ function normalizeKey(key: string): string {
|
|
|
162
164
|
function redactText(value: string, replacement: string): string {
|
|
163
165
|
return value
|
|
164
166
|
.replace(SECRET_TEXT_PATTERN, (_match, name: string, separator: string) => `${name}${separator}${replacement}`)
|
|
165
|
-
.replace(BEARER_TEXT_PATTERN, (_match, scheme: string) => `${scheme} ${replacement}`)
|
|
167
|
+
.replace(BEARER_TEXT_PATTERN, (_match, scheme: string) => `${scheme} ${replacement}`)
|
|
168
|
+
.replace(URL_USERINFO_PATTERN, (_match, scheme: string, userinfo: string) =>
|
|
169
|
+
`${scheme}${userinfo.includes(":") ? `${replacement}:${replacement}` : replacement}@`);
|
|
166
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.98.
|
|
3
|
+
"version": "0.98.29",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|