ingenium 0.0.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +943 -0
  3. package/dist/index.cjs +7078 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +4262 -0
  6. package/dist/index.d.ts +4262 -0
  7. package/dist/index.js +6963 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +47 -0
  10. package/src/api-key/middleware.ts +157 -0
  11. package/src/api-key/types.ts +37 -0
  12. package/src/app/scope.ts +392 -0
  13. package/src/app.ts +1752 -0
  14. package/src/body/limit.ts +21 -0
  15. package/src/body/middleware.ts +30 -0
  16. package/src/body/multipart-types.ts +40 -0
  17. package/src/body/multipart.ts +254 -0
  18. package/src/context/body.ts +324 -0
  19. package/src/context/context.ts +650 -0
  20. package/src/context/cookies.ts +282 -0
  21. package/src/context/pool.ts +32 -0
  22. package/src/cors/middleware.ts +182 -0
  23. package/src/cors/types.ts +79 -0
  24. package/src/cron/parser.ts +311 -0
  25. package/src/cron/registry.ts +49 -0
  26. package/src/cron/scheduler.ts +153 -0
  27. package/src/csrf/middleware.ts +224 -0
  28. package/src/csrf/types.ts +65 -0
  29. package/src/errors.ts +148 -0
  30. package/src/idempotency/middleware.ts +197 -0
  31. package/src/idempotency/store.ts +70 -0
  32. package/src/idempotency/types.ts +87 -0
  33. package/src/index.ts +328 -0
  34. package/src/jobs/queue.ts +306 -0
  35. package/src/jobs/registry.ts +82 -0
  36. package/src/jobs/store-memory.ts +113 -0
  37. package/src/jobs/types.ts +135 -0
  38. package/src/jwt/jwks.ts +143 -0
  39. package/src/jwt/middleware.ts +313 -0
  40. package/src/jwt/types.ts +137 -0
  41. package/src/jwt/verify.ts +370 -0
  42. package/src/middleware/compose.ts +94 -0
  43. package/src/middleware/types.ts +37 -0
  44. package/src/negotiation/accept.ts +159 -0
  45. package/src/negotiation/etag.ts +30 -0
  46. package/src/negotiation/format.ts +88 -0
  47. package/src/negotiation/fresh.ts +89 -0
  48. package/src/negotiation/json-etag.ts +122 -0
  49. package/src/negotiation/negotiate.ts +97 -0
  50. package/src/openapi/describe.ts +79 -0
  51. package/src/openapi/extract-params.ts +62 -0
  52. package/src/openapi/generate.ts +251 -0
  53. package/src/openapi/handler.ts +73 -0
  54. package/src/openapi/types.ts +145 -0
  55. package/src/plugin/decorators.ts +100 -0
  56. package/src/plugin/hooks.ts +114 -0
  57. package/src/plugin/types.ts +189 -0
  58. package/src/problem/middleware.ts +55 -0
  59. package/src/problem/serialize.ts +121 -0
  60. package/src/problem/types.ts +68 -0
  61. package/src/proxy/trust.ts +247 -0
  62. package/src/rate-limit/middleware.ts +72 -0
  63. package/src/rate-limit/store.ts +129 -0
  64. package/src/rate-limit/types.ts +60 -0
  65. package/src/response/reflect.ts +93 -0
  66. package/src/router/router.ts +284 -0
  67. package/src/router/trie.ts +309 -0
  68. package/src/router/types.ts +54 -0
  69. package/src/schema/standard.ts +67 -0
  70. package/src/session/middleware.ts +379 -0
  71. package/src/session/store-memory.ts +79 -0
  72. package/src/session/types.ts +95 -0
  73. package/src/sinatra/filters.ts +129 -0
  74. package/src/sinatra/top-level.ts +151 -0
  75. package/src/sse/keep-alive.ts +52 -0
  76. package/src/sse/sse.ts +115 -0
  77. package/src/static/middleware.ts +254 -0
  78. package/src/static/types.ts +31 -0
  79. package/src/transport/http2-helpers.ts +242 -0
  80. package/src/transport/http2.ts +316 -0
  81. package/src/transport/node.ts +261 -0
  82. package/src/transport/shutdown.ts +86 -0
  83. package/src/transport/types.ts +72 -0
  84. package/src/util/safe-json.ts +66 -0
  85. package/src/ws/index.ts +164 -0
  86. package/src/ws/middleware.ts +178 -0
  87. package/src/ws/types.ts +52 -0
  88. package/src/ws/ws-node-adapter.ts +162 -0
