@zerotal/core 1.8.1 → 1.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 (57) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/api-surface.md +56 -355
  3. package/package.json +2 -1
  4. package/src/application/Application.ts +6 -0
  5. package/src/application/currentApp.ts +7 -1
  6. package/src/command/OutputWriter.ts +5 -1
  7. package/src/command/builtin/DeployCommand.ts +72 -1
  8. package/src/command/builtin/TestCommand.ts +9 -2
  9. package/src/command/startZerotal.ts +34 -0
  10. package/src/config/ConfigLoader.ts +5 -1
  11. package/src/config/DeployConfig.ts +33 -0
  12. package/src/config/index.ts +6 -1
  13. package/src/config/registry.ts +5 -1
  14. package/src/config/validation.ts +10 -2
  15. package/src/conventions/ConventionLoader.ts +5 -1
  16. package/src/dev/CssPlugins.ts +11 -1
  17. package/src/dev/DevBuildHook.ts +10 -2
  18. package/src/dev/DevDeck.ts +6 -0
  19. package/src/dev/DevOrchestrator.ts +2 -0
  20. package/src/dev/DevProcess.ts +17 -3
  21. package/src/dev/DevReloadMiddleware.ts +7 -1
  22. package/src/dev/DevSupervisor.ts +22 -4
  23. package/src/dev/bootBuild.ts +9 -1
  24. package/src/dev/reloadClient.ts +2 -0
  25. package/src/dev/startDevMode.ts +7 -1
  26. package/src/doctor/AppDoctor.ts +43 -1
  27. package/src/doctor/throttleIdentity.ts +114 -0
  28. package/src/errors/RuntimeMismatchError.ts +21 -0
  29. package/src/errors/index.ts +1 -0
  30. package/src/events/Emitter.ts +5 -1
  31. package/src/helpers/html.ts +2 -0
  32. package/src/helpers/markdown.ts +7 -1
  33. package/src/helpers/pageElements.ts +2 -0
  34. package/src/http/HttpClient.ts +5 -1
  35. package/src/http/Uri.ts +5 -1
  36. package/src/http/negotiate.ts +5 -1
  37. package/src/http/originGuard.ts +2 -0
  38. package/src/http/sniffContentType.ts +7 -1
  39. package/src/macros/config.macro.ts +2 -0
  40. package/src/metrics/HttpMetrics.ts +10 -2
  41. package/src/middleware/BaseMiddleware.ts +20 -0
  42. package/src/pipeline/ContextRegistry.ts +12 -2
  43. package/src/pipeline/HttpContext.ts +4 -0
  44. package/src/pipeline/currentPage.ts +4 -0
  45. package/src/pipeline/types.ts +2 -0
  46. package/src/router/FileRouter.ts +20 -2
  47. package/src/router/Route.ts +3 -0
  48. package/src/router/Router.ts +2 -0
  49. package/src/router/domain.ts +1 -0
  50. package/src/router/registry.ts +12 -2
  51. package/src/shared/format.ts +135 -0
  52. package/src/shared/index.ts +34 -0
  53. package/src/storage/StorageFilesMiddleware.ts +2 -0
  54. package/src/support/cookie.ts +7 -1
  55. package/src/support/runtime.ts +136 -0
  56. package/src/view/FileRouteResolver.ts +2 -0
  57. package/src/view/jsx-runtime.ts +4 -0
@@ -17,6 +17,7 @@ import { appKeyStrengthWarning } from "../support/appKey.ts";
17
17
  import { unroutedRoutesWarning } from "../support/unroutedRoutes.ts";
18
18
  import { isWritableDir } from "../dev/bootBuild.ts";
19
19
  import { isProdLike } from "../support/env.ts";
20
+ import { throttlesKeyedOnSocket } from "./throttleIdentity.ts";
20
21
 
21
22
  /** One finding: ok is silent health, warn is worth reading, fail is broken now. */
22
23
  export interface DoctorCheckResult {
@@ -365,12 +366,53 @@ const secureHeadersCheck: DoctorCheck = {
365
366
  },
366
367
  };
367
368
 
