bazis 0.109.1 → 0.110.1

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.
@@ -15,6 +15,7 @@ import { grpcModule, type GrpcModuleOptions } from "../grpc";
15
15
  import { buildHttpOpenApiDocument } from "../http/OpenApi/openApiDocument";
16
16
  import { defaultAuthorizeChecks } from "../http/Authorization/metadata";
17
17
  import { KernelBuilder, Bazis, KernelError, type KernelOptions, type ValidatableConfig } from "../kernel";
18
+ import { reportBuildFailure } from "../kernel/Bazis";
18
19
  import { websocketModule, type WebSocketOptions } from "../websocket";
19
20
  import {
20
21
  isUiProfileAuthoringClass,
@@ -287,7 +288,14 @@ function normalizeAbsolutePath(path: string): string {
287
288
  */
288
289
  export async function runApp(root: BazisModuleRef, options: RunAppOptions = {}): Promise<number> {
289
290
  await loadBazisGeneratedRuntime();
290
- return Bazis.run(composeApp(root, options), options.kernel, (builder) => {
291
+ let app: BazisModuleRef;
292
+ try {
293
+ app = composeApp(root, options);
294
+ } catch (error) {
295
+ // Composing already reads routes and options: report its errors like the kernel build does.
296
+ return reportBuildFailure(error);
297
+ }
298
+ return Bazis.run(app, options.kernel, (builder) => {
291
299
  builder.useApplicationRoot(root);
292
300
  options.configure?.(builder);
293
301
  });
@@ -1,4 +1,5 @@
1
1
  import type { ServiceScope } from "../../di";
2
+ import { HttpSetupError } from "../Errors/HttpError";
2
3
 
3
4
  // Same one-line polyfill as the rest of the HTTP module: Bun executes TC39
4
5
  // decorators natively, but Symbol.metadata may be missing in the runtime.
@@ -46,6 +47,8 @@ interface AuthActionMeta {
46
47
  allowAnonymous?: boolean;
47
48
  /** Declaration made on this exact class/method metadata object. */
48
49
  declaration?: "authorize" | "anonymous";
50
+ /** Both `@Authorize` and `@AllowAnonymous` were declared here. */
51
+ conflict?: true;
49
52
  }
50
53
 
51
54
  interface AuthControllerMeta {
@@ -53,6 +56,8 @@ interface AuthControllerMeta {
53
56
  allowAnonymous?: boolean;
54
57
  /** Declaration made on this exact class metadata object. */
55
58
  declaration?: "authorize" | "anonymous";
59
+ /** Both `@Authorize` and `@AllowAnonymous` were declared on the class. */
60
+ conflict?: true;
56
61
  actions: Map<string | symbol, AuthActionMeta>;
57
62
  }
58
63
 
@@ -105,8 +110,15 @@ function ownActionMeta(metadata: object, methodName: string | symbol): AuthActio
105
110
  return action;
106
111
  }
107
112
 
113
+ // The other declaration on the same class or method: each would silently undo
114
+ // the other and the decorator order would decide. Reported when routes are built.
115
+ function markConflict(target: { declaration?: "authorize" | "anonymous"; conflict?: true }, declaration: "authorize" | "anonymous"): void {
116
+ if (target.declaration !== undefined && target.declaration !== declaration) target.conflict = true;
117
+ }
118
+
108
119
  export function setControllerAuthorize(metadata: object, options: AuthorizeOptions): void {
109
120
  const meta = ownMeta(metadata);
121
+ markConflict(meta, "authorize");
110
122
  meta.authorize = meta.declaration === "authorize" && meta.authorize
111
123
  ? { checks: [...options.checks, ...meta.authorize.checks] }
112
124
  : options;
@@ -116,6 +128,7 @@ export function setControllerAuthorize(metadata: object, options: AuthorizeOptio
116
128
 
117
129
  export function setControllerAllowAnonymous(metadata: object): void {
118
130
  const meta = ownMeta(metadata);
131
+ markConflict(meta, "anonymous");
119
132
  meta.allowAnonymous = true;
120
133
  meta.declaration = "anonymous";
121
134
  delete meta.authorize;
@@ -123,6 +136,7 @@ export function setControllerAllowAnonymous(metadata: object): void {
123
136
 
124
137
  export function setActionAuthorize(metadata: object, methodName: string | symbol, options: AuthorizeOptions): void {
125
138
  const action = ownActionMeta(metadata, methodName);
139
+ markConflict(action, "authorize");
126
140
  action.authorize = action.declaration === "authorize" && action.authorize
127
141
  ? { checks: [...options.checks, ...action.authorize.checks] }
128
142
  : options;
@@ -132,6 +146,7 @@ export function setActionAuthorize(metadata: object, methodName: string | symbol
132
146
 
133
147
  export function setActionAllowAnonymous(metadata: object, methodName: string | symbol): void {
134
148
  const action = ownActionMeta(metadata, methodName);
149
+ markConflict(action, "anonymous");
135
150
  action.allowAnonymous = true;
136
151
  action.declaration = "anonymous";
137
152
  delete action.authorize;
@@ -153,6 +168,7 @@ export interface ResolvedAuthorizeMeta {
153
168
  *
154
169
  * Inheritance is different: a subclass's own class-level declaration replaces
155
170
  * the base class's, and an overriding method's declaration replaces the base method's.
171
+ * Both decorators on the same class or method throw `HttpSetupError`.
156
172
  */
157
173
  export function resolveAuthorizeMeta(ctor: object, methodName: string | symbol): ResolvedAuthorizeMeta {
158
174
  const metadata = (ctor as { [key: symbol]: unknown })[Symbol.metadata as unknown as symbol] as
@@ -163,7 +179,20 @@ export function resolveAuthorizeMeta(ctor: object, methodName: string | symbol):
163
179
  return { allowAnonymous: false };
164
180
  }
165
181
 
182
+ const owner = (ctor as { readonly name?: string }).name || "Controller";
183
+ if (meta.conflict) {
184
+ throw new HttpSetupError(
185
+ `${owner} has both @AllowAnonymous() and @Authorize(...) on the class: the result would depend on the decorator order. `
186
+ + "Keep one; to open single methods of a protected class, put @AllowAnonymous() on them.",
187
+ );
188
+ }
166
189
  const action = meta.actions.get(methodName);
190
+ if (action?.conflict) {
191
+ throw new HttpSetupError(
192
+ `${owner}.${String(methodName)} has both @AllowAnonymous() and @Authorize(...): the result would depend on the decorator order. `
193
+ + "Keep @Authorize(...) to protect the route, or @AllowAnonymous() to open it.",
194
+ );
195
+ }
167
196
  if (action?.allowAnonymous) {
168
197
  return { allowAnonymous: true };
169
198
  }
@@ -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.109.1" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.110.1" }, 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");
@@ -47,17 +47,7 @@ export class Bazis {
47
47
  try {
48
48
  kernel = await builder.build();
49
49
  } catch (error) {
50
- if (screen !== undefined && !(error instanceof ShutdownTimeoutError)) {
51
- screen.failed(error, undefined, builder.report);
52
- process.exitCode = 1;
53
- return 1;
54
- }
55
- if (error instanceof DiError || error instanceof KernelError) {
56
- console.error(`[bazis] configuration error: ${redactSensitiveText(error.message)}`);
57
- process.exitCode = 1;
58
- return 1;
59
- }
60
- throw error;
50
+ return reportBuildFailure(error, screen, builder.report);
61
51
  }
62
52
 
63
53
  try {
@@ -96,3 +86,27 @@ export class Bazis {
96
86
  }
97
87
  }
98
88
  }
89
+
90
+ /**
91
+ * @internal How a start that failed before the kernel was built is reported:
92
+ * the startup screen on a terminal, otherwise one `[bazis] configuration error`
93
+ * line for DI and kernel errors. Returns exit code 1; anything else is rethrown.
94
+ * `runApp` uses it for errors found while it composes the application.
95
+ */
96
+ export function reportBuildFailure(
97
+ error: unknown,
98
+ screen: StartupScreen | undefined = startupScreenEnabled() ? new StartupScreen() : undefined,
99
+ report?: KernelBuilder["report"],
100
+ ): number {
101
+ if (screen !== undefined && !(error instanceof ShutdownTimeoutError)) {
102
+ screen.failed(error, undefined, report);
103
+ process.exitCode = 1;
104
+ return 1;
105
+ }
106
+ if (error instanceof DiError || error instanceof KernelError) {
107
+ console.error(`[bazis] configuration error: ${redactSensitiveText(error.message)}`);
108
+ process.exitCode = 1;
109
+ return 1;
110
+ }
111
+ throw error;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.109.1",
3
+ "version": "0.110.1",
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",