@beignet/core 0.0.46 → 0.0.48
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 +15 -0
- package/README.md +56 -23
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js +14 -2
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/metadata.d.ts +2 -0
- package/dist/providers/metadata.d.ts.map +1 -1
- package/dist/providers/metadata.js +78 -16
- package/dist/providers/metadata.js.map +1 -1
- package/dist/server/context.d.ts +7 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +3 -0
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js +18 -7
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +7 -7
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +15 -9
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks/security.d.ts +4 -3
- package/dist/server/hooks/security.d.ts.map +1 -1
- package/dist/server/hooks/security.js +5 -3
- package/dist/server/hooks/security.js.map +1 -1
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/request-executor.d.ts +5 -2
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +21 -2
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/server-context.d.ts +2 -1
- package/dist/server/server-context.d.ts.map +1 -1
- package/dist/server/server-context.js +2 -1
- package/dist/server/server-context.js.map +1 -1
- package/dist/server/server.d.ts +9 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +14 -2
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy-internal.d.ts +4 -0
- package/dist/server/trusted-proxy-internal.d.ts.map +1 -0
- package/dist/server/trusted-proxy-internal.js +27 -0
- package/dist/server/trusted-proxy-internal.js.map +1 -0
- package/dist/server/trusted-proxy.d.ts +11 -6
- package/dist/server/trusted-proxy.d.ts.map +1 -1
- package/dist/server/trusted-proxy.js +6 -10
- package/dist/server/trusted-proxy.js.map +1 -1
- package/package.json +1 -1
- package/skills/app-architecture/SKILL.md +5 -2
- package/src/providers/instrumentation.ts +15 -2
- package/src/providers/metadata.ts +132 -16
- package/src/server/context.ts +7 -0
- package/src/server/hooks/logging.ts +35 -7
- package/src/server/hooks/rate-limit.ts +25 -15
- package/src/server/hooks/security.ts +11 -7
- package/src/server/http.ts +18 -1
- package/src/server/request-executor.ts +43 -2
- package/src/server/server-context.ts +2 -1
- package/src/server/server.ts +30 -1
- package/src/server/trusted-proxy-internal.ts +41 -0
- package/src/server/trusted-proxy.ts +20 -17
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { HttpContractConfig } from "../../contracts/index.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveTrustedRequest,
|
|
8
|
+
type TrustedRequestInfo,
|
|
9
|
+
} from "../trusted-proxy.js";
|
|
6
10
|
import type { HttpRequestLike, ServerHook } from "../types.js";
|
|
7
11
|
import { getRequestIdFromContext } from "./utils.js";
|
|
8
12
|
|
|
@@ -46,6 +50,7 @@ export interface LoggingConfig<Ctx> {
|
|
|
46
50
|
onRequestStart?: (args: {
|
|
47
51
|
ctx?: Ctx;
|
|
48
52
|
req: HttpRequestLike;
|
|
53
|
+
requestInfo: TrustedRequestInfo;
|
|
49
54
|
contract?: HttpContractConfig;
|
|
50
55
|
}) => void;
|
|
51
56
|
/**
|
|
@@ -54,6 +59,7 @@ export interface LoggingConfig<Ctx> {
|
|
|
54
59
|
onRequestEnd?: (args: {
|
|
55
60
|
ctx?: Ctx;
|
|
56
61
|
req: HttpRequestLike;
|
|
62
|
+
requestInfo: TrustedRequestInfo;
|
|
57
63
|
res: { status: number; headers: Record<string, string> };
|
|
58
64
|
durationMs: number;
|
|
59
65
|
contract?: HttpContractConfig;
|
|
@@ -61,6 +67,13 @@ export interface LoggingConfig<Ctx> {
|
|
|
61
67
|
}) => void;
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
function loggingRequestInfo(
|
|
71
|
+
req: HttpRequestLike,
|
|
72
|
+
requestInfo: TrustedRequestInfo | undefined,
|
|
73
|
+
): TrustedRequestInfo {
|
|
74
|
+
return requestInfo ?? resolveTrustedRequest(req);
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/**
|
|
65
78
|
* Create request logging hooks.
|
|
66
79
|
*
|
|
@@ -74,10 +87,15 @@ export function createLoggingHooks<Ctx>(
|
|
|
74
87
|
|
|
75
88
|
return {
|
|
76
89
|
name: "logging",
|
|
77
|
-
onRequest: ({ req, contract }) => {
|
|
90
|
+
onRequest: ({ req, requestInfo, contract }) => {
|
|
78
91
|
if (config.onRequestStart) {
|
|
79
92
|
try {
|
|
80
|
-
config.onRequestStart({
|
|
93
|
+
config.onRequestStart({
|
|
94
|
+
req,
|
|
95
|
+
requestInfo: loggingRequestInfo(req, requestInfo),
|
|
96
|
+
contract,
|
|
97
|
+
ctx: undefined,
|
|
98
|
+
});
|
|
81
99
|
} catch {
|
|
82
100
|
// Ignore logging errors
|
|
83
101
|
}
|
|
@@ -86,9 +104,11 @@ export function createLoggingHooks<Ctx>(
|
|
|
86
104
|
|
|
87
105
|
if (!config.logger) return undefined;
|
|
88
106
|
try {
|
|
89
|
-
const url = new URL(req.url);
|
|
90
107
|
config.logger.info(
|
|
91
|
-
{
|
|
108
|
+
{
|
|
109
|
+
method: req.method,
|
|
110
|
+
url: loggingRequestInfo(req, requestInfo).url.pathname,
|
|
111
|
+
},
|
|
92
112
|
"Request start",
|
|
93
113
|
);
|
|
94
114
|
} catch {
|
|
@@ -114,13 +134,22 @@ export function createLoggingHooks<Ctx>(
|
|
|
114
134
|
},
|
|
115
135
|
}
|
|
116
136
|
: {}),
|
|
117
|
-
afterSend: ({
|
|
137
|
+
afterSend: ({
|
|
138
|
+
ctx,
|
|
139
|
+
req,
|
|
140
|
+
requestInfo,
|
|
141
|
+
response,
|
|
142
|
+
durationMs,
|
|
143
|
+
contract,
|
|
144
|
+
error,
|
|
145
|
+
}) => {
|
|
118
146
|
const headers = response.headers ?? {};
|
|
119
147
|
if (config.onRequestEnd) {
|
|
120
148
|
try {
|
|
121
149
|
config.onRequestEnd({
|
|
122
150
|
ctx,
|
|
123
151
|
req,
|
|
152
|
+
requestInfo: loggingRequestInfo(req, requestInfo),
|
|
124
153
|
res: { status: response.status, headers },
|
|
125
154
|
durationMs,
|
|
126
155
|
contract,
|
|
@@ -134,11 +163,10 @@ export function createLoggingHooks<Ctx>(
|
|
|
134
163
|
|
|
135
164
|
if (!config.logger) return;
|
|
136
165
|
try {
|
|
137
|
-
const url = new URL(req.url);
|
|
138
166
|
const requestId = getRequestIdFromContext(ctx);
|
|
139
167
|
const payload = {
|
|
140
168
|
method: req.method,
|
|
141
|
-
url: url.pathname,
|
|
169
|
+
url: loggingRequestInfo(req, requestInfo).url.pathname,
|
|
142
170
|
status: response.status,
|
|
143
171
|
durationMs: Math.round(durationMs),
|
|
144
172
|
...(requestId !== undefined ? { requestId } : {}),
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveTrustedClientIp,
|
|
14
14
|
type TrustedProxyClientIpSource,
|
|
15
15
|
type TrustedProxyConfig,
|
|
16
|
+
type TrustedRequestInfo,
|
|
16
17
|
} from "../trusted-proxy.js";
|
|
17
18
|
import type { HttpRequestLike, ServerHook } from "../types.js";
|
|
18
19
|
|
|
@@ -78,16 +79,16 @@ export interface RateLimitOptions<Ctx> {
|
|
|
78
79
|
* Resolve the client IP for `ip`-scoped limits.
|
|
79
80
|
*
|
|
80
81
|
* There is no default: when any contract declares an `ip`-scoped rate limit
|
|
81
|
-
* and neither `trustedProxy.clientIp`,
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* and neither the server-level `trustedProxy.clientIp`,
|
|
83
|
+
* this hook's `trustedProxy.clientIp`, `ipSource`, nor a custom `earlyKey`
|
|
84
|
+
* is configured, the hook fails at startup. Prefer the server-level policy
|
|
85
|
+
* for production proxy headers; keep these hook-local options for an
|
|
86
|
+
* intentional override or custom keying.
|
|
86
87
|
*/
|
|
87
88
|
ipSource?: RateLimitIpSource;
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
-
* not set.
|
|
90
|
+
* Hook-local trusted-proxy policy used to resolve client IPs when
|
|
91
|
+
* `ipSource` is not set. This overrides the server-level policy.
|
|
91
92
|
*
|
|
92
93
|
* Configure this only when the app is always behind a platform or reverse
|
|
93
94
|
* proxy that strips or normalizes forwarding headers. For `ip`-scoped rate
|
|
@@ -257,23 +258,32 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
257
258
|
const { ipSource, trustedProxy } = options;
|
|
258
259
|
const getClientIp = (
|
|
259
260
|
req: HttpRequestLike,
|
|
261
|
+
requestInfo: TrustedRequestInfo | undefined,
|
|
260
262
|
contractName: string,
|
|
261
263
|
): string | undefined => {
|
|
262
264
|
if (ipSource !== undefined) {
|
|
263
265
|
return resolveClientIp(req, ipSource);
|
|
264
266
|
}
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
+
if (trustedProxy !== undefined) {
|
|
268
|
+
if (hasTrustedProxyClientIp(trustedProxy) && trustedProxy) {
|
|
269
|
+
return resolveTrustedClientIp(req, trustedProxy.clientIp);
|
|
270
|
+
}
|
|
271
|
+
throw ipSourceConfigurationError(formatContractNames([contractName]));
|
|
272
|
+
}
|
|
273
|
+
if (requestInfo?.clientIpTrusted) {
|
|
274
|
+
return requestInfo.clientIp;
|
|
267
275
|
}
|
|
268
276
|
throw ipSourceConfigurationError(formatContractNames([contractName]));
|
|
269
277
|
};
|
|
270
278
|
|
|
271
279
|
return {
|
|
272
280
|
name: "rate-limit",
|
|
273
|
-
validate: ({ contracts }) => {
|
|
281
|
+
validate: ({ contracts, trustedProxy: serverTrustedProxy }) => {
|
|
274
282
|
if (
|
|
275
283
|
ipSource !== undefined ||
|
|
276
|
-
hasTrustedProxyClientIp(
|
|
284
|
+
hasTrustedProxyClientIp(
|
|
285
|
+
trustedProxy !== undefined ? trustedProxy : serverTrustedProxy,
|
|
286
|
+
) ||
|
|
277
287
|
options.earlyKey
|
|
278
288
|
) {
|
|
279
289
|
return;
|
|
@@ -286,7 +296,7 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
286
296
|
}
|
|
287
297
|
throw ipSourceConfigurationError(formatContractNames(ipScoped));
|
|
288
298
|
},
|
|
289
|
-
onRequest: async ({ contract, ports, req }) => {
|
|
299
|
+
onRequest: async ({ contract, ports, req, requestInfo }) => {
|
|
290
300
|
const rlMeta = contract.metadata?.rateLimit;
|
|
291
301
|
if (!rlMeta) {
|
|
292
302
|
return undefined;
|
|
@@ -300,7 +310,7 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
300
310
|
const key =
|
|
301
311
|
options.earlyKey?.({ req, scope }) ??
|
|
302
312
|
defaultEarlyRateLimitKey({ req, scope }, (r) =>
|
|
303
|
-
getClientIp(r, contract.name),
|
|
313
|
+
getClientIp(r, requestInfo, contract.name),
|
|
304
314
|
);
|
|
305
315
|
|
|
306
316
|
await enforceRateLimit(ports, {
|
|
@@ -312,7 +322,7 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
312
322
|
|
|
313
323
|
return undefined;
|
|
314
324
|
},
|
|
315
|
-
beforeHandle: async ({ ctx, contract, req }) => {
|
|
325
|
+
beforeHandle: async ({ ctx, contract, req, requestInfo }) => {
|
|
316
326
|
const rlMeta = contract.metadata?.rateLimit;
|
|
317
327
|
if (!rlMeta) {
|
|
318
328
|
return undefined;
|
|
@@ -326,7 +336,7 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
326
336
|
const key =
|
|
327
337
|
options.key?.({ ctx, req, scope }) ??
|
|
328
338
|
defaultRateLimitKey({ ctx, req, scope }, (r) =>
|
|
329
|
-
getClientIp(r, contract.name),
|
|
339
|
+
getClientIp(r, requestInfo, contract.name),
|
|
330
340
|
);
|
|
331
341
|
|
|
332
342
|
await enforceRateLimit(ctx.ports, {
|
|
@@ -154,12 +154,13 @@ export interface CsrfHooksOptions {
|
|
|
154
154
|
*/
|
|
155
155
|
token?: false | CsrfTokenOptions;
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
158
|
-
* origin against `Origin` or `Referer`.
|
|
157
|
+
* Hook-local trusted-proxy policy used when comparing the request's external
|
|
158
|
+
* origin against `Origin` or `Referer`. This overrides the server-level
|
|
159
|
+
* policy.
|
|
159
160
|
*
|
|
160
161
|
* Configure this only when the app is always behind a platform or reverse
|
|
161
162
|
* proxy that strips or normalizes forwarding headers. Without this option,
|
|
162
|
-
* CSRF uses `
|
|
163
|
+
* CSRF uses the `requestInfo` resolved by `createServer(...)`.
|
|
163
164
|
*/
|
|
164
165
|
trustedProxy?: TrustedProxyConfig;
|
|
165
166
|
/**
|
|
@@ -410,7 +411,7 @@ function enforceOrigin(args: {
|
|
|
410
411
|
contract: HttpContractConfig;
|
|
411
412
|
allowMissingOrigin: boolean;
|
|
412
413
|
trustedOrigins: CsrfHooksOptions["trustedOrigins"];
|
|
413
|
-
|
|
414
|
+
requestOrigin: string;
|
|
414
415
|
}): void {
|
|
415
416
|
const origin = originFromRequestHeaders(args.req);
|
|
416
417
|
if (!origin) {
|
|
@@ -426,7 +427,7 @@ function enforceOrigin(args: {
|
|
|
426
427
|
origin,
|
|
427
428
|
req: args.req,
|
|
428
429
|
contract: args.contract,
|
|
429
|
-
requestOrigin:
|
|
430
|
+
requestOrigin: args.requestOrigin,
|
|
430
431
|
trustedOrigins: args.trustedOrigins,
|
|
431
432
|
})
|
|
432
433
|
) {
|
|
@@ -485,7 +486,7 @@ export function createCsrfHooks<Ctx>(
|
|
|
485
486
|
|
|
486
487
|
return {
|
|
487
488
|
name: "csrf",
|
|
488
|
-
onRequest: async ({ req, contract, params }) => {
|
|
489
|
+
onRequest: async ({ req, requestInfo, contract, params }) => {
|
|
489
490
|
if (!protectedMethods.has(req.method.toUpperCase())) return undefined;
|
|
490
491
|
if (await options.skip?.({ req, contract, params })) return undefined;
|
|
491
492
|
|
|
@@ -494,7 +495,10 @@ export function createCsrfHooks<Ctx>(
|
|
|
494
495
|
contract,
|
|
495
496
|
allowMissingOrigin,
|
|
496
497
|
trustedOrigins,
|
|
497
|
-
|
|
498
|
+
requestOrigin:
|
|
499
|
+
options.trustedProxy !== undefined
|
|
500
|
+
? requestOrigin(req, options.trustedProxy)
|
|
501
|
+
: (requestInfo?.origin ?? requestOrigin(req, false)),
|
|
498
502
|
});
|
|
499
503
|
enforceToken(req, token);
|
|
500
504
|
return undefined;
|
package/src/server/http.ts
CHANGED
|
@@ -5,6 +5,10 @@ import type {
|
|
|
5
5
|
StandardSchema,
|
|
6
6
|
} from "../contracts/index.js";
|
|
7
7
|
import type { AnyPorts } from "../ports/index.js";
|
|
8
|
+
import type {
|
|
9
|
+
TrustedProxyConfig,
|
|
10
|
+
TrustedRequestInfo,
|
|
11
|
+
} from "./trusted-proxy.js";
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Framework-neutral request shape consumed by Beignet server adapters.
|
|
@@ -276,6 +280,7 @@ export type OnRequestHook<
|
|
|
276
280
|
C extends HttpContractConfig = HttpContractConfig,
|
|
277
281
|
> = (args: {
|
|
278
282
|
req: HttpRequestLike;
|
|
283
|
+
requestInfo: TrustedRequestInfo;
|
|
279
284
|
ports: Ports;
|
|
280
285
|
contract: C;
|
|
281
286
|
params: Record<string, string>;
|
|
@@ -303,6 +308,7 @@ export type BeforeHandleHook<
|
|
|
303
308
|
C extends HttpContractConfig = HttpContractConfig,
|
|
304
309
|
> = (args: {
|
|
305
310
|
req: HttpRequestLike;
|
|
311
|
+
requestInfo: TrustedRequestInfo;
|
|
306
312
|
ctx: Ctx;
|
|
307
313
|
contract: C;
|
|
308
314
|
path: InferPath<C>;
|
|
@@ -323,6 +329,7 @@ export type BeforeSendHook<
|
|
|
323
329
|
C extends HttpContractConfig = HttpContractConfig,
|
|
324
330
|
> = (args: {
|
|
325
331
|
req: HttpRequestLike;
|
|
332
|
+
requestInfo: TrustedRequestInfo;
|
|
326
333
|
ctx?: Ctx;
|
|
327
334
|
contract: C;
|
|
328
335
|
path?: InferPath<C>;
|
|
@@ -371,6 +378,7 @@ export type AfterSendHook<
|
|
|
371
378
|
C extends HttpContractConfig = HttpContractConfig,
|
|
372
379
|
> = (args: {
|
|
373
380
|
req: HttpRequestLike;
|
|
381
|
+
requestInfo: TrustedRequestInfo;
|
|
374
382
|
ctx?: Ctx;
|
|
375
383
|
contract: C;
|
|
376
384
|
path?: InferPath<C>;
|
|
@@ -395,6 +403,7 @@ export type ServerCaughtErrorHook<
|
|
|
395
403
|
> = (args: {
|
|
396
404
|
err: unknown;
|
|
397
405
|
req: HttpRequestLike;
|
|
406
|
+
requestInfo?: TrustedRequestInfo;
|
|
398
407
|
ctx?: Ctx;
|
|
399
408
|
contract: C;
|
|
400
409
|
path?: InferPath<C>;
|
|
@@ -415,6 +424,7 @@ export type ServerUnhandledErrorMapper<
|
|
|
415
424
|
> = (args: {
|
|
416
425
|
err: unknown;
|
|
417
426
|
req: HttpRequestLike;
|
|
427
|
+
requestInfo?: TrustedRequestInfo;
|
|
418
428
|
ctx?: Ctx;
|
|
419
429
|
contract: C;
|
|
420
430
|
path?: InferPath<C>;
|
|
@@ -447,7 +457,14 @@ export interface ServerHook<Ctx, Ports extends AnyPorts = AnyPorts> {
|
|
|
447
457
|
* registered later through `server.route(...)` are not seen here, so hooks
|
|
448
458
|
* that need full coverage should keep a runtime check as a backstop.
|
|
449
459
|
*/
|
|
450
|
-
validate?: (args: {
|
|
460
|
+
validate?: (args: {
|
|
461
|
+
contracts: readonly HttpContractConfig[];
|
|
462
|
+
/**
|
|
463
|
+
* Server-level trusted-proxy policy. Request phases receive its resolved
|
|
464
|
+
* value as `requestInfo`.
|
|
465
|
+
*/
|
|
466
|
+
trustedProxy?: TrustedProxyConfig;
|
|
467
|
+
}) => void;
|
|
451
468
|
/**
|
|
452
469
|
* Runs after route matching and before body/query/header parsing.
|
|
453
470
|
*/
|
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
decodeMatchedParams,
|
|
82
82
|
PathDecodeError,
|
|
83
83
|
} from "./route-matching.js";
|
|
84
|
+
import type { TrustedRequestInfo } from "./trusted-proxy.js";
|
|
84
85
|
|
|
85
86
|
function withoutHeadResponseBody(
|
|
86
87
|
response: HttpResponse,
|
|
@@ -133,6 +134,23 @@ type ExecutionTarget<Ctx, C extends HttpContractConfig> = {
|
|
|
133
134
|
responseValidationExemptStatus?: number;
|
|
134
135
|
};
|
|
135
136
|
|
|
137
|
+
function copyTrustedRequestInfo(
|
|
138
|
+
requestInfo: TrustedRequestInfo,
|
|
139
|
+
): TrustedRequestInfo;
|
|
140
|
+
function copyTrustedRequestInfo(requestInfo: undefined): undefined;
|
|
141
|
+
function copyTrustedRequestInfo(
|
|
142
|
+
requestInfo: TrustedRequestInfo | undefined,
|
|
143
|
+
): TrustedRequestInfo | undefined;
|
|
144
|
+
function copyTrustedRequestInfo(
|
|
145
|
+
requestInfo: TrustedRequestInfo | undefined,
|
|
146
|
+
): TrustedRequestInfo | undefined {
|
|
147
|
+
if (!requestInfo) return undefined;
|
|
148
|
+
return {
|
|
149
|
+
...requestInfo,
|
|
150
|
+
url: new URL(requestInfo.url),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
136
154
|
/**
|
|
137
155
|
* Build the per-request execution pipeline once.
|
|
138
156
|
*
|
|
@@ -151,8 +169,10 @@ export function createRequestExecutor<
|
|
|
151
169
|
createRequestContext: (
|
|
152
170
|
req: HttpRequestLike,
|
|
153
171
|
contract: HttpContractConfig,
|
|
172
|
+
requestInfo: TrustedRequestInfo,
|
|
154
173
|
) => Promise<Ctx>;
|
|
155
174
|
finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
|
|
175
|
+
resolveRequestInfo: (req: HttpRequestLike) => TrustedRequestInfo;
|
|
156
176
|
},
|
|
157
177
|
hooks: ServerHook<Ctx, FinalPorts>[],
|
|
158
178
|
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
@@ -179,6 +199,7 @@ export function createRequestExecutor<
|
|
|
179
199
|
preMatchedParams?: Record<string, string>,
|
|
180
200
|
) => {
|
|
181
201
|
const { contract, handler: userHandler } = target;
|
|
202
|
+
let requestInfo: TrustedRequestInfo | undefined;
|
|
182
203
|
let baseCtx: Ctx | undefined;
|
|
183
204
|
let pathValue: HandlerArgs<Ctx, C>["path"] | undefined;
|
|
184
205
|
let queryValue: HandlerArgs<Ctx, C>["query"] | undefined;
|
|
@@ -224,6 +245,7 @@ export function createRequestExecutor<
|
|
|
224
245
|
const args = {
|
|
225
246
|
err: caught,
|
|
226
247
|
req,
|
|
248
|
+
requestInfo: copyTrustedRequestInfo(requestInfo),
|
|
227
249
|
ctx,
|
|
228
250
|
contract,
|
|
229
251
|
path,
|
|
@@ -364,6 +386,7 @@ export function createRequestExecutor<
|
|
|
364
386
|
const handled = await hook.mapUnhandledError({
|
|
365
387
|
err: currentError,
|
|
366
388
|
req,
|
|
389
|
+
requestInfo: copyTrustedRequestInfo(requestInfo),
|
|
367
390
|
ctx,
|
|
368
391
|
contract,
|
|
369
392
|
path,
|
|
@@ -391,6 +414,7 @@ export function createRequestExecutor<
|
|
|
391
414
|
const handled = await options.mapUnhandledError({
|
|
392
415
|
err: currentError,
|
|
393
416
|
req,
|
|
417
|
+
requestInfo: copyTrustedRequestInfo(requestInfo),
|
|
394
418
|
ctx,
|
|
395
419
|
contract,
|
|
396
420
|
path,
|
|
@@ -422,6 +446,8 @@ export function createRequestExecutor<
|
|
|
422
446
|
};
|
|
423
447
|
|
|
424
448
|
try {
|
|
449
|
+
const resolvedRequestInfo = contextRuntime.resolveRequestInfo(req);
|
|
450
|
+
requestInfo = resolvedRequestInfo;
|
|
425
451
|
const url = new URL(req.url);
|
|
426
452
|
|
|
427
453
|
let matchedParams: Record<string, string>;
|
|
@@ -460,6 +486,7 @@ export function createRequestExecutor<
|
|
|
460
486
|
if (!hook.beforeSend) continue;
|
|
461
487
|
const nextResponse = await hook.beforeSend({
|
|
462
488
|
req,
|
|
489
|
+
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
|
|
463
490
|
ctx: initialResult.ctx,
|
|
464
491
|
contract,
|
|
465
492
|
path: pathValue,
|
|
@@ -515,6 +542,7 @@ export function createRequestExecutor<
|
|
|
515
542
|
if (!hook.beforeSend) continue;
|
|
516
543
|
const nextResponse = await hook.beforeSend({
|
|
517
544
|
req,
|
|
545
|
+
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
|
|
518
546
|
ctx: initialResult.ctx,
|
|
519
547
|
contract,
|
|
520
548
|
path: pathValue,
|
|
@@ -612,6 +640,7 @@ export function createRequestExecutor<
|
|
|
612
640
|
try {
|
|
613
641
|
const hookResult = await hook.onRequest({
|
|
614
642
|
req,
|
|
643
|
+
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
|
|
615
644
|
ports: finalPorts,
|
|
616
645
|
contract,
|
|
617
646
|
params: matchedParams,
|
|
@@ -644,7 +673,11 @@ export function createRequestExecutor<
|
|
|
644
673
|
let createdCtx!: Ctx;
|
|
645
674
|
try {
|
|
646
675
|
createdCtx = await timeStage("contextMs", () =>
|
|
647
|
-
contextRuntime.createRequestContext(
|
|
676
|
+
contextRuntime.createRequestContext(
|
|
677
|
+
req,
|
|
678
|
+
contract,
|
|
679
|
+
resolvedRequestInfo,
|
|
680
|
+
),
|
|
648
681
|
);
|
|
649
682
|
baseCtx = createdCtx;
|
|
650
683
|
} catch (error) {
|
|
@@ -727,7 +760,11 @@ export function createRequestExecutor<
|
|
|
727
760
|
let createdCtx!: Ctx;
|
|
728
761
|
try {
|
|
729
762
|
createdCtx = await timeStage("contextMs", () =>
|
|
730
|
-
contextRuntime.createRequestContext(
|
|
763
|
+
contextRuntime.createRequestContext(
|
|
764
|
+
req,
|
|
765
|
+
contract,
|
|
766
|
+
resolvedRequestInfo,
|
|
767
|
+
),
|
|
731
768
|
);
|
|
732
769
|
baseCtx = createdCtx;
|
|
733
770
|
} catch (error) {
|
|
@@ -792,6 +829,7 @@ export function createRequestExecutor<
|
|
|
792
829
|
try {
|
|
793
830
|
const hookResult = await hook.beforeHandle({
|
|
794
831
|
req,
|
|
832
|
+
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
|
|
795
833
|
ctx: currentCtx,
|
|
796
834
|
contract,
|
|
797
835
|
path,
|
|
@@ -951,6 +989,7 @@ export function createRequestExecutor<
|
|
|
951
989
|
try {
|
|
952
990
|
await hook.afterSend({
|
|
953
991
|
req,
|
|
992
|
+
requestInfo: copyTrustedRequestInfo(resolvedRequestInfo),
|
|
954
993
|
ctx: result.ctx,
|
|
955
994
|
contract,
|
|
956
995
|
path: pathValue,
|
|
@@ -1110,8 +1149,10 @@ export function buildHandler<
|
|
|
1110
1149
|
createRequestContext: (
|
|
1111
1150
|
req: HttpRequestLike,
|
|
1112
1151
|
contract: HttpContractConfig,
|
|
1152
|
+
requestInfo: TrustedRequestInfo,
|
|
1113
1153
|
) => Promise<Ctx>;
|
|
1114
1154
|
finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
|
|
1155
|
+
resolveRequestInfo: (req: HttpRequestLike) => TrustedRequestInfo;
|
|
1115
1156
|
},
|
|
1116
1157
|
contract: C,
|
|
1117
1158
|
userHandler: Handler<Ctx, C>,
|
|
@@ -16,10 +16,11 @@ import type { ServerContextConfig } from "./context.js";
|
|
|
16
16
|
* // server/context.ts
|
|
17
17
|
* export const appContext = defineServerContext<AppContext, AppPorts>()({
|
|
18
18
|
* gate: (ports) => ports.gate,
|
|
19
|
-
* request: async ({ req, ports, requestId, trace }) => ({
|
|
19
|
+
* request: async ({ req, ports, requestId, requestInfo, trace }) => ({
|
|
20
20
|
* actor: await resolveActor(req),
|
|
21
21
|
* auth: null,
|
|
22
22
|
* requestId,
|
|
23
|
+
* requestInfo,
|
|
23
24
|
* ...trace,
|
|
24
25
|
* ports,
|
|
25
26
|
* }),
|
package/src/server/server.ts
CHANGED
|
@@ -73,6 +73,12 @@ import {
|
|
|
73
73
|
} from "./route-matching.js";
|
|
74
74
|
import type { RuntimeIntegrityCheck } from "./runtime-integrity.js";
|
|
75
75
|
import { runRuntimeIntegrityCheck } from "./runtime-integrity.js";
|
|
76
|
+
import {
|
|
77
|
+
resolveTrustedRequest,
|
|
78
|
+
type TrustedProxyConfig,
|
|
79
|
+
type TrustedRequestInfo,
|
|
80
|
+
} from "./trusted-proxy.js";
|
|
81
|
+
import { assertValidTrustedProxyConfig } from "./trusted-proxy-internal.js";
|
|
76
82
|
import {
|
|
77
83
|
createUseCaseRouteHandler,
|
|
78
84
|
isUseCaseRouteDef,
|
|
@@ -107,6 +113,15 @@ type AnyServiceProvider = ServiceProvider<
|
|
|
107
113
|
any
|
|
108
114
|
>;
|
|
109
115
|
|
|
116
|
+
function copyTrustedRequestInfo(
|
|
117
|
+
requestInfo: TrustedRequestInfo,
|
|
118
|
+
): TrustedRequestInfo {
|
|
119
|
+
return {
|
|
120
|
+
...requestInfo,
|
|
121
|
+
url: new URL(requestInfo.url),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
110
125
|
/**
|
|
111
126
|
* Options for creating a Beignet server instance.
|
|
112
127
|
*/
|
|
@@ -155,6 +170,14 @@ export type CreateServerOptions<
|
|
|
155
170
|
Ports & InferProviderPorts<Providers>,
|
|
156
171
|
ServiceInput
|
|
157
172
|
>;
|
|
173
|
+
/**
|
|
174
|
+
* Explicit policy for trusting proxy- or edge-provided request metadata.
|
|
175
|
+
*
|
|
176
|
+
* Forwarding headers are ignored by default. Configure this only when every
|
|
177
|
+
* request reaches the app through a trusted platform or reverse proxy that
|
|
178
|
+
* strips or normalizes those headers.
|
|
179
|
+
*/
|
|
180
|
+
trustedProxy?: TrustedProxyConfig;
|
|
158
181
|
/**
|
|
159
182
|
* Server hooks that wrap every registered route.
|
|
160
183
|
*/
|
|
@@ -387,10 +410,12 @@ export async function createServer<
|
|
|
387
410
|
...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[]),
|
|
388
411
|
];
|
|
389
412
|
const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
|
|
413
|
+
const trustedProxy = options.trustedProxy ?? false;
|
|
414
|
+
assertValidTrustedProxyConfig(trustedProxy);
|
|
390
415
|
|
|
391
416
|
// Fail startup on hook misconfiguration before provider setup runs.
|
|
392
417
|
for (const hook of hooks) {
|
|
393
|
-
hook.validate?.({ contracts });
|
|
418
|
+
hook.validate?.({ contracts, trustedProxy });
|
|
394
419
|
}
|
|
395
420
|
|
|
396
421
|
const resolvedContext = resolveServerContext<Ctx, FinalPorts, ServiceInput>(
|
|
@@ -403,11 +428,13 @@ export async function createServer<
|
|
|
403
428
|
const createRequestContext = async (
|
|
404
429
|
req: HttpRequestLike,
|
|
405
430
|
contract?: HttpContractConfig,
|
|
431
|
+
requestInfo: TrustedRequestInfo = resolveTrustedRequest(req, trustedProxy),
|
|
406
432
|
): Promise<Ctx> => {
|
|
407
433
|
const { requestId, trace } = instrumentation.prepareRequest(req);
|
|
408
434
|
return finalizeContext(
|
|
409
435
|
await resolvedContext.request({
|
|
410
436
|
req,
|
|
437
|
+
requestInfo: copyTrustedRequestInfo(requestInfo),
|
|
411
438
|
ports: finalPorts,
|
|
412
439
|
contract,
|
|
413
440
|
requestId,
|
|
@@ -502,6 +529,8 @@ export async function createServer<
|
|
|
502
529
|
const contextRuntime = {
|
|
503
530
|
createRequestContext,
|
|
504
531
|
finalizeContext,
|
|
532
|
+
resolveRequestInfo: (req: HttpRequestLike) =>
|
|
533
|
+
resolveTrustedRequest(req, trustedProxy),
|
|
505
534
|
};
|
|
506
535
|
|
|
507
536
|
let serviceContextsAvailable = false;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TrustedProxyConfig } from "./trusted-proxy.js";
|
|
2
|
+
|
|
3
|
+
export function requireTrustedProxyHeaderName(
|
|
4
|
+
name: string,
|
|
5
|
+
optionName: string,
|
|
6
|
+
): string {
|
|
7
|
+
const header = name.trim();
|
|
8
|
+
if (!header) {
|
|
9
|
+
throw new Error(`${optionName} must be a non-empty header name.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
new Headers().get(header);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error(`${optionName} must be a valid HTTP header name.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return header;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function assertValidTrustedProxyConfig(
|
|
22
|
+
config: TrustedProxyConfig,
|
|
23
|
+
): void {
|
|
24
|
+
if (!config) return;
|
|
25
|
+
|
|
26
|
+
if (config.hostHeader !== undefined && config.hostHeader !== false) {
|
|
27
|
+
requireTrustedProxyHeaderName(config.hostHeader, "trustedProxy.hostHeader");
|
|
28
|
+
}
|
|
29
|
+
if (config.protocolHeader !== undefined && config.protocolHeader !== false) {
|
|
30
|
+
requireTrustedProxyHeaderName(
|
|
31
|
+
config.protocolHeader,
|
|
32
|
+
"trustedProxy.protocolHeader",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (typeof config.clientIp === "object") {
|
|
36
|
+
requireTrustedProxyHeaderName(
|
|
37
|
+
config.clientIp.header,
|
|
38
|
+
"trustedProxy.clientIp.header",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|