@openlup/platform 0.16.0 → 0.17.0

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 (296) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +285 -5
  3. package/dist/communications/accountingDocumentDelivery.d.ts +35 -0
  4. package/dist/communications/accountingDocumentDelivery.js +33 -0
  5. package/dist/communications/authEmailError.d.ts +5 -0
  6. package/dist/communications/authEmailError.js +8 -0
  7. package/dist/communications/authEmailPorts.d.ts +2 -0
  8. package/dist/communications/authEmailPorts.js +1 -0
  9. package/dist/communications/authSendEmailHook.d.ts +35 -0
  10. package/dist/communications/authSendEmailHook.js +250 -0
  11. package/dist/communications/capturedTransactionalDelivery.d.ts +13 -0
  12. package/dist/communications/capturedTransactionalDelivery.js +14 -0
  13. package/dist/communications/communicationSync.d.ts +11 -0
  14. package/dist/communications/communicationSync.js +79 -0
  15. package/dist/communications/communicationSyncWorker.d.ts +68 -0
  16. package/dist/communications/communicationSyncWorker.js +218 -0
  17. package/dist/communications/consentPolicy.d.ts +19 -0
  18. package/dist/communications/consentPolicy.js +43 -0
  19. package/dist/communications/consentPurposeRegistry.d.ts +4 -0
  20. package/dist/communications/consentPurposeRegistry.js +24 -0
  21. package/dist/communications/contracts/abandonedCart.d.ts +27 -0
  22. package/dist/communications/contracts/abandonedCart.js +25 -0
  23. package/dist/communications/contracts/authEmail.d.ts +62 -0
  24. package/dist/communications/contracts/authEmail.js +1 -0
  25. package/dist/communications/contracts/backInStock.d.ts +28 -0
  26. package/dist/communications/contracts/backInStock.js +28 -0
  27. package/dist/communications/contracts/blocks.d.ts +24 -0
  28. package/dist/communications/contracts/blocks.js +23 -0
  29. package/dist/communications/contracts/checkoutExpired.d.ts +22 -0
  30. package/dist/communications/contracts/checkoutExpired.js +25 -0
  31. package/dist/communications/contracts/checkoutRecovery.d.ts +26 -0
  32. package/dist/communications/contracts/checkoutRecovery.js +37 -0
  33. package/dist/communications/contracts/commerceEmailContent.d.ts +176 -0
  34. package/dist/communications/contracts/commerceEmailContent.js +1 -0
  35. package/dist/communications/contracts/commerceEngagementEmailContent.d.ts +95 -0
  36. package/dist/communications/contracts/commerceEngagementEmailContent.js +1 -0
  37. package/dist/communications/contracts/consent.d.ts +12 -0
  38. package/dist/communications/contracts/consent.js +1 -0
  39. package/dist/communications/contracts/email.d.ts +182 -0
  40. package/dist/communications/contracts/email.js +1 -0
  41. package/dist/communications/contracts/guideCard.d.ts +3 -0
  42. package/dist/communications/contracts/guideCard.js +26 -0
  43. package/dist/communications/contracts/index.d.ts +51 -0
  44. package/dist/communications/contracts/index.js +42 -0
  45. package/dist/communications/contracts/inquiryContent.d.ts +32 -0
  46. package/dist/communications/contracts/inquiryContent.js +33 -0
  47. package/dist/communications/contracts/invoiceDocumentContent.d.ts +26 -0
  48. package/dist/communications/contracts/invoiceDocumentContent.js +36 -0
  49. package/dist/communications/contracts/marketingAutomation.d.ts +52 -0
  50. package/dist/communications/contracts/marketingAutomation.js +1 -0
  51. package/dist/communications/contracts/newsletter.d.ts +63 -0
  52. package/dist/communications/contracts/newsletter.js +1 -0
  53. package/dist/communications/contracts/newsletterWebhook.d.ts +26 -0
  54. package/dist/communications/contracts/newsletterWebhook.js +1 -0
  55. package/dist/communications/contracts/optOutContent.d.ts +11 -0
  56. package/dist/communications/contracts/optOutContent.js +17 -0
  57. package/dist/communications/contracts/orderCanceled.d.ts +25 -0
  58. package/dist/communications/contracts/orderCanceled.js +27 -0
  59. package/dist/communications/contracts/orderDraft.d.ts +46 -0
  60. package/dist/communications/contracts/orderDraft.js +46 -0
  61. package/dist/communications/contracts/orderPaid.d.ts +53 -0
  62. package/dist/communications/contracts/orderPaid.js +71 -0
  63. package/dist/communications/contracts/orderRefunded.d.ts +24 -0
  64. package/dist/communications/contracts/orderRefunded.js +27 -0
  65. package/dist/communications/contracts/paymentFailed.d.ts +24 -0
  66. package/dist/communications/contracts/paymentFailed.js +24 -0
  67. package/dist/communications/contracts/publicIntake.d.ts +49 -0
  68. package/dist/communications/contracts/publicIntake.js +1 -0
  69. package/dist/communications/contracts/render.d.ts +2 -0
  70. package/dist/communications/contracts/render.js +229 -0
  71. package/dist/communications/contracts/reorderReminder.d.ts +24 -0
  72. package/dist/communications/contracts/reorderReminder.js +24 -0
  73. package/dist/communications/contracts/returnEmails.d.ts +25 -0
  74. package/dist/communications/contracts/returnEmails.js +62 -0
  75. package/dist/communications/contracts/reviewEffects.d.ts +26 -0
  76. package/dist/communications/contracts/reviewEffects.js +25 -0
  77. package/dist/communications/contracts/reviewRequest.d.ts +26 -0
  78. package/dist/communications/contracts/reviewRequest.js +24 -0
  79. package/dist/communications/contracts/roleNotificationContent.d.ts +25 -0
  80. package/dist/communications/contracts/roleNotificationContent.js +46 -0
  81. package/dist/communications/contracts/shipmentDelivered.d.ts +23 -0
  82. package/dist/communications/contracts/shipmentDelivered.js +24 -0
  83. package/dist/communications/contracts/shipmentDispatched.d.ts +27 -0
  84. package/dist/communications/contracts/shipmentDispatched.js +35 -0
  85. package/dist/communications/contracts/shipmentException.d.ts +21 -0
  86. package/dist/communications/contracts/shipmentException.js +26 -0
  87. package/dist/communications/contracts/subscriptionActivationActionRequired.d.ts +17 -0
  88. package/dist/communications/contracts/subscriptionActivationActionRequired.js +22 -0
  89. package/dist/communications/contracts/subscriptionAddressChanged.d.ts +19 -0
  90. package/dist/communications/contracts/subscriptionAddressChanged.js +25 -0
  91. package/dist/communications/contracts/subscriptionCancelled.d.ts +19 -0
  92. package/dist/communications/contracts/subscriptionCancelled.js +25 -0
  93. package/dist/communications/contracts/subscriptionCycleSkipped.d.ts +25 -0
  94. package/dist/communications/contracts/subscriptionCycleSkipped.js +40 -0
  95. package/dist/communications/contracts/subscriptionDeliveryRescheduled.d.ts +25 -0
  96. package/dist/communications/contracts/subscriptionDeliveryRescheduled.js +39 -0
  97. package/dist/communications/contracts/subscriptionEmailContent.d.ts +145 -0
  98. package/dist/communications/contracts/subscriptionEmailContent.js +1 -0
  99. package/dist/communications/contracts/subscriptionPackageChanged.d.ts +20 -0
  100. package/dist/communications/contracts/subscriptionPackageChanged.js +26 -0
  101. package/dist/communications/contracts/subscriptionPauseReminder.d.ts +23 -0
  102. package/dist/communications/contracts/subscriptionPauseReminder.js +29 -0
  103. package/dist/communications/contracts/subscriptionPaused.d.ts +21 -0
  104. package/dist/communications/contracts/subscriptionPaused.js +25 -0
  105. package/dist/communications/contracts/subscriptionPaymentExpired.d.ts +34 -0
  106. package/dist/communications/contracts/subscriptionPaymentExpired.js +37 -0
  107. package/dist/communications/contracts/subscriptionPaymentFailed.d.ts +49 -0
  108. package/dist/communications/contracts/subscriptionPaymentFailed.js +56 -0
  109. package/dist/communications/contracts/subscriptionPaymentRecovered.d.ts +19 -0
  110. package/dist/communications/contracts/subscriptionPaymentRecovered.js +22 -0
  111. package/dist/communications/contracts/subscriptionRenewalAtRisk.d.ts +36 -0
  112. package/dist/communications/contracts/subscriptionRenewalAtRisk.js +36 -0
  113. package/dist/communications/contracts/subscriptionRenewalUpcoming.d.ts +49 -0
  114. package/dist/communications/contracts/subscriptionRenewalUpcoming.js +43 -0
  115. package/dist/communications/contracts/subscriptionResumed.d.ts +23 -0
  116. package/dist/communications/contracts/subscriptionResumed.js +25 -0
  117. package/dist/communications/contracts/subscriptionWelcome.d.ts +44 -0
  118. package/dist/communications/contracts/subscriptionWelcome.js +45 -0
  119. package/dist/communications/contracts/subscriptionWinback.d.ts +21 -0
  120. package/dist/communications/contracts/subscriptionWinback.js +28 -0
  121. package/dist/communications/contracts/transactionalDelivery.d.ts +42 -0
  122. package/dist/communications/contracts/transactionalDelivery.js +1 -0
  123. package/dist/communications/contracts/types.d.ts +46 -0
  124. package/dist/communications/contracts/types.js +1 -0
  125. package/dist/communications/contribution.d.ts +10 -0
  126. package/dist/communications/contribution.js +24 -0
  127. package/dist/communications/deliveryReconciliation.d.ts +64 -0
  128. package/dist/communications/deliveryReconciliation.js +60 -0
  129. package/dist/communications/emailEgress.d.ts +38 -0
  130. package/dist/communications/emailEgress.js +100 -0
  131. package/dist/communications/emailHealthAssessment.d.ts +39 -0
  132. package/dist/communications/emailHealthAssessment.js +59 -0
  133. package/dist/communications/emailHealthMetricsPort.d.ts +17 -0
  134. package/dist/communications/emailHealthMetricsPort.js +1 -0
  135. package/dist/communications/emailNotificationControl.d.ts +27 -0
  136. package/dist/communications/emailNotificationControl.js +10 -0
  137. package/dist/communications/emailObservation.d.ts +28 -0
  138. package/dist/communications/emailObservation.js +23 -0
  139. package/dist/communications/emailTransport.d.ts +82 -0
  140. package/dist/communications/emailTransport.js +95 -0
  141. package/dist/communications/errors.d.ts +15 -0
  142. package/dist/communications/errors.js +30 -0
  143. package/dist/communications/index.d.ts +46 -0
  144. package/dist/communications/index.js +38 -0
  145. package/dist/communications/integrationEventSignature.d.ts +15 -0
  146. package/dist/communications/integrationEventSignature.js +37 -0
  147. package/dist/communications/managedTransactionalDeliveryStore.d.ts +12 -0
  148. package/dist/communications/managedTransactionalDeliveryStore.js +49 -0
  149. package/dist/communications/nativeOperations.d.ts +79 -0
  150. package/dist/communications/nativeOperations.js +3 -0
  151. package/dist/communications/newsletterIntegrationContracts.d.ts +130 -0
  152. package/dist/communications/newsletterIntegrationContracts.js +128 -0
  153. package/dist/communications/newsletterWebhookPort.d.ts +11 -0
  154. package/dist/communications/newsletterWebhookPort.js +84 -0
  155. package/dist/communications/newsletterWebhookProcessing.d.ts +17 -0
  156. package/dist/communications/newsletterWebhookProcessing.js +71 -0
  157. package/dist/communications/originPolicy.d.ts +40 -0
  158. package/dist/communications/originPolicy.js +100 -0
  159. package/dist/communications/postgresCommunicationSync.d.ts +79 -0
  160. package/dist/communications/postgresCommunicationSync.js +111 -0
  161. package/dist/communications/postgresConsentEvaluator.d.ts +19 -0
  162. package/dist/communications/postgresConsentEvaluator.js +43 -0
  163. package/dist/communications/postgresDeliveryReconciliation.d.ts +3 -0
  164. package/dist/communications/postgresDeliveryReconciliation.js +113 -0
  165. package/dist/communications/postgresNativeOperations.d.ts +13 -0
  166. package/dist/communications/postgresNativeOperations.js +63 -0
  167. package/dist/communications/postgresNotificationRecipients.d.ts +37 -0
  168. package/dist/communications/postgresNotificationRecipients.js +60 -0
  169. package/dist/communications/postgresTransactionalDeliveryStore.d.ts +7 -0
  170. package/dist/communications/postgresTransactionalDeliveryStore.js +55 -0
  171. package/dist/communications/preferencesToken.d.ts +10 -0
  172. package/dist/communications/preferencesToken.js +40 -0
  173. package/dist/communications/publicPreferenceToken.d.ts +24 -0
  174. package/dist/communications/publicPreferenceToken.js +53 -0
  175. package/dist/communications/shipmentNotifications.d.ts +34 -0
  176. package/dist/communications/shipmentNotifications.js +61 -0
  177. package/dist/communications/sql.d.ts +26 -0
  178. package/dist/communications/sql.js +7 -0
  179. package/dist/communications/standardWebhooks.d.ts +14 -0
  180. package/dist/communications/standardWebhooks.js +73 -0
  181. package/dist/communications/status.d.ts +11 -0
  182. package/dist/communications/status.js +36 -0
  183. package/dist/communications/transactionalDelivery.d.ts +19 -0
  184. package/dist/communications/transactionalDelivery.js +96 -0
  185. package/dist/communications/transactionalDeliveryIdentifier.d.ts +3 -0
  186. package/dist/communications/transactionalDeliveryIdentifier.js +6 -0
  187. package/dist/communications/unsubscribeToken.d.ts +77 -0
  188. package/dist/communications/unsubscribeToken.js +150 -0
  189. package/dist/communications/validation.d.ts +2 -0
  190. package/dist/communications/validation.js +3 -0
  191. package/dist/http/index.d.ts +1 -0
  192. package/dist/http/index.js +1 -0
  193. package/dist/http/webhookSignature.d.ts +42 -0
  194. package/dist/http/webhookSignature.js +86 -0
  195. package/package.json +12 -2
  196. package/release-gates.json +41 -8
  197. package/sql/20260807190000_comms_outbox_rail.sql +329 -0
  198. package/sql/20260810120001_transactional_delivery_receipts.sql +209 -0
  199. package/sql/20260813120000_communications_control_plane.sql +568 -0
  200. package/sql/20261019200000_communications_native_operations.sql +68 -0
  201. package/src/communications/accountingDocumentDelivery.ts +63 -0
  202. package/src/communications/authEmailError.ts +10 -0
  203. package/src/communications/authEmailPorts.ts +2 -0
  204. package/src/communications/authSendEmailHook.ts +310 -0
  205. package/src/communications/capturedTransactionalDelivery.ts +29 -0
  206. package/src/communications/communicationSync.ts +96 -0
  207. package/src/communications/communicationSyncWorker.ts +302 -0
  208. package/src/communications/consentPolicy.ts +82 -0
  209. package/src/communications/consentPurposeRegistry.ts +31 -0
  210. package/src/communications/contracts/abandonedCart.ts +64 -0
  211. package/src/communications/contracts/authEmail.ts +73 -0
  212. package/src/communications/contracts/backInStock.ts +69 -0
  213. package/src/communications/contracts/blocks.ts +47 -0
  214. package/src/communications/contracts/checkoutExpired.ts +62 -0
  215. package/src/communications/contracts/checkoutRecovery.ts +68 -0
  216. package/src/communications/contracts/commerceEmailContent.ts +189 -0
  217. package/src/communications/contracts/commerceEngagementEmailContent.ts +106 -0
  218. package/src/communications/contracts/consent.ts +13 -0
  219. package/src/communications/contracts/email.ts +210 -0
  220. package/src/communications/contracts/guideCard.ts +34 -0
  221. package/src/communications/contracts/index.ts +51 -0
  222. package/src/communications/contracts/inquiryContent.ts +53 -0
  223. package/src/communications/contracts/invoiceDocumentContent.ts +57 -0
  224. package/src/communications/contracts/marketingAutomation.ts +45 -0
  225. package/src/communications/contracts/newsletter.ts +78 -0
  226. package/src/communications/contracts/newsletterWebhook.ts +23 -0
  227. package/src/communications/contracts/optOutContent.ts +23 -0
  228. package/src/communications/contracts/orderCanceled.ts +64 -0
  229. package/src/communications/contracts/orderDraft.ts +112 -0
  230. package/src/communications/contracts/orderPaid.ts +147 -0
  231. package/src/communications/contracts/orderRefunded.ts +63 -0
  232. package/src/communications/contracts/paymentFailed.ts +59 -0
  233. package/src/communications/contracts/publicIntake.ts +53 -0
  234. package/src/communications/contracts/render.ts +271 -0
  235. package/src/communications/contracts/reorderReminder.ts +59 -0
  236. package/src/communications/contracts/returnEmails.ts +113 -0
  237. package/src/communications/contracts/reviewEffects.ts +60 -0
  238. package/src/communications/contracts/reviewRequest.ts +59 -0
  239. package/src/communications/contracts/roleNotificationContent.ts +65 -0
  240. package/src/communications/contracts/shipmentDelivered.ts +59 -0
  241. package/src/communications/contracts/shipmentDispatched.ts +77 -0
  242. package/src/communications/contracts/shipmentException.ts +62 -0
  243. package/src/communications/contracts/subscriptionActivationActionRequired.ts +37 -0
  244. package/src/communications/contracts/subscriptionAddressChanged.ts +56 -0
  245. package/src/communications/contracts/subscriptionCancelled.ts +57 -0
  246. package/src/communications/contracts/subscriptionCycleSkipped.ts +80 -0
  247. package/src/communications/contracts/subscriptionDeliveryRescheduled.ts +79 -0
  248. package/src/communications/contracts/subscriptionEmailContent.ts +163 -0
  249. package/src/communications/contracts/subscriptionPackageChanged.ts +59 -0
  250. package/src/communications/contracts/subscriptionPauseReminder.ts +63 -0
  251. package/src/communications/contracts/subscriptionPaused.ts +58 -0
  252. package/src/communications/contracts/subscriptionPaymentExpired.ts +83 -0
  253. package/src/communications/contracts/subscriptionPaymentFailed.ts +117 -0
  254. package/src/communications/contracts/subscriptionPaymentRecovered.ts +50 -0
  255. package/src/communications/contracts/subscriptionRenewalAtRisk.ts +85 -0
  256. package/src/communications/contracts/subscriptionRenewalUpcoming.ts +103 -0
  257. package/src/communications/contracts/subscriptionResumed.ts +60 -0
  258. package/src/communications/contracts/subscriptionWelcome.ts +102 -0
  259. package/src/communications/contracts/subscriptionWinback.ts +62 -0
  260. package/src/communications/contracts/transactionalDelivery.ts +43 -0
  261. package/src/communications/contracts/types.ts +58 -0
  262. package/src/communications/contribution.ts +33 -0
  263. package/src/communications/deliveryReconciliation.ts +108 -0
  264. package/src/communications/emailEgress.ts +111 -0
  265. package/src/communications/emailHealthAssessment.ts +106 -0
  266. package/src/communications/emailHealthMetricsPort.ts +17 -0
  267. package/src/communications/emailNotificationControl.ts +28 -0
  268. package/src/communications/emailObservation.ts +43 -0
  269. package/src/communications/emailTransport.ts +167 -0
  270. package/src/communications/errors.ts +34 -0
  271. package/src/communications/index.ts +46 -0
  272. package/src/communications/integrationEventSignature.ts +49 -0
  273. package/src/communications/managedTransactionalDeliveryStore.ts +59 -0
  274. package/src/communications/nativeOperations.ts +40 -0
  275. package/src/communications/newsletterIntegrationContracts.ts +154 -0
  276. package/src/communications/newsletterWebhookPort.ts +104 -0
  277. package/src/communications/newsletterWebhookProcessing.ts +102 -0
  278. package/src/communications/originPolicy.ts +151 -0
  279. package/src/communications/postgresCommunicationSync.ts +127 -0
  280. package/src/communications/postgresConsentEvaluator.ts +49 -0
  281. package/src/communications/postgresDeliveryReconciliation.ts +101 -0
  282. package/src/communications/postgresNativeOperations.ts +55 -0
  283. package/src/communications/postgresNotificationRecipients.ts +57 -0
  284. package/src/communications/postgresTransactionalDeliveryStore.ts +79 -0
  285. package/src/communications/preferencesToken.ts +62 -0
  286. package/src/communications/publicPreferenceToken.ts +68 -0
  287. package/src/communications/shipmentNotifications.ts +78 -0
  288. package/src/communications/sql.ts +7 -0
  289. package/src/communications/standardWebhooks.ts +84 -0
  290. package/src/communications/status.ts +51 -0
  291. package/src/communications/transactionalDelivery.ts +114 -0
  292. package/src/communications/transactionalDeliveryIdentifier.ts +9 -0
  293. package/src/communications/unsubscribeToken.ts +186 -0
  294. package/src/communications/validation.ts +3 -0
  295. package/src/http/index.ts +1 -0
  296. package/src/http/webhookSignature.ts +96 -0
