@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.
Files changed (145) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +213 -32
  3. package/dist/application/index.d.ts +5 -5
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +5 -4
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/events/index.d.ts +54 -5
  8. package/dist/events/index.d.ts.map +1 -1
  9. package/dist/events/index.js +188 -34
  10. package/dist/events/index.js.map +1 -1
  11. package/dist/events/payload-state.d.ts +4 -0
  12. package/dist/events/payload-state.d.ts.map +1 -0
  13. package/dist/events/payload-state.js +11 -0
  14. package/dist/events/payload-state.js.map +1 -0
  15. package/dist/idempotency/index.d.ts +7 -3
  16. package/dist/idempotency/index.d.ts.map +1 -1
  17. package/dist/idempotency/index.js +45 -12
  18. package/dist/idempotency/index.js.map +1 -1
  19. package/dist/locks/index.d.ts.map +1 -1
  20. package/dist/locks/index.js +0 -4
  21. package/dist/locks/index.js.map +1 -1
  22. package/dist/mail/index.d.ts.map +1 -1
  23. package/dist/mail/index.js +6 -3
  24. package/dist/mail/index.js.map +1 -1
  25. package/dist/outbox/index.d.ts +163 -15
  26. package/dist/outbox/index.d.ts.map +1 -1
  27. package/dist/outbox/index.js +1009 -152
  28. package/dist/outbox/index.js.map +1 -1
  29. package/dist/payments/index.d.ts.map +1 -1
  30. package/dist/payments/index.js +0 -4
  31. package/dist/payments/index.js.map +1 -1
  32. package/dist/ports/best-effort-work.d.ts +21 -0
  33. package/dist/ports/best-effort-work.d.ts.map +1 -0
  34. package/dist/ports/best-effort-work.js +2 -0
  35. package/dist/ports/best-effort-work.js.map +1 -0
  36. package/dist/ports/events.d.ts +7 -5
  37. package/dist/ports/events.d.ts.map +1 -1
  38. package/dist/ports/index.d.ts +7 -2
  39. package/dist/ports/index.d.ts.map +1 -1
  40. package/dist/ports/index.js +1 -0
  41. package/dist/ports/index.js.map +1 -1
  42. package/dist/ports/testing.d.ts +28 -0
  43. package/dist/ports/testing.d.ts.map +1 -1
  44. package/dist/ports/testing.js +50 -0
  45. package/dist/ports/testing.js.map +1 -1
  46. package/dist/ports/unit-of-work.d.ts +9 -7
  47. package/dist/ports/unit-of-work.d.ts.map +1 -1
  48. package/dist/ports/unit-of-work.js +16 -6
  49. package/dist/ports/unit-of-work.js.map +1 -1
  50. package/dist/providers/index.d.ts +1 -1
  51. package/dist/providers/index.d.ts.map +1 -1
  52. package/dist/providers/index.js.map +1 -1
  53. package/dist/providers/provider.d.ts +8 -43
  54. package/dist/providers/provider.d.ts.map +1 -1
  55. package/dist/providers/provider.js.map +1 -1
  56. package/dist/search/index.d.ts.map +1 -1
  57. package/dist/search/index.js +0 -4
  58. package/dist/search/index.js.map +1 -1
  59. package/dist/server/hooks/cors.d.ts +7 -2
  60. package/dist/server/hooks/cors.d.ts.map +1 -1
  61. package/dist/server/hooks/cors.js +31 -2
  62. package/dist/server/hooks/cors.js.map +1 -1
  63. package/dist/server/hooks/logging.d.ts +2 -2
  64. package/dist/server/hooks/logging.d.ts.map +1 -1
  65. package/dist/server/hooks/logging.js.map +1 -1
  66. package/dist/server/hooks/security.d.ts +2 -2
  67. package/dist/server/hooks/security.d.ts.map +1 -1
  68. package/dist/server/hooks/security.js.map +1 -1
  69. package/dist/server/http.d.ts +21 -2
  70. package/dist/server/http.d.ts.map +1 -1
  71. package/dist/server/index.d.ts +4 -0
  72. package/dist/server/index.d.ts.map +1 -1
  73. package/dist/server/index.js +4 -0
  74. package/dist/server/index.js.map +1 -1
  75. package/dist/server/instrumentation.d.ts.map +1 -1
  76. package/dist/server/instrumentation.js +5 -3
  77. package/dist/server/instrumentation.js.map +1 -1
  78. package/dist/server/request-executor.d.ts.map +1 -1
  79. package/dist/server/request-executor.js +9 -0
  80. package/dist/server/request-executor.js.map +1 -1
  81. package/dist/server/request-preparation.d.ts.map +1 -1
  82. package/dist/server/request-preparation.js +20 -2
  83. package/dist/server/request-preparation.js.map +1 -1
  84. package/dist/server/response-finalization.d.ts +2 -2
  85. package/dist/server/response-finalization.d.ts.map +1 -1
  86. package/dist/server/response-finalization.js +25 -8
  87. package/dist/server/response-finalization.js.map +1 -1
  88. package/dist/server/route-matching.d.ts.map +1 -1
  89. package/dist/server/route-matching.js +12 -1
  90. package/dist/server/route-matching.js.map +1 -1
  91. package/dist/server/server-sent-events.d.ts +94 -0
  92. package/dist/server/server-sent-events.d.ts.map +1 -0
  93. package/dist/server/server-sent-events.js +275 -0
  94. package/dist/server/server-sent-events.js.map +1 -0
  95. package/dist/server/server.d.ts.map +1 -1
  96. package/dist/server/server.js +75 -31
  97. package/dist/server/server.js.map +1 -1
  98. package/dist/server/trusted-proxy-internal.d.ts +4 -0
  99. package/dist/server/trusted-proxy-internal.d.ts.map +1 -1
  100. package/dist/server/trusted-proxy-internal.js +20 -0
  101. package/dist/server/trusted-proxy-internal.js.map +1 -1
  102. package/dist/server/trusted-proxy.d.ts.map +1 -1
  103. package/dist/server/trusted-proxy.js +3 -8
  104. package/dist/server/trusted-proxy.js.map +1 -1
  105. package/dist/testing/index.d.ts +17 -0
  106. package/dist/testing/index.d.ts.map +1 -1
  107. package/dist/testing/index.js +18 -7
  108. package/dist/testing/index.js.map +1 -1
  109. package/package.json +1 -1
  110. package/skills/app-architecture/SKILL.md +37 -3
  111. package/src/application/index.ts +18 -7
  112. package/src/events/index.ts +268 -40
  113. package/src/events/payload-state.ts +24 -0
  114. package/src/idempotency/index.ts +65 -17
  115. package/src/locks/index.ts +0 -4
  116. package/src/mail/index.ts +7 -3
  117. package/src/outbox/index.ts +1382 -175
  118. package/src/payments/index.ts +0 -4
  119. package/src/ports/best-effort-work.ts +21 -0
  120. package/src/ports/events.ts +9 -4
  121. package/src/ports/index.ts +12 -0
  122. package/src/ports/testing.ts +76 -0
  123. package/src/ports/unit-of-work.ts +28 -14
  124. package/src/providers/index.ts +0 -1
  125. package/src/providers/provider.ts +8 -45
  126. package/src/search/index.ts +0 -4
  127. package/src/server/hooks/cors.ts +53 -5
  128. package/src/server/hooks/logging.ts +6 -2
  129. package/src/server/hooks/security.ts +8 -4
  130. package/src/server/http.ts +23 -2
  131. package/src/server/index.ts +4 -0
  132. package/src/server/instrumentation.ts +12 -4
  133. package/src/server/request-executor.ts +14 -0
  134. package/src/server/request-preparation.ts +23 -3
  135. package/src/server/response-finalization.ts +51 -15
  136. package/src/server/route-matching.ts +24 -1
  137. package/src/server/server-sent-events.ts +415 -0
  138. package/src/server/server.ts +98 -41
  139. package/src/server/trusted-proxy-internal.ts +20 -0
  140. package/src/server/trusted-proxy.ts +6 -7
  141. package/src/testing/index.ts +50 -9
  142. package/dist/query-codec.d.ts +0 -27
  143. package/dist/query-codec.d.ts.map +0 -1
  144. package/dist/query-codec.js +0 -245
  145. package/dist/query-codec.js.map +0 -1
