castrum 0.7.0 → 0.9.0

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 (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
@@ -0,0 +1,102 @@
1
+ // src/ingress/headers/baked-templates.ts — Header-template builder (pre-baked
2
+ // path).
3
+ //
4
+ // Precomputes the 32 header-variant templates used by the pre-baked handler
5
+ // path (`ratelimit-*` header names, benchmark wire format).
6
+ //
7
+ // NOTE: this is the pre-baked-path template builder. The fast path has its own
8
+ // builder (./fast-templates.ts) with a different wire format (x-ratelimit-*) —
9
+ // do not unify them (see AGENTS.md).
10
+
11
+ import { HV_JSON, HV_CORS_SIMPLE, HV_CORS_PREFLIGHT, HV_RATE_ACTIVE } from '../constants'
12
+
13
+ /** Inputs needed to build the pre-baked header templates. */
14
+ export interface BakedTemplateParams {
15
+ securityEntries: ReadonlyArray<[string, string]>
16
+ cors?: {
17
+ allowOrigin?: string[]
18
+ allowMethods?: string[]
19
+ allowHeaders?: string[]
20
+ exposeHeaders?: string[]
21
+ allowCredentials?: boolean
22
+ maxAge?: number
23
+ }
24
+ corsAllowMethods: string
25
+ corsAllowHeaders: string
26
+ corsExposeHeaders: string
27
+ corsMaxAge: string
28
+ rateLimitStr: string
29
+ }
30
+
31
+ /**
32
+ * Build the frozen, variant-indexed header templates.
33
+ *
34
+ * Returns two 32-entry sets:
35
+ * - `regular`: the base templates.
36
+ * - `terminal`: every regular template with `cache-control: no-store` appended
37
+ * (the header set used for terminal/error responses). Baking it in means the
38
+ * steady-state error path can return a frozen array with no per-response
39
+ * allocation or copy.
40
+ */
41
+ export function buildBakedHeaderTemplates(params: BakedTemplateParams): {
42
+ regular: ReadonlyArray<ReadonlyArray<[string, string]>>
43
+ terminal: ReadonlyArray<ReadonlyArray<[string, string]>>
44
+ } {
45
+ const {
46
+ securityEntries,
47
+ cors,
48
+ corsAllowMethods,
49
+ corsAllowHeaders,
50
+ corsExposeHeaders,
51
+ corsMaxAge,
52
+ rateLimitStr,
53
+ } = params
54
+
55
+ const regular = Object.freeze(
56
+ Array.from({ length: 32 }, (_, variant) => {
57
+ const entries: [string, string][] = [...securityEntries]
58
+
59
+ if ((variant & HV_JSON) !== 0) {
60
+ entries.push(['content-type', 'application/json'])
61
+ }
62
+
63
+ if ((variant & HV_CORS_SIMPLE) !== 0) {
64
+ entries.push(['vary', 'Origin'])
65
+ if (cors?.allowCredentials) {
66
+ entries.push(['access-control-allow-credentials', 'true'])
67
+ }
68
+ if (corsExposeHeaders.length > 0) {
69
+ entries.push(['access-control-expose-headers', corsExposeHeaders])
70
+ }
71
+ }
72
+
73
+ if ((variant & HV_CORS_PREFLIGHT) !== 0) {
74
+ entries.push([
75
+ 'vary',
76
+ 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers',
77
+ ])
78
+ if (cors?.allowCredentials) {
79
+ entries.push(['access-control-allow-credentials', 'true'])
80
+ }
81
+ entries.push(['access-control-allow-methods', corsAllowMethods])
82
+ entries.push(['access-control-allow-headers', corsAllowHeaders])
83
+ entries.push(['access-control-max-age', corsMaxAge])
84
+ }
85
+
86
+ if ((variant & HV_RATE_ACTIVE) !== 0) {
87
+ entries.push(['ratelimit-limit', rateLimitStr])
88
+ }
89
+
90
+ return Object.freeze(entries)
91
+ }),
92
+ )
93
+
94
+ const terminal = Object.freeze(
95
+ regular.map((t) => {
96
+ const entries: [string, string][] = [...t, ['cache-control', 'no-store']]
97
+ return Object.freeze(entries)
98
+ }),
99
+ )
100
+
101
+ return { regular, terminal }
102
+ }
@@ -0,0 +1,44 @@
1
+ // src/ingress/headers/cors.ts — CORS static-string computation (fast path).
2
+ //
3
+ // Precomputes the static parts of the CORS policy (joined allow-lists, wildcard
4
+ // detection, credentials flag) once per handler so per-request header building
5
+ // never re-joins arrays.
6
+
7
+ /** User-facing CORS configuration. */
8
+ export interface CorsOptions {
9
+ allowOrigin?: string[]
10
+ allowMethods?: string[]
11
+ allowHeaders?: string[]
12
+ exposeHeaders?: string[]
13
+ allowCredentials?: boolean
14
+ maxAge?: number
15
+ }
16
+
17
+ /** Precomputed, immutable CORS policy strings. */
18
+ export interface CorsStaticStrings {
19
+ readonly allowMethodsJoined: string
20
+ readonly allowHeadersJoined: string
21
+ readonly exposeHeadersJoined: string
22
+ readonly maxAgeString: string | null
23
+ readonly isWildcard: boolean
24
+ readonly credentials: boolean
25
+ }
26
+
27
+ /** Compute the static CORS strings for a config, or `null` when disabled. */
28
+ export function buildCorsStaticStrings(cors: CorsOptions | undefined): CorsStaticStrings | null {
29
+ if (!cors) return null
30
+
31
+ const isWildcard =
32
+ !cors.allowOrigin || cors.allowOrigin.length === 0 || cors.allowOrigin.includes('*')
33
+
34
+ return {
35
+ allowMethodsJoined: cors.allowMethods?.length
36
+ ? cors.allowMethods.join(', ')
37
+ : 'GET, HEAD, POST',
38
+ allowHeadersJoined: cors.allowHeaders?.length ? cors.allowHeaders.join(', ') : '',
39
+ exposeHeadersJoined: cors.exposeHeaders?.length ? cors.exposeHeaders.join(', ') : '',
40
+ maxAgeString: typeof cors.maxAge === 'number' ? String(cors.maxAge) : null,
41
+ isWildcard,
42
+ credentials: cors.allowCredentials === true,
43
+ }
44
+ }
@@ -0,0 +1,238 @@
1
+ // src/ingress/headers/fast-templates.ts — Header-template system (fast path).
2
+ //
3
+ // Precomputes, per ingress handler, the full cross product of header variants
4
+ // (HV_* bits) → header pairs, plus the per-request builder that applies the
5
+ // dynamic parts (origin echo, rate-limit counters, dynamic HSTS, request-id).
6
+ //
7
+ // NOTE: this is the fast-path (x-ratelimit-* / {"error":{...}}) template
8
+ // builder. The pre-baked path has its own builder (./baked-templates.ts) with
9
+ // a different wire format — do not unify them (see AGENTS.md).
10
+
11
+ import {
12
+ HV_COUNT,
13
+ HV_CORS_SIMPLE,
14
+ HV_CORS_PREFLIGHT,
15
+ HV_RATE_ACTIVE,
16
+ HV_RATE_LIMITED,
17
+ } from '../constants'
18
+ import { buildCorsStaticStrings, type CorsOptions, type CorsStaticStrings } from './cors'
19
+ import { buildHstsValue, buildSecurityPairs, type SecurityHeadersOptions } from './hsts'
20
+ import { secondsFromMs } from '../shared'
21
+
22
+ /** A single precomputed header-variant template. */
23
+ export interface HeaderTemplate {
24
+ readonly entries: ReadonlyArray<readonly [string, string]>
25
+ readonly needsOriginEcho: boolean
26
+ readonly needsRateDynamic: boolean
27
+ readonly needsRetryAfter: boolean
28
+ readonly needsDynamicHsts: boolean
29
+ }
30
+
31
+ /**
32
+ * Fallback used only when a variant template is unexpectedly absent. All 32
33
+ * variants are always built, so this never serves a real response.
34
+ */
35
+ const EMPTY_HEADER_TEMPLATE: HeaderTemplate = Object.freeze({
36
+ entries: [],
37
+ needsOriginEcho: false,
38
+ needsRateDynamic: false,
39
+ needsRetryAfter: false,
40
+ needsDynamicHsts: false,
41
+ })
42
+
43
+ /** Precomputed per-handler response context. */
44
+ export interface ResponseBuildContext {
45
+ readonly templates: HeaderTemplate[]
46
+ readonly corsStatic: CorsStaticStrings | null
47
+ readonly hstsValue: string | null
48
+ }
49
+
50
+ /**
51
+ * Build the full response context (header templates + static CORS strings +
52
+ * HSTS value) for a handler configuration.
53
+ */
54
+ export function buildResponseContext(options: {
55
+ cors?: CorsOptions
56
+ security?: SecurityHeadersOptions
57
+ https?: boolean
58
+ enableSecurityHeaders?: boolean
59
+ rateLimit?: { limit?: number }
60
+ }): ResponseBuildContext {
61
+ const templates = buildHeaderTemplates(options)
62
+ const corsStatic = buildCorsStaticStrings(options.cors)
63
+ const hstsValue = buildHstsValue(options.security ?? {})
64
+
65
+ return { templates, corsStatic, hstsValue }
66
+ }
67
+
68
+ function buildHeaderTemplates(options: {
69
+ cors?: CorsOptions
70
+ security?: SecurityHeadersOptions
71
+ https?: boolean
72
+ enableSecurityHeaders?: boolean
73
+ rateLimit?: { limit?: number }
74
+ }): HeaderTemplate[] {
75
+ const templates: HeaderTemplate[] = new Array(HV_COUNT)
76
+
77
+ const sec = options.security ?? {}
78
+ const securityEnabled = options.enableSecurityHeaders !== false
79
+ const corsStatic = buildCorsStaticStrings(options.cors)
80
+ const hstsValue = buildHstsValue(sec)
81
+ const hstsIsDynamic = options.https === undefined && hstsValue !== null
82
+
83
+ // Single source of truth for security-option → header pairs (shared with the
84
+ // pre-baked handlers.ts path — see buildSecurityPairs in ./hsts.ts), so a
85
+ // security option can never be honored on one path and ignored on the other.
86
+ const securityPairs = buildSecurityPairs(sec, options.https, securityEnabled)
87
+
88
+ const corsVaryPairs: Array<[string, string]> = []
89
+
90
+ if (corsStatic) {
91
+ corsVaryPairs.push(['vary', 'Origin'])
92
+
93
+ if (!corsStatic.isWildcard) {
94
+ corsVaryPairs.push(['vary', 'Access-Control-Request-Method'])
95
+ corsVaryPairs.push(['vary', 'Access-Control-Request-Headers'])
96
+ }
97
+ }
98
+
99
+ const corsPolicyPairs: Array<[string, string]> = []
100
+
101
+ if (corsStatic) {
102
+ if (corsStatic.credentials) {
103
+ corsPolicyPairs.push(['access-control-allow-credentials', 'true'])
104
+ }
105
+
106
+ if (corsStatic.exposeHeadersJoined) {
107
+ corsPolicyPairs.push(['access-control-expose-headers', corsStatic.exposeHeadersJoined])
108
+ }
109
+ }
110
+
111
+ const corsPreflightPairs: Array<[string, string]> = []
112
+
113
+ if (corsStatic) {
114
+ corsPreflightPairs.push(['access-control-allow-methods', corsStatic.allowMethodsJoined])
115
+
116
+ if (corsStatic.allowHeadersJoined) {
117
+ corsPreflightPairs.push(['access-control-allow-headers', corsStatic.allowHeadersJoined])
118
+ }
119
+
120
+ if (corsStatic.maxAgeString) {
121
+ corsPreflightPairs.push(['access-control-max-age', corsStatic.maxAgeString])
122
+ }
123
+ }
124
+
125
+ const rateLimitCeiling: Array<[string, string]> = []
126
+
127
+ if (options.rateLimit?.limit && options.rateLimit.limit > 0) {
128
+ rateLimitCeiling.push(['x-ratelimit-limit', String(options.rateLimit.limit)])
129
+ }
130
+
131
+ for (let variant = 0; variant < HV_COUNT; variant++) {
132
+ const isCorsSimple = (variant & HV_CORS_SIMPLE) !== 0
133
+ const isCorsPreflight = (variant & HV_CORS_PREFLIGHT) !== 0
134
+ const isRateActive = (variant & HV_RATE_ACTIVE) !== 0
135
+ const isRateLimited = (variant & HV_RATE_LIMITED) !== 0
136
+
137
+ const entries: Array<[string, string]> = []
138
+
139
+ if (securityEnabled) {
140
+ for (const pair of securityPairs) {
141
+ entries.push(pair)
142
+ }
143
+ }
144
+
145
+ for (const pair of corsVaryPairs) {
146
+ entries.push(pair)
147
+ }
148
+
149
+ if (isCorsSimple || isCorsPreflight) {
150
+ for (const pair of corsPolicyPairs) {
151
+ entries.push(pair)
152
+ }
153
+ }
154
+
155
+ if (isCorsPreflight) {
156
+ for (const pair of corsPreflightPairs) {
157
+ entries.push(pair)
158
+ }
159
+ }
160
+
161
+ if (isRateActive) {
162
+ for (const pair of rateLimitCeiling) {
163
+ entries.push(pair)
164
+ }
165
+ }
166
+
167
+ templates[variant] = {
168
+ entries,
169
+ needsOriginEcho: isCorsSimple || isCorsPreflight,
170
+ needsRateDynamic: isRateActive,
171
+ needsRetryAfter: isRateLimited,
172
+ needsDynamicHsts: hstsIsDynamic,
173
+ }
174
+ }
175
+
176
+ return templates
177
+ }
178
+
179
+ /**
180
+ * Build the response headers for a decoded result, applying the dynamic parts
181
+ * (request-id, origin echo, rate-limit counters, retry-after, dynamic HSTS).
182
+ */
183
+ export function headersForResult(
184
+ ctx: ResponseBuildContext,
185
+ r: {
186
+ readonly headerVariant: number
187
+ readonly corsAllowed: boolean
188
+ readonly rateRemaining: number
189
+ readonly rateResetMs: number
190
+ readonly retryAfterMs: number
191
+ readonly https: boolean
192
+ },
193
+ req: Request,
194
+ requestId: string,
195
+ ): Headers {
196
+ const template =
197
+ ctx.templates[r.headerVariant & 0x1f] ?? ctx.templates[0] ?? EMPTY_HEADER_TEMPLATE
198
+
199
+ const headers = new Headers()
200
+
201
+ for (const [name, value] of template.entries) {
202
+ headers.append(name, value)
203
+ }
204
+
205
+ if (requestId) {
206
+ headers.set('x-request-id', requestId)
207
+ }
208
+
209
+ if (template.needsOriginEcho && r.corsAllowed && ctx.corsStatic) {
210
+ const origin = req.headers.get('origin')
211
+
212
+ if (origin) {
213
+ if (ctx.corsStatic.isWildcard && !ctx.corsStatic.credentials) {
214
+ headers.set('access-control-allow-origin', '*')
215
+ } else {
216
+ headers.set('access-control-allow-origin', origin)
217
+ }
218
+ }
219
+ }
220
+
221
+ if (template.needsRateDynamic) {
222
+ headers.set('x-ratelimit-remaining', String(Math.max(0, r.rateRemaining)))
223
+
224
+ if (r.rateResetMs > 0) {
225
+ headers.set('x-ratelimit-reset', String(secondsFromMs(r.rateResetMs)))
226
+ }
227
+ }
228
+
229
+ if (template.needsRetryAfter && r.retryAfterMs > 0) {
230
+ headers.set('retry-after', String(Math.max(1, secondsFromMs(r.retryAfterMs))))
231
+ }
232
+
233
+ if (template.needsDynamicHsts && r.https && ctx.hstsValue) {
234
+ headers.set('strict-transport-security', ctx.hstsValue)
235
+ }
236
+
237
+ return headers
238
+ }
@@ -0,0 +1,83 @@
1
+ // src/ingress/headers/hsts.ts — Strict-Transport-Security value builder
2
+ // (fast path).
3
+
4
+ /** User-facing security-headers configuration. */
5
+ export interface SecurityHeadersOptions {
6
+ contentSecurityPolicy?: string
7
+ hsts?: boolean
8
+ hstsMaxAge?: number
9
+ hstsIncludeSubdomains?: boolean
10
+ hstsPreload?: boolean
11
+ frameOptions?: string
12
+ nosniff?: boolean
13
+ referrerPolicy?: string
14
+ coep?: string
15
+ coop?: string
16
+ corp?: string
17
+ xssProtection?: string
18
+ }
19
+
20
+ /** Build the HSTS header value, or `null` when HSTS is not configured. */
21
+ export function buildHstsValue(sec: SecurityHeadersOptions): string | null {
22
+ const wantHsts =
23
+ sec.hsts === true ||
24
+ sec.hstsMaxAge !== undefined ||
25
+ sec.hstsIncludeSubdomains === true ||
26
+ sec.hstsPreload === true
27
+
28
+ if (!wantHsts) return null
29
+
30
+ const maxAge = sec.hstsMaxAge ?? 31_536_000
31
+ let value = `max-age=${maxAge}`
32
+
33
+ if (sec.hstsIncludeSubdomains) value += '; includeSubDomains'
34
+ if (sec.hstsPreload) value += '; preload'
35
+
36
+ return value
37
+ }
38
+
39
+ /**
40
+ * Convert structured `security` options into ordered `[name, value][]` pairs.
41
+ *
42
+ * Shared by BOTH header-template paths (`fast-templates.ts` and the pre-baked
43
+ * `handlers.ts`) so a security option is honored consistently — it can never
44
+ * be applied on one path and silently ignored on the other (the historic
45
+ * "security-preheat gotcha"). `https === true` gates the static HSTS pair
46
+ * (dynamic HSTS — `https` undefined — is handled per-request by the fast-path
47
+ * response builder); `enabled === false` yields no pairs.
48
+ */
49
+ export function buildSecurityPairs(
50
+ security: SecurityHeadersOptions | undefined,
51
+ https: boolean | undefined,
52
+ enabled: boolean,
53
+ ): Array<[string, string]> {
54
+ if (!enabled) return []
55
+ const sec = security ?? {}
56
+ const pairs: Array<[string, string]> = []
57
+
58
+ if (sec.nosniff !== false) {
59
+ pairs.push(['x-content-type-options', 'nosniff'])
60
+ }
61
+ pairs.push(['x-frame-options', sec.frameOptions ?? 'DENY'])
62
+ pairs.push(['referrer-policy', sec.referrerPolicy ?? 'no-referrer'])
63
+ if (sec.xssProtection) {
64
+ pairs.push(['x-xss-protection', sec.xssProtection])
65
+ }
66
+ if (sec.contentSecurityPolicy) {
67
+ pairs.push(['content-security-policy', sec.contentSecurityPolicy])
68
+ }
69
+ if (sec.coep) {
70
+ pairs.push(['cross-origin-embedder-policy', sec.coep])
71
+ }
72
+ if (sec.coop) {
73
+ pairs.push(['cross-origin-opener-policy', sec.coop])
74
+ }
75
+ if (sec.corp) {
76
+ pairs.push(['cross-origin-resource-policy', sec.corp])
77
+ }
78
+ const hstsValue = buildHstsValue(sec)
79
+ if (hstsValue !== null && https === true) {
80
+ pairs.push(['strict-transport-security', hstsValue])
81
+ }
82
+ return pairs
83
+ }
@@ -0,0 +1,67 @@
1
+ // src/ingress/health.ts — Liveness / readiness / health probe route factories.
2
+ //
3
+ // Enterprise-grade operators need orchestrator probes (Kubernetes `livenessProbe`
4
+ // / `readinessProbe`). These factories return route handlers compatible with the
5
+ // pre-baked route spec — serve them from `createIngressServer` (Bun) or
6
+ // `createIngressServerNode` (Node):
7
+ //
8
+ // createIngressServer({ routes: {
9
+ // "/healthz": { read: livenessHandler() },
10
+ // "/readyz": { read: readinessHandler(async () => db.ping() === "ok") },
11
+ // "/livez": { read: healthHandler() },
12
+ // }});
13
+ //
14
+ // The liveness probe NEVER touches dependencies (process alive? -> 200).
15
+ // The readiness probe runs the optional `check` (deps up? -> 200/503).
16
+ // The health probe is an alias of liveness (process up) plus optional check.
17
+
18
+ export interface ReadinessResult {
19
+ status: number
20
+ body: string
21
+ }
22
+
23
+ function jsonResponse(status: number, body: Record<string, unknown>): Response {
24
+ return new Response(JSON.stringify(body), {
25
+ status,
26
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
27
+ })
28
+ }
29
+
30
+ /**
31
+ * Liveness probe: `200 {"status":"ok"}` as long as the process is alive.
32
+ * Never touches dependencies.
33
+ */
34
+ export function livenessHandler() {
35
+ return (_req: Request): Response => jsonResponse(200, { status: 'ok' })
36
+ }
37
+
38
+ /**
39
+ * Readiness probe: runs the optional async `check` (dependency readiness).
40
+ * Returns `200 {"status":"ready"}` when the check passes (or no check is
41
+ * given), `503 {"status":"not_ready"}` when it throws or returns false.
42
+ */
43
+ export function readinessHandler(check?: () => boolean | Promise<boolean>) {
44
+ return async (_req: Request): Promise<Response> => {
45
+ if (!check) return jsonResponse(200, { status: 'ready' })
46
+ try {
47
+ const ok = await check()
48
+ return ok
49
+ ? jsonResponse(200, { status: 'ready' })
50
+ : jsonResponse(503, { status: 'not_ready' })
51
+ } catch (err) {
52
+ return jsonResponse(503, {
53
+ status: 'not_ready',
54
+ error: err instanceof Error ? err.message : String(err),
55
+ })
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Combined health probe: liveness by default, plus an optional readiness
62
+ * `check`. When a `check` is provided the response is 503 on failure
63
+ * (readiness semantics), otherwise always 200.
64
+ */
65
+ export function healthHandler(check?: () => boolean | Promise<boolean>) {
66
+ return check ? readinessHandler(check) : livenessHandler()
67
+ }