@beignet/core 0.0.50 → 0.0.52
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 +24 -0
- package/README.md +99 -32
- package/dist/application/index.d.ts +5 -5
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +8 -4
- package/dist/application/index.js.map +1 -1
- package/dist/events/index.d.ts +26 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +84 -7
- package/dist/events/index.js.map +1 -1
- package/dist/events/payload-state.d.ts +16 -0
- package/dist/events/payload-state.d.ts.map +1 -0
- package/dist/events/payload-state.js +123 -0
- package/dist/events/payload-state.js.map +1 -0
- package/dist/events/transport.d.ts +25 -0
- package/dist/events/transport.d.ts.map +1 -0
- package/dist/events/transport.js +192 -0
- package/dist/events/transport.js.map +1 -0
- package/dist/locks/index.d.ts.map +1 -1
- package/dist/locks/index.js +0 -4
- package/dist/locks/index.js.map +1 -1
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +27 -4
- package/dist/openapi/index.js.map +1 -1
- package/dist/outbox/index.d.ts +163 -15
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +1009 -155
- package/dist/outbox/index.js.map +1 -1
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +0 -4
- package/dist/payments/index.js.map +1 -1
- package/dist/ports/events.d.ts +4 -1
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/storage.d.ts +7 -0
- package/dist/ports/storage.d.ts.map +1 -1
- package/dist/ports/storage.js +4 -0
- package/dist/ports/storage.js.map +1 -1
- package/dist/ports/testing.d.ts +16 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +19 -4
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts +9 -7
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +16 -7
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider.d.ts +0 -38
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +2 -6
- package/dist/search/index.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +5 -0
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/cors.js +29 -1
- package/dist/server/hooks/cors.js.map +1 -1
- package/dist/server/instrumentation.d.ts +5 -5
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +3 -4
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +25 -13
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/server.d.ts +8 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +35 -12
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +12 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +37 -16
- package/dist/uploads/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-architecture/SKILL.md +16 -0
- package/src/application/index.ts +39 -7
- package/src/events/index.ts +137 -7
- package/src/events/payload-state.ts +223 -0
- package/src/events/transport.ts +242 -0
- package/src/locks/index.ts +0 -4
- package/src/openapi/index.ts +41 -3
- package/src/outbox/index.ts +1393 -178
- package/src/payments/index.ts +0 -4
- package/src/ports/events.ts +4 -1
- package/src/ports/index.ts +3 -0
- package/src/ports/storage.ts +10 -0
- package/src/ports/testing.ts +42 -4
- package/src/ports/unit-of-work.ts +33 -15
- package/src/providers/index.ts +0 -1
- package/src/providers/provider.ts +0 -40
- package/src/search/index.ts +2 -6
- package/src/server/hooks/cors.ts +42 -0
- package/src/server/instrumentation.ts +10 -8
- package/src/server/response-finalization.ts +33 -15
- package/src/server/server.ts +61 -27
- package/src/testing/index.ts +20 -9
- package/src/uploads/index.ts +34 -16
package/src/payments/index.ts
CHANGED
|
@@ -624,10 +624,6 @@ export function createMemoryPaymentsProvider(
|
|
|
624
624
|
|
|
625
625
|
return createProvider({
|
|
626
626
|
name,
|
|
627
|
-
metadata: {
|
|
628
|
-
ports: ["payments"],
|
|
629
|
-
watchers: ["payments"],
|
|
630
|
-
},
|
|
631
627
|
setup({ ports }) {
|
|
632
628
|
const instrumentation = createProviderInstrumentation(ports, {
|
|
633
629
|
providerName: name,
|
package/src/ports/events.ts
CHANGED
|
@@ -53,7 +53,10 @@ export type InferJobPayload<J extends JobDef> = InferContractJobPayload<J>;
|
|
|
53
53
|
* An EventBus port for publishing and subscribing to domain events.
|
|
54
54
|
*
|
|
55
55
|
* This interface defines a framework-agnostic contract for event-driven
|
|
56
|
-
* communication within your application.
|
|
56
|
+
* communication within your application. Implementations must prepare
|
|
57
|
+
* producer payloads with `prepareEventPayloadForTransport(...)` from
|
|
58
|
+
* `@beignet/core/events` so direct publication and provider swaps preserve the
|
|
59
|
+
* same canonical JSON semantics.
|
|
57
60
|
*
|
|
58
61
|
* @example
|
|
59
62
|
* ```ts
|
package/src/ports/index.ts
CHANGED
|
@@ -283,6 +283,7 @@ export type {
|
|
|
283
283
|
ClaimedOutboxMessage,
|
|
284
284
|
OutboxAdminPort,
|
|
285
285
|
OutboxClaimBatchOptions,
|
|
286
|
+
OutboxClaimBatchResult,
|
|
286
287
|
OutboxCountMessagesOptions,
|
|
287
288
|
OutboxDeleteResult,
|
|
288
289
|
OutboxEnqueueInput,
|
|
@@ -296,6 +297,8 @@ export type {
|
|
|
296
297
|
OutboxPort,
|
|
297
298
|
OutboxPruneDeliveredInput,
|
|
298
299
|
OutboxPurgeDeadLetteredInput,
|
|
300
|
+
OutboxRenewClaimInput,
|
|
301
|
+
OutboxRenewClaimResult,
|
|
299
302
|
OutboxRequeueMessageInput,
|
|
300
303
|
} from "../outbox/index.js";
|
|
301
304
|
/**
|
package/src/ports/storage.ts
CHANGED
|
@@ -82,6 +82,13 @@ export interface StorageObjectBody extends StorageObject {
|
|
|
82
82
|
* buffering them.
|
|
83
83
|
*/
|
|
84
84
|
readonly bodyUsed: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Discard an unread body and release any resources held by its provider.
|
|
87
|
+
*
|
|
88
|
+
* Calling this after consumption has started is a no-op. Callers that only
|
|
89
|
+
* inspect object metadata should cancel the body in a `finally` block.
|
|
90
|
+
*/
|
|
91
|
+
cancel(reason?: unknown): Promise<void>;
|
|
85
92
|
/**
|
|
86
93
|
* Consume the object as a readable byte stream.
|
|
87
94
|
*/
|
|
@@ -376,6 +383,9 @@ function createObjectBody(entry: MemoryStorageEntry): StorageObjectBody {
|
|
|
376
383
|
get bodyUsed() {
|
|
377
384
|
return bodyUsed;
|
|
378
385
|
},
|
|
386
|
+
async cancel() {
|
|
387
|
+
if (!bodyUsed) bodyUsed = true;
|
|
388
|
+
},
|
|
379
389
|
stream() {
|
|
380
390
|
return bytesToStream(consumeBytes());
|
|
381
391
|
},
|
package/src/ports/testing.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { prepareEventPayloadForTransport } from "../events/index.js";
|
|
1
2
|
import type {
|
|
2
3
|
MemoryIdempotencyEntry,
|
|
3
4
|
MemoryIdempotencyStore,
|
|
@@ -81,8 +82,9 @@ export interface RecordedEventExpectation {
|
|
|
81
82
|
/**
|
|
82
83
|
* Create a recording event bus for testing.
|
|
83
84
|
*
|
|
84
|
-
* This bus
|
|
85
|
-
*
|
|
85
|
+
* This bus validates canonical transport output and records published events
|
|
86
|
+
* asynchronously for later assertion. Await `publish(...)` before reading the
|
|
87
|
+
* captured log. Subscription is not supported and throws when called.
|
|
86
88
|
*
|
|
87
89
|
* @example
|
|
88
90
|
* ```ts
|
|
@@ -104,8 +106,13 @@ export function createRecordingEventBus(): {
|
|
|
104
106
|
const events: RecordedEvent[] = [];
|
|
105
107
|
|
|
106
108
|
const bus: EventBusPort = {
|
|
107
|
-
publish(event, payload) {
|
|
108
|
-
|
|
109
|
+
async publish(event, payload, options) {
|
|
110
|
+
const prepared = await prepareEventPayloadForTransport(
|
|
111
|
+
event,
|
|
112
|
+
payload,
|
|
113
|
+
options,
|
|
114
|
+
);
|
|
115
|
+
events.push({ name: event.name, payload: prepared.payload });
|
|
109
116
|
},
|
|
110
117
|
subscribe() {
|
|
111
118
|
throw new Error("Not implemented for recording bus");
|
|
@@ -953,6 +960,19 @@ export interface OutboxDrainResultExpectation {
|
|
|
953
960
|
* Expected dead-lettered count.
|
|
954
961
|
*/
|
|
955
962
|
deadLettered?: number;
|
|
963
|
+
/**
|
|
964
|
+
* Expected count of eligible messages dead-lettered after exhausting their
|
|
965
|
+
* claim attempt budget.
|
|
966
|
+
*/
|
|
967
|
+
abandonedDeadLettered?: number;
|
|
968
|
+
/**
|
|
969
|
+
* Expected count of messages whose durable settlement remained unknown.
|
|
970
|
+
*/
|
|
971
|
+
settlementFailed?: number;
|
|
972
|
+
/**
|
|
973
|
+
* Expected count of messages whose active claim could not be confirmed.
|
|
974
|
+
*/
|
|
975
|
+
leaseLost?: number;
|
|
956
976
|
}
|
|
957
977
|
|
|
958
978
|
/**
|
|
@@ -2421,6 +2441,24 @@ function outboxDrainResultMatches(
|
|
|
2421
2441
|
) {
|
|
2422
2442
|
return false;
|
|
2423
2443
|
}
|
|
2444
|
+
if (
|
|
2445
|
+
expectation.abandonedDeadLettered !== undefined &&
|
|
2446
|
+
result.abandonedDeadLettered !== expectation.abandonedDeadLettered
|
|
2447
|
+
) {
|
|
2448
|
+
return false;
|
|
2449
|
+
}
|
|
2450
|
+
if (
|
|
2451
|
+
expectation.settlementFailed !== undefined &&
|
|
2452
|
+
result.settlementFailed !== expectation.settlementFailed
|
|
2453
|
+
) {
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
if (
|
|
2457
|
+
expectation.leaseLost !== undefined &&
|
|
2458
|
+
result.leaseLost !== expectation.leaseLost
|
|
2459
|
+
) {
|
|
2460
|
+
return false;
|
|
2461
|
+
}
|
|
2424
2462
|
|
|
2425
2463
|
return true;
|
|
2426
2464
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type EventPublishOptions,
|
|
3
|
-
|
|
3
|
+
prepareEventPayloadForTransport,
|
|
4
4
|
} from "../events/index.js";
|
|
5
|
+
import { isEventPayloadParsed } from "../events/payload-state.js";
|
|
5
6
|
import type {
|
|
6
7
|
DomainEventDef,
|
|
7
8
|
EventBusPort,
|
|
@@ -83,17 +84,18 @@ export interface RecordedDomainEvent {
|
|
|
83
84
|
/**
|
|
84
85
|
* Event definition used to validate the payload before publishing.
|
|
85
86
|
*/
|
|
86
|
-
event: DomainEventDef;
|
|
87
|
+
readonly event: DomainEventDef;
|
|
87
88
|
/**
|
|
88
89
|
* Stable event name.
|
|
89
90
|
*/
|
|
90
|
-
eventName: string;
|
|
91
|
+
readonly eventName: string;
|
|
91
92
|
/**
|
|
92
|
-
*
|
|
93
|
+
* Recorded payload. Use-case helpers store canonical transport-stable schema
|
|
94
|
+
* output; direct recorder calls are validated when the buffer is flushed.
|
|
93
95
|
*/
|
|
94
|
-
payload: unknown;
|
|
96
|
+
readonly payload: unknown;
|
|
95
97
|
/** Optional metadata propagated when the event is flushed. */
|
|
96
|
-
options?: EventPublishOptions;
|
|
98
|
+
readonly options?: EventPublishOptions;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
@@ -119,7 +121,7 @@ export interface DomainEventRecorderPort {
|
|
|
119
121
|
*/
|
|
120
122
|
export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
|
|
121
123
|
/**
|
|
122
|
-
* Return recorded events without clearing them.
|
|
124
|
+
* Return a snapshot of recorded events without clearing them.
|
|
123
125
|
*/
|
|
124
126
|
entries(): readonly RecordedDomainEvent[];
|
|
125
127
|
/**
|
|
@@ -127,7 +129,8 @@ export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
|
|
|
127
129
|
*/
|
|
128
130
|
clear(): void;
|
|
129
131
|
/**
|
|
130
|
-
*
|
|
132
|
+
* Publish recorded events in FIFO order, proving transport stability for
|
|
133
|
+
* entries recorded directly without the use-case event helper.
|
|
131
134
|
*/
|
|
132
135
|
flush(eventBus: EventBusPort): Promise<void>;
|
|
133
136
|
}
|
|
@@ -223,19 +226,30 @@ export function createObservedUnitOfWork<TxPorts>(
|
|
|
223
226
|
*/
|
|
224
227
|
export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
225
228
|
const records: RecordedDomainEvent[] = [];
|
|
229
|
+
const validationOptions = new WeakMap<
|
|
230
|
+
RecordedDomainEvent,
|
|
231
|
+
EventPublishOptions
|
|
232
|
+
>();
|
|
226
233
|
|
|
227
234
|
return {
|
|
228
235
|
record(event, payload, options) {
|
|
229
|
-
|
|
236
|
+
const record: RecordedDomainEvent = {
|
|
230
237
|
event,
|
|
231
238
|
eventName: event.name,
|
|
232
239
|
payload,
|
|
233
|
-
...(options ? { options } : {}),
|
|
234
|
-
}
|
|
240
|
+
...(options?.trace ? { options: { trace: options.trace } } : {}),
|
|
241
|
+
};
|
|
242
|
+
if (isEventPayloadParsed(event, payload, options) && options) {
|
|
243
|
+
validationOptions.set(record, { ...options });
|
|
244
|
+
}
|
|
245
|
+
records.push(record);
|
|
235
246
|
},
|
|
236
247
|
|
|
237
248
|
entries() {
|
|
238
|
-
return records
|
|
249
|
+
return records.map((record) => ({
|
|
250
|
+
...record,
|
|
251
|
+
...(record.options ? { options: { ...record.options } } : {}),
|
|
252
|
+
}));
|
|
239
253
|
},
|
|
240
254
|
|
|
241
255
|
clear() {
|
|
@@ -245,11 +259,15 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
|
245
259
|
async flush(eventBus) {
|
|
246
260
|
while (records.length > 0) {
|
|
247
261
|
const record = records[0];
|
|
248
|
-
await
|
|
262
|
+
const prepared = await prepareEventPayloadForTransport(
|
|
263
|
+
record.event,
|
|
264
|
+
record.payload,
|
|
265
|
+
validationOptions.get(record) ?? record.options,
|
|
266
|
+
);
|
|
249
267
|
await eventBus.publish(
|
|
250
268
|
record.event,
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
prepared.payload as never,
|
|
270
|
+
prepared.publishOptions,
|
|
253
271
|
);
|
|
254
272
|
records.shift();
|
|
255
273
|
}
|
package/src/providers/index.ts
CHANGED
|
@@ -126,41 +126,6 @@ export type ProviderSetupResult<
|
|
|
126
126
|
): MaybePromise<void>;
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
* Static provider metadata used by docs and app-local tooling.
|
|
131
|
-
*
|
|
132
|
-
* Metadata is descriptive. It does not change provider setup, ordering, or
|
|
133
|
-
* runtime port merging behavior.
|
|
134
|
-
*
|
|
135
|
-
* Reusable provider packages should also declare package-owned
|
|
136
|
-
* `beignet.provider` metadata in package.json so external tooling can inspect
|
|
137
|
-
* provider facts without importing runtime code.
|
|
138
|
-
*/
|
|
139
|
-
export interface ServiceProviderMetadata {
|
|
140
|
-
/**
|
|
141
|
-
* Package that exports this provider, when it comes from a reusable package.
|
|
142
|
-
*/
|
|
143
|
-
packageName?: string;
|
|
144
|
-
/**
|
|
145
|
-
* App port keys this provider contributes or replaces.
|
|
146
|
-
*/
|
|
147
|
-
ports?: readonly string[];
|
|
148
|
-
/**
|
|
149
|
-
* App port keys this provider expects previous providers or base app ports to
|
|
150
|
-
* have installed before setup runs.
|
|
151
|
-
*/
|
|
152
|
-
requires?: readonly string[];
|
|
153
|
-
/**
|
|
154
|
-
* Environment variables this provider reads directly or via config loading.
|
|
155
|
-
*/
|
|
156
|
-
env?: readonly string[];
|
|
157
|
-
/**
|
|
158
|
-
* Devtools watcher names this provider can emit through provider
|
|
159
|
-
* instrumentation.
|
|
160
|
-
*/
|
|
161
|
-
watchers?: readonly string[];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
129
|
/**
|
|
165
130
|
* A service provider that can extend or replace ports during app initialization.
|
|
166
131
|
*
|
|
@@ -205,11 +170,6 @@ export interface ServiceProvider<
|
|
|
205
170
|
*/
|
|
206
171
|
name: string;
|
|
207
172
|
|
|
208
|
-
/**
|
|
209
|
-
* Optional static metadata for docs and diagnostics.
|
|
210
|
-
*/
|
|
211
|
-
metadata?: ServiceProviderMetadata;
|
|
212
|
-
|
|
213
173
|
/**
|
|
214
174
|
* Optional configuration definition.
|
|
215
175
|
* If provided, the config will be loaded and validated before calling setup.
|
package/src/search/index.ts
CHANGED
|
@@ -351,10 +351,6 @@ export function createMemorySearchProvider(
|
|
|
351
351
|
|
|
352
352
|
return createProvider({
|
|
353
353
|
name,
|
|
354
|
-
metadata: {
|
|
355
|
-
ports: ["search"],
|
|
356
|
-
watchers: ["search"],
|
|
357
|
-
},
|
|
358
354
|
setup({ ports }) {
|
|
359
355
|
const instrumentation = createProviderInstrumentation(ports, {
|
|
360
356
|
providerName: name,
|
|
@@ -439,10 +435,10 @@ function instrumentSearch(
|
|
|
439
435
|
instrumentation.custom({
|
|
440
436
|
name: "search.query",
|
|
441
437
|
label: "Search query",
|
|
442
|
-
summary: `${index.name}: ${result.
|
|
438
|
+
summary: `${index.name}: ${result.hits.length} hits`,
|
|
443
439
|
details: {
|
|
444
440
|
index: index.name,
|
|
445
|
-
|
|
441
|
+
queryLength: result.query.length,
|
|
446
442
|
hits: result.hits.length,
|
|
447
443
|
total: result.page.total,
|
|
448
444
|
durationMs: Date.now() - startedAt,
|
package/src/server/hooks/cors.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CORS hook utilities for @beignet/core/server
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { BEIGNET_ERROR_OWNER_HEADER } from "../../contracts/types.js";
|
|
5
6
|
import type {
|
|
6
7
|
HttpRequestLike,
|
|
7
8
|
HttpResponseHeaders,
|
|
@@ -25,6 +26,11 @@ export interface CorsConfig {
|
|
|
25
26
|
* Allowed request headers.
|
|
26
27
|
*/
|
|
27
28
|
headers?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Additional response headers browser JavaScript may read. Beignet always
|
|
31
|
+
* exposes its framework error-ownership header.
|
|
32
|
+
*/
|
|
33
|
+
exposedHeaders?: string[];
|
|
28
34
|
/**
|
|
29
35
|
* Whether credentialed requests are allowed.
|
|
30
36
|
*/
|
|
@@ -35,6 +41,7 @@ const DEFAULT_CORS: Required<CorsConfig> = {
|
|
|
35
41
|
origins: "*",
|
|
36
42
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
37
43
|
headers: ["Content-Type", "Authorization"],
|
|
44
|
+
exposedHeaders: [],
|
|
38
45
|
credentials: false,
|
|
39
46
|
};
|
|
40
47
|
|
|
@@ -43,6 +50,7 @@ function resolveCorsConfig(config: CorsConfig): Required<CorsConfig> {
|
|
|
43
50
|
origins: config.origins ?? DEFAULT_CORS.origins,
|
|
44
51
|
methods: config.methods ?? DEFAULT_CORS.methods,
|
|
45
52
|
headers: config.headers ?? DEFAULT_CORS.headers,
|
|
53
|
+
exposedHeaders: config.exposedHeaders ?? DEFAULT_CORS.exposedHeaders,
|
|
46
54
|
credentials: config.credentials ?? DEFAULT_CORS.credentials,
|
|
47
55
|
};
|
|
48
56
|
|
|
@@ -71,6 +79,35 @@ function appendVaryOrigin(headers: HttpResponseHeaders): void {
|
|
|
71
79
|
headers[varyKey] = current ? `${current}, Origin` : "Origin";
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
function appendCommaSeparatedHeader(
|
|
83
|
+
headers: HttpResponseHeaders,
|
|
84
|
+
name: string,
|
|
85
|
+
appended: readonly string[],
|
|
86
|
+
): void {
|
|
87
|
+
const key =
|
|
88
|
+
Object.keys(headers).find(
|
|
89
|
+
(candidate) => candidate.toLowerCase() === name.toLowerCase(),
|
|
90
|
+
) ?? name;
|
|
91
|
+
const existing = headers[key];
|
|
92
|
+
const values = [
|
|
93
|
+
...(typeof existing === "string"
|
|
94
|
+
? existing.split(",")
|
|
95
|
+
: (existing ?? []).flatMap((value) => value.split(","))),
|
|
96
|
+
...appended,
|
|
97
|
+
]
|
|
98
|
+
.map((value) => value.trim())
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
const unique = values.filter((value) => {
|
|
102
|
+
const normalized = value.toLowerCase();
|
|
103
|
+
if (seen.has(normalized)) return false;
|
|
104
|
+
seen.add(normalized);
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
headers[key] = unique.join(", ");
|
|
109
|
+
}
|
|
110
|
+
|
|
74
111
|
/**
|
|
75
112
|
* Apply CORS response headers to a mutable header record.
|
|
76
113
|
*
|
|
@@ -86,6 +123,7 @@ export function applyCorsHeaders(
|
|
|
86
123
|
origins,
|
|
87
124
|
methods,
|
|
88
125
|
headers: allowedHeaders,
|
|
126
|
+
exposedHeaders,
|
|
89
127
|
credentials,
|
|
90
128
|
} = resolveCorsConfig(corsConfig);
|
|
91
129
|
|
|
@@ -101,6 +139,10 @@ export function applyCorsHeaders(
|
|
|
101
139
|
|
|
102
140
|
headers["Access-Control-Allow-Methods"] = methods.join(", ");
|
|
103
141
|
headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
|
|
142
|
+
appendCommaSeparatedHeader(headers, "Access-Control-Expose-Headers", [
|
|
143
|
+
BEIGNET_ERROR_OWNER_HEADER,
|
|
144
|
+
...exposedHeaders,
|
|
145
|
+
]);
|
|
104
146
|
|
|
105
147
|
if (credentials) {
|
|
106
148
|
headers["Access-Control-Allow-Credentials"] = "true";
|
|
@@ -61,8 +61,8 @@ export interface ServerInstrumentationOptions<Ctx = unknown> {
|
|
|
61
61
|
traceContextHeader?: string | false;
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Request path prefixes that should not
|
|
65
|
-
*
|
|
64
|
+
* Request path prefixes that should not record instrumentation events.
|
|
65
|
+
* Ambient correlation still runs, and enabled response headers are written.
|
|
66
66
|
*
|
|
67
67
|
* Defaults to the devtools dashboard prefix so its polling traffic does not
|
|
68
68
|
* fill the event timeline.
|
|
@@ -125,10 +125,10 @@ export interface ServerInstrumentationRuntime<Ctx> {
|
|
|
125
125
|
*/
|
|
126
126
|
createServiceCorrelation(): RequestCorrelation;
|
|
127
127
|
/**
|
|
128
|
-
* Pipeline hook installed before user hooks
|
|
129
|
-
*
|
|
128
|
+
* Pipeline hook installed before user hooks. It always owns ambient
|
|
129
|
+
* correlation; response headers and event recording remain configurable.
|
|
130
130
|
*/
|
|
131
|
-
hook
|
|
131
|
+
hook: ServerHook<Ctx, AnyPorts>;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
type TraceContextFields = {
|
|
@@ -350,7 +350,6 @@ export function createServerInstrumentation<Ctx>(
|
|
|
350
350
|
req: HttpRequestLike;
|
|
351
351
|
ctx?: unknown;
|
|
352
352
|
}) => {
|
|
353
|
-
if (isIgnoredPath(getPathname(args.req), ignorePaths)) return;
|
|
354
353
|
const trace = resolveTraceContext(args);
|
|
355
354
|
enterActiveRequestContext({
|
|
356
355
|
requestId: resolveRequestId(args),
|
|
@@ -377,7 +376,10 @@ export function createServerInstrumentation<Ctx>(
|
|
|
377
376
|
return undefined;
|
|
378
377
|
},
|
|
379
378
|
beforeSend: ({ req, ctx, response }) => {
|
|
380
|
-
if (
|
|
379
|
+
if (
|
|
380
|
+
!enabled ||
|
|
381
|
+
(requestIdHeader === false && traceContextHeader === false)
|
|
382
|
+
) {
|
|
381
383
|
return undefined;
|
|
382
384
|
}
|
|
383
385
|
|
|
@@ -502,6 +504,6 @@ export function createServerInstrumentation<Ctx>(
|
|
|
502
504
|
requestId: createRequestId(),
|
|
503
505
|
trace: tracing?.current() ?? createTraceContext(),
|
|
504
506
|
}),
|
|
505
|
-
hook
|
|
507
|
+
hook,
|
|
506
508
|
};
|
|
507
509
|
}
|
|
@@ -312,18 +312,18 @@ function getDeclaredCatalogErrorsForStatus(
|
|
|
312
312
|
);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
async function
|
|
315
|
+
async function parseCatalogErrorResponse<C extends HttpContractConfig>(
|
|
316
316
|
contract: C,
|
|
317
317
|
res: HttpResponseLike,
|
|
318
|
-
): Promise<
|
|
318
|
+
): Promise<HttpResponseLike> {
|
|
319
319
|
const body = res.body;
|
|
320
|
-
if (res.status < 400 || !isErrorResponseBody(body)) return;
|
|
320
|
+
if (res.status < 400 || !isErrorResponseBody(body)) return res;
|
|
321
321
|
|
|
322
322
|
const declaredErrors = getDeclaredCatalogErrorsForStatus(
|
|
323
323
|
contract,
|
|
324
324
|
res.status,
|
|
325
325
|
);
|
|
326
|
-
if (declaredErrors.length === 0) return;
|
|
326
|
+
if (declaredErrors.length === 0) return res;
|
|
327
327
|
|
|
328
328
|
const matchingError = declaredErrors.find(
|
|
329
329
|
(error) => error.code === body.code,
|
|
@@ -346,7 +346,18 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
346
346
|
|
|
347
347
|
if (matchingError.details && body.details !== undefined) {
|
|
348
348
|
try {
|
|
349
|
-
await parseStandardSchema(
|
|
349
|
+
const parsedDetails = await parseStandardSchema(
|
|
350
|
+
matchingError.details,
|
|
351
|
+
body.details,
|
|
352
|
+
);
|
|
353
|
+
const { details: _details, ...bodyWithoutDetails } = body;
|
|
354
|
+
return {
|
|
355
|
+
...res,
|
|
356
|
+
body:
|
|
357
|
+
parsedDetails === undefined
|
|
358
|
+
? bodyWithoutDetails
|
|
359
|
+
: { ...bodyWithoutDetails, details: parsedDetails },
|
|
360
|
+
};
|
|
350
361
|
} catch (error) {
|
|
351
362
|
if (error instanceof SchemaValidationError) {
|
|
352
363
|
throw new ResponseContractViolationError({
|
|
@@ -360,18 +371,20 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
360
371
|
throw error;
|
|
361
372
|
}
|
|
362
373
|
}
|
|
374
|
+
|
|
375
|
+
return res;
|
|
363
376
|
}
|
|
364
377
|
|
|
365
|
-
async function
|
|
378
|
+
async function parseResponseAgainstContract<C extends HttpContractConfig>(
|
|
366
379
|
contract: C,
|
|
367
380
|
res: HttpResponseLike,
|
|
368
381
|
responseValidationExemptStatus?: number,
|
|
369
|
-
): Promise<
|
|
382
|
+
): Promise<HttpResponseLike> {
|
|
370
383
|
const statusKey = String(res.status);
|
|
371
384
|
const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
|
|
372
385
|
|
|
373
386
|
if (!hasDeclaredStatus) {
|
|
374
|
-
if (Object.keys(contract.responses).length === 0) return;
|
|
387
|
+
if (Object.keys(contract.responses).length === 0) return res;
|
|
375
388
|
|
|
376
389
|
throw new ResponseContractViolationError({
|
|
377
390
|
code: "UNDECLARED_RESPONSE_STATUS",
|
|
@@ -400,19 +413,22 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
400
413
|
}),
|
|
401
414
|
});
|
|
402
415
|
}
|
|
403
|
-
return;
|
|
416
|
+
return res;
|
|
404
417
|
}
|
|
405
418
|
|
|
406
|
-
if (!responseSchema) return;
|
|
419
|
+
if (!responseSchema) return res;
|
|
407
420
|
|
|
408
421
|
// Binder routes whose use case output schema is the same object as the
|
|
409
422
|
// declared success response schema skip the redundant success-status parse.
|
|
410
423
|
// Error statuses and undeclared statuses are validated unchanged.
|
|
411
|
-
if (res.status === responseValidationExemptStatus) return;
|
|
424
|
+
if (res.status === responseValidationExemptStatus) return res;
|
|
412
425
|
|
|
413
426
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
427
|
+
const parsed = {
|
|
428
|
+
...res,
|
|
429
|
+
body: await parseStandardSchema(responseSchema, res.body),
|
|
430
|
+
};
|
|
431
|
+
return await parseCatalogErrorResponse(contract, parsed);
|
|
416
432
|
} catch (error) {
|
|
417
433
|
if (error instanceof SchemaValidationError) {
|
|
418
434
|
throw new ResponseContractViolationError({
|
|
@@ -463,7 +479,7 @@ export async function finalizeResponse<C extends HttpContractConfig>(
|
|
|
463
479
|
const normalized = normalizeResponse(res);
|
|
464
480
|
validateHttpResponseSemantics(contract, normalized);
|
|
465
481
|
if (options.validateContract ?? true) {
|
|
466
|
-
|
|
482
|
+
return parseResponseAgainstContract(
|
|
467
483
|
contract,
|
|
468
484
|
normalized,
|
|
469
485
|
responseValidationExemptStatus,
|
|
@@ -490,6 +506,8 @@ export function defaultErrorResponse(
|
|
|
490
506
|
ctx?: unknown,
|
|
491
507
|
): HttpResponseLike {
|
|
492
508
|
const requestId = getRequestIdFromContext(ctx);
|
|
509
|
+
const exposeErrorDetails =
|
|
510
|
+
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
493
511
|
return {
|
|
494
512
|
status: 500,
|
|
495
513
|
body: createErrorResponseBody({
|
|
@@ -497,7 +515,7 @@ export function defaultErrorResponse(
|
|
|
497
515
|
message: "Internal server error",
|
|
498
516
|
requestId,
|
|
499
517
|
details:
|
|
500
|
-
|
|
518
|
+
exposeErrorDetails && err instanceof Error
|
|
501
519
|
? {
|
|
502
520
|
error: {
|
|
503
521
|
message: err.message,
|