@beignet/core 0.0.49 → 0.0.51
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 +30 -0
- package/README.md +213 -32
- package/dist/application/index.d.ts +5 -5
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +5 -4
- package/dist/application/index.js.map +1 -1
- package/dist/events/index.d.ts +54 -5
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +188 -34
- package/dist/events/index.js.map +1 -1
- package/dist/events/payload-state.d.ts +4 -0
- package/dist/events/payload-state.d.ts.map +1 -0
- package/dist/events/payload-state.js +11 -0
- package/dist/events/payload-state.js.map +1 -0
- package/dist/idempotency/index.d.ts +7 -3
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +45 -12
- package/dist/idempotency/index.js.map +1 -1
- 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/mail/index.d.ts.map +1 -1
- package/dist/mail/index.js +6 -3
- package/dist/mail/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 -152
- 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/best-effort-work.d.ts +21 -0
- package/dist/ports/best-effort-work.d.ts.map +1 -0
- package/dist/ports/best-effort-work.js +2 -0
- package/dist/ports/best-effort-work.js.map +1 -0
- package/dist/ports/events.d.ts +7 -5
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +7 -2
- 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/ports/testing.d.ts +28 -0
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +50 -0
- 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 -6
- 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 +8 -43
- 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 +0 -4
- package/dist/search/index.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +7 -2
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/cors.js +31 -2
- package/dist/server/hooks/cors.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +2 -2
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/security.d.ts +2 -2
- package/dist/server/hooks/security.d.ts.map +1 -1
- package/dist/server/hooks/security.js.map +1 -1
- package/dist/server/http.d.ts +21 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +5 -3
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +9 -0
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/request-preparation.d.ts.map +1 -1
- package/dist/server/request-preparation.js +20 -2
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/response-finalization.d.ts +2 -2
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +25 -8
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/route-matching.d.ts.map +1 -1
- package/dist/server/route-matching.js +12 -1
- package/dist/server/route-matching.js.map +1 -1
- package/dist/server/server-sent-events.d.ts +94 -0
- package/dist/server/server-sent-events.d.ts.map +1 -0
- package/dist/server/server-sent-events.js +275 -0
- package/dist/server/server-sent-events.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +75 -31
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy-internal.d.ts +4 -0
- package/dist/server/trusted-proxy-internal.d.ts.map +1 -1
- package/dist/server/trusted-proxy-internal.js +20 -0
- package/dist/server/trusted-proxy-internal.js.map +1 -1
- package/dist/server/trusted-proxy.d.ts.map +1 -1
- package/dist/server/trusted-proxy.js +3 -8
- package/dist/server/trusted-proxy.js.map +1 -1
- package/dist/testing/index.d.ts +17 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +18 -7
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/app-architecture/SKILL.md +37 -3
- package/src/application/index.ts +18 -7
- package/src/events/index.ts +268 -40
- package/src/events/payload-state.ts +24 -0
- package/src/idempotency/index.ts +65 -17
- package/src/locks/index.ts +0 -4
- package/src/mail/index.ts +7 -3
- package/src/outbox/index.ts +1382 -175
- package/src/payments/index.ts +0 -4
- package/src/ports/best-effort-work.ts +21 -0
- package/src/ports/events.ts +9 -4
- package/src/ports/index.ts +12 -0
- package/src/ports/testing.ts +76 -0
- package/src/ports/unit-of-work.ts +28 -14
- package/src/providers/index.ts +0 -1
- package/src/providers/provider.ts +8 -45
- package/src/search/index.ts +0 -4
- package/src/server/hooks/cors.ts +53 -5
- package/src/server/hooks/logging.ts +6 -2
- package/src/server/hooks/security.ts +8 -4
- package/src/server/http.ts +23 -2
- package/src/server/index.ts +4 -0
- package/src/server/instrumentation.ts +12 -4
- package/src/server/request-executor.ts +14 -0
- package/src/server/request-preparation.ts +23 -3
- package/src/server/response-finalization.ts +51 -15
- package/src/server/route-matching.ts +24 -1
- package/src/server/server-sent-events.ts +415 -0
- package/src/server/server.ts +98 -41
- package/src/server/trusted-proxy-internal.ts +20 -0
- package/src/server/trusted-proxy.ts +6 -7
- package/src/testing/index.ts +50 -9
- package/dist/query-codec.d.ts +0 -27
- package/dist/query-codec.d.ts.map +0 -1
- package/dist/query-codec.js +0 -245
- package/dist/query-codec.js.map +0 -1
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,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-durable work scheduled outside the originating application operation.
|
|
3
|
+
*/
|
|
4
|
+
export type BestEffortWork = () => Promise<void> | void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Non-durable, best-effort follow-up work.
|
|
8
|
+
*
|
|
9
|
+
* Use this for follow-up work that may be lost without changing the outcome
|
|
10
|
+
* of the originating operation. Required or retryable work belongs in a job
|
|
11
|
+
* or durable outbox instead.
|
|
12
|
+
*/
|
|
13
|
+
export interface BestEffortWorkPort {
|
|
14
|
+
/**
|
|
15
|
+
* Schedule work without waiting for it in the originating operation.
|
|
16
|
+
*
|
|
17
|
+
* Adapters should isolate scheduling and callback failures from the caller
|
|
18
|
+
* and report them through their configured error observer.
|
|
19
|
+
*/
|
|
20
|
+
defer(work: BestEffortWork): void;
|
|
21
|
+
}
|
package/src/ports/events.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EventPayloadDef,
|
|
3
3
|
EventPublishOptions,
|
|
4
|
+
EventSubscription,
|
|
4
5
|
InferEventPayload as InferContractEventPayload,
|
|
5
6
|
StandardSchema,
|
|
6
7
|
} from "../events/index.js";
|
|
8
|
+
|
|
9
|
+
export type { EventSubscription } from "../events/index.js";
|
|
10
|
+
|
|
7
11
|
import type {
|
|
8
12
|
JobDef as ContractJobDef,
|
|
9
13
|
InferJobPayload as InferContractJobPayload,
|
|
@@ -58,15 +62,16 @@ export type InferJobPayload<J extends JobDef> = InferContractJobPayload<J>;
|
|
|
58
62
|
* const eventBus = createMemoryEventBus();
|
|
59
63
|
*
|
|
60
64
|
* // Subscribe to an event
|
|
61
|
-
* const
|
|
65
|
+
* const subscription = eventBus.subscribe(UserRegistered, (payload) => {
|
|
62
66
|
* console.log(`User registered: ${payload.email}`);
|
|
63
67
|
* });
|
|
68
|
+
* await subscription.ready;
|
|
64
69
|
*
|
|
65
70
|
* // Publish an event
|
|
66
71
|
* await eventBus.publish(UserRegistered, { userId: "123", email: "test@example.com" });
|
|
67
72
|
*
|
|
68
73
|
* // Unsubscribe when done
|
|
69
|
-
* unsubscribe();
|
|
74
|
+
* await subscription.unsubscribe();
|
|
70
75
|
* ```
|
|
71
76
|
*/
|
|
72
77
|
export interface EventBusPort {
|
|
@@ -80,7 +85,7 @@ export interface EventBusPort {
|
|
|
80
85
|
): Promise<void> | void;
|
|
81
86
|
|
|
82
87
|
/**
|
|
83
|
-
* Subscribe to a domain event. Returns
|
|
88
|
+
* Subscribe to a domain event. Returns initial readiness and cleanup.
|
|
84
89
|
*/
|
|
85
90
|
subscribe<E extends DomainEventDef>(
|
|
86
91
|
event: E,
|
|
@@ -88,7 +93,7 @@ export interface EventBusPort {
|
|
|
88
93
|
payload: InferEventPayload<E>,
|
|
89
94
|
options?: EventPublishOptions,
|
|
90
95
|
) => Promise<void> | void,
|
|
91
|
-
):
|
|
96
|
+
): EventSubscription;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
/**
|
package/src/ports/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - `definePorts` – helper to define a typed ports object
|
|
9
9
|
* - `PortsContext` – a generic type that describes ctx objects that carry ports
|
|
10
10
|
* - `EventBusPort` – interface for event bus implementations
|
|
11
|
+
* - `BestEffortWorkPort` – interface for non-durable follow-up work
|
|
11
12
|
* - `JobDispatcherPort` – interface for job dispatch implementations
|
|
12
13
|
* - `UnitOfWorkPort` – interface for app-owned transaction boundaries
|
|
13
14
|
* - `OutboxPort` – interface for durable event/job delivery storage
|
|
@@ -282,6 +283,7 @@ export type {
|
|
|
282
283
|
ClaimedOutboxMessage,
|
|
283
284
|
OutboxAdminPort,
|
|
284
285
|
OutboxClaimBatchOptions,
|
|
286
|
+
OutboxClaimBatchResult,
|
|
285
287
|
OutboxCountMessagesOptions,
|
|
286
288
|
OutboxDeleteResult,
|
|
287
289
|
OutboxEnqueueInput,
|
|
@@ -295,6 +297,8 @@ export type {
|
|
|
295
297
|
OutboxPort,
|
|
296
298
|
OutboxPruneDeliveredInput,
|
|
297
299
|
OutboxPurgeDeadLetteredInput,
|
|
300
|
+
OutboxRenewClaimInput,
|
|
301
|
+
OutboxRenewClaimResult,
|
|
298
302
|
OutboxRequeueMessageInput,
|
|
299
303
|
} from "../outbox/index.js";
|
|
300
304
|
/**
|
|
@@ -468,6 +472,13 @@ export {
|
|
|
468
472
|
requireUserId,
|
|
469
473
|
TenantRequiredError,
|
|
470
474
|
} from "./auth.js";
|
|
475
|
+
/**
|
|
476
|
+
* Best-effort work port exports.
|
|
477
|
+
*/
|
|
478
|
+
export type {
|
|
479
|
+
BestEffortWork,
|
|
480
|
+
BestEffortWorkPort,
|
|
481
|
+
} from "./best-effort-work.js";
|
|
471
482
|
/**
|
|
472
483
|
* Ports builder exports.
|
|
473
484
|
*/
|
|
@@ -498,6 +509,7 @@ export { createFrozenClock, createSystemClock } from "./clock.js";
|
|
|
498
509
|
export type {
|
|
499
510
|
DomainEventDef,
|
|
500
511
|
EventBusPort,
|
|
512
|
+
EventSubscription,
|
|
501
513
|
InferEventPayload,
|
|
502
514
|
InferJobPayload,
|
|
503
515
|
JobDef,
|
package/src/ports/testing.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
createTenant,
|
|
39
39
|
createUserActor,
|
|
40
40
|
} from "./audit.js";
|
|
41
|
+
import type { BestEffortWork, BestEffortWorkPort } from "./best-effort-work.js";
|
|
41
42
|
import type { EventBusPort, JobDef, JobDispatcherPort } from "./events.js";
|
|
42
43
|
import {
|
|
43
44
|
type CreateGateOptions,
|
|
@@ -114,6 +115,50 @@ export function createRecordingEventBus(): {
|
|
|
114
115
|
return { bus, events };
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Recording best-effort-work adapter for deterministic tests.
|
|
120
|
+
*
|
|
121
|
+
* Calls to `defer(...)` append work without running it. `flush()` runs the
|
|
122
|
+
* currently pending batch in FIFO order, then rejects with the callback error
|
|
123
|
+
* or an `AggregateError` after every callback in that batch has been attempted.
|
|
124
|
+
* Work deferred by a running callback remains pending for the next flush so
|
|
125
|
+
* one flush stays bounded.
|
|
126
|
+
*/
|
|
127
|
+
export function createRecordingBestEffortWork(): {
|
|
128
|
+
bestEffortWork: BestEffortWorkPort;
|
|
129
|
+
pending: BestEffortWork[];
|
|
130
|
+
flush(): Promise<void>;
|
|
131
|
+
} {
|
|
132
|
+
const pending: BestEffortWork[] = [];
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
bestEffortWork: {
|
|
136
|
+
defer(work) {
|
|
137
|
+
pending.push(work);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
pending,
|
|
141
|
+
async flush() {
|
|
142
|
+
const batch = pending.splice(0);
|
|
143
|
+
const errors: unknown[] = [];
|
|
144
|
+
for (const work of batch) {
|
|
145
|
+
try {
|
|
146
|
+
await work();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
errors.push(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (errors.length === 1) {
|
|
153
|
+
throw errors[0];
|
|
154
|
+
}
|
|
155
|
+
if (errors.length > 1) {
|
|
156
|
+
throw new AggregateError(errors, "Best-effort work batch failed.");
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
117
162
|
/**
|
|
118
163
|
* A job dispatch captured by `createRecordingJobDispatcher(...)`.
|
|
119
164
|
*/
|
|
@@ -908,6 +953,19 @@ export interface OutboxDrainResultExpectation {
|
|
|
908
953
|
* Expected dead-lettered count.
|
|
909
954
|
*/
|
|
910
955
|
deadLettered?: number;
|
|
956
|
+
/**
|
|
957
|
+
* Expected count of eligible messages dead-lettered after exhausting their
|
|
958
|
+
* claim attempt budget.
|
|
959
|
+
*/
|
|
960
|
+
abandonedDeadLettered?: number;
|
|
961
|
+
/**
|
|
962
|
+
* Expected count of messages whose durable settlement remained unknown.
|
|
963
|
+
*/
|
|
964
|
+
settlementFailed?: number;
|
|
965
|
+
/**
|
|
966
|
+
* Expected count of messages whose active claim could not be confirmed.
|
|
967
|
+
*/
|
|
968
|
+
leaseLost?: number;
|
|
911
969
|
}
|
|
912
970
|
|
|
913
971
|
/**
|
|
@@ -2376,6 +2434,24 @@ function outboxDrainResultMatches(
|
|
|
2376
2434
|
) {
|
|
2377
2435
|
return false;
|
|
2378
2436
|
}
|
|
2437
|
+
if (
|
|
2438
|
+
expectation.abandonedDeadLettered !== undefined &&
|
|
2439
|
+
result.abandonedDeadLettered !== expectation.abandonedDeadLettered
|
|
2440
|
+
) {
|
|
2441
|
+
return false;
|
|
2442
|
+
}
|
|
2443
|
+
if (
|
|
2444
|
+
expectation.settlementFailed !== undefined &&
|
|
2445
|
+
result.settlementFailed !== expectation.settlementFailed
|
|
2446
|
+
) {
|
|
2447
|
+
return false;
|
|
2448
|
+
}
|
|
2449
|
+
if (
|
|
2450
|
+
expectation.leaseLost !== undefined &&
|
|
2451
|
+
result.leaseLost !== expectation.leaseLost
|
|
2452
|
+
) {
|
|
2453
|
+
return false;
|
|
2454
|
+
}
|
|
2379
2455
|
|
|
2380
2456
|
return true;
|
|
2381
2457
|
}
|
|
@@ -2,6 +2,10 @@ import {
|
|
|
2
2
|
type EventPublishOptions,
|
|
3
3
|
parseEventPayload,
|
|
4
4
|
} from "../events/index.js";
|
|
5
|
+
import {
|
|
6
|
+
isEventPayloadParsed,
|
|
7
|
+
markEventPayloadParsed,
|
|
8
|
+
} from "../events/payload-state.js";
|
|
5
9
|
import type {
|
|
6
10
|
DomainEventDef,
|
|
7
11
|
EventBusPort,
|
|
@@ -83,17 +87,18 @@ export interface RecordedDomainEvent {
|
|
|
83
87
|
/**
|
|
84
88
|
* Event definition used to validate the payload before publishing.
|
|
85
89
|
*/
|
|
86
|
-
event: DomainEventDef;
|
|
90
|
+
readonly event: DomainEventDef;
|
|
87
91
|
/**
|
|
88
92
|
* Stable event name.
|
|
89
93
|
*/
|
|
90
|
-
eventName: string;
|
|
94
|
+
readonly eventName: string;
|
|
91
95
|
/**
|
|
92
|
-
*
|
|
96
|
+
* Recorded payload. Use-case helpers store parsed schema output; direct
|
|
97
|
+
* recorder calls are validated when the buffer is flushed.
|
|
93
98
|
*/
|
|
94
|
-
payload: unknown;
|
|
99
|
+
readonly payload: unknown;
|
|
95
100
|
/** Optional metadata propagated when the event is flushed. */
|
|
96
|
-
options?: EventPublishOptions;
|
|
101
|
+
readonly options?: EventPublishOptions;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/**
|
|
@@ -119,7 +124,7 @@ export interface DomainEventRecorderPort {
|
|
|
119
124
|
*/
|
|
120
125
|
export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
|
|
121
126
|
/**
|
|
122
|
-
* Return recorded events without clearing them.
|
|
127
|
+
* Return a snapshot of recorded events without clearing them.
|
|
123
128
|
*/
|
|
124
129
|
entries(): readonly RecordedDomainEvent[];
|
|
125
130
|
/**
|
|
@@ -127,7 +132,8 @@ export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
|
|
|
127
132
|
*/
|
|
128
133
|
clear(): void;
|
|
129
134
|
/**
|
|
130
|
-
*
|
|
135
|
+
* Publish recorded events in FIFO order, validating entries recorded
|
|
136
|
+
* directly without the use-case event helper.
|
|
131
137
|
*/
|
|
132
138
|
flush(eventBus: EventBusPort): Promise<void>;
|
|
133
139
|
}
|
|
@@ -223,19 +229,25 @@ export function createObservedUnitOfWork<TxPorts>(
|
|
|
223
229
|
*/
|
|
224
230
|
export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
225
231
|
const records: RecordedDomainEvent[] = [];
|
|
232
|
+
const parsedRecords = new WeakSet<RecordedDomainEvent>();
|
|
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(options)) parsedRecords.add(record);
|
|
243
|
+
records.push(record);
|
|
235
244
|
},
|
|
236
245
|
|
|
237
246
|
entries() {
|
|
238
|
-
return records
|
|
247
|
+
return records.map((record) => ({
|
|
248
|
+
...record,
|
|
249
|
+
...(record.options ? { options: { ...record.options } } : {}),
|
|
250
|
+
}));
|
|
239
251
|
},
|
|
240
252
|
|
|
241
253
|
clear() {
|
|
@@ -245,11 +257,13 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
|
|
|
245
257
|
async flush(eventBus) {
|
|
246
258
|
while (records.length > 0) {
|
|
247
259
|
const record = records[0];
|
|
248
|
-
|
|
260
|
+
const payload = parsedRecords.has(record)
|
|
261
|
+
? record.payload
|
|
262
|
+
: await parseEventPayload(record.event, record.payload);
|
|
249
263
|
await eventBus.publish(
|
|
250
264
|
record.event,
|
|
251
|
-
|
|
252
|
-
record.options,
|
|
265
|
+
payload as never,
|
|
266
|
+
markEventPayloadParsed(record.options),
|
|
253
267
|
);
|
|
254
268
|
records.shift();
|
|
255
269
|
}
|
package/src/providers/index.ts
CHANGED
|
@@ -55,9 +55,11 @@ export type MaybePromise<T> = T | Promise<T>;
|
|
|
55
55
|
/**
|
|
56
56
|
* Late-bound service context factory exposed to providers.
|
|
57
57
|
*
|
|
58
|
-
* Calling it
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Calling it during provider setup or after provider shutdown throws. Start
|
|
59
|
+
* and stop hooks may invoke it after every provider has contributed ports and
|
|
60
|
+
* the server has verified that no deferred port remains unbound. Runtime
|
|
61
|
+
* entrypoints such as job dispatch, listeners, or scheduled work can close
|
|
62
|
+
* over it for later use.
|
|
61
63
|
*
|
|
62
64
|
* App-local providers can type the factory by declaring `Context` and
|
|
63
65
|
* `ServiceInput` through the curried `createProvider<Requires, Context,
|
|
@@ -124,41 +126,6 @@ export type ProviderSetupResult<
|
|
|
124
126
|
): MaybePromise<void>;
|
|
125
127
|
};
|
|
126
128
|
|
|
127
|
-
/**
|
|
128
|
-
* Static provider metadata used by docs and app-local tooling.
|
|
129
|
-
*
|
|
130
|
-
* Metadata is descriptive. It does not change provider setup, ordering, or
|
|
131
|
-
* runtime port merging behavior.
|
|
132
|
-
*
|
|
133
|
-
* Reusable provider packages should also declare package-owned
|
|
134
|
-
* `beignet.provider` metadata in package.json so external tooling can inspect
|
|
135
|
-
* provider facts without importing runtime code.
|
|
136
|
-
*/
|
|
137
|
-
export interface ServiceProviderMetadata {
|
|
138
|
-
/**
|
|
139
|
-
* Package that exports this provider, when it comes from a reusable package.
|
|
140
|
-
*/
|
|
141
|
-
packageName?: string;
|
|
142
|
-
/**
|
|
143
|
-
* App port keys this provider contributes or replaces.
|
|
144
|
-
*/
|
|
145
|
-
ports?: readonly string[];
|
|
146
|
-
/**
|
|
147
|
-
* App port keys this provider expects previous providers or base app ports to
|
|
148
|
-
* have installed before setup runs.
|
|
149
|
-
*/
|
|
150
|
-
requires?: readonly string[];
|
|
151
|
-
/**
|
|
152
|
-
* Environment variables this provider reads directly or via config loading.
|
|
153
|
-
*/
|
|
154
|
-
env?: readonly string[];
|
|
155
|
-
/**
|
|
156
|
-
* Devtools watcher names this provider can emit through provider
|
|
157
|
-
* instrumentation.
|
|
158
|
-
*/
|
|
159
|
-
watchers?: readonly string[];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
129
|
/**
|
|
163
130
|
* A service provider that can extend or replace ports during app initialization.
|
|
164
131
|
*
|
|
@@ -203,11 +170,6 @@ export interface ServiceProvider<
|
|
|
203
170
|
*/
|
|
204
171
|
name: string;
|
|
205
172
|
|
|
206
|
-
/**
|
|
207
|
-
* Optional static metadata for docs and diagnostics.
|
|
208
|
-
*/
|
|
209
|
-
metadata?: ServiceProviderMetadata;
|
|
210
|
-
|
|
211
173
|
/**
|
|
212
174
|
* Optional configuration definition.
|
|
213
175
|
* If provided, the config will be loaded and validated before calling setup.
|
|
@@ -222,8 +184,9 @@ export interface ServiceProvider<
|
|
|
222
184
|
* @param ctx.ports - Ports contributed by previous providers
|
|
223
185
|
* @param ctx.config - Validated config (if config was defined), or undefined
|
|
224
186
|
* @param ctx.createServiceContext - Late-bound service context factory.
|
|
225
|
-
* Throws
|
|
226
|
-
* entrypoints such as job
|
|
187
|
+
* Throws during setup. Start and stop hooks may invoke it after all provider
|
|
188
|
+
* ports have been contributed and validated; runtime entrypoints such as job
|
|
189
|
+
* dispatchers and event listeners may call it lazily afterward.
|
|
227
190
|
*/
|
|
228
191
|
setup(ctx: {
|
|
229
192
|
ports: Readonly<Ports>;
|
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,
|
package/src/server/hooks/cors.ts
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* CORS hook utilities for @beignet/core/server
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { BEIGNET_ERROR_OWNER_HEADER } from "../../contracts/types.js";
|
|
6
|
+
import type {
|
|
7
|
+
HttpRequestLike,
|
|
8
|
+
HttpResponseHeaders,
|
|
9
|
+
ServerHook,
|
|
10
|
+
} from "../types.js";
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* CORS configuration for `createCorsHooks(...)`.
|
|
@@ -21,6 +26,11 @@ export interface CorsConfig {
|
|
|
21
26
|
* Allowed request headers.
|
|
22
27
|
*/
|
|
23
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[];
|
|
24
34
|
/**
|
|
25
35
|
* Whether credentialed requests are allowed.
|
|
26
36
|
*/
|
|
@@ -31,6 +41,7 @@ const DEFAULT_CORS: Required<CorsConfig> = {
|
|
|
31
41
|
origins: "*",
|
|
32
42
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
33
43
|
headers: ["Content-Type", "Authorization"],
|
|
44
|
+
exposedHeaders: [],
|
|
34
45
|
credentials: false,
|
|
35
46
|
};
|
|
36
47
|
|
|
@@ -39,6 +50,7 @@ function resolveCorsConfig(config: CorsConfig): Required<CorsConfig> {
|
|
|
39
50
|
origins: config.origins ?? DEFAULT_CORS.origins,
|
|
40
51
|
methods: config.methods ?? DEFAULT_CORS.methods,
|
|
41
52
|
headers: config.headers ?? DEFAULT_CORS.headers,
|
|
53
|
+
exposedHeaders: config.exposedHeaders ?? DEFAULT_CORS.exposedHeaders,
|
|
42
54
|
credentials: config.credentials ?? DEFAULT_CORS.credentials,
|
|
43
55
|
};
|
|
44
56
|
|
|
@@ -55,16 +67,47 @@ function resolveCorsConfig(config: CorsConfig): Required<CorsConfig> {
|
|
|
55
67
|
return resolved;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
function appendVaryOrigin(headers:
|
|
70
|
+
function appendVaryOrigin(headers: HttpResponseHeaders): void {
|
|
59
71
|
const varyKey =
|
|
60
72
|
Object.keys(headers).find((key) => key.toLowerCase() === "vary") ?? "Vary";
|
|
61
|
-
const
|
|
73
|
+
const currentValue = headers[varyKey];
|
|
74
|
+
const current =
|
|
75
|
+
typeof currentValue === "string" ? currentValue : currentValue?.join(", ");
|
|
62
76
|
const values = current?.split(",").map((value) => value.trim().toLowerCase());
|
|
63
77
|
if (values?.includes("origin")) return;
|
|
64
78
|
|
|
65
79
|
headers[varyKey] = current ? `${current}, Origin` : "Origin";
|
|
66
80
|
}
|
|
67
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
|
+
|
|
68
111
|
/**
|
|
69
112
|
* Apply CORS response headers to a mutable header record.
|
|
70
113
|
*
|
|
@@ -72,7 +115,7 @@ function appendVaryOrigin(headers: Record<string, string>): void {
|
|
|
72
115
|
* when cookies or authorization headers are allowed cross-origin.
|
|
73
116
|
*/
|
|
74
117
|
export function applyCorsHeaders(
|
|
75
|
-
headers:
|
|
118
|
+
headers: HttpResponseHeaders,
|
|
76
119
|
req: HttpRequestLike,
|
|
77
120
|
corsConfig: CorsConfig,
|
|
78
121
|
): void {
|
|
@@ -80,6 +123,7 @@ export function applyCorsHeaders(
|
|
|
80
123
|
origins,
|
|
81
124
|
methods,
|
|
82
125
|
headers: allowedHeaders,
|
|
126
|
+
exposedHeaders,
|
|
83
127
|
credentials,
|
|
84
128
|
} = resolveCorsConfig(corsConfig);
|
|
85
129
|
|
|
@@ -95,6 +139,10 @@ export function applyCorsHeaders(
|
|
|
95
139
|
|
|
96
140
|
headers["Access-Control-Allow-Methods"] = methods.join(", ");
|
|
97
141
|
headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
|
|
142
|
+
appendCommaSeparatedHeader(headers, "Access-Control-Expose-Headers", [
|
|
143
|
+
BEIGNET_ERROR_OWNER_HEADER,
|
|
144
|
+
...exposedHeaders,
|
|
145
|
+
]);
|
|
98
146
|
|
|
99
147
|
if (credentials) {
|
|
100
148
|
headers["Access-Control-Allow-Credentials"] = "true";
|
|
@@ -121,7 +169,7 @@ export function createCorsHooks<Ctx>(config: CorsConfig): ServerHook<Ctx> {
|
|
|
121
169
|
) {
|
|
122
170
|
return undefined;
|
|
123
171
|
}
|
|
124
|
-
const headers:
|
|
172
|
+
const headers: HttpResponseHeaders = {};
|
|
125
173
|
applyCorsHeaders(headers, req, corsConfig);
|
|
126
174
|
return {
|
|
127
175
|
status: 204,
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
resolveTrustedRequest,
|
|
8
8
|
type TrustedRequestInfo,
|
|
9
9
|
} from "../trusted-proxy.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
HttpRequestLike,
|
|
12
|
+
HttpResponseHeaders,
|
|
13
|
+
ServerHook,
|
|
14
|
+
} from "../types.js";
|
|
11
15
|
import { getRequestIdFromContext } from "./utils.js";
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -60,7 +64,7 @@ export interface LoggingConfig<Ctx> {
|
|
|
60
64
|
ctx?: Ctx;
|
|
61
65
|
req: HttpRequestLike;
|
|
62
66
|
requestInfo: TrustedRequestInfo;
|
|
63
|
-
res: { status: number; headers:
|
|
67
|
+
res: { status: number; headers: HttpResponseHeaders };
|
|
64
68
|
durationMs: number;
|
|
65
69
|
contract?: HttpContractConfig;
|
|
66
70
|
error?: unknown;
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
type TrustedProxyConfig,
|
|
10
10
|
type TrustedProxyOptions,
|
|
11
11
|
} from "../trusted-proxy.js";
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
HttpRequestLike,
|
|
14
|
+
HttpResponseHeaders,
|
|
15
|
+
ServerHook,
|
|
16
|
+
} from "../types.js";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Strict-Transport-Security configuration.
|
|
@@ -180,7 +184,7 @@ const DEFAULT_CSRF_HEADER = "x-csrf-token";
|
|
|
180
184
|
const DEFAULT_CSRF_COOKIE = "beignet.csrf";
|
|
181
185
|
|
|
182
186
|
function headerKey(
|
|
183
|
-
headers:
|
|
187
|
+
headers: HttpResponseHeaders,
|
|
184
188
|
name: string,
|
|
185
189
|
): string | undefined {
|
|
186
190
|
const lowerName = name.toLowerCase();
|
|
@@ -188,7 +192,7 @@ function headerKey(
|
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
function setHeaderIfMissing(
|
|
191
|
-
headers:
|
|
195
|
+
headers: HttpResponseHeaders,
|
|
192
196
|
name: string,
|
|
193
197
|
value: string | false | undefined,
|
|
194
198
|
): void {
|
|
@@ -217,7 +221,7 @@ function formatStrictTransportSecurity(
|
|
|
217
221
|
* provide more specific policies.
|
|
218
222
|
*/
|
|
219
223
|
export function applySecurityHeaders(
|
|
220
|
-
headers:
|
|
224
|
+
headers: HttpResponseHeaders,
|
|
221
225
|
options: SecurityHeadersOptions = {},
|
|
222
226
|
): void {
|
|
223
227
|
setHeaderIfMissing(
|
package/src/server/http.ts
CHANGED
|
@@ -29,6 +29,14 @@ export interface HttpRequestLike {
|
|
|
29
29
|
* Request headers.
|
|
30
30
|
*/
|
|
31
31
|
headers: Headers;
|
|
32
|
+
/**
|
|
33
|
+
* Abort signal for the request lifecycle when the platform exposes one.
|
|
34
|
+
*
|
|
35
|
+
* Streaming handlers should pass this to resources that must close when
|
|
36
|
+
* the client disconnects. Adapters without cancellation support may omit
|
|
37
|
+
* it.
|
|
38
|
+
*/
|
|
39
|
+
signal?: AbortSignal;
|
|
32
40
|
/**
|
|
33
41
|
* The platform request when an adapter has one available.
|
|
34
42
|
*
|
|
@@ -62,6 +70,19 @@ export interface HttpRequestLike {
|
|
|
62
70
|
clone?(): HttpRequestLike;
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Header values accepted by a framework-neutral Beignet response.
|
|
75
|
+
*
|
|
76
|
+
* Use an array when the field must be emitted more than once, such as
|
|
77
|
+
* `Set-Cookie`. Adapters append every array item as a separate field value.
|
|
78
|
+
*/
|
|
79
|
+
export type HttpResponseHeaderValue = string | readonly string[];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Framework-neutral response headers.
|
|
83
|
+
*/
|
|
84
|
+
export type HttpResponseHeaders = Record<string, HttpResponseHeaderValue>;
|
|
85
|
+
|
|
65
86
|
/**
|
|
66
87
|
* Framework-neutral response object returned by route handlers and hooks.
|
|
67
88
|
*/
|
|
@@ -71,9 +92,9 @@ export interface HttpResponseLike {
|
|
|
71
92
|
*/
|
|
72
93
|
status: number;
|
|
73
94
|
/**
|
|
74
|
-
* Response headers.
|
|
95
|
+
* Response headers. Array values are emitted as repeated header fields.
|
|
75
96
|
*/
|
|
76
|
-
headers?:
|
|
97
|
+
headers?: HttpResponseHeaders;
|
|
77
98
|
/**
|
|
78
99
|
* JSON-serializable body or an adapter-specific body value.
|
|
79
100
|
*/
|
package/src/server/index.ts
CHANGED
|
@@ -95,6 +95,10 @@ export {
|
|
|
95
95
|
* Server context blueprint declaration helper.
|
|
96
96
|
*/
|
|
97
97
|
export { defineServerContext } from "./server-context.js";
|
|
98
|
+
/**
|
|
99
|
+
* Portable Server-Sent Events response helper.
|
|
100
|
+
*/
|
|
101
|
+
export * from "./server-sent-events.js";
|
|
98
102
|
/**
|
|
99
103
|
* Trusted proxy request metadata helpers.
|
|
100
104
|
*/
|