@nextrush/core 3.0.7 → 4.0.0-beta.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.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
- import { Middleware, Context, Plugin, Next } from '@nextrush/types';
2
- export { ContentType, Context, ContextState, HttpMethod, HttpStatus, HttpStatusCode, Middleware, Next, Plugin, PluginWithHooks, QueryParams, RouteHandler, RouteParams } from '@nextrush/types';
3
- export { BadRequestError, ForbiddenError, HttpError, InternalServerError, NextRushError, NotFoundError, UnauthorizedError, createError as createHttpError } from '@nextrush/errors';
1
+ import { Logger, ProxyTrust, Router, Container, Middleware, RouteEntry, Context, Extension, Next } from '@nextrush/types';
2
+ export { ContentType, Context, ContextState, Extension, ExtensionContext, ExtensionHost, HttpMethod, HttpStatus, HttpStatusCode, Logger, Middleware, Next, QueryParams, RouteEntry, RouteHandler, RouteParams, Router } from '@nextrush/types';
3
+ export { BadRequestError, ForbiddenError, HttpError, InternalServerError, NextRushError, NotFoundError, UnauthorizedError } from '@nextrush/errors';
4
4
 
5
5
  /**
6
6
  * @nextrush/core - Application Class
7
7
  *
8
8
  * The Application class is the main entry point for NextRush.
9
- * It manages middleware registration and plugin installation.
9
+ * It manages middleware registration and the extension lifecycle.
10
+ *
11
+ * See docs/RFC/class-runtime/005-plugin-system.md for the extension model.
10
12
  *
11
13
  * @packageDocumentation
12
14
  */
