@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,322 @@
1
+ import type { EventPayloadDef, ListenerDef } from "../events/index.js";
2
+ import type { JobDef } from "../jobs/index.js";
3
+ import type { OutboxRegistry } from "../outbox/index.js";
4
+ import type { ScheduleDef } from "../schedules/index.js";
5
+ import type { TaskDef } from "../tasks/index.js";
6
+
7
+ export type RuntimeIntegrityArtifact =
8
+ | "listener"
9
+ | "schedule"
10
+ | "task"
11
+ | "outboxEvent"
12
+ | "outboxJob";
13
+
14
+ export type RuntimeIntegrityFindingCode =
15
+ | "missing_listener_registration"
16
+ | "missing_schedule_registration"
17
+ | "missing_task_registration"
18
+ | "missing_outbox_event_registration"
19
+ | "missing_outbox_job_registration"
20
+ | "duplicate_runtime_manifest_entry"
21
+ | "duplicate_runtime_registry_entry";
22
+
23
+ export type RuntimeIntegrityFindingLocation = "manifest" | "registry";
24
+
25
+ export interface RuntimeIntegrityFinding {
26
+ readonly code: RuntimeIntegrityFindingCode;
27
+ readonly artifact: RuntimeIntegrityArtifact;
28
+ readonly name: string;
29
+ readonly message: string;
30
+ readonly location?: RuntimeIntegrityFindingLocation;
31
+ }
32
+
33
+ export interface RuntimeIntegrityResult {
34
+ readonly ok: boolean;
35
+ readonly findings: readonly RuntimeIntegrityFinding[];
36
+ }
37
+
38
+ export interface RuntimeOutboxManifest {
39
+ readonly events?: readonly EventPayloadDef[];
40
+ readonly jobs?: readonly JobDef[];
41
+ }
42
+
43
+ export interface RuntimeManifestInput {
44
+ readonly listeners?: readonly ListenerDef[];
45
+ readonly schedules?: readonly ScheduleDef[];
46
+ readonly tasks?: readonly TaskDef[];
47
+ readonly outbox?: RuntimeOutboxManifest;
48
+ }
49
+
50
+ export interface RuntimeManifest {
51
+ readonly listeners: readonly ListenerDef[];
52
+ readonly schedules: readonly ScheduleDef[];
53
+ readonly tasks: readonly TaskDef[];
54
+ readonly outbox: {
55
+ readonly events: readonly EventPayloadDef[];
56
+ readonly jobs: readonly JobDef[];
57
+ };
58
+ }
59
+
60
+ export interface RuntimeRegistriesInput {
61
+ readonly listeners?: readonly ListenerDef[];
62
+ readonly schedules?: readonly ScheduleDef[];
63
+ readonly tasks?: readonly TaskDef[];
64
+ readonly outbox?: OutboxRegistry;
65
+ }
66
+
67
+ export interface RuntimeRegistries {
68
+ readonly listeners: readonly ListenerDef[];
69
+ readonly schedules: readonly ScheduleDef[];
70
+ readonly tasks: readonly TaskDef[];
71
+ readonly outbox?: OutboxRegistry;
72
+ }
73
+
74
+ export type RuntimeIntegrityMode = "error" | "warn";
75
+
76
+ export interface RuntimeIntegrityLogger {
77
+ warn(message: string): void;
78
+ }
79
+
80
+ export interface CreateRuntimeIntegrityOptions {
81
+ readonly manifest: RuntimeManifest;
82
+ readonly registries: RuntimeRegistries;
83
+ /**
84
+ * How to handle integrity findings.
85
+ *
86
+ * @default "error"
87
+ */
88
+ readonly mode?: RuntimeIntegrityMode;
89
+ /**
90
+ * Logger used when `mode` is `"warn"`.
91
+ *
92
+ * @default console
93
+ */
94
+ readonly logger?: RuntimeIntegrityLogger;
95
+ }
96
+
97
+ export interface RuntimeIntegrityCheck {
98
+ readonly kind: "runtime-integrity";
99
+ check(): RuntimeIntegrityResult;
100
+ }
101
+
102
+ type NamedDefinition = {
103
+ readonly name: string;
104
+ };
105
+
106
+ type IntegrityArtifactConfig = {
107
+ readonly artifact: RuntimeIntegrityArtifact;
108
+ readonly label: string;
109
+ readonly manifestNames: readonly string[];
110
+ readonly registryNames: readonly string[];
111
+ readonly missingCode: RuntimeIntegrityFindingCode;
112
+ readonly registryDescription: string;
113
+ };
114
+
115
+ export class RuntimeIntegrityError extends Error {
116
+ readonly findings: readonly RuntimeIntegrityFinding[];
117
+
118
+ constructor(findings: readonly RuntimeIntegrityFinding[]) {
119
+ super(formatRuntimeIntegrityFailure(findings));
120
+ this.name = "RuntimeIntegrityError";
121
+ this.findings = findings;
122
+ }
123
+ }
124
+
125
+ export function defineRuntimeManifest(
126
+ input: RuntimeManifestInput,
127
+ ): RuntimeManifest {
128
+ return {
129
+ listeners: input.listeners ?? [],
130
+ schedules: input.schedules ?? [],
131
+ tasks: input.tasks ?? [],
132
+ outbox: {
133
+ events: input.outbox?.events ?? [],
134
+ jobs: input.outbox?.jobs ?? [],
135
+ },
136
+ };
137
+ }
138
+
139
+ export function defineRuntimeRegistries(
140
+ input: RuntimeRegistriesInput,
141
+ ): RuntimeRegistries {
142
+ return {
143
+ listeners: input.listeners ?? [],
144
+ schedules: input.schedules ?? [],
145
+ tasks: input.tasks ?? [],
146
+ outbox: input.outbox,
147
+ };
148
+ }
149
+
150
+ export function inspectRuntimeIntegrity(
151
+ manifest: RuntimeManifest,
152
+ registries: RuntimeRegistries,
153
+ ): RuntimeIntegrityResult {
154
+ const configs: readonly IntegrityArtifactConfig[] = [
155
+ {
156
+ artifact: "listener",
157
+ label: "listener",
158
+ manifestNames: namesFromDefinitions(manifest.listeners),
159
+ registryNames: namesFromDefinitions(registries.listeners),
160
+ missingCode: "missing_listener_registration",
161
+ registryDescription: "runtime registries.listeners",
162
+ },
163
+ {
164
+ artifact: "schedule",
165
+ label: "schedule",
166
+ manifestNames: namesFromDefinitions(manifest.schedules),
167
+ registryNames: namesFromDefinitions(registries.schedules),
168
+ missingCode: "missing_schedule_registration",
169
+ registryDescription: "runtime registries.schedules",
170
+ },
171
+ {
172
+ artifact: "task",
173
+ label: "task",
174
+ manifestNames: namesFromDefinitions(manifest.tasks),
175
+ registryNames: namesFromDefinitions(registries.tasks),
176
+ missingCode: "missing_task_registration",
177
+ registryDescription: "runtime registries.tasks",
178
+ },
179
+ {
180
+ artifact: "outboxEvent",
181
+ label: "outbox event",
182
+ manifestNames: namesFromDefinitions(manifest.outbox.events),
183
+ registryNames: namesFromMap(registries.outbox?.events),
184
+ missingCode: "missing_outbox_event_registration",
185
+ registryDescription: "runtime registries.outbox.events",
186
+ },
187
+ {
188
+ artifact: "outboxJob",
189
+ label: "outbox job",
190
+ manifestNames: namesFromDefinitions(manifest.outbox.jobs),
191
+ registryNames: namesFromMap(registries.outbox?.jobs),
192
+ missingCode: "missing_outbox_job_registration",
193
+ registryDescription: "runtime registries.outbox.jobs",
194
+ },
195
+ ];
196
+
197
+ const findings = configs.flatMap((config) =>
198
+ inspectArtifactIntegrity(config),
199
+ );
200
+
201
+ return {
202
+ ok: findings.length === 0,
203
+ findings,
204
+ };
205
+ }
206
+
207
+ export function createRuntimeIntegrity(
208
+ options: CreateRuntimeIntegrityOptions,
209
+ ): RuntimeIntegrityCheck {
210
+ const mode = options.mode ?? "error";
211
+ const logger = options.logger ?? console;
212
+
213
+ return {
214
+ kind: "runtime-integrity",
215
+ check() {
216
+ const result = inspectRuntimeIntegrity(
217
+ options.manifest,
218
+ options.registries,
219
+ );
220
+
221
+ if (result.ok) return result;
222
+
223
+ if (mode === "warn") {
224
+ logger.warn(
225
+ `[beignet] ${formatRuntimeIntegrityFailure(result.findings)}`,
226
+ );
227
+ return result;
228
+ }
229
+
230
+ throw new RuntimeIntegrityError(result.findings);
231
+ },
232
+ };
233
+ }
234
+
235
+ export function runRuntimeIntegrityCheck(
236
+ check: RuntimeIntegrityCheck | undefined,
237
+ ): RuntimeIntegrityResult | undefined {
238
+ return check?.check();
239
+ }
240
+
241
+ function inspectArtifactIntegrity(
242
+ config: IntegrityArtifactConfig,
243
+ ): RuntimeIntegrityFinding[] {
244
+ const findings: RuntimeIntegrityFinding[] = [];
245
+ const manifestDuplicates = duplicateNames(config.manifestNames);
246
+ const registryDuplicates = duplicateNames(config.registryNames);
247
+
248
+ for (const name of manifestDuplicates) {
249
+ findings.push({
250
+ code: "duplicate_runtime_manifest_entry",
251
+ artifact: config.artifact,
252
+ name,
253
+ location: "manifest",
254
+ message: `Duplicate ${config.label} "${name}" in runtime manifest.`,
255
+ });
256
+ }
257
+
258
+ for (const name of registryDuplicates) {
259
+ findings.push({
260
+ code: "duplicate_runtime_registry_entry",
261
+ artifact: config.artifact,
262
+ name,
263
+ location: "registry",
264
+ message: `Duplicate ${config.label} "${name}" in ${config.registryDescription}.`,
265
+ });
266
+ }
267
+
268
+ const registryNameSet = new Set(config.registryNames);
269
+ for (const name of uniqueNames(config.manifestNames)) {
270
+ if (registryNameSet.has(name)) continue;
271
+ findings.push({
272
+ code: config.missingCode,
273
+ artifact: config.artifact,
274
+ name,
275
+ message: `Missing ${config.label} registration "${name}": declared in runtime manifest but not present in ${config.registryDescription}.`,
276
+ });
277
+ }
278
+
279
+ return findings;
280
+ }
281
+
282
+ function namesFromDefinitions(
283
+ definitions: readonly NamedDefinition[] | undefined,
284
+ ): string[] {
285
+ return (definitions ?? []).map((definition) => definition.name);
286
+ }
287
+
288
+ function namesFromMap<T extends NamedDefinition>(
289
+ definitions: ReadonlyMap<string, T> | undefined,
290
+ ): string[] {
291
+ return [...(definitions?.keys() ?? [])];
292
+ }
293
+
294
+ function duplicateNames(names: readonly string[]): string[] {
295
+ const seen = new Set<string>();
296
+ const duplicates = new Set<string>();
297
+
298
+ for (const name of names) {
299
+ if (seen.has(name)) {
300
+ duplicates.add(name);
301
+ continue;
302
+ }
303
+ seen.add(name);
304
+ }
305
+
306
+ return [...duplicates].sort();
307
+ }
308
+
309
+ function uniqueNames(names: readonly string[]): string[] {
310
+ return [...new Set(names)].sort();
311
+ }
312
+
313
+ function formatRuntimeIntegrityFailure(
314
+ findings: readonly RuntimeIntegrityFinding[],
315
+ ): string {
316
+ const summary = `Runtime integrity check failed with ${findings.length} finding${findings.length === 1 ? "" : "s"}.`;
317
+ const details = findings
318
+ .map((finding) => `- [${finding.code}] ${finding.message}`)
319
+ .join("\n");
320
+
321
+ return `${summary}\n${details}`;
322
+ }
@@ -44,6 +44,12 @@ import {
44
44
  resolveProviderInstrumentationPort,
45
45
  type ServiceProvider,
46
46
  } from "../providers/index.js";
