exisjs 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/build.js +3 -0
  3. package/dist/cli/commands/build.js.map +1 -1
  4. package/dist/cli/commands/doctor.d.ts +2 -0
  5. package/dist/cli/commands/doctor.d.ts.map +1 -0
  6. package/dist/cli/commands/doctor.js +258 -0
  7. package/dist/cli/commands/doctor.js.map +1 -0
  8. package/dist/cli/commands/generate.d.ts +7 -1
  9. package/dist/cli/commands/generate.d.ts.map +1 -1
  10. package/dist/cli/commands/generate.js +26 -19
  11. package/dist/cli/commands/generate.js.map +1 -1
  12. package/dist/cli/index.js +15 -1
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/manifest.d.ts.map +1 -1
  15. package/dist/cli/manifest.js +3 -1
  16. package/dist/cli/manifest.js.map +1 -1
  17. package/dist/cli/resolve-aliases.d.ts.map +1 -1
  18. package/dist/cli/resolve-aliases.js +37 -31
  19. package/dist/cli/resolve-aliases.js.map +1 -1
  20. package/dist/cli/routes.d.ts +8 -1
  21. package/dist/cli/routes.d.ts.map +1 -1
  22. package/dist/cli/routes.js +96 -47
  23. package/dist/cli/routes.js.map +1 -1
  24. package/dist/config/config.d.ts +2 -1
  25. package/dist/config/config.d.ts.map +1 -1
  26. package/dist/config/config.js +1 -0
  27. package/dist/config/config.js.map +1 -1
  28. package/dist/config/index.d.ts +1 -0
  29. package/dist/config/index.d.ts.map +1 -1
  30. package/dist/config/index.js +6 -1
  31. package/dist/config/index.js.map +1 -1
  32. package/dist/config/types.d.ts +4 -1
  33. package/dist/config/types.d.ts.map +1 -1
  34. package/dist/docs/content/cli/commands.mdx +36 -3
  35. package/dist/docs/content/cli/overview.mdx +1 -0
  36. package/dist/docs/content/config.mdx +8 -1
  37. package/dist/docs/content/errors.mdx +14 -14
  38. package/dist/docs/content/responses.mdx +106 -0
  39. package/dist/docs/content/routing.mdx +20 -15
  40. package/dist/docs/content/security.mdx +6 -1
  41. package/dist/docs/content/structure.mdx +12 -6
  42. package/dist/middleware/compression.d.ts.map +1 -1
  43. package/dist/middleware/compression.js +20 -5
  44. package/dist/middleware/compression.js.map +1 -1
  45. package/dist/middleware/middleware.d.ts +3 -0
  46. package/dist/middleware/middleware.d.ts.map +1 -1
  47. package/dist/middleware/middleware.js +112 -46
  48. package/dist/middleware/middleware.js.map +1 -1
  49. package/dist/middleware/security.d.ts.map +1 -1
  50. package/dist/middleware/security.js +55 -10
  51. package/dist/middleware/security.js.map +1 -1
  52. package/dist/router/index.d.ts +2 -0
  53. package/dist/router/index.d.ts.map +1 -1
  54. package/dist/router/index.js +5 -1
  55. package/dist/router/index.js.map +1 -1
  56. package/dist/router/route-scanner.d.ts.map +1 -1
  57. package/dist/router/route-scanner.js +16 -3
  58. package/dist/router/route-scanner.js.map +1 -1
  59. package/dist/router/router.d.ts.map +1 -1
  60. package/dist/router/router.js +9 -2
  61. package/dist/router/router.js.map +1 -1
  62. package/dist/sanitize/index.d.ts +9 -9
  63. package/dist/sanitize/index.d.ts.map +1 -1
  64. package/dist/sanitize/index.js +19 -10
  65. package/dist/sanitize/index.js.map +1 -1
  66. package/dist/sanitize/sanitize.d.ts +11 -11
  67. package/dist/sanitize/sanitize.d.ts.map +1 -1
  68. package/dist/sanitize/sanitize.js +22 -13
  69. package/dist/sanitize/sanitize.js.map +1 -1
  70. package/dist/server/app.d.ts.map +1 -1
  71. package/dist/server/app.js +18 -0
  72. package/dist/server/app.js.map +1 -1
  73. package/dist/server/response.d.ts +27 -0
  74. package/dist/server/response.d.ts.map +1 -1
  75. package/dist/server/response.js +109 -4
  76. package/dist/server/response.js.map +1 -1
  77. package/dist/server/sse.d.ts +82 -0
  78. package/dist/server/sse.d.ts.map +1 -0
  79. package/dist/server/sse.js +216 -0
  80. package/dist/server/sse.js.map +1 -0
  81. package/dist/testing/client.d.ts.map +1 -1
  82. package/dist/testing/client.js +33 -5
  83. package/dist/testing/client.js.map +1 -1
  84. package/dist/testing/index.d.ts +1 -1
  85. package/dist/testing/index.d.ts.map +1 -1
  86. package/dist/testing/index.js +2 -1
  87. package/dist/testing/index.js.map +1 -1
  88. package/dist/types.d.ts +1 -0
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +1 -0
  91. package/dist/types.js.map +1 -1
  92. package/dist/validator/index.d.ts +2 -0
  93. package/dist/validator/index.d.ts.map +1 -1
  94. package/dist/validator/index.js.map +1 -1
  95. package/dist/validator/tex-types.d.ts +2 -2
  96. package/dist/validator/tex-types.d.ts.map +1 -1
  97. package/dist/validator/tex.d.ts.map +1 -1
  98. package/dist/validator/tex.js +1 -0
  99. package/dist/validator/tex.js.map +1 -1
  100. package/package.json +1 -1