@@ -16,13 +18,21 @@ export { BadRequestError, ForbiddenError, HttpError, InternalServerError, NextRu
16
18
  */
17
19
  type ErrorHandler = (error: Error, ctx: Context) => void | Promise<void>;
18
20
  /**
19
- * Logger interface for pluggable logging
21
+ * Options for {@link Application.close}.
22
+ *
23
+ * @see RFC-022 (`docs/RFC/class-runtime/022-bounded-teardown-lifecycle.md`) / ADR-0012.
20
24
  */
21
- interface Logger {
22
- error(...args: unknown[]): void;
23
- warn(...args: unknown[]): void;
24
- info(...args: unknown[]): void;
25
- debug(...args: unknown[]): void;
25
+ interface CloseOptions {
26
+ /**
27
+ * Bound, in milliseconds, on total teardown time. Every teardown unit
28
+ * (extension `destroy()`) races against this budget independently — a
29
+ * unit that does not finish in time is reported as a {@link TeardownTimeoutError}
30
+ * in the returned array, and never blocks `close()` past the budget.
31
+ *
32
+ * @remarks Omitted = unbounded, identical to calling `close()` with no
33
+ * arguments today (`Promise.allSettled`, no timeout).
34
+ */
35
+ timeout?: number;
26
36
  }
27
37
  /**
28
38
  * Application options
@@ -34,37 +44,65 @@ interface ApplicationOptions {
34
44
  */
35
45
  env?: 'development' | 'production' | 'test';
36
46
  /**
37
- * Whether to use proxy headers (X-Forwarded-For, etc.)
47
+ * Proxy-trust specification for `ctx.ip` resolution (RFC-030, SEC-01).
48
+ *
49
+ * @remarks
50
+ * `false` trusts nothing (the socket peer is always `ctx.ip`); a `number`
51
+ * trusts exactly that many reverse-proxy hops; a `string[]` of CIDR
52
+ * ranges/IPs trusts only a direct peer inside that set. `true` and `0`
53
+ * throw at construction — see {@link validateProxyTrust}.
54
+ *
38
55
  * @default false
39
56
  */
40
- proxy?: boolean;
57
+ proxy?: ProxyTrust;
41
58
  /**
42
59
  * Custom logger. Defaults to no-op (silent).
43
60
  *
44
61
  * @remarks
45
- * Pass `console` for quick development logging:
46
- * ```ts
47
- * const app = createApp({ logger: console });
48
- * ```
49
- *
50
- * For production, provide a structured logger (e.g. pino, winston)
51
- * that implements the {@link Logger} interface.
52
- *
53
- * Adapters and internal hooks use `app.logger` — configuring it here
54
- * ensures consistent logging across the framework.
62
+ * Pass `console` for quick development logging. For production, provide a
63
+ * structured logger (pino, winston, `@nextrush/logger`) implementing {@link Logger}.
55
64
  */
56
65
  logger?: Logger;
66
+ /**
67
+ * A router the app owns. Route methods (`app.get`, `app.post`, …) delegate to
68
+ * it, and it is mounted last at `ready()`. The `nextrush` meta-package's
69
+ * `createApp` injects one automatically; pass it explicitly when using
70
+ * `@nextrush/core` directly.
71
+ */
72
+ router?: Router;
73
+ /**
74
+ * A per-app DI container. Exposed to extensions via `ExtensionContext.container`
75
+ * and used by class-based registrars (e.g. `registerControllers`). Injected by
76
+ * `nextrush/class`; omitted for functional, DI-free apps.
77
+ */
78
+ container?: Container;
57
79
  }
58
80
  /**
59
81
  * Listener callback type
60
82
  */
61
83
  type ListenCallback = () => void;
62
84
  /**
63
- * Routable interface - any object with routes() method
64
- * This allows mounting Router instances without circular dependency
85
+ * Routable interface - any object with routes() method.
86
+ * Allows mounting Router instances without a circular dependency.
65
87
  */
66
88
  interface Routable {
67
89
  routes(): Middleware;
90
+ /**
91
+ * Test whether `path` falls under this router's mount prefix, using the
92
+ * SAME normalization the router itself matches with (case folding per its
93
+ * `caseSensitive` option, structural normalization) — never a raw
94
+ * `startsWith` comparison, which would apply a different rule than the
95
+ * router's own dispatch (RFC-029, SEC-02/SEC-15). Optional so a minimal
96
+ * `Routable` (e.g. a test double) still mounts; {@link createPrefixMount}
97
+ * falls back to a literal prefix test when this is absent.
98
+ *
99
+ * @param path - The full, still-query-stripped request path being tested.
100
+ * @param prefix - The normalized mount prefix (leading `/`, no trailing `/`).
101
+ * @returns The path's remainder past the prefix (e.g. `/users` for
102
+ * `/Admin/Users` mounted at `/admin`), or `undefined` if `path` is not
103
+ * under `prefix` per this router's own normalization.
104
+ */
105
+ matchesMountPrefix?(path: string, prefix: string): string | undefined;
68
106
  }
69
107
  /**
70
108
  * The Application class
@@ -77,195 +115,183 @@ interface Routable {
77
115
  * ctx.json({ message: 'Hello World' });
78
116
  * });
79
117
  *
80
- * // Mount with an adapter
81
- * listen(app, { port: 3000 });
118
+ * // Extensions (rare long-lived services) are booted at ready(), which
119
+ * // adapters call automatically before start().
120
+ * app.extend(events());
121
+ *
122
+ * listen(app, { port: 8080 });
82
123
  * ```
83
124
  */
84
125
  declare class Application {
85
- /**
86
- * Middleware stack
87
- */
88
126
  private readonly middlewareStack;
89
127
  /**
90
- * Installed plugins
128
+ * Boot-time security audit checks contributed by tagged middleware
129
+ * (`security-boundaries` capability, task 8.1). Collected as `use()`
130
+ * registers each middleware; run once by `_boot()`, production-only.
91
131
  */
92
- private readonly plugins;
132
+ private readonly securityAudits;
133
+ /** Registered extensions, in registration order (setup runs at ready()) */
134
+ private readonly extensions;
135
+ /** Registered extension names — enforces uniqueness */
136
+ private readonly extensionNames;
93
137
  /**
94
- * Custom error handler
138
+ * Combined teardown-unit registration order: extension `destroy()`s and
139
+ * {@link onClose} hooks interleaved in the exact order they were registered,
140
+ * so `_shutdown()` can run the whole set in one reverse-of-registration pass
141
+ * (RFC-022 §7.3 — "teardown becomes a flat list of independently-isolated,
142
+ * budget-raced units"). Populated lazily by {@link extend} (for extensions
143
+ * declaring `destroy()`) and {@link onClose}.
95
144
  */
145
+ private readonly teardownUnits;
146
+ /** Whether {@link close} has started — {@link onClose} is a pre-shutdown-only registration point (RFC-022 §8.6). */
147
+ private _closeStarted;
148
+ /** Names decorated onto the app — enforces collision detection */
149
+ private readonly decorations;
96
150
  private _errorHandler;
97
- /**
98
- * Pluggable logger
99
- */
100
151
  readonly logger: Logger;
101
- /**
102
- * Application options
103
- */
104
152
  readonly options: ApplicationOptions;
105
- /**
106
- * Whether the app is running
107
- */
153
+ /** Whether the app is running (server listening) */
108
154
  private _isRunning;
155
+ /** Whether the app has been booted (extensions set up, config frozen) */
156
+ private _isReady;
157
+ /** Whether ready() mounted the app-owned router (so close() can undo it) */
158
+ private _routerMounted;
159
+ /** In-flight boot promise — memoized so concurrent ready() calls share one boot (H-1) */
160
+ private _readyPromise;
161
+ /** In-flight shutdown promise — memoized so concurrent close() calls share one (H-3) */
162
+ private _closePromise;
163
+ /** The app-owned router (optional). Route methods delegate to it. */
164
+ readonly router?: Router;
165
+ /** The app-owned DI container (optional). Exposed to extensions and registrars. */
166
+ readonly container?: Container;
109
167
  constructor(options?: ApplicationOptions);
110
- /**
111
- * Check if running in production
112
- */
168
+ /** Check if running in production */
113
169
  get isProduction(): boolean;
114
- /**
115
- * Check if app is running
116
- */
170
+ /** Check if app is running */
117
171
  get isRunning(): boolean;
118
172
  /**
119
- * Get middleware count
173
+ * Whether a graceful shutdown is currently in progress — `true` from the
174
+ * moment {@link close} begins tearing down until it completes (F-12:
175
+ * shutdown observability, RFC-022). A readiness check or operator tooling
176
+ * can read this to reflect an in-progress drain.
120
177
  */
178
+ get isDraining(): boolean;
179
+ /** Check if app has been booted via ready() */
180
+ get isReady(): boolean;
181
+ /** Get middleware count */
121
182
  get middlewareCount(): number;
183
+ /** Get registered extension count */
184
+ get extensionCount(): number;
122
185
  /**
123
- * Throws if the app is already running.
124
- * Prevents configuration mutations after start().
186
+ * Throws if the app configuration is frozen (after ready() or start()).
187
+ * Prevents unsafe mutations once the app has booted or is serving traffic.
125
188
  */
126
- private assertNotRunning;
189
+ private assertConfigurable;
127
190
  /**
128
191
  * Register middleware function(s)
129
192
  *
130
- * @param middleware - Middleware function or array of middleware
193
+ * @param middleware - Middleware function(s)
131
194
  * @returns this for chaining
132
- *
133
- * @example
134
- * ```typescript
135
- * // Single middleware
136
- * app.use(async (ctx) => {
137
- * console.log(ctx.method, ctx.path);
138
- * await ctx.next();
139
- * });
140
- *
141
- * // Multiple middleware
142
- * app.use(cors(), helmet(), json());
143
- * ```
144
195
  */
145
196
  use(...middleware: Middleware[]): this;
146
197
  /**
147
- * Mount a router at a path prefix
148
- *
149
- * This is the Hono-style API for mounting routers directly on the app.
150
- * The router's routes will only match requests that start with the given prefix.
198
+ * Mount a router at a path prefix.
151
199
  *
152
- * @param path - Path prefix for the router (e.g., '/api/users')
200
+ * @param path - Path prefix (e.g. '/api/users')
153
201
  * @param router - Router instance to mount
154
202
  * @returns this for chaining
155
- *
156
- * @example
157
- * ```typescript
158
- * // Create modular routers
159
- * const users = createRouter();
160
- * users.get('/', listUsers);
161
- * users.get('/:id', getUser);
162
- * users.post('/', createUser);
163
- *
164
- * const posts = createRouter();
165
- * posts.get('/', listPosts);
166
- * posts.get('/:id', getPost);
167
- *
168
- * // Mount directly on app - clean like Hono!
169
- * const app = createApp();
170
- * app.route('/api/users', users);
171
- * app.route('/api/posts', posts);
172
- *
173
- * listen(app, 3000);
174
- * ```
175
203
  */
176
204
  route(path: string, router: Routable): this;
205
+ private requireRouter;
206
+ /** Register a GET route on the app-owned router. */
207
+ get(path: string, ...entries: RouteEntry[]): this;
208
+ /** Register a POST route on the app-owned router. */
209
+ post(path: string, ...entries: RouteEntry[]): this;
210
+ /** Register a PUT route on the app-owned router. */
211
+ put(path: string, ...entries: RouteEntry[]): this;
212
+ /** Register a PATCH route on the app-owned router. */
213
+ patch(path: string, ...entries: RouteEntry[]): this;
214
+ /** Register a DELETE route on the app-owned router. */
215
+ delete(path: string, ...entries: RouteEntry[]): this;
216
+ /** Register a HEAD route on the app-owned router. */
217
+ head(path: string, ...entries: RouteEntry[]): this;
177
218
  /**
178
- * Set the application error handler.
219
+ * Register a route for all HTTP methods on the app-owned router.
179
220
  *
180
- * Replaces any previously set handler. Only one error handler is active
181
- * at a time. For additive error handling, compose logic within a single
182
- * handler or use the `errorHandler()` middleware from `@nextrush/errors`.
183
- *
184
- * @param handler - Error handler function
185
- * @returns this for chaining
186
- *
187
- * @example
188
- * ```typescript
189
- * app.setErrorHandler((error, ctx) => {
190
- * console.error('Request failed:', error);
191
- *
192
- * if (error instanceof ValidationError) {
193
- * ctx.status = 400;
194
- * ctx.json({ error: error.message, details: error.details });
195
- * return;
196
- * }
197
- *
198
- * ctx.status = 500;
199
- * ctx.json({ error: 'Internal Server Error' });
200
- * });
201
- * ```
221
+ * @remarks
222
+ * There is intentionally no `app.options()` verb method it would collide
223
+ * with the `app.options` configuration property. Register OPTIONS routes via
224
+ * `app.all()`, the router directly, or let CORS middleware handle preflight.
202
225
  */
203
- setErrorHandler(handler: ErrorHandler): this;
226
+ all(path: string, ...entries: RouteEntry[]): this;
204
227
  /**
205
- * Set custom error handler.
228
+ * Set the application error handler. Replaces any previously set handler.
206
229
  *
207
230
  * @param handler - Error handler function
208
231
  * @returns this for chaining
209
- *
210
- * @deprecated Use `setErrorHandler()` instead — `onError()` implies
211
- * event subscription (additive), but this is a setter (replaces).
212
232
  */
213
- onError(handler: ErrorHandler): this;
233
+ setErrorHandler(handler: ErrorHandler): this;
214
234
  /**
215
- * Install a plugin.
216
- *
217
- * Handles both sync and async `install()` methods automatically.
218
- * Returns `this` for sync plugins and `Promise<this>` for async ones.
235
+ * Register an extension. Queues it — `setup()` runs later, at `ready()`,
236
+ * in registration order. Synchronous and chainable.
219
237
  *
220
- * @param plugin - Plugin to install
221
- * @returns this (sync) or Promise<this> (async)
238
+ * @param extension - Extension to register. If it declares a decorated
239
+ * shape via `Extension<TDecorated>`, the return type carries that shape —
240
+ * `app.extend(events<MyEvents>()).events` is statically known, no
241
+ * `declare module` augmentation required.
242
+ * @returns this, intersected with the extension's declared decorated shape
222
243
  *
223
244
  * @example
224
245
  * ```typescript
225
- * // Sync plugin
226
- * app.plugin(loggerPlugin({ level: 'info' }));
227
- *
228
- * // Async plugin — just await it
229
- * await app.plugin(databasePlugin({ uri: '...' }));
246
+ * const extended = app.extend(events<MyEvents>());
247
+ * await app.ready(); // adapters call this automatically — runs setup()/decorate()
248
+ * extended.events.emit('user:created', { id: '1' }); // available only AFTER ready()
230
249
  * ```
231
250
  */
232
- plugin(plugin: Plugin): this | Promise<this>;
251
+ extend<TDecorated = Record<string, never>>(extension: Extension<TDecorated>): this & TDecorated;
233
252
  /**
234
- * Install a plugin asynchronously.
235
- *
236
- * @param plugin - Plugin to install
237
- * @returns Promise that resolves when plugin is installed
253
+ * Boot the application: run every registered extension's `setup()` once, in
254
+ * registration order, awaiting async setups. Idempotent — safe to call twice.
255
+ * Adapters call this automatically before `start()`.
238
256
  *
239
- * @deprecated Use `plugin()` instead it handles both sync and async
240
- * plugins automatically.
257
+ * After `ready()` resolves, the configuration is frozen (`use`/`route`/`extend`
258
+ * throw).
241
259
  *
242
- * @example
243
- * ```typescript
244
- * await app.plugin(new DatabasePlugin({ connectionString: '...' }));
245
- * ```
260
+ * @returns this
261
+ * @throws if an extension declares a `needs` dependency not yet registered
246
262
  */
247
- pluginAsync(plugin: Plugin): Promise<this>;
263
+ ready(): Promise<this>;
248
264
  /**
249
- * Get an installed plugin by name
250
- *
251
- * @param name - Plugin name
252
- * @returns Plugin instance or undefined
265
+ * Run the boot sequence once (extension setup + router mount). Guarded and
266
+ * memoized by {@link ready}.
253
267
  */
254
- getPlugin<T extends Plugin>(name: string): T | undefined;
268
+ private _boot;
255
269
  /**
256
- * Check if a plugin is installed
270
+ * Run every registered middleware's boot-time security audit check
271
+ * (`security-boundaries` capability, task 8.1/8.2). `throw`-level verdicts
272
+ * fail boot immediately; `warn`-level verdicts log once each via the app
273
+ * logger and never block boot. Production-only — `_boot()` calls this
274
+ * conditionally so a dev/test app never pays the cost or sees the noise.
257
275
  */
258
- hasPlugin(name: string): boolean;
276
+ private _runSecurityAudits;
259
277
  /**
260
- * Create the request handler callback
278
+ * Attach a value to the app under `name`. Extension-author primitive, invoked
279
+ * through {@link ExtensionContext.decorate} — there is intentionally no public
280
+ * `app.decorate()` (RFC §6.1). Throws on collision.
261
281
  *
262
- * This is the function that adapters call to handle each request.
263
- * It composes all middleware and returns a function that processes context.
282
+ * @internal
283
+ */
284
+ private decorate;
285
+ /**
286
+ * Whether a decoration already occupies `name`.
287
+ */
288
+ hasDecorator(name: string): boolean;
289
+ /**
290
+ * Create the request handler callback.
264
291
  *
265
- * **Important:** The middleware stack is snapshot at call time.
266
- * Middleware or plugins added after `callback()` will NOT take effect
267
- * on the returned handler. Call `callback()` again to get a
268
- * handler that includes subsequent registrations.
292
+ * The middleware stack is snapshot at call time — call `ready()` before
293
+ * `callback()` so extension-registered middleware is included. Adapters do
294
+ * this automatically.
269
295
  *
270
296
  * @returns Request handler function
271
297
  */
@@ -275,46 +301,58 @@ declare class Application {
275
301
  */
276
302
  private handleError;
277
303
  /**
278
- * Default error handler
279
- *
280
- * Uses the `expose` flag on errors (set by HttpError/NextRushError)
281
- * to decide whether to include the error message in the response.
282
- * 4xx errors expose messages by default; 5xx errors never do.
283
- * This prevents leaking internal details (paths, SQL, stack info)
284
- * regardless of environment.
304
+ * Default error handler — delegates to the shared error serializer so the
305
+ * default response matches `@nextrush/errors`' `errorHandler()` (audit C-1).
285
306
  */
286
307
  private defaultErrorHandler;
287
308
  /**
288
309
  * Mark app as running and freeze configuration.
289
310
  *
290
- * Called by adapters when the server starts listening. After this call,
291
- * `use()`, `route()`, `plugin()`, and `pluginAsync()` will throw.
292
- * This prevents unsafe middleware mutations while requests are in flight.
293
- *
294
- * To re-enable registration (e.g. for testing), call `close()` first.
311
+ * Called by adapters when the server starts listening (after `ready()`).
295
312
  */
296
313
  start(): void;
297
314
  /**
298
- * Graceful shutdown
299
- * Destroys all plugins that have a destroy method.
300
- * Uses Promise.allSettled to ensure all plugins are destroyed
301
- * even if some throw errors.
315
+ * Register a teardown hook for a resource outside the extension system
316
+ * (stateful middleware, a manually-attached service, …). Run during
317
+ * {@link close} under the SAME bounded/isolated guarantee as extension
318
+ * `destroy()` one throwing/hanging hook never strands the others — in one
319
+ * combined reverse-of-registration order together with every `extend()`ed
320
+ * extension's `destroy()` (RFC-022 §8.1/§8.2).
321
+ *
322
+ * @param hook - Teardown callback. May be sync or async; a thrown/rejected
323
+ * hook is isolated and its error collected in {@link close}'s returned array.
302
324
  *
303
- * @returns Array of errors from plugins that failed to destroy (empty on success)
325
+ * @remarks Registration is pre-shutdown only: a hook registered after
326
+ * `close()` has already started is not run in that shutdown (RFC-022 §8.6).
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * const store = new MemoryStore(options);
331
+ * app.onClose(() => store.shutdown());
332
+ * ```
304
333
  */
305
- close(): Promise<Error[]>;
334
+ onClose(hook: () => void | Promise<void>): void;
335
+ /**
336
+ * Graceful shutdown. Destroys extensions in reverse registration order,
337
+ * each isolated from the others' failures — one failing/hanging `destroy()`
338
+ * never strands the rest (RFC-022 / ADR-0012).
339
+ *
340
+ * @param options - Optional teardown budget. Omitted = unbounded, byte-identical
341
+ * to today's behavior (`Promise.allSettled`, no timeout). When `timeout` is
342
+ * given, every teardown unit races against it independently; a unit that
343
+ * does not finish in time is reported as a {@link TeardownTimeoutError} in
344
+ * the returned array instead of blocking `close()` past the budget.
345
+ * @returns Array of errors from units that failed or timed out (empty on success)
346
+ */
347
+ close(options?: CloseOptions): Promise<Error[]>;
348
+ /** Run the shutdown sequence once. Guarded and memoized by {@link close}. */
349
+ private _shutdown;
306
350
  }
307
351
  /**
308
352
  * Create a new Application instance
309
353
  *
310
354
  * @param options - Application options
311
355
  * @returns New Application instance
312
- *
313
- * @example
314
- * ```typescript
315
- * const app = createApp();
316
- * const app = createApp({ env: 'production' });
317
- * ```
318
356
  */
319
357
  declare function createApp(options?: ApplicationOptions): Application;
320
358
 
@@ -337,8 +375,14 @@ type ComposedMiddleware = (ctx: Context, next?: Next) => Promise<void>;
337
375
  interface ComposeOptions {
338
376
  /**
339
377
  * Warn when a middleware sends a response AND calls next().
340
- * Enabled by default in non-production environments.
341
- * @default process.env.NODE_ENV !== 'production'
378
+ *
379
+ * @remarks
380
+ * Opt-in and defaults to `false` — `@nextrush/core` is a runtime-agnostic
381
+ * package and must not read `process.env` (global-rules §2, audit C-4). The
382
+ * `Application` enables this in non-production by passing the flag from its
383
+ * own `env` option.
384
+ *
385
+ * @default false
342
386
  */
343
387
  warnDoubleResponse?: boolean;
344
388
  }
@@ -385,4 +429,4 @@ declare function isMiddleware(fn: unknown): fn is Middleware;
385
429
  */
386
430
  declare function flattenMiddleware(arr: (Middleware | Middleware[])[]): Middleware[];
387
431
 
388
- export { Application, type ApplicationOptions, type ComposeOptions, type ComposedMiddleware, type ErrorHandler, type ListenCallback, type Logger, type Routable, compose, createApp, flattenMiddleware, isMiddleware };
432
+ export { Application, type ApplicationOptions, type ComposeOptions, type ComposedMiddleware, type ErrorHandler, type ListenCallback, type Routable, compose, createApp, flattenMiddleware, isMiddleware };