@beignet/core 0.0.45 → 0.0.47

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 (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +69 -22
  3. package/dist/server/context.d.ts +7 -0
  4. package/dist/server/context.d.ts.map +1 -1
  5. package/dist/server/context.js.map +1 -1
  6. package/dist/server/hooks/logging.d.ts +3 -0
  7. package/dist/server/hooks/logging.d.ts.map +1 -1
  8. package/dist/server/hooks/logging.js +18 -7
  9. package/dist/server/hooks/logging.js.map +1 -1
  10. package/dist/server/hooks/rate-limit.d.ts +7 -7
  11. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  12. package/dist/server/hooks/rate-limit.js +15 -9
  13. package/dist/server/hooks/rate-limit.js.map +1 -1
  14. package/dist/server/hooks/security.d.ts +4 -3
  15. package/dist/server/hooks/security.d.ts.map +1 -1
  16. package/dist/server/hooks/security.js +5 -3
  17. package/dist/server/hooks/security.js.map +1 -1
  18. package/dist/server/http.d.ts +12 -0
  19. package/dist/server/http.d.ts.map +1 -1
  20. package/dist/server/request-executor.d.ts +5 -2
  21. package/dist/server/request-executor.d.ts.map +1 -1
  22. package/dist/server/request-executor.js +21 -2
  23. package/dist/server/request-executor.js.map +1 -1
  24. package/dist/server/server-context.d.ts +2 -1
  25. package/dist/server/server-context.d.ts.map +1 -1
  26. package/dist/server/server-context.js +2 -1
  27. package/dist/server/server-context.js.map +1 -1
  28. package/dist/server/server.d.ts +9 -0
  29. package/dist/server/server.d.ts.map +1 -1
  30. package/dist/server/server.js +14 -2
  31. package/dist/server/server.js.map +1 -1
  32. package/dist/server/trusted-proxy-internal.d.ts +4 -0
  33. package/dist/server/trusted-proxy-internal.d.ts.map +1 -0
  34. package/dist/server/trusted-proxy-internal.js +27 -0
  35. package/dist/server/trusted-proxy-internal.js.map +1 -0
  36. package/dist/server/trusted-proxy.d.ts +11 -6
  37. package/dist/server/trusted-proxy.d.ts.map +1 -1
  38. package/dist/server/trusted-proxy.js +6 -10
  39. package/dist/server/trusted-proxy.js.map +1 -1
  40. package/package.json +3 -1
  41. package/skills/app-architecture/SKILL.md +5 -2
  42. package/src/server/context.ts +7 -0
  43. package/src/server/hooks/logging.ts +35 -7
  44. package/src/server/hooks/rate-limit.ts +25 -15
  45. package/src/server/hooks/security.ts +11 -7
  46. package/src/server/http.ts +18 -1
  47. package/src/server/request-executor.ts +43 -2
  48. package/src/server/server-context.ts +2 -1
  49. package/src/server/server.ts +30 -1
  50. package/src/server/trusted-proxy-internal.ts +41 -0
  51. package/src/server/trusted-proxy.ts +20 -17
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import type { HttpRequestLike } from "./http.js";
2
+ import { requireTrustedProxyHeaderName } from "./trusted-proxy-internal.js";
2
3
 
3
4
  /**
4
5
  * Header source used to resolve a client IP after an app has explicitly opted
@@ -51,27 +52,32 @@ export interface TrustedRequestInfo {
51
52
  /**
52
53
  * URL as seen by the app or reconstructed from trusted proxy headers.
53
54
  */
54
- url: URL;
55
+ readonly url: Readonly<URL>;
55
56
  /**
56
57
  * External request origin.
57
58
  */
58
- origin: string;
59
+ readonly origin: string;
59
60
  /**
60
61
  * External request protocol without a trailing colon.
61
62
  */
62
- protocol: "http" | "https";
63
+ readonly protocol: "http" | "https";
63
64
  /**
64
65
  * External request host, including port when present.
65
66
  */
66
- host: string;
67
+ readonly host: string;
67
68
  /**
68
69
  * Resolved client IP when a trusted client-IP source is configured.
69
70
  */
70
- clientIp?: string;
71
+ readonly clientIp?: string;
72
+ /**
73
+ * Whether the server policy configured a trusted client-IP source. This can
74
+ * be true while `clientIp` is absent when the expected header is missing.
75
+ */
76
+ readonly clientIpTrusted: boolean;
71
77
  /**
72
78
  * Whether forwarding headers were eligible to affect this result.
73
79
  */
74
- trustedProxy: boolean;
80
+ readonly trustedProxy: boolean;
75
81
  }
76
82
 
77
83
  const DEFAULT_PROTOCOL_HEADER = "x-forwarded-proto";
@@ -92,14 +98,6 @@ function firstHeaderValue(
92
98
  return splitForwardedHeader(req.headers.get(header))[0];
93
99
  }
94
100
 
95
- function requireHeaderName(name: string, optionName: string): string {
96
- const header = name.trim();
97
- if (!header) {
98
- throw new Error(`${optionName} must be a non-empty header name.`);
99
- }
100
- return header;
101
- }
102
-
103
101
  function normalizeProtocol(
104
102
  value: string | undefined,
105
103
  ): "http" | "https" | undefined {
@@ -172,7 +170,10 @@ export function resolveTrustedClientIp(
172
170
  if (typeof source === "object") {
173
171
  return firstHeaderValue(
174
172
  req,
175
- requireHeaderName(source.header, "trustedProxy.clientIp.header"),
173
+ requireTrustedProxyHeaderName(
174
+ source.header,
175
+ "trustedProxy.clientIp.header",
176
+ ),
176
177
  );
177
178
  }
178
179
 
@@ -205,6 +206,7 @@ export function resolveTrustedRequest(
205
206
  origin: baseUrl.origin,
206
207
  protocol: baseProtocol,
207
208
  host: baseHost,
209
+ clientIpTrusted: false,
208
210
  trustedProxy: false,
209
211
  };
210
212
  }
@@ -212,14 +214,14 @@ export function resolveTrustedRequest(
212
214
  const protocolHeader =
213
215
  config.protocolHeader === false
214
216
  ? undefined
215
- : requireHeaderName(
217
+ : requireTrustedProxyHeaderName(
216
218
  config.protocolHeader ?? DEFAULT_PROTOCOL_HEADER,
217
219
  "trustedProxy.protocolHeader",
218
220
  );
219
221
  const hostHeader =
220
222
  config.hostHeader === false
221
223
  ? undefined
222
- : requireHeaderName(
224
+ : requireTrustedProxyHeaderName(
223
225
  config.hostHeader ?? DEFAULT_HOST_HEADER,
224
226
  "trustedProxy.hostHeader",
225
227
  );
@@ -244,6 +246,7 @@ export function resolveTrustedRequest(
244
246
  protocol,
245
247
  host,
246
248
  ...(clientIp !== undefined ? { clientIp } : {}),
249
+ clientIpTrusted: Boolean(config.clientIp),
247
250
  trustedProxy: true,
248
251
  };
249
252
  }