@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.
- package/CHANGELOG.md +115 -0
- package/README.md +346 -21
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +63 -47
- package/dist/application/index.js.map +1 -1
- package/dist/domain/entity.d.ts +37 -6
- package/dist/domain/entity.d.ts.map +1 -1
- package/dist/domain/entity.js +9 -7
- package/dist/domain/entity.js.map +1 -1
- package/dist/domain/index.d.ts +4 -4
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +4 -4
- package/dist/domain/index.js.map +1 -1
- package/dist/events/index.d.ts +6 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +20 -10
- package/dist/events/index.js.map +1 -1
- package/dist/flags/index.d.ts +5 -4
- package/dist/flags/index.d.ts.map +1 -1
- package/dist/flags/index.js +10 -9
- package/dist/flags/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +11 -1
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +4 -3
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +449 -9
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +472 -12
- package/dist/jobs/index.js.map +1 -1
- package/dist/outbox/index.d.ts +170 -3
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +192 -8
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/index.d.ts +6 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +4 -0
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/provider.d.ts +10 -2
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +8 -7
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +47 -16
- package/dist/schedules/index.js.map +1 -1
- package/dist/search/index.d.ts +2 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -0
- package/dist/search/index.js.map +1 -1
- package/dist/server/hooks/index.d.ts +1 -0
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +1 -0
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +26 -13
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +28 -34
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks/security.d.ts +179 -0
- package/dist/server/hooks/security.d.ts.map +1 -0
- package/dist/server/hooks/security.js +225 -0
- package/dist/server/hooks/security.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +22 -6
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
- package/dist/server/providers/loadProviderConfig.js +15 -2
- package/dist/server/providers/loadProviderConfig.js.map +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +3 -0
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/runtime-integrity.d.ts +84 -0
- package/dist/server/runtime-integrity.d.ts.map +1 -0
- package/dist/server/runtime-integrity.js +160 -0
- package/dist/server/runtime-integrity.js.map +1 -0
- package/dist/server/server.d.ts +10 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +55 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy.d.ts +77 -0
- package/dist/server/trusted-proxy.d.ts.map +1 -0
- package/dist/server/trusted-proxy.js +129 -0
- package/dist/server/trusted-proxy.js.map +1 -0
- package/dist/tasks/index.d.ts +6 -7
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +22 -5
- package/dist/tasks/index.js.map +1 -1
- package/dist/tenancy/index.d.ts +41 -0
- package/dist/tenancy/index.d.ts.map +1 -0
- package/dist/tenancy/index.js +34 -0
- package/dist/tenancy/index.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +12 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/execution.d.ts +14 -0
- package/dist/tracing/execution.d.ts.map +1 -0
- package/dist/tracing/execution.js +17 -0
- package/dist/tracing/execution.js.map +1 -0
- package/dist/tracing/index.d.ts +49 -0
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js +59 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/client.d.ts.map +1 -1
- package/dist/uploads/client.js +44 -6
- package/dist/uploads/client.js.map +1 -1
- package/dist/uploads/index.d.ts +182 -2
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +468 -76
- package/dist/uploads/index.js.map +1 -1
- package/dist/webhooks/index.d.ts +56 -4
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +107 -2
- package/dist/webhooks/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/app-architecture/SKILL.md +23 -2
- package/src/application/index.ts +88 -56
- package/src/domain/entity.ts +61 -13
- package/src/domain/index.ts +8 -7
- package/src/events/index.ts +27 -14
- package/src/flags/index.ts +23 -19
- package/src/idempotency/index.ts +17 -3
- package/src/jobs/index.ts +1022 -19
- package/src/outbox/index.ts +409 -9
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +17 -0
- package/src/providers/instrumentation.ts +4 -0
- package/src/providers/provider.ts +11 -2
- package/src/schedules/index.ts +60 -26
- package/src/search/index.ts +3 -1
- package/src/server/hooks/index.ts +10 -0
- package/src/server/hooks/rate-limit.ts +52 -46
- package/src/server/hooks/security.ts +503 -0
- package/src/server/index.ts +8 -0
- package/src/server/instrumentation.ts +25 -5
- package/src/server/providers/loadProviderConfig.ts +19 -2
- package/src/server/request-context.ts +7 -0
- package/src/server/runtime-integrity.ts +322 -0
- package/src/server/server.ts +86 -1
- package/src/server/trusted-proxy.ts +249 -0
- package/src/tasks/index.ts +29 -12
- package/src/tenancy/index.ts +55 -0
- package/src/testing/index.ts +19 -3
- package/src/tracing/execution.ts +37 -0
- package/src/tracing/index.ts +137 -0
- package/src/uploads/client.ts +65 -6
- package/src/uploads/index.ts +713 -76
- package/src/webhooks/index.ts +240 -9
- 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
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|