368
- /** The core checks every app gets. */
369
+ /**
370
+ * A rate limiter behind a proxy that was never told about the proxy keys every
371
+ * visitor to the same bucket, because the socket address is the proxy's. It then
372
+ * does the opposite of its job: one attacker spends the allowance for everybody.
373
+ *
374
+ * A warning rather than a failure, because the framework cannot see the deployment.
375
+ * An app served directly, with no proxy in front of it, is correctly configured
376
+ * exactly as it stands — and that app should not be blocked from deploying by a
377
+ * guess. What is worth saying is that the combination is almost always wrong, and
378
+ * that nothing else will ever tell you.
379
+ */
380
+ const throttleIdentityCheck: DoctorCheck = {
381
+ id: "throttle-trusted-proxies",
382
+ label: "Rate-limit identity",
383
+ run(app) {
384
+ const exposed = throttlesKeyedOnSocket(app);
385
+ if (exposed.length === 0) return ok("every registered throttle can identify a client.");
386
+ if (!_isProductionEnv(app)) {
387
+ return ok(`${exposed.length} throttle(s) key on the socket address — fine locally.`);
388
+ }
389
+ const where = exposed
390
+ .slice(0, 3)
391
+ .map((t) => t.where)
392
+ .join(", ");
393
+ return warn(
394
+ `${exposed.length} throttle(s) (${where}${exposed.length > 3 ? ", …" : ""}) key on the ` +
395
+ `socket address and no trustedProxies is set. Behind a reverse proxy that address is ` +
396
+ `the proxy's for every request, so all visitors share one bucket and one of them can ` +
397
+ `lock out the rest.`,
398
+ "If a proxy fronts this app, set trustedProxies to how many — `ThrottleMiddleware.with({ " +
399
+ "maxAttempts: 5, trustedProxies: 1 })`, or `app.throttle.trustedProxies` for all of " +
400
+ "them. If nothing fronts it, this is already correct.",
401
+ );
402
+ },
403
+ };
404
+
405
+ /**
406
+ * The core checks every app gets.
407
+ *
408
+ * @internal
409
+ */
369
410
  export const builtinDoctorChecks: DoctorCheck[] = [
370
411
  appKeyCheck,
371
412
  allowedOriginsCheck,
372
413
  corsWildcardCheck,
373
414
  secureHeadersCheck,
415
+ throttleIdentityCheck,
374
416
  bootAssetWriteCheck,
375
417
  syncVsMigrationsCheck,
376
418
  unroutedRoutesCheck,
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Can the rate limiter tell two people apart?
3
+ *
4
+ * `ThrottleMiddleware` keys its buckets on the client IP, and resolves that IP
5
+ * from the socket address unless `trustedProxies` says how many proxies sit in
6
+ * front of the app. That default is right: `X-Forwarded-For` is written by the
7
+ * client, and trusting it without being told how deep the real value sits is how a
8
+ * limiter is bypassed with one header.
9
+ *
10
+ * But behind a reverse proxy the socket address is `127.0.0.1` — for every request
11
+ * ever made. Every visitor shares one bucket per form, and the middleware inverts
12
+ * into the thing it was installed to prevent: one attacker making twenty bad
13
+ * sign-ins a minute locks the entire staff out of the console, and five
14
+ * registrations in five minutes stops anybody in the world from creating an
15
+ * account. A limiter that cannot tell two people apart is a denial-of-service tool
16
+ * aimed at its own users.
17
+ *
18
+ * Nothing observable says this is happening. The proxy goes in, everything works,
19
+ * and the limiter quietly stops distinguishing people. The docblock on the option
20
+ * explains it, but the option is read while writing middleware and the mistake is
21
+ * made while writing a Caddyfile — so the two never meet. That gap is what this
22
+ * check closes.
23
+ *
24
+ * @module
25
+ */
26
+ import type { Application } from "../application/Application.ts";
27
+ import { ThrottleMiddleware } from "../middleware/ThrottleMiddleware.ts";
28
+ import { Router } from "../router/Router.ts";
29
+
30
+ /** A registered throttle and how it decides who is who. */
31
+ export interface ThrottleIdentity {
32
+ /** The middleware class name, as it appears in a pipeline listing. */
33
+ name: string;
34
+ /** Where it is registered — `global`, or the route it guards. */
35
+ where: string;
36
+ /** Whether it resolves identity from a client-supplied header chain. */
37
+ trustsProxies: boolean;
38
+ /** Whether the app replaced IP-keying with its own resolver. */
39
+ customKey: boolean;
40
+ }
41
+
42
+ /** Whether a middleware class is `ThrottleMiddleware` or one of its `.with()` subclasses. */
43
+ function isThrottle(cls: unknown): boolean {
44
+ if (typeof cls !== "function") return false;
45
+ if (cls === ThrottleMiddleware) return true;
46
+ return Object.prototype.isPrototypeOf.call(ThrottleMiddleware, cls);
47
+ }
48
+
49
+ /**
50
+ * Read a throttle's configured identity strategy.
51
+ *
52
+ * `.with()` applies its options in the constructor, so the only way to see them is
53
+ * to build one. That is safe here — the constructor merges `app.throttle` config
54
+ * over the defaults and does nothing else — and it is also the only way to observe
55
+ * the config layer, which can set `trustedProxies` for every throttle at once.
56
+ */
57
+ function readIdentity(cls: unknown, where: string): ThrottleIdentity | null {
58
+ try {
59
+ const instance = new (cls as new () => unknown)() as {
60
+ options?: { trustedProxies?: number; keyResolver?: unknown };
61
+ };
62
+ const options = instance.options ?? {};
63
+ return {
64
+ name: (cls as { name?: string }).name || "ThrottleMiddleware",
65
+ where,
66
+ trustsProxies: typeof options.trustedProxies === "number" && options.trustedProxies > 0,
67
+ customKey: typeof options.keyResolver === "function",
68
+ };
69
+ } catch {
70
+ // A middleware that will not construct is a different problem, and not one
71
+ // the doctor should report as a rate-limiting finding.
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Every throttle registered in this app, global and per-route.
78
+ *
79
+ * @param app - The booted application.
80
+ */
81
+ export function registeredThrottles(app: Application): ThrottleIdentity[] {
82
+ const found: ThrottleIdentity[] = [];
83
+
84
+ for (const cls of app.globalMiddleware ?? []) {
85
+ if (!isThrottle(cls)) continue;
86
+ const identity = readIdentity(cls, "global");
87
+ if (identity) found.push(identity);
88
+ }
89
+
90
+ try {
91
+ for (const route of Router.routes.values()) {
92
+ for (const cls of route.middleware ?? []) {
93
+ if (!isThrottle(cls)) continue;
94
+ const identity = readIdentity(cls, `${route.method} ${route.path}`);
95
+ if (identity) found.push(identity);
96
+ }
97
+ }
98
+ } catch {
99
+ // No compiled router — a console-only app, or a test harness. Global
100
+ // middleware alone is still worth reporting on.
101
+ }
102
+
103
+ return found;
104
+ }
105
+
106
+ /**
107
+ * The throttles that will key every visitor to the same bucket behind a proxy.
108
+ *
109
+ * A custom `keyResolver` is excluded: an app that keys on a user id or an API key
110
+ * has already decided identity for itself, and this check has nothing to add.
111
+ */
112
+ export function throttlesKeyedOnSocket(app: Application): ThrottleIdentity[] {
113
+ return registeredThrottles(app).filter((t) => !t.trustsProxies && !t.customKey);
114
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The error raised when a project has two Bun runtimes in it — the one this
3
+ * process is executing under, and a different one installed in `node_modules`.
4
+ */
5
+ import { ZerotalError } from "./ZerotalError.ts";
6
+ import type { RuntimeMismatch } from "../support/runtime.ts";
7
+
8
+ /**
9
+ * Raised by `startZerotal()` when the running Bun and the installed Bun disagree.
10
+ *
11
+ * Carries both versions in `context` so a harness can report the disagreement
12
+ * rather than re-deriving it from the message.
13
+ */
14
+ export class RuntimeMismatchError extends ZerotalError {
15
+ constructor(
16
+ message: string,
17
+ public readonly mismatch: RuntimeMismatch,
18
+ ) {
19
+ super(message, "E_RUNTIME_MISMATCH", 500, { ...mismatch });
20
+ }
21
+ }
@@ -31,6 +31,7 @@ export {
31
31
  } from "./HttpError.ts";
32
32
  export { ValidationError } from "./ValidationError.ts";
33
33
  export { ConfigError } from "./ConfigError.ts";
34
+ export { RuntimeMismatchError } from "./RuntimeMismatchError.ts";
34
35
  export { BootCheckError } from "../application/BootDoctor.ts";
35
36
  export type { BootCheckFailure } from "../application/BootDoctor.ts";
36
37
  export { ConfigValidationError } from "../config/validation.ts";
@@ -14,7 +14,11 @@ type ListenerClass<T extends object> = new (...args: unknown[]) => {
14
14
  retryDelay?: number;
15
15
  };
16
16
 
17
- /** A listener that opts into deferred execution by declaring a `queue` target. */
17
+ /**
18
+ * A listener that opts into deferred execution by declaring a `queue` target.
19
+ *
20
+ * @internal
21
+ */
18
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the event payload type is listener-specific and not known at this boundary.
19
23
  export interface QueuedListener<T = any> {
20
24
  queue: boolean | string;
@@ -5,6 +5,8 @@
5
5
  * set (`&` `<` `>` `"` `'`, emitting `&#x27;` for the apostrophe). Safe for
6
6
  * both text content and double-quoted attribute values; both JSX runtimes and
7
7
  * the docs app render through it, so it runs on every SSR text child.
8
+ *
9
+ * @internal
8
10
  */
9
11
  export function escapeHtml(value: string): string {
10
12
  return Bun.escapeHTML(value);
@@ -8,7 +8,11 @@ export function markdownExtractTitle(content: string): string | undefined {
8
8
  return content.match(/^#{1,2}\s+(.+)$/m)?.[1]?.trim();
9
9
  }
10
10
 
11
- /** Minimal HTML shell for rendered markdown pages. */
11
+ /**
12
+ * Minimal HTML shell for rendered markdown pages.
13
+ *
14
+ * @internal
15
+ */
12
16
  export function markdownPage(title: string, body: string): string {
13
17
  return `<html lang="en">
14
18
  <head>
@@ -44,6 +48,8 @@ ${body}
44
48
  /**
45
49
  * Options for `Bun.markdown.html()`. bun-types exposes the options type only inside
46
50
  * its `markdown` namespace; this is the named, exported equivalent the framework uses.
51
+ *
52
+ * @internal
47
53
  */
48
54
  export interface BunMarkdownOptions {
49
55
  tables?: boolean;
@@ -8,6 +8,8 @@
8
8
  * @param last - The last page number.
9
9
  * @param each - How many page links to show on each side of the current page (default `1`).
10
10
  * @returns Page numbers interleaved with `'...'` for elided ranges.
11
+ *
12
+ * @internal
11
13
  */
12
14
  export function pageElements(current: number, last: number, each = 1): (number | "...")[] {
13
15
  if (last <= 1) return [1];
@@ -9,7 +9,11 @@ import { FrameworkEvents, OutgoingRequestCompleted } from "../events/FrameworkEv
9
9
  /** An HTTP request method. */
10
10
  export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
11
11
 
12
- /** A fake response definition matched by URL while `Http.fake()` is active. */
12
+ /**
13
+ * A fake response definition matched by URL while `Http.fake()` is active.
14
+ *
15
+ * @internal
16
+ */
13
17
  export interface FakeStub {
14
18
  /** URL to match — exact string or glob-style `*` wildcard. */
15
19
  url: string;
package/src/http/Uri.ts CHANGED
@@ -40,7 +40,11 @@ interface UriParts {
40
40
  fragment?: string;
41
41
  }
42
42
 
43
- /** Read-only view over a URI's query string, returned by `Uri.query()`. */
43
+ /**
44
+ * Read-only view over a URI's query string, returned by `Uri.query()`.
45
+ *
46
+ * @internal
47
+ */
44
48
  export interface UriQueryString {
45
49
  /** All query parameters as a plain object. */
46
50
  all(): Record<string, QueryValue>;
@@ -17,7 +17,11 @@ const ANSI: Record<string, string> = {
17
17
  dim: "\x1b[2m",
18
18
  };
19
19
 
20
- /** A supported ANSI text color for CLI output. */
20
+ /**
21
+ * A supported ANSI text color for CLI output.
22
+ *
23
+ * @internal
24
+ */
21
25
  export type AnsiColor = "red" | "green" | "yellow" | "blue" | "cyan" | "dim";
22
26
 
23
27
  // ── Channel detection ──────────────────────────────────────────────────────────
@@ -36,6 +36,8 @@
36
36
  * return new Response('Forbidden origin', { status: 403 });
37
37
  * }
38
38
  * ```
39
+ *
40
+ * @internal
39
41
  */
40
42
  export function isAllowedOrigin(request: Request, allowedOrigins: string[] = []): boolean {
41
43
  const origin = request.headers.get("origin");
@@ -65,7 +65,11 @@ export const FALLBACK_CONTENT_TYPE = "application/octet-stream";
65
65
  /** Extension stored when the bytes match nothing known. */
66
66
  export const FALLBACK_EXTENSION = "bin";
67
67
 
68
- /** What {@link sniffContentType} determined about a file. */
68
+ /**
69
+ * What {@link sniffContentType} determined about a file.
70
+ *
71
+ * @internal
72
+ */
69
73
  export interface SniffedType {
70
74
  /** The detected media type, or {@link FALLBACK_CONTENT_TYPE} when unrecognised. */
71
75
  contentType: string;
@@ -88,6 +92,8 @@ export interface SniffedType {
88
92
  *
89
93
  * @example
90
94
  * const { contentType, extension } = sniffContentType(await file.bytes());
95
+ *
96
+ * @internal
91
97
  */
92
98
  export function sniffContentType(bytes: Uint8Array): SniffedType {
93
99
  for (const sig of SIGNATURES) {
@@ -34,6 +34,8 @@ import { join } from "node:path";
34
34
  * Note: config files that read `Bun.env` capture their values from the
35
35
  * environment at macro-evaluation time. For bun build --compile this is the
36
36
  * CI/CD environment — the same intended behaviour as any immutable artifact.
37
+ *
38
+ * @internal
37
39
  */
38
40
  export function loadConfigsSync(configDir: string): Record<string, Record<string, unknown>> {
39
41
  const glob = new Bun.Glob("*.ts");
@@ -25,12 +25,20 @@ let _2xx = 0,
25
25
  let _sumMs = 0,
26
26
  _maxMs = 0;
27
27
 
28
- /** Mark a request as started — increments the in-flight (currently-processing) gauge. */
28
+ /**
29
+ * Mark a request as started — increments the in-flight (currently-processing) gauge.
30
+ *
31
+ * @internal
32
+ */
29
33
  export function beginHttp(): void {
30
34
  _inFlight++;
31
35
  }
32
36
 
33
- /** Mark a request as finished — decrements the in-flight gauge (floored at 0). */
37
+ /**
38
+ * Mark a request as finished — decrements the in-flight gauge (floored at 0).
39
+ *
40
+ * @internal
41
+ */
34
42
  export function endHttp(): void {
35
43
  if (_inFlight > 0) _inFlight--;
36
44
  }
@@ -46,6 +46,26 @@ export abstract class BaseMiddleware<O extends object = object> implements Pipe<
46
46
  * Returns a zero-arg subclass with the given options deep-merged on top of
47
47
  * the subclass defaults, usable directly in app.use([...]).
48
48
  *
49
+ * **Each call creates a distinct class, and any per-class state goes with it —
50
+ * so sharing one `.with()` export across two routes shares that state.** For
51
+ * `ThrottleMiddleware` that state is the hit counter, which means two routes on
52
+ * one exported instance share a budget: a handful of fumbled sign-ins can spend
53
+ * the allowance a legitimate person needs to answer their second factor. That is
54
+ * defensible behaviour and it is not what a reader expects from a factory, so it
55
+ * is worth saying where the factory is. Call `.with()` once per thing that
56
+ * deserves its own budget.
57
+ *
58
+ * ```ts
59
+ * // One bucket, shared by both forms — 5 attempts across the pair.
60
+ * const AuthThrottle = ThrottleMiddleware.with({ maxAttempts: 5 });
61
+ * Router.post("/login", ...[AuthThrottle]);
62
+ * Router.post("/two-factor", ...[AuthThrottle]);
63
+ *
64
+ * // A bucket each, which is almost always what was meant.
65
+ * Router.post("/login", ...[ThrottleMiddleware.with({ maxAttempts: 5 })]);
66
+ * Router.post("/two-factor", ...[ThrottleMiddleware.with({ maxAttempts: 5 })]);
67
+ * ```
68
+ *
49
69
  * `NoInfer` on the parameter is what makes this type-check at all. `Opts` has
50
70
  * a default computed from the middleware class, but a type parameter that
51
71
  * appears in an argument position is inferred from the *argument* first and
@@ -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
 
@@ -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