@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.
- package/CHANGELOG.md +204 -0
- package/api-surface.md +192 -424
- package/package.json +3 -1
- package/src/application/Application.ts +10 -4
- package/src/application/currentApp.ts +7 -1
- package/src/build/codemod.ts +1 -1
- package/src/command/CommandRunner.ts +2 -0
- package/src/command/OutputWriter.ts +5 -1
- package/src/command/builtin/AssetsPruneCommand.ts +138 -0
- package/src/command/builtin/DeployCommand.ts +91 -1
- package/src/command/builtin/TestCommand.ts +9 -2
- package/src/command/builtin/index.ts +1 -0
- package/src/command/startZerotal.ts +62 -1
- package/src/config/AppConfig.ts +10 -10
- package/src/config/ConfigLoader.ts +5 -1
- package/src/config/DeployConfig.ts +33 -0
- package/src/config/index.ts +6 -1
- package/src/config/registry.ts +5 -1
- package/src/config/validation.ts +10 -2
- package/src/conventions/ConventionLoader.ts +44 -3
- package/src/dev/BuildOutput.ts +58 -6
- package/src/dev/CssPlugins.ts +13 -3
- package/src/dev/DevBuildHook.ts +10 -2
- package/src/dev/DevDeck.ts +9 -3
- package/src/dev/DevOrchestrator.ts +2 -0
- package/src/dev/DevProcess.ts +19 -5
- package/src/dev/DevReloadMiddleware.ts +7 -1
- package/src/dev/DevSupervisor.ts +27 -9
- package/src/dev/bootBuild.ts +9 -1
- package/src/dev/index.ts +6 -1
- package/src/dev/reloadClient.ts +2 -0
- package/src/dev/startDevMode.ts +8 -2
- package/src/doctor/AppDoctor.ts +43 -1
- package/src/doctor/throttleIdentity.ts +118 -0
- package/src/errors/RuntimeMismatchError.ts +21 -0
- package/src/errors/index.ts +1 -0
- package/src/events/Emitter.ts +5 -1
- package/src/health/Health.ts +5 -5
- package/src/helpers/html.ts +2 -0
- package/src/helpers/index.ts +2 -1
- package/src/helpers/markdown.ts +14 -1
- package/src/helpers/pageElements.ts +2 -0
- package/src/http/HttpClient.ts +5 -1
- package/src/http/UploadedFile.ts +2 -2
- package/src/http/Uri.ts +5 -1
- package/src/http/negotiate.ts +5 -1
- package/src/http/originGuard.ts +2 -0
- package/src/http/sniffContentType.ts +7 -1
- package/src/index.ts +2 -1
- package/src/lock/LockManager.ts +4 -4
- package/src/logger/LoggerMiddleware.ts +1 -1
- package/src/logger/types.ts +4 -4
- package/src/macros/config.macro.ts +2 -0
- package/src/metrics/HttpMetrics.ts +10 -2
- package/src/middleware/BaseMiddleware.ts +20 -0
- package/src/middleware/CorsMiddleware.ts +6 -6
- package/src/middleware/RateLimiter.ts +105 -22
- package/src/middleware/SecureHeadersMiddleware.ts +8 -8
- package/src/middleware/ThrottleMiddleware.ts +57 -4
- package/src/middleware/WebhookMiddleware.ts +7 -7
- package/src/pipeline/ContextRegistry.ts +12 -2
- package/src/pipeline/HttpContext.ts +4 -0
- package/src/pipeline/currentPage.ts +4 -0
- package/src/pipeline/types.ts +2 -0
- package/src/router/FileRouter.ts +20 -2
- package/src/router/Route.ts +3 -0
- package/src/router/RouteHandler.ts +2 -2
- package/src/router/Router.ts +7 -5
- package/src/router/domain.ts +1 -0
- package/src/router/registry.ts +12 -2
- package/src/security/redactGraph.ts +1 -1
- package/src/shared/format.ts +135 -0
- package/src/shared/index.ts +34 -0
- package/src/storage/StorageFilesMiddleware.ts +4 -2
- package/src/storage/types.ts +10 -10
- package/src/support/cookie.ts +13 -7
- package/src/support/deepMerge.ts +49 -0
- package/src/support/runtime.ts +226 -0
- package/src/view/FileRouteResolver.ts +2 -0
- 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
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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();
|
package/src/pipeline/types.ts
CHANGED
|
@@ -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;
|
package/src/router/FileRouter.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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);
|
package/src/router/Route.ts
CHANGED
|
@@ -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
|
/**
|
package/src/router/Router.ts
CHANGED
|
@@ -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>();
|
package/src/router/domain.ts
CHANGED
package/src/router/registry.ts
CHANGED
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
* updates with it.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
/**
|
|
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
|
-
/**
|
|
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
|
/**
|