@@ -0,0 +1,26 @@
1
+ /** Sparse projections of canonical units, never an install chain. */
2
+ export declare const COMMUNICATIONS_MIGRATIONS: readonly [{
3
+ readonly id: "20260807190000_comms_outbox_rail";
4
+ readonly catalogPath: "db/platform/migrations/20260807190000_comms_outbox_rail.sql";
5
+ readonly position: 2;
6
+ readonly sha256: "dfb3e9808714846738222bb74a1bfdb38ceafdde2ca8a0c71ab835739fb333bd";
7
+ readonly url: URL;
8
+ }, {
9
+ readonly id: "20260810120001_transactional_delivery_receipts";
10
+ readonly catalogPath: "db/platform/migrations/20260810120001_transactional_delivery_receipts.sql";
11
+ readonly position: 9;
12
+ readonly sha256: "a157f075f8684c29958c565239c19a55894bffaaff4fd80e04ee689c926e33f7";
13
+ readonly url: URL;
14
+ }, {
15
+ readonly id: "20260813120000_communications_control_plane";
16
+ readonly catalogPath: "db/platform/migrations/20260813120000_communications_control_plane.sql";
17
+ readonly position: 20;
18
+ readonly sha256: "dea5fe631f1329272989e1598f6f48aeb6ce67fa070ddeb0200f69f62fcd972a";
19
+ readonly url: URL;
20
+ }, {
21
+ readonly id: "20261019200000_communications_native_operations";
22
+ readonly catalogPath: "db/platform/migrations/20261019200000_communications_native_operations.sql";
23
+ readonly position: 70;
24
+ readonly sha256: "1d28380f12324c3bcccaa5a6d38c22e72332c7992bbb1f90651774dfc5e24982";
25
+ readonly url: URL;
26
+ }];
@@ -0,0 +1,7 @@
1
+ /** Sparse projections of canonical units, never an install chain. */
2
+ export const COMMUNICATIONS_MIGRATIONS = [
3
+ { id: "20260807190000_comms_outbox_rail", catalogPath: "db/platform/migrations/20260807190000_comms_outbox_rail.sql", position: 2, sha256: "dfb3e9808714846738222bb74a1bfdb38ceafdde2ca8a0c71ab835739fb333bd", url: new URL("../../sql/20260807190000_comms_outbox_rail.sql", import.meta.url) },
4
+ { id: "20260810120001_transactional_delivery_receipts", catalogPath: "db/platform/migrations/20260810120001_transactional_delivery_receipts.sql", position: 9, sha256: "a157f075f8684c29958c565239c19a55894bffaaff4fd80e04ee689c926e33f7", url: new URL("../../sql/20260810120001_transactional_delivery_receipts.sql", import.meta.url) },
5
+ { id: "20260813120000_communications_control_plane", catalogPath: "db/platform/migrations/20260813120000_communications_control_plane.sql", position: 20, sha256: "dea5fe631f1329272989e1598f6f48aeb6ce67fa070ddeb0200f69f62fcd972a", url: new URL("../../sql/20260813120000_communications_control_plane.sql", import.meta.url) },
6
+ { id: "20261019200000_communications_native_operations", catalogPath: "db/platform/migrations/20261019200000_communications_native_operations.sql", position: 70, sha256: "1d28380f12324c3bcccaa5a6d38c22e72332c7992bbb1f90651774dfc5e24982", url: new URL("../../sql/20261019200000_communications_native_operations.sql", import.meta.url) },
7
+ ];
@@ -0,0 +1,14 @@
1
+ export interface StandardWebhookHeaders {
2
+ id: string | null;
3
+ timestamp: string | null;
4
+ signature: string | null;
5
+ }
6
+ export declare function timingSafeEqual(a: string, b: string): boolean;
7
+ export declare function standardWebhookKeyBytes(secret: string): Uint8Array;
8
+ export declare function verifyStandardWebhookSignature(opts: {
9
+ headers: StandardWebhookHeaders;
10
+ rawBody: string;
11
+ secret: string;
12
+ toleranceSeconds?: number;
13
+ now?: () => number;
14
+ }): Promise<boolean>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Node-native Standard Webhooks verifier for the inactive Auth email candidate.
3
+ * It intentionally mirrors the deployed Edge algorithm: `id.timestamp.rawBody`
4
+ * is signed with a decoded `whsec_` key and any space-separated v1 signature may
5
+ * match. Keep this independent of deployable Edge packaging.
6
+ */
7
+ import { createHmac } from "node:crypto";
8
+ const DEFAULT_TOLERANCE_SECONDS = 5 * 60;
9
+ export function timingSafeEqual(a, b) {
10
+ if (a.length !== b.length)
11
+ return false;
12
+ let diff = 0;
13
+ for (let index = 0; index < a.length; index += 1)
14
+ diff |= a.charCodeAt(index) ^ b.charCodeAt(index);
15
+ return diff === 0;
16
+ }
17
+ export function standardWebhookKeyBytes(secret) {
18
+ let value = secret.trim();
19
+ if (!value)
20
+ throw new Error("standard_webhook_secret_empty");
21
+ if (value.startsWith("v1,"))
22
+ value = value.slice("v1,".length);
23
+ if (value.startsWith("whsec_")) {
24
+ const decoded = decodeStandardWebhookKey(value.slice("whsec_".length));
25
+ // An empty `whsec_` would make a publicly reproducible empty-key HMAC. It
26
+ // is not a usable Standard Webhooks secret even though it is valid base64.
27
+ if (decoded.length === 0)
28
+ throw new Error("standard_webhook_secret_empty");
29
+ return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
30
+ }
31
+ return new TextEncoder().encode(value);
32
+ }
33
+ function decodeStandardWebhookKey(base64) {
34
+ if (base64.length === 0)
35
+ return "";
36
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64) || base64.length % 4 === 1) {
37
+ throw new Error("standard_webhook_secret_invalid_base64");
38
+ }
39
+ const decoded = atob(base64);
40
+ const canonicalPadded = Buffer.from(decoded, "latin1").toString("base64");
41
+ const canonicalUnpadded = canonicalPadded.replace(/=+$/, "");
42
+ if (base64 !== canonicalPadded && base64 !== canonicalUnpadded) {
43
+ throw new Error("standard_webhook_secret_invalid_base64");
44
+ }
45
+ return decoded;
46
+ }
47
+ async function hmacSha256Base64(keyBytes, message) {
48
+ return createHmac("sha256", Buffer.from(keyBytes)).update(message).digest("base64");
49
+ }
50
+ export async function verifyStandardWebhookSignature(opts) {
51
+ const { headers, rawBody, secret } = opts;
52
+ if (!secret || !headers.id || !headers.timestamp || !headers.signature)
53
+ return false;
54
+ const timestampSeconds = Number(headers.timestamp);
55
+ if (!Number.isFinite(timestampSeconds))
56
+ return false;
57
+ const nowSeconds = Math.floor((opts.now?.() ?? Date.now()) / 1000);
58
+ if (Math.abs(nowSeconds - timestampSeconds) > (opts.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS))
59
+ return false;
60
+ let expected;
61
+ try {
62
+ expected = await hmacSha256Base64(standardWebhookKeyBytes(secret), `${headers.id}.${headers.timestamp}.${rawBody}`);
63
+ }
64
+ catch {
65
+ // A malformed whsec_ value is an invalid verification key, never a reason
66
+ // to bypass signature verification or surface an unhandled request error.
67
+ return false;
68
+ }
69
+ return headers.signature
70
+ .split(" ")
71
+ .map((part) => (part.includes(",") ? part.slice(part.indexOf(",") + 1) : part))
72
+ .some((candidate) => timingSafeEqual(candidate, expected));
73
+ }
@@ -0,0 +1,11 @@
1
+ import type { EmailSendDecision, EmailSendStatus } from "./contracts/types.js";
2
+ export declare function isTerminalEmailSendStatus(status: string | null): status is EmailSendStatus;
3
+ export declare function buildEmailSendDedupeKey(input: {
4
+ recipientId: string;
5
+ templateSlug: string;
6
+ }): string;
7
+ export declare function decideEmailSend(input: {
8
+ sequencePaused: boolean | null;
9
+ existingNonFailedMessageId: string | null;
10
+ templateAvailable: boolean;
11
+ }): EmailSendDecision;
@@ -0,0 +1,36 @@
1
+ const TERMINAL_EMAIL_STATUSES = new Set([
2
+ "bounced",
3
+ "complained",
4
+ "failed",
5
+ "skipped",
6
+ ]);
7
+ export function isTerminalEmailSendStatus(status) {
8
+ return status !== null && TERMINAL_EMAIL_STATUSES.has(status);
9
+ }
10
+ export function buildEmailSendDedupeKey(input) {
11
+ return `email:${input.recipientId.trim()}:${input.templateSlug.trim()}`;
12
+ }
13
+ export function decideEmailSend(input) {
14
+ if (input.sequencePaused) {
15
+ return {
16
+ outcome: "skip",
17
+ reason: "sequence_paused",
18
+ existingMessageId: null,
19
+ };
20
+ }
21
+ if (input.existingNonFailedMessageId) {
22
+ return {
23
+ outcome: "skip",
24
+ reason: "already_sent",
25
+ existingMessageId: input.existingNonFailedMessageId,
26
+ };
27
+ }
28
+ if (!input.templateAvailable) {
29
+ return {
30
+ outcome: "skip",
31
+ reason: "template_unavailable",
32
+ existingMessageId: null,
33
+ };
34
+ }
35
+ return { outcome: "send" };
36
+ }
@@ -0,0 +1,19 @@
1
+ import type { CapturedTransactionalDeliveryPort, TransactionalDeliveryCommand, TransactionalDeliveryReceipt, TransactionalDeliveryReceiptStore } from "./contracts/transactionalDelivery.js";
2
+ export declare class TransactionalDeliveryValidationError extends Error {
3
+ constructor();
4
+ }
5
+ export declare class TransactionalDeliveryConflictError extends Error {
6
+ constructor();
7
+ }
8
+ export declare class TransactionalDeliveryUnavailableError extends Error {
9
+ constructor();
10
+ }
11
+ /** One direct command, one captured delivery attempt, and one durable receipt transition. */
12
+ export declare function createTransactionalDelivery(deps: {
13
+ store: TransactionalDeliveryReceiptStore;
14
+ delivery: CapturedTransactionalDeliveryPort;
15
+ }, options?: {
16
+ fingerprint?: (command: TransactionalDeliveryCommand) => string;
17
+ }): {
18
+ send(command: TransactionalDeliveryCommand): Promise<TransactionalDeliveryReceipt>;
19
+ };
@@ -0,0 +1,96 @@
1
+ import { isTransactionalDeliveryIdentifier } from "./transactionalDeliveryIdentifier.js";
2
+ import { createHash } from "node:crypto";
3
+ const FINGERPRINT = /^[a-f0-9]{64}$/;
4
+ const FAILURE_CODE = /^[a-z][a-z0-9_.-]{0,127}$/;
5
+ export class TransactionalDeliveryValidationError extends Error {
6
+ constructor() {
7
+ super("transactional_delivery_command_invalid");
8
+ }
9
+ }
10
+ export class TransactionalDeliveryConflictError extends Error {
11
+ constructor() {
12
+ super("transactional_delivery_command_conflict");
13
+ }
14
+ }
15
+ export class TransactionalDeliveryUnavailableError extends Error {
16
+ constructor() {
17
+ super("transactional_delivery_unavailable");
18
+ }
19
+ }
20
+ function fingerprint(command) {
21
+ return createHash("sha256")
22
+ .update(JSON.stringify({
23
+ recipientReference: command.recipientReference,
24
+ templateReference: command.templateReference,
25
+ accessGrantReference: command.accessGrantReference ?? null,
26
+ }))
27
+ .digest("hex");
28
+ }
29
+ function validate(command) {
30
+ if (!isTransactionalDeliveryIdentifier(command.idempotencyKey)
31
+ || !isTransactionalDeliveryIdentifier(command.recipientReference)
32
+ || !isTransactionalDeliveryIdentifier(command.templateReference)
33
+ || (command.accessGrantReference !== undefined
34
+ && !isTransactionalDeliveryIdentifier(command.accessGrantReference))) {
35
+ throw new TransactionalDeliveryValidationError();
36
+ }
37
+ }
38
+ function accepted(receipt, idempotencyKey, commandFingerprint) {
39
+ if (receipt.idempotencyKey !== idempotencyKey || receipt.commandFingerprint !== commandFingerprint
40
+ || receipt.state !== "accepted" || !isTransactionalDeliveryIdentifier(receipt.deliveryReference)
41
+ || !Number.isInteger(receipt.attemptCount) || receipt.attemptCount < 1) {
42
+ throw new Error("transactional_delivery_receipt_invalid");
43
+ }
44
+ return receipt;
45
+ }
46
+ /** One direct command, one captured delivery attempt, and one durable receipt transition. */
47
+ export function createTransactionalDelivery(deps, options = {}) {
48
+ const commandFingerprint = options.fingerprint ?? fingerprint;
49
+ return {
50
+ async send(command) {
51
+ validate(command);
52
+ const digest = commandFingerprint(command);
53
+ if (!FINGERPRINT.test(digest))
54
+ throw new Error("transactional_delivery_fingerprint_invalid");
55
+ const prior = await deps.store.readReceipt(command.idempotencyKey);
56
+ if (prior) {
57
+ if (prior.commandFingerprint !== digest)
58
+ throw new TransactionalDeliveryConflictError();
59
+ if (prior.state === "accepted")
60
+ return accepted(prior, command.idempotencyKey, digest);
61
+ }
62
+ try {
63
+ const { deliveryReference } = await deps.delivery.deliver(command);
64
+ if (!isTransactionalDeliveryIdentifier(deliveryReference))
65
+ throw new Error("captured_delivery_reference_invalid");
66
+ try {
67
+ return accepted(await deps.store.recordAccepted({
68
+ idempotencyKey: command.idempotencyKey,
69
+ commandFingerprint: digest,
70
+ deliveryReference,
71
+ }), command.idempotencyKey, digest);
72
+ }
73
+ catch {
74
+ // Delivery may already have happened. Never turn an accepted-write
75
+ // failure into a durable failed receipt for the same command.
76
+ throw new TransactionalDeliveryUnavailableError();
77
+ }
78
+ }
79
+ catch (error) {
80
+ if (error instanceof TransactionalDeliveryUnavailableError)
81
+ throw error;
82
+ try {
83
+ await deps.store.recordFailed({
84
+ idempotencyKey: command.idempotencyKey,
85
+ commandFingerprint: digest,
86
+ errorCode: "captured_delivery_failed",
87
+ });
88
+ }
89
+ catch {
90
+ // The caller still receives the same sanitized unavailable result.
91
+ }
92
+ throw new TransactionalDeliveryUnavailableError();
93
+ }
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,3 @@
1
+ /** Narrow, portable receipt contract for the captured transactional action. */
2
+ /** Bounded opaque identifiers keep commands and persisted evidence content-free. */
3
+ export declare function isTransactionalDeliveryIdentifier(value: string): boolean;
@@ -0,0 +1,6 @@
1
+ /** Narrow, portable receipt contract for the captured transactional action. */
2
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
3
+ /** Bounded opaque identifiers keep commands and persisted evidence content-free. */
4
+ export function isTransactionalDeliveryIdentifier(value) {
5
+ return IDENTIFIER.test(value);
6
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Stateless, signed unsubscribe tokens for marketing emails (RODO one-click
3
+ * opt-out): an HMAC-SHA256 over the recipient email + consent purpose, so the
4
+ * unsubscribe route can suppress the right permission without a lookup table or
5
+ * trusting raw query params. Node marketing senders build the token to embed in
6
+ * the email footer; `api/unsubscribe.ts` verifies it.
7
+ *
8
+ * This module is the only implementation. The former Deno mirror was deleted
9
+ * with the `unsubscribe` Edge function; nothing has to be kept in sync any more.
10
+ *
11
+ * WebCrypto only (global `crypto.subtle`/`btoa`/`atob`, Node 18+).
12
+ */
13
+ export interface UnsubscribePayload {
14
+ email: string;
15
+ purpose: string;
16
+ }
17
+ /**
18
+ * How long a newly minted unsubscribe link stays valid: 548 days, i.e. about
19
+ * eighteen months, expressed in whole days so the value does not depend on which
20
+ * months a token happens to span.
21
+ *
22
+ * Contract, and why this number:
23
+ *
24
+ * - Until this field existed the token carried `{e, p, v}` and the verifier
25
+ * checked only the HMAC, so a link minted in 2026 stayed valid forever. That
26
+ * made "wait for the old links to age out" an unbounded strategy and left a
27
+ * permanently replayable capability sitting in mailboxes.
28
+ * - Eighteen months is longer than a marketing message is realistically acted
29
+ * on and longer than a full annual cycle plus a lapsed re-engagement, so it
30
+ * does not cut short a genuine opt-out; it is short enough that the capability
31
+ * is not effectively perpetual.
32
+ * - ⛔ A token WITHOUT `exp` stays valid. Every link already delivered was
33
+ * minted without one, and refusing them would retro-expire opt-outs the
34
+ * recipient was told would work. Expiry is a forward-only narrowing.
35
+ *
36
+ * ⛔ Do not rename this to anything containing `TOKEN`, `SECRET`, `PASSWORD`,
37
+ * `SERVICE_ROLE` or `API_KEY`. This module is client-reachable `src/` source and
38
+ * `scripts/check-client-secret-boundary.ts` refuses a SCREAMING_SNAKE
39
+ * identifier carrying any of those words there — a TTL is not a credential, so
40
+ * the name avoids the shape rather than the allowlist.
41
+ */
42
+ export declare const UNSUBSCRIBE_LINK_TTL_SECONDS: number;
43
+ /** Clock seam, so expiry can be proved without waiting eighteen months. */
44
+ export interface UnsubscribeTokenClock {
45
+ now?: Date;
46
+ }
47
+ /**
48
+ * Build a `<base64url(payload)>.<base64url(sig)>` unsubscribe token.
49
+ *
50
+ * Quoted-printable safety: this token rides as `?token=…` inside marketing emails
51
+ * whose Polish diacritics force quoted-printable encoding. A token beginning with
52
+ * two hex digits could be corrupted if a transport leaves the `=` separator raw
53
+ * (`=7d` -> `}`). This token is structurally immune: the payload always serializes
54
+ * as `{"e":…`, so base64url makes the token always begin "ey" (`e` is hex, `y` is
55
+ * not) — `=ey` is never a valid `=<2 hex>` escape. unsubscribeToken.test.ts guards
56
+ * this; do NOT reshape the payload to lead with a field that could base64url into
57
+ * two hex digits without re-checking (and prefixing the token if so). `exp` is
58
+ * appended last for exactly that reason.
59
+ */
60
+ export declare function buildUnsubscribeToken(payload: UnsubscribePayload, secret: string, clock?: UnsubscribeTokenClock): Promise<string>;
61
+ /**
62
+ * Add the signed query to a resolved unsubscribe endpoint. The adapter that
63
+ * owns deployment/provider configuration must choose and validate that endpoint
64
+ * before it reaches this neutral communications-domain helper.
65
+ */
66
+ export declare function buildUnsubscribeUrl(unsubscribeEndpointUrl: string, token: string, locale: "pl" | "en"): string;
67
+ /**
68
+ * Verify a token; returns the payload or null on any tampering / bad shape /
69
+ * elapsed expiry.
70
+ *
71
+ * ⛔ An absent `exp` is valid, not a rejection: tokens minted before expiry
72
+ * existed are still in mailboxes, and a recipient who was promised a working
73
+ * opt-out link must keep one. A present `exp` must be a finite number and must
74
+ * still be in the future — it is inside the signed payload, so it cannot be
75
+ * stripped or moved without breaking the HMAC.
76
+ */
77
+ export declare function verifyUnsubscribeToken(token: string, secret: string, clock?: UnsubscribeTokenClock): Promise<UnsubscribePayload | null>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Stateless, signed unsubscribe tokens for marketing emails (RODO one-click
3
+ * opt-out): an HMAC-SHA256 over the recipient email + consent purpose, so the
4
+ * unsubscribe route can suppress the right permission without a lookup table or
5
+ * trusting raw query params. Node marketing senders build the token to embed in
6
+ * the email footer; `api/unsubscribe.ts` verifies it.
7
+ *
8
+ * This module is the only implementation. The former Deno mirror was deleted
9
+ * with the `unsubscribe` Edge function; nothing has to be kept in sync any more.
10
+ *
11
+ * WebCrypto only (global `crypto.subtle`/`btoa`/`atob`, Node 18+).
12
+ */
13
+ /**
14
+ * How long a newly minted unsubscribe link stays valid: 548 days, i.e. about
15
+ * eighteen months, expressed in whole days so the value does not depend on which
16
+ * months a token happens to span.
17
+ *
18
+ * Contract, and why this number:
19
+ *
20
+ * - Until this field existed the token carried `{e, p, v}` and the verifier
21
+ * checked only the HMAC, so a link minted in 2026 stayed valid forever. That
22
+ * made "wait for the old links to age out" an unbounded strategy and left a
23
+ * permanently replayable capability sitting in mailboxes.
24
+ * - Eighteen months is longer than a marketing message is realistically acted
25
+ * on and longer than a full annual cycle plus a lapsed re-engagement, so it
26
+ * does not cut short a genuine opt-out; it is short enough that the capability
27
+ * is not effectively perpetual.
28
+ * - ⛔ A token WITHOUT `exp` stays valid. Every link already delivered was
29
+ * minted without one, and refusing them would retro-expire opt-outs the
30
+ * recipient was told would work. Expiry is a forward-only narrowing.
31
+ *
32
+ * ⛔ Do not rename this to anything containing `TOKEN`, `SECRET`, `PASSWORD`,
33
+ * `SERVICE_ROLE` or `API_KEY`. This module is client-reachable `src/` source and
34
+ * `scripts/check-client-secret-boundary.ts` refuses a SCREAMING_SNAKE
35
+ * identifier carrying any of those words there — a TTL is not a credential, so
36
+ * the name avoids the shape rather than the allowlist.
37
+ */
38
+ export const UNSUBSCRIBE_LINK_TTL_SECONDS = 548 * 24 * 60 * 60;
39
+ function base64urlEncode(bytes) {
40
+ let binary = "";
41
+ for (const b of bytes)
42
+ binary += String.fromCharCode(b);
43
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
44
+ }
45
+ function base64urlDecodeToString(value) {
46
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
47
+ return atob(padded);
48
+ }
49
+ function timingSafeEqual(a, b) {
50
+ if (a.length !== b.length)
51
+ return false;
52
+ let diff = 0;
53
+ for (let i = 0; i < a.length; i += 1)
54
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
55
+ return diff === 0;
56
+ }
57
+ async function hmacBase64url(secret, message) {
58
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
59
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
60
+ return base64urlEncode(new Uint8Array(sig));
61
+ }
62
+ /**
63
+ * Build a `<base64url(payload)>.<base64url(sig)>` unsubscribe token.
64
+ *
65
+ * Quoted-printable safety: this token rides as `?token=…` inside marketing emails
66
+ * whose Polish diacritics force quoted-printable encoding. A token beginning with
67
+ * two hex digits could be corrupted if a transport leaves the `=` separator raw
68
+ * (`=7d` -> `}`). This token is structurally immune: the payload always serializes
69
+ * as `{"e":…`, so base64url makes the token always begin "ey" (`e` is hex, `y` is
70
+ * not) — `=ey` is never a valid `=<2 hex>` escape. unsubscribeToken.test.ts guards
71
+ * this; do NOT reshape the payload to lead with a field that could base64url into
72
+ * two hex digits without re-checking (and prefixing the token if so). `exp` is
73
+ * appended last for exactly that reason.
74
+ */
75
+ export async function buildUnsubscribeToken(payload, secret, clock = {}) {
76
+ const issuedAtSeconds = Math.floor((clock.now ?? new Date()).getTime() / 1000);
77
+ const body = base64urlEncode(new TextEncoder().encode(JSON.stringify({
78
+ e: payload.email,
79
+ p: payload.purpose,
80
+ v: 1,
81
+ exp: issuedAtSeconds + UNSUBSCRIBE_LINK_TTL_SECONDS,
82
+ })));
83
+ const sig = await hmacBase64url(secret, body);
84
+ return `${body}.${sig}`;
85
+ }
86
+ /**
87
+ * Add the signed query to a resolved unsubscribe endpoint. The adapter that
88
+ * owns deployment/provider configuration must choose and validate that endpoint
89
+ * before it reaches this neutral communications-domain helper.
90
+ */
91
+ export function buildUnsubscribeUrl(unsubscribeEndpointUrl, token, locale) {
92
+ const endpoint = parseUnsubscribeEndpointUrl(unsubscribeEndpointUrl);
93
+ const params = new URLSearchParams({ token });
94
+ if (locale === "en")
95
+ params.set("lang", "en");
96
+ endpoint.search = `?${params.toString()}`;
97
+ return endpoint.toString();
98
+ }
99
+ function parseUnsubscribeEndpointUrl(value) {
100
+ const endpoint = new URL(value);
101
+ if (endpoint.protocol !== "https:" ||
102
+ endpoint.username ||
103
+ endpoint.password ||
104
+ endpoint.search ||
105
+ endpoint.hash ||
106
+ endpoint.hostname.endsWith(".")) {
107
+ throw new Error("unsubscribe endpoint URL must be HTTPS without credentials, query, fragment, or a trailing-dot hostname");
108
+ }
109
+ return endpoint;
110
+ }
111
+ /**
112
+ * Verify a token; returns the payload or null on any tampering / bad shape /
113
+ * elapsed expiry.
114
+ *
115
+ * ⛔ An absent `exp` is valid, not a rejection: tokens minted before expiry
116
+ * existed are still in mailboxes, and a recipient who was promised a working
117
+ * opt-out link must keep one. A present `exp` must be a finite number and must
118
+ * still be in the future — it is inside the signed payload, so it cannot be
119
+ * stripped or moved without breaking the HMAC.
120
+ */
121
+ export async function verifyUnsubscribeToken(token, secret, clock = {}) {
122
+ if (!token || !secret)
123
+ return null;
124
+ const dot = token.indexOf(".");
125
+ if (dot <= 0)
126
+ return null;
127
+ const body = token.slice(0, dot);
128
+ const sig = token.slice(dot + 1);
129
+ const expected = await hmacBase64url(secret, body);
130
+ if (!timingSafeEqual(sig, expected))
131
+ return null;
132
+ try {
133
+ const parsed = JSON.parse(base64urlDecodeToString(body));
134
+ if (typeof parsed.e !== "string" || typeof parsed.p !== "string")
135
+ return null;
136
+ if (!parsed.e || !parsed.p)
137
+ return null;
138
+ if (parsed.exp !== undefined) {
139
+ if (typeof parsed.exp !== "number" || !Number.isFinite(parsed.exp))
140
+ return null;
141
+ const nowSeconds = Math.floor((clock.now ?? new Date()).getTime() / 1000);
142
+ if (parsed.exp <= nowSeconds)
143
+ return null;
144
+ }
145
+ return { email: parsed.e, purpose: parsed.p };
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
@@ -0,0 +1,2 @@
1
+ import { z } from "zod";
2
+ export { z };
@@ -0,0 +1,3 @@
1
+ import { z } from "zod";
2
+ z.config({ jitless: true });
3
+ export { z };
@@ -4,3 +4,4 @@ export { NODE_HTTP_TIMEOUT_CODE, closeInputAfterResponse, runWithHttpDeadline, t
4
4
  export { applySecurityHeaders, contentTypeFor, looksLikeAsset, serveSpaStatic, type NodeSecurityHeaderPolicy, type StaticAssetReader, } from "./security.js";
5
5
  export { createDistStaticReader } from "./staticAssets.js";
6
6
  export { createNodeHttpRuntime, createRequestHandler, type HttpEntrypointRegistration, type HttpPathEntrypoint, type NodeCsrFallback, type NodeHttpRuntimeOptions, type NodeLegacyRedirect, } from "./runtime.js";
7
+ export { MAX_WEBHOOK_BODY_BYTES, WebhookBodyTooLargeError, readRawBody, verifyHmacSignature, timingSafeStringEqual } from "./webhookSignature.js";
@@ -3,3 +3,4 @@ export { NODE_HTTP_TIMEOUT_CODE, closeInputAfterResponse, runWithHttpDeadline, }
3
3
  export { applySecurityHeaders, contentTypeFor, looksLikeAsset, serveSpaStatic, } from "./security.js";
4
4
  export { createDistStaticReader } from "./staticAssets.js";
5
5
  export { createNodeHttpRuntime, createRequestHandler, } from "./runtime.js";
6
+ export { MAX_WEBHOOK_BODY_BYTES, WebhookBodyTooLargeError, readRawBody, verifyHmacSignature, timingSafeStringEqual } from "./webhookSignature.js";
@@ -0,0 +1,42 @@
1
+ import type { HttpRequest } from "./types.js";
2
+ /** Hard cap on raw webhook body bytes — the payment provider events are well under 256 KiB. */
3
+ export declare const MAX_WEBHOOK_BODY_BYTES = 1048576;
4
+ /** Thrown by readRawBody when the streamed body exceeds MAX_WEBHOOK_BODY_BYTES. */
5
+ export declare class WebhookBodyTooLargeError extends Error {
6
+ constructor();
7
+ }
8
+ /**
9
+ * Reads the exact raw request body bytes for provider webhook signature
10
+ * verification.
11
+ *
12
+ * `@vercel/node` (v3, the builder for `/api` functions in this Vite project)
13
+ * auto-parses `application/json` request bodies into `req.body` and IGNORES the
14
+ * `export const config = { api: { bodyParser: false } }` opt-out — that opt-out
15
+ * is a Next.js Pages Router convention, not a vanilla `@vercel/node` one
16
+ * (verified empirically against the hidden-preview deploy: a JSON payload posted
17
+ * with `Content-Type: application/json` arrives parsed, the same bytes posted as
18
+ * `text/plain` arrive raw). Re-serializing the parsed object with
19
+ * `JSON.stringify` does NOT reproduce the provider's exact bytes — the payment provider signs
20
+ * pretty-printed JSON, so a compacted re-serialization breaks HMAC verification.
21
+ *
22
+ * Reading the underlying request stream FIRST — before touching `req.body`,
23
+ * whose getter triggers the parse that consumes the stream — preserves the exact
24
+ * bytes when the platform parses lazily. When `req` is not a stream (unit tests
25
+ * pass a plain `{ body }` object) or the stream yields nothing (already
26
+ * consumed), we fall back to `req.body`.
27
+ */
28
+ export declare function readRawBody(req: HttpRequest): Promise<string>;
29
+ export declare function verifyHmacSignature(input: {
30
+ rawBody: string;
31
+ signatureHeader: string | undefined;
32
+ secret: string;
33
+ }): boolean;
34
+ /**
35
+ * Constant-time comparison of two shared-secret strings.
36
+ *
37
+ * Exported because it is not payment-specific: any route holding a shared token has to compare it
38
+ * the same way, and a second hand-rolled copy of a security primitive is how one of them quietly
39
+ * stops being constant-time. The length check comes first because `timingSafeEqual` THROWS on a
40
+ * length mismatch rather than returning false, and the length of a shared token is not the secret.
41
+ */
42
+ export declare function timingSafeStringEqual(left: string, right: string): boolean;