@zerotal/core 1.8.1 → 1.10.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 (80) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/api-surface.md +192 -424
  3. package/package.json +3 -1
  4. package/src/application/Application.ts +10 -4
  5. package/src/application/currentApp.ts +7 -1
  6. package/src/build/codemod.ts +1 -1
  7. package/src/command/CommandRunner.ts +2 -0
  8. package/src/command/OutputWriter.ts +5 -1
  9. package/src/command/builtin/AssetsPruneCommand.ts +138 -0
  10. package/src/command/builtin/DeployCommand.ts +91 -1
  11. package/src/command/builtin/TestCommand.ts +9 -2
  12. package/src/command/builtin/index.ts +1 -0
  13. package/src/command/startZerotal.ts +62 -1
  14. package/src/config/AppConfig.ts +10 -10
  15. package/src/config/ConfigLoader.ts +5 -1
  16. package/src/config/DeployConfig.ts +33 -0
  17. package/src/config/index.ts +6 -1
  18. package/src/config/registry.ts +5 -1
  19. package/src/config/validation.ts +10 -2
  20. package/src/conventions/ConventionLoader.ts +44 -3
  21. package/src/dev/BuildOutput.ts +58 -6
  22. package/src/dev/CssPlugins.ts +13 -3
  23. package/src/dev/DevBuildHook.ts +10 -2
  24. package/src/dev/DevDeck.ts +9 -3
  25. package/src/dev/DevOrchestrator.ts +2 -0
  26. package/src/dev/DevProcess.ts +19 -5
  27. package/src/dev/DevReloadMiddleware.ts +7 -1
  28. package/src/dev/DevSupervisor.ts +27 -9
  29. package/src/dev/bootBuild.ts +9 -1
  30. package/src/dev/index.ts +6 -1
  31. package/src/dev/reloadClient.ts +2 -0
  32. package/src/dev/startDevMode.ts +8 -2
  33. package/src/doctor/AppDoctor.ts +43 -1
  34. package/src/doctor/throttleIdentity.ts +118 -0
  35. package/src/errors/RuntimeMismatchError.ts +21 -0
  36. package/src/errors/index.ts +1 -0
  37. package/src/events/Emitter.ts +5 -1
  38. package/src/health/Health.ts +5 -5
  39. package/src/helpers/html.ts +2 -0
  40. package/src/helpers/index.ts +2 -1
  41. package/src/helpers/markdown.ts +14 -1
  42. package/src/helpers/pageElements.ts +2 -0
  43. package/src/http/HttpClient.ts +5 -1
  44. package/src/http/UploadedFile.ts +2 -2
  45. package/src/http/Uri.ts +5 -1
  46. package/src/http/negotiate.ts +5 -1
  47. package/src/http/originGuard.ts +2 -0
  48. package/src/http/sniffContentType.ts +7 -1
  49. package/src/index.ts +2 -1
  50. package/src/lock/LockManager.ts +4 -4
  51. package/src/logger/LoggerMiddleware.ts +1 -1
  52. package/src/logger/types.ts +4 -4
  53. package/src/macros/config.macro.ts +2 -0
  54. package/src/metrics/HttpMetrics.ts +10 -2
  55. package/src/middleware/BaseMiddleware.ts +20 -0
  56. package/src/middleware/CorsMiddleware.ts +6 -6
  57. package/src/middleware/RateLimiter.ts +105 -22
  58. package/src/middleware/SecureHeadersMiddleware.ts +8 -8
  59. package/src/middleware/ThrottleMiddleware.ts +57 -4
  60. package/src/middleware/WebhookMiddleware.ts +7 -7
  61. package/src/pipeline/ContextRegistry.ts +12 -2
  62. package/src/pipeline/HttpContext.ts +4 -0
  63. package/src/pipeline/currentPage.ts +4 -0
  64. package/src/pipeline/types.ts +2 -0
  65. package/src/router/FileRouter.ts +20 -2
  66. package/src/router/Route.ts +3 -0
  67. package/src/router/RouteHandler.ts +2 -2
  68. package/src/router/Router.ts +7 -5
  69. package/src/router/domain.ts +1 -0
  70. package/src/router/registry.ts +12 -2
  71. package/src/security/redactGraph.ts +1 -1
  72. package/src/shared/format.ts +135 -0
  73. package/src/shared/index.ts +34 -0
  74. package/src/storage/StorageFilesMiddleware.ts +4 -2
  75. package/src/storage/types.ts +10 -10
  76. package/src/support/cookie.ts +13 -7
  77. package/src/support/deepMerge.ts +49 -0
  78. package/src/support/runtime.ts +226 -0
  79. package/src/view/FileRouteResolver.ts +2 -0
  80. package/src/view/jsx-runtime.ts +4 -0
