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,309 @@
1
+ import type { ComposedHandler } from '../middleware/types.ts'
2
+ import type { HttpMethod } from './types.ts'
3
+
4
+ /**
5
+ * One node in the radix trie. Static segments win over `:param`, which wins
6
+ * over `*wild`. Method-specific composed handlers live at the leaf.
7
+ */
8
+ export class TrieNode {
9
+ staticChildren: Map<string, TrieNode> = new Map()
10
+ paramChild: TrieNode | null = null
11
+ paramName: string | null = null
12
+ wildcardChild: TrieNode | null = null
13
+ wildcardName: string | null = null
14
+
15
+ /**
16
+ * Compiled inline constraint for this node *as a param child*, or `null`
17
+ * when the param is unconstrained. Set at insert time when the registered
18
+ * segment carries a `(regex)` group (e.g. `:id(\d+)`). The `find()` hot
19
+ * path loads this field and only runs `.test()` when it is non-null, so
20
+ * unconstrained routes pay zero extra cost. Lives on the param node itself
21
+ * (the child) so the matcher can test it the instant it descends.
22
+ */
23
+ paramConstraint: RegExp | null = null
24
+
25
+ /** Per-method composed handlers, populated by `RouteRegistry` after compose. */
26
+ handlers: Partial<Record<HttpMethod, ComposedHandler>> = {}
27
+
28
+ /**
29
+ * Param names accumulated from root → this node, in order. Cached so
30
+ * matching can fill the params object in O(k) without re-walking parents.
31
+ */
32
+ paramNames: readonly string[] = []
33
+ }
34
+
35
+ /** Result of a trie lookup. `params` may be empty if the route had none. */
36
+ export interface MatchResult {
37
+ handler: ComposedHandler
38
+ params: Record<string, string>
39
+ /** Methods registered at this leaf — used to populate `Allow` on 405. */
40
+ allowed: readonly HttpMethod[]
41
+ }
42
+
43
+ /** Why a lookup failed. */
44
+ export type MatchMiss =
45
+ | { kind: 'not-found' }
46
+ | { kind: 'method-not-allowed'; allowed: readonly HttpMethod[] }
47
+
48
+ /**
49
+ * Radix trie router. `insert()` is called at registration; `find()` runs on
50
+ * every request and is the single hottest piece of code in the framework.
51
+ */
52
+ export class RouterTrie {
53
+ readonly root = new TrieNode()
54
+
55
+ /**
56
+ * Walks/creates trie nodes for the path. Returns the leaf where handlers
57
+ * should be attached. Path must start with `/`.
58
+ */
59
+ insert(path: string): TrieNode {
60
+ if (path.length === 0 || path[0] !== '/') {
61
+ throw new Error(`Route path must start with '/': ${path}`)
62
+ }
63
+ const segments = splitPath(path)
64
+ let node = this.root
65
+ const paramNames: string[] = []
66
+
67
+ for (const seg of segments) {
68
+ if (seg.length === 0) continue
69
+
70
+ if (seg[0] === ':') {
71
+ const { name, constraint } = parseParamSegment(seg)
72
+ if (!node.paramChild) {
73
+ node.paramChild = new TrieNode()
74
+ node.paramName = name
75
+ node.paramChild.paramConstraint = constraint
76
+ } else {
77
+ if (node.paramName !== name) {
78
+ throw new Error(
79
+ `Conflicting param names at the same trie level: ':${node.paramName}' vs ':${name}'`,
80
+ )
81
+ }
82
+ // Same name, but the constraint may differ. Rule: a constraint is a
83
+ // promise about the shape of matched segments; two registrations of
84
+ // the same param must agree on that promise. We require the *source*
85
+ // of the compiled regex to match (or both to be unconstrained).
86
+ // Last-writer-wins would silently let one route's `:id(\d+)` weaken
87
+ // another's, which is a footgun, so we throw instead — same style as
88
+ // the param-name conflict above.
89
+ const existing = node.paramChild.paramConstraint
90
+ const incoming = constraint
91
+ const existingSrc = existing ? existing.source : ''
92
+ const incomingSrc = incoming ? incoming.source : ''
93
+ if (existingSrc !== incomingSrc) {
94
+ const fmt = (n: string, c: RegExp | null) => (c ? `:${n}(...)` : `:${n}`)
95
+ throw new Error(
96
+ `Conflicting param constraints at the same trie level: ` +
97
+ `'${fmt(name, existing)}' vs '${fmt(name, incoming)}' for param ':${name}'`,
98
+ )
99
+ }
100
+ }
101
+ paramNames.push(name)
102
+ node = node.paramChild
103
+ } else if (seg[0] === '*') {
104
+ const name = seg.slice(1) || 'wildcard'
105
+ if (!node.wildcardChild) {
106
+ node.wildcardChild = new TrieNode()
107
+ node.wildcardName = name
108
+ }
109
+ paramNames.push(name)
110
+ node = node.wildcardChild
111
+ // Wildcards consume the rest of the path; later segments are ignored
112
+ // by the matcher anyway, but we don't allow more registration past *.
113
+ break
114
+ } else {
115
+ let child = node.staticChildren.get(seg)
116
+ if (!child) {
117
+ child = new TrieNode()
118
+ node.staticChildren.set(seg, child)
119
+ }
120
+ node = child
121
+ }
122
+ }
123
+
124
+ node.paramNames = paramNames
125
+ return node
126
+ }
127
+
128
+ /**
129
+ * Look up a route. Iterative with single-level wildcard backtrack — if the
130
+ * static/param walk dead-ends and an ancestor had a `*wildcard` child, we
131
+ * retry from the wildcard with the remaining segments. Backtrack frames
132
+ * are tracked in a small stack (one per wildcard ancestor encountered).
133
+ */
134
+ find(method: HttpMethod, path: string): MatchResult | MatchMiss {
135
+ const segments = splitPath(path)
136
+
137
+ // Stack of wildcard fallback points. `paramCount` is paramValues.length
138
+ // captured at the moment the fallback was recorded — used to truncate
139
+ // any params collected past that point if we have to backtrack.
140
+ type Fallback = { node: TrieNode; segIdx: number; paramCount: number }
141
+ const fallbacks: Fallback[] = []
142
+
143
+ let node: TrieNode = this.root
144
+ const paramValues: string[] = []
145
+ let consumedWildcard = false
146
+
147
+ let i = 0
148
+ walk: while (i < segments.length) {
149
+ const seg = segments[i]!
150
+ if (seg.length === 0) {
151
+ i++
152
+ continue
153
+ }
154
+
155
+ // Record a wildcard fallback at this level *before* descending, so a
156
+ // later miss can rewind and consume from `i` greedily via the wildcard.
157
+ if (node.wildcardChild) {
158
+ fallbacks.push({ node: node.wildcardChild, segIdx: i, paramCount: paramValues.length })
159
+ }
160
+
161
+ const staticChild = node.staticChildren.get(seg)
162
+ if (staticChild) {
163
+ node = staticChild
164
+ i++
165
+ continue
166
+ }
167
+
168
+ if (node.paramChild) {
169
+ // Hot-path gate: only constrained params (a tiny minority of routes)
170
+ // run a regex. The field load + `!== null` is one branch; unconstrained
171
+ // routes never touch `.test()`, so they pay zero extra cost. Constrained
172
+ // routes pay one anchored `.test()` against the raw segment — justified
173
+ // because the alternative (matching, then 404ing in user code) is both
174
+ // slower and wrong (a sibling `*wild` could legitimately catch it).
175
+ const constraint = node.paramChild.paramConstraint
176
+ if (constraint === null || constraint.test(seg)) {
177
+ paramValues.push(decodeParam(seg))
178
+ node = node.paramChild
179
+ i++
180
+ continue
181
+ }
182
+ // Constraint miss: this param branch is dead. Fall through to the
183
+ // wildcard child / backtrack stack exactly as a structural dead-end
184
+ // would, so a sibling `*wild` can still catch the segment, else 404.
185
+ }
186
+
187
+ if (node.wildcardChild) {
188
+ const remaining = segments.slice(i).join('/')
189
+ paramValues.push(decodeParam(remaining))
190
+ node = node.wildcardChild
191
+ consumedWildcard = true
192
+ break walk
193
+ }
194
+
195
+ // Dead end — try the most recent wildcard fallback.
196
+ const fb = fallbacks.pop()
197
+ if (!fb) return { kind: 'not-found' }
198
+ const remaining = segments.slice(fb.segIdx).join('/')
199
+ paramValues.length = fb.paramCount
200
+ paramValues.push(decodeParam(remaining))
201
+ node = fb.node
202
+ consumedWildcard = true
203
+ break walk
204
+ }
205
+
206
+ if (!consumedWildcard && !node.handlers[method] && fallbacks.length > 0) {
207
+ // Walked to the end via static/param but no handler at this leaf —
208
+ // try the most recent wildcard fallback.
209
+ const fb = fallbacks.pop()!
210
+ const remaining = segments.slice(fb.segIdx).join('/')
211
+ paramValues.length = fb.paramCount
212
+ paramValues.push(decodeParam(remaining))
213
+ node = fb.node
214
+ }
215
+
216
+ const handler = node.handlers[method]
217
+ if (!handler) {
218
+ const allowed = Object.keys(node.handlers) as HttpMethod[]
219
+ if (allowed.length === 0) return { kind: 'not-found' }
220
+ return { kind: 'method-not-allowed', allowed }
221
+ }
222
+
223
+ // Build params object — one allocation per match. Stable key insertion
224
+ // order (driven by paramNames recorded at insert time) → V8 monomorphic
225
+ // hidden class per route.
226
+ let params: Record<string, string>
227
+ if (node.paramNames.length === 0) {
228
+ params = EMPTY_PARAMS
229
+ } else {
230
+ params = {}
231
+ for (let j = 0; j < node.paramNames.length; j++) {
232
+ params[node.paramNames[j]!] = paramValues[j]!
233
+ }
234
+ }
235
+
236
+ return {
237
+ handler,
238
+ params,
239
+ allowed: Object.keys(node.handlers) as HttpMethod[],
240
+ }
241
+ }
242
+ }
243
+
244
+ /** Shared frozen empty-params sentinel — exported so the dispatcher can identity-compare. */
245
+ export const EMPTY_PARAMS: Record<string, string> = Object.freeze({}) as Record<string, string>
246
+
247
+ /**
248
+ * Split `/users/42/posts` into `['users', '42', 'posts']`. Reused by both
249
+ * insert and lookup, so the implementation is hot — manual scan beats
250
+ * `String.prototype.split` only marginally; we use split for clarity.
251
+ */
252
+ function splitPath(path: string): string[] {
253
+ // Strip leading and trailing slash for a stable segment count.
254
+ let start = 0
255
+ let end = path.length
256
+ if (start < end && path[start] === '/') start++
257
+ if (end > start && path[end - 1] === '/') end--
258
+ if (start >= end) return []
259
+ return path.slice(start, end).split('/')
260
+ }
261
+
262
+ /**
263
+ * Parse a `:param` segment into its clean name and an optional compiled
264
+ * constraint. Runs at *insert* time only (never on the request hot path), so
265
+ * the regex compile cost is paid once per route.
266
+ *
267
+ * Grammar handled:
268
+ * `:name` → { name: 'name', constraint: null }
269
+ * `:name?` → { name: 'name', constraint: null }
270
+ * `:name(regex)` → { name: 'name', constraint: /^(?:regex)$/ }
271
+ * `:name(regex)?` → { name: 'name', constraint: /^(?:regex)$/ }
272
+ *
273
+ * The constraint is anchored with `^(?:...)$` so it must match the *entire*
274
+ * segment — a partial match (e.g. `\d+` against `12a`) does NOT slip through.
275
+ * The `(?:...)` wrapper keeps the user's alternations (`a|b`) from binding
276
+ * past the anchors.
277
+ */
278
+ function parseParamSegment(seg: string): { name: string; constraint: RegExp | null } {
279
+ // Strip the leading ':'.
280
+ let body = seg.slice(1)
281
+
282
+ // Strip a trailing optional marker first; it sits *after* the constraint
283
+ // group in the documented grammar (`:id(\d+)?`).
284
+ if (body.length > 0 && body[body.length - 1] === '?') {
285
+ body = body.slice(0, -1)
286
+ }
287
+
288
+ // Detect a constraint group: `name(regex)`. The regex body is everything
289
+ // between the first '(' and the final ')'.
290
+ const open = body.indexOf('(')
291
+ if (open !== -1 && body[body.length - 1] === ')') {
292
+ const name = body.slice(0, open)
293
+ const pattern = body.slice(open + 1, -1)
294
+ // Anchor fully so the constraint governs the whole segment.
295
+ return { name, constraint: new RegExp(`^(?:${pattern})$`) }
296
+ }
297
+
298
+ return { name: body, constraint: null }
299
+ }
300
+
301
+ function decodeParam(raw: string): string {
302
+ // Hot path: skip decode if no '%' present.
303
+ if (raw.indexOf('%') === -1) return raw
304
+ try {
305
+ return decodeURIComponent(raw)
306
+ } catch {
307
+ return raw
308
+ }
309
+ }
@@ -0,0 +1,54 @@
1
+ /** HTTP methods supported by the router. */
2
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
3
+
4
+ export const HTTP_METHODS: readonly HttpMethod[] = [
5
+ 'GET',
6
+ 'POST',
7
+ 'PUT',
8
+ 'PATCH',
9
+ 'DELETE',
10
+ 'HEAD',
11
+ 'OPTIONS',
12
+ ] as const
13
+
14
+ /**
15
+ * Recursively extracts named params from a path string at the type level.
16
+ *
17
+ * - `:name` → required string
18
+ * - `:name?` → optional string (becomes `string | undefined`)
19
+ * - `:name(regex)` → required string. The regex is type-stripped here, but
20
+ * the constraint IS enforced at runtime by the trie
21
+ * (`RouterTrie.find` tests the segment against the
22
+ * compiled, fully-anchored pattern), so the `string`
23
+ * type is honest about the matched shape.
24
+ * Note: number-narrowing (typing `:id(\d+)` as `number`)
25
+ * remains deferred — constrained params stay `string`.
26
+ * - `*name` → required string (greedy wildcard tail)
27
+ *
28
+ * @example
29
+ * type P = ExtractParams<'/users/:id(\\d+)/posts/:slug?'>
30
+ * // { id: string; slug?: string | undefined }
31
+ */
32
+ export type ExtractParams<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}`
33
+ ? ParamRecord<Param> & ExtractParams<`/${Rest}`>
34
+ : Path extends `${string}:${infer Param}`
35
+ ? ParamRecord<Param>
36
+ : Path extends `${string}*${infer Wild}`
37
+ ? { [K in Wild]: string }
38
+ : EmptyParams
39
+
40
+ type EmptyParams = Record<string, never>
41
+
42
+ /**
43
+ * Drop a single parenthesized constraint group from a param name.
44
+ * `id(\\d+)` → `id`
45
+ * `id(\\d+)?` → `id?` (optionality marker preserved for ParamRecord)
46
+ * `id` → `id` (no-op when no constraint present)
47
+ */
48
+ type StripConstraint<P extends string> = P extends `${infer Head}(${string})${infer Tail}`
49
+ ? `${Head}${Tail}`
50
+ : P
51
+
52
+ type ParamRecord<P extends string> = StripConstraint<P> extends `${infer Name}?`
53
+ ? { [K in Name]?: string }
54
+ : { [K in StripConstraint<P>]: string }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Local, zero-dependency type definitions for the
3
+ * [Standard Schema](https://standardschema.dev) v1 spec.
4
+ *
5
+ * Ingenium detects schemas implementing this contract on
6
+ * `IngeniumBody.json(schema)` and runs their `validate` function, mapping
7
+ * `issues` into a `IngeniumValidationError` with field-level messages.
8
+ *
9
+ * We intentionally do NOT import `@standard-schema/spec` to keep the
10
+ * core dependency-free. These types mirror the spec exactly.
11
+ */
12
+
13
+ /** A successful validation result: parsed/transformed value. */
14
+ export interface StandardSuccessResult<TOut> {
15
+ readonly value: TOut
16
+ readonly issues?: undefined
17
+ }
18
+
19
+ /** A single issue describing why validation failed at a particular path. */
20
+ export interface StandardIssue {
21
+ readonly message: string
22
+ readonly path?: ReadonlyArray<PropertyKey | StandardPathSegment> | undefined
23
+ }
24
+
25
+ /** A path segment may be a bare key OR an object with a `key` property. */
26
+ export interface StandardPathSegment {
27
+ readonly key: PropertyKey
28
+ }
29
+
30
+ /** A failed validation result: one or more issues. */
31
+ export interface StandardFailureResult {
32
+ readonly issues: ReadonlyArray<StandardIssue>
33
+ readonly value?: undefined
34
+ }
35
+
36
+ /** Standard Schema validation result: success XOR failure. */
37
+ export type StandardResult<TOut> = StandardSuccessResult<TOut> | StandardFailureResult
38
+
39
+ /** The properties living under the `~standard` key. */
40
+ export interface StandardSchemaV1Props<TIn = unknown, TOut = TIn> {
41
+ readonly version: 1
42
+ readonly vendor: string
43
+ readonly validate: (input: unknown) => StandardResult<TOut> | Promise<StandardResult<TOut>>
44
+ readonly types?: {
45
+ readonly input: TIn
46
+ readonly output: TOut
47
+ } | undefined
48
+ }
49
+
50
+ /** The Standard Schema v1 interface — anything with a `~standard` property. */
51
+ export interface StandardSchemaV1<TIn = unknown, TOut = TIn> {
52
+ readonly '~standard': StandardSchemaV1Props<TIn, TOut>
53
+ }
54
+
55
+ /**
56
+ * Type guard: is `x` a Standard Schema v1?
57
+ *
58
+ * Checks for the `~standard` property and that its `version` is `1` and
59
+ * `validate` is a function. Cheap enough to call on every body.json() call.
60
+ */
61
+ export function isStandardSchema(x: unknown): x is StandardSchemaV1 {
62
+ if (x === null || (typeof x !== 'object' && typeof x !== 'function')) return false
63
+ const std = (x as { '~standard'?: unknown })['~standard']
64
+ if (std === null || typeof std !== 'object') return false
65
+ const props = std as { version?: unknown; validate?: unknown }
66
+ return props.version === 1 && typeof props.validate === 'function'
67
+ }