@beignet/core 0.0.31 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +346 -21
  3. package/dist/application/index.d.ts.map +1 -1
  4. package/dist/application/index.js +63 -47
  5. package/dist/application/index.js.map +1 -1
  6. package/dist/domain/entity.d.ts +37 -6
  7. package/dist/domain/entity.d.ts.map +1 -1
  8. package/dist/domain/entity.js +9 -7
  9. package/dist/domain/entity.js.map +1 -1
  10. package/dist/domain/index.d.ts +4 -4
  11. package/dist/domain/index.d.ts.map +1 -1
  12. package/dist/domain/index.js +4 -4
  13. package/dist/domain/index.js.map +1 -1
  14. package/dist/events/index.d.ts +6 -0
  15. package/dist/events/index.d.ts.map +1 -1
  16. package/dist/events/index.js +20 -10
  17. package/dist/events/index.js.map +1 -1
  18. package/dist/flags/index.d.ts +5 -4
  19. package/dist/flags/index.d.ts.map +1 -1
  20. package/dist/flags/index.js +10 -9
  21. package/dist/flags/index.js.map +1 -1
  22. package/dist/idempotency/index.d.ts +11 -1
  23. package/dist/idempotency/index.d.ts.map +1 -1
  24. package/dist/idempotency/index.js +4 -3
  25. package/dist/idempotency/index.js.map +1 -1
  26. package/dist/jobs/index.d.ts +449 -9
  27. package/dist/jobs/index.d.ts.map +1 -1
  28. package/dist/jobs/index.js +472 -12
  29. package/dist/jobs/index.js.map +1 -1
  30. package/dist/outbox/index.d.ts +170 -3
  31. package/dist/outbox/index.d.ts.map +1 -1
  32. package/dist/outbox/index.js +192 -8
  33. package/dist/outbox/index.js.map +1 -1
  34. package/dist/ports/events.d.ts +2 -2
  35. package/dist/ports/index.d.ts +6 -1
  36. package/dist/ports/index.d.ts.map +1 -1
  37. package/dist/ports/index.js +1 -0
  38. package/dist/ports/index.js.map +1 -1
  39. package/dist/providers/instrumentation.d.ts +4 -0
  40. package/dist/providers/instrumentation.d.ts.map +1 -1
  41. package/dist/providers/instrumentation.js.map +1 -1
  42. package/dist/providers/provider.d.ts +10 -2
  43. package/dist/providers/provider.d.ts.map +1 -1
  44. package/dist/providers/provider.js.map +1 -1
  45. package/dist/schedules/index.d.ts +8 -7
  46. package/dist/schedules/index.d.ts.map +1 -1
  47. package/dist/schedules/index.js +47 -16
  48. package/dist/schedules/index.js.map +1 -1
  49. package/dist/search/index.d.ts +2 -1
  50. package/dist/search/index.d.ts.map +1 -1
  51. package/dist/search/index.js +1 -0
  52. package/dist/search/index.js.map +1 -1
  53. package/dist/server/hooks/index.d.ts +1 -0
  54. package/dist/server/hooks/index.d.ts.map +1 -1
  55. package/dist/server/hooks/index.js +1 -0
  56. package/dist/server/hooks/index.js.map +1 -1
  57. package/dist/server/hooks/rate-limit.d.ts +26 -13
  58. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  59. package/dist/server/hooks/rate-limit.js +28 -34
  60. package/dist/server/hooks/rate-limit.js.map +1 -1
  61. package/dist/server/hooks/security.d.ts +179 -0
  62. package/dist/server/hooks/security.d.ts.map +1 -0
  63. package/dist/server/hooks/security.js +225 -0
  64. package/dist/server/hooks/security.js.map +1 -0
  65. package/dist/server/index.d.ts +8 -0
  66. package/dist/server/index.d.ts.map +1 -1
  67. package/dist/server/index.js +8 -0
  68. package/dist/server/index.js.map +1 -1
  69. package/dist/server/instrumentation.d.ts.map +1 -1
  70. package/dist/server/instrumentation.js +22 -6
  71. package/dist/server/instrumentation.js.map +1 -1
  72. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  73. package/dist/server/providers/loadProviderConfig.js +15 -2
  74. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  75. package/dist/server/request-context.d.ts +4 -0
  76. package/dist/server/request-context.d.ts.map +1 -1
  77. package/dist/server/request-context.js +3 -0
  78. package/dist/server/request-context.js.map +1 -1
  79. package/dist/server/runtime-integrity.d.ts +84 -0
  80. package/dist/server/runtime-integrity.d.ts.map +1 -0
  81. package/dist/server/runtime-integrity.js +160 -0
  82. package/dist/server/runtime-integrity.js.map +1 -0
  83. package/dist/server/server.d.ts +10 -0
  84. package/dist/server/server.d.ts.map +1 -1
  85. package/dist/server/server.js +55 -1
  86. package/dist/server/server.js.map +1 -1
  87. package/dist/server/trusted-proxy.d.ts +77 -0
  88. package/dist/server/trusted-proxy.d.ts.map +1 -0
  89. package/dist/server/trusted-proxy.js +129 -0
  90. package/dist/server/trusted-proxy.js.map +1 -0
  91. package/dist/tasks/index.d.ts +6 -7
  92. package/dist/tasks/index.d.ts.map +1 -1
  93. package/dist/tasks/index.js +22 -5
  94. package/dist/tasks/index.js.map +1 -1
  95. package/dist/tenancy/index.d.ts +41 -0
  96. package/dist/tenancy/index.d.ts.map +1 -0
  97. package/dist/tenancy/index.js +34 -0
  98. package/dist/tenancy/index.js.map +1 -0
  99. package/dist/testing/index.d.ts +6 -0
  100. package/dist/testing/index.d.ts.map +1 -1
  101. package/dist/testing/index.js +12 -3
  102. package/dist/testing/index.js.map +1 -1
  103. package/dist/tracing/execution.d.ts +14 -0
  104. package/dist/tracing/execution.d.ts.map +1 -0
  105. package/dist/tracing/execution.js +17 -0
  106. package/dist/tracing/execution.js.map +1 -0
  107. package/dist/tracing/index.d.ts +49 -0
  108. package/dist/tracing/index.d.ts.map +1 -1
  109. package/dist/tracing/index.js +59 -0
  110. package/dist/tracing/index.js.map +1 -1
  111. package/dist/uploads/client.d.ts.map +1 -1
  112. package/dist/uploads/client.js +44 -6
  113. package/dist/uploads/client.js.map +1 -1
  114. package/dist/uploads/index.d.ts +182 -2
  115. package/dist/uploads/index.d.ts.map +1 -1
  116. package/dist/uploads/index.js +468 -76
  117. package/dist/uploads/index.js.map +1 -1
  118. package/dist/webhooks/index.d.ts +56 -4
  119. package/dist/webhooks/index.d.ts.map +1 -1
  120. package/dist/webhooks/index.js +107 -2
  121. package/dist/webhooks/index.js.map +1 -1
  122. package/package.json +5 -1
  123. package/skills/app-architecture/SKILL.md +23 -2
  124. package/src/application/index.ts +88 -56
  125. package/src/domain/entity.ts +61 -13
  126. package/src/domain/index.ts +8 -7
  127. package/src/events/index.ts +27 -14
  128. package/src/flags/index.ts +23 -19
  129. package/src/idempotency/index.ts +17 -3
  130. package/src/jobs/index.ts +1022 -19
  131. package/src/outbox/index.ts +409 -9
  132. package/src/ports/events.ts +2 -2
  133. package/src/ports/index.ts +17 -0
  134. package/src/providers/instrumentation.ts +4 -0
  135. package/src/providers/provider.ts +11 -2
  136. package/src/schedules/index.ts +60 -26
  137. package/src/search/index.ts +3 -1
  138. package/src/server/hooks/index.ts +10 -0
  139. package/src/server/hooks/rate-limit.ts +52 -46
  140. package/src/server/hooks/security.ts +503 -0
  141. package/src/server/index.ts +8 -0
  142. package/src/server/instrumentation.ts +25 -5
  143. package/src/server/providers/loadProviderConfig.ts +19 -2
  144. package/src/server/request-context.ts +7 -0
  145. package/src/server/runtime-integrity.ts +322 -0
  146. package/src/server/server.ts +86 -1
  147. package/src/server/trusted-proxy.ts +249 -0
  148. package/src/tasks/index.ts +29 -12
  149. package/src/tenancy/index.ts +55 -0
  150. package/src/testing/index.ts +19 -3
  151. package/src/tracing/execution.ts +37 -0
  152. package/src/tracing/index.ts +137 -0
  153. package/src/uploads/client.ts +65 -6
  154. package/src/uploads/index.ts +713 -76
  155. package/src/webhooks/index.ts +240 -9
  156. package/src/domain/events.ts +0 -59