@@ -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
+ }
@@ -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 unsubscribe = eventBus.subscribe(UserRegistered, (payload) => {
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 an unsubscribe function.
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
- ): () => void;
96
+ ): EventSubscription;
92
97
  }
93
98
 
94
99
  /**
@@ -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,
@@ -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
- * Unparsed payload recorded during the transaction.
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
- * Validate and publish all recorded events to an event bus in FIFO order.
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
- records.push({
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
- await parseEventPayload(record.event, record.payload);
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
- record.payload as never,
252
- record.options,
265
+ payload as never,
266
+ markEventPayloadParsed(record.options),
253
267
  );
254
268
  records.shift();
255
269
  }
@@ -51,5 +51,4 @@ export {
51
51
  type ProviderServiceContextFactory,
52
52
  type ProviderSetupResult,
53
53
  type ServiceProvider,
54
- type ServiceProviderMetadata,
55
54
  } from "./provider.js";
@@ -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 before all providers have started throws, so providers should
59
- * only invoke it from runtime entrypoints such as job dispatch, listeners, or
60
- * scheduled work.
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 until all providers have started, so call it lazily from runtime
226
- * entrypoints such as job dispatchers and event listeners.
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>;
@@ -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,
@@ -2,7 +2,12 @@
2
2
  * CORS hook utilities for @beignet/core/server
3
3
  */
4
4
 
5
- import type { HttpRequestLike, ServerHook } from "../types.js";
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: Record<string, string>): void {
70
+ function appendVaryOrigin(headers: HttpResponseHeaders): void {
59
71
  const varyKey =
60
72
  Object.keys(headers).find((key) => key.toLowerCase() === "vary") ?? "Vary";
61
- const current = headers[varyKey];
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: Record<string, string>,
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: Record<string, string> = {};
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 { HttpRequestLike, ServerHook } from "../types.js";
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: Record<string, string> };
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 { HttpRequestLike, ServerHook } from "../types.js";
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: Record<string, string>,
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: Record<string, string>,
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: Record<string, string>,
224
+ headers: HttpResponseHeaders,
221
225
  options: SecurityHeadersOptions = {},
222
226
  ): void {
223
227
  setHeaderIfMissing(
@@ -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?: Record<string, string>;
97
+ headers?: HttpResponseHeaders;
77
98
  /**
78
99
  * JSON-serializable body or an adapter-specific body value.
79
100
  */
@@ -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
  */