bazis 0.106.0 → 0.108.0

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 CHANGED
@@ -14,9 +14,13 @@ export interface BuildOptions {
14
14
  readonly outfile?: string;
15
15
  /** Let the executable read `.env` files from its working directory (off by default). */
16
16
  readonly dotenv?: boolean;
17
+ /** Another platform, `linux-x64`: `bun build --target=bun-linux-x64`. */
18
+ readonly platform?: string;
17
19
  }
18
20
 
19
21
  export interface CompileOptions {
22
+ /** Cross-compile for `bun-<platform>`; Bun downloads that runtime once. */
23
+ readonly platform?: string;
20
24
  /** Let the executable read `.env`, `.env.local`, `.env.<NODE_ENV>` from its working directory. */
21
25
  readonly dotenv?: boolean;
22
26
  }
@@ -149,10 +153,11 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
149
153
  const s = ui.out;
150
154
  const t = s.t;
151
155
  const startedAt = performance.now();
152
- const outfile = options.bin ? path.resolve(cwd, options.outfile ?? path.join("bin", await binaryName(cwd))) : undefined;
156
+ const outfile = options.bin ? path.resolve(cwd, options.outfile ?? path.join("bin", await defaultBinaryName(cwd, options.platform))) : undefined;
153
157
  commandHeader(s, "build", cwd, [
154
158
  s.kv(t.meta.target, s.text(options.bin ? "executable" : "typecheck")),
155
159
  ...(outfile === undefined ? [] : [s.kv(t.meta.out, s.text(path.relative(cwd, outfile)))]),
160
+ ...(options.platform === undefined ? [] : [s.kv(t.meta.platform, s.text(options.platform))]),
156
161
  ]);
157
162
  s.section(t.section.build);
158
163
  const generated = await codegen(cwd, "all", { ui });
@@ -182,7 +187,7 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
182
187
  s.section(t.section.binary);
183
188
  const stopCompile = s.progress(t.step.compile);
184
189
  const compileStarted = performance.now();
185
- const compiled = await compileBinaryAsync(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv });
190
+ const compiled = await compileBinaryAsync(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv, platform: options.platform });
186
191
  stopCompile();
187
192
  const compileMs = formatDuration(performance.now() - compileStarted);
188
193
  const relative = path.relative(cwd, outfile);
@@ -263,9 +268,10 @@ export async function compileBinaryAsync(bun: string, entry: string, outfile: st
263
268
  }
264
269
  }
265
270
 