@@ -4,7 +4,7 @@
4
4
  * later attached to routes or checked manually as `ThrottleMiddleware`.
5
5
  */
6
6
  import type { HttpContext } from "../pipeline/HttpContext.ts";
7
- import { ThrottleMiddleware } from "./ThrottleMiddleware.ts";
7
+ import { ThrottleMiddleware, _clientIp, _markIpDerived } from "./ThrottleMiddleware.ts";
8
8
  import type { ThrottleOptions } from "./ThrottleMiddleware.ts";
9
9
 
10
10
  /**
@@ -14,7 +14,19 @@ import type { ThrottleOptions } from "./ThrottleMiddleware.ts";
14
14
  export class LimiterDefinition {
15
15
  private _max = 60;
16
16
  private _window = 60;
17
- private _keyFn?: (ctx: HttpContext) => string;
17
+ private _trustedProxies?: number;
18
+
19
+ /**
20
+ * How this limiter keys its buckets, recorded rather than resolved.
21
+ *
22
+ * The built-in strategies all fall back to the client address, and resolving that
23
+ * needs `trustedProxies` — which the fluent API may not have been told yet when
24
+ * `.byIp()` runs. Keeping the *decision* here and building the resolver in
25
+ * {@link toMiddlewareClass} means `.byIp().trustedProxies(1)` and
26
+ * `.trustedProxies(1).byIp()` mean the same thing, which is the only defensible
27
+ * answer for a builder.
28
+ */
29
+ private _key: KeyStrategy = { kind: "default" };
18
30
 
19
31
  constructor(private readonly _name: string) {}
20
32
 
@@ -30,9 +42,33 @@ export class LimiterDefinition {
30
42
  return this;
31
43
  }
32
44
 
33
- /** Custom key resolver — defaults to client IP. */
45
+ /**
46
+ * Number of trusted reverse proxies in front of this server.
47
+ *
48
+ * Needed by every strategy that can fall back to the client address, which is all
49
+ * the built-in ones. Without it the address used is the socket's — the *proxy's*,
50
+ * behind a reverse proxy, so every visitor shares one bucket and the limiter
51
+ * inverts into the thing it was installed to prevent.
52
+ *
53
+ * @param count - How many proxies sit in front; see {@link ThrottleOptions.trustedProxies}.
54
+ *
55
+ * @example
56
+ * RateLimiter.for("login").limit(5).every(60).trustedProxies(1).register();
57
+ */
58
+ trustedProxies(count: number): this {
59
+ this._trustedProxies = count;
60
+ return this;
61
+ }
62
+
63
+ /**
64
+ * Custom key resolver — defaults to client IP.
65
+ *
66
+ * Whatever this returns is the bucket. If it derives from the client address,
67
+ * resolve that with `ctx.ip()` plus your own proxy handling, or prefer
68
+ * {@link byIp}, which honours {@link trustedProxies} for you.
69
+ */
34
70
  by(fn: (ctx: HttpContext) => string): this {
35
- this._keyFn = fn;
71
+ this._key = { kind: "custom", fn };
36
72
  return this;
37
73
  }
38
74
 