47
+ import {
48
+ parseTraceparent,
49
+ resolveTracingPort,
50
+ runWithTracing,
51
+ type TraceContextInput,
52
+ } from "../tracing/index.js";
47
53
  import type {
48
54
  ContextSeed,
49
55
  ServerContextConfig,
@@ -87,6 +93,8 @@ import {
87
93
  runWithActiveRequestContext,
88
94
  setActiveRequestIdentity,
89
95
  } from "./request-context.js";
96
+ import type { RuntimeIntegrityCheck } from "./runtime-integrity.js";
97
+ import { runRuntimeIntegrityCheck } from "./runtime-integrity.js";
90
98
  import type {
91
99
  AnyUseCaseLike,
92
100
  AnyUseCaseRouteDef,
@@ -353,6 +361,15 @@ export type CreateServerOptions<
353
361
  * Server hooks that wrap every registered route.
354
362
  */
355
363
  hooks?: ServerHook<Ctx, Ports & InferProviderPorts<Providers>>[];
364
+ /**
365
+ * Optional pure startup check for app workflow registrations.
366
+ *
367
+ * Runtime integrity compares app-declared workflow artifacts against the
368
+ * registries passed to runtime entrypoints. It performs no filesystem,
369
+ * provider, database, network, worker, or background-loop work, so it is safe
370
+ * to run during serverless cold starts.
371
+ */
372
+ integrity?: RuntimeIntegrityCheck;
356
373
  /**
357
374
  * Server-owned request instrumentation.
358
375
  *
@@ -1192,7 +1209,7 @@ function createRequestExecutor<
1192
1209
  const warnedNativeReplacementHooks = new WeakSet<object>();
1193
1210
  const maxRequestBodyBytes = requestBodyLimit(options.requestBody);
1194
1211
 
1195
- return async (
1212
+ const executeRequest = async (
1196
1213
  target: ExecutionTarget<Ctx, C>,
1197
1214
  req: HttpRequestLike,
1198
1215
  preMatchedParams?: Record<string, string>,
@@ -2131,6 +2148,72 @@ function createRequestExecutor<
2131
2148
  );
2132
2149
  }
2133
2150
  };
2151
+
2152
+ return async (
2153
+ target: ExecutionTarget<Ctx, C>,
2154
+ req: HttpRequestLike,
2155
+ preMatchedParams?: Record<string, string>,
2156
+ ) => {
2157
+ const tracing = resolveTracingPort(finalPorts);
2158
+ const instrumentationOptions =
2159
+ options.instrumentation === false ? undefined : options.instrumentation;
2160
+ const pathname = (() => {
2161
+ try {
2162
+ return new URL(req.url).pathname;
2163
+ } catch {
2164
+ return req.url;
2165
+ }
2166
+ })();
2167
+ const ignored = (
2168
+ instrumentationOptions?.ignorePaths ?? ["/api/devtools"]
2169
+ ).some((prefix) => {
2170
+ const normalized = prefix.replace(/\/+$/, "");
2171
+ return pathname === normalized || pathname.startsWith(`${normalized}/`);
2172
+ });
2173
+
2174
+ if (!tracing || ignored) {
2175
+ return executeRequest(target, req, preMatchedParams);
2176
+ }
2177
+
2178
+ const traceContextHeader =
2179
+ instrumentationOptions?.traceContextHeader ?? "traceparent";
2180
+ const active = tracing.current();
2181
+ const parsedTraceparent =
2182
+ traceContextHeader === false
2183
+ ? undefined
2184
+ : parseTraceparent(req.headers.get(traceContextHeader));
2185
+ const parent: TraceContextInput | undefined = active
2186
+ ? undefined
2187
+ : !parsedTraceparent
2188
+ ? undefined
2189
+ : {
2190
+ traceparent: parsedTraceparent.traceparent,
2191
+ tracestate: req.headers.get("tracestate") ?? undefined,
2192
+ };
2193
+ const traceAttributes = {
2194
+ "beignet.contract.name": target.contract.name,
2195
+ "http.request.method": req.method.toUpperCase(),
2196
+ "http.route": target.contract.path,
2197
+ } as const;
2198
+
2199
+ return await runWithTracing(
2200
+ tracing,
2201
+ {
2202
+ name: `beignet.request ${target.contract.name}`,
2203
+ type: "request",
2204
+ kind: active ? "internal" : "server",
2205
+ parent,
2206
+ attributes: traceAttributes,
2207
+ metricAttributes: traceAttributes,
2208
+ },
2209
+ async (span) => {
2210
+ const response = await executeRequest(target, req, preMatchedParams);
2211
+ span?.setAttribute("http.response.status_code", response.status);
2212
+ if (response.status >= 500) span?.setStatus("error");
2213
+ return response;
2214
+ },
2215
+ );
2216
+ };
2134
2217
  }
2135
2218
 
2136
2219
  /**
@@ -2581,6 +2664,8 @@ export async function createServer<
2581
2664
  }
2582
2665
  }
2583
2666
 
2667
+ runRuntimeIntegrityCheck(options.integrity);
2668
+
2584
2669
  try {
2585
2670
  for (const provider of providers) {
2586
2671
  const cfg = await loadProviderConfig(provider, env, overrides);
@@ -0,0 +1,249 @@
1
+ import type { HttpRequestLike } from "./http.js";
2
+
3
+ /**
4
+ * Header source used to resolve a client IP after an app has explicitly opted
5
+ * into trusting its deployment proxy or edge.
6
+ */
7
+ export type TrustedProxyClientIpSource =
8
+ | "x-forwarded-for-last"
9
+ | "x-forwarded-for-first"
10
+ | "x-real-ip"
11
+ | "cf-connecting-ip"
12
+ | { header: string }
13
+ | ((req: HttpRequestLike) => string | undefined);
14
+
15
+ /**
16
+ * Trusted proxy configuration for request metadata.
17
+ *
18
+ * Beignet trusts no forwarding headers by default. Configure this only when
19
+ * the app is always behind a platform or reverse proxy that strips or
20
+ * normalizes these headers before they reach application code.
21
+ */
22
+ export interface TrustedProxyOptions {
23
+ /**
24
+ * Header source for the end-user client IP. Omit or set to `false` when no
25
+ * client-IP header should be trusted.
26
+ */
27
+ clientIp?: TrustedProxyClientIpSource | false;
28
+ /**
29
+ * Header that carries the external request protocol.
30
+ *
31
+ * Defaults to `x-forwarded-proto` when trusted proxy handling is enabled.
32
+ */
33
+ protocolHeader?: string | false;
34
+ /**
35
+ * Header that carries the external request host.
36
+ *
37
+ * Defaults to `x-forwarded-host` when trusted proxy handling is enabled.
38
+ */
39
+ hostHeader?: string | false;
40
+ }
41
+
42
+ /**
43
+ * Set to `false` or omit the config to trust no forwarding headers.
44
+ */
45
+ export type TrustedProxyConfig = false | TrustedProxyOptions;
46
+
47
+ /**
48
+ * Request metadata after applying an explicit trusted-proxy policy.
49
+ */
50
+ export interface TrustedRequestInfo {
51
+ /**
52
+ * URL as seen by the app or reconstructed from trusted proxy headers.
53
+ */
54
+ url: URL;
55
+ /**
56
+ * External request origin.
57
+ */
58
+ origin: string;
59
+ /**
60
+ * External request protocol without a trailing colon.
61
+ */
62
+ protocol: "http" | "https";
63
+ /**
64
+ * External request host, including port when present.
65
+ */
66
+ host: string;
67
+ /**
68
+ * Resolved client IP when a trusted client-IP source is configured.
69
+ */
70
+ clientIp?: string;
71
+ /**
72
+ * Whether forwarding headers were eligible to affect this result.
73
+ */
74
+ trustedProxy: boolean;
75
+ }
76
+
77
+ const DEFAULT_PROTOCOL_HEADER = "x-forwarded-proto";
78
+ const DEFAULT_HOST_HEADER = "x-forwarded-host";
79
+
80
+ function splitForwardedHeader(value: string | null): string[] {
81
+ if (!value) return [];
82
+ return value
83
+ .split(",")
84
+ .map((entry) => entry.trim())
85
+ .filter(Boolean);
86
+ }
87
+
88
+ function firstHeaderValue(
89
+ req: HttpRequestLike,
90
+ header: string,
91
+ ): string | undefined {
92
+ return splitForwardedHeader(req.headers.get(header))[0];
93
+ }
94
+
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
+ function normalizeProtocol(
104
+ value: string | undefined,
105
+ ): "http" | "https" | undefined {
106
+ if (!value) return undefined;
107
+ const protocol = value.toLowerCase().replace(/:$/, "");
108
+ if (protocol === "http" || protocol === "https") return protocol;
109
+ return undefined;
110
+ }
111
+
112
+ function hasInvalidHostCharacter(value: string): boolean {
113
+ for (const char of value) {
114
+ const code = char.charCodeAt(0);
115
+ if (
116
+ code <= 32 ||
117
+ code === 127 ||
118
+ char === "/" ||
119
+ char === "\\" ||
120
+ char === "@" ||
121
+ char === "?" ||
122
+ char === "#"
123
+ ) {
124
+ return true;
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+
130
+ function normalizeHost(
131
+ value: string | undefined,
132
+ protocol: "http" | "https",
133
+ ): string | undefined {
134
+ if (!value || hasInvalidHostCharacter(value)) {
135
+ return undefined;
136
+ }
137
+
138
+ try {
139
+ return new URL(`${protocol}://${value}`).host;
140
+ } catch {
141
+ return undefined;
142
+ }
143
+ }
144
+
145
+ function baseRequestUrl(req: HttpRequestLike): URL {
146
+ try {
147
+ return new URL(req.url);
148
+ } catch {
149
+ throw new Error("req.url must be an absolute URL.");
150
+ }
151
+ }
152
+
153
+ function normalizedBaseProtocol(url: URL): "http" | "https" {
154
+ const protocol = normalizeProtocol(url.protocol);
155
+ if (protocol) return protocol;
156
+ throw new Error("req.url must use http or https.");
157
+ }
158
+
159
+ /**
160
+ * Resolve a client IP from a configured trusted proxy source.
161
+ */
162
+ export function resolveTrustedClientIp(
163
+ req: HttpRequestLike,
164
+ source: TrustedProxyClientIpSource | false | undefined,
165
+ ): string | undefined {
166
+ if (!source) return undefined;
167
+
168
+ if (typeof source === "function") {
169
+ return source(req)?.trim() || undefined;
170
+ }
171
+
172
+ if (typeof source === "object") {
173
+ return firstHeaderValue(
174
+ req,
175
+ requireHeaderName(source.header, "trustedProxy.clientIp.header"),
176
+ );
177
+ }
178
+
179
+ if (source === "x-forwarded-for-first" || source === "x-forwarded-for-last") {
180
+ const entries = splitForwardedHeader(req.headers.get("x-forwarded-for"));
181
+ if (entries.length === 0) return undefined;
182
+ return source === "x-forwarded-for-first"
183
+ ? entries[0]
184
+ : entries[entries.length - 1];
185
+ }
186
+
187
+ return firstHeaderValue(req, source);
188
+ }
189
+
190
+ /**
191
+ * Resolve request metadata using only app-visible URL data unless a trusted
192
+ * proxy policy is explicitly configured.
193
+ */
194
+ export function resolveTrustedRequest(
195
+ req: HttpRequestLike,
196
+ config: TrustedProxyConfig | undefined = false,
197
+ ): TrustedRequestInfo {
198
+ const baseUrl = baseRequestUrl(req);
199
+ const baseProtocol = normalizedBaseProtocol(baseUrl);
200
+ const baseHost = baseUrl.host;
201
+
202
+ if (!config) {
203
+ return {
204
+ url: baseUrl,
205
+ origin: baseUrl.origin,
206
+ protocol: baseProtocol,
207
+ host: baseHost,
208
+ trustedProxy: false,
209
+ };
210
+ }
211
+
212
+ const protocolHeader =
213
+ config.protocolHeader === false
214
+ ? undefined
215
+ : requireHeaderName(
216
+ config.protocolHeader ?? DEFAULT_PROTOCOL_HEADER,
217
+ "trustedProxy.protocolHeader",
218
+ );
219
+ const hostHeader =
220
+ config.hostHeader === false
221
+ ? undefined
222
+ : requireHeaderName(
223
+ config.hostHeader ?? DEFAULT_HOST_HEADER,
224
+ "trustedProxy.hostHeader",
225
+ );
226
+
227
+ const protocol =
228
+ normalizeProtocol(
229
+ protocolHeader ? firstHeaderValue(req, protocolHeader) : undefined,
230
+ ) ?? baseProtocol;
231
+ const host =
232
+ normalizeHost(
233
+ hostHeader ? firstHeaderValue(req, hostHeader) : undefined,
234
+ protocol,
235
+ ) ?? baseHost;
236
+ const url = new URL(baseUrl);
237
+ url.protocol = `${protocol}:`;
238
+ url.host = host;
239
+ const clientIp = resolveTrustedClientIp(req, config.clientIp);
240
+
241
+ return {
242
+ url,
243
+ origin: url.origin,
244
+ protocol,
245
+ host,
246
+ ...(clientIp !== undefined ? { clientIp } : {}),
247
+ trustedProxy: true,
248
+ };
249
+ }