@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
@@ -82,6 +82,7 @@ export interface WebhookDef<
82
82
  Name extends string = string,
83
83
  Events extends WebhookEventSchemas = WebhookEventSchemas,
84
84
  > {
85
+ kind: "webhook";
85
86
  name: Name;
86
87
  provider?: string;
87
88
  events: Events;
@@ -104,11 +105,72 @@ export interface DefineWebhookOptions<
104
105
  /**
105
106
  * Options accepted by `verifyWebhook(...)`.
106
107
  */
107
- export interface VerifyWebhookOptions {
108
+ export interface VerifyWebhookOptions<
109
+ AllowUnknownEvents extends boolean = boolean,
110
+ > {
108
111
  verifier?: WebhookVerifier;
109
- allowUnknownEvents?: boolean;
112
+ allowUnknownEvents?: AllowUnknownEvents;
110
113
  }
111
114
 
115
+ type InferWebhookVerificationResult<
116
+ TWebhook extends WebhookDef<string, WebhookEventSchemas>,
117
+ AllowUnknownEvents extends boolean,
118
+ > = true extends AllowUnknownEvents
119
+ ? InferWebhookEvent<TWebhook> | WebhookEvent
120
+ : InferWebhookEvent<TWebhook>;
121
+
122
+ /**
123
+ * Timestamp formats accepted by the generic HMAC webhook verifier.
124
+ */
125
+ export type HmacWebhookTimestampFormat =
126
+ | "unix-seconds"
127
+ | "unix-milliseconds"
128
+ | "iso8601";
129
+
130
+ /**
131
+ * Timestamp source and tolerance for generic HMAC replay protection.
132
+ */
133
+ export type HmacWebhookTimestampOptions =
134
+ | {
135
+ /**
136
+ * Header that carries the provider event timestamp.
137
+ */
138
+ header: string;
139
+ payloadPath?: never;
140
+ /**
141
+ * Timestamp format.
142
+ *
143
+ * @default "unix-seconds"
144
+ */
145
+ format?: HmacWebhookTimestampFormat;
146
+ /**
147
+ * Maximum absolute clock skew between the event timestamp and receipt.
148
+ *
149
+ * @default 300
150
+ */
151
+ toleranceSec?: number;
152
+ }
153
+ | {
154
+ header?: never;
155
+ /**
156
+ * Dot path used to read the provider event timestamp from the JSON
157
+ * payload.
158
+ */
159
+ payloadPath: string;
160
+ /**
161
+ * Timestamp format.
162
+ *
163
+ * @default "unix-seconds"
164
+ */
165
+ format?: HmacWebhookTimestampFormat;
166
+ /**
167
+ * Maximum absolute clock skew between the event timestamp and receipt.
168
+ *
169
+ * @default 300
170
+ */
171
+ toleranceSec?: number;
172
+ };
173
+
112
174
  /**
113
175
  * Options for the in-memory webhook verifier.
114
176
  */
@@ -162,6 +224,10 @@ export interface CreateHmacWebhookVerifierOptions {
162
224
  * @default "type"
163
225
  */
164
226
  eventTypePath?: string;
227
+ /**
228
+ * Optional timestamp source used to reject replayed generic HMAC webhooks.
229
+ */
230
+ timestamp?: HmacWebhookTimestampOptions;
165
231
  }
166
232
 
167
233
  /**
@@ -235,6 +301,7 @@ export function defineWebhook<
235
301
  if (!name) throw new WebhookOptionsError("Webhook name is required.");
236
302
 
237
303
  return {
304
+ kind: "webhook",
238
305
  name,
239
306
  provider: options.provider,
240
307
  events: (options.events ?? {}) as Events,
@@ -248,11 +315,12 @@ export function defineWebhook<
248
315
  */
249
316
  export async function verifyWebhook<
250
317
  TWebhook extends WebhookDef<string, WebhookEventSchemas>,
318
+ AllowUnknownEvents extends boolean = false,
251
319
  >(
252
320
  webhook: TWebhook,
253
321
  input: VerifyWebhookInput,
254
- options: VerifyWebhookOptions = {},
255
- ): Promise<InferWebhookEvent<TWebhook> | WebhookEvent> {
322
+ options: VerifyWebhookOptions<AllowUnknownEvents> = {},
323
+ ): Promise<InferWebhookVerificationResult<TWebhook, AllowUnknownEvents>> {
256
324
  validateWebhook(webhook);
257
325
  const verifier = options.verifier ?? webhook.verifier;
258
326
  if (!verifier) {
@@ -283,17 +351,26 @@ export async function verifyWebhook<
283
351
  */
284
352
  export async function parseWebhookEvent<
285
353
  TWebhook extends WebhookDef<string, WebhookEventSchemas>,
354
+ AllowUnknownEvents extends boolean = false,
286
355
  >(
287
356
  webhook: TWebhook,
288
357
  event: WebhookEvent,
289
- options: Pick<VerifyWebhookOptions, "allowUnknownEvents"> = {},
290
- ): Promise<InferWebhookEvent<TWebhook> | WebhookEvent> {
358
+ options: Pick<
359
+ VerifyWebhookOptions<AllowUnknownEvents>,
360
+ "allowUnknownEvents"
361
+ > = {},
362
+ ): Promise<InferWebhookVerificationResult<TWebhook, AllowUnknownEvents>> {
291
363
  validateWebhook(webhook);
292
364
  validateEvent(webhook, event);
293
365
 
294
366
  const schema = webhook.events[event.type];
295
367
  if (!schema) {
296
- if (options.allowUnknownEvents ?? true) return event;
368
+ if (options.allowUnknownEvents === true) {
369
+ return event as InferWebhookVerificationResult<
370
+ TWebhook,
371
+ AllowUnknownEvents
372
+ >;
373
+ }
297
374
  throw new WebhookVerificationError({
298
375
  message: `Webhook "${webhook.name}" received unknown event type "${event.type}".`,
299
376
  webhookName: webhook.name,
@@ -318,7 +395,7 @@ export async function parseWebhookEvent<
318
395
  return {
319
396
  ...event,
320
397
  payload: result.value,
321
- };
398
+ } as InferWebhookVerificationResult<TWebhook, AllowUnknownEvents>;
322
399
  }
323
400
 
324
401
  /**
@@ -371,6 +448,7 @@ export function createHmacWebhookVerifier(
371
448
  const algorithm = options.algorithm ?? "SHA-256";
372
449
  const eventIdPath = options.eventIdPath ?? "id";
373
450
  const eventTypePath = options.eventTypePath ?? "type";
451
+ const timestamp = normalizeHmacTimestampOptions(options.timestamp);
374
452
 
375
453
  return {
376
454
  async verify(input) {
@@ -394,6 +472,12 @@ export function createHmacWebhookVerifier(
394
472
  }
395
473
 
396
474
  const payload = parseJsonBody(input.rawBody);
475
+ const createdAt = validateHmacTimestamp({
476
+ input,
477
+ payload,
478
+ provider: options.provider,
479
+ timestamp,
480
+ });
397
481
  const id = readStringPath(payload, eventIdPath);
398
482
  const type = readStringPath(payload, eventTypePath);
399
483
 
@@ -416,6 +500,7 @@ export function createHmacWebhookVerifier(
416
500
  id,
417
501
  type,
418
502
  provider: options.provider,
503
+ ...(createdAt ? { createdAt } : {}),
419
504
  payload,
420
505
  raw: payload,
421
506
  };
@@ -500,7 +585,7 @@ function parseJsonBody(rawBody: WebhookRawBody): unknown {
500
585
  }
501
586
  }
502
587
 
503
- function readStringPath(input: unknown, path: string): string | undefined {
588
+ function readPath(input: unknown, path: string): unknown {
504
589
  let value = input;
505
590
  for (const segment of path.split(".")) {
506
591
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -509,9 +594,155 @@ function readStringPath(input: unknown, path: string): string | undefined {
509
594
  value = (value as Record<string, unknown>)[segment];
510
595
  }
511
596
 
597
+ return value;
598
+ }
599
+
600
+ function readStringPath(input: unknown, path: string): string | undefined {
601
+ const value = readPath(input, path);
512
602
  return typeof value === "string" && value.length > 0 ? value : undefined;
513
603
  }
514
604
 
605
+ type NormalizedHmacTimestampOptions =
606
+ | {
607
+ source: "header";
608
+ key: string;
609
+ format: HmacWebhookTimestampFormat;
610
+ toleranceSec: number;
611
+ }
612
+ | {
613
+ source: "payload";
614
+ path: string;
615
+ format: HmacWebhookTimestampFormat;
616
+ toleranceSec: number;
617
+ };
618
+
619
+ function normalizeHmacTimestampOptions(
620
+ timestamp: HmacWebhookTimestampOptions | undefined,
621
+ ): NormalizedHmacTimestampOptions | undefined {
622
+ if (!timestamp) return undefined;
623
+
624
+ const format = timestamp.format ?? "unix-seconds";
625
+ if (
626
+ format !== "unix-seconds" &&
627
+ format !== "unix-milliseconds" &&
628
+ format !== "iso8601"
629
+ ) {
630
+ throw new WebhookOptionsError(
631
+ "Webhook HMAC timestamp format must be unix-seconds, unix-milliseconds, or iso8601.",
632
+ );
633
+ }
634
+ const toleranceSec = timestamp.toleranceSec ?? 300;
635
+ if (!Number.isInteger(toleranceSec) || toleranceSec <= 0) {
636
+ throw new WebhookOptionsError(
637
+ "Webhook HMAC timestamp toleranceSec must be a positive integer.",
638
+ );
639
+ }
640
+
641
+ if ("header" in timestamp) {
642
+ if (!timestamp.header) {
643
+ throw new WebhookOptionsError(
644
+ "Webhook HMAC timestamp header is required.",
645
+ );
646
+ }
647
+
648
+ return {
649
+ source: "header",
650
+ key: normalizeHeaderName(timestamp.header),
651
+ format,
652
+ toleranceSec,
653
+ };
654
+ }
655
+
656
+ if (!timestamp.payloadPath) {
657
+ throw new WebhookOptionsError(
658
+ "Webhook HMAC timestamp payloadPath is required.",
659
+ );
660
+ }
661
+
662
+ return {
663
+ source: "payload",
664
+ path: timestamp.payloadPath,
665
+ format,
666
+ toleranceSec,
667
+ };
668
+ }
669
+
670
+ function validateHmacTimestamp(args: {
671
+ input: VerifyWebhookInput;
672
+ payload: unknown;
673
+ provider: string | undefined;
674
+ timestamp: NormalizedHmacTimestampOptions | undefined;
675
+ }): Date | undefined {
676
+ const { timestamp } = args;
677
+ if (!timestamp) return undefined;
678
+
679
+ const value =
680
+ timestamp.source === "header"
681
+ ? args.input.headers?.[timestamp.key]
682
+ : readPath(args.payload, timestamp.path);
683
+
684
+ if (value === undefined || value === null || value === "") {
685
+ throw new WebhookVerificationError({
686
+ message:
687
+ timestamp.source === "header"
688
+ ? `Missing ${timestamp.key} header.`
689
+ : `Webhook payload is missing timestamp at "${timestamp.path}".`,
690
+ provider: args.provider,
691
+ code: "missing_timestamp",
692
+ });
693
+ }
694
+
695
+ const createdAt = parseHmacTimestampValue(
696
+ value,
697
+ timestamp.format,
698
+ args.provider,
699
+ );
700
+ const receivedAt = args.input.receivedAt ?? new Date();
701
+ const skewMs = Math.abs(receivedAt.getTime() - createdAt.getTime());
702
+ if (skewMs > timestamp.toleranceSec * 1000) {
703
+ throw new WebhookVerificationError({
704
+ message: `Webhook timestamp is outside the ${timestamp.toleranceSec}s tolerance.`,
705
+ provider: args.provider,
706
+ code: "timestamp_outside_tolerance",
707
+ });
708
+ }
709
+
710
+ return createdAt;
711
+ }
712
+
713
+ function parseHmacTimestampValue(
714
+ value: unknown,
715
+ format: HmacWebhookTimestampFormat,
716
+ provider: string | undefined,
717
+ ): Date {
718
+ const fail = () =>
719
+ new WebhookVerificationError({
720
+ message: "Webhook timestamp is invalid.",
721
+ provider,
722
+ code: "invalid_timestamp",
723
+ });
724
+
725
+ let millis: number;
726
+ if (format === "iso8601") {
727
+ if (typeof value !== "string") throw fail();
728
+ millis = Date.parse(value);
729
+ } else {
730
+ const numeric =
731
+ typeof value === "number"
732
+ ? value
733
+ : typeof value === "string"
734
+ ? Number(value)
735
+ : Number.NaN;
736
+ if (!Number.isFinite(numeric)) throw fail();
737
+ millis = format === "unix-seconds" ? numeric * 1000 : numeric;
738
+ }
739
+
740
+ if (!Number.isFinite(millis)) throw fail();
741
+ const date = new Date(millis);
742
+ if (Number.isNaN(date.getTime())) throw fail();
743
+ return date;
744
+ }
745
+
515
746
  async function hmacHex(
516
747
  algorithm: CreateHmacWebhookVerifierOptions["algorithm"],
517
748
  secret: string,
@@ -1,59 +0,0 @@
1
- import type {
2
- EventDef,
3
- InferEventPayload,
4
- StandardSchema,
5
- } from "../events/index.js";
6
- import { defineEvent } from "../events/index.js";
7
-
8
- /**
9
- * Domain event definition with a stable name and payload schema.
10
- */
11
- export type DomainEventDef<
12
- Name extends string = string,
13
- Payload extends StandardSchema = StandardSchema,
14
- > = EventDef<Name, Payload>;
15
-
16
- /**
17
- * Infer the payload type from a DomainEventDef.
18
- *
19
- * @example
20
- * ```ts
21
- * const UserRegistered = defineDomainEvent(
22
- * "user.registered",
23
- * z.object({
24
- * userId: z.string(),
25
- * email: z.string().email(),
26
- * })
27
- * );
28
- *
29
- * type Payload = InferEventPayload<typeof UserRegistered>;
30
- * // { userId: string; email: string }
31
- * ```
32
- */
33
- export type { InferEventPayload };
34
-
35
- /**
36
- * Create a new domain event definition.
37
- *
38
- * This is a domain-focused alias around `defineEvent(...)` for applications
39
- * that separate domain events from integration events.
40
- *
41
- * @example
42
- * ```ts
43
- * const UserRegistered = defineDomainEvent(
44
- * "user.registered",
45
- * z.object({
46
- * userId: z.string(),
47
- * email: z.string().email(),
48
- * })
49
- * );
50
- *
51
- * type UserRegistered = typeof UserRegistered;
52
- * ```
53
- */
54
- export function defineDomainEvent<
55
- Name extends string,
56
- Payload extends StandardSchema,
57
- >(name: Name, payload: Payload): DomainEventDef<Name, Payload> {
58
- return defineEvent(name, { payload });
59
- }