@@ -45,10 +81,7 @@ export class LimiterDefinition {
45
81
  * RateLimiter.for('api').limit(1000).every(3600).byUser().register();
46
82
  */
47
83
  byUser(): this {
48
- this._keyFn = (ctx) => {
49
- const user = (ctx as unknown as Record<string, unknown>).user as { id?: number } | undefined;
50
- return user?.id !== undefined ? `user:${user.id}` : `ip:${_resolveIp(ctx)}`;
51
- };
84
+ this._key = { kind: "user" };
52
85
  return this;
53
86
  }
54
87
 
@@ -60,10 +93,7 @@ export class LimiterDefinition {
60
93
  * RateLimiter.for('api').limit(500).every(60).byApiKey('x-api-key').register();
61
94
  */
62
95
  byApiKey(header = "x-api-key"): this {
63
- this._keyFn = (ctx) => {
64
- const key = ctx.header(header);
65
- return key ? `apikey:${key}` : `ip:${_resolveIp(ctx)}`;
66
- };
96
+ this._key = { kind: "apiKey", header };
67
97
  return this;
68
98
  }
69
99
 
@@ -73,7 +103,7 @@ export class LimiterDefinition {
73
103
  * through the fluent API.
74
104
  */
75
105
  byIp(): this {
76
- this._keyFn = (ctx) => `ip:${_resolveIp(ctx)}`;
106
+ this._key = { kind: "ip" };
77
107
  return this;
78
108
  }
79
109
 
@@ -97,7 +127,12 @@ export class LimiterDefinition {
97
127
  maxAttempts: this._max,
98
128
  windowSeconds: this._window,
99
129
  };
100
- if (this._keyFn) options.keyResolver = this._keyFn;
130
+ if (this._trustedProxies !== undefined) options.trustedProxies = this._trustedProxies;
131
+
132
+ // `default` deliberately sets no resolver: ThrottleMiddleware's own is already
133
+ // proxy-aware, and every resolver set here has to be too.
134
+ const resolver = _resolverFor(this._key, this._trustedProxies);
135
+ if (resolver) options.keyResolver = resolver;
101
136
  return ThrottleMiddleware.with(options);
102
137
  }
103
138
 
@@ -114,14 +149,62 @@ export class LimiterDefinition {
114
149
  }
115
150
  }
116
151
 
117
- /** Resolve the best available client IP from the context. */
118
- function _resolveIp(ctx: HttpContext): string {
119
- return (
120
- ctx.ip() ??
121
- ctx.header("x-forwarded-for")?.split(",")[0]?.trim() ??
122
- ctx.header("x-real-ip") ??
123
- "unknown"
124
- );
152
+ /** How a limiter decides which bucket a request belongs to. */
153
+ type KeyStrategy =
154
+ | { kind: "default" }
155
+ | { kind: "ip" }
156
+ | { kind: "user" }
157
+ | { kind: "apiKey"; header: string }
158
+ | { kind: "custom"; fn: (ctx: HttpContext) => string };
159
+
160
+ /**
161
+ * Build the key resolver for a strategy, or `undefined` to let `ThrottleMiddleware`
162
+ * use its own.
163
+ *
164
+ * Every branch that can end up keying on an address goes through `_clientIp` with the
165
+ * proxy count, and is marked so `zt doctor` still audits it. The previous versions
166
+ * used a private helper that read the socket address and fell back to the *leftmost*
167
+ * `X-Forwarded-For` entry with no proxy count at all — so behind a reverse proxy
168
+ * every visitor keyed on the proxy's own address and shared a single bucket. A named
169
+ * `login` limiter of five attempts a minute was five attempts a minute across the
170
+ * entire user base, and one attacker locked everybody out.
171
+ *
172
+ * @param strategy - What the builder recorded.
173
+ * @param trustedProxies - How many proxies sit in front, if the builder was told.
174
+ */
175
+ function _resolverFor(
176
+ strategy: KeyStrategy,
177
+ trustedProxies: number | undefined,
178
+ ): ((ctx: HttpContext) => string) | undefined {
179
+ switch (strategy.kind) {
180
+ case "default":
181
+ return undefined;
182
+
183
+ case "ip":
184
+ return _markIpDerived((ctx) => `ip:${_clientIp(ctx, trustedProxies)}`);
185
+
186
+ case "user":
187
+ // Marked, because the *unauthenticated* half of this keys on an address —
188
+ // which is the half a rate limiter on a login form exists for.
189
+ return _markIpDerived((ctx) => {
190
+ const user = (ctx as unknown as Record<string, unknown>).user as
191
+ { id?: number } | undefined;
192
+ return user?.id !== undefined ? `user:${user.id}` : `ip:${_clientIp(ctx, trustedProxies)}`;
193
+ });
194
+
195
+ case "apiKey": {
196
+ const { header } = strategy;
197
+ return _markIpDerived((ctx) => {
198
+ const key = ctx.header(header);
199
+ return key ? `apikey:${key}` : `ip:${_clientIp(ctx, trustedProxies)}`;
200
+ });
201
+ }
202
+
203
+ case "custom":
204
+ // Unmarked on purpose: an app-supplied resolver is the app's to reason about,
205
+ // and most of them key on something no proxy can affect.
206
+ return strategy.fn;
207
+ }
125
208
  }
126
209
 
127
210
  /**
@@ -14,48 +14,48 @@ export interface SecureHeadersOptions {
14
14
  * `Content-Security-Policy` header value.
15
15
  * Omitted by default — set this to a policy appropriate for your application.
16
16
  */
17
- contentSecurityPolicy?: string;
17
+ contentSecurityPolicy?: string | undefined;
18
18
 
19
19
  /**
20
20
  * `Strict-Transport-Security` max-age in seconds.
21
21
  * Defaults to 1 year (31 536 000 s). Set to `0` to disable HSTS entirely.
22
22
  * Only emitted when `secure: true` to avoid HSTS issues in plain-HTTP dev environments.
23
23
  */
24
- hstsMaxAge?: number;
24
+ hstsMaxAge?: number | undefined;
25
25
 
26
26
  /**
27
27
  * Include `includeSubDomains` in the HSTS header. Defaults to `true`.
28
28
  */
29
- hstsIncludeSubDomains?: boolean;
29
+ hstsIncludeSubDomains?: boolean | undefined;
30
30
 
31
31
  /**
32
32
  * Set the HSTS `preload` directive. Defaults to `false`.
33
33
  * Only set this if you have registered the domain with the HSTS preload list.
34
34
  */
35
- hstsPreload?: boolean;
35
+ hstsPreload?: boolean | undefined;
36
36
 
37
37
  /**
38
38
  * Enable HSTS and the `Secure` flag on the XSRF-TOKEN cookie.
39
39
  * Default `false` — set to `true` in production when serving over HTTPS.
40
40
  */
41
- secure?: boolean;
41
+ secure?: boolean | undefined;
42
42
 
43
43
  /**
44
44
  * `X-Frame-Options` value. Defaults to `'SAMEORIGIN'`.
45
45
  * Set to `'DENY'` for maximum protection, or `false` to omit the header.
46
46
  */
47
- frameOptions?: "DENY" | "SAMEORIGIN" | false;
47
+ frameOptions?: "DENY" | "SAMEORIGIN" | false | undefined;
48
48
 
49
49
  /**
50
50
  * `Referrer-Policy` value. Defaults to `'strict-origin-when-cross-origin'`.
51
51
  */
52
- referrerPolicy?: string;
52
+ referrerPolicy?: string | undefined;
53
53
 
54
54
  /**
55
55
  * `Permissions-Policy` header value.
56
56
  * Defaults to a conservative policy disabling sensitive APIs.
57
57
  */
58
- permissionsPolicy?: string | false;
58
+ permissionsPolicy?: string | false | undefined;
59
59
  }
60
60
 
61
61
  const DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=(), payment=()";
@@ -14,12 +14,12 @@ export interface ThrottleOptions {
14
14
  /** Maximum number of requests allowed within the window. */
15
15
  maxAttempts: number;
16
16
  /** Time window in seconds. Defaults to 60. */
17
- windowSeconds?: number;
17
+ windowSeconds?: number | undefined;
18
18
  /**
19
19
  * Custom key resolver — defaults to client IP address.
20
20
  * Use this to rate-limit by user ID, API key, route, etc.
21
21
  */
22
- keyResolver?: (ctx: HttpContext) => string;
22
+ keyResolver?: ((ctx: HttpContext) => string) | undefined;
23
23
  /**
24
24
  * Number of trusted reverse proxies in front of this server.
25
25
  *
@@ -35,7 +35,7 @@ export interface ThrottleOptions {
35
35
  * // Behind one load balancer:
36
36
  * ThrottleMiddleware.with({ maxAttempts: 60, trustedProxies: 1 })
37
37
  */
38
- trustedProxies?: number;
38
+ trustedProxies?: number | undefined;
39
39
  }
40
40
 
41
41
  interface HitRecord {
@@ -226,7 +226,60 @@ export function _pruneStore(store: Map<string, HitRecord>, now: number): void {
226
226
  for (let i = 0; i < live.length - _MAX_KEYS; i++) store.delete(live[i]![0]);
227
227
  }
228
228
 
229
- function _clientIp(ctx: HttpContext, trustedProxies?: number): string {
229
+ /**
230
+ * A `keyResolver` that derives its key from the client's address, tagged so the
231
+ * doctor can tell it apart from one that does not.
232
+ *
233
+ * `zt doctor` exempts a throttle with a custom `keyResolver` from the
234
+ * `trustedProxies` check, and that is right for a limiter keyed on a user id or a
235
+ * tenant — no proxy can affect it. It is exactly wrong for one keyed on an IP,
236
+ * which is what `RateLimiter`'s `.byIp()`, `.byUser()` and `.byApiKey()` build.
237
+ * Unmarked, those slipped past the one check that exists to catch them.
238
+ *
239
+ * @internal
240
+ */
241
+ export const _IP_DERIVED_KEY = Symbol.for("zerotal.throttle.ipDerivedKey");
242
+
243
+ /** Shape of a resolver carrying the marker. @internal */
244
+ type MaybeMarked = { [_IP_DERIVED_KEY]?: boolean };
245
+
246
+ /**
247
+ * Tag `resolver` as deriving its key from the client address.
248
+ *
249
+ * @param resolver - The key resolver to mark.
250
+ * @returns The same function, marked.
251
+ * @internal
252
+ */
253
+ export function _markIpDerived<T extends (ctx: HttpContext) => string>(resolver: T): T {
254
+ (resolver as T & MaybeMarked)[_IP_DERIVED_KEY] = true;
255
+ return resolver;
256
+ }
257
+
258
+ /**
259
+ * Whether `resolver` keys on the client address, and so still needs `trustedProxies`.
260
+ *
261
+ * @param resolver - The key resolver to test, if there is one.
262
+ * @internal
263
+ */
264
+ export function _isIpDerived(resolver: unknown): boolean {
265
+ return typeof resolver === "function" && (resolver as MaybeMarked)[_IP_DERIVED_KEY] === true;
266
+ }
267
+
268
+ /**
269
+ * The client's address, honouring `trustedProxies`.
270
+ *
271
+ * Exported so `RateLimiter`'s built-in key resolvers use the same rule rather than
272
+ * their own. Theirs read the socket address and fell back to the *leftmost*
273
+ * `X-Forwarded-For` entry with no proxy count at all — so behind a reverse proxy
274
+ * every visitor keyed on the proxy's address and shared one bucket, and a named
275
+ * `login` limiter of five attempts a minute was five attempts a minute for the whole
276
+ * user base. One attacker locked everybody out.
277
+ *
278
+ * @param ctx - The request to resolve an address for.
279
+ * @param trustedProxies - How many proxies sit in front; see {@link ThrottleOptions.trustedProxies}.
280
+ * @internal
281
+ */
282
+ export function _clientIp(ctx: HttpContext, trustedProxies?: number): string {
230
283
  // Default (undefined) and an explicit 0 both mean "not behind a trusted proxy": read the raw
231
284
  // socket address, which cannot be spoofed.
232
285
  //
@@ -13,32 +13,32 @@ export interface WebhookOptions {
13
13
  /** HMAC secret used to verify the signature. */
14
14
  secret: string;
15
15
  /** Request header that carries the signature. Default: 'x-webhook-signature'. */
16
- header?: string;
16
+ header?: string | undefined;
17
17
  /** HMAC algorithm. Default: 'sha256'. */
18
- algorithm?: "sha256" | "sha1" | "sha512";
18
+ algorithm?: "sha256" | "sha1" | "sha512" | undefined;
19
19
  /**
20
20
  * Expected signature prefix that will be stripped before comparison
21
21
  * (e.g. `'sha256='` for GitHub). Default: `''`.
22
22
  */
23
- prefix?: string;
23
+ prefix?: string | undefined;
24
24
  /**
25
25
  * Max age in seconds for replay-attack protection.
26
26
  * Requires `timestampHeader` (or `format: 'stripe'`). Disabled when `undefined`.
27
27
  */
28
- tolerance?: number;
28
+ tolerance?: number | undefined;
29
29
  /**
30
30
  * Header that carries the Unix timestamp of the request.
31
31
  * When present the signed payload becomes `{timestamp}{timestampSeparator}{body}`.
32
32
  */
33
- timestampHeader?: string;
33
+ timestampHeader?: string | undefined;
34
34
  /** Separator between timestamp and body in the signed payload. Default: `'.'`. */
35
- timestampSeparator?: string;
35
+ timestampSeparator?: string | undefined;
36
36
  /**
37
37
  * Header format.
38
38
  * - `'raw'` (default) — signature is the full (or prefix-stripped) header value.
39
39
  * - `'stripe'` — header is parsed as `t=<ts>,v1=<sig>` (Stripe-Signature style).
40
40
  */
41
- format?: "raw" | "stripe";
41
+ format?: "raw" | "stripe" | undefined;
42
42
  }
43
43
 
44
44
  /**
@@ -32,11 +32,21 @@
32
32
  * still compiles and returns `unknown`.
33
33
  *
34
34
  * @category Extension registries
35
+ *
36
+ * @internal
35
37
  */
36
38
  export interface ContextRegistry {}
37
39
 
38
- /** Union of every registered context key. `never` until a package augments {@link ContextRegistry}. */
40
+ /**
41
+ * Union of every registered context key. `never` until a package augments {@link ContextRegistry}.
42
+ *
43
+ * @internal
44
+ */
39
45
  export type ContextKey = keyof ContextRegistry;
40
46
 
41
- /** The value type stored under a registered context key. */
47
+ /**
48
+ * The value type stored under a registered context key.
49
+ *
50
+ * @internal
51
+ */
42
52
  export type ContextValue<K extends ContextKey> = ContextRegistry[K];
@@ -31,6 +31,8 @@ type AnyViewComponent = (ctx: HttpContext<any>, props: any) => ViewMarkup | Prom
31
31
  /**
32
32
  * Minimal Bun server interface needed for socket-level IP resolution.
33
33
  * Duck-typed so HttpContext has no hard dependency on Bun's global types.
34
+ *
35
+ * @internal
34
36
  */
35
37
  export interface RequestIPProvider {
36
38
  requestIP(req: Request): { address: string; family: string; port: number } | null;
@@ -874,6 +876,8 @@ const _prototypeMethods: ReadonlyArray<[string, (...args: unknown[]) => unknown]
874
876
  * @param origin - The current request origin to match against.
875
877
  * @returns The same-origin `url`, or `undefined` when it is absent, unparseable,
876
878
  * or points to a different origin.
879
+ *
880
+ * @internal
877
881
  */
878
882
  export function safeRedirectPath(
879
883
  url: string | null | undefined,
@@ -5,6 +5,8 @@ import { HttpContext } from "./HttpContext.ts";
5
5
  *
6
6
  * @param pageName - The paginator's name, so one request can drive several independently.
7
7
  * @returns The 1-based page, or `undefined` to fall back to the query string.
8
+ *
9
+ * @internal
8
10
  */
9
11
  export type CurrentPageResolver = (pageName: string) => number | undefined;
10
12
 
@@ -21,6 +23,8 @@ export type CurrentPageResolver = (pageName: string) => number | undefined;
21
23
  * another one. Keep it that way — a module-level slot would not be request-scoped.
22
24
  *
23
25
  * @param resolver - Called with the paginator name; return `undefined` to defer to the query string.
26
+ *
27
+ * @internal
24
28
  */
25
29
  export function setCurrentPageResolver(resolver: CurrentPageResolver): void {
26
30
  const ctx = HttpContext.tryGet();
@@ -74,6 +74,8 @@ export interface Pipe<T> {
74
74
  /**
75
75
  * A payload that can carry a `Response`. Pipes set `response` and return the
76
76
  * payload to short-circuit; the pipeline terminal reads it back.
77
+ *
78
+ * @internal
77
79
  */
78
80
  export interface HasResponse {
79
81
  response: Response | undefined;
@@ -23,6 +23,8 @@ export type { FileHandler };
23
23
  /**
24
24
  * Per-method middleware map for a route file's `export const middleware`.
25
25
  * `ALL` applies to every method the file handles; the per-verb arrays add to it.
26
+ *
27
+ * @internal
26
28
  */
27
29
  export interface RouteMethodMiddleware {
28
30
  ALL?: MiddlewareClass[];
@@ -45,6 +47,7 @@ export type RouteMiddleware = MiddlewareClass[] | RouteMethodMiddleware;
45
47
 
46
48
  /** Shape of a route module. `default` may be a FileHandler or a ViewComponent;
47
49
  * `layout` overrides the directory `_layout` (null = no layout). */
50
+ /** @internal */
48
51
  export interface RouteModule {
49
52
  default?: FileHandler | ViewComponent;
50
53
  GET?: FileHandler;
@@ -57,7 +60,11 @@ export interface RouteModule {
57
60
  middleware?: RouteMiddleware;
58
61
  }
59
62
 
60
- /** Optional `export const meta = { GET: { name: 'users.show' } }` in route files. */
63
+ /**
64
+ * Optional `export const meta = { GET: { name: 'users.show' } }` in route files.
65
+ *
66
+ * @internal
67
+ */
61
68
  export interface RouteFileMeta {
62
69
  GET?: { name?: string };
63
70
  POST?: { name?: string };
@@ -66,7 +73,11 @@ export interface RouteFileMeta {
66
73
  DELETE?: { name?: string };
67
74
  }
68
75
 
69
- /** Shape of a `_middleware.ts` file. */
76
+ /**
77
+ * Shape of a `_middleware.ts` file.
78
+ *
79
+ * @internal
80
+ */
70
81
  export interface MiddlewareModule {
71
82
  middleware: MiddlewareClass[];
72
83
  }
@@ -75,6 +86,8 @@ export interface MiddlewareModule {
75
86
 
76
87
  /**
77
88
  * Context handed to a file-route resolver for each scanned route file.
89
+ *
90
+ * @internal
78
91
  */
79
92
  export interface FileRouteContext {
80
93
  /** URL path derived from the file location (e.g. '/users/:id'). */
@@ -105,6 +118,8 @@ export interface FileRouteContext {
105
118
  * Router.flow(urlPath, PageClass, middleware);
106
119
  * return true;
107
120
  * });
121
+ *
122
+ * @internal
108
123
  */
109
124
  export type FileRouteResolver = (ctx: FileRouteContext) => boolean;
110
125
 
@@ -112,6 +127,7 @@ const _fileRouteResolvers: FileRouteResolver[] = [];
112
127
 
113
128
  /** Register a resolver. Call from a provider's onRegister() so it is in place
114
129
  * before Application.boot() scans file routes. */
130
+ /** @internal */
115
131
  export function registerFileRouteResolver(resolver: FileRouteResolver): void {
116
132
  _fileRouteResolvers.push(resolver);
117
133
  }
@@ -390,6 +406,8 @@ async function _collectLayout(
390
406
  * creates a fresh module namespace instead of returning the
391
407
  * cached version. Omit on first boot.
392
408
  * @returns The number of individual method handlers registered.
409
+ *
410
+ * @internal
393
411
  */
394
412
  export async function scanFileRoutes(baseDir: string, reloadId?: string): Promise<number> {
395
413
  const absoluteBase = _resolveAbsoluteBase(baseDir);
@@ -18,6 +18,7 @@ export type MiddlewareClass = new (...args: any[]) => Pipe<HttpContext>;
18
18
  /** Handler signature for file-based routes. Receives the request {@link HttpContext}
19
19
  * directly — route params and resolved model bindings live on `ctx.params`. May return
20
20
  * a Response or mutate `ctx` directly. */
21
+ /** @internal */
21
22
  export type FileHandler = (ctx: HttpContext) => void | Response | Promise<void | Response>;
22
23
 
23
24
  /**
@@ -49,6 +50,8 @@ export type ViewLayout = (ctx: HttpContext, props: { children: unknown }) => unk
49
50
  * Receives the raw string param value and the current context;
50
51
  * must return (or resolve to) the model instance.
51
52
  * Throw `ModelNotFoundError` (or any 404 error) when the record does not exist.
53
+ *
54
+ * @internal
52
55
  */
53
56
  export type ModelBindingResolver = (value: string, ctx: HttpContext) => Promise<unknown>;
54
57
 
@@ -85,7 +85,7 @@ export interface DispatchOptions {
85
85
  exceptionHandler?: ExceptionHandler | undefined;
86
86
  providerHooks?: ProviderHooks | undefined;
87
87
  /** Configure the freshly created context (params, route info) before dispatch. */
88
- configure?: (ctx: HttpContext) => void;
88
+ configure?: ((ctx: HttpContext) => void) | undefined;
89
89
  /**
90
90
  * Path-specific work: run the pipeline and produce the response (or leave it
91
91
  * on `ctx.response`). Errors thrown here are rendered by the exception handler.
@@ -95,7 +95,7 @@ export interface DispatchOptions {
95
95
  * Render an unhandled error to a Response. Defaults to reporting + rendering
96
96
  * through `exceptionHandler` when present, else `ExceptionHandler.defaultRender`.
97
97
  */
98
- renderError?: (error: unknown, ctx: HttpContext) => Promise<Response>;
98
+ renderError?: ((error: unknown, ctx: HttpContext) => Promise<Response>) | undefined;
99
99
  }
100
100
 
101
101
  /**
@@ -124,13 +124,13 @@ type CompiledRoutes = Record<string, Record<string, RouteHandlerFn> | Response>;
124
124
  /** Options for {@link Router.static}. */
125
125
  export interface StaticOptions {
126
126
  /** Extra response headers applied to every served file (e.g. Cache-Control). */
127
- headers?: Record<string, string>;
127
+ headers?: Record<string, string> | undefined;
128
128
  /**
129
129
  * When true (default) every file in the directory is pre-registered at
130
130
  * compile() time as a static `Response`, so Bun serves it without invoking
131
131
  * JS. Set false to fall back to the per-request lookup in Application.fetch.
132
132
  */
133
- eager?: boolean;
133
+ eager?: boolean | undefined;
134
134
  }
135
135
 
136
136
  type StaticDir = { prefix: string; rootDir: string; options?: StaticOptions | undefined };
@@ -143,7 +143,7 @@ type MarkdownDir = {
143
143
  /** Options for Router.group(). All fields are optional. */
144
144
  export interface GroupOptions {
145
145
  /** URL prefix applied to every route registered inside the group. */
146
- prefix?: string;
146
+ prefix?: string | undefined;
147
147
  /**
148
148
  * Middleware to prepend to every route inside the group.
149
149
  * Accepts a named group (string), an array of names/classes, or middleware class(es) directly.
@@ -152,9 +152,9 @@ export interface GroupOptions {
152
152
  * Router.group({ middleware: 'api' }, () => { ... });
153
153
  * Router.group({ middleware: ['web', AuthMiddleware] }, () => { ... });
154
154
  */
155
- middleware?: string | string[] | MiddlewareClass | MiddlewareClass[];
155
+ middleware?: string | string[] | MiddlewareClass | MiddlewareClass[] | undefined;
156
156
  /** Host pattern; routes inside the group only match this host (e.g. ':tenant.app.com'). */
157
- domain?: string;
157
+ domain?: string | undefined;
158
158
  }
159
159
 
160
160
  // ── Internal helpers ──────────────────────────────────────────────────────────
@@ -178,6 +178,8 @@ function _toResolver(modelOrResolver: ModelClass | ModelBindingResolver): ModelB
178
178
  * All mutable router state in one swappable object: registered routes, the
179
179
  * active group prefix/middleware/domain, static and markdown directories, named
180
180
  * routes, and named middleware groups.
181
+ *
182
+ * @internal
181
183
  */
182
184
  export class RouterState {
183
185
  routes = new Map<string, RouteDefinition>();
@@ -6,6 +6,7 @@
6
6
  * exposed on the request via `ctx.subdomains`.
7
7
  */
8
8
 
9
+ /** @internal */
9
10
  export interface CompiledDomain {
10
11
  /** The original pattern, e.g. ':tenant.app.com'. */
11
12
  source: string;
@@ -23,7 +23,11 @@
23
23
  * updates with it.
24
24
  */
25
25
 
26
- /** A value that can be substituted into a `:param` segment. */
26
+ /**
27
+ * A value that can be substituted into a `:param` segment.
28
+ *
29
+ * @internal
30
+ */
27
31
  export type RouteParamValue = string | number;
28
32
 
29
33
  /** The loose param bag accepted by the untyped `route()` overload. */
@@ -68,7 +72,11 @@ export type RouteName = Extract<keyof RouteRegistry, string>;
68
72
  */
69
73
  export type RouteTarget = [RouteName] extends [never] ? string : RouteName;
70
74
 
71
- /** The URL pattern registered for route `N`, or plain `string` when it isn't a known name. */
75
+ /**
76
+ * The URL pattern registered for route `N`, or plain `string` when it isn't a known name.
77
+ *
78
+ * @internal
79
+ */
72
80
  export type RoutePattern<N extends string> = N extends RouteName
73
81
  ? RouteRegistry[N] extends string
74
82
  ? RouteRegistry[N]
@@ -101,6 +109,8 @@ export type RouteParams<N extends string> = Prettify<ParamsOf<RoutePattern<N>>>;
101
109
  * generated, loose before that. Used by the helpers that take params without
102
110
  * being able to take `route()`'s rest-tuple — `redirect().to(name, params,
103
111
  * status)` and Flow's `redirectRoute`.
112
+ *
113
+ * @internal
104
114
  */
105
115
  export type RouteParamsArg<N extends string> = [RouteName] extends [never]
106
116
  ? RouteParamValues
@@ -48,7 +48,7 @@ export interface RedactGraphOptions {
48
48
  * `JSON.stringify` drops the key it sits under, and a debugging tool that
49
49
  * silently omits a field is worse than one that says `‹fn›`.
50
50
  */
51
- flatten?: (value: unknown) => string | undefined;
51
+ flatten?: ((value: unknown) => string | undefined) | undefined;
52
52
  }
53
53
 
54
54
  /**