266
- function compileArgs(bun: string, entry: string, outfile: string, options: CompileOptions): string[] {
271
+ export function compileArgs(bun: string, entry: string, outfile: string, options: CompileOptions): string[] {
267
272
  const autoload = ["--no-compile-autoload-bunfig", options.dotenv === true ? "--compile-autoload-dotenv" : "--no-compile-autoload-dotenv"];
268
- return [bun, "build", "--compile", ...autoload, path.resolve(entry), "--outfile", path.resolve(outfile)];
273
+ const target = options.platform === undefined ? [] : [`--target=bun-${options.platform}`];
274
+ return [bun, "build", "--compile", ...target, ...autoload, path.resolve(entry), "--outfile", path.resolve(outfile)];
269
275
  }
270
276
 
271
277
  function removeWorkDir(workDir: string): void {
@@ -287,6 +293,12 @@ async function projectEntry(cwd: string): Promise<string> {
287
293
  return path.resolve(cwd, entry);
288
294
  }
289
295
 
296
+ /** `bin/<name>`, or `bin/<name>-<platform>` for another platform, so the local build is kept. */
297
+ async function defaultBinaryName(cwd: string, platform: string | undefined): Promise<string> {
298
+ const name = await binaryName(cwd);
299
+ return platform === undefined ? name : `${name}-${platform}`;
300
+ }
301
+
290
302
  async function binaryName(cwd: string): Promise<string> {
291
303
  const manifest = Bun.file(path.join(cwd, "package.json"));
292
304
  const name = await manifest.exists() ? (await manifest.json() as { name?: unknown }).name : undefined;
package/cli/main.ts CHANGED
@@ -20,6 +20,8 @@ export const USAGE = `Usage:
20
20
  bazis build Codegen and typecheck
21
21
  bazis build --bin [--outfile <path>] Also compile a standalone executable (default bin/<name>)
22
22
  bazis build --bin --dotenv The executable also reads .env files from its working directory
23
+ bazis build --bin --platform <os-arch> For another platform, e.g. linux-x64, linux-arm64-musl
24
+ (default output bin/<name>-<os-arch>)
23
25
  bazis --help
24
26
  bazis --version Print the bazis version (also -v)
25
27
  g can also be written as generate.
@@ -130,7 +132,7 @@ export async function runCli(argv: readonly string[], runtime: CliRuntime = defa
130
132
  if (parsed.kind === "codegen") return await runCodegenCommand(cwd, parsed.target, codegen, ui);
131
133
  if (parsed.kind === "dev") return await runDev(cwd, codegen, ui, { watch: parsed.watch });
132
134
  if (parsed.kind === "test") return await runTest(cwd, parsed.args, codegen, ui);
133
- if (parsed.kind === "build") return await runBuild(cwd, { bin: parsed.bin, outfile: parsed.outfile, dotenv: parsed.dotenv }, codegen, ui);
135
+ if (parsed.kind === "build") return await runBuild(cwd, { bin: parsed.bin, outfile: parsed.outfile, dotenv: parsed.dotenv, platform: parsed.platform }, codegen, ui);
134
136
  if (parsed.kind === "new") return await runNew(parsed, ui);
135
137
  // Module registration parses the host module with the TypeScript API: load it
136
138
  // only after switching TypeScript 7 to @typescript/typescript6.
package/cli/parseCli.ts CHANGED
@@ -26,14 +26,16 @@ export type ParseCliResult =
26
26
  | { readonly kind: "codegen"; readonly target?: string }
27
27
  | { readonly kind: "dev"; readonly watch: boolean }
28
28
  | { readonly kind: "test"; readonly args: readonly string[] }
29
- | { readonly kind: "build"; readonly bin: boolean; readonly outfile?: string; readonly dotenv: boolean }
29
+ | { readonly kind: "build"; readonly bin: boolean; readonly outfile?: string; readonly dotenv: boolean; readonly platform?: string }
30
30
  | { readonly kind: "error"; readonly message: string };
31
31
 
32
32
  const GENERATOR_ALIASES = new Map<string, "module" | "pack">([
33
33
  ["module", "module"], ["m", "module"],
34
34
  ["pack", "pack"], ["p", "pack"], ["module-pack", "pack"],
35
35
  ]);
36
- const VALUE_OPTIONS = new Set(["--modules-root", "--app-module", "--pack", "--parts", "--target", "--path", "--framework", "--outfile"]);
36
+ const VALUE_OPTIONS = new Set(["--modules-root", "--app-module", "--pack", "--parts", "--target", "--path", "--framework", "--outfile", "--platform"]);
37
+ /** Platforms of `bun build --compile --target=bun-<platform>`. */
38
+ export const BINARY_PLATFORM = /^(linux|darwin|windows)-(x64|arm64)(-musl)?(-baseline|-modern)?$/;
37
39
  const FLAG_OPTIONS = new Set(["--no-register", "--force", "--full", "--enterprise", "--minimal", "--empty", "--dry-run", "--no-codegen", "--link-framework", "--vendor", "--bin", "--watch", "--dotenv"]);
38
40
 
39
41
  /** Parsing is pure: help and invalid input can never start generation. */
@@ -91,15 +93,20 @@ export function parseCliArgs(argv: readonly string[]): ParseCliResult {
91
93
  }
92
94
  if (options.has("--watch")) return error("--watch is only supported by dev.");
93
95
  if (command === "build") {
94
- if (positional.length !== 1) return error("Use: bazis build [--bin [--outfile <path>] [--dotenv]]");
96
+ if (positional.length !== 1) return error("Use: bazis build [--bin [--outfile <path>] [--platform <os-arch>] [--dotenv]]");
95
97
  for (const option of options.keys()) {
96
- if (option !== "--bin" && option !== "--outfile" && option !== "--dotenv") return error(`Option ${option} is not supported by build.`);
98
+ if (option !== "--bin" && option !== "--outfile" && option !== "--dotenv" && option !== "--platform") return error(`Option ${option} is not supported by build.`);
97
99
  }
98
100
  if (options.has("--outfile") && !options.has("--bin")) return error("--outfile requires --bin.");
99
101
  if (options.has("--dotenv") && !options.has("--bin")) return error("--dotenv requires --bin.");
100
- return { kind: "build", bin: options.has("--bin"), outfile: options.get("--outfile"), dotenv: options.has("--dotenv") };
102
+ if (options.has("--platform") && !options.has("--bin")) return error("--platform requires --bin.");
103
+ const platform = options.get("--platform");
104
+ if (platform !== undefined && !BINARY_PLATFORM.test(platform)) {
105
+ return error(`Unknown platform: ${platform}. Use <os>-<arch>[-musl]: linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, darwin-arm64, darwin-x64, windows-x64.`);
106
+ }
107
+ return { kind: "build", bin: options.has("--bin"), outfile: options.get("--outfile"), dotenv: options.has("--dotenv"), ...(platform === undefined ? {} : { platform }) };
101
108
  }
102
- if (options.has("--bin") || options.has("--outfile") || options.has("--dotenv")) return error("--bin, --outfile and --dotenv are only supported by build.");
109
+ if (options.has("--bin") || options.has("--outfile") || options.has("--dotenv") || options.has("--platform")) return error("--bin, --outfile, --platform and --dotenv are only supported by build.");
103
110
  if (command === "codegen") {
104
111
  if (positional.length !== 1) return error("Use: bazis codegen [--target <name|all>]");
105
112
  for (const option of options.keys()) {
@@ -1,10 +1,19 @@
1
- import { EN_HTTP_MESSAGES, RU_HTTP_MESSAGES, formatMessage, type HttpMessageKey, type Localize } from "../http/Errors/messages";
1
+ import { EN_HTTP_MESSAGES, RU_HTTP_MESSAGES, formatMessage, type HttpMessageKey, type LanguageSelection, type Localize, type SelectedLanguage } from "../http/Errors/messages";
2
2
  import { KernelError } from "../kernel";
3
3
  import { EN_VALIDATION_MESSAGES, MessageRegistry, RU_VALIDATION_MESSAGES, type ValidationCode } from "../../library/validation";
4
4
 
5
5
  /** Language of the texts the framework writes into responses. */
6
6
  export type AppLanguage = "en" | "ru";
7
7
 
8
+ /**
9
+ * Several response languages: each request gets the best match of its
10
+ * `Accept-Language` among `supported`, or `default`.
11
+ */
12
+ export interface AppLanguageOptions {
13
+ readonly default: AppLanguage;
14
+ readonly supported: readonly AppLanguage[];
15
+ }
16
+
8
17
  /**
9
18
  * Own texts for framework messages, by key: HTTP texts (`notFound`,
10
19
  * `malformedJson`, …), validation codes (`required`, `minLength`, …),
@@ -42,3 +51,49 @@ export function createLocalize(language: AppLanguage = "en", messages: AppMessag
42
51
  return text === undefined ? undefined : formatMessage(text, params, template);
43
52
  };
44
53
  }
54
+
55
+ /**
56
+ * The response language of every request: one language, or the best match of
57
+ * `Accept-Language` (`ru-RU,ru;q=0.9,en;q=0.8`) among `supported`.
58
+ */
59
+ export function createLanguages(option: AppLanguage | AppLanguageOptions = "en", messages: AppMessages = {}): LanguageSelection {
60
+ if (typeof option === "string") {
61
+ const single: SelectedLanguage = { language: option, localize: createLocalize(option, messages) };
62
+ return { negotiated: false, select: () => single };
63
+ }
64
+ const supported = [...new Set(option.supported)];
65
+ if (supported.length === 0) throw new KernelError("runApp: language.supported needs at least one language.");
66
+ if (!supported.includes(option.default)) {
67
+ throw new KernelError(`runApp: language.default "${String(option.default)}" must be one of language.supported (${supported.join(", ")}).`);
68
+ }
69
+ const selected = new Map<string, SelectedLanguage>(supported.map((language) => [language, { language, localize: createLocalize(language, messages) }]));
70
+ const fallback = selected.get(option.default)!;
71
+ return {
72
+ negotiated: supported.length > 1,
73
+ select(acceptLanguage) {
74
+ const language = negotiateLanguage(acceptLanguage, supported);
75
+ return language === undefined ? fallback : selected.get(language)!;
76
+ },
77
+ };
78
+ }
79
+
80
+ /** The supported language with the highest `q` in `Accept-Language`; ties keep the header order. */
81
+ export function negotiateLanguage(header: string | null | undefined, supported: readonly string[]): string | undefined {
82
+ if (header === null || header === undefined || header.length > 1024) return undefined;
83
+ let best: { language: string; q: number } | undefined;
84
+ for (const part of header.split(",").slice(0, 32)) {
85
+ const [range = "", ...parameters] = part.trim().split(";");
86
+ const tag = range.trim().toLowerCase();
87
+ if (tag.length === 0) continue;
88
+ let q = 1;
89
+ for (const parameter of parameters) {
90
+ const [name, value] = parameter.trim().split("=");
91
+ if (name?.trim() === "q") q = Number(value);
92
+ }
93
+ if (!Number.isFinite(q) || q <= 0 || q > 1) continue;
94
+ const primary = tag === "*" ? undefined : tag.split("-")[0];
95
+ const language = primary === undefined ? undefined : supported.find((candidate) => candidate === primary);
96
+ if (language !== undefined && (best === undefined || q > best.q)) best = { language, q };
97
+ }
98
+ return best?.language;
99
+ }
@@ -13,6 +13,7 @@ import { missingCacheModuleCheck } from "../cache/http/missingCacheModuleCheck";
13
13
  import { httpModule, useModelValidator, type HttpModuleOptions, type ModelValidator } from "../http";
14
14
  import { grpcModule, type GrpcModuleOptions } from "../grpc";
15
15
  import { buildHttpOpenApiDocument } from "../http/OpenApi/openApiDocument";
16
+ import { defaultAuthorizeChecks } from "../http/Authorization/metadata";
16
17
  import { KernelBuilder, Bazis, KernelError, type KernelOptions, type ValidatableConfig } from "../kernel";
17
18
  import { websocketModule, type WebSocketOptions } from "../websocket";
18
19
  import {
@@ -20,7 +21,7 @@ import {
20
21
  type UiProfileV1,
21
22
  } from "../../library/ui";
22
23
  import { modelValidatorAdapter } from "../../library/validation";
23
- import { createLocalize, type AppLanguage, type AppMessages } from "./messages";
24
+ import { createLanguages, type AppLanguage, type AppLanguageOptions, type AppMessages } from "./messages";
24
25
  import { useCodexLanguage } from "../infra/connectors/codex/contracts";
25
26
  import {
26
27
  type RunAppUiOptions,
@@ -65,8 +66,16 @@ export interface RunAppOptions {
65
66
  * ```ts
66
67
  * await runApp(AppModule, { language: "ru", http: { port: 3000 } });
67
68
  * ```
69
+ *
70
+ * Several languages — each request gets the best match of its
71
+ * `Accept-Language`, or `default`; such responses carry `Content-Language`
72
+ * and `Vary: Accept-Language`, and `ctx.language` names the choice:
73
+ *
74
+ * ```ts
75
+ * await runApp(AppModule, { language: { default: "ru", supported: ["ru", "en"] } });
76
+ * ```
68
77
  */
69
- readonly language?: AppLanguage;
78
+ readonly language?: AppLanguage | AppLanguageOptions;
70
79
  /**
71
80
  * Own texts for single framework messages, on top of `language`:
72
81
  * `{ notFound: "Нет такой страницы", required: "Заполните {property}" }`.
@@ -204,6 +213,7 @@ function buildUiOpenApiDocument(
204
213
  controllers: collectModuleControllers([root]),
205
214
  globalPrefix: http.prefix,
206
215
  versioning: http.versioning,
216
+ authorize: defaultAuthorizeChecks(http.authorize),
207
217
  title: docs?.title ?? "Bazis UI profiles",
208
218
  version: docs?.version ?? "1.0.0",
209
219
  });
@@ -290,8 +300,8 @@ export async function runApp(root: BazisModuleRef, options: RunAppOptions = {}):
290
300
  */
291
301
  export function composeApp(root: BazisModuleRef, options: RunAppOptions = {}): BazisModuleRef {
292
302
  // Checked even without HTTP: an unknown language is a startup error.
293
- const localize = createLocalize(options.language, options.messages);
294
- useCodexLanguage(options.language ?? "en");
303
+ const languages = createLanguages(options.language, options.messages);
304
+ useCodexLanguage(typeof options.language === "object" ? options.language.default : options.language ?? "en");
295
305
  // Infrastructure and cache are global modules; add them to the graph next to the feature root.
296
306
  const globals: BazisModuleRef[] = [];
297
307
  if (options.infra !== undefined) {
@@ -329,7 +339,7 @@ export function composeApp(root: BazisModuleRef, options: RunAppOptions = {}): B
329
339
  options.http,
330
340
  );
331
341
  const httpRoot = httpModule({
332
- localize,
342
+ languages,
333
343
  ...options.http,
334
344
  validator,
335
345
  imports: [httpAppRoot],
@@ -2,7 +2,7 @@ import { JwtError } from "../../../library/jwt/errors";
2
2
  import { ForbiddenError, UnauthorizedError } from "../Errors/HttpError";
3
3
  import type { HttpMiddleware } from "../Middleware/types";
4
4
  import type { RouteMiddlewareComposer } from "../options";
5
- import { resolveAuthorizeMeta } from "./metadata";
5
+ import { routeAuthorizeChecks, type AuthorizeCheck } from "./metadata";
6
6
 
7
7
  /**
8
8
  * Built-in composer for `@Authorize`. The HTTP server puts it first among
@@ -12,14 +12,13 @@ import { resolveAuthorizeMeta } from "./metadata";
12
12
  * `UnauthorizedError` → `401`). A `JwtError` (missing, malformed, expired or
13
13
  * forged token) escaping a check is a 401 as well, not a server error.
14
14
  */
15
- export function createAuthorizeComposer(): RouteMiddlewareComposer {
15
+ export function createAuthorizeComposer(defaults: readonly AuthorizeCheck[] = []): RouteMiddlewareComposer {
16
16
  return (controllerClass, methodName) => {
17
- const meta = resolveAuthorizeMeta(controllerClass, methodName);
18
- if (meta.allowAnonymous || meta.authorize === undefined) {
17
+ const checks = routeAuthorizeChecks(controllerClass, methodName, defaults);
18
+ if (checks === undefined) {
19
19
  return [];
20
20
  }
21
21
 
22
- const { checks } = meta.authorize;
23
22
  const middleware: HttpMiddleware = async (ctx, next) => {
24
23
  for (const check of checks) {
25
24
  let allowed: boolean;
@@ -177,3 +177,20 @@ export function resolveAuthorizeMeta(ctor: object, methodName: string | symbol):
177
177
  ? { allowAnonymous: false }
178
178
  : { allowAnonymous: false, authorize: { checks } };
179
179
  }
180
+
181
+ /**
182
+ * The checks a route runs: the server's default checks (`runApp({ http:
183
+ * { authorize } })`) first, then the route's own `@Authorize`; each function
184
+ * once. `undefined` — the route is open: `@AllowAnonymous`, or no checks at all.
185
+ */
186
+ export function routeAuthorizeChecks(ctor: object, methodName: string | symbol, defaults: readonly AuthorizeCheck[] = []): readonly AuthorizeCheck[] | undefined {
187
+ const meta = resolveAuthorizeMeta(ctor, methodName);
188
+ if (meta.allowAnonymous) return undefined;
189
+ const checks = [...new Set([...defaults, ...(meta.authorize?.checks ?? [])])];
190
+ return checks.length === 0 ? undefined : checks;
191
+ }
192
+
193
+ /** `authorize` of the HTTP options as a list. */
194
+ export function defaultAuthorizeChecks(option: AuthorizeCheck | readonly AuthorizeCheck[] | undefined): readonly AuthorizeCheck[] {
195
+ return option === undefined ? [] : typeof option === "function" ? [option] : option;
196
+ }
@@ -8,6 +8,27 @@ export type MessageParams = Readonly<Record<string, unknown>>;
8
8
  */
9
9
  export type Localize = (key: string, params: MessageParams) => string | undefined;
10
10
 
11
+ /** The response language of one request and its texts. */
12
+ export interface SelectedLanguage {
13
+ readonly language: string;
14
+ readonly localize: Localize;
15
+ }
16
+
17
+ /**
18
+ * Picks the response language of a request. `negotiated`: several languages,
19
+ * chosen by `Accept-Language`; responses whose text the framework wrote then
20
+ * carry `Content-Language` and `Vary: Accept-Language`.
21
+ */
22
+ export interface LanguageSelection {
23
+ readonly negotiated: boolean;
24
+ select(acceptLanguage: string | null | undefined): SelectedLanguage;
25
+ }
26
+
27
+ /** Headers of a framework-written response in a negotiated language. */
28
+ export function languageHeaders(selection: LanguageSelection | undefined, selected: SelectedLanguage | undefined): [string, string][] {
29
+ return selection?.negotiated === true && selected !== undefined ? [["content-language", selected.language], ["vary", "Accept-Language"]] : [];
30
+ }
31
+
11
32
  /** A message key and parameters carried by an error, rendered at the response. */
12
33
  export interface LocalizedMessage {
13
34
  readonly key: string;
@@ -46,6 +46,12 @@ export class HttpContext {
46
46
  readonly clientIp?: string,
47
47
  /** Validator captured by the owning HTTP server. */
48
48
  readonly modelValidator?: ModelValidator,
49
+ /**
50
+ * Response language of this request: `runApp({ language })`, or the best
51
+ * match of `Accept-Language` when several languages are supported.
52
+ * `undefined` without `language`.
53
+ */
54
+ readonly language?: string,
49
55
  ) {}
50
56
 
51
57
  get method(): string {
@@ -15,6 +15,7 @@ import type { HttpMiddleware } from "./Middleware/types";
15
15
  import { prepareOpenApiDocs, serveOpenApiDocs } from "./OpenApi/openApiDocs";
16
16
  import type { PreparedOpenApiDocs } from "../../library/openapi";
17
17
  import { RouterBuilder } from "./Routing/RouterBuilder";
18
+ import { defaultAuthorizeChecks } from "./Authorization/metadata";
18
19
  import type { Router } from "./Routing/Router";
19
20
  import { parseRequestPath } from "./Routing/template";
20
21
  import { assertVersioningOptions, createVersionReader, type VersionReader } from "./Versioning/VersionReader";
@@ -22,7 +23,7 @@ import { HTTP_ERROR_HOOK, ROUTE_MIDDLEWARE_COMPOSER, SERVER_MIDDLEWARE, sortByOr
22
23
  import type { HealthEndpointOptions, RouteMiddlewareComposer, HttpModuleOptions, HttpServerOptions } from "./options";
23
24
  import { WEBSOCKET_UPGRADE, type WebSocketUpgrade } from "./WebSocketUpgrade";
24
25
  import { HttpSetupError } from "./Errors/HttpError";
25
- import { EN_HTTP_MESSAGES, formatMessage, type HttpMessageKey, type MessageParams } from "./Errors/messages";
26
+ import { EN_HTTP_MESSAGES, formatMessage, languageHeaders, type HttpMessageKey, type MessageParams, type SelectedLanguage } from "./Errors/messages";
26
27
  import { getModelValidator, type ModelValidator } from "./Binding/modelValidator";
27
28
 
28
29
  const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
@@ -167,7 +168,7 @@ export class HttpServer implements HostedService {
167
168
  errorHandler({
168
169
  exposeDetails,
169
170
  ...(errorLogger ? { logger: errorLogger } : {}),
170
- ...(this.options.localize ? { localize: this.options.localize } : {}),
171
+ ...(this.options.languages ? { languages: this.options.languages } : {}),
171
172
  ...this.options.errorHandler,
172
173
  onUnexpectedError: this.collectErrorHook(),
173
174
  }),
@@ -177,11 +178,13 @@ export class HttpServer implements HostedService {
177
178
  this.globalCors = this.options.cors || serverMiddleware.map(corsOptionsOf).find((options) => options !== undefined);
178
179
  serverChain.push(...serverMiddleware.filter((middleware) => !isCorrelationIdMiddleware(middleware)));
179
180
 
181
+ const defaultChecks = defaultAuthorizeChecks(this.options.authorize);
180
182
  const builder = new RouterBuilder(
181
183
  serverChain,
182
184
  this.options.prefix,
183
185
  this.options.versioning,
184
186
  this.collectRouteComposer(),
187
+ defaultChecks,
185
188
  );
186
189
  this.router = builder.build(this.options.controllers);
187
190
  const routes = builder.routes;
@@ -192,6 +195,7 @@ export class HttpServer implements HostedService {
192
195
  globalPrefix: this.options.prefix,
193
196
  versioning: this.options.versioning,
194
197
  environment: this.resolver.tryResolve(Environment),
198
+ authorize: defaultChecks,
195
199
  });
196
200
 
197
201
  this.webSocket = this.resolver.tryResolve(WEBSOCKET_UPGRADE);
@@ -204,7 +208,7 @@ export class HttpServer implements HostedService {
204
208
  await ws.initialize();
205
209
  this.server = await this.startBunServer((listen) => Bun.serve({
206
210
  ...listen,
207
- error: (error) => this.transportError(error),
211
+ error: (error) => this.transportError(error, undefined),
208
212
  websocket: ws.createBunHandler(),
209
213
  fetch: async (request, server) => {
210
214
  // null -> not for the WS runtime, fall through to HTTP.
@@ -230,7 +234,7 @@ export class HttpServer implements HostedService {
230
234
  Bun.serve({
231
235
  ...listen,
232
236
  fetch: (request, server) => this.dispatch(request, server.requestIP(request)?.address),
233
- error: (error) => this.transportError(error),
237
+ error: (error) => this.transportError(error, undefined),
234
238
  }),
235
239
  );
236
240
  }
@@ -380,29 +384,44 @@ export class HttpServer implements HostedService {
380
384
  } catch (error) {
381
385
  // Covers failures before a request context can be created. Normal route
382
386
  // and short-circuit failures are handled by the configured error boundary.
383
- response = this.prepareClientResponse(this.transportError(error), request);
387
+ response = this.prepareClientResponse(this.transportError(error, request), request);
384
388
  }
385
389
  // HEAD must echo the GET response's status/headers without a body.
386
390
  return request.method === "HEAD" ? stripBody(response) : response;
387
391
  }
388
392
 
389
- /** A response text the server writes itself, in the application language. */
390
- private text(key: HttpMessageKey, params: MessageParams = {}): string {
393
+ /** The response language of a request; `undefined` without `runApp({ language })`. */
394
+ private selected(request: Request | undefined): SelectedLanguage | undefined {
395
+ try {
396
+ return this.options.languages?.select(request?.headers.get("accept-language"));
397
+ } catch {
398
+ return undefined;
399
+ }
400
+ }
401
+
402
+ /** A response text the server writes itself, in the language of the request. */
403
+ private text(key: HttpMessageKey, request: Request | undefined, params: MessageParams = {}): string {
391
404
  const english = formatMessage(EN_HTTP_MESSAGES[key], params);
392
405
  try {
393
- return this.options.localize?.(key, params) ?? english;
406
+ return this.selected(request)?.localize(key, params) ?? english;
394
407
  } catch {
395
408
  return english;
396
409
  }
397
410
  }
398
411
 
399
- private transportError(error: unknown): Response {
412
+ /** `{ error, …extra }` the server writes itself, with the language headers when negotiated. */
413
+ private errorJson(request: Request | undefined, status: number, key: HttpMessageKey, extra: Record<string, unknown> = {}, headers: Record<string, string> = {}): Response {
414
+ const language = Object.fromEntries(languageHeaders(this.options.languages, this.selected(request)));
415
+ return json(status, { error: this.text(key, request), ...extra }, { ...headers, ...language });
416
+ }
417
+
418
+ private transportError(error: unknown, request: Request | undefined): Response {
400
419
  try {
401
420
  console.error("[http] transport failed:", redactSensitive(error));
402
421
  } catch {
403
422
  // Best-effort diagnostics only.
404
423
  }
405
- const response = json(500, { error: this.text("internalServerError") });
424
+ const response = this.errorJson(request, 500, "internalServerError");
406
425
  if (this.options.securityHeaders !== false) {
407
426
  applySecurityHeaders(response, this.options.securityHeaders ?? {});
408
427
  }
@@ -446,9 +465,9 @@ export class HttpServer implements HostedService {
446
465
  if (!segments) {
447
466
  if (this.maxBodyBytes !== undefined && exceedsBodyLimit(request, this.maxBodyBytes)) {
448
467
  return this.runShortCircuit(request, url, clientIp, () =>
449
- json(413, { error: this.text("payloadTooLarge"), details: { maxBytes: this.maxBodyBytes } }));
468
+ this.errorJson(request, 413, "payloadTooLarge", { details: { maxBytes: this.maxBodyBytes } }));
450
469
  }
451
- return this.runShortCircuit(request, url, clientIp, () => json(400, { error: this.text("malformedPath") }));
470
+ return this.runShortCircuit(request, url, clientIp, () => this.errorJson(request, 400, "malformedPath"));
452
471
  }
453
472
 
454
473
  const match = this.router!.match(request.method, segments, this.readVersion(request, url));
@@ -457,18 +476,18 @@ export class HttpServer implements HostedService {
457
476
  : this.maxBodyBytes;
458
477
  if (effectiveMaxBodyBytes !== undefined && exceedsBodyLimit(request, effectiveMaxBodyBytes)) {
459
478
  return this.runShortCircuit(request, url, clientIp, () =>
460
- json(413, { error: this.text("payloadTooLarge"), details: { maxBytes: effectiveMaxBodyBytes } }));
479
+ this.errorJson(request, 413, "payloadTooLarge", { details: { maxBytes: effectiveMaxBodyBytes } }));
461
480
  }
462
481
  if (match.kind === "not-found") {
463
- return this.runShortCircuit(request, url, clientIp, () => json(404, { error: this.text("notFound") }));
482
+ return this.runShortCircuit(request, url, clientIp, () => this.errorJson(request, 404, "notFound"));
464
483
  }
465
484
  if (match.kind === "method-not-allowed") {
466
485
  return this.runShortCircuit(request, url, clientIp, () =>
467
- json(405, { error: this.text("methodNotAllowed") }, { allow: match.allow.join(", ") }));
486
+ this.errorJson(request, 405, "methodNotAllowed", {}, { allow: match.allow.join(", ") }));
468
487
  }
469
488
  if (match.kind === "unsupported-version") {
470
489
  return this.runShortCircuit(request, url, clientIp, () =>
471
- json(400, { error: this.text("unsupportedApiVersion"), details: { supported: match.supported } }));
490
+ this.errorJson(request, 400, "unsupportedApiVersion", { details: { supported: match.supported } }));
472
491
  }
473
492
 
474
493
  const scope = this.provider.createScope();
@@ -481,13 +500,14 @@ export class HttpServer implements HostedService {
481
500
  effectiveMaxBodyBytes,
482
501
  clientIp,
483
502
  this.modelValidator,
503
+ this.selected(request)?.language,
484
504
  );
485
505
  let scopeTransferred = false;
486
506
  try {
487
507
  await runPipeline(match.action.chain, ctx);
488
508
  // The error boundary always sets a response on failure; an empty
489
509
  // response here means no middleware produced one.
490
- const response = this.prepareClientResponse(ctx.response ?? json(404, { error: this.text("notFound") }), request);
510
+ const response = this.prepareClientResponse(ctx.response ?? this.errorJson(request, 404, "notFound"), request);
491
511
  if (request.method === "HEAD") return stripBody(response);
492
512
  if (!response.body) return response;
493
513
  this.trackResponse(holdResponseScope(response, request.signal, () =>
@@ -525,6 +545,7 @@ export class HttpServer implements HostedService {
525
545
  this.maxBodyBytes,
526
546
  clientIp,
527
547
  this.modelValidator,
548
+ this.selected(request)?.language,
528
549
  );
529
550
  const terminal: HttpMiddleware = async (context) => {
530
551
  context.response = await produce();
@@ -532,7 +553,7 @@ export class HttpServer implements HostedService {
532
553
  let scopeTransferred = false;
533
554
  try {
534
555
  await runPipeline([...this.shortCircuitChain, terminal], ctx);
535
- const response = this.prepareClientResponse(ctx.response ?? json(500, { error: this.text("internalServerError") }), request);
556
+ const response = this.prepareClientResponse(ctx.response ?? this.errorJson(request, 500, "internalServerError"), request);
536
557
  if (request.method === "HEAD") return stripBody(response);
537
558
  if (!response.body) return response;
538
559
  this.trackResponse(holdResponseScope(response, request.signal, () =>
@@ -3,7 +3,7 @@ import { redactSensitive, redactSensitiveText } from "../../../library/redaction
3
3
  import { HttpClientError, HttpErrorCode } from "../../../library/http-client/errors";
4
4
  import type { HttpContext } from "../HttpContext/HttpContext";
5
5
  import { HttpError, ModelValidationError } from "../Errors/HttpError";
6
- import { EN_HTTP_MESSAGES, STATUS_MESSAGE_KEYS, type HttpMessageKey, type Localize } from "../Errors/messages";
6
+ import { EN_HTTP_MESSAGES, STATUS_MESSAGE_KEYS, type HttpMessageKey, languageHeaders, type LanguageSelection, type Localize } from "../Errors/messages";
7
7
  import { upstreamFailure } from "../Errors/upstreamFailure";
8
8
  import type { HttpMiddleware } from "./types";
9
9
 
@@ -36,11 +36,11 @@ export interface ErrorHandlerOptions {
36
36
  logger?: Logger;
37
37
  /**
38
38
  * Renders framework texts (`Not Found`, validation messages, …) in the
39
- * application language; `runApp` builds it from `language` and `messages`.
40
- * Without it the texts stay English. Messages an application passes to an
41
- * error itself are never changed.
39
+ * response language of each request; `runApp` builds it from `language` and
40
+ * `messages`. Without it the texts stay English. Messages an application
41
+ * passes to an error itself are never changed.
42
42
  */
43
- localize?: Localize;
43
+ languages?: LanguageSelection;
44
44
  }
45
45
 
46
46
  function jsonResponse(status: number, body: unknown, extraHeaders?: readonly [string, string][]): Response {
@@ -111,13 +111,15 @@ function safelyReportUnexpected(
111
111
  */
112
112
  export function errorHandler(options: ErrorHandlerOptions = {}): HttpMiddleware {
113
113
  const log = resolveErrorLog(options);
114
- const text = textRenderer(options.localize);
115
114
  return async (ctx, next) => {
116
115
  try {
117
116
  await next();
118
117
  } catch (error) {
118
+ const selected = options.languages?.select(ctx.header("accept-language"));
119
+ const text = textRenderer(selected?.localize);
120
+ const language = languageHeaders(options.languages, selected);
119
121
  if (error instanceof HttpError) {
120
- const extra: [string, string][] = [];
122
+ const extra: [string, string][] = [...language];
121
123
  const allow = (error as { allow?: readonly string[] }).allow;
122
124
  if (allow) {
123
125
  extra.push(["allow", allow.join(", ")]);
@@ -159,8 +161,8 @@ export function errorHandler(options: ErrorHandlerOptions = {}): HttpMiddleware
159
161
  // message must not reach a browser or a shared screenshot.
160
162
  message: redactSensitiveText(error instanceof Error ? error.message : String(error)),
161
163
  stack: error instanceof Error && error.stack !== undefined ? redactSensitiveText(error.stack) : undefined,
162
- })
163
- : safeJsonResponse(status, { error: title });
164
+ }, language)
165
+ : safeJsonResponse(status, { error: title }, language);
164
166
  }
165
167
  };
166
168
  }
@@ -1,3 +1,4 @@
1
+ import type { AuthorizeCheck } from "../Authorization/metadata";
1
2
  import type { Class } from "../../di";
2
3
  import { Environment } from "../../kernel";
3
4
  import {
@@ -15,6 +16,7 @@ interface PrepareOpenApiDocsInput {
15
16
  readonly globalPrefix?: string;
16
17
  readonly versioning?: ApiVersioningOptions;
17
18
  readonly environment?: Environment;
19
+ readonly authorize?: readonly AuthorizeCheck[];
18
20
  }
19
21
 
20
22
  const DEFAULT_UI_PATH = "/docs";
@@ -38,6 +40,7 @@ export function prepareOpenApiDocs(input: PrepareOpenApiDocsInput): PreparedOpen
38
40
  versioning: input.versioning,
39
41
  title,
40
42
  version,
43
+ ...(input.authorize === undefined ? {} : { authorize: input.authorize }),
41
44
  });
42
45
  const json = JSON.stringify(document, null, 2);
43
46
  return {
@@ -8,7 +8,7 @@ import {
8
8
  type OpenApiSchema,
9
9
  } from "../../../library/openapi";
10
10
  import { optionsFromSchema } from "../../../library/jsonapi";
11
- import { resolveAuthorizeMeta } from "../Authorization/metadata";
11
+ import { resolveAuthorizeMeta, routeAuthorizeChecks, type AuthorizeCheck } from "../Authorization/metadata";
12
12
  import type { ParameterBinding } from "../Binding/bindings";
13
13
  import { resolveGeneratedBindings } from "../Binding/autoBindings";
14
14
  import { controllerMetaOf, type ActionMeta, type ControllerMeta } from "../Decorators/metadata";
@@ -23,6 +23,8 @@ export interface HttpOpenApiDocumentBuildInput {
23
23
  readonly versioning?: ApiVersioningOptions;
24
24
  readonly title: string;
25
25
  readonly version: string;
26
+ /** Default checks of `runApp({ http: { authorize } })`: such routes are secured too. */
27
+ readonly authorize?: readonly AuthorizeCheck[];
26
28
  }
27
29
 
28
30
  /** Body of every error response: `{ "error": "...", "details": ... }` (see Middleware/errorHandler). */
@@ -80,6 +82,7 @@ function collectOpenApiOperations(
80
82
  routeTemplate: joinPaths(meta.prefix, route.template),
81
83
  versioning: input.versioning,
82
84
  generated,
85
+ defaultChecks: input.authorize ?? [],
83
86
  }));
84
87
  }
85
88
  }
@@ -98,6 +101,7 @@ interface CreateOperationInput {
98
101
  readonly routeTemplate: string;
99
102
  readonly versioning?: ApiVersioningOptions;
100
103
  readonly generated: GeneratedOpenApiMetadata;
104
+ readonly defaultChecks: readonly AuthorizeCheck[];
101
105
  }
102
106
 
103
107
  function createOperation(input: CreateOperationInput): OpenApiCatalogOperation {
@@ -108,7 +112,7 @@ function createOperation(input: CreateOperationInput): OpenApiCatalogOperation {
108
112
  const status = String(successStatus(input.httpMethod, input.action, generatedOperation?.status));
109
113
  const responseSchema = generatedOperation?.response;
110
114
  const contentType = input.action.produces ?? "application/json";
111
- const authorized = !auth.allowAnonymous && auth.authorize !== undefined;
115
+ const authorized = routeAuthorizeChecks(input.controllerClass, input.methodName, input.defaultChecks) !== undefined;
112
116
 
113
117
  return {
114
118
  path: input.path,
@@ -10,7 +10,7 @@ import { corsOptionsOf } from "../Middleware/cors";
10
10
  import { normalizeResult } from "../Results/normalize";
11
11
  import type { ApiVersioningOptions, RouteMiddlewareComposer } from "../options";
12
12
  import { createAuthorizeComposer } from "../Authorization/authorizeComposer";
13
- import { resolveAuthorizeMeta } from "../Authorization/metadata";
13
+ import { resolveAuthorizeMeta, routeAuthorizeChecks, type AuthorizeCheck } from "../Authorization/metadata";
14
14
 
15
15
  /** A registered route as the startup screen lists it. */
16
16
  export interface RegisteredRoute {
@@ -39,7 +39,7 @@ interface ActionPipeline {
39
39
  * precompiled per route — request dispatch performs no metadata work at all.
40
40
  */
41
41
  export class RouterBuilder {
42
- private readonly authorizeComposer = createAuthorizeComposer();
42
+ private readonly authorizeComposer: RouteMiddlewareComposer;
43
43
  /** Every route of the last {@link build}, in registration order. */
44
44
  readonly routes: RegisteredRoute[] = [];
45
45
 
@@ -49,7 +49,11 @@ export class RouterBuilder {
49
49
  private readonly globalPrefix: string | undefined,
50
50
  private readonly versioning: ApiVersioningOptions | undefined,
51
51
  private readonly routeMiddlewareComposer?: RouteMiddlewareComposer,
52
- ) {}
52
+ /** Checks for every route that is not `@AllowAnonymous`. */
53
+ private readonly defaultChecks: readonly AuthorizeCheck[] = [],
54
+ ) {
55
+ this.authorizeComposer = createAuthorizeComposer(defaultChecks);
56
+ }
53
57
 
54
58
  build(controllers: readonly Class<object>[]): Router {
55
59
  const router = new Router();
@@ -110,7 +114,7 @@ export class RouterBuilder {
110
114
  method: declaration.httpMethod,
111
115
  path: fullPath,
112
116
  handler: routeAction.name,
113
- open: access.allowAnonymous === true || access.authorize === undefined,
117
+ open: routeAuthorizeChecks(controllerClass, methodName, this.defaultChecks) === undefined,
114
118
  anonymous: access.allowAnonymous === true,
115
119
  }));
116
120
  }
@@ -2,12 +2,13 @@ import type { Class } from "../di";
2
2
  import type { AccessLogOptions } from "./Middleware/accessLog";
3
3
  import type { CorsOptions } from "./Middleware/cors";
4
4
  import type { ErrorHandlerOptions } from "./Middleware/errorHandler";
5
- import type { Localize } from "./Errors/messages";
5
+ import type { LanguageSelection } from "./Errors/messages";
6
6
  import type { SecurityHeadersOptions } from "./Middleware/securityHeaders";
7
7
  import type { HttpMiddleware } from "./Middleware/types";
8
8
  import type { ActionMeta, ControllerMeta } from "./Decorators/metadata";
9
9
  import type { OpenApiDocsOptions } from "../../library/openapi";
10
10
  import type { ModelValidator } from "./Binding/modelValidator";
11
+ import type { AuthorizeCheck } from "./Authorization/metadata";
11
12
 
12
13
  /** Built-in health endpoint (liveness/readiness probe). */
13
14
  export interface HealthEndpointOptions {
@@ -60,11 +61,11 @@ export interface HttpModuleOptions {
60
61
  /** Request-model validator captured by this HTTP server instance. */
61
62
  readonly validator?: ModelValidator;
62
63
  /**
63
- * Renders framework response texts in the application language. `runApp`
64
+ * Response language of each request and its framework texts. `runApp`
64
65
  * sets it from its `language` and `messages` options; without it the
65
66
  * texts are English.
66
67
  */
67
- readonly localize?: Localize;
68
+ readonly languages?: LanguageSelection;
68
69
  /**
69
70
  * Modules providing the controllers' dependencies. Controllers live inside
70
71
  * the returned HttpModule, so their dependencies must be visible to it:
@@ -129,6 +130,16 @@ export interface HttpModuleOptions {
129
130
  * title/version.
130
131
  */
131
132
  readonly docs?: OpenApiDocsOptions | boolean;
133
+ /**
134
+ * Closed by default: these checks run on every route that is not
135
+ * `@AllowAnonymous`, before the route's own `@Authorize` checks
136
+ * (like ASP.NET Core's fallback policy). `/health` and the docs stay open.
137
+ *
138
+ * ```ts
139
+ * runApp(AppModule, { http: { authorize: signedIn } });
140
+ * ```
141
+ */
142
+ readonly authorize?: AuthorizeCheck | readonly AuthorizeCheck[];
132
143
  /**
133
144
  * Access log for every request (method, path, status, duration, correlation
134
145
  * id). `true` enables the default single-line sink; pass an object to plug a
@@ -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.106.0" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.108.0" }, 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");
@@ -261,18 +261,32 @@ export class Kernel {
261
261
  if (this.startupCleanupFailure !== undefined) return Promise.reject(this.startupCleanupFailure);
262
262
  return Promise.resolve();
263
263
  }
264
+ // Only an application that started announces its stop.
265
+ const wasStarted = this.state === "started";
264
266
  this.state = "stopping";
265
- this.shutdownPromise = this.stopCore(request);
267
+ this.shutdownPromise = this.stopCore(request, wasStarted);
266
268
  return this.shutdownPromise;
267
269
  }
268
270
 
269
- private async stopCore(request: StopRequest): Promise<void> {
271
+ private async stopCore(request: StopRequest, wasStarted: boolean): Promise<void> {
270
272
  // The whole graceful-stop sequence — lifetime notifications, the STOPPING
271
273
  // event, hosted services/hooks and disposal — is bounded by one budget. A
272
274
  // single hanging callback or handler must not wedge shutdown forever.
275
+ const announce = wasStarted && this.options.startupReport && this.options.schemaOnly !== true;
276
+ const logger = announce ? this.container.tryResolve(LOGGER) : undefined;
277
+ const startedAt = performance.now();
273
278
  const work = this.runShutdownSequence(request);
274
279
  try {
275
280
  await awaitShutdown(work, this.options.shutdownTimeoutMs);
281
+ if (announce) {
282
+ const fields = {
283
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
284
+ exitCode: request.exitCode,
285
+ shutdownMs: Number((performance.now() - startedAt).toFixed(1)),
286
+ };
287
+ if (logger) logger.info("bazis stopped", fields);
288
+ else console.log(`[bazis] stopped ${JSON.stringify(fields)}`);
289
+ }
276
290
  } finally {
277
291
  this.state = "stopped";
278
292
  }
@@ -441,6 +455,12 @@ export class Kernel {
441
455
  console.error("[bazis] Unhandled error, shutting down:", redacted);
442
456
  }
443
457
 
458
+ /** Where the servers listen: `{ listening: ["http://localhost:3000"] }`. */
459
+ private listening(): { listening?: readonly string[] } {
460
+ const urls = (this.container.tryResolve(StartupReport)?.listeners ?? []).map((listener) => listener.url);
461
+ return urls.length > 0 ? { listening: urls } : {};
462
+ }
463
+
444
464
  private printStartupReport(): void {
445
465
  if (this.options.onStarted !== undefined) {
446
466
  try {
@@ -460,6 +480,7 @@ export class Kernel {
460
480
  hostedServices: this.coordinator.startedHostedCount,
461
481
  hooks: this.coordinator.hookCount,
462
482
  startupMs: Number(this.startupMs.toFixed(1)),
483
+ ...this.listening(),
463
484
  };
464
485
  if (logger) {
465
486
  logger.info("bazis started", fields);
@@ -166,9 +166,11 @@ export class KernelBuilder {
166
166
  const userRoot = resolveUserRoot(this.rootModule, environment);
167
167
  const configs = collectModuleConfigs([userRoot]);
168
168
  const configRegistry = new ConfigRegistry(configs, environment.name, configuration);
169
- const startupReport = this.startupReport ?? !environment.isProduction;
170
- // The screen replaces the report line, so it follows the same switch.
171
- const screen = this.startupScreen !== undefined && startupReport;
169
+ // The `bazis started` / `bazis stopped` lines are printed in every
170
+ // environment: in production they are the only sign that the process is up.
171
+ const startupReport = this.startupReport ?? true;
172
+ // The screen replaces the started line outside production.
173
+ const screen = this.startupScreen !== undefined && (this.startupReport ?? !environment.isProduction);
172
174
  const report = new StartupReport(screen);
173
175
  report.configuration(configRegistry.inspect());
174
176
  this.builtReport = report;
@@ -98,7 +98,7 @@ const en = {
98
98
  codegen: "codegen", typecheck: "typecheck", config: "config", connections: "connections", schema: "schema",
99
99
  modules: "modules", services: "services", tests: "bun test", compile: "compile", files: "files", register: "register",
100
100
  },
101
- meta: { env: "env", watch: "watch", on: "on", off: "off", src: "src", pid: "pid", target: "target", out: "output", path: "path" },
101
+ meta: { env: "env", watch: "watch", on: "on", off: "off", src: "src", pid: "pid", target: "target", out: "output", path: "path", platform: "platform" },
102
102
  common: {
103
103
  of: "of", in: "in", and: "and", none: "none", ok: "ok", empty: "empty", plus: "plus", memory: "memory",
104
104
  declaredIn: "declared in", code: "code", where: "where", try: "try", logs: "logs", inProcess: "in process",
@@ -260,7 +260,7 @@ const ru: Messages = {
260
260
  codegen: "кодогенерация", typecheck: "типы", config: "конфигурация", connections: "подключения", schema: "схема",
261
261
  modules: "модули", services: "сервисы", tests: "bun test", compile: "компиляция", files: "файлы", register: "подключение",
262
262
  },
263
- meta: { env: "среда", watch: "слежение", on: "вкл", off: "выкл", src: "вход", pid: "pid", target: "цель", out: "файл", path: "путь" },
263
+ meta: { env: "среда", watch: "слежение", on: "вкл", off: "выкл", src: "вход", pid: "pid", target: "цель", out: "файл", path: "путь", platform: "платформа" },
264
264
  common: {
265
265
  of: "из", in: "в", and: "и", none: "нет", ok: "в порядке", empty: "пусто", plus: "ещё", memory: "память",
266
266
  declaredIn: "объявлено в", code: "код", where: "где", try: "попробуйте", logs: "журнал", inProcess: "в процессе",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.106.0",
3
+ "version": "0.108.0",
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",