@@ -16,6 +16,8 @@ import {
16
16
  type InferJobPayload,
17
17
  type JobDef,
18
18
  parseJobPayload,
19
+ SINGLE_ATTEMPT_DISPATCH,
20
+ type SingleAttemptJobDispatch,
19
21
  shouldRetryJob,
20
22
  } from "../jobs/index.js";
21
23
  import type { JobDispatcherPort } from "../ports/events.js";
@@ -267,6 +269,116 @@ export interface OutboxMarkFailedInput {
267
269
  now?: Date;
268
270
  }
269
271
 
272
+ /**
273
+ * Default maximum messages returned from outbox admin list calls.
274
+ */
275
+ export const DEFAULT_OUTBOX_ADMIN_LIST_LIMIT = 50;
276
+
277
+ /**
278
+ * Shared filters for outbox admin read operations.
279
+ */
280
+ export interface OutboxMessageQuery {
281
+ /**
282
+ * Status or statuses to include.
283
+ */
284
+ status?: OutboxMessageStatus | readonly OutboxMessageStatus[];
285
+ /**
286
+ * Message kind to include.
287
+ */
288
+ kind?: OutboxMessageKind;
289
+ /**
290
+ * Event or job name to include.
291
+ */
292
+ name?: string;
293
+ /**
294
+ * Include messages last updated before this timestamp.
295
+ */
296
+ updatedBefore?: Date;
297
+ /**
298
+ * Include delivered messages delivered before this timestamp.
299
+ */
300
+ deliveredBefore?: Date;
301
+ }
302
+
303
+ /**
304
+ * Options for listing outbox messages through the admin port.
305
+ */
306
+ export interface OutboxListMessagesOptions extends OutboxMessageQuery {
307
+ /**
308
+ * Maximum messages to return. Defaults to
309
+ * `DEFAULT_OUTBOX_ADMIN_LIST_LIMIT`.
310
+ */
311
+ limit?: number;
312
+ }
313
+
314
+ /**
315
+ * Options for counting outbox messages through the admin port.
316
+ */
317
+ export interface OutboxCountMessagesOptions extends OutboxMessageQuery {}
318
+
319
+ /**
320
+ * Input for returning a dead-lettered message to the pending queue.
321
+ */
322
+ export interface OutboxRequeueMessageInput {
323
+ /**
324
+ * Dead-lettered message ID.
325
+ */
326
+ id: string;
327
+ /**
328
+ * Earliest time the message may be claimed again. Defaults to `now`.
329
+ */
330
+ availableAt?: Date;
331
+ /**
332
+ * Reset attempts to zero before requeueing. Defaults to preserving the
333
+ * attempt count so operators can decide whether to grant a fresh retry
334
+ * budget.
335
+ */
336
+ resetAttempts?: boolean;
337
+ /**
338
+ * Requeue timestamp.
339
+ */
340
+ now?: Date;
341
+ }
342
+
343
+ /**
344
+ * Input for purging dead-lettered messages.
345
+ */
346
+ export interface OutboxPurgeDeadLetteredInput {
347
+ /**
348
+ * Only purge messages last updated before this timestamp. Omit only when the
349
+ * caller intentionally wants to purge all dead-lettered messages.
350
+ */
351
+ before?: Date;
352
+ /**
353
+ * Maximum messages to purge.
354
+ */
355
+ limit?: number;
356
+ }
357
+
358
+ /**
359
+ * Input for pruning delivered messages.
360
+ */
361
+ export interface OutboxPruneDeliveredInput {
362
+ /**
363
+ * Prune delivered messages delivered before this timestamp.
364
+ */
365
+ before: Date;
366
+ /**
367
+ * Maximum messages to prune.
368
+ */
369
+ limit?: number;
370
+ }
371
+
372
+ /**
373
+ * Result for outbox admin delete operations.
374
+ */
375
+ export interface OutboxDeleteResult {
376
+ /**
377
+ * Number of rows deleted.
378
+ */
379
+ deleted: number;
380
+ }
381
+
270
382
  /**
271
383
  * App-facing outbox storage port.
272
384
  *
@@ -292,10 +404,48 @@ export interface OutboxPort {
292
404
  markFailed(input: OutboxMarkFailedInput): Promise<void>;
293
405
  }
294
406
 
407
+ /**
408
+ * Operational outbox admin port.
409
+ *
410
+ * Keep this separate from `OutboxPort` so request and drain contexts can expose
411
+ * only the hot-path delivery operations. Wire this port into maintenance
412
+ * contexts for CLI commands, runbooks, and devtools.
413
+ */
414
+ export interface OutboxAdminPort {
415
+ /**
416
+ * List messages ordered by newest update first.
417
+ */
418
+ listMessages(
419
+ options?: OutboxListMessagesOptions,
420
+ ): Promise<readonly OutboxMessage[]>;
421
+ /**
422
+ * Count messages matching admin filters.
423
+ */
424
+ countMessages(options?: OutboxCountMessagesOptions): Promise<number>;
425
+ /**
426
+ * Fetch one message by ID.
427
+ */
428
+ getMessage(id: string): Promise<OutboxMessage | null>;
429
+ /**
430
+ * Return a dead-lettered message to pending state.
431
+ */
432
+ requeueMessage(input: OutboxRequeueMessageInput): Promise<OutboxMessage>;
433
+ /**
434
+ * Delete dead-lettered messages.
435
+ */
436
+ purgeDeadLettered(
437
+ input?: OutboxPurgeDeadLetteredInput,
438
+ ): Promise<OutboxDeleteResult>;
439
+ /**
440
+ * Delete delivered messages older than a retention cutoff.
441
+ */
442
+ pruneDelivered(input: OutboxPruneDeliveredInput): Promise<OutboxDeleteResult>;
443
+ }
444
+
295
445
  /**
296
446
  * In-memory outbox for tests and local examples.
297
447
  */