@@ -79,4 +79,5 @@ $ npx exisjs g r users --oop
79
79
  | `repl` | `console` | Starts an interactive Node.js REPL loaded with the application context. |
80
80
  | `exports` | | Displays all available `@exisjs` and `exisjs/*` subpath exports. |
81
81
  | `info` | | Displays system, Node.js, and ExisJS runtime information. |
82
+ | `doctor` | | Performs system, native Rust acceleration, TypeScript, and routing diagnostics. |
82
83
  | `init` | | Initializes a new ExisJS application in the current directory. |
@@ -140,7 +140,14 @@ Here is a full breakdown of the configuration schema:
140
140
 
141
141
  ### Global Middleware
142
142
 
143
- - `cors` _(object | boolean)_: Global CORS policy configuration (`origin`, `methods`, `credentials`, etc.).
143
+ - `cors` _(object | boolean)_: Global CORS policy configuration. Supports:
144
+ - Exact string origins (e.g. `'https://example.com'` or `'*'`).
145
+ - Regular expressions (e.g. `/localhost:\d+/`).
146
+ - Arrays mixing strings and regexes (`['https://app.example.com', /https:\/\/.*\.domain\.com/']`).
147
+ - Dynamic callbacks: `(origin, cb) => ...` or async functions: `async (origin) => boolean` for multi-tenant setups.
148
+ - `transformResponse` _(boolean | function)_: Automatically wraps all successful handler responses into a standardized envelope:
149
+ - When `true`: `{ success: true, data: ..., timestamp: ... }` (matching `HttpError`'s `{ success: false, error: ... }`).
150
+ - When a function: `(data, req, res) => ({ ... })` to provide your own custom envelope.
144
151
  - `helmet` _(object | boolean)_: Automatically secures your app with standard HTTP headers. (Enabled by default).
145
152
  - `compression` _(object | boolean)_: Enables Gzip/Brotli compression for all HTTP responses.
146
153
  - `logger` _(object | boolean)_: Configures the internal Pino-based logger (`level`, `pretty`).
@@ -247,8 +247,8 @@ You can use the `metrics` middleware to track request durations and counts.
247
247
 
248
248
  ### Functional Paradigm
249
249
 
250
- ```typescript filename="src/http/gateway.ts"
251
- import { defineGateway } from 'exisjs/router'
250
+ ```typescript filename="src/http/boundary.ts"
251
+ import { defineBoundary } from 'exisjs/router'
252
252
  import { metrics } from 'exisjs/observability'
253
253
 
254
254
  const myPrometheusAdapter = {
@@ -261,15 +261,15 @@ const myPrometheusAdapter = {
261
261
  }
262
262
  }
263
263
 
264
- export default defineGateway({
264
+ export const config = defineBoundary({
265
265
  middleware: [metrics(myPrometheusAdapter)]
266
266
  })
267
267
  ```
268
268
 
269
269
  ### Class-Based (OOP) Paradigm
270
270
 
271
- ```typescript filename="src/http/gateway.ts"
272
- import { Gateway } from 'exisjs/decorators'
271
+ ```typescript filename="src/http/boundary.ts"
272
+ import { Boundary } from 'exisjs/decorators'
273
273
  import { metrics } from 'exisjs/observability'
274
274
 
275
275
  const myPrometheusAdapter = {
@@ -280,10 +280,10 @@ const myPrometheusAdapter = {
280
280
  }
281
281
  }
282
282
 
283
- @Gateway({
283
+ @Boundary({
284
284
  middleware: [metrics(myPrometheusAdapter)]
285
285
  })
286
- export default class RootGateway {}
286
+ export default class RootBoundary {}
287
287
  ```
288
288
 
289
289
  *Note: ExisJS normalizes dynamic paths (e.g., turning `/users/123` into `/users/:id`) to prevent metrics cardinality issues.*
@@ -294,8 +294,8 @@ ExisJS seamlessly integrates with OpenTelemetry (OTel) via the `tracing` middlew
294
294
 
295
295
  ### Functional Paradigm
296
296
 
297
- ```typescript filename="src/http/gateway.ts"
298
- import { defineGateway } from 'exisjs/router'
297
+ ```typescript filename="src/http/boundary.ts"
298
+ import { defineBoundary } from 'exisjs/router'
299
299
  import { tracing } from 'exisjs/observability'
300
300
 
301
301
  const myOtelAdapter = {
@@ -312,15 +312,15 @@ const myOtelAdapter = {
312
312
  }
313
313
  }
314
314
 
315
- export default defineGateway({
315
+ export const config = defineBoundary({
316
316
  middleware: [tracing(myOtelAdapter)]
317
317
  })
318
318
  ```
319
319
 
320
320
  ### Class-Based (OOP) Paradigm
321
321
 
322
- ```typescript filename="src/http/gateway.ts"
323
- import { Gateway } from 'exisjs/decorators'
322
+ ```typescript filename="src/http/boundary.ts"
323
+ import { Boundary } from 'exisjs/decorators'
324
324
  import { tracing } from 'exisjs/observability'
325
325
 
326
326
  const myOtelAdapter = {
@@ -335,10 +335,10 @@ const myOtelAdapter = {
335
335
  }
336
336
  }
337
337
 
338
- @Gateway({
338
+ @Boundary({
339
339
  middleware: [tracing(myOtelAdapter)]
340
340
  })
341
- export default class RootGateway {}
341
+ export default class RootBoundary {}
342
342
  ```
343
343
 
344
344
  By providing these standard interfaces natively, your application metrics and traces are bound to the ExisJS router lifecycle.
@@ -269,3 +269,109 @@ ExisJS features native, automatic ETag generation.
269
269
  If the client sends an `If-None-Match` header matching the ETag of the current response, ExisJS will immediately abort serialization and send a `304 Not Modified` header with an empty body, saving bandwidth and CPU cycles.
270
270
 
271
271
  *Note: ETag generation is disabled by default, but can be enabled globally in your `exis()` config, or manually per-route via `res.etagEnabled = true`.*
272
+
273
+ ---
274
+
275
+ ## 7. Server-Sent Events (SSE) & AI Streaming
276
+
277
+ ExisJS provides a first-class `res.sse()` method for real-time live events and AI chat completions (OpenAI, Anthropic, Ollama, LangChain).
278
+
279
+ When called, `res.sse()` automatically configures and flushes the required HTTP headers:
280
+ - `Content-Type: text/event-stream; charset=utf-8`
281
+ - `Cache-Control: no-cache, no-transform`
282
+ - `Connection: keep-alive`
283
+ - `X-Accel-Buffering: no` (disables NGINX proxy buffering)
284
+
285
+ You can import `SSEStream`, `formatSSEEvent`, `SSEMessage`, and `SSEOptions` from `exisjs/router`.
286
+
287
+ ### Basic SSE Stream
288
+
289
+ #### Functional Paradigm
290
+
291
+ ```typescript filename="src/http/events/route.ts"
292
+ import { controller, route } from 'exisjs/router'
293
+
294
+ export default controller({
295
+ liveEvents: route.get('/', {
296
+ handle(req, res) {
297
+ // Initialize the stream (heartbeat ping every 15s by default)
298
+ const sse = res.sse()
299
+
300
+ sse.send({ event: 'connected', data: { timestamp: Date.now() } })
301
+
302
+ const interval = setInterval(() => {
303
+ sse.send({ event: 'price-update', data: { symbol: 'BTC', price: 95000 } })
304
+ }, 1000)
305
+
306
+ // Automatically clean up intervals when the client disconnects or closes the tab
307
+ sse.onClose(() => {
308
+ clearInterval(interval)
309
+ })
310
+ }
311
+ })
312
+ })
313
+ ```
314
+
315
+ #### Class-Based (OOP) Paradigm
316
+
317
+ ```typescript filename="src/http/events/route.ts"
318
+ import { Controller, Get, Res } from 'exisjs/decorators'
319
+ import type { Response } from 'exisjs/router'
320
+
321
+ @Controller()
322
+ export default class EventsController {
323
+ @Get('/events')
324
+ streamEvents(@Res() res: Response) {
325
+ const sse = res.sse()
326
+
327
+ sse.send({ event: 'connected', data: { status: 'ok' } })
328
+
329
+ sse.onClose(() => {
330
+ console.log('Client disconnected from SSE stream')
331
+ })
332
+ }
333
+ }
334
+ ```
335
+
336
+ ### AI / LLM Streaming with `sse.pipeFrom()`
337
+
338
+ You can directly stream chunks from any `AsyncIterable` (such as the OpenAI SDK or custom generators) using `sse.pipeFrom()`:
339
+
340
+ ```typescript filename="src/http/ai/route.ts"
341
+ import { controller, route } from 'exisjs/router'
342
+
343
+ export default controller({
344
+ chat: route.post('/', {
345
+ handle(req, res) {
346
+ res.sse(async (sse) => {
347
+ // Stream directly from an async generator or AI SDK
348
+ async function* generateTokens() {
349
+ yield 'Hello'
350
+ yield ' from'
351
+ yield ' ExisJS'
352
+ yield ' AI streaming!'
353
+ }
354
+
355
+ await sse.pipeFrom(generateTokens(), (token) => ({
356
+ event: 'delta',
357
+ data: { text: token }
358
+ }))
359
+
360
+ sse.close()
361
+ })
362
+ }
363
+ })
364
+ })
365
+ ```
366
+
367
+ ### SSE Options & Methods
368
+
369
+ - `res.sse(options?)`: Options include:
370
+ - `heartbeat`: Heartbeat interval in milliseconds (default: `15000`). Pass `false` to disable.
371
+ - `heartbeatComment`: Custom comment string for keep-alive pings (default: `": ping\n\n"`).
372
+ - `headers`: Additional custom HTTP headers.
373
+ - `sse.send(message)`: Sends an event. Accepts string, number, object (auto JSON-stringified), or `{ event, data, id, retry }`.
374
+ - `sse.comment(text)`: Sends a raw SSE comment line (e.g. `: ping\n\n`) without triggering client-side event listeners.
375
+ - `sse.onClose(callback)`: Registers a callback fired when the client disconnects.
376
+ - `sse.close()`: Gracefully terminates the stream and closes the HTTP response.
377
+
@@ -15,11 +15,11 @@ ExisJS natively supports both a **Functional Paradigm** and a **Class-Based OOP
15
15
 
16
16
  ExisJS uses **folder-based file-system routing** instead of programmatic route declaration in a single entry file.
17
17
 
18
- For a folder to become an accessible API endpoint, it must contain a `route.ts` file that exports a route definition.
18
+ For a folder to become an accessible API endpoint, it must contain a `route.ts` or `<feature>.route.ts` (e.g. `user.route.ts`) file that exports a route definition.
19
19
 
20
20
  In addition to `route.ts`, the ExisJS router utilizes two other special gatekeeper files:
21
21
  - **`server.ts`**: Placed at the root of your `http` directory, it is the entry point that boots your application, registers global plugins, and connects to databases.
22
- - **`gateway.ts`**: Can be placed in any folder. It acts as a gatekeeper, automatically applying middleware, CORS, headers, and dependencies to *all* routes within its directory and subdirectories.
22
+ - **`boundary.ts`** (or `*.boundary.ts`): Can be placed in any folder. It acts as a gatekeeper and request pipeline, automatically configuring middleware, CORS, headers, and dependencies for *all* routes within its directory and subdirectories.
23
23
 
24
24
  ### Routing Conventions
25
25
 
@@ -38,9 +38,9 @@ The `server.ts` file acts as the root configuration for your application. It pro
38
38
  import { exis } from 'exisjs'
39
39
 
40
40
  export default exis({
41
- async onStart(app) {
42
- // Boot your database here!
43
- }
41
+ providers: [
42
+ ['LoggerService', { useValue: console }]
43
+ ]
44
44
  })
45
45
  ```
46
46
 
@@ -59,27 +59,27 @@ export default class RootServer {
59
59
  }
60
60
  ```
61
61
 
62
- ### Folder Gateways (`gateway.ts`)
62
+ ### Folder Boundaries (`boundary.ts`)
63
63
 
64
- Gateways apply configurations to all routes in their directory and subdirectories.
64
+ Boundaries apply configurations, security policies, and request pipelines to all routes in their directory and subdirectories.
65
65
 
66
66
  #### Functional Paradigm
67
- ```typescript filename="src/http/admin/gateway.ts"
68
- import { defineGateway } from 'exisjs/router'
67
+ ```typescript filename="src/http/admin/boundary.ts"
68
+ import { defineBoundary } from 'exisjs/router'
69
69
 
70
- export default defineGateway({
70
+ export const config = defineBoundary({
71
71
  headers: { 'X-Admin-Area': 'true' }
72
72
  })
73
73
  ```
74
74
 
75
75
  #### Class-Based (OOP) Paradigm
76
- ```typescript filename="src/http/admin/gateway.ts"
77
- import { Gateway } from 'exisjs/decorators'
76
+ ```typescript filename="src/http/admin/boundary.ts"
77
+ import { Boundary } from 'exisjs/decorators'
78
78
 
79
- @Gateway({
79
+ @Boundary({
80
80
  headers: { 'X-Admin-Area': 'true' }
81
81
  })
82
- export default class AdminGateway {}
82
+ export default class AdminBoundary {}
83
83
  ```
84
84
 
85
85
  ### Route Definitions (`route.ts`)
@@ -264,13 +264,18 @@ export default controller({
264
264
  }),
265
265
  getSingle: route.get('/:id', {
266
266
  handle({ params }) {
267
+ // params.id is automatically inferred as string with full autocomplete!
267
268
  const { id } = params
268
- return { message: \`Fetching user \${id}\` }
269
+ return { message: `Fetching user ${id}` }
269
270
  }
270
271
  })
271
272
  })
272
273
  ```
273
274
 
275
+ <Info title="Automatic Route Parameter Type Inference">
276
+ In ExisJS v0.7+, paths with named parameters like `route.get('/users/:userId/posts/:postId', ...)` automatically parse the path string literal to infer `params: { userId: string; postId: string }` on the `handle` context. No manual casting or boilerplate interfaces required.
277
+ </Info>
278
+
274
279
 
275
280
  #### Class-Based (OOP) Paradigm
276
281
 
@@ -16,18 +16,23 @@ The following security middlewares are available from `exisjs/middleware` and ca
16
16
  - `helmet(options?)`: Sets 14+ secure HTTP headers (Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and CSP).
17
17
  - `csrf(options)`: Enforces the Signed Double Submit Cookie pattern to protect state-changing requests (POST, PUT, DELETE) from CSRF attacks.
18
18
  - `hpp()`: Protects against HTTP Parameter Pollution attacks by ensuring array query parameters/form fields are safely handled.
19
+ - `mongoSanitize()`: Strips MongoDB operator keys (keys starting with `$` or containing `.`) from `req.body`, `req.query`, and `req.params`.
19
20
  - `dbSanitize(options?)`: Protects against NoSQL query operator injection (`$where` and other malicious keys) before reaching database layers.
20
21
  - `timeout(ms)`: Automatically terminates requests that exceed the specified timeout limit, returning a 503 response.
21
22
 
23
+ > [!TIP]
24
+ > Security middlewares like `hpp()`, `mongoSanitize()`, and `dbSanitize()` automatically synchronize with the request body parser. If registered globally before route handlers, they safely await the body and sanitize it deterministically without runtime crashes.
25
+
22
26
  ```typescript filename="src/http/server.ts"
23
27
  import { exis } from 'exisjs'
24
- import { helmet, csrf, timeout, hpp } from 'exisjs/middleware'
28
+ import { helmet, csrf, timeout, hpp, mongoSanitize } from 'exisjs/middleware'
25
29
 
26
30
  export default exis({
27
31
  async onStart(app) {
28
32
  app.use(helmet())
29
33
  app.use(timeout(10000))
30
34
  app.use(hpp())
35
+ app.use(mongoSanitize())
31
36
  }
32
37
  })
33
38
  ```
@@ -60,12 +60,18 @@ my-backend/
60
60
  A file's name declares its role. Functional vs. class-based (OOP) only changes
61
61
  _syntax_, never _responsibility_.
62
62
 
63
- | File | Role | Never contains |
64
- | ------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------- |
65
- | `route.ts` | Binds a URL + method to a handler. | Business logic, validation logic |
66
- | `schema.ts` | `tex` validation schemas, imported by `route.ts`. Mandatory for every `route.ts` that accepts input. | Handlers, routing |
67
- | `service.ts` | Business logic and data access. DI-injectable. | Routing, response shaping |
68
- | `boundary.ts` | Folder-scoped config + request pipeline for everything in this folder and below. | — |
63
+ ExisJS supports two naming conventions seamlessly:
64
+ 1. **Standard Flat Names** (default): `route.ts`, `schema.ts`, `service.ts`, `boundary.ts`.
65
+ 2. **Feature-Prefixed Named Files**: `<name>.route.ts`, `<name>.schema.ts`, `<name>.service.ts`, `<name>.boundary.ts` (e.g., `user.route.ts`, `user.schema.ts`, `user.service.ts`).
66
+
67
+ Both styles are automatically detected by the route scanner, CLI manifest generator, and development watcher with 100% parity.
68
+
69
+ | File Pattern | Role | Never contains |
70
+ | :--- | :--- | :--- |
71
+ | `route.ts` / `*.route.ts` | Binds a URL + method to a handler. | Business logic, validation logic |
72
+ | `schema.ts` / `*.schema.ts` | `tex` validation schemas, imported by route files. Mandatory for every route accepting input. | Handlers, routing |
73
+ | `service.ts` / `*.service.ts` | Business logic and data access. DI-injectable. | Routing, response shaping |
74
+ | `boundary.ts` / `*.boundary.ts` | Folder-scoped config + request pipeline for everything in this folder and below. | — |
69
75
 
70
76
  ---
71
77
 
@@ -1 +1 @@
1
- {"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../../src/middleware/compression.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,UAAU,CAAA;AAE1D;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAiFrC"}
1
+ {"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../../src/middleware/compression.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,UAAU,CAAA;AAE1D;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAgGrC"}
@@ -59,21 +59,36 @@ function compression() {
59
59
  res.raw.setHeader('Vary', 'Accept-Encoding');
60
60
  // Remove Content-Length since compressed size will differ
61
61
  res.raw.removeHeader('Content-Length');
62
- // Compress synchronously for small-to-medium payloads to avoid extra async overhead
62
+ // Compress using hardware-accelerated Rust engine (@exisjs/rs) if available, with graceful zlib fallback
63
63
  let compressed;
64
64
  try {
65
65
  if (selectedEncoding === 'br') {
66
- compressed = node_zlib_1.default.brotliCompressSync(buf);
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
67
+ const rs = require('@exisjs/rs');
68
+ compressed =
69
+ typeof rs.brotliCompress === 'function'
70
+ ? rs.brotliCompress(buf)
71
+ : node_zlib_1.default.brotliCompressSync(buf);
67
72
  }
68
73
  else if (selectedEncoding === 'gzip') {
69
- compressed = node_zlib_1.default.gzipSync(buf);
74
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
75
+ const rs = require('@exisjs/rs');
76
+ compressed =
77
+ typeof rs.gzipCompress === 'function'
78
+ ? rs.gzipCompress(buf)
79
+ : node_zlib_1.default.gzipSync(buf);
70
80
  }
71
81
  else {
72
- compressed = node_zlib_1.default.deflateSync(buf);
82
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
83
+ const rs = require('@exisjs/rs');
84
+ compressed =
85
+ typeof rs.deflateCompress === 'function'
86
+ ? rs.deflateCompress(buf)
87
+ : node_zlib_1.default.deflateSync(buf);
73
88
  }
74
89
  }
75
90
  catch {
76
- // If compression fails, send uncompressed
91
+ // If native or zlib compression fails, send uncompressed
77
92
  originalEnd(data);
78
93
  return;
79
94
  }
@@ -1 +1 @@
1
- {"version":3,"file":"compression.js","sourceRoot":"","sources":["../../src/middleware/compression.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"compression.js","sourceRoot":"","sources":["../../src/middleware/compression.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,9 @@ export * from './guard';
18
18
  export * from './pipe';
19
19
  export declare function serveStatic(root: string, options?: {
20
20
  maxAge?: number;
21
+ etag?: boolean;
22
+ brotli?: boolean;
23
+ gzip?: boolean;
21
24
  }): Handler;
22
25
  export interface ValidateSchema {
23
26
  body?: TexEngine<any>;
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACzE,OAAO,EACL,SAAS,EAEV,MAAM,kBAAkB,CAAA;AAKzB,wBAAgB,IAAI,CAAC,MAAM,GAAE,UAAe,GAAG,OAAO,CAwFrD;AAMD,wBAAgB,SAAS,IAAI,OAAO,CAenC;AAID,wBAAgB,aAAa,CAC3B,cAAc,GAAE,MAAM,GAAG,YAAiB,GACzC,OAAO,CAqDT;AAID,eAAO,MAAM,QAAQ,EAAE,OAQtB,CAAA;AAID,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EACL,MAAM,EACN,IAAI,EACJ,OAAO,EACP,GAAG,EACH,aAAa,EACb,UAAU,GACX,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAE,gBAAgB,IAAI,MAAM,EAAE,MAAM,UAAU,CAAA;AACrD,OAAO,EAAE,sBAAsB,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACvE,OAAO,EAAE,kBAAkB,IAAI,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC/C,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAChC,OAAO,CA0ET;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;IACtB,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;CACxB;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CA8BxD;AAID,wBAAgB,cAAc,IAAI,OAAO,CAKxC;AAED,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACzE,OAAO,EACL,SAAS,EAEV,MAAM,kBAAkB,CAAA;AAKzB,wBAAgB,IAAI,CAAC,MAAM,GAAE,UAAe,GAAG,OAAO,CAoIrD;AAMD,wBAAgB,SAAS,IAAI,OAAO,CAenC;AAID,wBAAgB,aAAa,CAC3B,cAAc,GAAE,MAAM,GAAG,YAAiB,GACzC,OAAO,CAqDT;AAID,eAAO,MAAM,QAAQ,EAAE,OAQtB,CAAA;AAID,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EACL,MAAM,EACN,IAAI,EACJ,OAAO,EACP,GAAG,EACH,aAAa,EACb,UAAU,GACX,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAE,gBAAgB,IAAI,MAAM,EAAE,MAAM,UAAU,CAAA;AACrD,OAAO,EAAE,sBAAsB,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACvE,OAAO,EAAE,kBAAkB,IAAI,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC/C,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,OAAO,CAAA;CACV,GACL,OAAO,CAgHT;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;IACtB,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;CACxB;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CA8BxD;AAID,wBAAgB,cAAc,IAAI,OAAO,CAKxC;AAED,cAAc,eAAe,CAAA"}
@@ -35,63 +35,100 @@ function cors(config = {}) {
35
35
  return (req, res, next) => {
36
36
  const reqOrigin = req.get('origin');
37
37
  // 1. Resolve Origin
38
- let allowOrigin = '';
39
- if (origin === '*') {
40
- allowOrigin = '*';
41
- }
42
- else if (typeof origin === 'string') {
43
- allowOrigin = origin;
44
- }
45
- else if (reqOrigin) {
46
- if (Array.isArray(origin)) {
47
- allowOrigin = origin.some((o) => o instanceof RegExp ? o.test(reqOrigin) : o === reqOrigin)
48
- ? reqOrigin
49
- : '';
38
+ const applyOriginAndProceed = (allowOrigin) => {
39
+ if (allowOrigin) {
40
+ res.set('Access-Control-Allow-Origin', allowOrigin);
50
41
  }
51
- else if (origin instanceof RegExp) {
52
- allowOrigin = origin.test(reqOrigin) ? reqOrigin : '';
42
+ // 2. Credentials
43
+ if (credentials) {
44
+ res.set('Access-Control-Allow-Credentials', 'true');
53
45
  }
54
- else if (typeof origin === 'function') {
55
- allowOrigin = origin(reqOrigin) ? reqOrigin : '';
46
+ // 3. Exposed Headers
47
+ if (exposedHeaders && exposedHeaders.length > 0) {
48
+ res.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
49
+ }
50
+ // 4. Preflight (OPTIONS)
51
+ if (req.method === 'OPTIONS') {
52
+ res.set('Access-Control-Allow-Methods', methods.join(', '));
53
+ const reqHeaders = req.get('access-control-request-headers');
54
+ if (reqHeaders) {
55
+ res.set('Access-Control-Allow-Headers', reqHeaders);
56
+ }
57
+ else if (allowedHeaders && allowedHeaders.length > 0) {
58
+ res.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
59
+ }
60
+ if (maxAge) {
61
+ res.set('Access-Control-Max-Age', String(maxAge));
62
+ }
63
+ if (!allowOrigin && reqOrigin) {
64
+ if (req.log && typeof req.log.warn === 'function') {
65
+ req.log.warn({ origin: reqOrigin }, `[CORS] Rejected preflight request from origin '${reqOrigin}' (not in allowed origins)`);
66
+ }
67
+ }
68
+ if (config.preflightContinue) {
69
+ next();
70
+ return;
71
+ }
72
+ res.status(204).send('');
73
+ return;
56
74
  }
75
+ next();
76
+ };
77
+ if (origin === '*' || !origin) {
78
+ applyOriginAndProceed(origin === '*' ? '*' : '');
79
+ return;
57
80
  }
58
- if (allowOrigin) {
59
- res.set('Access-Control-Allow-Origin', allowOrigin);
81
+ if (typeof origin === 'string') {
82
+ applyOriginAndProceed(origin);
83
+ return;
60
84
  }
61
- // 2. Credentials
62
- if (credentials) {
63
- res.set('Access-Control-Allow-Credentials', 'true');
85
+ if (!reqOrigin) {
86
+ applyOriginAndProceed('');
87
+ return;
64
88
  }
65
- // 3. Exposed Headers
66
- if (exposedHeaders && exposedHeaders.length > 0) {
67
- res.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
89
+ if (Array.isArray(origin)) {
90
+ const matched = origin.some((o) => o instanceof RegExp ? o.test(reqOrigin) : o === reqOrigin);
91
+ applyOriginAndProceed(matched ? reqOrigin : '');
92
+ return;
68
93
  }
69
- // 4. Preflight (OPTIONS)
70
- if (req.method === 'OPTIONS') {
71
- res.set('Access-Control-Allow-Methods', methods.join(', '));
72
- const reqHeaders = req.get('access-control-request-headers');
73
- if (reqHeaders) {
74
- res.set('Access-Control-Allow-Headers', reqHeaders);
75
- }
76
- else if (allowedHeaders && allowedHeaders.length > 0) {
77
- res.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
78
- }
79
- if (maxAge) {
80
- res.set('Access-Control-Max-Age', String(maxAge));
94
+ if (origin instanceof RegExp) {
95
+ applyOriginAndProceed(origin.test(reqOrigin) ? reqOrigin : '');
96
+ return;
97
+ }
98
+ if (typeof origin === 'function') {
99
+ // Check if function accepts a callback (origin, callback)
100
+ if (origin.length >= 2) {
101
+ try {
102
+ origin(reqOrigin, (err, allow) => {
103
+ if (err)
104
+ return next(err);
105
+ applyOriginAndProceed(allow ? reqOrigin : '');
106
+ });
107
+ }
108
+ catch (err) {
109
+ next(err);
110
+ }
111
+ return;
81
112
  }
82
- if (!allowOrigin && reqOrigin) {
83
- if (req.log && typeof req.log.warn === 'function') {
84
- req.log.warn({ origin: reqOrigin }, `[CORS] Rejected preflight request from origin '${reqOrigin}' (not in allowed origins)`);
113
+ try {
114
+ const result = origin(reqOrigin);
115
+ if (result && typeof result.then === 'function') {
116
+ ;
117
+ result
118
+ .then((allow) => applyOriginAndProceed(allow ? reqOrigin : ''))
119
+ .catch(next);
120
+ return;
85
121
  }
122
+ applyOriginAndProceed(result ? reqOrigin : '');
123
+ return;
86
124
  }
87
- if (config.preflightContinue) {
88
- next();
125
+ catch (err) {
126
+ next(err);
89
127
  return;
90
128
  }
91
- res.status(204).send('');
92
- return;
93
129
  }
94
- next();
130
+ applyOriginAndProceed('');
131
+ return;
95
132
  };
96
133
  }
97
134
  // ─── Request ID ───────────────────────────────────────────────────────────────
@@ -233,7 +270,23 @@ function serveStatic(root, options = {}) {
233
270
  if (!filePath.startsWith(rootPath)) {
234
271
  return next();
235
272
  }
236
- fs.stat(filePath, (err, stat) => {
273
+ const acceptEncoding = req.headers['accept-encoding'] || '';
274
+ // Check for pre-compressed Brotli or Gzip static file if supported
275
+ let targetPath = filePath;
276
+ let contentEncoding = null;
277
+ if (options.brotli !== false &&
278
+ acceptEncoding.includes('br') &&
279
+ fs.existsSync(filePath + '.br')) {
280
+ targetPath = filePath + '.br';
281
+ contentEncoding = 'br';
282
+ }
283
+ else if (options.gzip !== false &&
284
+ acceptEncoding.includes('gzip') &&
285
+ fs.existsSync(filePath + '.gz')) {
286
+ targetPath = filePath + '.gz';
287
+ contentEncoding = 'gzip';
288
+ }
289
+ fs.stat(targetPath, (err, stat) => {
237
290
  if (err || !stat.isFile()) {
238
291
  return next();
239
292
  }
@@ -241,14 +294,27 @@ function serveStatic(root, options = {}) {
241
294
  const mime = mimeTypes[ext] || 'application/octet-stream';
242
295
  res.set('Content-Type', mime);
243
296
  res.set('Content-Length', String(stat.size));
297
+ if (contentEncoding) {
298
+ res.set('Content-Encoding', contentEncoding);
299
+ res.set('Vary', 'Accept-Encoding');
300
+ }
244
301
  if (options.maxAge !== undefined) {
245
302
  res.set('Cache-Control', `public, max-age=${options.maxAge}`);
246
303
  }
304
+ if (options.etag !== false) {
305
+ // Strong ETag based on mtime and size
306
+ const etag = `"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
307
+ res.set('ETag', etag);
308
+ if (req.headers['if-none-match'] === etag) {
309
+ res.status(304).send('');
310
+ return;
311
+ }
312
+ }
247
313
  if (req.method === 'HEAD') {
248
314
  res.status(200).send('');
249
315
  return;
250
316
  }
251
- const sendStream = fs.createReadStream(filePath);
317
+ const sendStream = fs.createReadStream(targetPath);
252
318
  const abortHandler = () => sendStream.destroy();
253
319
  req.raw.on('aborted', abortHandler);
254
320
  req.raw.on('close', abortHandler);