@@ -0,0 +1,4262 @@
1
+ import { Buffer as Buffer$1 } from 'node:buffer';
2
+ import * as node_http from 'node:http';
3
+ import { IncomingHttpHeaders, Server as Server$1 } from 'node:http';
4
+ import { Readable } from 'node:stream';
5
+ import { KeyObject } from 'node:crypto';
6
+ import { WebSocket as WebSocket$1 } from 'ws';
7
+
8
+ /**
9
+ * Trust-proxy resolution for `X-Forwarded-*` headers.
10
+ *
11
+ * Mirrors Express's `app.set('trust proxy', ...)` semantics:
12
+ * - `false` (default): never trust XFF — `ctx.ip` always reflects the immediate
13
+ * socket peer.
14
+ * - `true`: trust the entire `X-Forwarded-For` chain — last entry wins.
15
+ * - `number n`: trust `n` upstream hops — return chain entry `n` from the right.
16
+ * - `string` (single CIDR/IP/keyword) or `string[]` (list): trust connections
17
+ * from these addresses; walk the chain skipping trusted IPs.
18
+ * - `(ip, hopIdx) => boolean`: custom predicate, called per chain entry.
19
+ *
20
+ * Supported keywords: `'loopback'` (127.0.0.0/8, ::1), `'linklocal'`
21
+ * (169.254.0.0/16, fe80::/10), `'uniquelocal'` (10/8, 172.16/12, 192.168/16,
22
+ * fc00::/7). CIDRs accepted in IPv4 dotted (`10.0.0.0/8`) and IPv6
23
+ * (`fc00::/7`) form. Single addresses without `/` match exactly.
24
+ */
25
+ type TrustProxy = boolean | number | string | string[] | ((ip: string, hopIdx: number) => boolean);
26
+ interface ForwardedInfo {
27
+ /** The resolved client IP after walking the trusted hop chain. */
28
+ ip: string;
29
+ /** Full forwarded chain, left-to-right (closest to client first), plus the immediate peer at the end. */
30
+ ips: readonly string[];
31
+ /** Best-effort protocol: `http` or `https`. */
32
+ protocol: 'http' | 'https';
33
+ /** Best-effort hostname (no port). */
34
+ hostname: string;
35
+ }
36
+ /**
37
+ * Resolve forwarded info from raw headers + the immediate socket peer.
38
+ *
39
+ * @param trust The `trustProxy` configuration.
40
+ * @param remoteAddress The socket-level peer address (always present).
41
+ * @param headers Lowercased request headers (Node convention).
42
+ * @param defaultProtocol The protocol of the underlying transport (`http` for `node:http`,
43
+ * `https` for TLS, `http` for h2c, `https` for h2/TLS).
44
+ */
45
+ declare function resolveForwarded(trust: TrustProxy, remoteAddress: string, headers: Readonly<Record<string, string | string[] | undefined>>, defaultProtocol?: 'http' | 'https'): ForwardedInfo;
46
+
47
+ /**
48
+ * Background-job type surface for Ingenium.
49
+ *
50
+ * The core abstraction is a {@link QueueStore} — a pluggable persistence layer
51
+ * for FIFO jobs with retries and a dead-letter list. The default implementation
52
+ * ({@link MemoryQueueStore}) keeps everything in process; a Redis adapter can
53
+ * land later by simply implementing this interface.
54
+ *
55
+ * A {@link IngeniumQueue} wraps a store with a worker pool, retry policy, and
56
+ * pause/resume/drain controls; a {@link QueueRegistry} (held by `IngeniumApp`)
57
+ * indexes named queues so route handlers can enqueue from any code path.
58
+ */
59
+ /**
60
+ * Worker function for a registered queue. Throwing causes a retry per the
61
+ * configured {@link RetryPolicy}; resolving means the job is `ack`'d.
62
+ */
63
+ type QueueWorker<TData> = (job: {
64
+ /** Stable id assigned by the store at enqueue time. */
65
+ id: string;
66
+ /** Job payload (the value passed to `add`). */
67
+ data: TData;
68
+ /**
69
+ * 1-indexed attempt counter. `1` on first delivery; incremented before each
70
+ * retry. Use this in the worker to back off external calls or short-circuit
71
+ * non-recoverable failures.
72
+ */
73
+ attempt: number;
74
+ }) => unknown | Promise<unknown>;
75
+ /**
76
+ * Retry policy. The first attempt is included in the count: `attempts: 3`
77
+ * means one initial try + two retries.
78
+ */
79
+ interface RetryPolicy {
80
+ /** Total tries including the first delivery. Must be `>= 1`. */
81
+ attempts: number;
82
+ /**
83
+ * Delay (ms) before the NEXT attempt, given the attempt that just failed
84
+ * (1-indexed). E.g. for `attempts: 3`, this is called with `1` then `2`.
85
+ */
86
+ backoffMs: (attempt: number) => number;
87
+ }
88
+ /**
89
+ * Options for {@link IngeniumApp.queue}. All fields optional.
90
+ *
91
+ * @typeParam TData - shape of job payloads enqueued onto this queue
92
+ */
93
+ interface QueueOptions<TData> {
94
+ /**
95
+ * Max concurrent jobs processed in parallel by this queue's worker pool.
96
+ * Default `1` (strict FIFO). Bump this for I/O-bound work.
97
+ */
98
+ concurrency?: number;
99
+ /**
100
+ * Retry policy on worker throw. Numeric shorthand `n` is equivalent to
101
+ * `{ attempts: n, backoffMs: exponential }`. Default: 3 attempts at
102
+ * 100ms / 400ms / 1.6s.
103
+ */
104
+ retries?: number | RetryPolicy;
105
+ /**
106
+ * Custom store. Default {@link MemoryQueueStore}. Implement this to back
107
+ * the queue with Redis / Postgres / SQS / etc.
108
+ */
109
+ store?: QueueStore<TData>;
110
+ /**
111
+ * Called once retries are exhausted, just before the job is moved to the
112
+ * dead-letter list. Throwing here is logged and swallowed — the job is
113
+ * still moved to the DLQ.
114
+ */
115
+ onFailed?: (job: FailedJob<TData>) => void | Promise<void>;
116
+ }
117
+ /**
118
+ * Pluggable persistence layer. The default {@link MemoryQueueStore} keeps
119
+ * pending and failed jobs in arrays + an in-flight map. A Redis adapter
120
+ * would map this onto LPUSH / BRPOPLPUSH / a processing list / a DLQ list.
121
+ *
122
+ * Implementations MUST guarantee at-least-once delivery (a `next()`-ed
123
+ * job that is neither `ack`'d nor `retry`'d nor `fail`'d is considered
124
+ * stuck and may be re-delivered by the store on its own schedule).
125
+ */
126
+ interface QueueStore<TData> {
127
+ /** Append a job to the tail. Returns the assigned id. */
128
+ enqueue(data: TData): Promise<{
129
+ id: string;
130
+ }>;
131
+ /**
132
+ * Pop the next pending job and move it to the in-flight set. Returns
133
+ * `null` when the queue is empty. The returned `attempt` reflects how
134
+ * many times this job has been delivered (1 on first delivery).
135
+ */
136
+ next(): Promise<{
137
+ id: string;
138
+ data: TData;
139
+ attempt: number;
140
+ } | null>;
141
+ /** Mark an in-flight job as completed. Removes it from the store. */
142
+ ack(id: string): Promise<void>;
143
+ /**
144
+ * Re-enqueue the in-flight job for another attempt after `delayMs`.
145
+ * The store MUST increment its internal attempt counter so the next
146
+ * `next()` returns it with the bumped count.
147
+ */
148
+ retry(id: string, delayMs: number): Promise<void>;
149
+ /** Move the in-flight job to the dead-letter list. */
150
+ fail(id: string): Promise<void>;
151
+ /** Number of pending (not in-flight, not failed) jobs. */
152
+ size(): Promise<number>;
153
+ /** Number of jobs in the dead-letter list. */
154
+ failedCount(): Promise<number>;
155
+ }
156
+ /** Payload passed to {@link QueueOptions.onFailed} when retries are exhausted. */
157
+ interface FailedJob<TData> {
158
+ id: string;
159
+ data: TData;
160
+ /** Final attempt number (== `retries.attempts`). */
161
+ attempt: number;
162
+ /** Whatever the worker threw on its last attempt. */
163
+ lastError: unknown;
164
+ }
165
+ /** Per-request handle returned by `ctx.queue(name)`. */
166
+ interface JobHandle<TData = unknown> {
167
+ /** Enqueue `data`. Resolves to the assigned job id. */
168
+ add(data: TData): Promise<{
169
+ id: string;
170
+ }>;
171
+ }
172
+ /**
173
+ * Bookkeeping wrapper a {@link QueueRegistry} keeps for every registered
174
+ * queue. Mostly useful for introspection / tests.
175
+ */
176
+ interface RegisteredQueue<TData = unknown> {
177
+ name: string;
178
+ options: Required<Pick<QueueOptions<TData>, 'concurrency'>> & {
179
+ retries: RetryPolicy;
180
+ onFailed: ((job: FailedJob<TData>) => void | Promise<void>) | undefined;
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Options for `IngeniumBody.multipart()`.
186
+ *
187
+ * All limits are validated mid-parse — exceeding any of them throws before
188
+ * the full body is fully decoded so memory usage stays bounded.
189
+ */
190
+ interface MultipartOptions {
191
+ /** Total request body cap. Default 100,000 bytes (matches Express's body-parser default). */
192
+ maxBytes?: number;
193
+ /** Per-file size cap. Default 10 * 1024 * 1024 (10 MiB). */
194
+ maxFileSize?: number;
195
+ /** Maximum number of file parts in one request. Default 20. */
196
+ maxFiles?: number;
197
+ /** Maximum number of plain (non-file) field parts. Default 100. */
198
+ maxFields?: number;
199
+ /** Allowed MIME prefixes (e.g. ['image/']). File rejected if no prefix matches. Default: any. */
200
+ allowedMimePrefixes?: string[];
201
+ }
202
+ /** A single uploaded file part, fully buffered. */
203
+ interface MultipartFile {
204
+ /** Original filename as supplied by the client. */
205
+ filename: string;
206
+ /** MIME type from the part's `Content-Type` header (defaults to `application/octet-stream`). */
207
+ mimeType: string;
208
+ /** Byte length of `data`. */
209
+ size: number;
210
+ /** Raw file bytes — fully buffered. For very large uploads, prefer `ctx.body.stream()`. */
211
+ data: Buffer$1;
212
+ }
213
+ /** Result of parsing a `multipart/form-data` body. */
214
+ interface MultipartResult {
215
+ /** Plain-text form fields keyed by name. Repeated names collapse into an array. */
216
+ fields: Record<string, string | string[]>;
217
+ /** File parts keyed by name. Repeated names collapse into an array. */
218
+ files: Record<string, MultipartFile | MultipartFile[]>;
219
+ }
220
+
221
+ /**
222
+ * Local, zero-dependency type definitions for the
223
+ * [Standard Schema](https://standardschema.dev) v1 spec.
224
+ *
225
+ * Ingenium detects schemas implementing this contract on
226
+ * `IngeniumBody.json(schema)` and runs their `validate` function, mapping
227
+ * `issues` into a `IngeniumValidationError` with field-level messages.
228
+ *
229
+ * We intentionally do NOT import `@standard-schema/spec` to keep the
230
+ * core dependency-free. These types mirror the spec exactly.
231
+ */
232
+ /** A successful validation result: parsed/transformed value. */
233
+ interface StandardSuccessResult<TOut> {
234
+ readonly value: TOut;
235
+ readonly issues?: undefined;
236
+ }
237
+ /** A single issue describing why validation failed at a particular path. */
238
+ interface StandardIssue {
239
+ readonly message: string;
240
+ readonly path?: ReadonlyArray<PropertyKey | StandardPathSegment> | undefined;
241
+ }
242
+ /** A path segment may be a bare key OR an object with a `key` property. */
243
+ interface StandardPathSegment {
244
+ readonly key: PropertyKey;
245
+ }
246
+ /** A failed validation result: one or more issues. */
247
+ interface StandardFailureResult {
248
+ readonly issues: ReadonlyArray<StandardIssue>;
249
+ readonly value?: undefined;
250
+ }
251
+ /** Standard Schema validation result: success XOR failure. */
252
+ type StandardResult<TOut> = StandardSuccessResult<TOut> | StandardFailureResult;
253
+ /** The properties living under the `~standard` key. */
254
+ interface StandardSchemaV1Props<TIn = unknown, TOut = TIn> {
255
+ readonly version: 1;
256
+ readonly vendor: string;
257
+ readonly validate: (input: unknown) => StandardResult<TOut> | Promise<StandardResult<TOut>>;
258
+ readonly types?: {
259
+ readonly input: TIn;
260
+ readonly output: TOut;
261
+ } | undefined;
262
+ }
263
+ /** The Standard Schema v1 interface — anything with a `~standard` property. */
264
+ interface StandardSchemaV1<TIn = unknown, TOut = TIn> {
265
+ readonly '~standard': StandardSchemaV1Props<TIn, TOut>;
266
+ }
267
+ /**
268
+ * Type guard: is `x` a Standard Schema v1?
269
+ *
270
+ * Checks for the `~standard` property and that its `version` is `1` and
271
+ * `validate` is a function. Cheap enough to call on every body.json() call.
272
+ */
273
+ declare function isStandardSchema(x: unknown): x is StandardSchemaV1;
274
+
275
+ /** Minimal duck-type for any validation library that accepts unknown and returns a typed value. */
276
+ interface ParseSchema<T> {
277
+ parse(input: unknown): T;
278
+ }
279
+ /** Optional Zod-like schema: success/failure object output (used internally for friendlier errors). */
280
+ interface SafeParseSchema<T> {
281
+ safeParse(input: unknown): {
282
+ success: true;
283
+ data: T;
284
+ } | {
285
+ success: false;
286
+ error: {
287
+ issues: ZodLikeIssue[];
288
+ };
289
+ };
290
+ }
291
+ interface ZodLikeIssue {
292
+ path: ReadonlyArray<string | number>;
293
+ message: string;
294
+ }
295
+ /**
296
+ * Lazy body accessor. Bytes are not read until one of the consume methods
297
+ * (`json`, `text`, `urlencoded`, `buffer`, `stream`) is called.
298
+ *
299
+ * One instance is allocated per `IngeniumContext` (pool-bound), so per-request
300
+ * cost is just a `reset()`.
301
+ */
302
+ declare class IngeniumBody {
303
+ /** @internal */ _source: Readable | null;
304
+ /** @internal */ _consumed: boolean;
305
+ /** @internal */ _contentType: string | undefined;
306
+ /** @internal */ _contentLength: number | undefined;
307
+ /**
308
+ * @internal
309
+ * Parse cache. Stores the raw body bytes after the first successful
310
+ * `buffer()` (or `text()` / `json()` / `urlencoded()`, which all go
311
+ * through `buffer()`). Subsequent buffer-producing consumers reuse
312
+ * these bytes instead of throwing "already consumed".
313
+ *
314
+ * Caches the RAW Buffer (not parsed objects) so different callers can
315
+ * apply different schemas / decoders against the same bytes — a
316
+ * common pattern when an audit middleware reads the body before the
317
+ * handler does. Re-parsing JSON from a cached buffer is cheap; mixing
318
+ * schemas against a cached parsed object would be incorrect.
319
+ *
320
+ * `stream()` opts out (it hands the caller ownership of the raw
321
+ * Readable) and `multipart()` opts out (its result is bespoke and
322
+ * re-parsing with different options would be ambiguous).
323
+ *
324
+ * Checked with `!== null` rather than truthiness so an empty body
325
+ * (`Buffer.alloc(0)`) still hits the cache on subsequent reads.
326
+ */
327
+ /** @internal */ _cached: Buffer$1 | null;
328
+ /** @internal Adapter calls this on each request before dispatch. */
329
+ _attach(source: Readable | null, contentType: string | undefined, contentLength: number | undefined): void;
330
+ /** @internal Pool reset. */
331
+ _reset(): void;
332
+ /**
333
+ * Returns the raw request body stream. Throws if already consumed OR
334
+ * if the body has already been buffered (cached) — once we hold the
335
+ * bytes, we can't hand the caller back a fresh Readable to own.
336
+ */
337
+ stream(): Readable;
338
+ /**
339
+ * Buffers the entire body into a `Buffer`. Honors `maxBytes` (default 100KB).
340
+ *
341
+ * If the body has already been buffered once (by any prior `buffer()`,
342
+ * `text()`, `json()`, or `urlencoded()` call), returns the cached bytes
343
+ * — `maxBytes` is still enforced against `cached.length`, so a caller
344
+ * passing a tighter cap than the original still gets a 413.
345
+ */
346
+ buffer(maxBytes?: number): Promise<Buffer$1>;
347
+ /** Buffers the body and decodes as UTF-8 text. */
348
+ text(maxBytes?: number): Promise<string>;
349
+ /**
350
+ * Parses the body as JSON. If a schema is provided, the parsed value is
351
+ * validated. Detection order:
352
+ *
353
+ * 1. Standard Schema v1 (`["~standard"]`) — async-aware, multi-issue
354
+ * 2. Zod-like `safeParse(input)` — multi-issue
355
+ * 3. Plain `parse(input): T` — throws on failure
356
+ *
357
+ * Validation failures are normalized into `IngeniumValidationError` with a
358
+ * field-level `fields` map (dot-joined paths; empty path → `_`).
359
+ */
360
+ json<T = unknown>(schema?: StandardSchemaV1<unknown, T> | SafeParseSchema<T> | ParseSchema<T>, maxBytes?: number): Promise<T>;
361
+ /** Parses the body as `application/x-www-form-urlencoded`. */
362
+ urlencoded(maxBytes?: number): Promise<Record<string, string>>;
363
+ /**
364
+ * Parses the body as `multipart/form-data` (RFC 7578).
365
+ *
366
+ * Returns plain-text fields and fully buffered file parts. For very large
367
+ * uploads prefer `stream()` and parse manually — this method holds every
368
+ * file in memory.
369
+ *
370
+ * Failure modes:
371
+ * - Body exceeds `maxBytes` → `IngeniumPayloadTooLargeError`
372
+ * - Single file exceeds `maxFileSize` → `IngeniumPayloadTooLargeError`
373
+ * - Too many files / fields → `IngeniumBadRequestError`
374
+ * - Disallowed mime type → `IngeniumBadRequestError`
375
+ * - Content-Type isn't `multipart/form-data` or boundary missing → `IngeniumBadRequestError`
376
+ * - Malformed body → `IngeniumBadRequestError`
377
+ */
378
+ multipart(opts?: MultipartOptions): Promise<MultipartResult>;
379
+ }
380
+
381
+ /**
382
+ * Options accepted by {@link IngeniumCookies.set}. Maps 1:1 to RFC 6265 cookie
383
+ * attributes plus a few modern extensions (`Priority`, `Partitioned`).
384
+ *
385
+ * `sameSite: true` is normalized to `'strict'` (Express compatibility);
386
+ * `sameSite: false` omits the attribute entirely so the browser falls back
387
+ * to its default policy.
388
+ */
389
+ interface CookieSetOptions {
390
+ /** `Domain=` attribute. Omitted when undefined. */
391
+ domain?: string;
392
+ /** `Path=` attribute. Defaults to `'/'`. */
393
+ path?: string;
394
+ /** `Expires=` attribute. Serialized via `Date.toUTCString()`. */
395
+ expires?: Date;
396
+ /** `Max-Age=` (seconds). Floored to an integer. */
397
+ maxAge?: number;
398
+ /** `HttpOnly` flag. */
399
+ httpOnly?: boolean;
400
+ /** `Secure` flag. */
401
+ secure?: boolean;
402
+ /** `SameSite=` attribute. `true` → `'strict'`; `false`/omitted → no attr. */
403
+ sameSite?: 'strict' | 'lax' | 'none' | true | false;
404
+ /** `Priority=` attribute (CHIPS / RFC 9220). Capitalized on the wire. */
405
+ priority?: 'low' | 'medium' | 'high';
406
+ /** `Partitioned` flag (CHIPS). */
407
+ partitioned?: boolean;
408
+ /**
409
+ * When `true`, the cookie value is HMAC-SHA-256 signed with the app's
410
+ * `cookieSecrets[0]`. On the wire: `name=value.signature`. Throws
411
+ * `IngeniumError(500, 'COOKIE_SECRET_MISSING')` if no secrets are configured.
412
+ */
413
+ signed?: boolean;
414
+ }
415
+ /** Options accepted by {@link IngeniumCookies.get}. */
416
+ interface CookieGetOptions {
417
+ /**
418
+ * When `true`, the cookie value is treated as `value.signature` and the
419
+ * HMAC is verified against every configured secret (rotation-safe).
420
+ * Returns `null` on tamper, missing signature, or no configured secrets.
421
+ */
422
+ signed?: boolean;
423
+ }
424
+ /**
425
+ * First-class cookie API exposed via `ctx.cookies`. Pool-bound and lazy —
426
+ * the holder is allocated on first access and dropped to `null` on context
427
+ * reset, so routes that never touch cookies pay zero overhead.
428
+ *
429
+ * Read side parses `ctx.headers.cookie` once and caches the resulting record.
430
+ * Write side appends to the response `set-cookie` header, preserving prior
431
+ * values (a single response may carry multiple `Set-Cookie` headers).
432
+ */
433
+ interface IngeniumCookies {
434
+ /**
435
+ * Read a cookie by name. With `{ signed: true }`, verifies the HMAC
436
+ * suffix and returns `null` on mismatch. Returns `null` when the cookie
437
+ * is absent.
438
+ */
439
+ get(name: string, opts?: CookieGetOptions): string | null;
440
+ /**
441
+ * Snapshot of all parsed cookies. Signed cookies appear with their raw
442
+ * `value.signature` suffix — call `.get(name, { signed: true })` to verify.
443
+ */
444
+ all(): Record<string, string>;
445
+ /**
446
+ * Write a `Set-Cookie` header. Multiple calls accumulate (the response
447
+ * carries one `Set-Cookie` header per call). With `{ signed: true }`,
448
+ * the value is HMAC-SHA-256 signed.
449
+ */
450
+ set(name: string, value: string, opts?: CookieSetOptions): void;
451
+ /**
452
+ * Expire a cookie. Emits `Max-Age=0` plus an `Expires` in the past, and
453
+ * mirrors `path` / `domain` so the browser actually removes the right
454
+ * cookie (a `Set-Cookie` only matches the existing cookie on those attrs).
455
+ */
456
+ clear(name: string, opts?: Pick<CookieSetOptions, 'domain' | 'path'>): void;
457
+ }
458
+
459
+ /** HTTP methods supported by the router. */
460
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
461
+ declare const HTTP_METHODS: readonly HttpMethod[];
462
+ /**
463
+ * Recursively extracts named params from a path string at the type level.
464
+ *
465
+ * - `:name` → required string
466
+ * - `:name?` → optional string (becomes `string | undefined`)
467
+ * - `:name(regex)` → required string. The regex is type-stripped here, but
468
+ * the constraint IS enforced at runtime by the trie
469
+ * (`RouterTrie.find` tests the segment against the
470
+ * compiled, fully-anchored pattern), so the `string`
471
+ * type is honest about the matched shape.
472
+ * Note: number-narrowing (typing `:id(\d+)` as `number`)
473
+ * remains deferred — constrained params stay `string`.
474
+ * - `*name` → required string (greedy wildcard tail)
475
+ *
476
+ * @example
477
+ * type P = ExtractParams<'/users/:id(\\d+)/posts/:slug?'>
478
+ * // { id: string; slug?: string | undefined }
479
+ */
480
+ type ExtractParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}` ? ParamRecord<Param> & ExtractParams<`/${Rest}`> : Path extends `${string}:${infer Param}` ? ParamRecord<Param> : Path extends `${string}*${infer Wild}` ? {
481
+ [K in Wild]: string;
482
+ } : EmptyParams;
483
+ type EmptyParams = Record<string, never>;
484
+ /**
485
+ * Drop a single parenthesized constraint group from a param name.
486
+ * `id(\\d+)` → `id`
487
+ * `id(\\d+)?` → `id?` (optionality marker preserved for ParamRecord)
488
+ * `id` → `id` (no-op when no constraint present)
489
+ */
490
+ type StripConstraint<P extends string> = P extends `${infer Head}(${string})${infer Tail}` ? `${Head}${Tail}` : P;
491
+ type ParamRecord<P extends string> = StripConstraint<P> extends `${infer Name}?` ? {
492
+ [K in Name]?: string;
493
+ } : {
494
+ [K in StripConstraint<P>]: string;
495
+ };
496
+
497
+ /**
498
+ * Higher-level `accepts*` helpers, parameterized over a context-like object
499
+ * with a `headers` map. Kept context-agnostic so they're trivially testable
500
+ * with a plain `{ headers: {...} }` stub.
501
+ */
502
+
503
+ /** Minimal shape we depend on — `IngeniumContext` satisfies it. */
504
+ interface NegotiableCtx {
505
+ headers: IncomingHttpHeaders;
506
+ }
507
+ /**
508
+ * `accepts(ctx)` → list of accepted media types in preference order
509
+ * (after expanding shorthand inputs is a no-op here — it returns the raw
510
+ * mime strings the client sent).
511
+ *
512
+ * `accepts(ctx, ...types)` → best matching offered type, or `false`.
513
+ * Each `type` may be a shorthand (`'json'`, `'html'`) or full mime
514
+ * (`'application/json'`).
515
+ */
516
+ declare function accepts(ctx: NegotiableCtx): string[];
517
+ declare function accepts(ctx: NegotiableCtx, ...types: string[]): string | false;
518
+ /**
519
+ * `acceptsCharsets(ctx)` → all charsets in preference order.
520
+ * `acceptsCharsets(ctx, ...charsets)` → best match or `false`.
521
+ */
522
+ declare function acceptsCharsets(ctx: NegotiableCtx): string[];
523
+ declare function acceptsCharsets(ctx: NegotiableCtx, ...charsets: string[]): string | false;
524
+ /**
525
+ * `acceptsLanguages(ctx)` → all languages in preference order.
526
+ * `acceptsLanguages(ctx, ...langs)` → best match or `false`.
527
+ *
528
+ * Language matching is treated like opaque tokens with `*` as wildcard;
529
+ * partial-tag matching (e.g. `en` matching `en-US`) is **not** performed —
530
+ * use exact tags for predictable behavior, mirroring Express's default.
531
+ */
532
+ declare function acceptsLanguages(ctx: NegotiableCtx): string[];
533
+ declare function acceptsLanguages(ctx: NegotiableCtx, ...langs: string[]): string | false;
534
+ /**
535
+ * `acceptsEncodings(ctx)` → all encodings in preference order.
536
+ * `acceptsEncodings(ctx, ...encodings)` → best match or `false`.
537
+ *
538
+ * Per RFC 9110 §12.5.4, when `Accept-Encoding` is absent, the server
539
+ * MAY assume the client accepts any encoding — we follow Express and
540
+ * return the first offered.
541
+ */
542
+ declare function acceptsEncodings(ctx: NegotiableCtx): string[];
543
+ declare function acceptsEncodings(ctx: NegotiableCtx, ...encodings: string[]): string | false;
544
+
545
+ /**
546
+ * `formatResponse(ctx, handlers)` — Express's `res.format` for Ingenium.
547
+ *
548
+ * Picks the best handler key against the request `Accept` header, runs it,
549
+ * sets `Content-Type` to the matched key, and writes the result as the
550
+ * response body. If no handler matches and no `default` key is provided,
551
+ * throws a `IngeniumError(406, 'NOT_ACCEPTABLE')`.
552
+ *
553
+ * Handlers may be sync or async — `formatResponse` always awaits.
554
+ */
555
+
556
+ /** Minimal context shape required by `formatResponse` — narrower than full `IngeniumContext`. */
557
+ interface FormattableCtx extends NegotiableCtx {
558
+ set(name: string, value: string | string[]): unknown;
559
+ json(body: unknown, status?: number): void;
560
+ send(body: Buffer$1 | string, status?: number): void;
561
+ }
562
+ /** Map of `mime → handler`. The reserved key `default` is the no-match fallback. */
563
+ type FormatHandlers = Record<string, () => unknown | Promise<unknown>>;
564
+ /**
565
+ * Pick the best handler key for `Accept` and run it.
566
+ *
567
+ * - JSON-shaped result objects are written via `ctx.json`.
568
+ * - String / Buffer results are written via `ctx.send` with the matched
569
+ * content-type preserved (instead of `send`'s default text/plain inference).
570
+ * - `default` handler is used when no explicit key matches.
571
+ * - No match + no default → throws `IngeniumError(406, 'NOT_ACCEPTABLE')`.
572
+ */
573
+ declare function formatResponse(ctx: FormattableCtx, handlers: FormatHandlers): Promise<void>;
574
+
575
+ /**
576
+ * `respondJsonWithEtag(ctx, body, opts)` — JSON response with auto ETag and
577
+ * 304 short-circuit when `If-None-Match` matches.
578
+ *
579
+ * Behavior:
580
+ * 1. Stringify `body` to JSON exactly once.
581
+ * 2. Compute weak ETag (default) over the stringified bytes.
582
+ * 3. If `If-None-Match` (after weak normalization) matches → set 304,
583
+ * clear body, mark written. Skip writing the JSON.
584
+ * 4. Otherwise: set `ETag` + `Content-Type` headers, write the body via
585
+ * the same internal shape `ctx.json` uses, and mark written.
586
+ *
587
+ * Uses the lower-level shape from `IngeniumContext` directly (rather than
588
+ * calling `ctx.json`) so the JSON.stringify result can be reused without
589
+ * a second pass.
590
+ */
591
+
592
+ /** Options for `respondJsonWithEtag`. */
593
+ interface JsonEtagOptions {
594
+ /** Prefix the ETag with `W/`. Defaults to `true`. */
595
+ weak?: boolean;
596
+ /** HTTP status to use for the success path. Defaults to `200`. */
597
+ status?: number;
598
+ }
599
+ /**
600
+ * Minimal context shape required by `respondJsonWithEtag` — keeps the
601
+ * helper testable with a plain stub and avoids a hard import cycle on
602
+ * the full `IngeniumContext` class.
603
+ */
604
+ interface JsonEtagCtx {
605
+ headers: IncomingHttpHeaders;
606
+ _statusCode: number;
607
+ _headers: Record<string, string | string[]>;
608
+ _body: ResponseBody;
609
+ _written: boolean;
610
+ }
611
+ declare function respondJsonWithEtag(ctx: JsonEtagCtx, body: unknown, opts?: JsonEtagOptions): void;
612
+
613
+ /**
614
+ * `URLSearchParams` augmented with a `parse(schema)` method that runs the
615
+ * query through the same schema-detection pipeline as `ctx.body.json(schema)`.
616
+ *
617
+ * The shape passed to the schema is a **shallow array-aware** object:
618
+ *
619
+ * `?id=42&tag=a&tag=b&active=true`
620
+ * → `{ id: '42', tag: ['a','b'], active: 'true' }`
621
+ *
622
+ * Single-occurrence keys → `string`. Repeated keys → `string[]`. Everything is
623
+ * a string on the wire, so the user's schema is responsible for coercing
624
+ * numbers/booleans (Zod: use `z.coerce.number()`; ArkType: use `'string.numeric.parse'`).
625
+ *
626
+ * Rationale for picking THIS coercion over alternatives:
627
+ * - "raw strings only" loses repeated-key fidelity (qs/Express-style arrays)
628
+ * - "pre-coerced booleans/numbers" surprises users when "12foo" silently
629
+ * becomes a string or "true" becomes a boolean against their schema
630
+ * - Shallow-array matches `Object.fromEntries` semantics PLUS the most
631
+ * common ergonomic ask (tag=a&tag=b → tag: string[])
632
+ */
633
+ interface IngeniumQuery extends URLSearchParams {
634
+ parse<T = unknown>(schema: StandardSchemaV1<unknown, T> | SafeParseSchema<T> | ParseSchema<T>): T;
635
+ }
636
+ /** Internal response body shape — adapter writes one of these to the wire. */
637
+ type ResponseBody = {
638
+ kind: 'none';
639
+ } | {
640
+ kind: 'buffer';
641
+ data: Buffer$1;
642
+ } | {
643
+ kind: 'string';
644
+ data: string;
645
+ } | {
646
+ kind: 'stream';
647
+ data: Readable;
648
+ };
649
+ /**
650
+ * Per-request context. Pool-bound: one instance per pool slot, reused
651
+ * across thousands of requests. All mutable fields are reset between uses.
652
+ *
653
+ * The `Params` generic is a phantom — it narrows `ctx.params` for typed
654
+ * route handlers but is `Record<string, string>` at runtime.
655
+ */
656
+ declare class IngeniumContext<Params = Record<string, string>> {
657
+ /** HTTP method, uppercase. */
658
+ method: HttpMethod;
659
+ /** Full request URL including query string (e.g. `/users/42?expand=posts`). */
660
+ url: string;
661
+ /** Path portion of the URL (no query string). Set by the adapter. */
662
+ path: string;
663
+ /** Raw query string (no leading `?`). Use `query` for parsed access. */
664
+ rawQuery: string;
665
+ /** Route params, written at trie-match time. */
666
+ params: Params;
667
+ /** Lowercased request headers (Node convention). */
668
+ headers: IncomingHttpHeaders;
669
+ /** Lazy body accessor. */
670
+ readonly body: IngeniumBody;
671
+ /** Free-form per-request state for plugins/middleware (e.g. `ctx.user = ...`). */
672
+ state: Record<string, unknown>;
673
+ /**
674
+ * Per-request handle to enqueue background jobs onto a registered queue.
675
+ * Wired by `IngeniumApp` as a lazy decorator (declared with `!` because the
676
+ * runtime value is installed by the decorator registry, not the class
677
+ * initializer). Throws if the named queue isn't registered.
678
+ *
679
+ * @example
680
+ * await ctx.queue<{ to: string }>('emails').add({ to: 'a@b.com' })
681
+ */
682
+ queue: <TData = unknown>(name: string) => JobHandle<TData>;
683
+ /** Lazy-parsed query. First access caches the URLSearchParams. */
684
+ private _query;
685
+ get query(): IngeniumQuery;
686
+ /**
687
+ * @internal Lazy cookie holder. `null` until first read of `ctx.cookies`.
688
+ * Reset to `null` in `reset()` so a context returned to the pool drops the
689
+ * parsed-cookie cache (and any closed-over write state — though writes go
690
+ * straight to `_headers`, which is itself reset by reassignment).
691
+ */
692
+ _cookies: IngeniumCookies | null;
693
+ /**
694
+ * First-class cookie API. Lazy: the holder is allocated on first access so
695
+ * apps that never touch cookies pay zero per-request overhead. See
696
+ * `cookies.ts` for the read/write contract and signing rules.
697
+ *
698
+ * @example
699
+ * const sid = ctx.cookies.get('sid', { signed: true })
700
+ * ctx.cookies.set('theme', 'dark', { httpOnly: true, sameSite: 'lax' })
701
+ * ctx.cookies.clear('legacy')
702
+ */
703
+ get cookies(): IngeniumCookies;
704
+ /**
705
+ * @internal App-wide cookie-signing secrets. Stamped by `IngeniumApp.handle`
706
+ * on dispatch entry when configured (mirrors `_trustProxy`). First secret
707
+ * signs new cookies; all entries verify reads (supports rotation). Empty
708
+ * means signed cookies will throw `IngeniumError(500, 'COOKIE_SECRET_MISSING')`.
709
+ *
710
+ * NOT cleared in `reset()` — this is app-wide config, not per-request state,
711
+ * and the cost of re-stamping every request is wasteful when the value is
712
+ * stable across the app's lifetime. The first call to `handle()` after a
713
+ * compose sets it; subsequent requests reuse the same array reference.
714
+ */
715
+ _cookieSecrets: readonly string[];
716
+ /** Immediate socket peer address — populated by the adapter. */
717
+ remoteAddress: string;
718
+ /** Underlying transport protocol — populated by the adapter (http for node:http, https for TLS). */
719
+ baseProtocol: 'http' | 'https';
720
+ /** @internal `trustProxy` config carried in from the app. */ _trustProxy: TrustProxy;
721
+ /** @internal Cached forwarded resolution; computed lazily from headers. */
722
+ private _forwarded;
723
+ private resolveForwarded;
724
+ /**
725
+ * Best-effort client IP. With `trustProxy: false` this is the immediate
726
+ * socket peer; with trust-proxy enabled the X-Forwarded-For chain is
727
+ * walked according to the configured trust policy.
728
+ */
729
+ get ip(): string;
730
+ /** Full forwarded chain (left-to-right, immediate peer last). */
731
+ get ips(): readonly string[];
732
+ /** Best-effort protocol — honors `X-Forwarded-Proto` when trust-proxy is enabled. */
733
+ get protocol(): 'http' | 'https';
734
+ /** Convenience: `protocol === 'https'`. */
735
+ get secure(): boolean;
736
+ /** Best-effort hostname (no port) — honors `X-Forwarded-Host` when trust-proxy is enabled. */
737
+ get hostname(): string;
738
+ /** @internal */ _statusCode: number;
739
+ /** @internal */ _headers: Record<string, string | string[]>;
740
+ /** @internal */ _body: ResponseBody;
741
+ /** @internal Whether a response helper has been called. */
742
+ _written: boolean;
743
+ /**
744
+ * @internal Per-request generation counter. Incremented every time the
745
+ * pool resets this context (and also bumped by `IngeniumApp.handle` when a
746
+ * request times out, so writes from the orphaned handler can be detected
747
+ * as stale). Compared against `_dispatchEpoch` by every response writer.
748
+ */
749
+ _epoch: number;
750
+ /**
751
+ * @internal Last `_epoch` value captured by `IngeniumApp.withEpochGuard`.
752
+ * Set on dispatch entry; the per-dispatch wrappers installed around the
753
+ * response writers close over this value to detect late writes from an
754
+ * orphaned (timed-out) handler. The wrappers compare `_epoch` against
755
+ * the captured value at call time — mismatch ⇒ orphan ⇒ swallow.
756
+ *
757
+ * `0` means no guard is active (no `requestTimeoutMs` configured, or
758
+ * the dispatch already resolved naturally).
759
+ */
760
+ _dispatchEpoch: number;
761
+ /**
762
+ * @internal Dev-only — emit `IngeniumDoubleWriteWarning` when a writer is
763
+ * called after `_written` is already true. No-op in production: V8
764
+ * eliminates the branch body behind the `IS_DEV` gate.
765
+ */
766
+ private _warnDoubleWrite;
767
+ /** Set the HTTP status code. Returns `this` for chaining. */
768
+ status(code: number): this;
769
+ /**
770
+ * Set a response header (case-insensitive). Returns `this` for chaining.
771
+ *
772
+ * Throws `IngeniumHeaderInjectionError` if `name` or `value` contains CR
773
+ * or LF — these would otherwise enable header-injection / response-
774
+ * splitting attacks if a caller forwards untrusted user input directly.
775
+ */
776
+ set(name: string, value: string | string[]): this;
777
+ /** Alias for `set` — matches Express's `res.setHeader`. */
778
+ setHeader(name: string, value: string | string[]): this;
779
+ /** Get a previously-set response header (lowercase lookup). */
780
+ getHeader(name: string): string | string[] | undefined;
781
+ /**
782
+ * Send a JSON response.
783
+ *
784
+ * Throws `IngeniumUnserializableError` if `body` cannot be encoded
785
+ * (circular structure, `BigInt`, etc.) — surfaces a clean 500 from the
786
+ * framework error boundary instead of a deep `TypeError`.
787
+ */
788
+ json(body: unknown, status?: number): void;
789
+ /** Send a `text/plain` response. */
790
+ text(body: string, status?: number): void;
791
+ /** Send a `text/html` response. */
792
+ html(body: string, status?: number): void;
793
+ /** Send a redirect (default 302). */
794
+ redirect(location: string, status?: number): void;
795
+ /** Stream a `Readable` to the client. Sets content-type if not already set. */
796
+ stream(readable: Readable, contentType?: string): void;
797
+ /**
798
+ * Sinatra-style short-circuit. Throws `IngeniumHaltError(status, body?)`
799
+ * — the framework error boundary catches it and serializes per `bodyShape`:
800
+ *
801
+ * - `ctx.halt(401)` → 401 with default JSON `{ error, code: 'HALT' }`.
802
+ * - `ctx.halt(404, 'Not Found')` → 404 `text/plain` body verbatim.
803
+ * - `ctx.halt(422, { fields })` → 422 `application/json` body verbatim.
804
+ *
805
+ * The TypeScript `never` return type lets `if (!found) ctx.halt(404)`
806
+ * narrow the rest of the function — code after the call is unreachable.
807
+ *
808
+ * To bypass the error boundary entirely (write the response without
809
+ * throwing) call `ctx.json(body, status)` and `return` from the handler.
810
+ *
811
+ * @example
812
+ * if (!authorized(ctx)) ctx.halt(401, 'Unauthorized')
813
+ * if (!user) ctx.halt(404, { error: 'Not Found', id })
814
+ */
815
+ halt(status: number, body?: string | Record<string, unknown>): never;
816
+ /** Send a `Buffer` body verbatim. */
817
+ send(body: Buffer$1 | string, status?: number): void;
818
+ /**
819
+ * Return the best mime type the client accepts from the offered list, or
820
+ * `false` if none are acceptable. With no arguments, returns the parsed
821
+ * preference-ordered list of accepted types from `Accept`.
822
+ *
823
+ * Each `type` may be a shorthand (`'json'`, `'html'`, `'csv'`, …) or a full
824
+ * mime (`'application/json'`). Quality factors are honored.
825
+ *
826
+ * @example
827
+ * if (ctx.accepts('json')) ctx.json({ ok: true })
828
+ * else ctx.status(406).text('Not Acceptable')
829
+ */
830
+ accepts(): string[];
831
+ accepts(...types: string[]): string | false;
832
+ /** Best matching charset from the offered list against `Accept-Charset`. */
833
+ acceptsCharsets(): string[];
834
+ acceptsCharsets(...charsets: string[]): string | false;
835
+ /** Best matching language against `Accept-Language` (exact-tag match only). */
836
+ acceptsLanguages(): string[];
837
+ acceptsLanguages(...langs: string[]): string | false;
838
+ /** Best matching encoding against `Accept-Encoding` (first offered when header absent). */
839
+ acceptsEncodings(): string[];
840
+ acceptsEncodings(...encodings: string[]): string | false;
841
+ /**
842
+ * Run the handler whose key best matches the request `Accept` header. The
843
+ * matched key is set as `Content-Type`. If no key matches and no `default`
844
+ * handler is provided, throws `IngeniumError(406, 'NOT_ACCEPTABLE')`.
845
+ */
846
+ format(handlers: FormatHandlers): Promise<void>;
847
+ /**
848
+ * `true` when the client's `If-None-Match` matches the response `ETag`,
849
+ * or `If-Modified-Since` is at-or-after the response `Last-Modified`.
850
+ * Reads from `_headers` so handlers can set ETag / Last-Modified before checking.
851
+ */
852
+ get fresh(): boolean;
853
+ /** `!fresh`. */
854
+ get stale(): boolean;
855
+ /**
856
+ * Send a JSON body with an auto-computed weak ETag. If the request's
857
+ * `If-None-Match` matches the computed tag, short-circuits to 304.
858
+ */
859
+ jsonWithEtag(body: unknown, opts?: JsonEtagOptions): void;
860
+ /**
861
+ * Reset all per-request state. Called by the pool before returning the
862
+ * context to the free list. Reassignments preserve the V8 hidden class
863
+ * so subsequent allocations stay monomorphic.
864
+ */
865
+ reset(): void;
866
+ }
867
+
868
+ /**
869
+ * A function that runs as part of the middleware chain. Call `next()` to
870
+ * continue to the next middleware; omit it to short-circuit.
871
+ *
872
+ * @example
873
+ * const logger: IngeniumMiddleware = async (ctx, next) => {
874
+ * const start = Date.now()
875
+ * await next()
876
+ * ctx.logger?.info(`${ctx.method} ${ctx.path} ${Date.now() - start}ms`)
877
+ * }
878
+ */
879
+ type IngeniumMiddleware = (ctx: IngeniumContext, next: () => Promise<void>) => unknown | Promise<unknown>;
880
+ /**
881
+ * A composed middleware chain plus terminal handler. Returned by `compose()`.
882
+ * Internally cached on each trie leaf.
883
+ */
884
+ type ComposedHandler = (ctx: IngeniumContext) => Promise<void>;
885
+ /**
886
+ * A user-facing route handler. Its return value is reflected to the wire by
887
+ * the response-helper dispatcher (see `response/helpers.ts`):
888
+ *
889
+ * - `undefined` → 204 (unless `ctx.json/...` was already called)
890
+ * - `string` → 200 text/plain (or text/html if it starts with `<`)
891
+ * - object → 200 application/json
892
+ * - `Buffer`/`Uint8Array` → 200 application/octet-stream
893
+ * - `Readable` → streamed response
894
+ *
895
+ * For full control, call `ctx.json/text/html/stream/redirect` and return
896
+ * `void` (or any value — return value is ignored once a helper has run).
897
+ */
898
+ type IngeniumHandler<Params = Record<string, string>> = (ctx: IngeniumContext<Params>) => unknown | Promise<unknown>;
899
+
900
+ /** A journal entry — replayed against the trie when the app composes. */
901
+ type Registration = {
902
+ kind: 'use-global';
903
+ mw: IngeniumMiddleware;
904
+ } | {
905
+ kind: 'use-prefix';
906
+ prefix: string;
907
+ mw: IngeniumMiddleware;
908
+ } | {
909
+ kind: 'use-router';
910
+ prefix: string;
911
+ router: Router;
912
+ } | {
913
+ kind: 'route';
914
+ method: HttpMethod;
915
+ path: string;
916
+ handler: IngeniumHandler;
917
+ /**
918
+ * Inline middleware passed positionally to `app.get(path, mw1, mw2, handler)`
919
+ * (and the equivalent declarative-options form on `IngeniumApp`). Spliced into
920
+ * the composed chain AFTER global + scoped middleware AND BEFORE the handler.
921
+ * `undefined` for the back-compat single-arg form.
922
+ */
923
+ inlineMiddleware?: IngeniumMiddleware[];
924
+ };
925
+ /**
926
+ * A mountable router. Registrations are journaled, not eagerly composed —
927
+ * mounting via `app.use('/api', router)` replays this journal into the
928
+ * parent's trie with the prefix prepended.
929
+ */
930
+ declare class Router {
931
+ /** @internal */ readonly journal: Registration[];
932
+ /** Add middleware that runs for every request below this router. */
933
+ use(mw: IngeniumMiddleware): this;
934
+ /** Mount middleware or a sub-router at a path prefix. */
935
+ use(prefix: string, mw: IngeniumMiddleware | Router): this;
936
+ get<P extends string>(path: P, handler: IngeniumHandler<ExtractParams<P>>): this;
937
+ get<P extends string>(path: P, ...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
938
+ post<P extends string>(path: P, handler: IngeniumHandler<ExtractParams<P>>): this;
939
+ post<P extends string>(path: P, ...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
940
+ put<P extends string>(path: P, handler: IngeniumHandler<ExtractParams<P>>): this;
941
+ put<P extends string>(path: P, ...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
942
+ patch<P extends string>(path: P, handler: IngeniumHandler<ExtractParams<P>>): this;
943
+ patch<P extends string>(path: P, ...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
944
+ delete<P extends string>(path: P, handler: IngeniumHandler<ExtractParams<P>>): this;
945
+ delete<P extends string>(path: P, ...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
946
+ head<P extends string>(path: P, handler: IngeniumHandler<ExtractParams<P>>): this;
947
+ head<P extends string>(path: P, ...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
948
+ options<P extends string>(path: P, handler: IngeniumHandler<ExtractParams<P>>): this;
949
+ options<P extends string>(path: P, ...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
950
+ /**
951
+ * Chainable per-path registration. Returns a builder that holds the path
952
+ * and lets you stack verbs on it without retyping:
953
+ *
954
+ * @example
955
+ * router
956
+ * .route('/users/:id')
957
+ * .get((ctx) => loadUser(ctx.params.id))
958
+ * .put(requireAdmin, (ctx) => updateUser(ctx))
959
+ * .delete(requireAdmin, (ctx) => deleteUser(ctx))
960
+ *
961
+ * Pure registration sugar — every call delegates to `router.method(...)`,
962
+ * so all features (inline middleware, declarative options, typed params
963
+ * via `ExtractParams<P>`) work identically.
964
+ */
965
+ route<P extends string>(path: P): RouteBuilder<P>;
966
+ /**
967
+ * Internal — register a route under any HTTP method. Accepts the variadic
968
+ * `(...inlineMiddleware, handler)` tail; the LAST positional arg is always
969
+ * the handler.
970
+ */
971
+ method(method: HttpMethod, path: string, handler: IngeniumHandler): this;
972
+ method(method: HttpMethod, path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
973
+ }
974
+ /**
975
+ * Per-path chainable builder returned by `app.route(path)` and
976
+ * `router.route(path)`. Holds the path and an "emit" callback that registers
977
+ * a route on the underlying host (an `IngeniumApp` or a `Router`); the
978
+ * builder itself is just sugar — no per-request cost, no separate dispatch
979
+ * path. The host's verb method does all the validation, dirty-bit flipping,
980
+ * and journal writes.
981
+ *
982
+ * The generic `P` flows `ExtractParams<P>` into every handler signature so
983
+ * `app.route('/users/:id').get(ctx => ctx.params.id)` narrows `ctx.params`
984
+ * exactly like the bare verb form does.
985
+ */
986
+ declare class RouteBuilder<P extends string> {
987
+ private readonly emit;
988
+ constructor(emit: (method: HttpMethod, args: unknown[]) => void);
989
+ get(handler: IngeniumHandler<ExtractParams<P>>): this;
990
+ get(...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
991
+ post(handler: IngeniumHandler<ExtractParams<P>>): this;
992
+ post(...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
993
+ put(handler: IngeniumHandler<ExtractParams<P>>): this;
994
+ put(...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
995
+ patch(handler: IngeniumHandler<ExtractParams<P>>): this;
996
+ patch(...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
997
+ delete(handler: IngeniumHandler<ExtractParams<P>>): this;
998
+ delete(...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
999
+ head(handler: IngeniumHandler<ExtractParams<P>>): this;
1000
+ head(...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
1001
+ options(handler: IngeniumHandler<ExtractParams<P>>): this;
1002
+ options(...args: [...IngeniumMiddleware[], IngeniumHandler<ExtractParams<P>>]): this;
1003
+ /** Register the same handler for all common HTTP methods (GET, POST, PUT, PATCH, DELETE). */
1004
+ all(handler: IngeniumHandler<ExtractParams<P>>): this;
1005
+ }
1006
+
1007
+ /**
1008
+ * Payload fired to `onRoute` hooks each time a route is registered into the
1009
+ * trie during composition. Plugins can observe — they MUST NOT mutate.
1010
+ */
1011
+ interface RegistrationEvent {
1012
+ /** HTTP method (uppercase). */
1013
+ readonly method: HttpMethod;
1014
+ /** Final composed route path (after all prefixes). */
1015
+ readonly path: string;
1016
+ }
1017
+ /**
1018
+ * The shape a plugin can rely on regardless of whether it's registered onto
1019
+ * the root `IngeniumApp` or onto a `ScopedApp` (via `app.scope(...).register(...)`).
1020
+ *
1021
+ * Both root and scoped registration targets implement this interface. Plugins
1022
+ * that previously took `(app: IngeniumApp, opts)` are source-compatible:
1023
+ * `IngeniumApp` implements every member of `PluginTarget`. The only practical
1024
+ * difference at the call site is that `target.scope(...)`, `target.use(mw)`,
1025
+ * and the verb methods become prefix-relative when `target` is a `ScopedApp`.
1026
+ *
1027
+ * # Scoping semantics for plugin authors
1028
+ *
1029
+ * - `target.use(mw)` / `target.use(subprefix, mw)` — middleware is scoped to
1030
+ * the target's prefix at compose time. On the root, this is "global". In a
1031
+ * scope, it's "applies only to paths under the scope's prefix".
1032
+ * - `target.get/post/...` and `target.method(...)` — paths are prefix-
1033
+ * relative; the scope prepends its absolute prefix at registration time.
1034
+ * - `target.register(plugin, opts)` — runs the plugin against the SAME
1035
+ * target. Nested scopes compose as expected.
1036
+ * - `target.hooks` — lifecycle hooks are GLOBAL even when called inside a
1037
+ * scope. Hooks fire per request, before route dispatch; making them
1038
+ * scope-aware would require runtime path-prefix checks on every request.
1039
+ * If a plugin needs scope-aware behavior, it should inspect `ctx.path`
1040
+ * inside the hook body.
1041
+ * - `target.decorate(...)` / `target.decorateRequest(...)` — decorators are
1042
+ * GLOBAL even when called inside a scope (see {@link IngeniumPlugin} JSDoc
1043
+ * for the rationale). `ScopedApp.decorate` emits a one-shot
1044
+ * `process.emitWarning` in non-production environments to surface this
1045
+ * footgun.
1046
+ */
1047
+ interface PluginTarget {
1048
+ /** Lifecycle hooks (global — see interface JSDoc). */
1049
+ readonly hooks: Hooks;
1050
+ /** Add middleware that runs for every request below this target. */
1051
+ use(mw: IngeniumMiddleware): this;
1052
+ /** Mount middleware or a sub-router at a path prefix (relative to this target). */
1053
+ use(prefix: string, mw: IngeniumMiddleware | Router): this;
1054
+ /** Register a route under any HTTP method (path is relative to this target). */
1055
+ method(method: HttpMethod, path: string, handler: IngeniumHandler): this;
1056
+ method(method: HttpMethod, path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1057
+ /**
1058
+ * Chainable per-path builder. Same path-joining rules as the bare verbs —
1059
+ * inside a `ScopedApp`, the builder's emitted routes are prefix-relative.
1060
+ */
1061
+ route<P extends string>(path: P): RouteBuilder<P>;
1062
+ /** Convenience verb shortcuts (paths are relative to this target). */
1063
+ get(path: string, handler: IngeniumHandler): this;
1064
+ get(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1065
+ post(path: string, handler: IngeniumHandler): this;
1066
+ post(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1067
+ put(path: string, handler: IngeniumHandler): this;
1068
+ put(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1069
+ patch(path: string, handler: IngeniumHandler): this;
1070
+ patch(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1071
+ delete(path: string, handler: IngeniumHandler): this;
1072
+ delete(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1073
+ head(path: string, handler: IngeniumHandler): this;
1074
+ head(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1075
+ options(path: string, handler: IngeniumHandler): this;
1076
+ options(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
1077
+ /**
1078
+ * Pre-handler / post-handler middleware (paths are relative to this target).
1079
+ * Inside a `ScopedApp` these are confined to the scope prefix — `s.before(h)`
1080
+ * only fires for requests under the scope, mirroring `s.use`.
1081
+ */
1082
+ before(handler: IngeniumMiddleware): this;
1083
+ before(pattern: string, handler: IngeniumMiddleware): this;
1084
+ after(handler: IngeniumMiddleware): this;
1085
+ after(pattern: string, handler: IngeniumMiddleware): this;
1086
+ /** Decorator registration. NOTE: GLOBAL even when called inside a scope. */
1087
+ decorate<T>(name: string, factory: LazyDecorator<T>): this;
1088
+ decorateRequest<T>(name: string, factory: EagerDecorator<T>): this;
1089
+ /**
1090
+ * Register a plugin against this target. Plugins may be async and the
1091
+ * caller should `await` the returned promise.
1092
+ */
1093
+ register<O>(plugin: IngeniumPlugin<O>, opts: O): Promise<this>;
1094
+ register(plugin: IngeniumPlugin<void>): Promise<this>;
1095
+ /**
1096
+ * Open a nested registration scope. All registrations inside `registrar`
1097
+ * are prefix-relative to `prefix` (and inherit any outer scope prefix).
1098
+ */
1099
+ scope(prefix: string, registrar: (scope: PluginTarget) => void): this | Promise<this>;
1100
+ }
1101
+ /**
1102
+ * A plugin is a function that mutates a registration target: it can register
1103
+ * routes, middleware, decorators, and hook handlers. Plugins are registered
1104
+ * before `compose()` runs; they may be async.
1105
+ *
1106
+ * The `target` parameter is `PluginTarget` — implemented by both `IngeniumApp`
1107
+ * (the root) and `ScopedApp` (created by `app.scope(prefix, ...)`). When a
1108
+ * plugin is registered inside a scope, its `target.use(...)` / `target.get(...)`
1109
+ * are automatically prefix-scoped at compose time.
1110
+ *
1111
+ * # Scoped-decorator caveat (V1)
1112
+ *
1113
+ * Decorators (`target.decorate`, `target.decorateRequest`) install onto the
1114
+ * pooled `IngeniumContext` at request start; the registry is per-app, not
1115
+ * per-path. That means a plugin registered inside `app.scope('/api', ...)`
1116
+ * that calls `target.decorate('user', ...)` will decorate EVERY request,
1117
+ * not just `/api/*` requests. The first such call inside a scope emits a
1118
+ * `process.emitWarning` in non-production environments. Plugin authors who
1119
+ * want per-scope decorator behavior should make the decorator's factory
1120
+ * inspect `ctx.path` and return a sentinel for out-of-scope requests.
1121
+ *
1122
+ * @example
1123
+ * const myPlugin: IngeniumPlugin<{ secret: string }> = async (target, opts) => {
1124
+ * target.hooks.onRequest((ctx) => { ... })
1125
+ * target.use((ctx, next) => next()) // scoped if target is a ScopedApp
1126
+ * target.get('/whoami', (ctx) => ...) // path is relative to scope
1127
+ * }
1128
+ *
1129
+ * await app.register(myPlugin, { secret: 'shh' })
1130
+ * app.scope('/api', (s) => s.register(myPlugin, { secret: 'shh' }))
1131
+ */
1132
+ type IngeniumPlugin<O = void> = (target: PluginTarget, opts: O) => void | Promise<void>;
1133
+ /** Fires once per route as the trie is built (during `compose()`). */
1134
+ type OnRouteHook = (registration: RegistrationEvent) => void;
1135
+ /** Fires before composition runs. May be async. */
1136
+ type OnComposeHook = () => void | Promise<void>;
1137
+ /** Fires at the start of every request, before middleware dispatch. */
1138
+ type OnRequestHook = (ctx: IngeniumContext) => void | Promise<void>;
1139
+ /** Fires after the handler resolves successfully. */
1140
+ type OnResponseHook = (ctx: IngeniumContext) => void | Promise<void>;
1141
+ /**
1142
+ * Fires when the handler chain throws. OBSERVATION ONLY — the framework's
1143
+ * error boundary still owns the response. Throwing inside an `onError` hook
1144
+ * is swallowed; this is by design so observers can't mask the original error.
1145
+ */
1146
+ type OnErrorHook = (err: unknown, ctx: IngeniumContext) => void | Promise<void>;
1147
+ /**
1148
+ * Public hooks API exposed on `app.hooks`. Each method appends a listener;
1149
+ * listeners are invoked in registration order, sequentially (`await`-ed in
1150
+ * a loop) for predictable ordering.
1151
+ */
1152
+ interface Hooks {
1153
+ onRoute(fn: OnRouteHook): void;
1154
+ onCompose(fn: OnComposeHook): void;
1155
+ onRequest(fn: OnRequestHook): void;
1156
+ onResponse(fn: OnResponseHook): void;
1157
+ onError(fn: OnErrorHook): void;
1158
+ }
1159
+ /** Lazy decorator — computed on first access, then cached on the ctx. */
1160
+ type LazyDecorator<T = unknown> = (ctx: IngeniumContext) => T;
1161
+ /** Eager decorator — evaluated at request start, value assigned directly. */
1162
+ type EagerDecorator<T = unknown> = (ctx: IngeniumContext) => T;
1163
+ /** Generic decorator factory shape (covers both lazy and eager). */
1164
+ type Decorator<T = unknown> = (ctx: IngeniumContext) => T;
1165
+
1166
+ /** A function the framework hands to the transport — call it per request. */
1167
+ type TransportDispatch = (ctx: IngeniumContext) => Promise<void>;
1168
+ /** A function the transport calls to acquire a context from the pool. */
1169
+ type TransportAcquire = () => IngeniumContext;
1170
+ /** A function the transport calls to release a context back to the pool. */
1171
+ type TransportRelease = (ctx: IngeniumContext) => void;
1172
+ /**
1173
+ * The hooks a transport uses to interact with the framework. The transport
1174
+ * owns the request/response objects from its underlying server (node:http,
1175
+ * Bun.serve, etc.), populates a `IngeniumContext` from each request, awaits the
1176
+ * `dispatch` callback, then writes the context's response state to the wire.
1177
+ */
1178
+ interface TransportHooks {
1179
+ acquire: TransportAcquire;
1180
+ release: TransportRelease;
1181
+ dispatch: TransportDispatch;
1182
+ /**
1183
+ * Hard ceiling (bytes) on the total request body. Adapters SHOULD wrap the
1184
+ * inbound body stream in `createByteLimit(maxRequestBytes)` before handing
1185
+ * it to `ctx.body._attach(...)`, AND reject with a 413 immediately when
1186
+ * the request advertises a `Content-Length` greater than this value (no
1187
+ * need to read the body). `Number.POSITIVE_INFINITY` disables the cap.
1188
+ *
1189
+ * Optional for backward compatibility with adapters / test fixtures that
1190
+ * predate this hook. The framework's `app.listen()` always populates the
1191
+ * field (default 2 MiB); consumers that read it should treat `undefined`
1192
+ * as "no cap" (`Number.POSITIVE_INFINITY`).
1193
+ */
1194
+ maxRequestBytes?: number;
1195
+ }
1196
+ /** Options accepted by {@link ListeningServer.close}. */
1197
+ interface CloseOptions {
1198
+ /**
1199
+ * Maximum time (ms) to wait for keep-alive sockets to drain naturally
1200
+ * before they are forcibly destroyed. When omitted (or undefined), no
1201
+ * force-close occurs and `close()` waits indefinitely for sockets to
1202
+ * finish — this matches the historical Node `server.close()` behavior.
1203
+ */
1204
+ gracefulTimeoutMs?: number;
1205
+ }
1206
+ /** A transport-agnostic listening server handle. */
1207
+ interface ListeningServer {
1208
+ /** Bound port (resolved if `port: 0` was passed). */
1209
+ port: number;
1210
+ /** The bound host. */
1211
+ host: string;
1212
+ /**
1213
+ * Stop accepting new connections; resolves when in-flight requests
1214
+ * finish. If `gracefulTimeoutMs` is provided, idle keep-alive sockets
1215
+ * still open after that many milliseconds are forcibly destroyed.
1216
+ */
1217
+ close(opts?: CloseOptions): Promise<void>;
1218
+ }
1219
+ /**
1220
+ * A transport binds the Ingenium dispatch loop to a concrete server
1221
+ * runtime (Node's `node:http`, Bun.serve, etc).
1222
+ */
1223
+ interface Transport {
1224
+ /** Wire up the transport with framework-side hooks. Called once by `app.listen()`. */
1225
+ attach(hooks: TransportHooks): void;
1226
+ /** Bind to a port and start accepting requests. */
1227
+ listen(port: number, host?: string): Promise<ListeningServer>;
1228
+ }
1229
+
1230
+ /**
1231
+ * Minimal OpenAPI 3.1 type surface — just enough for what Ingenium
1232
+ * generates today. Not a full mirror of the spec; we keep it intentionally
1233
+ * narrow so the generator's outputs typecheck without dragging in a
1234
+ * 4000-line ambient module.
1235
+ *
1236
+ * Spec reference: https://spec.openapis.org/oas/v3.1.0
1237
+ *
1238
+ * Intentional gaps (out of scope for v0.0.1, document-as-TODO):
1239
+ * - `callbacks`, `links`, `webhooks` — none of these have a registration
1240
+ * surface in Ingenium yet.
1241
+ * - `discriminator` / `xml` — schema is passed through verbatim, so callers
1242
+ * can include these themselves if they want to.
1243
+ * - `pathItems` under `components` — we only emit operations under `paths`.
1244
+ */
1245
+ /** Permissive `$ref`-or-inline union used in many slots. */
1246
+ type Ref<T> = T | {
1247
+ $ref: string;
1248
+ };
1249
+ /** A JSON Schema fragment (per OpenAPI 3.1 = full JSON Schema 2020-12). */
1250
+ type Schema = Record<string, unknown>;
1251
+ /** Where a parameter lives. Ingenium only emits `path` from route syntax. */
1252
+ type ParameterLocation = 'query' | 'header' | 'path' | 'cookie';
1253
+ interface Parameter {
1254
+ name: string;
1255
+ in: ParameterLocation;
1256
+ description?: string;
1257
+ required?: boolean;
1258
+ deprecated?: boolean;
1259
+ schema?: Schema;
1260
+ example?: unknown;
1261
+ examples?: Record<string, Example>;
1262
+ /** Free-form passthrough so callers can stamp `x-*` extensions. */
1263
+ [extension: `x-${string}`]: unknown;
1264
+ }
1265
+ interface Example {
1266
+ summary?: string;
1267
+ description?: string;
1268
+ value?: unknown;
1269
+ externalValue?: string;
1270
+ }
1271
+ interface MediaType {
1272
+ schema?: Schema;
1273
+ example?: unknown;
1274
+ examples?: Record<string, Example>;
1275
+ }
1276
+ interface RequestBody {
1277
+ description?: string;
1278
+ required?: boolean;
1279
+ content: Record<string, MediaType>;
1280
+ }
1281
+ interface Response {
1282
+ description: string;
1283
+ headers?: Record<string, Ref<Header>>;
1284
+ content?: Record<string, MediaType>;
1285
+ }
1286
+ interface Header {
1287
+ description?: string;
1288
+ required?: boolean;
1289
+ deprecated?: boolean;
1290
+ schema?: Schema;
1291
+ }
1292
+ interface SecurityRequirement {
1293
+ [name: string]: string[];
1294
+ }
1295
+ interface Operation {
1296
+ tags?: string[];
1297
+ summary?: string;
1298
+ description?: string;
1299
+ operationId?: string;
1300
+ parameters?: Parameter[];
1301
+ requestBody?: RequestBody;
1302
+ responses?: Record<string, Response>;
1303
+ deprecated?: boolean;
1304
+ security?: SecurityRequirement[];
1305
+ /** Free-form passthrough so callers can stamp `x-*` extensions. */
1306
+ [extension: `x-${string}`]: unknown;
1307
+ }
1308
+ type PathItem = Partial<Record<'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'trace', Operation>> & {
1309
+ summary?: string;
1310
+ description?: string;
1311
+ parameters?: Parameter[];
1312
+ };
1313
+ interface Server {
1314
+ url: string;
1315
+ description?: string;
1316
+ variables?: Record<string, {
1317
+ default: string;
1318
+ enum?: string[];
1319
+ description?: string;
1320
+ }>;
1321
+ }
1322
+ interface Tag {
1323
+ name: string;
1324
+ description?: string;
1325
+ }
1326
+ interface Info {
1327
+ title: string;
1328
+ version: string;
1329
+ description?: string;
1330
+ termsOfService?: string;
1331
+ contact?: {
1332
+ name?: string;
1333
+ url?: string;
1334
+ email?: string;
1335
+ };
1336
+ license?: {
1337
+ name: string;
1338
+ url?: string;
1339
+ identifier?: string;
1340
+ };
1341
+ summary?: string;
1342
+ }
1343
+ interface Components {
1344
+ schemas?: Record<string, Schema>;
1345
+ responses?: Record<string, Response>;
1346
+ parameters?: Record<string, Parameter>;
1347
+ examples?: Record<string, Example>;
1348
+ requestBodies?: Record<string, RequestBody>;
1349
+ headers?: Record<string, Header>;
1350
+ securitySchemes?: Record<string, SecurityScheme>;
1351
+ }
1352
+ /**
1353
+ * Loose security-scheme type — we do not interpret this, we pass it through
1354
+ * verbatim to `components.securitySchemes`. Use the OpenAPI spec's full
1355
+ * shape (apiKey / http / oauth2 / openIdConnect / mutualTLS).
1356
+ */
1357
+ type SecurityScheme = Record<string, unknown>;
1358
+ interface OpenApiSpec {
1359
+ openapi: '3.1.0';
1360
+ info: Info;
1361
+ servers?: Server[];
1362
+ paths: Record<string, PathItem>;
1363
+ components?: Components;
1364
+ security?: SecurityRequirement[];
1365
+ tags?: Tag[];
1366
+ externalDocs?: {
1367
+ url: string;
1368
+ description?: string;
1369
+ };
1370
+ }
1371
+
1372
+ /**
1373
+ * Per-route metadata supplied via `app.describe('METHOD', '/path', meta)`.
1374
+ * Merged into the generated Operation by `generateOpenApi`.
1375
+ *
1376
+ * Anything you put here ends up on the operation object verbatim, except:
1377
+ * - `hidden: true` skips the route entirely (won't appear in the spec).
1378
+ * - `parameters` are *appended* to the path-param parameters extracted from
1379
+ * the route syntax, so you typically only put `query`, `header`, or
1380
+ * `cookie` parameters here.
1381
+ */
1382
+ interface RouteDescriptor {
1383
+ summary?: string;
1384
+ description?: string;
1385
+ operationId?: string;
1386
+ tags?: string[];
1387
+ deprecated?: boolean;
1388
+ hidden?: boolean;
1389
+ parameters?: Parameter[];
1390
+ requestBody?: RequestBody;
1391
+ responses?: Record<string | number, Response>;
1392
+ security?: SecurityRequirement[];
1393
+ /** Extension passthrough — anything starting with `x-` is preserved. */
1394
+ [extension: `x-${string}`]: unknown;
1395
+ }
1396
+
1397
+ /**
1398
+ * A single named background queue. Wraps a {@link QueueStore} with a worker
1399
+ * pool, retry/backoff logic, pause/resume controls, and a `drain()` for
1400
+ * graceful shutdown.
1401
+ *
1402
+ * The pool is event-driven: when a slot frees up, the queue immediately
1403
+ * tries to pull another job; if none is ready (empty or all delayed), the
1404
+ * pool sleeps until either:
1405
+ * - a new `add()` call wakes it, or
1406
+ * - the earliest delayed job's `notBefore` elapses (timer-based wake).
1407
+ *
1408
+ * All timers are `unref()`'d so the queue alone never keeps the event loop alive.
1409
+ */
1410
+ declare class IngeniumQueue<TData = unknown> {
1411
+ readonly name: string;
1412
+ private readonly store;
1413
+ private readonly worker;
1414
+ private readonly retries;
1415
+ private readonly concurrency;
1416
+ private readonly onFailed;
1417
+ /** Active worker slot count. When `< concurrency`, pull more work. */
1418
+ private active;
1419
+ /** Whether `pause()` has been called and `resume()` not yet. */
1420
+ private paused;
1421
+ /** Whether `drain()`/close has been called. No new jobs accepted. */
1422
+ private closed;
1423
+ /** Timer for waking the pool when a delayed retry becomes due. */
1424
+ private wakeTimer;
1425
+ /** Resolvers waiting for `active` to hit 0 (used by `drain()`). */
1426
+ private idleWaiters;
1427
+ /** Set when the pool is started — protects against double-start. */
1428
+ private started;
1429
+ constructor(name: string, opts: QueueOptions<TData>, worker: QueueWorker<TData>);
1430
+ /**
1431
+ * Start the worker pool. Idempotent — safe to call multiple times. The
1432
+ * pool is also implicitly started by the first `add()` call so direct
1433
+ * invocation is optional.
1434
+ */
1435
+ start(): void;
1436
+ /** Enqueue a job. Returns the assigned id. */
1437
+ add(data: TData): Promise<{
1438
+ id: string;
1439
+ }>;
1440
+ /** Approximate number of pending jobs. */
1441
+ size(): Promise<number>;
1442
+ /** Number of jobs in the dead-letter list. */
1443
+ failedCount(): Promise<number>;
1444
+ /**
1445
+ * Empty the dead-letter list. Only effective when the underlying store
1446
+ * is the default {@link MemoryQueueStore}; custom stores should provide
1447
+ * their own clearing surface.
1448
+ */
1449
+ clearFailed(): void;
1450
+ /**
1451
+ * Stop pulling new jobs from the store. In-flight jobs continue to run
1452
+ * until they complete. Idempotent.
1453
+ */
1454
+ pause(): void;
1455
+ /** Resume pulling jobs. Wakes the pool. */
1456
+ resume(): void;
1457
+ /**
1458
+ * Wait for all in-flight jobs to complete, then stop accepting new ones.
1459
+ * If `timeoutMs` elapses first, resolve anyway — the orphaned jobs keep
1460
+ * running until they naturally finish (JS can't cancel a Promise), but
1461
+ * the framework stops waiting.
1462
+ *
1463
+ * Returns `true` if the queue drained cleanly; `false` on timeout.
1464
+ */
1465
+ drain(timeoutMs?: number): Promise<boolean>;
1466
+ /**
1467
+ * Pump the pool: while we have free slots and we're not paused, pull jobs
1468
+ * and dispatch them. No-op when paused / saturated. Re-entrant: every
1469
+ * job completion calls `pump()` again to fill the slot.
1470
+ */
1471
+ private pump;
1472
+ /**
1473
+ * Attempts to take one job from the store and run it. Returns `false` if
1474
+ * the store is empty (or all-delayed) so the caller stops looping.
1475
+ *
1476
+ * NOTE: we increment `active` BEFORE the async `next()` resolves so a
1477
+ * burst of synchronous `pump()` calls doesn't over-subscribe the pool.
1478
+ * We decrement on the `null` path.
1479
+ */
1480
+ private tryFillSlot;
1481
+ private runOne;
1482
+ private safeFail;
1483
+ /**
1484
+ * If the store has only delayed entries (e.g. just-retried jobs whose
1485
+ * backoff hasn't elapsed), schedule a one-shot wake when the earliest
1486
+ * delay fires so we don't spin or sleep forever.
1487
+ *
1488
+ * Only the default in-memory store exposes `earliestPendingAt`; for
1489
+ * custom stores we don't poll — we rely on the next `add()` to wake us.
1490
+ */
1491
+ private scheduleDelayedWake;
1492
+ private clearWakeTimer;
1493
+ private checkIdle;
1494
+ }
1495
+
1496
+ /**
1497
+ * Maps queue names → {@link IngeniumQueue} instances. Held by `IngeniumApp`,
1498
+ * mirroring the shape of {@link CronRegistry}.
1499
+ *
1500
+ * Lookup is O(1). Names must be unique within an app — re-registering the
1501
+ * same name throws (mirrors how `app.get('/users')` would conflict if you
1502
+ * registered the same path twice with the same method).
1503
+ */
1504
+ declare class QueueRegistry {
1505
+ private readonly queues;
1506
+ /**
1507
+ * Register a new queue. Returns the created instance. Throws if a queue
1508
+ * with `name` is already registered.
1509
+ */
1510
+ register<TData>(name: string, opts: QueueOptions<TData>, worker: QueueWorker<TData>): IngeniumQueue<TData>;
1511
+ /** Look up a queue. Throws if not registered (typos surface immediately). */
1512
+ get<TData = unknown>(name: string): IngeniumQueue<TData>;
1513
+ /** Has a queue with this name been registered? */
1514
+ has(name: string): boolean;
1515
+ /** Number of registered queues. */
1516
+ count(): number;
1517
+ /** All registered queue names (insertion order). */
1518
+ names(): string[];
1519
+ /**
1520
+ * Start the worker pool of every registered queue. Called by the app's
1521
+ * composition step so workers don't process jobs before the app is ready.
1522
+ */
1523
+ startAll(): void;
1524
+ /**
1525
+ * Drain every queue concurrently. Resolves when all queues either finish
1526
+ * their in-flight work or hit `timeoutMs`. The returned object reports
1527
+ * which queues drained cleanly vs timed out — useful for shutdown logs.
1528
+ */
1529
+ drainAll(timeoutMs?: number): Promise<{
1530
+ clean: string[];
1531
+ timedOut: string[];
1532
+ }>;
1533
+ }
1534
+
1535
+ /**
1536
+ * Handler signature for `app.cron(...)` jobs. Receives the scheduled fire
1537
+ * time AND a fresh `now` so handlers can detect drift / late starts.
1538
+ */
1539
+ type CronHandler = (ctx: {
1540
+ now: Date;
1541
+ firedAt: Date;
1542
+ }) => unknown | Promise<unknown>;
1543
+ /** Options for {@link IngeniumCronJob}. */
1544
+ interface CronOptions {
1545
+ /** IANA timezone for the spec. Default `'UTC'`. */
1546
+ timezone?: string;
1547
+ /**
1548
+ * If `true`, fire once at `start()` BEFORE waiting for the next scheduled
1549
+ * slot. The synthetic `firedAt` for this immediate run is `now`. Default `false`.
1550
+ */
1551
+ runOnStart?: boolean;
1552
+ /**
1553
+ * Behavior when a previous run is still in flight at the next fire time.
1554
+ * - `'skip'` → drop the new tick (default).
1555
+ * - `'queue'` → queue ONE pending run; subsequent ticks during the same
1556
+ * pile-up still drop. (We don't do unbounded queuing; that's an
1557
+ * anti-pattern that hides bugs.)
1558
+ */
1559
+ overlap?: 'skip' | 'queue';
1560
+ /** Optional name for logs / introspection. Defaults to the original spec. */
1561
+ name?: string;
1562
+ }
1563
+ /**
1564
+ * A single scheduled cron job. Owns its parsed spec, a `setTimeout`-based
1565
+ * one-shot rescheduler, and bookkeeping for in-flight runs.
1566
+ *
1567
+ * Lifecycle: `start()` arms the first timer; `stop()` cancels it. The
1568
+ * timer is `unref()`'d so a registered cron alone never keeps the event
1569
+ * loop alive — production apps that have an HTTP listener will keep
1570
+ * running normally; standalone scripts will exit when other work finishes.
1571
+ */
1572
+ declare class IngeniumCronJob {
1573
+ readonly name: string;
1574
+ readonly spec: string;
1575
+ readonly timezone: string;
1576
+ readonly overlap: 'skip' | 'queue';
1577
+ private readonly match;
1578
+ private readonly handler;
1579
+ private readonly runOnStart;
1580
+ private timer;
1581
+ private inFlight;
1582
+ private queuedRun;
1583
+ private nextAt;
1584
+ private started;
1585
+ private stopped;
1586
+ constructor(spec: string, opts: CronOptions, handler: CronHandler);
1587
+ /** Arm the scheduler. Idempotent. */
1588
+ start(): void;
1589
+ /** Cancel the scheduler. In-flight runs continue until they naturally finish. */
1590
+ stop(): void;
1591
+ /** Next scheduled fire time, or `null` if not started / stopped. */
1592
+ nextRunAt(): Date | null;
1593
+ /** Are there currently any in-flight invocations of the handler? */
1594
+ isRunning(): boolean;
1595
+ /** @internal Test helper — does this job have its wake timer armed? */
1596
+ hasArmedTimer(): boolean;
1597
+ private scheduleNext;
1598
+ private dispatch;
1599
+ private runOnce;
1600
+ }
1601
+
1602
+ /**
1603
+ * Holds every registered {@link IngeniumCronJob} for an app. Mirrors the shape
1604
+ * of `QueueRegistry` so the integration in `IngeniumApp` is symmetric.
1605
+ *
1606
+ * Cron jobs are NOT auto-started on registration — `startAll()` runs at
1607
+ * compose time so handlers don't fire before the app is ready (e.g. before
1608
+ * `app.decorate()` plugins have wired up `ctx`-style state the handler may
1609
+ * inspect via the registry from another code path).
1610
+ */
1611
+ declare class CronRegistry {
1612
+ private readonly jobs;
1613
+ private started;
1614
+ /** Register a new cron job. Returns the job for advanced introspection. */
1615
+ register(spec: string, opts: CronOptions, handler: CronHandler): IngeniumCronJob;
1616
+ /** Number of registered jobs. */
1617
+ count(): number;
1618
+ /** All registered job names (insertion order). */
1619
+ names(): string[];
1620
+ /** Start every registered job. Idempotent. */
1621
+ startAll(): void;
1622
+ /** Stop every registered job. In-flight handlers continue until they finish. */
1623
+ stopAll(): void;
1624
+ }
1625
+
1626
+ /** Options accepted by `ingenium(...)` and `new IngeniumApp(...)`. */
1627
+ interface IngeniumAppOptions {
1628
+ /** Max number of pooled `IngeniumContext` instances kept in the free list. Default 1024. */
1629
+ poolSize?: number;
1630
+ /** Inject a custom transport (e.g. for tests). Default: `NodeAdapter`. */
1631
+ transport?: Transport;
1632
+ /**
1633
+ * Trust-proxy configuration — controls whether `X-Forwarded-For`,
1634
+ * `X-Forwarded-Proto`, `X-Forwarded-Host` are honored when computing
1635
+ * `ctx.ip`, `ctx.protocol`, `ctx.hostname`. Mirrors Express's
1636
+ * `app.set('trust proxy', ...)` semantics. Default `false` (never trust).
1637
+ * See `proxy/trust.ts` for the full type. Set to `true` only when running
1638
+ * behind a reverse proxy you control.
1639
+ */
1640
+ trustProxy?: TrustProxy;
1641
+ /**
1642
+ * Maximum wall-clock time (ms) for a single request to complete from
1643
+ * dispatch to response. When exceeded, throws `IngeniumTimeoutError(503)`
1644
+ * and the response becomes 503 Service Unavailable.
1645
+ *
1646
+ * The handler that timed out is NOT cancelled — JavaScript can't safely
1647
+ * cancel a Promise. The framework just stops waiting for it; the in-flight
1648
+ * work continues until it naturally completes or the process exits. This
1649
+ * means a slow handler can still leak compute (but not connections or
1650
+ * pool slots, since the response is sent and the context is released).
1651
+ *
1652
+ * Scoped to HTTP request handling only — does NOT apply to upgraded
1653
+ * connections (WebSocket, SSE) which are explicitly long-lived.
1654
+ *
1655
+ * Default: undefined (no timeout). Production deploys SHOULD set this.
1656
+ */
1657
+ requestTimeoutMs?: number;
1658
+ /**
1659
+ * Hard ceiling (bytes) on the total request body, enforced at the
1660
+ * transport layer — applies regardless of which `ctx.body.*` consumer
1661
+ * reads the body, including `ctx.body.stream()`. Defaults to **2 MiB**
1662
+ * (2_097_152) — high enough for typical JSON / form payloads,
1663
+ * low enough that an unauthenticated attacker can't exhaust memory.
1664
+ *
1665
+ * Per-call limits on `ctx.body.json(schema, maxBytes)` etc. are still
1666
+ * honored and apply WITHIN this ceiling. To allow larger uploads on a
1667
+ * specific route, raise this AND use `ctx.body.stream()` with your own
1668
+ * size accounting.
1669
+ *
1670
+ * Set to `Infinity` to disable (NOT recommended outside controlled deploys).
1671
+ */
1672
+ maxRequestBytes?: number;
1673
+ /**
1674
+ * Default queue-drain timeout (ms) used when the listener closes. Per-queue
1675
+ * timeouts can also be passed to `app.queues.drainAll(timeoutMs)`. Default
1676
+ * `10_000`ms — matches `gracefulShutdown`'s default `gracefulTimeoutMs`.
1677
+ */
1678
+ queueDrainTimeoutMs?: number;
1679
+ /**
1680
+ * Secret(s) used to HMAC-sign cookies written via
1681
+ * `ctx.cookies.set(name, value, { signed: true })`.
1682
+ *
1683
+ * Accepts a single string or an array — the FIRST entry signs new cookies,
1684
+ * ALL entries verify reads (so rotating a secret is: prepend the new key,
1685
+ * keep the old key, deploy; remove the old key on the next deploy once all
1686
+ * outstanding signed cookies have expired).
1687
+ *
1688
+ * If omitted, calling `ctx.cookies.set(..., { signed: true })` or
1689
+ * `ctx.cookies.get(..., { signed: true })` throws
1690
+ * `IngeniumError(500, 'COOKIE_SECRET_MISSING')`. The unsigned cookie API
1691
+ * still works without any secrets configured.
1692
+ */
1693
+ cookieSecrets?: string | string[];
1694
+ }
1695
+ /** A user-supplied error handler. Return a non-error or call a `ctx` writer to recover. */
1696
+ type IngeniumErrorHandler = (err: unknown, ctx: IngeniumContext) => unknown | Promise<unknown>;
1697
+ /**
1698
+ * Options for {@link IngeniumApp.inject} — the in-process test client. Mirrors
1699
+ * the shape a real request would carry, minus a socket. `url` is the only
1700
+ * required field; everything else defaults to a bare GET.
1701
+ */
1702
+ interface InjectRequest {
1703
+ /** HTTP method. Defaults to `GET`. */
1704
+ method?: HttpMethod;
1705
+ /** Request URL including any query string (e.g. `/users/42?expand=posts`). */
1706
+ url: string;
1707
+ /**
1708
+ * Request headers. Names are lowercased before they reach the handler so
1709
+ * `ctx.headers['x-custom']` works regardless of the casing passed here
1710
+ * (matching node:http's lowercasing of inbound headers).
1711
+ */
1712
+ headers?: Record<string, string | string[]>;
1713
+ /**
1714
+ * Request body. A `string` / `Buffer` / `Uint8Array` is sent verbatim; a
1715
+ * plain object is JSON-serialized and (unless the caller set a
1716
+ * `content-type`) tagged `application/json`.
1717
+ */
1718
+ body?: string | Buffer$1 | Uint8Array | Record<string, unknown> | unknown[];
1719
+ /** Socket peer address surfaced as `ctx.remoteAddress`. Defaults to `127.0.0.1`. */
1720
+ remoteAddress?: string;
1721
+ }
1722
+ /**
1723
+ * Result of {@link IngeniumApp.inject}. A fully-materialized snapshot of the
1724
+ * response — captured BEFORE the pooled context is released, so reading any
1725
+ * field is safe even though the underlying context has been recycled.
1726
+ */
1727
+ interface InjectResponse {
1728
+ /** Final HTTP status code. */
1729
+ status: number;
1730
+ /**
1731
+ * Response headers, lowercased. A deep-ish copy of the context's header bag
1732
+ * (the array values are cloned) so sequential `inject()` calls never share
1733
+ * or bleed header state.
1734
+ */
1735
+ headers: Record<string, string | string[]>;
1736
+ /**
1737
+ * Response body decoded as a UTF-8 string. Stream responses are drained to
1738
+ * completion first; a bodyless response (`{ kind: 'none' }`) yields `''`.
1739
+ */
1740
+ body: string;
1741
+ /** Parse {@link InjectResponse.body} as JSON. Throws on invalid JSON, like `JSON.parse`. */
1742
+ json<T = unknown>(): T;
1743
+ }
1744
+ /**
1745
+ * Per-route options object accepted as the second positional arg to a verb
1746
+ * registration (`app.get(path, { auth: ['admin'] }, handler)`). Each key must
1747
+ * match a registered declarator (see `app.declare(...)`); the value is passed
1748
+ * to the declarator's factory at REGISTRATION time and the resulting
1749
+ * middleware is prepended to the route's chain.
1750
+ */
1751
+ type RouteOptions = Record<string, unknown>;
1752
+ /**
1753
+ * The Ingenium application. Combines a `Router` (registration journal),
1754
+ * a `RouterTrie` (matched at request time), a context pool, and a
1755
+ * transport. Composition is lazy: the trie's composed handlers are built
1756
+ * on first request (or when `compose()` is called explicitly), and a dirty
1757
+ * bit triggers recomposition if registrations are added later.
1758
+ */
1759
+ declare class IngeniumApp implements PluginTarget {
1760
+ private readonly pool;
1761
+ private readonly transport;
1762
+ private readonly router;
1763
+ private trie;
1764
+ private dirty;
1765
+ private errorHandler;
1766
+ private readonly _hooks;
1767
+ private readonly _decorators;
1768
+ /** @internal Per-route OpenAPI metadata. Keyed by `${method} ${path}`. */
1769
+ private readonly _routeDescriptors;
1770
+ /** @internal Bumped on every `describe()` call so the OpenAPI handler's cache invalidates. */
1771
+ private _routeDescriptorVersion;
1772
+ /** @internal Carried onto each `IngeniumContext` so its `ip`/`protocol`/`hostname` getters can resolve. */
1773
+ private readonly _trustProxy;
1774
+ /** @internal Wall-clock per-request ceiling. `undefined` disables the race entirely. */
1775
+ private readonly _requestTimeoutMs;
1776
+ /**
1777
+ * @internal Hard transport-layer ceiling on request body bytes. Passed to
1778
+ * the transport via `TransportHooks.maxRequestBytes`. `Infinity` disables.
1779
+ */
1780
+ private readonly _maxRequestBytes;
1781
+ /** @internal Background job registry. Workers start at compose() / first request. */
1782
+ private readonly _queues;
1783
+ /** @internal Cron job registry. Timers start at compose() / first request. */
1784
+ private readonly _crons;
1785
+ /** @internal Default queue-drain timeout used when the listener closes. */
1786
+ private readonly _queueDrainTimeoutMs;
1787
+ /**
1788
+ * @internal Frozen list of cookie-signing secrets, normalized from the
1789
+ * scalar/array input. Empty array when not configured. Stamped onto each
1790
+ * dispatched context only when non-empty (per-request field write avoided
1791
+ * for the common case of "no cookie secrets configured").
1792
+ */
1793
+ private readonly _cookieSecrets;
1794
+ /** @internal Whether `cookieSecrets` was configured — caches the truthy check. */
1795
+ private readonly _hasCookieSecrets;
1796
+ /**
1797
+ * @internal Per-request dispatch booleans, recomputed at every `compose()`.
1798
+ * Cached because their underlying `.hasAny()` / `.hasOnXxx()` calls walk
1799
+ * arrays and we don't want to pay that on every request — the registries
1800
+ * are frozen between composes.
1801
+ *
1802
+ * `_handleFast` is the hot-path closure: when ALL of these are off
1803
+ * (`!_hasHooks && !_hasDecorators && !_hasTimeout && !_hasTrustProxy`)
1804
+ * we route through a stripped dispatch that skips every conditional.
1805
+ */
1806
+ private _hasHooks;
1807
+ private _hasOnRequest;
1808
+ private _hasOnResponse;
1809
+ private _hasOnError;
1810
+ private _hasDecorators;
1811
+ private readonly _hasTimeout;
1812
+ private readonly _hasTrustProxy;
1813
+ private _useFastPath;
1814
+ /**
1815
+ * @internal Whether `listen()` has bound a server that hasn't been closed
1816
+ * yet. Guards against the double-listen footgun: a second `listen()` on the
1817
+ * same app would silently bind a second server (two ports dispatching to one
1818
+ * pool), almost always a copy-paste mistake. Cleared when the returned
1819
+ * handle's `close()` resolves, so re-listening after a clean shutdown works.
1820
+ */
1821
+ private _listening;
1822
+ constructor(options?: IngeniumAppOptions);
1823
+ /**
1824
+ * @internal Recompute the cached dispatch booleans. Called at the end of
1825
+ * `compose()` (and `composeAsync()`) so per-request reads are O(1) field
1826
+ * loads instead of `.hasAny()` array scans.
1827
+ *
1828
+ * The fast path is taken when an app uses zero opt-in features beyond the
1829
+ * router itself: no plugins, no decorators, no request timeout, no
1830
+ * trust-proxy. That's the typical "Sinatra-shape" / "Express-shape"
1831
+ * register-routes-and-go app — the case we want to be the absolute fastest.
1832
+ * Note `ctx.queue` registers a decorator at construction, so any app that
1833
+ * uses background jobs is OFF the fast path. That's the correct semantics:
1834
+ * if you want the queue ergonomic, you accept the per-request decorator
1835
+ * apply cost.
1836
+ */
1837
+ private _recomputeDispatchFlags;
1838
+ /** Lifecycle hooks API — plugins call `app.hooks.onRequest(...)` etc. */
1839
+ get hooks(): Hooks;
1840
+ /**
1841
+ * Register a plugin. Plugins are invoked immediately and may be async;
1842
+ * callers should `await app.register(...)` if the plugin returns a Promise.
1843
+ * Plugins must be registered BEFORE `compose()` runs (i.e. before the
1844
+ * first request); registering a plugin sets the dirty bit so the next
1845
+ * request will recompose.
1846
+ */
1847
+ register<O>(plugin: IngeniumPlugin<O>, opts: O): Promise<this>;
1848
+ register(plugin: IngeniumPlugin<void>): Promise<this>;
1849
+ /**
1850
+ * Open a registration scope rooted at `prefix`. Routes, middleware, and
1851
+ * sub-plugins registered through the `scope` parameter inside `registrar`
1852
+ * are automatically prefix-qualified, so a plugin's `use(mw)` only applies
1853
+ * to requests under `prefix` — not the whole app.
1854
+ *
1855
+ * This is the killer feature for sub-app affinity: today, plugins registered
1856
+ * via `app.register(...)` decorate the WHOLE app (their `app.use(mw)` calls
1857
+ * become global). Wrapping a `register(...)` call inside `app.scope(...)`
1858
+ * confines the plugin's middleware to the scope's subtree, without forcing
1859
+ * the user to manually prefix every route.
1860
+ *
1861
+ * The actual prefix-matching happens at compose time: scope translates
1862
+ * `s.use(mw)` into `app.use(prefix, mw)` on the underlying router, which
1863
+ * the existing `flattenRouter` / `RouterTrie` machinery already handles.
1864
+ * Per-request dispatch sees no new work.
1865
+ *
1866
+ * @example
1867
+ * app.scope('/api/v2', (s) => {
1868
+ * s.use(authPlugin) // only runs for /api/v2/*
1869
+ * s.get('/users', listUsers) // registers at /api/v2/users
1870
+ * })
1871
+ *
1872
+ * Nested scopes compose:
1873
+ *
1874
+ * @example
1875
+ * app.scope('/api', (s) => {
1876
+ * s.scope('/v2', (s2) => {
1877
+ * s2.get('/users', listUsers) // /api/v2/users
1878
+ * })
1879
+ * })
1880
+ *
1881
+ * # Caveat — decorators (V1)
1882
+ *
1883
+ * `scope.decorate(...)` decorates EVERY request, not just requests under
1884
+ * `prefix`. Decorators install onto the pooled context at request start,
1885
+ * before the route is matched. The first call from inside any scope emits
1886
+ * a `process.emitWarning` in non-production environments to surface this.
1887
+ * See `ScopedApp` for details.
1888
+ *
1889
+ * @returns `this` for chaining. If `registrar` is async, the returned
1890
+ * promise is NOT awaited — callers who need async plugin registration
1891
+ * inside a scope should await the `registrar` themselves (or use
1892
+ * `scope.register(asyncPlugin)`, which returns a Promise).
1893
+ */
1894
+ scope(prefix: string, registrar: (scope: PluginTarget) => void): this;
1895
+ /**
1896
+ * @internal Mark the compose-cache dirty. Used by `ScopedApp` to invalidate
1897
+ * after registering routes/middleware/plugins inside a scope. External
1898
+ * callers should not depend on this — it's a friend-access seam for the
1899
+ * scope facade.
1900
+ */
1901
+ _markDirty(): void;
1902
+ /**
1903
+ * Add a lazy decorator. The factory is invoked the first time `ctx[name]`
1904
+ * is read; the result is cached on the context for the rest of the request.
1905
+ *
1906
+ * @example
1907
+ * app.decorate('user', async (ctx) => loadUser(ctx.headers.authorization))
1908
+ */
1909
+ decorate<T>(name: string, factory: LazyDecorator<T>): this;
1910
+ /**
1911
+ * Add an eager decorator. The factory runs at the start of every request,
1912
+ * and the value is assigned directly to the context.
1913
+ *
1914
+ * @example
1915
+ * app.decorateRequest('startedAt', () => Date.now())
1916
+ */
1917
+ decorateRequest<T>(name: string, factory: EagerDecorator<T>): this;
1918
+ use(mw: IngeniumMiddleware): this;
1919
+ use(prefix: string, mw: IngeniumMiddleware | Router): this;
1920
+ get(path: string, handler: IngeniumHandler): this;
1921
+ get(path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1922
+ post(path: string, handler: IngeniumHandler): this;
1923
+ post(path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1924
+ put(path: string, handler: IngeniumHandler): this;
1925
+ put(path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1926
+ patch(path: string, handler: IngeniumHandler): this;
1927
+ patch(path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1928
+ delete(path: string, handler: IngeniumHandler): this;
1929
+ delete(path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1930
+ head(path: string, handler: IngeniumHandler): this;
1931
+ head(path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1932
+ options(path: string, handler: IngeniumHandler): this;
1933
+ options(path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1934
+ /**
1935
+ * Chainable per-path registration. Returns a `RouteBuilder` that stacks
1936
+ * verb registrations on the same path without retyping it:
1937
+ *
1938
+ * @example
1939
+ * app
1940
+ * .route('/users/:id')
1941
+ * .get((ctx) => loadUser(ctx.params.id))
1942
+ * .put(requireAdmin, (ctx) => updateUser(ctx))
1943
+ * .delete(requireAdmin, (ctx) => deleteUser(ctx))
1944
+ *
1945
+ * Pure registration sugar — every call delegates to `app.method(...)`, so
1946
+ * declarative options, inline middleware, and typed params via
1947
+ * `ExtractParams<P>` work exactly as they do on the bare verb form.
1948
+ */
1949
+ route<P extends string>(path: P): RouteBuilder<P>;
1950
+ /**
1951
+ * Register a route under any HTTP method. Accepts the variadic shape with
1952
+ * an optional declarative-options object as the first arg (after `path`).
1953
+ */
1954
+ method(method: HttpMethod, path: string, handler: IngeniumHandler): this;
1955
+ method(method: HttpMethod, path: string, optsOrFirst: RouteOptions | IngeniumMiddleware, ...rest: [...IngeniumMiddleware[], IngeniumHandler]): this;
1956
+ /**
1957
+ * @internal Registry of declarators. Looked up at REGISTRATION time, not
1958
+ * request time — so unknown-declarator errors fire eagerly and the per-
1959
+ * request hot path stays clean.
1960
+ */
1961
+ private readonly _declarators;
1962
+ /**
1963
+ * Register a declarator: a name → middleware-factory mapping. When a route
1964
+ * registration includes an options object with that name as a key, the value
1965
+ * is passed to the factory and the resulting middleware is composed into
1966
+ * the route's chain (in the same position as positional inline middleware,
1967
+ * but BEFORE any positional middleware on the same call).
1968
+ *
1969
+ * Declarators are global to the app and survive across all subsequent route
1970
+ * registrations until overridden by a second `declare(name, ...)` call.
1971
+ * Lookup is REGISTRATION-time: a route registered before the matching
1972
+ * `declare(...)` call throws at registration with a clear hint, not at
1973
+ * request time. This trades flexibility for debuggability — the error is
1974
+ * caught at boot, not under load.
1975
+ *
1976
+ * @example
1977
+ * app.declare('auth', (roles: string[]) => requireRoles(roles))
1978
+ * app.declare('rateLimit', (spec: string) => parseRateLimitSpec(spec))
1979
+ * app.get('/admin', { auth: ['admin'], rateLimit: '10/min' }, handler)
1980
+ */
1981
+ declare<O>(name: string, factory: (opts: O) => IngeniumMiddleware): this;
1982
+ /**
1983
+ * @internal Resolve a route's options object into a list of middleware by
1984
+ * looking up each key in the declarator registry. Iterates keys in object
1985
+ * insertion order (ES2015+ guarantee for string keys). Throws at REG TIME
1986
+ * with a contextual message when a key has no registered declarator.
1987
+ */
1988
+ private translateRouteOptions;
1989
+ /** Register a global error handler. Re-throw to delegate to the default boundary. */
1990
+ onError(handler: IngeniumErrorHandler): this;
1991
+ /**
1992
+ * Register a `before` filter — runs BEFORE the route handler resolves.
1993
+ * The user writes only the body; `await next()` is called automatically
1994
+ * after it returns. If the body writes a response (e.g. `ctx.json(...)`)
1995
+ * the chain short-circuits and the route handler does not run.
1996
+ *
1997
+ * - `before(handler)` — runs for every request (global).
1998
+ * - `before(pattern, handler)` — boundary-respecting prefix match
1999
+ * (`/admin/*` and `/admin` both match `/admin` and `/admin/x`, neither
2000
+ * matches `/administrator`). See `sinatra/filters.ts` for details.
2001
+ */
2002
+ before(handler: IngeniumMiddleware): this;
2003
+ before(pattern: string, handler: IngeniumMiddleware): this;
2004
+ /**
2005
+ * Register an `after` filter — runs AFTER the route handler resolves but
2006
+ * BEFORE the adapter writes the response to the wire. The filter sees the
2007
+ * final ctx state (status, headers, body buffer) and may inspect or
2008
+ * augment it. Errors thrown by the filter propagate to the error boundary.
2009
+ *
2010
+ * - `after(handler)` — runs for every request (global).
2011
+ * - `after(pattern, handler)` — same prefix semantics as `before`.
2012
+ */
2013
+ after(handler: IngeniumMiddleware): this;
2014
+ after(pattern: string, handler: IngeniumMiddleware): this;
2015
+ /**
2016
+ * Attach OpenAPI metadata to a route. The route must be registered separately
2017
+ * via `app.get/post/...`. Multiple calls MERGE shallowly onto the existing
2018
+ * descriptor for the same `(method, path)` pair — later keys overwrite
2019
+ * earlier ones, but keys absent from `meta` are preserved. This is what lets
2020
+ * the inline-options form (`app.get(p, { tags: [...] }, h)`) coexist with a
2021
+ * later explicit `app.describe(method, p, { summary })` without one wiping
2022
+ * the other. Reads via `generateOpenApi(app)`.
2023
+ */
2024
+ describe(method: HttpMethod, path: string, meta: RouteDescriptor): this;
2025
+ /** @internal Read-only view of route descriptors — used by the OpenAPI generator. */
2026
+ get routeDescriptors(): ReadonlyMap<string, RouteDescriptor>;
2027
+ /** @internal Bumps on every `describe()` call so the OpenAPI handler can cache-bust. */
2028
+ get routeDescriptorVersion(): number;
2029
+ /** @internal Read-only view of the registration journal — used by the OpenAPI generator. */
2030
+ get routerJournal(): Router;
2031
+ /**
2032
+ * Register a background queue with a worker. The worker pool starts when
2033
+ * the app is composed (first request, `app.compose()`, or `app.listen()`).
2034
+ *
2035
+ * @example
2036
+ * app.queue<{ to: string; body: string }>('emails',
2037
+ * { concurrency: 4, retries: 5 },
2038
+ * async (job) => sendEmail(job.data),
2039
+ * )
2040
+ *
2041
+ * // From a route handler:
2042
+ * app.post('/signup', async (ctx) => {
2043
+ * const u = await createUser(await ctx.body.json())
2044
+ * await ctx.queue('emails').add({ to: u.email, body: 'Welcome!' })
2045
+ * return { ok: true }
2046
+ * })
2047
+ */
2048
+ queue<TData>(name: string, opts: QueueOptions<TData>, worker: QueueWorker<TData>): this;
2049
+ queue<TData>(name: string, worker: QueueWorker<TData>): this;
2050
+ /**
2051
+ * Register a cron job. Spec is a 5-field crontab string. See
2052
+ * `src/cron/parser.ts` for the supported grammar.
2053
+ *
2054
+ * @example
2055
+ * app.cron('0 *\/15 * * *', () => refreshCaches()) // every 15 min
2056
+ * app.cron('0 0 * * 0', { timezone: 'America/Los_Angeles' }, weeklyReport)
2057
+ */
2058
+ cron(spec: string, handler: CronHandler): this;
2059
+ cron(spec: string, opts: CronOptions, handler: CronHandler): this;
2060
+ /** @internal Read-only access to the queue registry for ops/test introspection. */
2061
+ get queues(): QueueRegistry;
2062
+ /** @internal Read-only access to the cron registry for ops/test introspection. */
2063
+ get crons(): CronRegistry;
2064
+ /**
2065
+ * Walk the registration journal and rebuild the trie with composed
2066
+ * handlers at every leaf. Auto-runs on first request; safe to call
2067
+ * explicitly to pre-warm.
2068
+ */
2069
+ /** Cached flat registrations — used to build the on-miss fallback chain. */
2070
+ private _flat;
2071
+ compose(): void;
2072
+ /**
2073
+ * Async composition entry — runs `onCompose` hooks first, then composes.
2074
+ * Used by `handle()` and `listen()` when there are async pre-compose
2075
+ * listeners. Sync-only composition still works via `compose()` above.
2076
+ */
2077
+ private composeAsync;
2078
+ /**
2079
+ * Dispatch a single context through the framework. Handles route lookup,
2080
+ * 404/405 generation, and the error boundary. The transport is responsible
2081
+ * for populating the request side of the context and writing the response
2082
+ * side after this resolves.
2083
+ */
2084
+ handle(ctx: IngeniumContext): Promise<void>;
2085
+ /**
2086
+ * Run global + path-matching scoped middleware as a fallback chain when the
2087
+ * trie has no matching route. The terminal handler re-throws the original
2088
+ * trie miss so the error boundary still produces 404/405 if no middleware
2089
+ * wrote the response. Composed per-request — misses are exceptional.
2090
+ */
2091
+ private runFallback;
2092
+ private handleError;
2093
+ /**
2094
+ * Dispatch a synthetic request through the SAME path as the wire — no
2095
+ * socket, no transport. Acquires a pooled context, populates it exactly as
2096
+ * the Node adapter would from an `IncomingMessage`, runs `handle()` (so
2097
+ * routing, 404/405, middleware, hooks, and the error boundary all behave
2098
+ * identically), then materializes the response into a plain {@link InjectResponse}.
2099
+ *
2100
+ * Crucially the context is extracted FULLY and only THEN released back to
2101
+ * the pool: releasing bumps `_epoch` and reassigns `_headers`/`_body`, so
2102
+ * reading them after release would surface the next request's state (or
2103
+ * empty). The returned object holds copies, so it survives the recycle and
2104
+ * sequential `inject()` calls never bleed header/body state into each other.
2105
+ *
2106
+ * @example
2107
+ * const res = await app.inject({ method: 'POST', url: '/users', body: { name: 'a' } })
2108
+ * expect(res.status).toBe(201)
2109
+ * expect(res.json<{ id: string }>().id).toBeDefined()
2110
+ */
2111
+ inject(opts: InjectRequest): Promise<InjectResponse>;
2112
+ /** Bind a port and accept requests. Returns a handle for graceful shutdown. */
2113
+ listen(port: number, host?: string): Promise<ListeningServer>;
2114
+ }
2115
+ /**
2116
+ * Factory function. Mirrors the `express()` ergonomics — `ingenium(...)` returns
2117
+ * a new app, and the function carries the body-parser middleware factories
2118
+ * plus a `Router` constructor as static properties.
2119
+ */
2120
+ interface IngeniumFactory {
2121
+ (options?: IngeniumAppOptions): IngeniumApp;
2122
+ Router: () => Router;
2123
+ }
2124
+
2125
+ /**
2126
+ * Express compatibility shim. In Ingenium, body parsing is lazy via
2127
+ * `ctx.body.json()` / `ctx.body.urlencoded()` / `ctx.body.text()` — there is
2128
+ * no parse-on-every-request middleware to register. This factory exists so
2129
+ * existing `app.use(express.json())` migration patterns keep compiling and
2130
+ * reading naturally; the returned middleware is a zero-cost no-op.
2131
+ *
2132
+ * If you need to enforce a default `maxBytes` across all body access, set it
2133
+ * via the `limit` option here and read it inside your handlers when calling
2134
+ * `ctx.body.json({ limit })` — Ingenium doesn't store it implicitly.
2135
+ *
2136
+ * @returns a no-op middleware
2137
+ */
2138
+ declare function jsonMiddleware(_opts?: {
2139
+ limit?: number;
2140
+ }): IngeniumMiddleware;
2141
+ /**
2142
+ * See `jsonMiddleware` — same rationale. URL-encoded parsing is lazy via
2143
+ * `ctx.body.urlencoded()`.
2144
+ */
2145
+ declare function urlencodedMiddleware(_opts?: {
2146
+ limit?: number;
2147
+ }): IngeniumMiddleware;
2148
+
2149
+ /**
2150
+ * Options for the `ingenium.static` middleware.
2151
+ */
2152
+ interface StaticOptions {
2153
+ /**
2154
+ * The file to serve when a directory is requested. Set to `false` to
2155
+ * disable directory-index resolution. Default: `'index.html'`.
2156
+ */
2157
+ index?: string | false;
2158
+ /**
2159
+ * `Cache-Control: max-age=<seconds>` to set on served files, in
2160
+ * MILLISECONDS (Express convention). Default: `0` (no caching).
2161
+ */
2162
+ maxAge?: number;
2163
+ /**
2164
+ * Extensions to try (in order) when the requested path doesn't exist.
2165
+ * For example, `['html']` lets `/about` resolve to `/about.html`.
2166
+ * Default: `[]` (off).
2167
+ */
2168
+ extensions?: string[];
2169
+ /**
2170
+ * How to treat files / directories whose name starts with `.`:
2171
+ * - `'allow'` — serve normally
2172
+ * - `'deny'` — respond with 403
2173
+ * - `'ignore'` — call `next()` (let routes 404 it). DEFAULT.
2174
+ */
2175
+ dotfiles?: 'allow' | 'deny' | 'ignore';
2176
+ }
2177
+
2178
+ /**
2179
+ * Static-file middleware. Serves files from `root`, supporting directory
2180
+ * indexes, weak ETags, `If-None-Match`, byte-range requests, and basic
2181
+ * dotfile policy. Misses (file not found) call `next()` — they do NOT
2182
+ * write 404 themselves, so downstream routes still get a chance.
2183
+ *
2184
+ * @example
2185
+ * app.use(ingenium.static('./public', { maxAge: 60_000 }))
2186
+ */
2187
+ declare function staticMiddleware(root: string, opts?: StaticOptions): IngeniumMiddleware;
2188
+
2189
+ /**
2190
+ * Function form of the `origin` option. Receives the request's `Origin`
2191
+ * header value (always a string — never called when no `Origin` is present)
2192
+ * and the active `IngeniumContext`. May return:
2193
+ *
2194
+ * - `true` — allow the request, reflect the request's `Origin` back.
2195
+ * - `false` — deny the request (no `Access-Control-Allow-Origin` header set).
2196
+ * - `string` — allow the request, use this exact value as the
2197
+ * `Access-Control-Allow-Origin` header (use `'*'` for the wildcard).
2198
+ *
2199
+ * May be sync or async.
2200
+ */
2201
+ type CorsOriginFn = (origin: string, ctx: IngeniumContext) => boolean | string | Promise<boolean | string>;
2202
+ /**
2203
+ * Spec for the `origin` option.
2204
+ *
2205
+ * - `boolean` — `true` reflects any request `Origin`; `false` disables CORS.
2206
+ * - `'*'` — wildcard: `Access-Control-Allow-Origin: *`.
2207
+ * - any other `string` — exact match against the request's `Origin`.
2208
+ * - `string[]` — allowlist; matched exactly.
2209
+ * - `RegExp` — tested against the request's `Origin`.
2210
+ * - `CorsOriginFn` — fully custom predicate (see above).
2211
+ */
2212
+ type CorsOrigin = boolean | string | string[] | RegExp | CorsOriginFn;
2213
+ /**
2214
+ * Options for `ingenium.cors`. All fields are optional. See README for details.
2215
+ */
2216
+ interface CorsOptions {
2217
+ /** Origin policy. Default: `'*'`. */
2218
+ origin?: CorsOrigin;
2219
+ /**
2220
+ * Methods advertised on `Access-Control-Allow-Methods` for preflight.
2221
+ * Default: `['GET','HEAD','PUT','PATCH','POST','DELETE']`.
2222
+ */
2223
+ methods?: string[];
2224
+ /**
2225
+ * Headers advertised on `Access-Control-Allow-Headers` for preflight.
2226
+ * If `undefined`, the value of `Access-Control-Request-Headers` from the
2227
+ * preflight request is mirrored back. Default: `undefined`.
2228
+ */
2229
+ allowedHeaders?: string[];
2230
+ /**
2231
+ * Headers advertised on `Access-Control-Expose-Headers` for simple
2232
+ * responses. Default: `undefined` (header omitted).
2233
+ */
2234
+ exposedHeaders?: string[];
2235
+ /**
2236
+ * If `true`, sets `Access-Control-Allow-Credentials: true`.
2237
+ * Incompatible with `origin: '*'` — throws at construction time.
2238
+ * Default: `false`.
2239
+ */
2240
+ credentials?: boolean;
2241
+ /**
2242
+ * `Access-Control-Max-Age` (seconds). Default: `undefined` (header omitted).
2243
+ */
2244
+ maxAge?: number;
2245
+ /**
2246
+ * Status code for successful preflight responses. Default: `204`.
2247
+ */
2248
+ optionsSuccessStatus?: number;
2249
+ }
2250
+
2251
+ /**
2252
+ * CORS middleware. Implements the standard CORS protocol (Fetch spec
2253
+ * §3.2.4) for both simple requests and preflight (`OPTIONS` +
2254
+ * `Access-Control-Request-Method`).
2255
+ *
2256
+ * @example
2257
+ * app.use(ingenium.cors())
2258
+ * app.use(ingenium.cors({ origin: ['https://app.example.com'], credentials: true }))
2259
+ */
2260
+ declare function corsMiddleware(opts?: CorsOptions): IngeniumMiddleware;
2261
+
2262
+ /**
2263
+ * A single Server-Sent Event. The `data` field is required; if you pass an
2264
+ * object, it's `JSON.stringify`'d before being written. All other fields are
2265
+ * optional and serialized per the EventSource specification.
2266
+ *
2267
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
2268
+ */
2269
+ interface SseEvent {
2270
+ /** Payload. Strings are written verbatim; objects are JSON-encoded. */
2271
+ data: string | object;
2272
+ /** Optional event name — populates `event:` field. */
2273
+ event?: string;
2274
+ /** Optional event id — populates `id:` field. */
2275
+ id?: string;
2276
+ /** Optional retry hint in milliseconds — populates `retry:` field. */
2277
+ retry?: number;
2278
+ }
2279
+ /**
2280
+ * Handle for an open SSE connection. Returned by {@link sse}. Use `send()`
2281
+ * to push events, `comment()` for keep-alive frames, and `close()` to end
2282
+ * the response stream cleanly.
2283
+ */
2284
+ interface SseStream {
2285
+ /**
2286
+ * Send a single event. A bare string is treated as `{ data: <string> }`.
2287
+ */
2288
+ send(event: SseEvent | string): void;
2289
+ /** Write a comment line (`: <text>`). Useful for heartbeats / keep-alive. */
2290
+ comment(text: string): void;
2291
+ /** End the response stream. Subsequent calls are no-ops. */
2292
+ close(): void;
2293
+ /** Whether the underlying stream has been closed (locally or by the client). */
2294
+ readonly closed: boolean;
2295
+ }
2296
+ /**
2297
+ * Open a Server-Sent Events response on the given context. Sets the
2298
+ * appropriate headers (`Content-Type: text/event-stream`, no caching, no
2299
+ * proxy buffering) and wires a `PassThrough` into `ctx.stream()`.
2300
+ *
2301
+ * @example
2302
+ * app.get('/events', (ctx) => {
2303
+ * const stream = sse(ctx)
2304
+ * stream.send({ event: 'hello', data: { msg: 'world' } })
2305
+ * setTimeout(() => stream.close(), 1000)
2306
+ * })
2307
+ */
2308
+ declare function sse(ctx: IngeniumContext): SseStream;
2309
+
2310
+ /**
2311
+ * Pluggable backing store for the rate-limit middleware. The default
2312
+ * in-memory implementation is sync internally but exposes a Promise-based
2313
+ * surface so a Redis (or other distributed) store can drop in unchanged.
2314
+ */
2315
+ interface RateLimitStore {
2316
+ /**
2317
+ * Record a hit for `key`. Returns the new count and the unix-millis
2318
+ * timestamp at which the current window expires.
2319
+ *
2320
+ * Implementations MUST roll the window over when `Date.now() >= resetAt`,
2321
+ * resetting the count to 1.
2322
+ */
2323
+ hit(key: string, windowMs: number): Promise<{
2324
+ count: number;
2325
+ resetAt: number;
2326
+ }>;
2327
+ /** Clear the counter for `key`. Used by tests and by ops tooling. */
2328
+ reset(key: string): Promise<void>;
2329
+ }
2330
+ /**
2331
+ * Options for {@link rateLimit}.
2332
+ */
2333
+ interface RateLimitOptions {
2334
+ /**
2335
+ * Window length in milliseconds. Default: `60_000` (one minute).
2336
+ *
2337
+ * Each key is allowed at most `max` requests per window. Counts reset
2338
+ * sharply at window boundaries (fixed-window algorithm).
2339
+ */
2340
+ windowMs?: number;
2341
+ /** Max requests per `windowMs` per key. Default: `100`. */
2342
+ max?: number;
2343
+ /**
2344
+ * Build the limiter key for a request. Default uses `X-Forwarded-For`
2345
+ * (first hop), then `X-Real-IP`, then the literal string `'unknown'`.
2346
+ *
2347
+ * **Security**: the default trusts `X-Forwarded-For` blindly. Without
2348
+ * an upstream that strips client-supplied values, this header is
2349
+ * forgeable. Production deployments behind a proxy should validate the
2350
+ * proxy chain or supply a custom `keyGenerator`.
2351
+ */
2352
+ keyGenerator?: (ctx: IngeniumContext) => string;
2353
+ /**
2354
+ * Skip rate-limiting for a given request. When this returns `true`, no
2355
+ * counter hit is recorded and no `X-RateLimit-*` headers are written.
2356
+ * Default: never skip.
2357
+ */
2358
+ skip?: (ctx: IngeniumContext) => boolean;
2359
+ /**
2360
+ * Backing store. Default: an in-process {@link MemoryStore}. Swap in a
2361
+ * shared store (Redis etc.) when running multiple replicas.
2362
+ */
2363
+ store?: RateLimitStore;
2364
+ }
2365
+
2366
+ /**
2367
+ * Fixed-window rate-limiting middleware. Each key is allowed at most `max`
2368
+ * requests per `windowMs`. Over-limit requests get a `429 Too Many
2369
+ * Requests` response with `Retry-After` and a JSON body.
2370
+ *
2371
+ * Every passing response carries `X-RateLimit-Limit`,
2372
+ * `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (unix seconds).
2373
+ *
2374
+ * @example
2375
+ * app.use(rateLimit({ max: 100, windowMs: 60_000 }))
2376
+ * app.use('/auth', rateLimit({ max: 5, windowMs: 60_000 }))
2377
+ */
2378
+ declare function rateLimit(opts?: RateLimitOptions): IngeniumMiddleware;
2379
+
2380
+ /**
2381
+ * Base error class for all framework-emitted errors. Errors that extend
2382
+ * `IngeniumError` are caught by the global error boundary and serialized to the
2383
+ * client according to their `statusCode` and `code`.
2384
+ */
2385
+ declare class IngeniumError extends Error {
2386
+ readonly statusCode: number;
2387
+ readonly code: string;
2388
+ readonly cause?: unknown | undefined;
2389
+ /**
2390
+ * @param statusCode HTTP status code to send to the client.
2391
+ * @param code Machine-readable error code (UPPER_SNAKE_CASE convention).
2392
+ * @param message Human-readable error message.
2393
+ * @param cause Optional underlying error.
2394
+ */
2395
+ constructor(statusCode: number, code: string, message: string, cause?: unknown | undefined);
2396
+ }
2397
+ /** 404 — no route matched. */
2398
+ declare class IngeniumNotFoundError extends IngeniumError {
2399
+ constructor(message?: string);
2400
+ }
2401
+ /** 401 — authentication required or invalid. */
2402
+ declare class IngeniumUnauthorizedError extends IngeniumError {
2403
+ constructor(message?: string);
2404
+ }
2405
+ /**
2406
+ * 405 — path matched but method did not. Includes the list of allowed methods,
2407
+ * which the framework writes into the `Allow` response header automatically.
2408
+ */
2409
+ declare class IngeniumMethodNotAllowedError extends IngeniumError {
2410
+ readonly allowed: readonly string[];
2411
+ constructor(allowed: readonly string[], message?: string);
2412
+ }
2413
+ /** 413 — request body exceeded the configured `maxBytes` limit. */
2414
+ declare class IngeniumPayloadTooLargeError extends IngeniumError {
2415
+ constructor(message?: string);
2416
+ }
2417
+ /**
2418
+ * 422 — request body parsed successfully but failed validation. The `fields`
2419
+ * map is serialized into the response body so clients can render field-level
2420
+ * error messages.
2421
+ */
2422
+ declare class IngeniumValidationError extends IngeniumError {
2423
+ readonly fields: Record<string, string>;
2424
+ constructor(fields: Record<string, string>, message?: string);
2425
+ }
2426
+ /** 400 — request was malformed (bad JSON, invalid content-type, etc). */
2427
+ declare class IngeniumBadRequestError extends IngeniumError {
2428
+ constructor(message?: string, cause?: unknown);
2429
+ }
2430
+ /**
2431
+ * 500 — caller attempted to write a header name or value containing CR or
2432
+ * LF. Node would eventually reject these at the wire level, but the late
2433
+ * throw produces a useless stack — we fail fast at the call site so the
2434
+ * offending header (and the route that set it) shows up in the trace.
2435
+ */
2436
+ declare class IngeniumHeaderInjectionError extends IngeniumError {
2437
+ constructor(message?: string);
2438
+ }
2439
+ /**
2440
+ * 500 — `ctx.json` (or `respondJsonWithEtag`) was handed a value that
2441
+ * `JSON.stringify` cannot serialize: a circular structure, a `BigInt`, or
2442
+ * any other unsupported shape. The original `TypeError` is attached as
2443
+ * `cause` and emitted via `process.emitWarning` for diagnostics.
2444
+ */
2445
+ declare class IngeniumUnserializableError extends IngeniumError {
2446
+ constructor(message: string, cause?: unknown);
2447
+ }
2448
+ /**
2449
+ * Sinatra-style `halt` short-circuit. Thrown by `ctx.halt(status, body?)`;
2450
+ * caught by the default error boundary and serialized according to `bodyShape`:
2451
+ *
2452
+ * - `'none'` → boundary uses default `{ error, code: 'HALT' }` JSON shape.
2453
+ * - `'text'` → boundary writes `body` as `text/plain` verbatim.
2454
+ * - `'json'` → boundary writes `body` as `application/json`.
2455
+ *
2456
+ * The body shape is decided at the call site (string ⇒ text, object ⇒ json,
2457
+ * undefined ⇒ none) so the boundary can branch without re-inspecting types.
2458
+ * Custom `app.onError` handlers still receive the error and can override it
2459
+ * (e.g. add a header, reshape the body) by writing the response themselves.
2460
+ */
2461
+ declare class IngeniumHaltError extends IngeniumError {
2462
+ /** What the default error boundary should do with `body`. */
2463
+ readonly bodyShape: 'none' | 'text' | 'json';
2464
+ /** The body argument from `ctx.halt(status, body?)`. */
2465
+ readonly body: string | Record<string, unknown> | undefined;
2466
+ constructor(statusCode: number, body?: string | Record<string, unknown>);
2467
+ }
2468
+ /**
2469
+ * 503 — handler exceeded the configured `requestTimeoutMs` ceiling. The
2470
+ * orphaned handler is NOT cancelled (JavaScript can't safely cancel a
2471
+ * Promise); the framework just stops waiting for it. Late writes from the
2472
+ * orphaned handler are guarded by the per-request epoch counter on the
2473
+ * context and discarded with a `process.emitWarning`.
2474
+ */
2475
+ declare class IngeniumTimeoutError extends IngeniumError {
2476
+ constructor(timeoutMs: number, message?: string);
2477
+ }
2478
+
2479
+ /**
2480
+ * Where the CSRF token lives between requests.
2481
+ *
2482
+ * - `'cookie'` (default): double-submit cookie pattern. Token is generated
2483
+ * on safe requests, written to a non-HttpOnly cookie, and the client must
2484
+ * echo it back via a header on unsafe requests. No session required.
2485
+ * - `'session'`: synchronizer pattern. Token is stored on `ctx.session`
2486
+ * and validated against the submitted token on unsafe requests. Requires
2487
+ * `sessionMiddleware` to run before this middleware.
2488
+ */
2489
+ type CsrfStorage = 'cookie' | 'session';
2490
+ /** How to extract the submitted token from an incoming request. */
2491
+ type CsrfValueReader = (ctx: IngeniumContext) => string | undefined | Promise<string | undefined>;
2492
+ interface CsrfCookieOptions {
2493
+ /** Cookie name. Default `ingenium.csrf`. */
2494
+ name?: string;
2495
+ /** Restrict cookie to a single subpath. Default `/`. */
2496
+ path?: string;
2497
+ /** Restrict cookie to a domain. Default unset. */
2498
+ domain?: string;
2499
+ /** SameSite policy. Default `'lax'`. */
2500
+ sameSite?: 'lax' | 'strict' | 'none';
2501
+ /** Mark cookie Secure. Default `false`; set `true` behind TLS. */
2502
+ secure?: boolean;
2503
+ /**
2504
+ * Mark cookie HttpOnly. **Default `false`** — clients must read the cookie
2505
+ * to copy the value into the request header. Setting `true` would break the
2506
+ * double-submit pattern; only enable with a custom value reader that pulls
2507
+ * the token from elsewhere.
2508
+ */
2509
+ httpOnly?: boolean;
2510
+ /** Cookie max-age (seconds). Default 7 days. */
2511
+ maxAgeSeconds?: number;
2512
+ }
2513
+ interface CsrfOptions {
2514
+ /**
2515
+ * HMAC secret used to sign the token. Required for the cookie storage
2516
+ * mode (signed double-submit). For session storage the secret is optional
2517
+ * — the session id already authenticates the binding.
2518
+ */
2519
+ secret?: string | string[];
2520
+ /** Token storage strategy. Default `'cookie'`. */
2521
+ storage?: CsrfStorage;
2522
+ /** Cookie options when `storage === 'cookie'`. */
2523
+ cookie?: CsrfCookieOptions;
2524
+ /** Methods that bypass validation. Default `['GET', 'HEAD', 'OPTIONS', 'TRACE']`. */
2525
+ ignoreMethods?: readonly string[];
2526
+ /**
2527
+ * How to extract the submitted token. Default reads (in order):
2528
+ * 1. `X-CSRF-Token` header
2529
+ * 2. `X-XSRF-Token` header (Angular convention)
2530
+ * 3. `_csrf` query string parameter
2531
+ */
2532
+ value?: CsrfValueReader;
2533
+ /**
2534
+ * Per-request opt-out. Return `true` to skip validation entirely for
2535
+ * this request (and skip token issuance).
2536
+ */
2537
+ skip?: (ctx: IngeniumContext) => boolean | Promise<boolean>;
2538
+ }
2539
+
2540
+ /** 403 Forbidden — CSRF token missing or mismatched. */
2541
+ declare class IngeniumCsrfError extends IngeniumError {
2542
+ constructor(message?: string);
2543
+ }
2544
+ /**
2545
+ * CSRF protection middleware. Two modes:
2546
+ *
2547
+ * - `storage: 'cookie'` (default) — double-submit cookie pattern. A
2548
+ * randomly-generated token is HMAC-signed, written to a non-HttpOnly
2549
+ * cookie on safe requests, and the client must echo the cookie value
2550
+ * back in a header (`X-CSRF-Token`) on unsafe requests. The signature
2551
+ * prevents client-side forgery; the same-origin policy prevents
2552
+ * cross-origin sites from reading the cookie.
2553
+ *
2554
+ * - `storage: 'session'` — synchronizer pattern. The token is stored on
2555
+ * `ctx.session` and matched against the submitted token. Requires
2556
+ * `sessionMiddleware` to run before this middleware.
2557
+ *
2558
+ * Use `ctx.state.csrfToken` (or call `(ctx as IngeniumContext & { csrfToken(): string }).csrfToken()`)
2559
+ * to read the current token to embed in HTML forms or send to a JS client.
2560
+ */
2561
+ declare function csrfMiddleware(opts?: CsrfOptions): IngeniumMiddleware;
2562
+
2563
+ /**
2564
+ * RFC 7807 Problem Details for HTTP APIs.
2565
+ *
2566
+ * The five standard members are reserved by the spec; arbitrary additional
2567
+ * extension members are permitted via the index signature. See
2568
+ * https://datatracker.ietf.org/doc/html/rfc7807#section-3.1.
2569
+ */
2570
+ interface ProblemDetails {
2571
+ /**
2572
+ * A URI reference that identifies the problem type. When dereferenced it
2573
+ * SHOULD provide human-readable documentation. Default per spec is
2574
+ * `'about:blank'`, indicating that no specific problem-type URL exists
2575
+ * (in which case `title` is conventionally the HTTP status reason phrase).
2576
+ */
2577
+ type: string;
2578
+ /**
2579
+ * A short, human-readable summary of the problem type. SHOULD NOT change
2580
+ * from occurrence to occurrence (use `detail` for instance-specific info).
2581
+ */
2582
+ title: string;
2583
+ /** HTTP status code generated by the origin server. */
2584
+ status: number;
2585
+ /** Human-readable explanation specific to this occurrence of the problem. */
2586
+ detail?: string;
2587
+ /** A URI reference identifying the specific occurrence of the problem. */
2588
+ instance?: string;
2589
+ /** RFC 7807 permits arbitrary extension members. */
2590
+ [key: string]: unknown;
2591
+ }
2592
+ /** Options accepted by `ingenium.problemDetails(...)`. */
2593
+ interface ProblemDetailsOptions {
2594
+ /**
2595
+ * Prefix used when constructing the `type` URI from an error's `code`.
2596
+ * Example: `'https://api.example.com/errors/'` + `NOT_FOUND` →
2597
+ * `'https://api.example.com/errors/not-found'`.
2598
+ *
2599
+ * Default `'about:blank'` (per spec — no problem-specific docs URL).
2600
+ */
2601
+ typeBaseUrl?: string;
2602
+ /**
2603
+ * If true, attaches the error's `stack` as an extension member. Useful in
2604
+ * development; never enable in production — stack traces leak source paths
2605
+ * and internal structure. Default `false`.
2606
+ */
2607
+ includeStack?: boolean;
2608
+ /**
2609
+ * Override how the `instance` URI is derived. Default returns `ctx.path`.
2610
+ * Return `undefined` to omit the field entirely.
2611
+ */
2612
+ instance?: (ctx: IngeniumContext) => string | undefined;
2613
+ }
2614
+ /** Options after defaults have been applied. Internal use. */
2615
+ interface ResolvedProblemDetailsOptions {
2616
+ typeBaseUrl: string;
2617
+ includeStack: boolean;
2618
+ instance: (ctx: IngeniumContext) => string | undefined;
2619
+ }
2620
+
2621
+ /**
2622
+ * RFC 7807 Problem Details middleware. Wraps downstream handlers in a
2623
+ * try/catch and serializes any `IngeniumError` (or unknown error) as
2624
+ * `application/problem+json` instead of the framework's default
2625
+ * `{ error, code, fields? }` shape.
2626
+ *
2627
+ * Composition notes:
2628
+ * - This sits as a regular middleware in front of user handlers, NOT in
2629
+ * place of `app.onError`. If `app.onError` is configured AND it re-throws
2630
+ * (or the user handler throws past the onError), this middleware catches
2631
+ * the error before it reaches the default boundary.
2632
+ * - Composes cleanly with other middleware (e.g. idempotency) — the
2633
+ * try/catch is the only thing it does on the way out.
2634
+ *
2635
+ * @example
2636
+ * app.use(ingenium.problemDetails({
2637
+ * typeBaseUrl: 'https://api.example.com/errors/',
2638
+ * includeStack: process.env.NODE_ENV !== 'production',
2639
+ * }))
2640
+ */
2641
+ declare function problemDetailsMiddleware(opts?: ProblemDetailsOptions): IngeniumMiddleware;
2642
+
2643
+ /**
2644
+ * A frozen snapshot of an outgoing response. Captured AFTER the handler
2645
+ * runs and stored in the idempotency cache for replay on retry.
2646
+ *
2647
+ * `body` is `null` only for empty responses (e.g. 204).
2648
+ */
2649
+ interface CachedResponse {
2650
+ /** HTTP status code from the original response. */
2651
+ statusCode: number;
2652
+ /** Plain header bag (lowercased keys), copied from `ctx._headers`. */
2653
+ headers: Record<string, string | string[]>;
2654
+ /** Serialized body. `null` when the original response had no body. */
2655
+ body: string | Buffer$1 | null;
2656
+ }
2657
+ /**
2658
+ * Pluggable storage for the idempotency cache. Default impl is in-memory;
2659
+ * swap for Redis/etc. when running multiple replicas.
2660
+ */
2661
+ interface IdempotencyStore {
2662
+ /** Returns the cached response for `key` or `null` if missing/expired. */
2663
+ get(key: string): Promise<CachedResponse | null>;
2664
+ /** Persist `value` under `key` for `ttlMs` milliseconds. */
2665
+ set(key: string, value: CachedResponse, ttlMs: number): Promise<void>;
2666
+ /** Remove `key`. Idempotent — does nothing if absent. */
2667
+ delete(key: string): Promise<void>;
2668
+ }
2669
+ /** Options accepted by `ingenium.idempotency(...)`. */
2670
+ interface IdempotencyOptions {
2671
+ /**
2672
+ * Header name carrying the idempotency key. Comparison is
2673
+ * case-insensitive (Node lowercases header names automatically).
2674
+ * Default `'Idempotency-Key'`.
2675
+ */
2676
+ header?: string;
2677
+ /** Backing cache. Default: an in-process `IdempotencyMemoryStore`. */
2678
+ store?: IdempotencyStore;
2679
+ /**
2680
+ * Time-to-live for cached responses, in seconds. After this elapses, the
2681
+ * same key replays nothing and the handler runs again. Default `86400`
2682
+ * (24h) — matches Stripe's documented behavior.
2683
+ */
2684
+ ttlSeconds?: number;
2685
+ /**
2686
+ * Namespace function — distinguishes keys belonging to different callers
2687
+ * so two clients can independently use the same idempotency-key string.
2688
+ * Default uses the `Authorization` header (or `'anon'` when absent).
2689
+ */
2690
+ scope?: (ctx: IngeniumContext) => string;
2691
+ /**
2692
+ * HTTP methods eligible for idempotency caching. Default: `['POST',
2693
+ * 'PATCH', 'DELETE']` — only mutating methods. Safe methods (GET/HEAD/
2694
+ * OPTIONS) and idempotent-by-spec PUT are skipped by default; opt in by
2695
+ * extending this list if you need PUT semantics cached too.
2696
+ */
2697
+ methods?: readonly HttpMethod[];
2698
+ /**
2699
+ * Predicate deciding which response status codes are cacheable. Default:
2700
+ * `(s) => s >= 200 && s < 500` — caches 2xx/3xx/4xx but NOT 5xx, matching
2701
+ * Stripe's documented behavior. The intent: a transient 500 (DB blip,
2702
+ * deploy race) must NOT be cached for the entire TTL — every retry would
2703
+ * replay the same failure and the user would be permanently broken until
2704
+ * the cache expires. Validation errors (4xx) are deterministic and worth
2705
+ * caching. Override to opt in to 5xx caching or tighten further.
2706
+ */
2707
+ cacheable?: (statusCode: number) => boolean;
2708
+ }
2709
+
2710
+ /**
2711
+ * Idempotency-Key middleware (per Stripe / IETF idempotency-key draft).
2712
+ *
2713
+ * Behavior:
2714
+ * - Non-mutating method or missing header → pass through.
2715
+ * - Mutating method WITH header:
2716
+ * 1. Build cache key: `<scope>:<method>:<path>:<idempotency-key>`.
2717
+ * 2. Cache hit → replay the cached (status, headers, body) and set
2718
+ * `Idempotent-Replayed: true`. Handler does NOT run.
2719
+ * 3. Cache miss → run handler. If the response is cacheable (i.e. not a
2720
+ * stream and something was written), persist it under the key with
2721
+ * the configured TTL.
2722
+ * 4. Concurrent in-flight requests for the same key are coordinated via
2723
+ * an in-process Promise map: the second request awaits the first and
2724
+ * replays its result.
2725
+ *
2726
+ * Note: the cache key intentionally does NOT include the request body —
2727
+ * the spec assumes the client guarantees byte-for-byte identical retries,
2728
+ * and reading the body at middleware-entry time would defeat lazy parsing.
2729
+ *
2730
+ * @example
2731
+ * app.use(ingenium.idempotency({
2732
+ * store: new IdempotencyMemoryStore(),
2733
+ * ttlSeconds: 86_400,
2734
+ * }))
2735
+ */
2736
+ declare function idempotencyMiddleware(opts?: IdempotencyOptions): IngeniumMiddleware;
2737
+
2738
+ /**
2739
+ * Supported JWT signing algorithms.
2740
+ *
2741
+ * - `HSxxx` — HMAC with the supplied shared secret.
2742
+ * - `RSxxx` — RSASSA-PKCS1-v1_5 with the supplied RSA public key (PEM / JWK / KeyObject).
2743
+ * - `PSxxx` — RSASSA-PSS (MGF1, salt length = digest length).
2744
+ * - `ESxxx` — ECDSA on P-256 / P-384 / P-521 (raw r||s, NOT DER — per the JWT spec).
2745
+ *
2746
+ * `alg: 'none'` is intentionally absent from this union and is hard-rejected
2747
+ * at the verifier — never accept unsigned tokens, even with an empty allowlist.
2748
+ */
2749
+ type JwtAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' | 'ES256' | 'ES384' | 'ES512' | 'PS256' | 'PS384' | 'PS512';
2750
+ /** Decoded JWT header. The `alg` field is required by the spec. */
2751
+ interface JwtHeader {
2752
+ alg: string;
2753
+ typ?: string;
2754
+ kid?: string;
2755
+ [k: string]: unknown;
2756
+ }
2757
+ /**
2758
+ * A successfully verified JWT. `payload` carries the typed claims object,
2759
+ * `header` carries the decoded protected header, and `raw` is the original
2760
+ * compact-serialization string the client sent (useful for re-emitting).
2761
+ */
2762
+ interface JwtVerified<T = Record<string, unknown>> {
2763
+ header: JwtHeader;
2764
+ payload: T;
2765
+ raw: string;
2766
+ }
2767
+ /** Internal — verifier failure mode. Public surface only ever sees `'Invalid token'`. */
2768
+ type JwtVerifyError = {
2769
+ error: 'malformed';
2770
+ } | {
2771
+ error: 'unsupported_alg';
2772
+ } | {
2773
+ error: 'bad_signature';
2774
+ } | {
2775
+ error: 'expired';
2776
+ } | {
2777
+ error: 'not_yet_valid';
2778
+ } | {
2779
+ error: 'too_old';
2780
+ } | {
2781
+ error: 'aud_mismatch';
2782
+ } | {
2783
+ error: 'iss_mismatch';
2784
+ } | {
2785
+ error: 'kid_unknown';
2786
+ } | {
2787
+ error: 'jwks_fetch_failed';
2788
+ };
2789
+ /**
2790
+ * A single signing/verification key. For HMAC algorithms (HSxxx) this is the
2791
+ * shared secret as a string; for asymmetric algorithms it's the PUBLIC key in
2792
+ * PEM (string / Buffer) or as a pre-built `KeyObject`.
2793
+ *
2794
+ * Wrapping with `{ kid, key }` enables header-based key selection — the
2795
+ * verifier picks the entry whose `kid` matches `header.kid`.
2796
+ */
2797
+ type JwtKey = string | Buffer$1 | KeyObject | {
2798
+ kid: string;
2799
+ key: string | Buffer$1 | KeyObject;
2800
+ };
2801
+ /**
2802
+ * Resolve a per-request key. Receives the decoded JWT header so callers can
2803
+ * implement `kid`-based JWKS-style routing without parsing the token themselves.
2804
+ */
2805
+ type JwtSecretResolver<_T = Record<string, unknown>> = (header: JwtHeader) => JwtKey | Promise<JwtKey>;
2806
+ /** All ways `secret` can be supplied. */
2807
+ type JwtSecret<T = Record<string, unknown>> = JwtKey | JwtKey[] | JwtSecretResolver<T>;
2808
+ /** Signature for pulling the raw compact-serialization out of the request. */
2809
+ type JwtTokenReader = (ctx: IngeniumContext) => string | undefined | Promise<string | undefined>;
2810
+ /** Optional structured logger for redacted verification diagnostics. */
2811
+ type JwtLogger = (event: {
2812
+ reason: string;
2813
+ alg?: string;
2814
+ }) => void;
2815
+ interface JwtOptions<T = Record<string, unknown>> {
2816
+ /**
2817
+ * Verification key material. Accepts:
2818
+ * - A single key (string secret, PEM, Buffer, or `KeyObject`).
2819
+ * - `{ kid, key }` for explicit key-id tagging.
2820
+ * - An array of any of the above (rotation / multi-key — the verifier
2821
+ * picks by `kid` if present, else tries each in order).
2822
+ * - A function `(header) => key | Promise<key>` for fully custom routing.
2823
+ */
2824
+ secret: JwtSecret<T>;
2825
+ /** Allowed signing algorithms. Default `['HS256']`. */
2826
+ algorithms?: readonly JwtAlgorithm[];
2827
+ /** Required `aud` claim. Token's `aud` must match (or include) one of these. */
2828
+ audience?: string | readonly string[];
2829
+ /** Required `iss` claim. */
2830
+ issuer?: string | readonly string[];
2831
+ /** Reject tokens whose `iat` is older than N seconds. */
2832
+ maxAgeSeconds?: number;
2833
+ /** Leeway for `nbf` / `exp` checks, in seconds. Default `5`. */
2834
+ clockSkewSeconds?: number;
2835
+ /**
2836
+ * If `true` (default), missing tokens raise `IngeniumUnauthorizedError`.
2837
+ * If `false`, missing tokens just call `next()` with no `ctx.jwt`.
2838
+ */
2839
+ required?: boolean;
2840
+ /**
2841
+ * Custom token reader. Default reads `Authorization: Bearer <token>`.
2842
+ * Return `undefined` to indicate "no token in this request".
2843
+ */
2844
+ getToken?: JwtTokenReader;
2845
+ /**
2846
+ * Optional sink for redacted verification failure reasons. Useful for
2847
+ * observability without leaking the failure type to the wire (which would
2848
+ * be an oracle for attackers).
2849
+ */
2850
+ logger?: JwtLogger;
2851
+ /**
2852
+ * Optional JWKS endpoint URL. When set, the middleware fetches the keys
2853
+ * from this URL on demand and looks them up by `header.kid`. Cached for
2854
+ * `jwksCacheMs` (default 10 minutes) per URL with a single in-flight
2855
+ * request coalesced across concurrent callers.
2856
+ */
2857
+ jwksUrl?: string;
2858
+ /** JWKS cache TTL in milliseconds. Default `600_000` (10 minutes). */
2859
+ jwksCacheMs?: number;
2860
+ /** Phantom — narrows `ctx.jwt.payload` for typed handlers. */
2861
+ _payload?: T;
2862
+ }
2863
+
2864
+ /**
2865
+ * Bearer-token JWT verification middleware.
2866
+ *
2867
+ * Attaches the verified token at `ctx.jwt`. Callers should module-augment
2868
+ * `IngeniumContext` for typed access (the framework purposely doesn't ship a
2869
+ * baked-in `jwt` field — payload shape is application-specific).
2870
+ *
2871
+ * @example
2872
+ * ```ts
2873
+ * declare module 'ingenium' {
2874
+ * interface IngeniumContext {
2875
+ * jwt?: import('ingenium').JwtVerified<{ sub: string; roles: string[] }>
2876
+ * }
2877
+ * }
2878
+ *
2879
+ * import { ingenium } from 'ingenium'
2880
+ * const app = ingenium()
2881
+ * // HMAC
2882
+ * app.use(ingenium.jwt({ secret: process.env.JWT_SECRET! }))
2883
+ * // RSA via JWKS (Auth0, Okta, Cognito, Clerk, Supabase, ...)
2884
+ * app.use(ingenium.jwt({
2885
+ * secret: [],
2886
+ * algorithms: ['RS256'],
2887
+ * jwksUrl: 'https://example.auth0.com/.well-known/jwks.json',
2888
+ * issuer: 'https://example.auth0.com/',
2889
+ * audience: 'https://api.example.com',
2890
+ * }))
2891
+ * ```
2892
+ *
2893
+ * Security choices:
2894
+ * - Default leeway (`clockSkewSeconds`) is **5 seconds** — enough for typical
2895
+ * multi-host clock drift, small enough that an expired token does not stay
2896
+ * usable for long.
2897
+ * - HMAC signature comparison uses `crypto.timingSafeEqual` after an explicit
2898
+ * length check inside {@link verifyJwt}; asymmetric verification uses
2899
+ * `crypto.verify` (constant-time within OpenSSL).
2900
+ * - The algorithm allowlist is enforced at verify time. Even if an attacker
2901
+ * crafts `alg: 'RS256'` and we have a matching JWKS key, verification fails
2902
+ * unless `RS256` appears in `algorithms`. This is the canonical defence
2903
+ * against algorithm-confusion attacks.
2904
+ * - `'none'` is rejected unconditionally, regardless of the allowlist.
2905
+ * - The wire-facing error is always `IngeniumUnauthorizedError('Invalid token')`
2906
+ * regardless of which check failed (signature vs exp vs aud) — this avoids
2907
+ * handing attackers an oracle. Detailed reasons go to `opts.logger` (or
2908
+ * `process.emitWarning` if no logger is supplied).
2909
+ */
2910
+ declare function jwtMiddleware<T = Record<string, unknown>>(opts: JwtOptions<T>): IngeniumMiddleware;
2911
+
2912
+ /** Result of a custom API-key validator. */
2913
+ type ApiKeyValidator = (key: string, ctx: IngeniumContext) => boolean | Promise<boolean>;
2914
+ /** Optional logger for redacted failure reasons. */
2915
+ type ApiKeyLogger = (event: {
2916
+ reason: string;
2917
+ }) => void;
2918
+ interface ApiKeyOptions {
2919
+ /**
2920
+ * Either an allow-list of valid keys (compared with `timingSafeEqual`) or a
2921
+ * custom validator. Functions get the candidate key and the request ctx.
2922
+ */
2923
+ keys: readonly string[] | ApiKeyValidator;
2924
+ /** Header to read the key from. Default `'x-api-key'`. */
2925
+ header?: string;
2926
+ /**
2927
+ * Optional fallback query-string parameter, e.g. `'api_key'`. When set,
2928
+ * the middleware checks `?api_key=...` if no header / scheme key matched.
2929
+ */
2930
+ query?: string;
2931
+ /**
2932
+ * Optional `Authorization` scheme to accept, e.g. `'ApiKey'`. When set,
2933
+ * the middleware accepts `Authorization: ApiKey <key>`.
2934
+ */
2935
+ scheme?: string;
2936
+ /**
2937
+ * If `true` (default), missing keys raise `IngeniumUnauthorizedError`. If
2938
+ * `false`, missing keys just call `next()` with no `ctx.apiKey`.
2939
+ */
2940
+ required?: boolean;
2941
+ /** Optional sink for redacted failure reasons. Defaults to `process.emitWarning`. */
2942
+ logger?: ApiKeyLogger;
2943
+ }
2944
+
2945
+ /**
2946
+ * API-key authentication middleware.
2947
+ *
2948
+ * Attaches the validated key string at `ctx.apiKey`. Callers should
2949
+ * module-augment `IngeniumContext` for typed access:
2950
+ *
2951
+ * @example
2952
+ * ```ts
2953
+ * declare module 'ingenium' {
2954
+ * interface IngeniumContext { apiKey?: string }
2955
+ * }
2956
+ *
2957
+ * import { ingenium } from 'ingenium'
2958
+ * const app = ingenium()
2959
+ * app.use(ingenium.apiKey({
2960
+ * keys: process.env.API_KEYS!.split(','),
2961
+ * scheme: 'ApiKey',
2962
+ * query: 'api_key',
2963
+ * }))
2964
+ * ```
2965
+ *
2966
+ * Security choices:
2967
+ * - Allow-list comparisons go through `crypto.timingSafeEqual` after an
2968
+ * explicit length check, so neither equality nor length leaks via timing.
2969
+ * - The wire-facing error is always `IngeniumUnauthorizedError('Invalid API key')`
2970
+ * regardless of which lookup failed (header vs scheme vs query) — no
2971
+ * oracle for which transport surface the legit key uses.
2972
+ * - Custom validators get the candidate key + ctx; their boolean result is
2973
+ * trusted as-is. Validators should be constant-time when comparing keys.
2974
+ */
2975
+ declare function apiKeyMiddleware(opts: ApiKeyOptions): IngeniumMiddleware;
2976
+
2977
+ /** Public options for `generateOpenApi(app, opts)`. */
2978
+ interface GenerateOpenApiOptions {
2979
+ info: Info;
2980
+ servers?: Server[];
2981
+ tags?: Tag[];
2982
+ security?: SecurityRequirement[];
2983
+ /**
2984
+ * Auto-tag generated operations by path prefix. The longest matching
2985
+ * prefix wins. Routes that already have `tags` in their descriptor are
2986
+ * left alone.
2987
+ *
2988
+ * @example { '/users': 'users', '/auth': 'auth' }
2989
+ */
2990
+ tagsByPrefix?: Record<string, string>;
2991
+ /**
2992
+ * Hide routes whose path matches any entry. Strings match exactly,
2993
+ * RegExps are tested against the full path.
2994
+ */
2995
+ excludePaths?: (string | RegExp)[];
2996
+ /** Pass-through `components.securitySchemes`. */
2997
+ securitySchemes?: Record<string, SecurityScheme>;
2998
+ /**
2999
+ * Optional additional schemas to merge into `components.schemas`. Useful
3000
+ * when you reference shared models via `$ref: '#/components/schemas/X'`.
3001
+ */
3002
+ componentSchemas?: Record<string, Schema>;
3003
+ }
3004
+ /**
3005
+ * Generate an OpenAPI 3.1 spec from a composed (or uncomposed) IngeniumApp.
3006
+ * Walks the registration journal — does not require `compose()` to have run.
3007
+ *
3008
+ * Schema-conversion strategy (in priority order):
3009
+ * 1. If a request/response schema has a `toJsonSchema()` method (Zod 3.24+,
3010
+ * ArkType, Effect Schema, etc.), call it.
3011
+ * 2. If it looks like a Standard Schema (has `~standard`), emit `{}` plus
3012
+ * `x-schema-source: '<vendor>-untranslated'` as a TODO marker.
3013
+ * 3. Otherwise, pass the value through unchanged (assumed JSON Schema).
3014
+ */
3015
+ declare function generateOpenApi(app: IngeniumApp, opts: GenerateOpenApiOptions): OpenApiSpec;
3016
+
3017
+ /**
3018
+ * Build a route handler that serves the generated OpenAPI spec as JSON.
3019
+ *
3020
+ * The spec is generated lazily on the first request that hits this handler
3021
+ * and cached on the app under a private symbol. The cache invalidates when
3022
+ * the registration journal length changes — i.e. when new routes are added —
3023
+ * so live-registered routes are reflected on the next request.
3024
+ *
3025
+ * @example
3026
+ * app.get('/openapi.json', ingenium.openapiHandler({
3027
+ * info: { title: 'My API', version: '1.0.0' },
3028
+ * }))
3029
+ */
3030
+ declare function openapiHandler(opts: GenerateOpenApiOptions): IngeniumHandler;
3031
+
3032
+ /**
3033
+ * A bounded free-list of `IngeniumContext` objects. Acquire on each request,
3034
+ * release back when the response has been written. If the pool is empty,
3035
+ * a fresh context is allocated; if the pool is full on release, the
3036
+ * context is discarded (GC handles it). Never blocks.
3037
+ */
3038
+ declare class IngeniumContextPool {
3039
+ private readonly pool;
3040
+ private readonly max;
3041
+ constructor(maxSize?: number);
3042
+ /** Acquire a context. Caller must call `release()` when done. */
3043
+ acquire(): IngeniumContext;
3044
+ /** Reset and return the context to the free list (or discard if full). */
3045
+ release(ctx: IngeniumContext): void;
3046
+ /** Current free-list size. Useful for tests and metrics. */
3047
+ get size(): number;
3048
+ }
3049
+
3050
+ /**
3051
+ * Compose an array of middleware into a single async function. Composition
3052
+ * runs ONCE at registration / first-request time; the returned function has
3053
+ * no per-request `bind`, no index variable, and no `stack[n]` lookups.
3054
+ *
3055
+ * The dispatcher chain is pre-built bottom-up: `dispatchers[i]` runs
3056
+ * `stack[i]` with a `next` that invokes `dispatchers[i + 1]`. Each
3057
+ * middleware-level invocation still allocates one closure to capture `ctx`
3058
+ * (unavoidable without dropping concurrency safety).
3059
+ *
3060
+ * Double-`next()` calls are detected only when `process.env.REX_DEBUG` is
3061
+ * truthy, to keep the production hot path free of per-call guard variables.
3062
+ */
3063
+ declare function compose(stack: readonly IngeniumMiddleware[]): ComposedHandler;
3064
+ /**
3065
+ * Compose middleware then append a terminal handler that does not receive a
3066
+ * `next` (so a route handler can be the leaf of the chain). The handler's
3067
+ * return value is reflected to the response per the contract in
3068
+ * `response/reflect.ts` — unless the handler called a `ctx.json/...` helper,
3069
+ * in which case the return value is ignored.
3070
+ *
3071
+ * Hot-path optimization: when there are no middleware, we skip the
3072
+ * dispatcher chain entirely and return a thin wrapper that calls the
3073
+ * handler directly. We also detect synchronous handler return values
3074
+ * (non-thenable) and avoid the `await` microtask in that case — measurable
3075
+ * on JSON-returning routes that don't touch the body.
3076
+ */
3077
+ declare function composeWithHandler(middleware: readonly IngeniumMiddleware[], handler: (ctx: IngeniumContext) => unknown | Promise<unknown>): ComposedHandler;
3078
+
3079
+ /**
3080
+ * One node in the radix trie. Static segments win over `:param`, which wins
3081
+ * over `*wild`. Method-specific composed handlers live at the leaf.
3082
+ */
3083
+ declare class TrieNode {
3084
+ staticChildren: Map<string, TrieNode>;
3085
+ paramChild: TrieNode | null;
3086
+ paramName: string | null;
3087
+ wildcardChild: TrieNode | null;
3088
+ wildcardName: string | null;
3089
+ /**
3090
+ * Compiled inline constraint for this node *as a param child*, or `null`
3091
+ * when the param is unconstrained. Set at insert time when the registered
3092
+ * segment carries a `(regex)` group (e.g. `:id(\d+)`). The `find()` hot
3093
+ * path loads this field and only runs `.test()` when it is non-null, so
3094
+ * unconstrained routes pay zero extra cost. Lives on the param node itself
3095
+ * (the child) so the matcher can test it the instant it descends.
3096
+ */
3097
+ paramConstraint: RegExp | null;
3098
+ /** Per-method composed handlers, populated by `RouteRegistry` after compose. */
3099
+ handlers: Partial<Record<HttpMethod, ComposedHandler>>;
3100
+ /**
3101
+ * Param names accumulated from root → this node, in order. Cached so
3102
+ * matching can fill the params object in O(k) without re-walking parents.
3103
+ */
3104
+ paramNames: readonly string[];
3105
+ }
3106
+ /** Result of a trie lookup. `params` may be empty if the route had none. */
3107
+ interface MatchResult {
3108
+ handler: ComposedHandler;
3109
+ params: Record<string, string>;
3110
+ /** Methods registered at this leaf — used to populate `Allow` on 405. */
3111
+ allowed: readonly HttpMethod[];
3112
+ }
3113
+ /** Why a lookup failed. */
3114
+ type MatchMiss = {
3115
+ kind: 'not-found';
3116
+ } | {
3117
+ kind: 'method-not-allowed';
3118
+ allowed: readonly HttpMethod[];
3119
+ };
3120
+ /**
3121
+ * Radix trie router. `insert()` is called at registration; `find()` runs on
3122
+ * every request and is the single hottest piece of code in the framework.
3123
+ */
3124
+ declare class RouterTrie {
3125
+ readonly root: TrieNode;
3126
+ /**
3127
+ * Walks/creates trie nodes for the path. Returns the leaf where handlers
3128
+ * should be attached. Path must start with `/`.
3129
+ */
3130
+ insert(path: string): TrieNode;
3131
+ /**
3132
+ * Look up a route. Iterative with single-level wildcard backtrack — if the
3133
+ * static/param walk dead-ends and an ancestor had a `*wildcard` child, we
3134
+ * retry from the wildcard with the remaining segments. Backtrack frames
3135
+ * are tracked in a small stack (one per wildcard ancestor encountered).
3136
+ */
3137
+ find(method: HttpMethod, path: string): MatchResult | MatchMiss;
3138
+ }
3139
+
3140
+ /**
3141
+ * `safeJsonStringify(value, opts?)` — a lenient `JSON.stringify` that never
3142
+ * throws on circular references or `BigInt` values.
3143
+ *
3144
+ * Behavior:
3145
+ * - Circular references → replaced with the string `'[Circular]'`.
3146
+ * - `BigInt` values → serialized as a JSON string (e.g. `1n` → `"1"`).
3147
+ * This preserves precision and is reversible by the caller; if you need a
3148
+ * different convention, pass your own `replacer`.
3149
+ * - Symbol values → omitted (matches `JSON.stringify` default).
3150
+ * - Functions → omitted (matches `JSON.stringify` default).
3151
+ *
3152
+ * Intended for opt-in use by callers who want lenient behavior — the
3153
+ * default `ctx.json()` path remains strict and surfaces a
3154
+ * `IngeniumUnserializableError` so the bug is visible.
3155
+ *
3156
+ * @example
3157
+ * import { safeJsonStringify } from 'ingenium'
3158
+ * ctx.send(safeJsonStringify(value), 200)
3159
+ * ctx.set('content-type', 'application/json; charset=utf-8')
3160
+ */
3161
+ /** Options for `safeJsonStringify`. */
3162
+ interface SafeJsonStringifyOptions {
3163
+ /**
3164
+ * Pass-through to `JSON.stringify`'s third argument — number of spaces or
3165
+ * indent string for pretty-printing. Defaults to no indentation.
3166
+ */
3167
+ space?: string | number;
3168
+ /**
3169
+ * Optional user replacer applied AFTER the cycle/BigInt sanitization. If
3170
+ * provided, behaves like `JSON.stringify`'s second argument.
3171
+ */
3172
+ replacer?: (key: string, value: unknown) => unknown;
3173
+ }
3174
+ /**
3175
+ * Stringify `value` without throwing on circular structures or `BigInt`s.
3176
+ * See module doc for the exact substitution rules.
3177
+ */
3178
+ declare function safeJsonStringify(value: unknown, opts?: SafeJsonStringifyOptions): string;
3179
+
3180
+ /**
3181
+ * Node.js `node:http` transport. Owns a single `http.Server`; on each
3182
+ * request, populates a pooled `IngeniumContext` directly from the
3183
+ * `IncomingMessage` (no WinterCG translation), awaits dispatch, then writes
3184
+ * the context's response state to the `ServerResponse`.
3185
+ */
3186
+ declare class NodeAdapter implements Transport {
3187
+ private hooks;
3188
+ attach(hooks: TransportHooks): void;
3189
+ listen(port: number, host?: string): Promise<ListeningServer>;
3190
+ }
3191
+
3192
+ /** TLS options accepted by the h2 (secure) adapter. */
3193
+ interface Http2AdapterOptions {
3194
+ /** TLS certificate (PEM). */
3195
+ cert: Buffer | string;
3196
+ /** TLS private key (PEM). */
3197
+ key: Buffer | string;
3198
+ /**
3199
+ * If true, the secure server also accepts HTTP/1.1 connections via ALPN
3200
+ * fallback. Inbound HTTP/1 requests are dispatched through the same path
3201
+ * used by `NodeAdapter`. Default: false (HTTP/2 only).
3202
+ */
3203
+ allowHttp1?: boolean;
3204
+ }
3205
+ /**
3206
+ * HTTP/2-over-TLS (`h2`) transport. Uses Node's built-in `http2.createSecureServer`.
3207
+ * Browsers REQUIRE TLS for HTTP/2 — there is no cleartext HTTP/2 negotiation
3208
+ * over the open web. For local testing without certs, use {@link Http2cAdapter}.
3209
+ *
3210
+ * Per-request: on `'stream'`, populates a pooled `IngeniumContext` from pseudo-headers,
3211
+ * awaits dispatch, then writes the response via `stream.respond()` + `stream.end()`
3212
+ * (or pipes for `Readable` bodies).
3213
+ */
3214
+ declare class Http2Adapter implements Transport {
3215
+ private readonly options;
3216
+ private hooks;
3217
+ constructor(options: Http2AdapterOptions);
3218
+ attach(hooks: TransportHooks): void;
3219
+ listen(port: number, host?: string): Promise<ListeningServer>;
3220
+ }
3221
+ /**
3222
+ * HTTP/2 cleartext (`h2c`) transport. Uses Node's `http2.createServer` — no TLS,
3223
+ * so this is intended for local development, internal service-to-service calls
3224
+ * behind an L7 proxy that handles TLS termination, or test suites. Browsers do
3225
+ * not speak h2c; use {@link Http2Adapter} for browser traffic.
3226
+ *
3227
+ * Constructor takes no required arguments.
3228
+ */
3229
+ declare class Http2cAdapter implements Transport {
3230
+ private hooks;
3231
+ constructor(_options?: {});
3232
+ attach(hooks: TransportHooks): void;
3233
+ listen(port: number, host?: string): Promise<ListeningServer>;
3234
+ }
3235
+
3236
+ /**
3237
+ * Graceful shutdown helper. Wires POSIX signal handlers to drain a
3238
+ * {@link ListeningServer}, run a user cleanup hook, then exit.
3239
+ *
3240
+ * Most production deployments (Kubernetes, systemd, PM2, ECS, Fly, …) send
3241
+ * SIGTERM when they want a process to stop. By default Node simply dies on
3242
+ * SIGTERM, which kills in-flight requests and leaves keep-alive sockets
3243
+ * dangling. Calling {@link gracefulShutdown} after `app.listen()` opts the
3244
+ * process into a clean drain instead.
3245
+ */
3246
+
3247
+ /** Options for {@link gracefulShutdown}. */
3248
+ interface ShutdownOptions {
3249
+ /**
3250
+ * Maximum time (ms) to wait for sockets to drain before they are forcibly
3251
+ * destroyed. Defaults to `10_000` (10s) — matches Kubernetes' default
3252
+ * `terminationGracePeriodSeconds` headroom.
3253
+ */
3254
+ gracefulTimeoutMs?: number;
3255
+ /**
3256
+ * Signals to listen for. Defaults to `['SIGTERM', 'SIGINT']`.
3257
+ */
3258
+ signals?: NodeJS.Signals[];
3259
+ /**
3260
+ * User cleanup hook — runs AFTER the server stops accepting new
3261
+ * connections but BEFORE the process exits. Use for closing DB pools,
3262
+ * flushing logs, etc. Awaited; throwing exits with code 1.
3263
+ */
3264
+ onShutdown?: () => void | Promise<void>;
3265
+ /** Logger used to announce shutdown lifecycle events. Defaults to `console.log`. */
3266
+ logger?: (msg: string) => void;
3267
+ }
3268
+ /**
3269
+ * Wire signal handlers that gracefully shut down `server` on SIGTERM/SIGINT
3270
+ * (or whichever signals you pass). Returns an unsubscribe function that
3271
+ * removes the listeners — mostly useful for tests.
3272
+ *
3273
+ * @example
3274
+ * const server = await app.listen(3000)
3275
+ * gracefulShutdown(server, { onShutdown: async () => db.close() })
3276
+ */
3277
+ declare function gracefulShutdown(server: ListeningServer, opts?: ShutdownOptions): () => void;
3278
+
3279
+ /**
3280
+ * Send a `:keepalive` comment to the given SSE stream every `intervalMs`
3281
+ * milliseconds. Returns a cancellation function.
3282
+ *
3283
+ * The interval is automatically cancelled when the stream closes — but
3284
+ * callers should still hold onto the cancel function for explicit cleanup
3285
+ * (e.g. on a separate teardown signal).
3286
+ *
3287
+ * The internal timer is `unref()`'d so it won't keep the Node event loop
3288
+ * alive on its own.
3289
+ *
3290
+ * @example
3291
+ * const stream = sse(ctx)
3292
+ * const cancel = startKeepAlive(stream, 15_000)
3293
+ * ctx.req.on('close', cancel) // optional
3294
+ */
3295
+ declare function startKeepAlive(stream: SseStream, intervalMs?: number): () => void;
3296
+
3297
+ interface MemoryStoreOptions {
3298
+ /**
3299
+ * Hard ceiling on the number of distinct keys retained. When exceeded, the
3300
+ * **least-recently-touched** entry is evicted to make room. Default
3301
+ * `100_000`.
3302
+ *
3303
+ * The cap exists to bound memory under adversarial conditions: an attacker
3304
+ * generating one request per unique IP would otherwise grow the map without
3305
+ * bound. With the cap, the worst case is a fixed memory footprint and
3306
+ * attackers' counters get evicted (which means they bypass rate-limiting
3307
+ * for the exact endpoint they're hammering — a real trade-off, but better
3308
+ * than OOM).
3309
+ *
3310
+ * For genuinely high-cardinality production workloads (millions of distinct
3311
+ * users), prefer a Redis-backed store so eviction isn't required.
3312
+ */
3313
+ maxEntries?: number;
3314
+ }
3315
+ /**
3316
+ * In-process fixed-window counter store. Suitable for single-replica
3317
+ * deployments and tests; swap for a Redis-backed store when running
3318
+ * multiple replicas behind a load balancer.
3319
+ *
3320
+ * A periodic sweep removes expired entries every `windowMs` so long-lived
3321
+ * processes don't leak memory across forgotten keys. The sweep timer is
3322
+ * `.unref()`'d, so it never keeps the Node event loop alive.
3323
+ *
3324
+ * The `Map` itself is bounded by `maxEntries` (default 100k). When the cap
3325
+ * is reached, the least-recently-touched entry is evicted before the new
3326
+ * entry is inserted. We rely on the JS `Map` insertion-order guarantee:
3327
+ * delete-then-set on an existing key moves it to the end, so the first
3328
+ * iteration step always returns the genuine LRU. **This is intentional
3329
+ * defense against scanner attacks that would otherwise OOM the process by
3330
+ * generating unique keys.**
3331
+ */
3332
+ declare class MemoryStore$1 implements RateLimitStore {
3333
+ private readonly map;
3334
+ private sweeper;
3335
+ private sweepIntervalMs;
3336
+ private readonly maxEntries;
3337
+ constructor(opts?: MemoryStoreOptions);
3338
+ hit(key: string, windowMs: number): Promise<{
3339
+ count: number;
3340
+ resetAt: number;
3341
+ }>;
3342
+ reset(key: string): Promise<void>;
3343
+ /**
3344
+ * Stop the cleanup interval. Safe to call multiple times. Mostly useful
3345
+ * in tests; production usage doesn't need this because the timer is
3346
+ * already unref'd.
3347
+ */
3348
+ destroy(): void;
3349
+ /** @internal Current entry count — exposed for ops/tests. */
3350
+ get size(): number;
3351
+ private ensureSweeper;
3352
+ private sweep;
3353
+ }
3354
+
3355
+ /**
3356
+ * Pure parsers and matchers for HTTP `Accept`-family headers.
3357
+ *
3358
+ * Implemented from scratch — no `negotiator` / `accepts` runtime dep.
3359
+ * Used by `negotiate.ts`, `format.ts`, and downstream context helpers.
3360
+ *
3361
+ * Spec references:
3362
+ * - RFC 9110 §12.5.1 (Accept), §12.5.2 (Accept-Charset),
3363
+ * §12.5.4 (Accept-Encoding), §12.5.5 (Accept-Language).
3364
+ */
3365
+ /** A single parsed media-range entry from an `Accept` header. */
3366
+ interface ParsedAccept {
3367
+ /** The full media-range string (lowercased), e.g. `text/html`, `text/\*`, `\*\/\*`. */
3368
+ type: string;
3369
+ /** Quality factor from `;q=N`, default `1`. Out-of-range values are clamped. */
3370
+ quality: number;
3371
+ /** Any other extension parameters (e.g. `level=1`). */
3372
+ params: Record<string, string>;
3373
+ }
3374
+ /** Resolve a shorthand (`'json'`) to its canonical mime, or pass through. */
3375
+ declare function expandShorthand(token: string): string;
3376
+ /**
3377
+ * Parse a comma-separated `Accept`-family header into a list of entries.
3378
+ * Empty / undefined input returns an empty array. Malformed entries are
3379
+ * silently dropped (lenient parsing — same as Express).
3380
+ *
3381
+ * Result is **not** sorted; pass to `sortByPreference` if you need ordering.
3382
+ */
3383
+ declare function parseAcceptHeader(header: string | undefined): ParsedAccept[];
3384
+ /**
3385
+ * Stable sort by RFC preference: highest q first, then most-specific first.
3386
+ * Returns a NEW array; does not mutate input.
3387
+ */
3388
+ declare function sortByPreference(entries: readonly ParsedAccept[]): ParsedAccept[];
3389
+ /**
3390
+ * Return the best match for `offered` against `acceptHeader`, or `false`.
3391
+ *
3392
+ * Matching algorithm:
3393
+ * 1. If `acceptHeader` is missing/empty → first offered wins (Express behavior).
3394
+ * 2. Walk parsed entries sorted by quality + specificity.
3395
+ * 3. For each entry (in preference order), pick the first offered that matches.
3396
+ * Among ties at the same Accept entry, the offered's listed order wins.
3397
+ * 4. Entries with `q=0` reject — never match.
3398
+ */
3399
+ declare function selectBest(acceptHeader: string | undefined, offered: readonly string[]): string | false;
3400
+
3401
+ /**
3402
+ * `isFresh(reqHeaders, resHeaders)` — RFC 7232 conditional-request evaluator.
3403
+ *
3404
+ * Returns `true` when the response can be considered fresh relative to the
3405
+ * client's cached copy, i.e. a `304 Not Modified` is appropriate. This is
3406
+ * the engine behind `ctx.fresh` / `ctx.stale`.
3407
+ *
3408
+ * Decision matrix:
3409
+ * - `If-None-Match` present → compare against response `ETag`. Wildcard
3410
+ * `*` matches any current representation. Strong/weak prefixes are
3411
+ * normalized away (per RFC 7232 §2.3.2 weak-comparison rules).
3412
+ * - Else if `If-Modified-Since` present → compare against response
3413
+ * `Last-Modified` (or fall back to `Date`). Fresh when the resource has
3414
+ * not been modified since.
3415
+ * - Otherwise → not fresh (no precondition to evaluate).
3416
+ *
3417
+ * Methods other than GET/HEAD are not handled here — callers should gate
3418
+ * on method themselves (Express does the same in `req.fresh`).
3419
+ */
3420
+ /** Header bag shape — accepts both incoming-request and stored-response styles. */
3421
+ type HeaderBag = Record<string, string | string[] | undefined>;
3422
+ /**
3423
+ * Returns `true` when the response is fresh w.r.t. the client's preconditions.
3424
+ */
3425
+ declare function isFresh(reqHeaders: HeaderBag, resHeaders: HeaderBag): boolean;
3426
+
3427
+ /**
3428
+ * `computeEtag(body, weak?)` — sha1-based entity tag for response bodies.
3429
+ *
3430
+ * Format: `W/"<sha1-base64-without-padding>"` (weak) or `"<sha1-base64-without-padding>"`.
3431
+ * Weak is the default — fine for JSON where serialization may legitimately vary
3432
+ * (key order, whitespace) without representing a different resource.
3433
+ *
3434
+ * The empty-body ETag is special-cased to a fixed constant so two empty
3435
+ * bodies always compare equal without pumping through the hash.
3436
+ */
3437
+
3438
+ /**
3439
+ * Compute an ETag for the given body. Strings are treated as UTF-8.
3440
+ * @param body Response body — usually `JSON.stringify(...)` or a `Buffer`.
3441
+ * @param weak Prefix the tag with `W/`. Defaults to `true` for JSON safety.
3442
+ */
3443
+ declare function computeEtag(body: string | Buffer$1, weak?: boolean): string;
3444
+
3445
+ /**
3446
+ * Convert a thrown value into an RFC 7807 ProblemDetails object. Handles
3447
+ * `IngeniumError` and its subclasses with rich extensions; unknown errors are
3448
+ * reported as a generic 500 with `type: 'about:blank'`.
3449
+ *
3450
+ * Side effect: for `IngeniumMethodNotAllowedError`, the `Allow` header is set
3451
+ * on the response so it matches the framework's default boundary behavior.
3452
+ */
3453
+ declare function toProblemDetails(err: unknown, opts: ResolvedProblemDetailsOptions, ctx: IngeniumContext): ProblemDetails;
3454
+
3455
+ /**
3456
+ * In-process idempotency cache. Suitable for single-replica deployments and
3457
+ * tests; back with Redis when running multiple replicas behind a load
3458
+ * balancer (responses cached on one replica won't replay on another).
3459
+ *
3460
+ * A periodic sweep removes expired entries so long-lived processes don't
3461
+ * leak memory across forgotten keys. The sweep timer is `.unref()`'d, so
3462
+ * it never keeps the Node event loop alive.
3463
+ */
3464
+ declare class IdempotencyMemoryStore implements IdempotencyStore {
3465
+ private readonly map;
3466
+ private sweeper;
3467
+ private sweepIntervalMs;
3468
+ get(key: string): Promise<CachedResponse | null>;
3469
+ set(key: string, value: CachedResponse, ttlMs: number): Promise<void>;
3470
+ delete(key: string): Promise<void>;
3471
+ /**
3472
+ * Stop the cleanup interval. Safe to call multiple times. Mostly useful
3473
+ * in tests; production usage doesn't need this because the timer is
3474
+ * already unref'd.
3475
+ */
3476
+ destroy(): void;
3477
+ private ensureSweeper;
3478
+ private sweep;
3479
+ }
3480
+
3481
+ /**
3482
+ * A verification key as supplied by the caller (post-resolution).
3483
+ * - `string` / `Buffer` for HMAC secrets and PEM blobs.
3484
+ * - `KeyObject` for pre-built node:crypto keys (and JWKS-derived keys).
3485
+ */
3486
+ type VerifyKeyMaterial = string | Buffer$1 | KeyObject;
3487
+ /** Optional kid-tagged variant — what middleware passes after kid resolution. */
3488
+ interface KidTaggedKey {
3489
+ kid?: string;
3490
+ key: VerifyKeyMaterial;
3491
+ }
3492
+ /** Options accepted by {@link verifyJwt}. Mirrors the relevant subset of `JwtOptions`. */
3493
+ interface VerifyOptions {
3494
+ algorithms: readonly JwtAlgorithm[];
3495
+ audience?: string | readonly string[];
3496
+ issuer?: string | readonly string[];
3497
+ maxAgeSeconds?: number;
3498
+ clockSkewSeconds?: number;
3499
+ /** Override "now" for deterministic tests. Returns seconds since epoch. */
3500
+ nowSeconds?: () => number;
3501
+ }
3502
+ /**
3503
+ * Pure JWT verifier. No I/O, no logging — returns either a `JwtVerified` or
3504
+ * a tagged failure object. The middleware layer is responsible for collapsing
3505
+ * every failure into the same `IngeniumUnauthorizedError('Invalid token')` so
3506
+ * the wire never reveals which check tripped.
3507
+ *
3508
+ * `keys` is a flat array because key-resolution (rotation, kid-lookup, JWKS)
3509
+ * is the caller's responsibility; this function just tries them in order.
3510
+ * Each entry may carry an optional `kid` — when present AND `header.kid` is
3511
+ * set, only matching entries are considered. Without a `header.kid`, every
3512
+ * entry is tried.
3513
+ *
3514
+ * `alg: 'none'` is rejected unconditionally, even if for some reason the
3515
+ * allowlist were extended to include it. Defence in depth.
3516
+ */
3517
+ declare function verifyJwt<T = Record<string, unknown>>(token: string, keys: readonly (VerifyKeyMaterial | KidTaggedKey)[], opts: VerifyOptions): JwtVerified<T> | JwtVerifyError;
3518
+
3519
+ /**
3520
+ * Fetch + cache a JWKS. Returns a `Map<kid, KeyObject>`.
3521
+ *
3522
+ * Concurrency: if a fetch is already in flight for `url` we await the same
3523
+ * promise, ensuring a thundering-herd of requests collapses to one upstream
3524
+ * call. After the fetch resolves, all waiters get the same keys.
3525
+ *
3526
+ * Failure mode: any thrown error (network, JSON parse, malformed JWK, empty
3527
+ * keyset) bubbles as a generic `Error('jwks_fetch_failed')` — the caller is
3528
+ * responsible for translating to a wire-safe `IngeniumUnauthorizedError`. We
3529
+ * deliberately do NOT serve a stale cache on failure: stale public keys can
3530
+ * mean accepting tokens that the IdP has rotated away from.
3531
+ */
3532
+ declare function fetchJwks(url: string, ttlMs: number): Promise<Map<string, KeyObject>>;
3533
+ /** Reset the in-process cache. Tests use this; production code shouldn't need it. */
3534
+ declare function clearJwksCache(): void;
3535
+
3536
+ /**
3537
+ * In-process FIFO queue store. Backs {@link IngeniumQueue} when no custom
3538
+ * store is supplied. Suitable for single-instance deployments and tests.
3539
+ *
3540
+ * Layout:
3541
+ * - `pending`: ordered list of jobs ready to be picked up. Delayed jobs
3542
+ * (post-retry backoff) sit here too — `next()` skips entries whose
3543
+ * `notBefore` hasn't elapsed yet, so callers should poll on a timer.
3544
+ * - `inFlight`: jobs that have been `next()`-ed but not yet `ack`/`retry`/`fail`-ed.
3545
+ * - `failed`: dead-letter list. Persists until `clearFailed()` is called.
3546
+ *
3547
+ * No background timers — purely event-driven via the queue worker pool.
3548
+ */
3549
+ declare class MemoryQueueStore<TData> implements QueueStore<TData> {
3550
+ private readonly pending;
3551
+ private readonly inFlight;
3552
+ private readonly failed;
3553
+ private nextId;
3554
+ enqueue(data: TData): Promise<{
3555
+ id: string;
3556
+ }>;
3557
+ next(): Promise<{
3558
+ id: string;
3559
+ data: TData;
3560
+ attempt: number;
3561
+ } | null>;
3562
+ ack(id: string): Promise<void>;
3563
+ retry(id: string, delayMs: number): Promise<void>;
3564
+ fail(id: string): Promise<void>;
3565
+ size(): Promise<number>;
3566
+ failedCount(): Promise<number>;
3567
+ /** @internal Used by `IngeniumQueue.clearFailed()`. */
3568
+ clearFailed(): void;
3569
+ /** @internal Used by `IngeniumQueue.drain()` to know if work is outstanding. */
3570
+ inFlightCount(): number;
3571
+ /** @internal Earliest `notBefore` of any pending entry, or `null` if none. */
3572
+ earliestPendingAt(): number | null;
3573
+ }
3574
+
3575
+ /**
3576
+ * 5-field crontab parser + "next fire time" calculator.
3577
+ *
3578
+ * Grammar (per field):
3579
+ * - `*` every value
3580
+ * - `N` literal
3581
+ * - `N-M` inclusive range
3582
+ * - `* / S` or `N-M / S` step
3583
+ * - `A,B,C` list (each entry can be any of the above)
3584
+ *
3585
+ * Supported field ranges:
3586
+ * minute 0-59
3587
+ * hour 0-23
3588
+ * dom 1-31
3589
+ * month 1-12, or 3-letter names jan|feb|...|dec
3590
+ * dow 0-6 (sunday=0), or 3-letter names sun|mon|...|sat
3591
+ *
3592
+ * Explicitly NOT supported (would need a different parser):
3593
+ * - 6-field syntax with seconds
3594
+ * - L (last day-of-month), W (weekday), # (nth-of-month)
3595
+ * - Predefined macros (@hourly, @daily, ...)
3596
+ *
3597
+ * Day-of-month vs day-of-week conflict resolution: when BOTH `dom` and
3598
+ * `dow` are restricted (i.e. neither is `*`), the cron fires when EITHER
3599
+ * matches (this is the historical Vixie-cron behavior). When only one is
3600
+ * restricted, only that one matters. This is what every other production
3601
+ * cron implementation does and what users expect.
3602
+ */
3603
+ /** Parsed match-set. `Set<number>` of all valid integers per field. */
3604
+ interface CronMatch {
3605
+ minute: Set<number>;
3606
+ hour: Set<number>;
3607
+ dom: Set<number>;
3608
+ month: Set<number>;
3609
+ dow: Set<number>;
3610
+ /** Was the original `dom` field `*`? Used for dom/dow conflict resolution. */
3611
+ domIsWild: boolean;
3612
+ /** Was the original `dow` field `*`? */
3613
+ dowIsWild: boolean;
3614
+ }
3615
+ /**
3616
+ * Parse a 5-field crontab spec into a {@link CronMatch}. Throws on any
3617
+ * malformed input — out-of-range, wrong field count, garbage characters.
3618
+ */
3619
+ declare function parseCronSpec(spec: string): CronMatch;
3620
+ /**
3621
+ * Given a parsed {@link CronMatch}, find the next moment >= `from` that
3622
+ * matches the spec, in the given IANA timezone. Returns `null` if none
3623
+ * within ~5 years (defensive against pathological specs).
3624
+ *
3625
+ * Algorithm: walk forward minute-by-minute with smart skipping. We start
3626
+ * one minute past `from` (cron fires at the START of each minute and we
3627
+ * never want to re-fire the same slot back-to-back).
3628
+ *
3629
+ * Timezone handling:
3630
+ * - For `'UTC'` we use direct UTC accessors — fast path.
3631
+ * - For other zones we call `Intl.DateTimeFormat` to get the wall-clock
3632
+ * fields in that zone for each candidate. This relies on Node's bundled
3633
+ * ICU data; full-icu Node ships with a complete tz database.
3634
+ *
3635
+ * DST: by walking minute-by-minute on the UTC timeline and reading the
3636
+ * wall-clock fields per-step, we naturally skip the "spring forward" gap
3637
+ * (those minutes simply don't exist in the local clock so they can't match
3638
+ * the user's local-time spec) and double-fire on "fall back" (the wall
3639
+ * clock visits 1:30am twice; we fire each time). The latter matches Vixie
3640
+ * cron's documented behavior — users wanting strict once-per-day semantics
3641
+ * should pin their spec to UTC.
3642
+ */
3643
+ declare function nextFireFrom(match: CronMatch, from: Date, timezone?: string): Date | null;
3644
+
3645
+ /**
3646
+ * Session middleware types.
3647
+ *
3648
+ * @see ./middleware.ts for the {@link sessionMiddleware} factory and the
3649
+ * module-augmentation pattern users opt into for typed `ctx.session`.
3650
+ */
3651
+ /** Cookie attribute overrides. */
3652
+ interface SessionCookieOptions {
3653
+ /** Cookie `Domain` attribute. Omitted when undefined. */
3654
+ domain?: string;
3655
+ /** Cookie `Path` attribute. @default '/' */
3656
+ path?: string;
3657
+ /** Cookie `HttpOnly` attribute. @default true */
3658
+ httpOnly?: boolean;
3659
+ /** Cookie `SameSite` attribute. @default 'lax' */
3660
+ sameSite?: 'lax' | 'strict' | 'none';
3661
+ /** Cookie `Secure` attribute. @default false */
3662
+ secure?: boolean;
3663
+ }
3664
+ /** Options accepted by {@link sessionMiddleware}. */
3665
+ interface SessionOptions {
3666
+ /**
3667
+ * HMAC secret(s) for signing the session-id cookie.
3668
+ *
3669
+ * - Single string: used for both signing and verification.
3670
+ * - Array: index `0` is the active signing key; ALL entries are accepted
3671
+ * for verification, enabling key rotation. Cookies signed with an older
3672
+ * key are re-signed with the active key on the next response.
3673
+ */
3674
+ secret: string | string[];
3675
+ /** Name of the session cookie. @default 'ingenium.sid' */
3676
+ cookieName?: string;
3677
+ /** Cookie / store TTL in seconds. @default 604800 (7 days) */
3678
+ maxAgeSeconds?: number;
3679
+ /**
3680
+ * If true, the cookie expiry and store TTL are refreshed on every request,
3681
+ * even when the session data did not change. @default false
3682
+ */
3683
+ rolling?: boolean;
3684
+ /** Cookie attribute overrides. */
3685
+ cookie?: SessionCookieOptions;
3686
+ /**
3687
+ * Backing store. Defaults to an in-process {@link MemoryStore} which is
3688
+ * NOT suitable for clustered deployments — supply your own for Redis,
3689
+ * Postgres, etc.
3690
+ */
3691
+ store?: SessionStore;
3692
+ }
3693
+ /**
3694
+ * Per-request session handle attached as `ctx.session`.
3695
+ *
3696
+ * Mutations (`set`, `delete`, `destroy`, `regenerate`) mark the session as
3697
+ * dirty so the middleware persists changes after the handler returns.
3698
+ */
3699
+ interface Session {
3700
+ /** Stable, opaque session id (rotated by {@link Session.regenerate}). */
3701
+ readonly id: string;
3702
+ /** Frozen view of the session data. */
3703
+ readonly data: Readonly<Record<string, unknown>>;
3704
+ /** Read a value from the session. */
3705
+ get<T = unknown>(key: string): T | undefined;
3706
+ /** Write a value into the session. Marks the session dirty. */
3707
+ set(key: string, value: unknown): void;
3708
+ /** Remove a key from the session. Marks the session dirty. */
3709
+ delete(key: string): void;
3710
+ /** Drop the session: remove from store + clear the cookie. */
3711
+ destroy(): Promise<void>;
3712
+ /**
3713
+ * Issue a new session id while preserving the current data. The old id is
3714
+ * removed from the store. Use after privilege changes (e.g. login) to
3715
+ * mitigate session-fixation attacks.
3716
+ */
3717
+ regenerate(): Promise<void>;
3718
+ }
3719
+ /**
3720
+ * Pluggable session storage. Implementations must be safe to call
3721
+ * concurrently for distinct ids; per-id ordering is the caller's concern.
3722
+ */
3723
+ interface SessionStore {
3724
+ /** Look up a session by id. Returns `null` for unknown / expired ids. */
3725
+ get(id: string): Promise<Record<string, unknown> | null>;
3726
+ /** Persist `data` under `id` with the given TTL (seconds). */
3727
+ set(id: string, data: Record<string, unknown>, ttlSeconds: number): Promise<void>;
3728
+ /** Remove a session entirely. No-op if it does not exist. */
3729
+ destroy(id: string): Promise<void>;
3730
+ /**
3731
+ * OPTIONAL: extend an existing session's TTL without rewriting its data.
3732
+ * Used by `rolling` sessions on requests that did not mutate state.
3733
+ */
3734
+ touch?(id: string, ttlSeconds: number): Promise<void>;
3735
+ }
3736
+
3737
+ /**
3738
+ * Cookie-backed session middleware.
3739
+ *
3740
+ * The middleware attaches a {@link Session} instance at `ctx.session`. To
3741
+ * make this typesafe in user code, augment the `IngeniumContext` interface in
3742
+ * your own project:
3743
+ *
3744
+ * @example
3745
+ * ```ts
3746
+ * declare module 'ingenium' {
3747
+ * interface IngeniumContext { session: import('ingenium').Session }
3748
+ * }
3749
+ *
3750
+ * import { ingenium, sessionMiddleware } from 'ingenium'
3751
+ * const app = ingenium()
3752
+ * app.use(sessionMiddleware({ secret: process.env.SESSION_SECRET! }))
3753
+ *
3754
+ * app.get('/me', (ctx) => ({ user: ctx.session.get('user') }))
3755
+ * app.post('/login', async (ctx) => {
3756
+ * ctx.session.set('user', { id: 1 })
3757
+ * await ctx.session.regenerate() // mitigate session fixation
3758
+ * })
3759
+ * ```
3760
+ *
3761
+ * Security choices:
3762
+ * - HMAC-SHA-256 over the session id, base64url-encoded; verified with
3763
+ * `timingSafeEqual`.
3764
+ * - 144-bit (18-byte) random ids.
3765
+ * - Defaults: `HttpOnly`, `SameSite=Lax`, `Path=/`. Set `secure: true`
3766
+ * behind TLS to enable `Secure`.
3767
+ * - Tampered or unknown cookies silently issue a fresh session — never an
3768
+ * error response, since this is an attacker-influenced surface.
3769
+ */
3770
+ declare function sessionMiddleware(opts: SessionOptions): IngeniumMiddleware;
3771
+
3772
+ /**
3773
+ * In-process session store backed by a `Map`. Suitable for development and
3774
+ * single-instance deployments. NOT shared across workers/replicas.
3775
+ *
3776
+ * Expired entries are evicted lazily on access AND periodically by a
3777
+ * background sweep. The sweep timer is `unref()`'d so it never keeps the
3778
+ * Node process alive on its own.
3779
+ */
3780
+ declare class MemoryStore implements SessionStore {
3781
+ private readonly map;
3782
+ private readonly sweep;
3783
+ /**
3784
+ * @param sweepIntervalMs How often to scan the map for expired entries.
3785
+ * Defaults to 60s. Pass `0` to disable the timer entirely (tests).
3786
+ */
3787
+ constructor(sweepIntervalMs?: number);
3788
+ get(id: string): Promise<Record<string, unknown> | null>;
3789
+ set(id: string, data: Record<string, unknown>, ttlSeconds: number): Promise<void>;
3790
+ destroy(id: string): Promise<void>;
3791
+ touch(id: string, ttlSeconds: number): Promise<void>;
3792
+ /**
3793
+ * Stop the background sweep timer. Useful in tests / graceful shutdown.
3794
+ * After this call the store still works but expired entries are only
3795
+ * evicted on access.
3796
+ */
3797
+ stop(): void;
3798
+ /** @internal Test helper: number of live (non-expired) entries. */
3799
+ size(): number;
3800
+ private purge;
3801
+ }
3802
+
3803
+ /** Re-export the underlying `ws` `WebSocket` type for convenience. */
3804
+ type WebSocket = WebSocket$1;
3805
+ /**
3806
+ * Handler invoked when a client successfully upgrades to a WebSocket.
3807
+ *
3808
+ * `socket` is the `ws.WebSocket` instance. `ctx` is a minimal `IngeniumContext`
3809
+ * populated from the upgrade `IncomingMessage` — the body / response writers
3810
+ * are not meaningful for WS handlers (the upgrade has already happened).
3811
+ */
3812
+ type WebSocketHandler = (socket: WebSocket$1, ctx: IngeniumContext) => void | Promise<void>;
3813
+ /** Per-handler options forwarded to `WebSocketServer({ noServer: true, ... })`. */
3814
+ interface WebSocketHandlerOptions {
3815
+ /** Max payload size (bytes) for incoming frames. */
3816
+ maxPayload?: number;
3817
+ /** Enable permessage-deflate. Defaults to false (matches `ws` default). */
3818
+ perMessageDeflate?: boolean;
3819
+ }
3820
+ /** Bag passed to integrators (advanced). */
3821
+ interface WsIntegrator {
3822
+ (httpServer: node_http.Server): void;
3823
+ }
3824
+ /** Shape of the per-app registrar exposed to `enableWebSockets`. */
3825
+ interface WsRegistrar {
3826
+ add(path: string, handler: WebSocketHandler, options?: WebSocketHandlerOptions): void;
3827
+ attach(httpServer: node_http.Server): void;
3828
+ close(): Promise<void>;
3829
+ }
3830
+
3831
+ /**
3832
+ * WebSocket registrar — the small piece of state that holds path → handler
3833
+ * mappings and knows how to wire `'upgrade'` on a Node `http.Server`.
3834
+ *
3835
+ * Design: the `ws` package is loaded lazily via dynamic `import('ws')` so
3836
+ * apps that never use WebSockets pay no cost (no module load, no peer-dep
3837
+ * requirement). The first call to `attach()` resolves the import.
3838
+ */
3839
+
3840
+ /**
3841
+ * Attempt to detect whether `ws` is installed. Used by the test suite to
3842
+ * `describe.skipIf` the WS suite when the optional peer dep is missing.
3843
+ */
3844
+ declare function peerHasWs(): Promise<boolean>;
3845
+ /**
3846
+ * Build a registrar bound to an app. The registrar is intentionally
3847
+ * decoupled from `IngeniumApp` — the app calls `add()` from `app.ws()`, and
3848
+ * `enableWebSockets()` (or the app's `listen()` integration) calls `attach()`
3849
+ * once the underlying `http.Server` is created.
3850
+ */
3851
+ declare function createWebSocketRegistrar(): WsRegistrar;
3852
+
3853
+ /**
3854
+ * WebSocket-aware variant of `NodeAdapter`. Mirrors the behavior of
3855
+ * `transport/node.ts` (request handling, socket tracking, graceful close)
3856
+ * but exposes the underlying `http.Server` via an `onServerReady` callback
3857
+ * so the WS registrar can `.on('upgrade', …)` it.
3858
+ *
3859
+ * We did not modify the core `NodeAdapter` because the core has no awareness
3860
+ * of WebSockets; this adapter is opt-in via `enableWebSockets()`.
3861
+ */
3862
+
3863
+ type OnServerReady = (httpServer: Server$1) => void;
3864
+ declare class WsNodeAdapter implements Transport {
3865
+ private hooks;
3866
+ private readonly onServerReady;
3867
+ constructor(onServerReady: OnServerReady);
3868
+ attach(hooks: TransportHooks): void;
3869
+ listen(port: number, host?: string): Promise<ListeningServer>;
3870
+ }
3871
+
3872
+ /**
3873
+ * WebSocket adapter for Ingenium (optional `ws` peer dependency).
3874
+ *
3875
+ * # Usage
3876
+ * ```ts
3877
+ * import { ingenium } from 'ingenium'
3878
+ * import { enableWebSockets } from 'ingenium/ws'
3879
+ *
3880
+ * const app = ingenium()
3881
+ * enableWebSockets(app)
3882
+ * app.ws('/echo', (sock) => {
3883
+ * sock.on('message', (m) => sock.send(m))
3884
+ * })
3885
+ * await app.listen(3000)
3886
+ * ```
3887
+ *
3888
+ * # Why a monkey-patch?
3889
+ * `enableWebSockets(app)` augments the app instance with `app.ws()` and
3890
+ * wraps `app.listen()` so the registrar gets attached to the underlying
3891
+ * `http.Server` once it's bound. We chose this over extending `IngeniumApp` to
3892
+ * avoid pulling `./ws/middleware.ts` into the core import graph (which would
3893
+ * create a soft dep on `ws` types from every `app.ts` consumer). This is a
3894
+ * known pattern in WS-extending frameworks (e.g. `express-ws`).
3895
+ *
3896
+ * The trade-off: TypeScript can't statically see `app.ws` unless the
3897
+ * augmentation below is loaded. Importing this module both registers the
3898
+ * runtime patch AND adds the type augmentation to the global `IngeniumApp`.
3899
+ */
3900
+
3901
+ declare module '../app.ts' {
3902
+ interface IngeniumApp {
3903
+ ws(path: string, handler: WebSocketHandler, options?: WebSocketHandlerOptions): IngeniumApp;
3904
+ upgradeWith(integrator: WsIntegrator): IngeniumApp;
3905
+ }
3906
+ }
3907
+ /** Options for `enableWebSockets`. Reserved for future use. */
3908
+ interface EnableWebSocketsOptions {
3909
+ /**
3910
+ * When `true`, eagerly probes for the `ws` peer dependency at install
3911
+ * time and prints a warning if it is missing. Default: `false` (we wait
3912
+ * until the first upgrade attempt).
3913
+ */
3914
+ warnOnMissingPeer?: boolean;
3915
+ }
3916
+ /**
3917
+ * Augment a `IngeniumApp` with WebSocket support. Idempotent — calling more than
3918
+ * once on the same app is a no-op.
3919
+ */
3920
+ declare function enableWebSockets(app: IngeniumApp, opts?: EnableWebSocketsOptions): void;
3921
+
3922
+ /**
3923
+ * Registry for the framework's lifecycle hooks. Implements the `Hooks`
3924
+ * interface that plugins call into via `app.hooks`.
3925
+ *
3926
+ * # Execution model
3927
+ *
3928
+ * `runOn*` methods invoke listeners **sequentially** in registration order,
3929
+ * awaiting each one before invoking the next. This is intentional:
3930
+ *
3931
+ * - Predictable ordering: a hook registered first ALWAYS observes state
3932
+ * before a hook registered later. Plugins can rely on this.
3933
+ * - Backpressure: an async hook (e.g. fetching a session) blocks
3934
+ * subsequent hooks, ensuring downstream hooks see decorated state.
3935
+ * - Errors short-circuit `runOnRequest`/`runOnResponse`/`runOnCompose` —
3936
+ * they propagate to the caller (the request enters the error boundary).
3937
+ *
3938
+ * `runOnError` is the exception: it wraps each listener in a try/catch and
3939
+ * swallows throws, because observers must not mask the original error.
3940
+ *
3941
+ * # Reading order
3942
+ *
3943
+ * Within a single `run*` call, listeners run in the order they were added.
3944
+ * Across hook types within one request, the order is fixed by `app.handle`:
3945
+ *
3946
+ * onRequest -> (decorators applied) -> dispatch -> onResponse
3947
+ * \-> onError (on throw)
3948
+ *
3949
+ * # Hot-path note
3950
+ *
3951
+ * Each `runOn*` returns immediately if no listeners are registered. Callers
3952
+ * should additionally check `hasAny()` (or the per-hook `has*()` helpers) to
3953
+ * skip the `await` entirely on the zero-plugin path.
3954
+ */
3955
+ declare class HooksRegistry implements Hooks {
3956
+ private readonly _onRoute;
3957
+ private readonly _onCompose;
3958
+ private readonly _onRequest;
3959
+ private readonly _onResponse;
3960
+ private readonly _onError;
3961
+ onRoute(fn: OnRouteHook): void;
3962
+ onCompose(fn: OnComposeHook): void;
3963
+ onRequest(fn: OnRequestHook): void;
3964
+ onResponse(fn: OnResponseHook): void;
3965
+ onError(fn: OnErrorHook): void;
3966
+ /** True when any request-time hook is registered. */
3967
+ hasAny(): boolean;
3968
+ hasOnRequest(): boolean;
3969
+ hasOnResponse(): boolean;
3970
+ hasOnError(): boolean;
3971
+ hasOnRoute(): boolean;
3972
+ hasOnCompose(): boolean;
3973
+ /** Synchronous — `onRoute` is invoked during composition for each route. */
3974
+ runOnRoute(event: RegistrationEvent): void;
3975
+ runOnCompose(): Promise<void>;
3976
+ runOnRequest(ctx: IngeniumContext): Promise<void>;
3977
+ runOnResponse(ctx: IngeniumContext): Promise<void>;
3978
+ /** Observation only. Throws inside listeners are swallowed. */
3979
+ runOnError(err: unknown, ctx: IngeniumContext): Promise<void>;
3980
+ }
3981
+
3982
+ /**
3983
+ * Per-app registry of decorators. Decorators are NOT installed onto
3984
+ * `IngeniumContext.prototype` — that would mutate a shared class and leak across
3985
+ * apps in the same process. Instead, `applyTo(ctx)` writes them onto each
3986
+ * pooled context instance at request start.
3987
+ *
3988
+ * # Lazy vs eager — perf trade-off
3989
+ *
3990
+ * - **Lazy** (`decorate`): installed via `Object.defineProperty` with a
3991
+ * getter. The getter computes on first access, then redefines itself as
3992
+ * a plain data property holding the resolved value (define-self pattern).
3993
+ * Subsequent reads cost a normal property access — no getter call. Use
3994
+ * this for values that may not be needed (e.g. `ctx.user` on public
3995
+ * routes), and for values whose computation is non-trivial (DB lookups,
3996
+ * token decoding).
3997
+ *
3998
+ * - **Eager** (`decorateRequest`): factory is invoked at request start,
3999
+ * value assigned directly. Use this for cheap values that virtually every
4000
+ * handler will read (e.g. `ctx.startedAt = Date.now()`). Avoids the
4001
+ * per-property getter-redefinition overhead.
4002
+ *
4003
+ * # Pool reuse
4004
+ *
4005
+ * Pooled contexts are reset between requests; the `IngeniumContext.reset()`
4006
+ * method does not know about decorator names, so each request re-applies
4007
+ * via `applyTo(ctx)`. Lazy `defineProperty` overwrites the previous slot
4008
+ * configuration cleanly; eager assignment overwrites the previous value.
4009
+ * No leakage between requests.
4010
+ */
4011
+ declare class DecoratorRegistry {
4012
+ private readonly lazy;
4013
+ private readonly eager;
4014
+ /** Register a lazy decorator. Computed on first access; cached thereafter. */
4015
+ decorate<T>(name: string, factory: LazyDecorator<T>): void;
4016
+ /** Register an eager decorator. Factory runs at the start of every request. */
4017
+ decorateRequest<T>(name: string, factory: EagerDecorator<T>): void;
4018
+ /** True when any decorator is registered (lets the hot path skip work). */
4019
+ hasAny(): boolean;
4020
+ /**
4021
+ * Install all registered decorators onto a single context instance.
4022
+ * Called by `app.handle` after `onRequest` hooks and before dispatch.
4023
+ */
4024
+ applyTo(ctx: IngeniumContext): void;
4025
+ }
4026
+
4027
+ /**
4028
+ * `ScopedApp` — registration facade returned to the callback of
4029
+ * `app.scope(prefix, registrar)`. Translates every registration call into a
4030
+ * prefix-qualified registration on the underlying `IngeniumApp`, leveraging
4031
+ * the existing Router scoping primitives (`use-prefix`, prefix-prepended
4032
+ * paths) so the COMPOSE-TIME machinery does all the heavy lifting and the
4033
+ * per-request hot path stays untouched.
4034
+ *
4035
+ * # Design
4036
+ *
4037
+ * - Holds a reference to the root `IngeniumApp` plus the ABSOLUTE prefix
4038
+ * (already includes any outer scope prefix). Nested scopes just construct
4039
+ * a new `ScopedApp` with `parent.prefix + sub`.
4040
+ * - `scope.use(mw)` becomes `app.use(absolutePrefix, mw)` — the existing
4041
+ * `Router.use(prefix, mw)` plumbing produces a `use-prefix` registration,
4042
+ * and `flattenRouter` emits a `scopedMiddleware` entry that `app.compose()`
4043
+ * intersects against route paths via `pathStartsWith`.
4044
+ * - `scope.get(path, handler)` becomes `app.method('GET', absolutePrefix + path, handler)`.
4045
+ * - `scope.register(plugin, opts)` invokes the plugin with `this` as the
4046
+ * target. Any `target.use(...)` inside the plugin body is therefore
4047
+ * scope-prefixed; the plugin can't accidentally leak global middleware.
4048
+ * - `scope.scope(sub, fn)` constructs a child `ScopedApp` and runs `fn`
4049
+ * against it.
4050
+ *
4051
+ * # Out of scope for V1 (documented footguns)
4052
+ *
4053
+ * - **Decorators**: `scope.decorate(...)` / `scope.decorateRequest(...)`
4054
+ * forward to the root app and decorate EVERY request, not just requests
4055
+ * under the scope's prefix. The reason is structural: decorators install
4056
+ * onto pooled `IngeniumContext` instances at request start, BEFORE the
4057
+ * route is matched — there's no path information available at that point
4058
+ * without re-shaping the dispatch path. Per-scope decorators would require
4059
+ * either (a) a runtime path check on every property access, or (b) a
4060
+ * separate decorator registry per scope keyed by matched route — both of
4061
+ * which move work onto the hot path and complicate the pool. For V1 we
4062
+ * accept the footgun and emit a one-shot `process.emitWarning` in
4063
+ * non-production environments to surface it.
4064
+ * - **Hooks**: `scope.hooks` returns the SAME registry the root app uses.
4065
+ * Hook registration is global. A plugin that wants scope-aware hook
4066
+ * behavior should inspect `ctx.path` inside the hook body.
4067
+ */
4068
+
4069
+ /**
4070
+ * @internal Friend-access surface a `ScopedApp` needs from its parent
4071
+ * `IngeniumApp`. Kept narrow so refactors don't accidentally widen the
4072
+ * coupling. The methods are implemented on `IngeniumApp` itself.
4073
+ */
4074
+ interface ScopeHost {
4075
+ use(mw: IngeniumMiddleware): IngeniumApp;
4076
+ use(prefix: string, mw: IngeniumMiddleware | Router): IngeniumApp;
4077
+ method(method: HttpMethod, path: string, handler: IngeniumHandler): IngeniumApp;
4078
+ method(method: HttpMethod, path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): IngeniumApp;
4079
+ decorate<T>(name: string, factory: LazyDecorator<T>): IngeniumApp;
4080
+ decorateRequest<T>(name: string, factory: EagerDecorator<T>): IngeniumApp;
4081
+ readonly hooks: Hooks;
4082
+ /** @internal Marks the app's compose-cache dirty. */
4083
+ _markDirty(): void;
4084
+ }
4085
+ /**
4086
+ * A `ScopedApp` is the registration target passed to the `app.scope(prefix, registrar)`
4087
+ * callback. It exposes the registration surface a plugin needs (`use`, verbs,
4088
+ * `register`, `decorate`, `before`/`after`, nested `scope`) but NOT the
4089
+ * dispatch surface (`compose`, `handle`, `listen`) — those still belong to
4090
+ * the root app.
4091
+ *
4092
+ * Instances are cheap: a couple of fields and method-call forwarding. Do not
4093
+ * cache them across recompose boundaries — they hold a reference to the
4094
+ * `IngeniumApp` and rely on its mutable router journal.
4095
+ */
4096
+ declare class ScopedApp implements PluginTarget {
4097
+ /** @internal The root app this scope translates registrations onto. */
4098
+ private readonly _app;
4099
+ /** @internal Absolute prefix (already includes any outer scope's prefix). */
4100
+ private readonly _prefix;
4101
+ /** @internal Construct via `app.scope(...)`; not meant to be `new`'d directly. */
4102
+ constructor(app: ScopeHost, prefix: string);
4103
+ /** Absolute prefix this scope rewrites against (for debugging / introspection). */
4104
+ get prefix(): string;
4105
+ /** Lifecycle hooks. SHARED with the root app — hooks are global by design. */
4106
+ get hooks(): Hooks;
4107
+ use(mw: IngeniumMiddleware): this;
4108
+ use(subPrefix: string, mw: IngeniumMiddleware | Router): this;
4109
+ get(path: string, handler: IngeniumHandler): this;
4110
+ get(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4111
+ post(path: string, handler: IngeniumHandler): this;
4112
+ post(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4113
+ put(path: string, handler: IngeniumHandler): this;
4114
+ put(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4115
+ patch(path: string, handler: IngeniumHandler): this;
4116
+ patch(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4117
+ delete(path: string, handler: IngeniumHandler): this;
4118
+ delete(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4119
+ head(path: string, handler: IngeniumHandler): this;
4120
+ head(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4121
+ options(path: string, handler: IngeniumHandler): this;
4122
+ options(path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4123
+ method(method: HttpMethod, path: string, handler: IngeniumHandler): this;
4124
+ method(method: HttpMethod, path: string, ...args: [...IngeniumMiddleware[], IngeniumHandler]): this;
4125
+ /**
4126
+ * Chainable per-path builder. The builder closes over `this.method`, which
4127
+ * already does the scope-prefix join — so the same builder works identically
4128
+ * on the root app and inside a scope. Typed params via `ExtractParams<P>`
4129
+ * narrow against the RELATIVE path the user wrote, matching what the bare
4130
+ * verb form does.
4131
+ */
4132
+ route<P extends string>(path: P): RouteBuilder<P>;
4133
+ /**
4134
+ * Register a lazy decorator. **WARNING:** decorators are GLOBAL even when
4135
+ * registered inside a scope — they apply to every request regardless of
4136
+ * the scope's prefix. The first call from inside any scope in a process
4137
+ * emits a `process.emitWarning` (non-production only). See file header.
4138
+ */
4139
+ decorate<T>(name: string, factory: LazyDecorator<T>): this;
4140
+ /**
4141
+ * Register an eager decorator. **WARNING:** see {@link ScopedApp.decorate}.
4142
+ */
4143
+ decorateRequest<T>(name: string, factory: EagerDecorator<T>): this;
4144
+ /**
4145
+ * Register a plugin against THIS scope. The plugin receives the `ScopedApp`
4146
+ * as its `target`, so any `target.use(...)` inside the plugin body is
4147
+ * automatically prefix-scoped.
4148
+ */
4149
+ register<O>(plugin: IngeniumPlugin<O>, opts: O): Promise<this>;
4150
+ register(plugin: IngeniumPlugin<void>): Promise<this>;
4151
+ /**
4152
+ * Open a nested scope. `subPrefix` is relative to this scope's prefix.
4153
+ * The registrar may be async; the call returns a Promise that resolves
4154
+ * once the registrar finishes if it returned one, otherwise resolves
4155
+ * synchronously to `this`. We type-erase to `this` to match the
4156
+ * `PluginTarget` interface, which can't express the sync-or-async return
4157
+ * without polluting every caller.
4158
+ */
4159
+ scope(subPrefix: string, registrar: (scope: PluginTarget) => void): this;
4160
+ /**
4161
+ * Register a `before` filter scoped to this scope's prefix. If a pattern
4162
+ * is given it's appended to the scope's prefix (so `scope('/api').before('/users', h)`
4163
+ * matches `/api/users` and below). If omitted, the filter applies to the
4164
+ * scope's full subtree.
4165
+ */
4166
+ before(handler: IngeniumMiddleware): this;
4167
+ before(pattern: string, handler: IngeniumMiddleware): this;
4168
+ /** Register an `after` filter scoped to this scope's prefix. See {@link ScopedApp.before}. */
4169
+ after(handler: IngeniumMiddleware): this;
4170
+ after(pattern: string, handler: IngeniumMiddleware): this;
4171
+ }
4172
+
4173
+ /**
4174
+ * Sinatra-style top-level shorthand.
4175
+ *
4176
+ * Lets users skip the app object entirely:
4177
+ *
4178
+ * ```ts
4179
+ * import { get, post, listen } from 'ingenium'
4180
+ *
4181
+ * get('/', () => 'hi')
4182
+ * get('/users/:id', (ctx) => ({ id: ctx.params.id }))
4183
+ * post('/echo', async (ctx) => ctx.body.json())
4184
+ *
4185
+ * await listen(3000)
4186
+ * ```
4187
+ *
4188
+ * All exported verbs route to a lazy singleton `IngeniumApp` created on first
4189
+ * call. The instance is retained for the lifetime of the process; tests can
4190
+ * call `_resetDefaultApp()` to drop it (this throws in production).
4191
+ */
4192
+
4193
+ /**
4194
+ * Get the lazy default app. Created on first call, retained for the
4195
+ * lifetime of the process (or until `_resetDefaultApp()` is invoked).
4196
+ *
4197
+ * The same instance is returned on every subsequent call, so all
4198
+ * top-level verb functions and `listen()` operate on a single coherent
4199
+ * registration journal.
4200
+ */
4201
+ declare function defaultApp(): IngeniumApp;
4202
+ /**
4203
+ * Reset the default app — for tests only. The next call to any top-level
4204
+ * function will lazily create a fresh `IngeniumApp`. Throws when
4205
+ * `NODE_ENV === 'production'` so accidental production calls are loud.
4206
+ */
4207
+ declare function _resetDefaultApp(): void;
4208
+ declare function get(path: string, handler: IngeniumHandler): IngeniumApp;
4209
+ declare function post(path: string, handler: IngeniumHandler): IngeniumApp;
4210
+ declare function put(path: string, handler: IngeniumHandler): IngeniumApp;
4211
+ declare function patch(path: string, handler: IngeniumHandler): IngeniumApp;
4212
+ /**
4213
+ * Default-app shorthand for `app.delete(path, handler)`.
4214
+ * Exported as `del` because `delete` is a reserved word in JavaScript and
4215
+ * cannot be used as a top-level identifier. `index.ts` re-exports this as
4216
+ * `{ del as delete }` so the public name is `delete`.
4217
+ */
4218
+ declare function del(path: string, handler: IngeniumHandler): IngeniumApp;
4219
+ declare function head(path: string, handler: IngeniumHandler): IngeniumApp;
4220
+ declare function options(path: string, handler: IngeniumHandler): IngeniumApp;
4221
+ /**
4222
+ * Mount middleware on the default app. Same overload set as `app.use`:
4223
+ * - `use(mw)` — global
4224
+ * - `use(prefix, mw | Router)` — prefix-scoped
4225
+ */
4226
+ declare function use(mw: IngeniumMiddleware): IngeniumApp;
4227
+ declare function use(prefix: string, mw: IngeniumMiddleware | Router): IngeniumApp;
4228
+ /** Default-app shorthand for `app.onError(handler)`. */
4229
+ declare function onError(handler: IngeniumErrorHandler): IngeniumApp;
4230
+ /**
4231
+ * Bind the default app to a port. Returns a `ListeningServer` whose
4232
+ * `.close()` shuts down the underlying transport. Pass `0` for an
4233
+ * ephemeral port (useful in tests).
4234
+ */
4235
+ declare function listen(port: number, host?: string): Promise<ListeningServer>;
4236
+ declare function before(handler: IngeniumMiddleware): IngeniumApp;
4237
+ declare function before(pattern: string, handler: IngeniumMiddleware): IngeniumApp;
4238
+ declare function after(handler: IngeniumMiddleware): IngeniumApp;
4239
+ declare function after(pattern: string, handler: IngeniumMiddleware): IngeniumApp;
4240
+
4241
+ /**
4242
+ * Ingenium — Express DX, Hono/Fastify throughput.
4243
+ *
4244
+ * @packageDocumentation
4245
+ */
4246
+
4247
+ declare const ingenium: IngeniumFactory & {
4248
+ json: typeof jsonMiddleware;
4249
+ urlencoded: typeof urlencodedMiddleware;
4250
+ static: typeof staticMiddleware;
4251
+ cors: typeof corsMiddleware;
4252
+ csrf: typeof csrfMiddleware;
4253
+ sse: typeof sse;
4254
+ rateLimit: typeof rateLimit;
4255
+ problemDetails: typeof problemDetailsMiddleware;
4256
+ idempotency: typeof idempotencyMiddleware;
4257
+ jwt: typeof jwtMiddleware;
4258
+ apiKey: typeof apiKeyMiddleware;
4259
+ openapiHandler: typeof openapiHandler;
4260
+ };
4261
+
4262
+ export { type ApiKeyLogger, type ApiKeyOptions, type ApiKeyValidator, type CachedResponse, type CloseOptions, type ComposedHandler, type CookieGetOptions, type CookieSetOptions, type CorsOptions, type CorsOrigin, type CorsOriginFn, type CronHandler, type CronMatch, type CronOptions, CronRegistry, type CsrfCookieOptions, type CsrfOptions, type CsrfStorage, type CsrfValueReader, type Decorator, DecoratorRegistry, type EagerDecorator, type EnableWebSocketsOptions, type ExtractParams, type FailedJob, type FormatHandlers, type FormattableCtx, type ForwardedInfo, type GenerateOpenApiOptions, HTTP_METHODS, type HeaderBag, type Hooks, HooksRegistry, Http2Adapter, type Http2AdapterOptions, Http2cAdapter, type HttpMethod, IdempotencyMemoryStore, type IdempotencyOptions, type IdempotencyStore, IngeniumApp, type IngeniumAppOptions, IngeniumBadRequestError, IngeniumBody, IngeniumContext, IngeniumContextPool, type IngeniumCookies, IngeniumCronJob, IngeniumCsrfError, IngeniumError, type IngeniumErrorHandler, IngeniumHaltError, type IngeniumHandler, IngeniumHeaderInjectionError, IngeniumMethodNotAllowedError, type IngeniumMiddleware, IngeniumNotFoundError, IngeniumPayloadTooLargeError, type IngeniumPlugin, type IngeniumQuery, IngeniumQueue, IngeniumTimeoutError, IngeniumUnauthorizedError, IngeniumUnserializableError, IngeniumValidationError, type InjectRequest, type InjectResponse, type JobHandle, type JsonEtagCtx, type JsonEtagOptions, type JwtAlgorithm, type JwtHeader, type JwtKey, type JwtLogger, type JwtOptions, type JwtSecret, type JwtSecretResolver, type JwtTokenReader, type JwtVerified, type LazyDecorator, type ListeningServer, type MatchMiss, type MatchResult, MemoryQueueStore, type MultipartFile, type MultipartOptions, type MultipartResult, type NegotiableCtx, NodeAdapter, type OnComposeHook, type OnErrorHook, type OnRequestHook, type OnResponseHook, type OnRouteHook, type Components as OpenApiComponents, type Info as OpenApiInfo, type Response as OpenApiResponse, type Schema as OpenApiSchema, type SecurityRequirement as OpenApiSecurityRequirement, type SecurityScheme as OpenApiSecurityScheme, type Server as OpenApiServer, type OpenApiSpec, type Tag as OpenApiTag, type Operation, type Parameter, type ParseSchema, type ParsedAccept, type PathItem, type PluginTarget, type ProblemDetails, type ProblemDetailsOptions, type QueueOptions, QueueRegistry, type QueueStore, type QueueWorker, MemoryStore$1 as RateLimitMemoryStore, type RateLimitOptions, type RateLimitStore, type RegisteredQueue, type RegistrationEvent, type RequestBody, type ResponseBody, type RetryPolicy, RouteBuilder, type RouteDescriptor, type RouteOptions, Router, RouterTrie, type SafeJsonStringifyOptions, type SafeParseSchema, ScopedApp, type Session, type SessionCookieOptions, MemoryStore as SessionMemoryStore, type SessionOptions, type SessionStore, type ShutdownOptions, type SseEvent, type SseStream, type StandardFailureResult, type StandardIssue, type StandardPathSegment, type StandardResult, type StandardSchemaV1, type StandardSchemaV1Props, type StandardSuccessResult, type StaticOptions, type Transport, type TransportHooks, TrieNode, type TrustProxy, type WebSocket, type WebSocketHandler, type WebSocketHandlerOptions, type WsIntegrator, WsNodeAdapter, type WsRegistrar, _resetDefaultApp, accepts, acceptsCharsets, acceptsEncodings, acceptsLanguages, after, apiKeyMiddleware, before, clearJwksCache, compose, composeWithHandler, computeEtag, corsMiddleware as cors_, createWebSocketRegistrar, csrfMiddleware, ingenium as default, defaultApp, del as delete, enableWebSockets, expandShorthand, fetchJwks, formatResponse, generateOpenApi, get, gracefulShutdown, head, idempotencyMiddleware, ingenium, isFresh, isStandardSchema, jwtMiddleware, listen, nextFireFrom, onError, openapiHandler, options, parseAcceptHeader, parseCronSpec, patch, peerHasWs, post, problemDetailsMiddleware, put, rateLimit, resolveForwarded, respondJsonWithEtag, safeJsonStringify, selectBest, sessionMiddleware, sortByPreference, sse, startKeepAlive, staticMiddleware as static_, toProblemDetails, use, verifyJwt };