298
- export interface MemoryOutboxPort extends OutboxPort {
448
+ export interface MemoryOutboxPort extends OutboxPort, OutboxAdminPort {
299
449
  /**
300
450
  * Current message snapshots.
301
451
  */
@@ -311,9 +461,13 @@ export interface MemoryOutboxPort extends OutboxPort {
311
461
  */
312
462
  export interface CreateOutboxMessageOptions {
313
463
  /**
314
- * Generated message ID override.
464
+ * Generated message ID override. Wins over `input.id`.
315
465
  */
316
466
  id?: string;
467
+ /**
468
+ * Fallback ID factory used when neither `id` nor `input.id` is provided.
469
+ */
470
+ createId?: () => string;
317
471
  /**
318
472
  * Timestamp used for created/updated/available dates.
319
473
  */
@@ -498,6 +652,22 @@ export class OutboxClaimError extends Error {
498
652
  }
499
653
  }
500
654
 
655
+ /**
656
+ * Error thrown when an outbox admin operation cannot be completed safely.
657
+ */
658
+ export class OutboxAdminError extends Error {
659
+ /**
660
+ * Message ID involved in the admin error, when applicable.
661
+ */
662
+ readonly id?: string;
663
+
664
+ constructor(args: { message: string; id?: string }) {
665
+ super(args.message);
666
+ this.name = "OutboxAdminError";
667
+ this.id = args.id;
668
+ }
669
+ }
670
+
501
671
  function assertNonEmptyString(name: string, value: string): void {
502
672
  if (typeof value !== "string" || value.trim().length === 0) {
503
673
  throw new Error(`${name} must be a non-empty string`);
@@ -510,6 +680,12 @@ function assertPositiveInteger(name: string, value: number): void {
510
680
  }
511
681
  }
512
682
 
683
+ function assertValidDate(name: string, value: Date): void {
684
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
685
+ throw new Error(`${name} must be a valid Date`);
686
+ }
687
+ }
688
+
513
689
  function cloneDate(value: Date): Date {
514
690
  return new Date(value.getTime());
515
691
  }
@@ -639,6 +815,75 @@ function copyMessage(message: OutboxMessage): OutboxMessage {
639
815
  };
640
816
  }
641
817
 
818
+ function normalizeStatusFilter(
819
+ status: OutboxMessageQuery["status"],
820
+ ): ReadonlySet<OutboxMessageStatus> | undefined {
821
+ if (status === undefined) return undefined;
822
+ return new Set(Array.isArray(status) ? status : [status]);
823
+ }
824
+
825
+ function messageMatchesQuery(
826
+ message: OutboxMessage,
827
+ query: OutboxMessageQuery = {},
828
+ ): boolean {
829
+ const statuses = normalizeStatusFilter(query.status);
830
+ if (statuses && !statuses.has(message.status)) return false;
831
+ if (query.kind !== undefined && message.kind !== query.kind) return false;
832
+ if (query.name !== undefined && message.name !== query.name) return false;
833
+ if (
834
+ query.updatedBefore !== undefined &&
835
+ message.updatedAt.getTime() >= query.updatedBefore.getTime()
836
+ ) {
837
+ return false;
838
+ }
839
+ if (
840
+ query.deliveredBefore !== undefined &&
841
+ (message.deliveredAt === null ||
842
+ message.deliveredAt.getTime() >= query.deliveredBefore.getTime())
843
+ ) {
844
+ return false;
845
+ }
846
+ return true;
847
+ }
848
+
849
+ function compareMessagesForAdminList(
850
+ left: OutboxMessage,
851
+ right: OutboxMessage,
852
+ ): number {
853
+ const updated = right.updatedAt.getTime() - left.updatedAt.getTime();
854
+ if (updated !== 0) return updated;
855
+ const created = right.createdAt.getTime() - left.createdAt.getTime();
856
+ if (created !== 0) return created;
857
+ return right.id.localeCompare(left.id);
858
+ }
859
+
860
+ function compareMessagesForAdminCleanup(
861
+ left: OutboxMessage,
862
+ right: OutboxMessage,
863
+ ): number {
864
+ const updated = left.updatedAt.getTime() - right.updatedAt.getTime();
865
+ if (updated !== 0) return updated;
866
+ const created = left.createdAt.getTime() - right.createdAt.getTime();
867
+ if (created !== 0) return created;
868
+ return left.id.localeCompare(right.id);
869
+ }
870
+
871
+ function compareDeliveredMessagesForPrune(
872
+ left: OutboxMessage,
873
+ right: OutboxMessage,
874
+ ): number {
875
+ const delivered =
876
+ (left.deliveredAt?.getTime() ?? 0) - (right.deliveredAt?.getTime() ?? 0);
877
+ if (delivered !== 0) return delivered;
878
+ return compareMessagesForAdminCleanup(left, right);
879
+ }
880
+
881
+ function resolveAdminListLimit(limit: number | undefined): number {
882
+ const resolved = limit ?? DEFAULT_OUTBOX_ADMIN_LIST_LIMIT;
883
+ assertPositiveInteger("limit", resolved);
884
+ return resolved;
885
+ }
886
+
642
887
  function toClaimedMessage(message: OutboxMessage): ClaimedOutboxMessage {
643
888
  if (
644
889
  message.status !== "claimed" ||
@@ -677,7 +922,7 @@ export function createOutboxMessage(
677
922
 
678
923
  const now = options.now ?? new Date();
679
924
  return {
680
- id: options.id ?? input.id ?? createId(),
925
+ id: options.id ?? input.id ?? options.createId?.() ?? createId(),
681
926
  kind: input.kind,
682
927
  name: input.name,
683
928
  payload: toOutboxJsonValue(input.payload),
@@ -709,12 +954,31 @@ function isEligible(message: OutboxMessage, now: Date): boolean {
709
954
  );
710
955
  }
711
956
 
957
+ /**
958
+ * Options for `createMemoryOutbox(...)`.
959
+ */
960
+ export interface MemoryOutboxOptions {
961
+ /**
962
+ * Message and claim-token ID factory. Defaults to `crypto.randomUUID()`.
963
+ */
964
+ id?: () => string;
965
+ /**
966
+ * Clock used for enqueue, claim, and completion timestamps when a call does
967
+ * not supply its own `now`. Defaults to the system clock.
968
+ */
969
+ now?: () => Date;
970
+ }
971
+
712
972
  /**
713
973
  * Create an in-memory outbox for tests and local examples.
714
974
  *
715
975
  * The memory outbox is process-local and not durable.
716
976
  */
717
- export function createMemoryOutbox(): MemoryOutboxPort {
977
+ export function createMemoryOutbox(
978
+ storeOptions: MemoryOutboxOptions = {},
979
+ ): MemoryOutboxPort {
980
+ const createStoreId = storeOptions.id ?? createId;
981
+ const storeNow = storeOptions.now ?? (() => new Date());
718
982
  const messages = new Map<string, OutboxMessage>();
719
983
 
720
984
  function getClaimedOrThrow(id: string, claimToken: string): OutboxMessage {
@@ -739,8 +1003,125 @@ export function createMemoryOutbox(): MemoryOutboxPort {
739
1003
  return [...messages.values()].map(copyMessage);
740
1004
  },
741
1005
 
1006
+ async listMessages(options = {}) {
1007
+ const limit = resolveAdminListLimit(options.limit);
1008
+ if (options.updatedBefore) {
1009
+ assertValidDate("updatedBefore", options.updatedBefore);
1010
+ }
1011
+ if (options.deliveredBefore) {
1012
+ assertValidDate("deliveredBefore", options.deliveredBefore);
1013
+ }
1014
+
1015
+ return [...messages.values()]
1016
+ .filter((message) => messageMatchesQuery(message, options))
1017
+ .sort(compareMessagesForAdminList)
1018
+ .slice(0, limit)
1019
+ .map(copyMessage);
1020
+ },
1021
+
1022
+ async countMessages(options = {}) {
1023
+ if (options.updatedBefore) {
1024
+ assertValidDate("updatedBefore", options.updatedBefore);
1025
+ }
1026
+ if (options.deliveredBefore) {
1027
+ assertValidDate("deliveredBefore", options.deliveredBefore);
1028
+ }
1029
+
1030
+ return [...messages.values()].filter((message) =>
1031
+ messageMatchesQuery(message, options),
1032
+ ).length;
1033
+ },
1034
+
1035
+ async getMessage(id) {
1036
+ assertNonEmptyString("id", id);
1037
+ const message = messages.get(id);
1038
+ return message ? copyMessage(message) : null;
1039
+ },
1040
+
1041
+ async requeueMessage(input) {
1042
+ assertNonEmptyString("id", input.id);
1043
+ const message = messages.get(input.id);
1044
+ if (!message) {
1045
+ throw new OutboxAdminError({
1046
+ id: input.id,
1047
+ message: `Outbox message "${input.id}" does not exist.`,
1048
+ });
1049
+ }
1050
+ if (message.status !== "deadLettered") {
1051
+ throw new OutboxAdminError({
1052
+ id: input.id,
1053
+ message: `Outbox message "${input.id}" is not dead-lettered.`,
1054
+ });
1055
+ }
1056
+
1057
+ const now = input.now ?? storeNow();
1058
+ const availableAt = input.availableAt ?? now;
1059
+ assertValidDate("now", now);
1060
+ assertValidDate("availableAt", availableAt);
1061
+
1062
+ message.status = "pending";
1063
+ message.availableAt = cloneDate(availableAt);
1064
+ message.claimToken = null;
1065
+ message.claimedAt = null;
1066
+ message.lockedUntil = null;
1067
+ if (input.resetAttempts) message.attempts = 0;
1068
+ message.updatedAt = cloneDate(now);
1069
+
1070
+ return copyMessage(message);
1071
+ },
1072
+
1073
+ async purgeDeadLettered(input = {}) {
1074
+ const limit =
1075
+ input.limit === undefined
1076
+ ? undefined
1077
+ : resolveAdminListLimit(input.limit);
1078
+ if (input.before) assertValidDate("before", input.before);
1079
+
1080
+ const candidates = [...messages.values()]
1081
+ .filter(
1082
+ (message) =>
1083
+ message.status === "deadLettered" &&
1084
+ (input.before === undefined ||
1085
+ message.updatedAt.getTime() < input.before.getTime()),
1086
+ )
1087
+ .sort(compareMessagesForAdminCleanup)
1088
+ .slice(0, limit);
1089
+
1090
+ for (const message of candidates) {
1091
+ messages.delete(message.id);
1092
+ }
1093
+
1094
+ return { deleted: candidates.length };
1095
+ },
1096
+
1097
+ async pruneDelivered(input) {
1098
+ assertValidDate("before", input.before);
1099
+ const limit =
1100
+ input.limit === undefined
1101
+ ? undefined
1102
+ : resolveAdminListLimit(input.limit);
1103
+ const candidates = [...messages.values()]
1104
+ .filter(
1105
+ (message) =>
1106
+ message.status === "delivered" &&
1107
+ message.deliveredAt !== null &&
1108
+ message.deliveredAt.getTime() < input.before.getTime(),
1109
+ )
1110
+ .sort(compareDeliveredMessagesForPrune)
1111
+ .slice(0, limit);
1112
+
1113
+ for (const message of candidates) {
1114
+ messages.delete(message.id);
1115
+ }
1116
+
1117
+ return { deleted: candidates.length };
1118
+ },
1119
+
742
1120
  async enqueue(input) {
743
- const message = createOutboxMessage(input);
1121
+ const message = createOutboxMessage(input, {
1122
+ createId: createStoreId,
1123
+ now: storeNow(),
1124
+ });
744
1125
  if (messages.has(message.id)) {
745
1126
  throw new Error(`Outbox message "${message.id}" already exists.`);
746
1127
  }
@@ -750,7 +1131,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
750
1131
 
751
1132
  async claimBatch(options) {
752
1133
  assertPositiveInteger("limit", options.limit);
753
- const now = options.now ?? new Date();
1134
+ const now = options.now ?? storeNow();
754
1135
  const leaseMs = options.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
755
1136
  assertPositiveInteger("leaseMs", leaseMs);
756
1137
  const lockedUntil = new Date(now.getTime() + leaseMs);
@@ -768,7 +1149,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
768
1149
  for (const message of eligible) {
769
1150
  message.status = "claimed";
770
1151
  message.attempts += 1;
771
- message.claimToken = createId();
1152
+ message.claimToken = createStoreId();
772
1153
  message.claimedAt = cloneDate(now);
773
1154
  message.lockedUntil = cloneDate(lockedUntil);
774
1155
  message.updatedAt = cloneDate(now);
@@ -782,7 +1163,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
782
1163
  assertNonEmptyString("id", input.id);
783
1164
  assertNonEmptyString("claimToken", input.claimToken);
784
1165
  const message = getClaimedOrThrow(input.id, input.claimToken);
785
- const now = input.now ?? new Date();
1166
+ const now = input.now ?? storeNow();
786
1167
 
787
1168
  message.status = "delivered";
788
1169
  message.deliveredAt = cloneDate(now);
@@ -796,7 +1177,7 @@ export function createMemoryOutbox(): MemoryOutboxPort {
796
1177
  assertNonEmptyString("id", input.id);
797
1178
  assertNonEmptyString("claimToken", input.claimToken);
798
1179
  const message = getClaimedOrThrow(input.id, input.claimToken);
799
- const now = input.now ?? new Date();
1180
+ const now = input.now ?? storeNow();
800
1181
 
801
1182
  message.status = input.deadLetter ? "deadLettered" : "pending";
802
1183
  message.lastError = serializeOutboxError(input.error);
@@ -1018,6 +1399,25 @@ async function deliverOutboxMessage(
1018
1399
  }
1019
1400
 
1020
1401
  const payload = await parseJobPayload(job, message.payload);
1402
+
1403
+ // The drain owns execution retries: failed deliveries are rescheduled with
1404
+ // the job's own policy via markFailed/retryAt. When the dispatcher exposes
1405
+ // a single-attempt dispatch (the inline dispatcher does), use it so the
1406
+ // retry policy runs in exactly one layer. Durable providers do not expose
1407
+ // it — for them dispatch is an enqueue and the queue owns execution.
1408
+ const singleAttempt = (
1409
+ options.jobs as {
1410
+ [SINGLE_ATTEMPT_DISPATCH]?: SingleAttemptJobDispatch;
1411
+ }
1412
+ )[SINGLE_ATTEMPT_DISPATCH];
1413
+ if (singleAttempt) {
1414
+ await singleAttempt(job, payload, {
1415
+ attempt: message.attempts,
1416
+ maxAttempts: message.maxAttempts,
1417
+ });
1418
+ return;
1419
+ }
1420
+
1021
1421
  await options.jobs.dispatch(job, payload);
1022
1422
  }
1023
1423
 
@@ -10,8 +10,8 @@ import type {
10
10
 
11
11
  /**
12
12
  * Represents a defined Domain Event with name and payload schema.
13
- * This is a minimal interface that ports need - implementations can use
14
- * the full DomainEventDef from @beignet/core/domain if desired.
13
+ * This is a minimal structural interface that ports need - event
14
+ * declarations from `defineEvent` in @beignet/core/events satisfy it.
15
15
  */
16
16
  export interface DomainEventDef<
17
17
  Name extends string = string,
@@ -281,14 +281,22 @@ export type {
281
281
  */
282
282
  export type {
283
283
  ClaimedOutboxMessage,
284
+ OutboxAdminPort,
284
285
  OutboxClaimBatchOptions,
286
+ OutboxCountMessagesOptions,
287
+ OutboxDeleteResult,
285
288
  OutboxEnqueueInput,
289
+ OutboxListMessagesOptions,
286
290
  OutboxMarkDeliveredInput,
287
291
  OutboxMarkFailedInput,
288
292
  OutboxMessage,
289
293
  OutboxMessageKind,
294
+ OutboxMessageQuery,
290
295
  OutboxMessageStatus,
291
296
  OutboxPort,
297
+ OutboxPruneDeliveredInput,
298
+ OutboxPurgeDeadLetteredInput,
299
+ OutboxRequeueMessageInput,
292
300
  } from "../outbox/index.js";
293
301
  /**
294
302
  * Payments port exports.
@@ -359,6 +367,15 @@ export {
359
367
  SearchOptionsError,
360
368
  searchDocuments,
361
369
  } from "../search/index.js";
370
+ /**
371
+ * Tenant scope exports.
372
+ */
373
+ export type { TenantScope } from "../tenancy/index.js";
374
+ export {
375
+ createTenantScope,
376
+ requireTenantScope,
377
+ tenantScopeId,
378
+ } from "../tenancy/index.js";
362
379
  /**
363
380
  * Webhook exports.
364
381
  *
@@ -39,6 +39,10 @@ export interface BaseProviderInstrumentationEvent {
39
39
  * W3C traceparent header value, when available.
40
40
  */
41
41
  traceparent?: string;
42
+ /**
43
+ * W3C tracestate value associated with the current operation.
44
+ */
45
+ tracestate?: string;
42
46
  /**
43
47
  * Watcher category used by devtools and instrumentation sinks.
44
48
  */
@@ -36,6 +36,15 @@ export interface ProviderConfigDef<CfgSchema extends StandardSchemaV1> {
36
36
  * and pass them to the schema for validation.
37
37
  */
38
38
  envPrefix?: string;
39
+
40
+ /**
41
+ * Field-level config overrides, keyed by schema field name. Defined values
42
+ * are merged over the env-derived (or server-supplied) input before
43
+ * validation, so factory options win over environment variables and still
44
+ * satisfy required fields when the env var is absent. `undefined` values
45
+ * are ignored.
46
+ */
47
+ overrides?: Record<string, unknown>;
39
48
  }
40
49
 
41
50
  /**
@@ -161,8 +170,8 @@ export interface ServiceProviderMetadata {
161
170
  *
162
171
  * @example
163
172
  * ```ts
164
- * const redisProvider = createProvider({
165
- * name: "redis",
173
+ * const redisCacheProvider = createProvider({
174
+ * name: "cache-redis",
166
175
  * config: {
167
176
  * schema: z.object({ URL: z.string().url() }),
168
177
  * envPrefix: "REDIS_",