@beignet/core 0.0.31 → 0.0.33

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 (156) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +346 -21
  3. package/dist/application/index.d.ts.map +1 -1
  4. package/dist/application/index.js +63 -47
  5. package/dist/application/index.js.map +1 -1
  6. package/dist/domain/entity.d.ts +37 -6
  7. package/dist/domain/entity.d.ts.map +1 -1
  8. package/dist/domain/entity.js +9 -7
  9. package/dist/domain/entity.js.map +1 -1
  10. package/dist/domain/index.d.ts +4 -4
  11. package/dist/domain/index.d.ts.map +1 -1
  12. package/dist/domain/index.js +4 -4
  13. package/dist/domain/index.js.map +1 -1
  14. package/dist/events/index.d.ts +6 -0
  15. package/dist/events/index.d.ts.map +1 -1
  16. package/dist/events/index.js +20 -10
  17. package/dist/events/index.js.map +1 -1
  18. package/dist/flags/index.d.ts +5 -4
  19. package/dist/flags/index.d.ts.map +1 -1
  20. package/dist/flags/index.js +10 -9
  21. package/dist/flags/index.js.map +1 -1
  22. package/dist/idempotency/index.d.ts +11 -1
  23. package/dist/idempotency/index.d.ts.map +1 -1
  24. package/dist/idempotency/index.js +4 -3
  25. package/dist/idempotency/index.js.map +1 -1
  26. package/dist/jobs/index.d.ts +449 -9
  27. package/dist/jobs/index.d.ts.map +1 -1
  28. package/dist/jobs/index.js +472 -12
  29. package/dist/jobs/index.js.map +1 -1
  30. package/dist/outbox/index.d.ts +170 -3
  31. package/dist/outbox/index.d.ts.map +1 -1
  32. package/dist/outbox/index.js +192 -8
  33. package/dist/outbox/index.js.map +1 -1
  34. package/dist/ports/events.d.ts +2 -2
  35. package/dist/ports/index.d.ts +6 -1
  36. package/dist/ports/index.d.ts.map +1 -1
  37. package/dist/ports/index.js +1 -0
  38. package/dist/ports/index.js.map +1 -1
  39. package/dist/providers/instrumentation.d.ts +4 -0
  40. package/dist/providers/instrumentation.d.ts.map +1 -1
  41. package/dist/providers/instrumentation.js.map +1 -1
  42. package/dist/providers/provider.d.ts +10 -2
  43. package/dist/providers/provider.d.ts.map +1 -1
  44. package/dist/providers/provider.js.map +1 -1
  45. package/dist/schedules/index.d.ts +8 -7
  46. package/dist/schedules/index.d.ts.map +1 -1
  47. package/dist/schedules/index.js +47 -16
  48. package/dist/schedules/index.js.map +1 -1
  49. package/dist/search/index.d.ts +2 -1
  50. package/dist/search/index.d.ts.map +1 -1
  51. package/dist/search/index.js +1 -0
  52. package/dist/search/index.js.map +1 -1
  53. package/dist/server/hooks/index.d.ts +1 -0
  54. package/dist/server/hooks/index.d.ts.map +1 -1
  55. package/dist/server/hooks/index.js +1 -0
  56. package/dist/server/hooks/index.js.map +1 -1
  57. package/dist/server/hooks/rate-limit.d.ts +26 -13
  58. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  59. package/dist/server/hooks/rate-limit.js +28 -34
  60. package/dist/server/hooks/rate-limit.js.map +1 -1
  61. package/dist/server/hooks/security.d.ts +179 -0
  62. package/dist/server/hooks/security.d.ts.map +1 -0
  63. package/dist/server/hooks/security.js +225 -0
  64. package/dist/server/hooks/security.js.map +1 -0
  65. package/dist/server/index.d.ts +8 -0
  66. package/dist/server/index.d.ts.map +1 -1
  67. package/dist/server/index.js +8 -0
  68. package/dist/server/index.js.map +1 -1
  69. package/dist/server/instrumentation.d.ts.map +1 -1
  70. package/dist/server/instrumentation.js +22 -6
  71. package/dist/server/instrumentation.js.map +1 -1
  72. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  73. package/dist/server/providers/loadProviderConfig.js +15 -2
  74. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  75. package/dist/server/request-context.d.ts +4 -0
  76. package/dist/server/request-context.d.ts.map +1 -1
  77. package/dist/server/request-context.js +3 -0
  78. package/dist/server/request-context.js.map +1 -1
  79. package/dist/server/runtime-integrity.d.ts +84 -0
  80. package/dist/server/runtime-integrity.d.ts.map +1 -0
  81. package/dist/server/runtime-integrity.js +160 -0
  82. package/dist/server/runtime-integrity.js.map +1 -0
  83. package/dist/server/server.d.ts +10 -0
  84. package/dist/server/server.d.ts.map +1 -1
  85. package/dist/server/server.js +55 -1
  86. package/dist/server/server.js.map +1 -1
  87. package/dist/server/trusted-proxy.d.ts +77 -0
  88. package/dist/server/trusted-proxy.d.ts.map +1 -0
  89. package/dist/server/trusted-proxy.js +129 -0
  90. package/dist/server/trusted-proxy.js.map +1 -0
  91. package/dist/tasks/index.d.ts +6 -7
  92. package/dist/tasks/index.d.ts.map +1 -1
  93. package/dist/tasks/index.js +22 -5
  94. package/dist/tasks/index.js.map +1 -1
  95. package/dist/tenancy/index.d.ts +41 -0
  96. package/dist/tenancy/index.d.ts.map +1 -0
  97. package/dist/tenancy/index.js +34 -0
  98. package/dist/tenancy/index.js.map +1 -0
  99. package/dist/testing/index.d.ts +6 -0
  100. package/dist/testing/index.d.ts.map +1 -1
  101. package/dist/testing/index.js +12 -3
  102. package/dist/testing/index.js.map +1 -1
  103. package/dist/tracing/execution.d.ts +14 -0
  104. package/dist/tracing/execution.d.ts.map +1 -0
  105. package/dist/tracing/execution.js +17 -0
  106. package/dist/tracing/execution.js.map +1 -0
  107. package/dist/tracing/index.d.ts +49 -0
  108. package/dist/tracing/index.d.ts.map +1 -1
  109. package/dist/tracing/index.js +59 -0
  110. package/dist/tracing/index.js.map +1 -1
  111. package/dist/uploads/client.d.ts.map +1 -1
  112. package/dist/uploads/client.js +44 -6
  113. package/dist/uploads/client.js.map +1 -1
  114. package/dist/uploads/index.d.ts +182 -2
  115. package/dist/uploads/index.d.ts.map +1 -1
  116. package/dist/uploads/index.js +468 -76
  117. package/dist/uploads/index.js.map +1 -1
  118. package/dist/webhooks/index.d.ts +56 -4
  119. package/dist/webhooks/index.d.ts.map +1 -1
  120. package/dist/webhooks/index.js +107 -2
  121. package/dist/webhooks/index.js.map +1 -1
  122. package/package.json +5 -1
  123. package/skills/app-architecture/SKILL.md +23 -2
  124. package/src/application/index.ts +88 -56
  125. package/src/domain/entity.ts +61 -13
  126. package/src/domain/index.ts +8 -7
  127. package/src/events/index.ts +27 -14
  128. package/src/flags/index.ts +23 -19
  129. package/src/idempotency/index.ts +17 -3
  130. package/src/jobs/index.ts +1022 -19
  131. package/src/outbox/index.ts +409 -9
  132. package/src/ports/events.ts +2 -2
  133. package/src/ports/index.ts +17 -0
  134. package/src/providers/instrumentation.ts +4 -0
  135. package/src/providers/provider.ts +11 -2
  136. package/src/schedules/index.ts +60 -26
  137. package/src/search/index.ts +3 -1
  138. package/src/server/hooks/index.ts +10 -0
  139. package/src/server/hooks/rate-limit.ts +52 -46
  140. package/src/server/hooks/security.ts +503 -0
  141. package/src/server/index.ts +8 -0
  142. package/src/server/instrumentation.ts +25 -5
  143. package/src/server/providers/loadProviderConfig.ts +19 -2
  144. package/src/server/request-context.ts +7 -0
  145. package/src/server/runtime-integrity.ts +322 -0
  146. package/src/server/server.ts +86 -1
  147. package/src/server/trusted-proxy.ts +249 -0
  148. package/src/tasks/index.ts +29 -12
  149. package/src/tenancy/index.ts +55 -0
  150. package/src/testing/index.ts +19 -3
  151. package/src/tracing/execution.ts +37 -0
  152. package/src/tracing/index.ts +137 -0
  153. package/src/uploads/client.ts +65 -6
  154. package/src/uploads/index.ts +713 -76
  155. package/src/webhooks/index.ts +240 -9
  156. package/src/domain/events.ts +0 -59
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Security hooks for @beignet/core/server
3
+ */
4
+
5
+ import type { HttpContractConfig } from "../../contracts/index.js";
6
+ import { AppError, httpErrors } from "../../errors/index.js";
7
+ import {
8
+ resolveTrustedRequest,
9
+ type TrustedProxyConfig,
10
+ type TrustedProxyOptions,
11
+ } from "../trusted-proxy.js";
12
+ import type { HttpRequestLike, ServerHook } from "../types.js";
13
+
14
+ /**
15
+ * Strict-Transport-Security configuration.
16
+ *
17
+ * HSTS is disabled by default because it should only be sent by HTTPS
18
+ * deployments that intentionally commit browsers to the configured host policy.
19
+ */
20
+ export interface StrictTransportSecurityOptions {
21
+ /**
22
+ * HSTS max-age value in seconds.
23
+ *
24
+ * Defaults to one year when `strictTransportSecurity` is configured as an
25
+ * object.
26
+ */
27
+ maxAgeSec?: number;
28
+ /**
29
+ * Include subdomains in the HSTS policy.
30
+ */
31
+ includeSubDomains?: boolean;
32
+ /**
33
+ * Mark the policy as eligible for browser preload lists.
34
+ */
35
+ preload?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Response security headers applied by `createSecurityHeadersHooks(...)`.
40
+ *
41
+ * Headers are only added when the response does not already define the same
42
+ * header name. Route handlers can therefore opt into route-specific CSP or
43
+ * download headers without fighting the global hook.
44
+ */
45
+ export interface SecurityHeadersOptions {
46
+ /**
47
+ * Content Security Policy value. Disabled by default because browser apps need
48
+ * an app-owned asset, image, frame, and script policy.
49
+ */
50
+ contentSecurityPolicy?: string | false;
51
+ /**
52
+ * Cross-Origin-Opener-Policy value.
53
+ *
54
+ * Defaults to `"same-origin"`.
55
+ */
56
+ crossOriginOpenerPolicy?: string | false;
57
+ /**
58
+ * Cross-Origin-Resource-Policy value.
59
+ *
60
+ * Defaults to `"same-origin"`.
61
+ */
62
+ crossOriginResourcePolicy?: string | false;
63
+ /**
64
+ * Permissions-Policy value.
65
+ *
66
+ * Defaults to disabling camera, microphone, and geolocation.
67
+ */
68
+ permissionsPolicy?: string | false;
69
+ /**
70
+ * Referrer-Policy value.
71
+ *
72
+ * Defaults to `"strict-origin-when-cross-origin"`.
73
+ */
74
+ referrerPolicy?: string | false;
75
+ /**
76
+ * Strict-Transport-Security value. Pass a string for full control or an object
77
+ * for Beignet to format the header. Disabled by default.
78
+ */
79
+ strictTransportSecurity?: StrictTransportSecurityOptions | string | false;
80
+ /**
81
+ * X-Content-Type-Options value.
82
+ *
83
+ * Defaults to `"nosniff"`.
84
+ */
85
+ xContentTypeOptions?: "nosniff" | false;
86
+ /**
87
+ * X-Frame-Options value.
88
+ *
89
+ * Defaults to `"DENY"`. Use `contentSecurityPolicy` with `frame-ancestors`
90
+ * for more precise frame control.
91
+ */
92
+ xFrameOptions?: "DENY" | "SAMEORIGIN" | false;
93
+ }
94
+
95
+ export type CsrfFailureReason =
96
+ | "missing_origin"
97
+ | "untrusted_origin"
98
+ | "missing_token"
99
+ | "invalid_token";
100
+
101
+ /**
102
+ * Double-submit cookie token configuration for `createCsrfHooks(...)`.
103
+ */
104
+ export interface CsrfTokenOptions {
105
+ /**
106
+ * Header that must carry the CSRF token.
107
+ *
108
+ * Defaults to `"x-csrf-token"`.
109
+ */
110
+ headerName?: string;
111
+ /**
112
+ * Cookie that stores the expected CSRF token.
113
+ *
114
+ * Defaults to `"beignet.csrf"`.
115
+ */
116
+ cookieName?: string;
117
+ }
118
+
119
+ /**
120
+ * Options for `createCsrfHooks(...)`.
121
+ */
122
+ export interface CsrfHooksOptions {
123
+ /**
124
+ * Unsafe HTTP methods protected by the hook.
125
+ *
126
+ * Defaults to `POST`, `PUT`, `PATCH`, and `DELETE`.
127
+ */
128
+ protectedMethods?: readonly string[];
129
+ /**
130
+ * Additional trusted origins allowed to send protected requests.
131
+ *
132
+ * The request URL's own origin is always trusted. Use this for sibling
133
+ * frontends such as `https://app.example.com` calling `https://api.example.com`.
134
+ */
135
+ trustedOrigins?:
136
+ | readonly string[]
137
+ | ((args: {
138
+ origin: string;
139
+ req: HttpRequestLike;
140
+ contract: HttpContractConfig;
141
+ }) => boolean);
142
+ /**
143
+ * Whether unsafe requests without `Origin` or `Referer` are allowed.
144
+ *
145
+ * Defaults to `true` so server-to-server calls, tests, and older same-origin
146
+ * clients keep working. Set to `false` for cookie-backed browser-only APIs.
147
+ */
148
+ allowMissingOrigin?: boolean;
149
+ /**
150
+ * Optional double-submit cookie token check.
151
+ *
152
+ * When configured, protected requests must send the same token in the
153
+ * configured header and cookie.
154
+ */
155
+ token?: false | CsrfTokenOptions;
156
+ /**
157
+ * Shared trusted-proxy policy used when comparing the request's external
158
+ * origin against `Origin` or `Referer`.
159
+ *
160
+ * Configure this only when the app is always behind a platform or reverse
161
+ * proxy that strips or normalizes forwarding headers. Without this option,
162
+ * CSRF uses `req.url` exactly as the adapter provided it.
163
+ */
164
+ trustedProxy?: TrustedProxyConfig;
165
+ /**
166
+ * App-owned escape hatch for routes that have another verifier, such as
167
+ * provider webhooks or auth callbacks.
168
+ */
169
+ skip?: (args: {
170
+ req: HttpRequestLike;
171
+ contract: HttpContractConfig;
172
+ params: Record<string, string>;
173
+ }) => boolean | Promise<boolean>;
174
+ }
175
+
176
+ const DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
177
+ const DEFAULT_PROTECTED_METHODS = ["POST", "PUT", "PATCH", "DELETE"] as const;
178
+ const DEFAULT_CSRF_HEADER = "x-csrf-token";
179
+ const DEFAULT_CSRF_COOKIE = "beignet.csrf";
180
+
181
+ function headerKey(
182
+ headers: Record<string, string>,
183
+ name: string,
184
+ ): string | undefined {
185
+ const lowerName = name.toLowerCase();
186
+ return Object.keys(headers).find((key) => key.toLowerCase() === lowerName);
187
+ }
188
+
189
+ function setHeaderIfMissing(
190
+ headers: Record<string, string>,
191
+ name: string,
192
+ value: string | false | undefined,
193
+ ): void {
194
+ if (value === false || value === undefined) return;
195
+ if (headerKey(headers, name)) return;
196
+ headers[name] = value;
197
+ }
198
+
199
+ function formatStrictTransportSecurity(
200
+ config: SecurityHeadersOptions["strictTransportSecurity"],
201
+ ): string | false | undefined {
202
+ if (typeof config === "string" || config === false || config === undefined) {
203
+ return config;
204
+ }
205
+
206
+ const directives = [`max-age=${config.maxAgeSec ?? 31_536_000}`];
207
+ if (config.includeSubDomains) directives.push("includeSubDomains");
208
+ if (config.preload) directives.push("preload");
209
+ return directives.join("; ");
210
+ }
211
+
212
+ /**
213
+ * Apply Beignet's default security response headers to a mutable header record.
214
+ *
215
+ * Existing headers are preserved case-insensitively so route-owned responses can
216
+ * provide more specific policies.
217
+ */
218
+ export function applySecurityHeaders(
219
+ headers: Record<string, string>,
220
+ options: SecurityHeadersOptions = {},
221
+ ): void {
222
+ setHeaderIfMissing(
223
+ headers,
224
+ "Content-Security-Policy",
225
+ options.contentSecurityPolicy,
226
+ );
227
+ setHeaderIfMissing(
228
+ headers,
229
+ "Cross-Origin-Opener-Policy",
230
+ options.crossOriginOpenerPolicy ?? "same-origin",
231
+ );
232
+ setHeaderIfMissing(
233
+ headers,
234
+ "Cross-Origin-Resource-Policy",
235
+ options.crossOriginResourcePolicy ?? "same-origin",
236
+ );
237
+ setHeaderIfMissing(
238
+ headers,
239
+ "Permissions-Policy",
240
+ options.permissionsPolicy ?? DEFAULT_PERMISSIONS_POLICY,
241
+ );
242
+ setHeaderIfMissing(
243
+ headers,
244
+ "Referrer-Policy",
245
+ options.referrerPolicy ?? "strict-origin-when-cross-origin",
246
+ );
247
+ setHeaderIfMissing(
248
+ headers,
249
+ "Strict-Transport-Security",
250
+ formatStrictTransportSecurity(options.strictTransportSecurity),
251
+ );
252
+ setHeaderIfMissing(
253
+ headers,
254
+ "X-Content-Type-Options",
255
+ options.xContentTypeOptions ?? "nosniff",
256
+ );
257
+ setHeaderIfMissing(
258
+ headers,
259
+ "X-Frame-Options",
260
+ options.xFrameOptions ?? "DENY",
261
+ );
262
+ }
263
+
264
+ /**
265
+ * Create a server hook that adds common browser security headers to every
266
+ * response, including native streamed responses.
267
+ */
268
+ export function createSecurityHeadersHooks<Ctx>(
269
+ options: SecurityHeadersOptions = {},
270
+ ): ServerHook<Ctx> {
271
+ return {
272
+ name: "security-headers",
273
+ beforeSend: ({ response }) => {
274
+ const headers = { ...(response.headers ?? {}) };
275
+ applySecurityHeaders(headers, options);
276
+ return {
277
+ ...response,
278
+ headers,
279
+ };
280
+ },
281
+ };
282
+ }
283
+
284
+ function normalizeOrigin(value: string, source: string): string {
285
+ try {
286
+ return new URL(value).origin;
287
+ } catch {
288
+ throw new Error(`${source} must be an absolute URL origin.`);
289
+ }
290
+ }
291
+
292
+ function requestOrigin(
293
+ req: HttpRequestLike,
294
+ trustedProxy: TrustedProxyConfig | undefined,
295
+ ): string {
296
+ return resolveTrustedRequest(req, trustedProxyOriginConfig(trustedProxy))
297
+ .origin;
298
+ }
299
+
300
+ function trustedProxyOriginConfig(
301
+ trustedProxy: TrustedProxyConfig | undefined,
302
+ ): TrustedProxyConfig | undefined {
303
+ if (!trustedProxy) return trustedProxy;
304
+
305
+ const originConfig: TrustedProxyOptions = {};
306
+ if (trustedProxy.hostHeader !== undefined) {
307
+ originConfig.hostHeader = trustedProxy.hostHeader;
308
+ }
309
+ if (trustedProxy.protocolHeader !== undefined) {
310
+ originConfig.protocolHeader = trustedProxy.protocolHeader;
311
+ }
312
+ return originConfig;
313
+ }
314
+
315
+ function headerOrigin(value: string | null): string | undefined {
316
+ if (!value) return undefined;
317
+
318
+ try {
319
+ return new URL(value).origin;
320
+ } catch {
321
+ return undefined;
322
+ }
323
+ }
324
+
325
+ function originFromRequestHeaders(req: HttpRequestLike): string | undefined {
326
+ const origin = req.headers.get("origin");
327
+ if (origin) return headerOrigin(origin) ?? origin;
328
+
329
+ return headerOrigin(req.headers.get("referer"));
330
+ }
331
+
332
+ function normalizeTrustedOrigins(
333
+ trustedOrigins: CsrfHooksOptions["trustedOrigins"],
334
+ ): CsrfHooksOptions["trustedOrigins"] {
335
+ if (!Array.isArray(trustedOrigins)) return trustedOrigins;
336
+ return trustedOrigins.map((origin) =>
337
+ normalizeOrigin(origin, "trustedOrigins entries"),
338
+ );
339
+ }
340
+
341
+ function isTrustedOrigin(args: {
342
+ origin: string;
343
+ req: HttpRequestLike;
344
+ contract: HttpContractConfig;
345
+ requestOrigin: string;
346
+ trustedOrigins: CsrfHooksOptions["trustedOrigins"];
347
+ }): boolean {
348
+ if (args.origin === args.requestOrigin) return true;
349
+
350
+ if (Array.isArray(args.trustedOrigins)) {
351
+ return args.trustedOrigins.includes(args.origin);
352
+ }
353
+
354
+ if (typeof args.trustedOrigins === "function") {
355
+ return args.trustedOrigins({
356
+ origin: args.origin,
357
+ req: args.req,
358
+ contract: args.contract,
359
+ });
360
+ }
361
+
362
+ return false;
363
+ }
364
+
365
+ function decodeCookiePart(value: string): string {
366
+ try {
367
+ return decodeURIComponent(value);
368
+ } catch {
369
+ return value;
370
+ }
371
+ }
372
+
373
+ function parseCookieHeader(
374
+ cookieHeader: string | null,
375
+ ): Record<string, string> {
376
+ if (!cookieHeader) return {};
377
+
378
+ return Object.fromEntries(
379
+ cookieHeader
380
+ .split(";")
381
+ .map((entry) => entry.trim())
382
+ .filter(Boolean)
383
+ .map((entry) => {
384
+ const separator = entry.indexOf("=");
385
+ if (separator === -1) return [entry, ""];
386
+ return [
387
+ decodeCookiePart(entry.slice(0, separator).trim()),
388
+ decodeCookiePart(entry.slice(separator + 1).trim()),
389
+ ];
390
+ }),
391
+ );
392
+ }
393
+
394
+ function tokensMatch(left: string, right: string): boolean {
395
+ if (left.length !== right.length) return false;
396
+
397
+ let result = 0;
398
+ for (let index = 0; index < left.length; index += 1) {
399
+ result |= left.charCodeAt(index) ^ right.charCodeAt(index);
400
+ }
401
+ return result === 0;
402
+ }
403
+
404
+ function csrfError(reason: CsrfFailureReason, message: string): AppError {
405
+ return new AppError(httpErrors.Forbidden, { reason }, message);
406
+ }
407
+
408
+ function enforceOrigin(args: {
409
+ req: HttpRequestLike;
410
+ contract: HttpContractConfig;
411
+ allowMissingOrigin: boolean;
412
+ trustedOrigins: CsrfHooksOptions["trustedOrigins"];
413
+ trustedProxy: TrustedProxyConfig | undefined;
414
+ }): void {
415
+ const origin = originFromRequestHeaders(args.req);
416
+ if (!origin) {
417
+ if (args.allowMissingOrigin) return;
418
+ throw csrfError(
419
+ "missing_origin",
420
+ "CSRF check failed because the request did not include an Origin or Referer header.",
421
+ );
422
+ }
423
+
424
+ if (
425
+ !isTrustedOrigin({
426
+ origin,
427
+ req: args.req,
428
+ contract: args.contract,
429
+ requestOrigin: requestOrigin(args.req, args.trustedProxy),
430
+ trustedOrigins: args.trustedOrigins,
431
+ })
432
+ ) {
433
+ throw csrfError(
434
+ "untrusted_origin",
435
+ "CSRF check failed because the request origin is not trusted.",
436
+ );
437
+ }
438
+ }
439
+
440
+ function enforceToken(
441
+ req: HttpRequestLike,
442
+ token: CsrfTokenOptions | false,
443
+ ): void {
444
+ if (!token) return;
445
+
446
+ const headerName = token.headerName ?? DEFAULT_CSRF_HEADER;
447
+ const cookieName = token.cookieName ?? DEFAULT_CSRF_COOKIE;
448
+ const headerToken = req.headers.get(headerName);
449
+ const cookieToken = parseCookieHeader(req.headers.get("cookie"))[cookieName];
450
+
451
+ if (!headerToken || !cookieToken) {
452
+ throw csrfError(
453
+ "missing_token",
454
+ `CSRF check failed because ${headerName} and ${cookieName} must both be present.`,
455
+ );
456
+ }
457
+
458
+ if (!tokensMatch(headerToken, cookieToken)) {
459
+ throw csrfError(
460
+ "invalid_token",
461
+ "CSRF check failed because the submitted token does not match the cookie token.",
462
+ );
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Create CSRF protection for unsafe HTTP methods.
468
+ *
469
+ * The default protects cookie-backed browser routes from cross-origin unsafe
470
+ * requests while still allowing server-to-server calls and tests that do not
471
+ * send browser origin headers. Set `allowMissingOrigin: false` and configure
472
+ * `token` for stricter browser-only APIs.
473
+ */
474
+ export function createCsrfHooks<Ctx>(
475
+ options: CsrfHooksOptions = {},
476
+ ): ServerHook<Ctx> {
477
+ const protectedMethods = new Set(
478
+ (options.protectedMethods ?? DEFAULT_PROTECTED_METHODS).map((method) =>
479
+ method.toUpperCase(),
480
+ ),
481
+ );
482
+ const allowMissingOrigin = options.allowMissingOrigin ?? true;
483
+ const trustedOrigins = normalizeTrustedOrigins(options.trustedOrigins);
484
+ const token = options.token ?? false;
485
+
486
+ return {
487
+ name: "csrf",
488
+ onRequest: async ({ req, contract, params }) => {
489
+ if (!protectedMethods.has(req.method.toUpperCase())) return undefined;
490
+ if (await options.skip?.({ req, contract, params })) return undefined;
491
+
492
+ enforceOrigin({
493
+ req,
494
+ contract,
495
+ allowMissingOrigin,
496
+ trustedOrigins,
497
+ trustedProxy: options.trustedProxy,
498
+ });
499
+ enforceToken(req, token);
500
+ return undefined;
501
+ },
502
+ };
503
+ }
@@ -63,6 +63,10 @@ export {
63
63
  getActiveRequestContext,
64
64
  inheritActiveRequestContext,
65
65
  } from "./request-context.js";
66
+ /**
67
+ * Runtime workflow registration integrity helpers.
68
+ */
69
+ export * from "./runtime-integrity.js";
66
70
  /**
67
71
  * Server configuration and route types.
68
72
  */
@@ -89,6 +93,10 @@ export {
89
93
  * Server context blueprint declaration helper.
90
94
  */
91
95
  export { defineServerContext } from "./server-context.js";
96
+ /**
97
+ * Trusted proxy request metadata helpers.
98
+ */
99
+ export * from "./trusted-proxy.js";
92
100
  export * from "./types.js";
93
101
  /**
94
102
  * Contract-aware route binder types and helpers.
@@ -12,7 +12,9 @@ import {
12
12
  import {
13
13
  createTraceContext,
14
14
  parseTraceparent,
15
+ resolveTracingPort,
15
16
  type TraceContext,
17
+ type TracingPort,
16
18
  } from "../tracing/index.js";
17
19
  import type { HttpRequestLike, HttpResponseLike, ServerHook } from "./http.js";
18
20
  import {
@@ -129,6 +131,7 @@ type TraceContextFields = {
129
131
  spanId?: string;
130
132
  parentSpanId?: string;
131
133
  traceparent?: string;
134
+ tracestate?: string;
132
135
  };
133
136
 
134
137
  function getContextRequestId(ctx: unknown): string | undefined {
@@ -144,7 +147,8 @@ function getContextTraceContext(ctx: unknown): TraceContextFields | undefined {
144
147
  !context.traceId &&
145
148
  !context.spanId &&
146
149
  !context.parentSpanId &&
147
- !context.traceparent
150
+ !context.traceparent &&
151
+ !context.tracestate
148
152
  ) {
149
153
  return undefined;
150
154
  }
@@ -153,6 +157,7 @@ function getContextTraceContext(ctx: unknown): TraceContextFields | undefined {
153
157
  spanId: context.spanId,
154
158
  parentSpanId: context.parentSpanId,
155
159
  traceparent: context.traceparent,
160
+ tracestate: context.tracestate,
156
161
  };
157
162
  }
158
163
 
@@ -249,6 +254,7 @@ export function createServerInstrumentation<Ctx>(
249
254
  const ignorePaths = resolvedOptions.ignorePaths ?? ["/api/devtools"];
250
255
 
251
256
  let port: ProviderInstrumentationPort | undefined;
257
+ let tracing: TracingPort | undefined;
252
258
  const correlations = new WeakMap<HttpRequestLike, RequestCorrelation>();
253
259
 
254
260
  const prepareRequest = (req: HttpRequestLike): RequestCorrelation => {
@@ -263,12 +269,19 @@ export function createServerInstrumentation<Ctx>(
263
269
  traceContextHeader === false
264
270
  ? undefined
265
271
  : (req.headers.get(traceContextHeader) ?? undefined);
272
+ const parsedTraceparent = parseTraceparent(headerTraceparent);
266
273
 
274
+ const activeTrace = tracing?.current();
267
275
  const correlation: RequestCorrelation = {
268
276
  requestId: headerRequestId ?? createRequestId(),
269
- trace: createTraceContext({
270
- traceparent: parseTraceparent(headerTraceparent)?.traceparent,
271
- }),
277
+ trace:
278
+ activeTrace ??
279
+ createTraceContext({
280
+ traceparent: parsedTraceparent?.traceparent,
281
+ tracestate: parsedTraceparent
282
+ ? (req.headers.get("tracestate") ?? undefined)
283
+ : undefined,
284
+ }),
272
285
  };
273
286
  correlations.set(req, correlation);
274
287
  return correlation;
@@ -284,6 +297,9 @@ export function createServerInstrumentation<Ctx>(
284
297
  req: HttpRequestLike;
285
298
  ctx?: unknown;
286
299
  }): TraceContext => {
300
+ const activeTrace = tracing?.current();
301
+ if (activeTrace) return activeTrace;
302
+
287
303
  const contextTrace = getContextTraceContext(args.ctx);
288
304
  if (contextTrace) {
289
305
  return createTraceContext(contextTrace);
@@ -334,6 +350,7 @@ export function createServerInstrumentation<Ctx>(
334
350
  spanId: trace.spanId,
335
351
  parentSpanId: trace.parentSpanId,
336
352
  traceparent: trace.traceparent,
353
+ tracestate: trace.tracestate,
337
354
  actor: readContextActor(args.ctx),
338
355
  tenant: readContextTenant(args.ctx),
339
356
  });
@@ -416,6 +433,7 @@ export function createServerInstrumentation<Ctx>(
416
433
  spanId: trace.spanId,
417
434
  parentSpanId: trace.parentSpanId,
418
435
  traceparent: trace.traceparent,
436
+ tracestate: trace.tracestate,
419
437
  method: req.method,
420
438
  path,
421
439
  contractName: contract.name,
@@ -452,6 +470,7 @@ export function createServerInstrumentation<Ctx>(
452
470
  spanId: trace.spanId,
453
471
  parentSpanId: trace.parentSpanId,
454
472
  traceparent: trace.traceparent,
473
+ tracestate: trace.tracestate,
455
474
  message: getErrorMessage(error),
456
475
  stack: getErrorStack(error),
457
476
  contractName: contract.name,
@@ -466,13 +485,14 @@ export function createServerInstrumentation<Ctx>(
466
485
 
467
486
  return {
468
487
  attachPorts(ports) {
488
+ tracing = resolveTracingPort(ports);
469
489
  if (!enabled) return;
470
490
  port = resolveProviderInstrumentationPort(ports);
471
491
  },
472
492
  prepareRequest,
473
493
  createServiceCorrelation: () => ({
474
494
  requestId: createRequestId(),
475
- trace: createTraceContext(),
495
+ trace: tracing?.current() ?? createTraceContext(),
476
496
  }),
477
497
  hook: enabled ? hook : undefined,
478
498
  };
@@ -11,6 +11,7 @@ import type { ServiceProvider } from "../../providers/index.js";
11
11
  type ProviderConfigDef<Schema extends StandardSchemaV1<unknown, unknown>> = {
12
12
  schema: Schema;
13
13
  envPrefix?: string;
14
+ overrides?: Record<string, unknown>;
14
15
  };
15
16
 
16
17
  async function parseStandardSchema<
@@ -47,14 +48,30 @@ export async function loadProviderConfig<
47
48
  const configDef = provider.config as ProviderConfigDef<Schema> | undefined;
48
49
  if (!configDef) return undefined;
49
50
 
50
- const { schema, envPrefix } = configDef;
51
+ const { schema, envPrefix, overrides: fieldOverrides } = configDef;
51
52
  const hasOverride = Object.hasOwn(overrides, provider.name);
52
- const input = hasOverride
53
+ const base = hasOverride
53
54
  ? overrides[provider.name]
54
55
  : envPrefix
55
56
  ? readEnv({ env, prefix: envPrefix })
56
57
  : {};
57
58
 
59
+ // Field-level overrides from the provider's own config definition (factory
60
+ // options) win over env-derived and server-supplied values. Defined values
61
+ // only, so absent options fall back to the base input and schema defaults.
62
+ let input = base;
63
+ if (fieldOverrides) {
64
+ const defined = Object.fromEntries(
65
+ Object.entries(fieldOverrides).filter(([, value]) => value !== undefined),
66
+ );
67
+ if (Object.keys(defined).length > 0) {
68
+ input = {
69
+ ...(typeof base === "object" && base !== null ? base : {}),
70
+ ...defined,
71
+ };
72
+ }
73
+ }
74
+
58
75
  try {
59
76
  return await parseStandardSchema(schema, input);
60
77
  } catch (error) {
@@ -16,6 +16,7 @@ export interface ActiveRequestContext {
16
16
  spanId?: string;
17
17
  parentSpanId?: string;
18
18
  traceparent?: string;
19
+ tracestate?: string;
19
20
  /**
20
21
  * Actor for the current request or service execution, when known.
21
22
  */
@@ -116,7 +117,10 @@ export function inheritActiveRequestContext<
116
117
  Event extends {
117
118
  requestId?: string;
118
119
  traceId?: string;
120
+ spanId?: string;
121
+ parentSpanId?: string;
119
122
  traceparent?: string;
123
+ tracestate?: string;
120
124
  },
121
125
  >(event: Event): Event {
122
126
  const context = getActiveRequestContext();
@@ -126,6 +130,9 @@ export function inheritActiveRequestContext<
126
130
  ...event,
127
131
  requestId: event.requestId ?? context.requestId,
128
132
  traceId: event.traceId ?? context.traceId,
133
+ spanId: event.spanId ?? context.spanId,
134
+ parentSpanId: event.parentSpanId ?? context.parentSpanId,
129
135
  traceparent: event.traceparent ?? context.traceparent,
136
+ tracestate: event.tracestate ?? context.tracestate,
130
137
  };
131
138
  }