@openlup/platform 0.15.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 (531) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +633 -6
  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/dist/identity/adminAuthBinding.d.ts +28 -0
  196. package/dist/identity/adminAuthBinding.js +66 -0
  197. package/dist/identity/adminAuthorization.d.ts +34 -0
  198. package/dist/identity/adminAuthorization.js +72 -0
  199. package/dist/identity/adminContracts.d.ts +70 -0
  200. package/dist/identity/adminContracts.js +43 -0
  201. package/dist/identity/adminMagicLinkHandler.d.ts +39 -0
  202. package/dist/identity/adminMagicLinkHandler.js +94 -0
  203. package/dist/identity/adminMeHandler.d.ts +15 -0
  204. package/dist/identity/adminMeHandler.js +49 -0
  205. package/dist/identity/adminPreflight.d.ts +16 -0
  206. package/dist/identity/adminPreflight.js +38 -0
  207. package/dist/identity/adminUserActionPorts.d.ts +31 -0
  208. package/dist/identity/adminUserActionPorts.js +51 -0
  209. package/dist/identity/adminUserHandlers.d.ts +38 -0
  210. package/dist/identity/adminUserHandlers.js +140 -0
  211. package/dist/identity/authVerifyUrl.d.ts +6 -0
  212. package/dist/identity/authVerifyUrl.js +54 -0
  213. package/dist/identity/contracts/contracts.d.ts +8 -0
  214. package/dist/identity/contracts/contracts.js +2 -0
  215. package/dist/identity/contracts/emailTrust.d.ts +3 -0
  216. package/dist/identity/contracts/emailTrust.js +4 -0
  217. package/dist/identity/contracts/index.d.ts +8 -0
  218. package/dist/identity/contracts/index.js +8 -0
  219. package/dist/identity/contracts/oidc.d.ts +3 -0
  220. package/dist/identity/contracts/oidc.js +9 -0
  221. package/dist/identity/contracts/operatorAuth.d.ts +35 -0
  222. package/dist/identity/contracts/operatorAuth.js +53 -0
  223. package/dist/identity/contracts/operatorInvitations.d.ts +37 -0
  224. package/dist/identity/contracts/operatorInvitations.js +1 -0
  225. package/dist/identity/contracts/ports.d.ts +22 -0
  226. package/dist/identity/contracts/ports.js +1 -0
  227. package/dist/identity/contracts/privacy.d.ts +62 -0
  228. package/dist/identity/contracts/privacy.js +99 -0
  229. package/dist/identity/contracts/providerList.d.ts +3 -0
  230. package/dist/identity/contracts/providerList.js +11 -0
  231. package/dist/identity/contracts/types.d.ts +16 -0
  232. package/dist/identity/contracts/types.js +4 -0
  233. package/dist/identity/contribution.d.ts +8 -0
  234. package/dist/identity/contribution.js +22 -0
  235. package/dist/identity/customerAuth.d.ts +8 -0
  236. package/dist/identity/customerAuth.js +1 -0
  237. package/dist/identity/customerAuthorization.d.ts +5 -0
  238. package/dist/identity/customerAuthorization.js +22 -0
  239. package/dist/identity/customerContracts.d.ts +42 -0
  240. package/dist/identity/customerContracts.js +43 -0
  241. package/dist/identity/customerIdentityBinding.d.ts +44 -0
  242. package/dist/identity/customerIdentityBinding.js +60 -0
  243. package/dist/identity/customerIdentityVerifier.d.ts +8 -0
  244. package/dist/identity/customerIdentityVerifier.js +101 -0
  245. package/dist/identity/customerMagicLinkHandler.d.ts +38 -0
  246. package/dist/identity/customerMagicLinkHandler.js +115 -0
  247. package/dist/identity/customerMeHandler.d.ts +11 -0
  248. package/dist/identity/customerMeHandler.js +46 -0
  249. package/dist/identity/customerPorts.d.ts +4 -0
  250. package/dist/identity/customerPorts.js +1 -0
  251. package/dist/identity/customerSessionBinding.d.ts +38 -0
  252. package/dist/identity/customerSessionBinding.js +80 -0
  253. package/dist/identity/customerSessionIssuer.d.ts +10 -0
  254. package/dist/identity/customerSessionIssuer.js +57 -0
  255. package/dist/identity/customerVerifyOtpHandler.d.ts +8 -0
  256. package/dist/identity/customerVerifyOtpHandler.js +41 -0
  257. package/dist/identity/directOperatorInvitationStore.d.ts +19 -0
  258. package/dist/identity/directOperatorInvitationStore.js +193 -0
  259. package/dist/identity/emailTrustPolicy.d.ts +14 -0
  260. package/dist/identity/emailTrustPolicy.js +16 -0
  261. package/dist/identity/identityPersonalDataContributor.d.ts +7 -0
  262. package/dist/identity/identityPersonalDataContributor.js +46 -0
  263. package/dist/identity/identityReadiness.d.ts +10 -0
  264. package/dist/identity/identityReadiness.js +25 -0
  265. package/dist/identity/index.d.ts +67 -0
  266. package/dist/identity/index.js +66 -0
  267. package/dist/identity/inviteAdminUserUseCase.d.ts +89 -0
  268. package/dist/identity/inviteAdminUserUseCase.js +75 -0
  269. package/dist/identity/jwtLibrary.d.ts +54 -0
  270. package/dist/identity/jwtLibrary.js +1 -0
  271. package/dist/identity/machineActorWriteFence.d.ts +4 -0
  272. package/dist/identity/machineActorWriteFence.js +13 -0
  273. package/dist/identity/magicLinkPorts.d.ts +28 -0
  274. package/dist/identity/magicLinkPorts.js +1 -0
  275. package/dist/identity/managedAccountLinkStore.d.ts +2 -0
  276. package/dist/identity/managedAccountLinkStore.js +93 -0
  277. package/dist/identity/managedAdminAuthVerifier.d.ts +7 -0
  278. package/dist/identity/managedAdminAuthVerifier.js +27 -0
  279. package/dist/identity/managedAdminUserGateways.d.ts +123 -0
  280. package/dist/identity/managedAdminUserGateways.js +127 -0
  281. package/dist/identity/managedClient.d.ts +90 -0
  282. package/dist/identity/managedClient.js +10 -0
  283. package/dist/identity/managedClientAuthLink.d.ts +3 -0
  284. package/dist/identity/managedClientAuthLink.js +115 -0
  285. package/dist/identity/managedCustomerMe.d.ts +13 -0
  286. package/dist/identity/managedCustomerMe.js +30 -0
  287. package/dist/identity/managedEmailTrustPolicy.d.ts +11 -0
  288. package/dist/identity/managedEmailTrustPolicy.js +105 -0
  289. package/dist/identity/managedIdentityVerifier.d.ts +2 -0
  290. package/dist/identity/managedIdentityVerifier.js +16 -0
  291. package/dist/identity/managedPrivacy.d.ts +4 -0
  292. package/dist/identity/managedPrivacy.js +39 -0
  293. package/dist/identity/messages.d.ts +29 -0
  294. package/dist/identity/messages.js +39 -0
  295. package/dist/identity/oidcIdentityVerifier.d.ts +45 -0
  296. package/dist/identity/oidcIdentityVerifier.js +309 -0
  297. package/dist/identity/oidcPrincipal.d.ts +2 -0
  298. package/dist/identity/oidcPrincipal.js +10 -0
  299. package/dist/identity/operatorAdminAuth.d.ts +6 -0
  300. package/dist/identity/operatorAdminAuth.js +13 -0
  301. package/dist/identity/operatorAuthHandlers.d.ts +18 -0
  302. package/dist/identity/operatorAuthHandlers.js +71 -0
  303. package/dist/identity/operatorIdentityVerifier.d.ts +8 -0
  304. package/dist/identity/operatorIdentityVerifier.js +19 -0
  305. package/dist/identity/operatorInvitationLifecycle.d.ts +7 -0
  306. package/dist/identity/operatorInvitationLifecycle.js +15 -0
  307. package/dist/identity/operatorInvitationPorts.d.ts +31 -0
  308. package/dist/identity/operatorInvitationPorts.js +1 -0
  309. package/dist/identity/operatorPorts.d.ts +37 -0
  310. package/dist/identity/operatorPorts.js +1 -0
  311. package/dist/identity/operatorSession.d.ts +13 -0
  312. package/dist/identity/operatorSession.js +31 -0
  313. package/dist/identity/operatorTokenAdminAuth.d.ts +15 -0
  314. package/dist/identity/operatorTokenAdminAuth.js +50 -0
  315. package/dist/identity/personalDataRegistry.d.ts +4 -0
  316. package/dist/identity/personalDataRegistry.js +11 -0
  317. package/dist/identity/platformAdminAuth.d.ts +15 -0
  318. package/dist/identity/platformAdminAuth.js +1 -0
  319. package/dist/identity/ports.d.ts +127 -0
  320. package/dist/identity/ports.js +38 -0
  321. package/dist/identity/postgresAccountLinkStore.d.ts +7 -0
  322. package/dist/identity/postgresAccountLinkStore.js +52 -0
  323. package/dist/identity/postgresCustomerMe.d.ts +3 -0
  324. package/dist/identity/postgresCustomerMe.js +21 -0
  325. package/dist/identity/postgresCustomerRecovery.d.ts +11 -0
  326. package/dist/identity/postgresCustomerRecovery.js +57 -0
  327. package/dist/identity/postgresOperatorIdentity.d.ts +5 -0
  328. package/dist/identity/postgresOperatorIdentity.js +137 -0
  329. package/dist/identity/postgresPrivacy.d.ts +7 -0
  330. package/dist/identity/postgresPrivacy.js +126 -0
  331. package/dist/identity/privacyHandlers.d.ts +10 -0
  332. package/dist/identity/privacyHandlers.js +35 -0
  333. package/dist/identity/privacyPorts.d.ts +72 -0
  334. package/dist/identity/privacyPorts.js +8 -0
  335. package/dist/identity/privacyUseCases.d.ts +5 -0
  336. package/dist/identity/privacyUseCases.js +55 -0
  337. package/dist/identity/reconcileAccountForPrincipal.d.ts +20 -0
  338. package/dist/identity/reconcileAccountForPrincipal.js +66 -0
  339. package/dist/identity/reconcileAccountHandler.d.ts +11 -0
  340. package/dist/identity/reconcileAccountHandler.js +30 -0
  341. package/dist/identity/removeAdminUserUseCase.d.ts +31 -0
  342. package/dist/identity/removeAdminUserUseCase.js +60 -0
  343. package/dist/identity/response.d.ts +3 -0
  344. package/dist/identity/response.js +2 -0
  345. package/dist/identity/sql.d.ts +44 -0
  346. package/dist/identity/sql.js +10 -0
  347. package/package.json +22 -2
  348. package/release-gates.json +63 -8
  349. package/sql/20260807190000_comms_outbox_rail.sql +329 -0
  350. package/sql/20260810120001_transactional_delivery_receipts.sql +209 -0
  351. package/sql/20260810180001_self_host_actor_auth.sql +40 -0
  352. package/sql/20260810204057_customer_identity_read.sql +31 -0
  353. package/sql/20260813120000_communications_control_plane.sql +568 -0
  354. package/sql/20260814090000_customer_selfservice_recovery.sql +477 -0
  355. package/sql/20260901143000_admin_membership_authority_foundation.sql +234 -0
  356. package/sql/20261018120000_identity_operator_invitations.sql +42 -0
  357. package/sql/20261018180000_identity_operator_challenges.sql +56 -0
  358. package/sql/20261018190000_identity_privacy_erasures.sql +15 -0
  359. package/sql/20261019200000_communications_native_operations.sql +68 -0
  360. package/src/communications/accountingDocumentDelivery.ts +63 -0
  361. package/src/communications/authEmailError.ts +10 -0
  362. package/src/communications/authEmailPorts.ts +2 -0
  363. package/src/communications/authSendEmailHook.ts +310 -0
  364. package/src/communications/capturedTransactionalDelivery.ts +29 -0
  365. package/src/communications/communicationSync.ts +96 -0
  366. package/src/communications/communicationSyncWorker.ts +302 -0
  367. package/src/communications/consentPolicy.ts +82 -0
  368. package/src/communications/consentPurposeRegistry.ts +31 -0
  369. package/src/communications/contracts/abandonedCart.ts +64 -0
  370. package/src/communications/contracts/authEmail.ts +73 -0
  371. package/src/communications/contracts/backInStock.ts +69 -0
  372. package/src/communications/contracts/blocks.ts +47 -0
  373. package/src/communications/contracts/checkoutExpired.ts +62 -0
  374. package/src/communications/contracts/checkoutRecovery.ts +68 -0
  375. package/src/communications/contracts/commerceEmailContent.ts +189 -0
  376. package/src/communications/contracts/commerceEngagementEmailContent.ts +106 -0
  377. package/src/communications/contracts/consent.ts +13 -0
  378. package/src/communications/contracts/email.ts +210 -0
  379. package/src/communications/contracts/guideCard.ts +34 -0
  380. package/src/communications/contracts/index.ts +51 -0
  381. package/src/communications/contracts/inquiryContent.ts +53 -0
  382. package/src/communications/contracts/invoiceDocumentContent.ts +57 -0
  383. package/src/communications/contracts/marketingAutomation.ts +45 -0
  384. package/src/communications/contracts/newsletter.ts +78 -0
  385. package/src/communications/contracts/newsletterWebhook.ts +23 -0
  386. package/src/communications/contracts/optOutContent.ts +23 -0
  387. package/src/communications/contracts/orderCanceled.ts +64 -0
  388. package/src/communications/contracts/orderDraft.ts +112 -0
  389. package/src/communications/contracts/orderPaid.ts +147 -0
  390. package/src/communications/contracts/orderRefunded.ts +63 -0
  391. package/src/communications/contracts/paymentFailed.ts +59 -0
  392. package/src/communications/contracts/publicIntake.ts +53 -0
  393. package/src/communications/contracts/render.ts +271 -0
  394. package/src/communications/contracts/reorderReminder.ts +59 -0
  395. package/src/communications/contracts/returnEmails.ts +113 -0
  396. package/src/communications/contracts/reviewEffects.ts +60 -0
  397. package/src/communications/contracts/reviewRequest.ts +59 -0
  398. package/src/communications/contracts/roleNotificationContent.ts +65 -0
  399. package/src/communications/contracts/shipmentDelivered.ts +59 -0
  400. package/src/communications/contracts/shipmentDispatched.ts +77 -0
  401. package/src/communications/contracts/shipmentException.ts +62 -0
  402. package/src/communications/contracts/subscriptionActivationActionRequired.ts +37 -0
  403. package/src/communications/contracts/subscriptionAddressChanged.ts +56 -0
  404. package/src/communications/contracts/subscriptionCancelled.ts +57 -0
  405. package/src/communications/contracts/subscriptionCycleSkipped.ts +80 -0
  406. package/src/communications/contracts/subscriptionDeliveryRescheduled.ts +79 -0
  407. package/src/communications/contracts/subscriptionEmailContent.ts +163 -0
  408. package/src/communications/contracts/subscriptionPackageChanged.ts +59 -0
  409. package/src/communications/contracts/subscriptionPauseReminder.ts +63 -0
  410. package/src/communications/contracts/subscriptionPaused.ts +58 -0
  411. package/src/communications/contracts/subscriptionPaymentExpired.ts +83 -0
  412. package/src/communications/contracts/subscriptionPaymentFailed.ts +117 -0
  413. package/src/communications/contracts/subscriptionPaymentRecovered.ts +50 -0
  414. package/src/communications/contracts/subscriptionRenewalAtRisk.ts +85 -0
  415. package/src/communications/contracts/subscriptionRenewalUpcoming.ts +103 -0
  416. package/src/communications/contracts/subscriptionResumed.ts +60 -0
  417. package/src/communications/contracts/subscriptionWelcome.ts +102 -0
  418. package/src/communications/contracts/subscriptionWinback.ts +62 -0
  419. package/src/communications/contracts/transactionalDelivery.ts +43 -0
  420. package/src/communications/contracts/types.ts +58 -0
  421. package/src/communications/contribution.ts +33 -0
  422. package/src/communications/deliveryReconciliation.ts +108 -0
  423. package/src/communications/emailEgress.ts +111 -0
  424. package/src/communications/emailHealthAssessment.ts +106 -0
  425. package/src/communications/emailHealthMetricsPort.ts +17 -0
  426. package/src/communications/emailNotificationControl.ts +28 -0
  427. package/src/communications/emailObservation.ts +43 -0
  428. package/src/communications/emailTransport.ts +167 -0
  429. package/src/communications/errors.ts +34 -0
  430. package/src/communications/index.ts +46 -0
  431. package/src/communications/integrationEventSignature.ts +49 -0
  432. package/src/communications/managedTransactionalDeliveryStore.ts +59 -0
  433. package/src/communications/nativeOperations.ts +40 -0
  434. package/src/communications/newsletterIntegrationContracts.ts +154 -0
  435. package/src/communications/newsletterWebhookPort.ts +104 -0
  436. package/src/communications/newsletterWebhookProcessing.ts +102 -0
  437. package/src/communications/originPolicy.ts +151 -0
  438. package/src/communications/postgresCommunicationSync.ts +127 -0
  439. package/src/communications/postgresConsentEvaluator.ts +49 -0
  440. package/src/communications/postgresDeliveryReconciliation.ts +101 -0
  441. package/src/communications/postgresNativeOperations.ts +55 -0
  442. package/src/communications/postgresNotificationRecipients.ts +57 -0
  443. package/src/communications/postgresTransactionalDeliveryStore.ts +79 -0
  444. package/src/communications/preferencesToken.ts +62 -0
  445. package/src/communications/publicPreferenceToken.ts +68 -0
  446. package/src/communications/shipmentNotifications.ts +78 -0
  447. package/src/communications/sql.ts +7 -0
  448. package/src/communications/standardWebhooks.ts +84 -0
  449. package/src/communications/status.ts +51 -0
  450. package/src/communications/transactionalDelivery.ts +114 -0
  451. package/src/communications/transactionalDeliveryIdentifier.ts +9 -0
  452. package/src/communications/unsubscribeToken.ts +186 -0
  453. package/src/communications/validation.ts +3 -0
  454. package/src/http/index.ts +1 -0
  455. package/src/http/webhookSignature.ts +96 -0
  456. package/src/identity/adminAuthBinding.ts +76 -0
  457. package/src/identity/adminAuthorization.ts +106 -0
  458. package/src/identity/adminContracts.ts +52 -0
  459. package/src/identity/adminMagicLinkHandler.ts +132 -0
  460. package/src/identity/adminMeHandler.ts +64 -0
  461. package/src/identity/adminPreflight.ts +46 -0
  462. package/src/identity/adminUserActionPorts.ts +78 -0
  463. package/src/identity/adminUserHandlers.ts +174 -0
  464. package/src/identity/authVerifyUrl.ts +56 -0
  465. package/src/identity/contracts/contracts.ts +15 -0
  466. package/src/identity/contracts/emailTrust.ts +5 -0
  467. package/src/identity/contracts/index.ts +8 -0
  468. package/src/identity/contracts/oidc.ts +10 -0
  469. package/src/identity/contracts/operatorAuth.ts +61 -0
  470. package/src/identity/contracts/operatorInvitations.ts +32 -0
  471. package/src/identity/contracts/ports.ts +25 -0
  472. package/src/identity/contracts/privacy.ts +86 -0
  473. package/src/identity/contracts/providerList.ts +13 -0
  474. package/src/identity/contracts/types.ts +25 -0
  475. package/src/identity/contribution.ts +29 -0
  476. package/src/identity/customerAuth.ts +3 -0
  477. package/src/identity/customerAuthorization.ts +38 -0
  478. package/src/identity/customerContracts.ts +52 -0
  479. package/src/identity/customerIdentityBinding.ts +125 -0
  480. package/src/identity/customerIdentityVerifier.ts +141 -0
  481. package/src/identity/customerMagicLinkHandler.ts +171 -0
  482. package/src/identity/customerMeHandler.ts +64 -0
  483. package/src/identity/customerPorts.ts +2 -0
  484. package/src/identity/customerSessionBinding.ts +114 -0
  485. package/src/identity/customerSessionIssuer.ts +71 -0
  486. package/src/identity/customerVerifyOtpHandler.ts +44 -0
  487. package/src/identity/directOperatorInvitationStore.ts +178 -0
  488. package/src/identity/emailTrustPolicy.ts +16 -0
  489. package/src/identity/identityPersonalDataContributor.ts +48 -0
  490. package/src/identity/identityReadiness.ts +26 -0
  491. package/src/identity/index.ts +69 -0
  492. package/src/identity/inviteAdminUserUseCase.ts +134 -0
  493. package/src/identity/jwtLibrary.ts +24 -0
  494. package/src/identity/machineActorWriteFence.ts +19 -0
  495. package/src/identity/magicLinkPorts.ts +7 -0
  496. package/src/identity/managedAccountLinkStore.ts +105 -0
  497. package/src/identity/managedAdminAuthVerifier.ts +40 -0
  498. package/src/identity/managedAdminUserGateways.ts +164 -0
  499. package/src/identity/managedClient.ts +58 -0
  500. package/src/identity/managedClientAuthLink.ts +120 -0
  501. package/src/identity/managedCustomerMe.ts +52 -0
  502. package/src/identity/managedEmailTrustPolicy.ts +90 -0
  503. package/src/identity/managedIdentityVerifier.ts +20 -0
  504. package/src/identity/managedPrivacy.ts +37 -0
  505. package/src/identity/messages.ts +41 -0
  506. package/src/identity/oidcIdentityVerifier.ts +229 -0
  507. package/src/identity/oidcPrincipal.ts +10 -0
  508. package/src/identity/operatorAdminAuth.ts +14 -0
  509. package/src/identity/operatorAuthHandlers.ts +52 -0
  510. package/src/identity/operatorIdentityVerifier.ts +19 -0
  511. package/src/identity/operatorInvitationLifecycle.ts +14 -0
  512. package/src/identity/operatorInvitationPorts.ts +11 -0
  513. package/src/identity/operatorPorts.ts +11 -0
  514. package/src/identity/operatorSession.ts +30 -0
  515. package/src/identity/operatorTokenAdminAuth.ts +79 -0
  516. package/src/identity/personalDataRegistry.ts +8 -0
  517. package/src/identity/platformAdminAuth.ts +7 -0
  518. package/src/identity/ports.ts +158 -0
  519. package/src/identity/postgresAccountLinkStore.ts +49 -0
  520. package/src/identity/postgresCustomerMe.ts +33 -0
  521. package/src/identity/postgresCustomerRecovery.ts +72 -0
  522. package/src/identity/postgresOperatorIdentity.ts +123 -0
  523. package/src/identity/postgresPrivacy.ts +117 -0
  524. package/src/identity/privacyHandlers.ts +33 -0
  525. package/src/identity/privacyPorts.ts +52 -0
  526. package/src/identity/privacyUseCases.ts +48 -0
  527. package/src/identity/reconcileAccountForPrincipal.ts +99 -0
  528. package/src/identity/reconcileAccountHandler.ts +36 -0
  529. package/src/identity/removeAdminUserUseCase.ts +85 -0
  530. package/src/identity/response.ts +3 -0
  531. package/src/identity/sql.ts +10 -0
@@ -0,0 +1,158 @@
1
+ import { assessEmailTrust, type EmailTrustPolicy } from "./emailTrustPolicy.js";
2
+ import type { EmailTrustFailureCode } from "./contracts/emailTrust.js";
3
+
4
+ export interface VerifiedPrincipal {
5
+ principalId: string;
6
+ email: string | null;
7
+ emailVerified: boolean;
8
+ }
9
+
10
+
11
+ export interface IdentityVerifierPort {
12
+ verifyAccessToken(accessToken: string): Promise<VerifiedPrincipal | null>;
13
+ }
14
+
15
+ export interface CustomerChallengeStore {
16
+ issue(input: {
17
+ email: string;
18
+ tokenHash: string;
19
+ expiresAt: string;
20
+ }): Promise<{ deliverable: boolean }>;
21
+ redeem(input: { tokenHash: string; now: string }): Promise<{
22
+ principalId: string;
23
+ email: string;
24
+ } | null>;
25
+ }
26
+
27
+ export interface CustomerChallengeDeliveryPort {
28
+ deliver(input: { email: string; token: string; expiresAt: string }): Promise<void>;
29
+ }
30
+
31
+ export interface CustomerSessionIssuerPort {
32
+ issue(input: { principalId: string; email: string }): Promise<{
33
+ accessToken: string;
34
+ expiresAt: string;
35
+ }>;
36
+ }
37
+
38
+
39
+ export type LinkClientAuthUserResult =
40
+ | {
41
+ ok: true;
42
+ clientId: string;
43
+ authUserId: string;
44
+ created: boolean;
45
+ alreadyLinked: boolean;
46
+ adminSelfLink: boolean;
47
+ }
48
+ | {
49
+ ok: false;
50
+ code: EmailTrustFailureCode | "CLIENT_NOT_FOUND" | "MACHINE_ACTOR_REFUSED" | "CLIENT_AUTH_LINK_CONFLICT";
51
+ message: string;
52
+ };
53
+
54
+ export type SetClientAuthUserIdResult =
55
+ | { ok: true; alreadyLinked: boolean }
56
+ | { ok: false; code: "CLIENT_AUTH_LINK_CONFLICT"; existingAuthUserId: string | null };
57
+
58
+ export interface ClientAuthLinkStore {
59
+ emailTrustPolicy: EmailTrustPolicy;
60
+ findClientByEmail(email: string): Promise<{ id: string; authUserId: string | null } | null>;
61
+ findAuthUserIdByEmail(email: string): Promise<string | null>;
62
+ findAuthUserEmailById(authUserId: string): Promise<string | null>;
63
+ /** Provision an unconfirmed principal; null means an existing identity needs lookup. */
64
+ createAuthUser(email: string): Promise<string | null>;
65
+ findAdminIdentity(authUserId: string): Promise<{ isMachineActor: boolean } | null>;
66
+ setClientAuthUserId(clientId: string, authUserId: string): Promise<SetClientAuthUserIdResult>;
67
+ }
68
+
69
+ /** Link a commerce contact without proving mailbox ownership; failed links never delete identities. */
70
+ export async function linkClientAuthUser(
71
+ store: ClientAuthLinkStore,
72
+ rawEmail: string,
73
+ ): Promise<LinkClientAuthUserResult> {
74
+ const email = rawEmail.trim().toLowerCase();
75
+ const client = await store.findClientByEmail(email);
76
+ if (!client) return { ok: false, code: "CLIENT_NOT_FOUND", message: "No client row exists for this email; provision the client first." };
77
+
78
+ if (client.authUserId) {
79
+ const linkedEmail = await store.findAuthUserEmailById(client.authUserId);
80
+ if (linkedEmail?.trim().toLowerCase() !== email) {
81
+ return { ok: false, code: "CLIENT_AUTH_LINK_CONFLICT", message: "Client account is linked to a different auth identity." };
82
+ }
83
+ return { ok: true, clientId: client.id, authUserId: client.authUserId, created: false, alreadyLinked: true, adminSelfLink: false };
84
+ }
85
+
86
+ const trust = await assessEmailTrust(store.emailTrustPolicy);
87
+ if (!trust.trusted) return { ok: false, code: trust.code, message: "Soft-account provisioning requires a trusted email envelope." };
88
+
89
+ const { authUserId, created } = await findOrCreateAuthUserId(store, email);
90
+ const adminIdentity = await store.findAdminIdentity(authUserId);
91
+ if (adminIdentity?.isMachineActor) {
92
+ return { ok: false, code: "MACHINE_ACTOR_REFUSED", message: "Refusing to link a machine/service admin identity to a customer client row." };
93
+ }
94
+ const link = await store.setClientAuthUserId(client.id, authUserId);
95
+ if (!link.ok) {
96
+ return { ok: false, code: "CLIENT_AUTH_LINK_CONFLICT", message: "Client account was linked to another auth identity concurrently." };
97
+ }
98
+ return { ok: true, clientId: client.id, authUserId, created, alreadyLinked: link.alreadyLinked, adminSelfLink: adminIdentity !== null };
99
+ }
100
+
101
+
102
+ export type AdminPrincipalRole = "admin" | "distributor";
103
+
104
+
105
+ export type AdminAuthorizationResult =
106
+ | { ok: true; principalId: string; role: AdminPrincipalRole; isMachineActor: boolean }
107
+ | { ok: false; code: "UNAUTHORIZED" | "FORBIDDEN"; message: string };
108
+
109
+
110
+ export interface AdminAuthPort {
111
+ authorize(
112
+ accessToken: string | null,
113
+ options?: { allowedRoles?: readonly AdminPrincipalRole[] },
114
+ ): Promise<AdminAuthorizationResult>;
115
+ }
116
+
117
+
118
+ export interface CustomerAccount {
119
+ id: string;
120
+
121
+ email: string | null;
122
+
123
+ principalId: string | null;
124
+ }
125
+
126
+ export interface CreateAccountInput {
127
+ email: string;
128
+ principalId: string;
129
+ acquisitionSource: string;
130
+ lifecycleStage: string;
131
+ }
132
+
133
+ export type LinkPrincipalResult =
134
+ | { ok: true; alreadyLinked: boolean }
135
+ | { ok: false; existingPrincipalId: string | null };
136
+
137
+
138
+ export interface AccountLinkStore {
139
+ findAccountByPrincipalId(principalId: string): Promise<CustomerAccount | null>;
140
+ findAccountByEmail(email: string): Promise<CustomerAccount | null>;
141
+
142
+ isReservedPrincipal(principalId: string): Promise<boolean>;
143
+
144
+ linkPrincipalToAccount(accountId: string, principalId: string): Promise<LinkPrincipalResult>;
145
+ createAccount(input: CreateAccountInput): Promise<{ id: string }>;
146
+ }
147
+
148
+ /** Attempt creation before scanning the existing identity directory. */
149
+ export async function findOrCreateAuthUserId(
150
+ store: Pick<ClientAuthLinkStore, "createAuthUser" | "findAuthUserIdByEmail">,
151
+ email: string,
152
+ ): Promise<{ authUserId: string; created: boolean }> {
153
+ const createdId = await store.createAuthUser(email);
154
+ if (createdId) return { authUserId: createdId, created: true };
155
+ const existingId = await store.findAuthUserIdByEmail(email);
156
+ if (!existingId) throw new Error("auth_user_registered_but_unresolved");
157
+ return { authUserId: existingId, created: false };
158
+ }
@@ -0,0 +1,49 @@
1
+ import { createPostgresRoleFreeTransactionLane, type PgQueryExecutor } from "../data/index.js";
2
+ import type { AccountLinkStore, CustomerAccount } from "./ports.js";
3
+ /** Trusted contact-link capability; conditional writes never replace an existing principal. */
4
+ export function createPostgresAccountLinkStore(env: { connectionString: string }): AccountLinkStore & { close(): Promise<void> } {
5
+ const lane = createPostgresRoleFreeTransactionLane(env);
6
+ const run = <T>(work: (db: PgQueryExecutor) => Promise<T>) => lane.run(db => work(db as PgQueryExecutor));
7
+ const account = (row: Record<string, unknown> | undefined): CustomerAccount | null => row
8
+ ? { id: String(row.id), email: row.email === null ? null : String(row.email), principalId: row.principal_id === null ? null : String(row.principal_id) } : null;
9
+ async function writable(db: PgQueryExecutor, principalId: string, clientId: string | null = null) {
10
+ // Same receipt-before-contact lock order as privacy erasure; no concurrent resurrection.
11
+ await db.query("LOCK TABLE public.identity_privacy_erasures IN SHARE MODE");
12
+ if ((await db.query("SELECT 1 FROM public.identity_privacy_erasures WHERE principal_id = $1 OR client_id = $2 LIMIT 1", [principalId, clientId])).rows.length) throw new Error("account_erased_refused");
13
+ }
14
+ return {
15
+ findAccountByPrincipalId(id) {
16
+ return run(async db => account((await db.query("SELECT id, email, principal_id FROM public.clients WHERE principal_id = $1", [id])).rows[0]));
17
+ },
18
+ findAccountByEmail(email) {
19
+ return run(async db => {
20
+ const rows = (await db.query("SELECT id, email, principal_id FROM public.clients WHERE lower(btrim(email)) = $1 LIMIT 2", [email.trim().toLowerCase()])).rows;
21
+ if (rows.length > 1) throw new Error("account_normalized_email_ambiguous");
22
+ return account(rows[0]);
23
+ });
24
+ },
25
+ isReservedPrincipal(id) {
26
+ return run(async db => (await db.query("SELECT 1 FROM public.platform_control_operators WHERE principal_id = $1 UNION ALL SELECT 1 FROM public.identity_privacy_erasures WHERE principal_id = $1 LIMIT 1", [id])).rows.length > 0);
27
+ },
28
+ linkPrincipalToAccount(id, principalId) {
29
+ return run(async db => {
30
+ await writable(db, principalId, id);
31
+ const updated = await db.query("UPDATE public.clients SET principal_id = $2 WHERE id = $1 AND principal_id IS NULL RETURNING id", [id, principalId]);
32
+ if (updated.rows.length) return { ok: true, alreadyLinked: false };
33
+ const row = (await db.query("SELECT principal_id FROM public.clients WHERE id = $1", [id])).rows[0];
34
+ return row?.principal_id === principalId ? { ok: true, alreadyLinked: true }
35
+ : { ok: false, existingPrincipalId: row?.principal_id == null ? null : String(row.principal_id) };
36
+ });
37
+ },
38
+ createAccount(input) {
39
+ return run(async db => {
40
+ await writable(db, input.principalId);
41
+ const row = (await db.query(`INSERT INTO public.clients (email, principal_id, acquisition_source, lifecycle_stage)
42
+ VALUES ($1, $2, $3, $4) RETURNING id`, [input.email, input.principalId, input.acquisitionSource, input.lifecycleStage])).rows[0];
43
+ if (!row) throw new Error("account_insert_failed");
44
+ return { id: String(row.id) };
45
+ });
46
+ },
47
+ close: lane.close,
48
+ };
49
+ }
@@ -0,0 +1,33 @@
1
+ import type { CustomerMeResponse } from "./customerContracts.js";
2
+ import type { CustomerMePort } from "./customerPorts.js";
3
+ import type { PgGatewayClient } from "../data/index.js";
4
+
5
+ type CustomerMeRow = {
6
+ id: string;
7
+ email: string;
8
+ first_name: string | null;
9
+ last_name: string | null;
10
+ lifecycle_stage: CustomerMeResponse["lifecycleStage"];
11
+ };
12
+
13
+
14
+ export function createPostgresCustomerMePort(gateway: PgGatewayClient): CustomerMePort {
15
+ return {
16
+ async getCustomerMe(): Promise<CustomerMeResponse | null> {
17
+ const { data, error } = await gateway
18
+ .from<CustomerMeRow>("clients")
19
+ .select("id, email, first_name, last_name, lifecycle_stage")
20
+ .maybeSingle();
21
+ if (error) throw error;
22
+ if (!data) return null;
23
+
24
+ return {
25
+ clientId: data.id,
26
+ email: data.email,
27
+ firstName: data.first_name,
28
+ lastName: data.last_name,
29
+ lifecycleStage: data.lifecycle_stage as CustomerMeResponse["lifecycleStage"],
30
+ };
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,72 @@
1
+ import type { CustomerChallengeStore } from "./ports.js";
2
+ import {
3
+ createPostgresRoleFreeTransactionLane,
4
+ type PostgresDataGatewayEnv,
5
+ type PostgresDataGatewayOptions,
6
+ } from "../data/index.js";
7
+ import type { PgQueryExecutor } from "../data/index.js";
8
+
9
+ export type PostgresCustomerChallengeStore = CustomerChallengeStore & {
10
+ close(): Promise<void>;
11
+ pruneChallenges(input: { limit: number }): Promise<{ deleted: number }>;
12
+ };
13
+
14
+ export function createPostgresCustomerChallengeStore(
15
+ env: PostgresDataGatewayEnv,
16
+ options: PostgresDataGatewayOptions = {},
17
+ ): PostgresCustomerChallengeStore {
18
+ const lane = createPostgresRoleFreeTransactionLane(env, options);
19
+ return {
20
+ async issue(input) {
21
+ return lane.run(async (executor) => {
22
+ const db = executor as PgQueryExecutor;
23
+ // EXCLUSIVE conflicts with erasure's EXCLUSIVE lock and stays held through the RPC.
24
+ // Receipt reads use ACCESS SHARE, compatible with erasure's receipt-table lock.
25
+ await db.query("LOCK TABLE public.customer_identity_challenges IN EXCLUSIVE MODE");
26
+ const erased = await db.query(`SELECT 1 FROM public.clients c JOIN public.identity_privacy_erasures e ON e.client_id = c.id OR e.principal_id = c.principal_id
27
+ WHERE lower(c.email) = lower(btrim($1))`, [input.email]);
28
+ if (erased.rows.length) return { deliverable: false };
29
+ const { rows } = await db.query(
30
+ "SELECT public.customer_identity_challenge_issue($1,$2,$3::timestamptz,now()) AS result",
31
+ [input.email, input.tokenHash, input.expiresAt],
32
+ );
33
+ const result = rows[0]?.result;
34
+ if (!result || typeof result !== "object") throw new Error("customer challenge issue response invalid");
35
+ return { deliverable: (result as { deliverable?: unknown }).deliverable === true };
36
+ });
37
+ },
38
+ async redeem(input) {
39
+ return lane.run(async (executor) => {
40
+ const db = executor as PgQueryExecutor;
41
+ // EXCLUSIVE conflicts with erasure's EXCLUSIVE lock and stays held through the RPC.
42
+ // Receipt reads use ACCESS SHARE, compatible with erasure's receipt-table lock.
43
+ await db.query("LOCK TABLE public.customer_identity_challenges IN EXCLUSIVE MODE");
44
+ const erased = await db.query(`SELECT 1 FROM public.customer_identity_challenges c
45
+ JOIN public.identity_privacy_erasures e ON e.client_id = c.client_id OR e.principal_id = c.principal_id WHERE c.token_hash = $1`, [input.tokenHash]);
46
+ if (erased.rows.length) return null;
47
+ const { rows } = await db.query(
48
+ "SELECT public.customer_identity_challenge_redeem($1,$2::timestamptz) AS result",
49
+ [input.tokenHash, input.now],
50
+ );
51
+ const result = rows[0]?.result;
52
+ if (result === null || result === undefined) return null;
53
+ if (!result || typeof result !== "object") throw new Error("customer challenge redeem response invalid");
54
+ const value = result as Record<string, unknown>;
55
+ if (typeof value.principalId !== "string" || typeof value.email !== "string") {
56
+ throw new Error("customer challenge redeem response invalid");
57
+ }
58
+ return { principalId: value.principalId, email: value.email };
59
+ });
60
+ },
61
+ async pruneChallenges({ limit }) {
62
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("customer_prune_limit_invalid");
63
+ return lane.run(async executor => {
64
+ const deleted = await (executor as PgQueryExecutor).query(`WITH batch AS (SELECT id FROM public.customer_identity_challenges
65
+ WHERE expires_at < clock_timestamp() - interval '24 hours' ORDER BY expires_at, id LIMIT $1 FOR UPDATE SKIP LOCKED)
66
+ DELETE FROM public.customer_identity_challenges c USING batch WHERE c.id = batch.id RETURNING c.id`, [limit]);
67
+ return { deleted: deleted.rows.length };
68
+ });
69
+ },
70
+ close: lane.close,
71
+ };
72
+ }
@@ -0,0 +1,123 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createPostgresRoleFreeTransactionLane, type PgQueryExecutor } from "../data/index.js";
3
+ import { acceptDirectOperatorInvitationInTransaction } from "./directOperatorInvitationStore.js";
4
+ import { operatorChallengeRequestSchema } from "./contracts/operatorAuth.js";
5
+ import type { OperatorIdentityStore } from "./operatorPorts.js";
6
+ const fingerprint = (email: string) => createHash("sha256").update(email.trim().toLowerCase()).digest("hex");
7
+ const allowed = (role: unknown) => role === "admin" || role === "distributor";
8
+
9
+ /** Dedicated owner lane: no actor gateway, customer writes or machine grants. */
10
+ export function createPostgresOperatorIdentityStore(env: { connectionString: string }): OperatorIdentityStore {
11
+ const lane = createPostgresRoleFreeTransactionLane(env);
12
+ const run = <T>(work: (db: PgQueryExecutor) => Promise<T>) => lane.run(db => work(db as PgQueryExecutor));
13
+ async function safeStore(db: PgQueryExecutor) {
14
+ // Includes privileges inherited via memberships/default ACLs, not just explicit grants.
15
+ const unsafe = (await db.query(`SELECT EXISTS (
16
+ SELECT 1 FROM pg_roles r WHERE r.rolname IN ('anon', 'authenticated', 'authenticator')
17
+ AND (has_table_privilege(r.oid, 'public.operator_identity_challenges', 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER')
18
+ OR has_any_column_privilege(r.oid, 'public.operator_identity_challenges', 'SELECT,INSERT,UPDATE,REFERENCES'))
19
+ ) OR EXISTS (SELECT 1 FROM pg_class c, LATERAL aclexplode(coalesce(c.relacl, acldefault('r', c.relowner))) a
20
+ WHERE c.oid = 'public.operator_identity_challenges'::regclass AND a.grantee = 0)
21
+ OR EXISTS (SELECT 1 FROM pg_attribute c, LATERAL aclexplode(c.attacl) a
22
+ WHERE c.attrelid = 'public.operator_identity_challenges'::regclass AND a.grantee = 0) AS unsafe`)).rows[0]?.unsafe;
23
+ if (unsafe !== false) throw new Error("operator_challenge_store_privileges_unsafe");
24
+ }
25
+ async function lockPrincipal(db: PgQueryExecutor, email: string) {
26
+ // Same serialization key as the invitation lifecycle, before row locks.
27
+ await db.query("SELECT pg_advisory_xact_lock(hashtext($1)::bigint)", [`operator_invitation:${email}`]);
28
+ const matches = (await db.query("SELECT principal_id, email FROM public.platform_human_principals WHERE lower(btrim(email)) = $1 FOR UPDATE", [email])).rows;
29
+ if (matches.length > 1) throw new Error("operator_normalized_email_conflict");
30
+ return matches[0] ?? null;
31
+ }
32
+ async function eligible(db: PgQueryExecutor, id: string, email: string): Promise<boolean> {
33
+ if ((await db.query("SELECT 1 FROM public.platform_control_operators WHERE principal_id = $1", [id])).rows.length) return false;
34
+ const member = (await db.query("SELECT role, membership_state FROM public.platform_human_memberships WHERE principal_id = $1 FOR UPDATE", [id])).rows[0];
35
+ if (member?.membership_state === "active") return allowed(member.role);
36
+ const pending = (await db.query(`SELECT role FROM public.identity_operator_invitations WHERE principal_id = $1
37
+ AND email_key = $2 AND phase = 'invited' AND accepted_at IS NULL AND closed_at IS NULL
38
+ AND expires_at > clock_timestamp() FOR UPDATE`, [id, email])).rows[0];
39
+ return !!pending && allowed(pending.role);
40
+ }
41
+ return {
42
+ issue({ email, tokenHash }) {
43
+ return run(async db => {
44
+ await safeStore(db);
45
+ const principal = await lockPrincipal(db, email);
46
+ if (!principal || !await eligible(db, String(principal.principal_id), email)) return { deliverable: false };
47
+ const stamp = (await db.query("SELECT clock_timestamp() AS at")).rows[0]!.at;
48
+ const counts = (await db.query(`SELECT count(*) AS hour_count,
49
+ count(*) FILTER (WHERE created_at > $2::timestamptz - interval '60 seconds') AS minute_count
50
+ FROM public.operator_identity_challenges WHERE principal_id = $1 AND created_at > $2::timestamptz - interval '1 hour'`, [principal.principal_id, stamp])).rows[0]!;
51
+ if (Number(counts.minute_count) >= 1 || Number(counts.hour_count) >= 5) return { deliverable: false };
52
+ const row = (await db.query(`INSERT INTO public.operator_identity_challenges
53
+ (principal_id, email_fingerprint, token_hash, created_at, expires_at)
54
+ VALUES ($1, $2, $3, $4, $4::timestamptz + interval '10 minutes') RETURNING expires_at`, [principal.principal_id, fingerprint(email), tokenHash, stamp])).rows[0]!;
55
+ return { deliverable: true, email: String(principal.email).trim().toLowerCase(), expiresAt: new Date(String(row.expires_at)).toISOString() };
56
+ });
57
+ },
58
+ redeem({ tokenHash }) {
59
+ return run(async db => {
60
+ await safeStore(db);
61
+ const candidate = (await db.query(`SELECT p.email, p.principal_id FROM public.operator_identity_challenges c
62
+ JOIN public.platform_human_principals p USING (principal_id) WHERE c.token_hash = $1`, [tokenHash])).rows[0];
63
+ if (!candidate) return null;
64
+ const email = String(candidate.email).trim().toLowerCase();
65
+ const principal = await lockPrincipal(db, email);
66
+ if (!principal || principal.principal_id !== candidate.principal_id || !await eligible(db, String(principal.principal_id), email)) return null;
67
+ const challenge = (await db.query(`SELECT * FROM public.operator_identity_challenges WHERE token_hash = $1
68
+ AND principal_id = $2 AND used_at IS NULL AND expires_at > clock_timestamp() FOR UPDATE`, [tokenHash, principal.principal_id])).rows[0];
69
+ if (!challenge || challenge.email_fingerprint !== fingerprint(email)) return null;
70
+ const accepted = await acceptDirectOperatorInvitationInTransaction(db, String(principal.principal_id));
71
+ if ((accepted.code !== "ACTIVATED" && accepted.code !== "ALREADY_ACTIVE") || !allowed(accepted.role)) return null;
72
+ const consumed = await db.query(`WITH stamp AS (SELECT clock_timestamp() AS at)
73
+ UPDATE public.operator_identity_challenges SET used_at = stamp.at FROM stamp
74
+ WHERE id = $1 AND used_at IS NULL AND expires_at > stamp.at RETURNING id`, [challenge.id]);
75
+ // Throw to roll activation back if the challenge expired during activation.
76
+ if (!consumed.rows.length) throw new Error("operator_challenge_expired_during_activation");
77
+ return { principalId: String(principal.principal_id), email };
78
+ }).catch(error => {
79
+ if (error instanceof Error && error.message === "operator_challenge_expired_during_activation") return null;
80
+ throw error;
81
+ });
82
+ },
83
+ findMembership(id) {
84
+ return run(async db => {
85
+ await safeStore(db);
86
+ const row = (await db.query(`SELECT p.principal_id, p.email, m.role, m.membership_state
87
+ FROM public.platform_human_principals p JOIN public.platform_human_memberships m USING (principal_id)
88
+ WHERE p.principal_id = $1 AND NOT EXISTS (SELECT 1 FROM public.platform_control_operators o WHERE o.principal_id = p.principal_id)`, [id])).rows[0];
89
+ return row ? { principalId: String(row.principal_id), email: String(row.email), role: row.role, membershipState: String(row.membership_state) } : null;
90
+ });
91
+ },
92
+ bootstrapFirstAdministrator(input) {
93
+ const { email } = operatorChallengeRequestSchema.parse(input);
94
+ return run(async db => {
95
+ await safeStore(db);
96
+ await db.query("LOCK TABLE public.platform_human_memberships, public.identity_operator_invitations IN SHARE ROW EXCLUSIVE MODE");
97
+ if ((await db.query(`SELECT 1 FROM public.platform_human_memberships UNION ALL SELECT 1 FROM public.identity_operator_invitations LIMIT 1`)).rows.length) throw new Error("OPERATOR_BOOTSTRAP_ALREADY_INITIALIZED");
98
+ let principal = await lockPrincipal(db, email);
99
+ if (!principal) principal = (await db.query("INSERT INTO public.platform_human_principals (principal_id, email) VALUES (gen_random_uuid(), $1) RETURNING principal_id, email", [email])).rows[0]!;
100
+ const id = String(principal.principal_id);
101
+ if ((await db.query("SELECT 1 FROM public.platform_control_operators WHERE principal_id = $1", [id])).rows.length) throw new Error("operator_bootstrap_machine_refused");
102
+ const pending = (await db.query(`WITH stamp AS (SELECT clock_timestamp() AS at)
103
+ INSERT INTO public.identity_operator_invitations
104
+ (principal_id, invitation_id, email_key, role, phase, invited_by, invited_at, expires_at)
105
+ SELECT $1, gen_random_uuid(), $2, 'admin', 'invited', $1, at, at + interval '7 days' FROM stamp RETURNING *`, [id, email])).rows[0]!;
106
+ await db.query(`INSERT INTO public.identity_operator_invitation_events
107
+ (event_id, invitation_id, principal_id, action, actor_kind, old_value, new_value, occurred_at)
108
+ VALUES (gen_random_uuid(), $1, $2, 'issue', 'system', '{}'::jsonb, $3::jsonb, clock_timestamp())`, [pending.invitation_id, id, JSON.stringify(pending)]);
109
+ return { principalId: id, membershipState: "invited" };
110
+ });
111
+ },
112
+ pruneChallenges({ limit }) {
113
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("operator_prune_limit_invalid");
114
+ return run(async db => {
115
+ const deleted = await db.query(`WITH batch AS (SELECT id FROM public.operator_identity_challenges
116
+ WHERE expires_at < clock_timestamp() - interval '24 hours' ORDER BY expires_at, id LIMIT $1 FOR UPDATE SKIP LOCKED)
117
+ DELETE FROM public.operator_identity_challenges c USING batch WHERE c.id = batch.id RETURNING c.id`, [limit]);
118
+ return { deleted: deleted.rows.length };
119
+ });
120
+ },
121
+ close: lane.close,
122
+ };
123
+ }
@@ -0,0 +1,117 @@
1
+ import { createPostgresRoleFreeTransactionLane, type PgQueryExecutor, type PostgresDataGatewayEnv, type PostgresDataGatewayOptions } from "../data/index.js";
2
+ import { PrivacyRefusal, type PrivacyStore, type ResolvedPrivacySubject, type AuthorizedHumanAdministrator } from "./privacyPorts.js";
3
+ import { erasureReceiptSchema, type PrivacySubjectRef, type PrivacyJson } from "./contracts/privacy.js";
4
+
5
+ type Row = Record<string, unknown>;
6
+ function json(value: unknown): PrivacyJson { return JSON.parse(JSON.stringify(value)) as PrivacyJson; }
7
+ export function createPostgresPrivacyStore(): PrivacyStore<PgQueryExecutor> {
8
+ return {
9
+ erasureSupported: true,
10
+ async prepareErasure(db) {
11
+ await db.query("SET LOCAL lock_timeout = '5s'");
12
+ await db.query("SET LOCAL statement_timeout = '15s'");
13
+ // Serialize receipt/key decisions; challenge relation precedes every contact lock.
14
+ await db.query("LOCK TABLE public.identity_privacy_erasures IN SHARE ROW EXCLUSIVE MODE");
15
+ const unsafe = (await db.query(`SELECT EXISTS (
16
+ SELECT 1 FROM pg_roles r WHERE r.rolname IN ('anon', 'authenticated', 'authenticator')
17
+ AND (has_table_privilege(r.oid, 'public.identity_privacy_erasures', 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER')
18
+ OR has_any_column_privilege(r.oid, 'public.identity_privacy_erasures', 'SELECT,INSERT,UPDATE,REFERENCES'))
19
+ ) OR EXISTS (SELECT 1 FROM pg_class c, LATERAL aclexplode(coalesce(c.relacl, acldefault('r', c.relowner))) a
20
+ WHERE c.oid = 'public.identity_privacy_erasures'::regclass AND a.grantee = 0)
21
+ OR EXISTS (SELECT 1 FROM pg_attribute c, LATERAL aclexplode(c.attacl) a
22
+ WHERE c.attrelid = 'public.identity_privacy_erasures'::regclass AND a.grantee = 0) AS unsafe`)).rows[0]?.unsafe;
23
+ if (unsafe !== false) throw new PrivacyRefusal("PRIVACY_STORE_PRIVILEGES_UNSAFE");
24
+ await db.query("LOCK TABLE public.customer_identity_challenges IN EXCLUSIVE MODE");
25
+ },
26
+ async authorizeErasure(db, actor) {
27
+ const row = (await db.query(`SELECT m.role, m.membership_state FROM public.platform_human_memberships m
28
+ WHERE m.principal_id = $1 AND NOT EXISTS (SELECT 1 FROM public.platform_control_operators o WHERE o.principal_id = m.principal_id)`, [actor.principalId])).rows[0];
29
+ if (row?.role !== "admin" || row.membership_state !== "active") throw new PrivacyRefusal("PRIVACY_PROTECTED_OPERATOR");
30
+ },
31
+ async resolve(db, ref, actor) {
32
+ const contactPredicate = ref.kind === "contact" ? "c.id = $1" : "c.principal_id = $1";
33
+ const id = ref.kind === "contact" ? ref.contactId : ref.principalId;
34
+ let contact = (await db.query(`SELECT c.* FROM public.clients c WHERE ${contactPredicate}`, [id])).rows[0] ?? null;
35
+ if (ref.kind === "principal" && !contact) {
36
+ const receipt = (await db.query("SELECT client_id FROM public.identity_privacy_erasures WHERE principal_id = $1", [id])).rows[0];
37
+ if (receipt?.client_id) contact = (await db.query("SELECT * FROM public.clients WHERE id = $1", [receipt.client_id])).rows[0] ?? null;
38
+ }
39
+ let principalId = ref.kind === "principal" ? ref.principalId : contact?.principal_id ? String(contact.principal_id) : null;
40
+ if (!principalId && contact) {
41
+ const receipt = (await db.query("SELECT principal_id FROM public.identity_privacy_erasures WHERE client_id = $1", [contact.id])).rows[0];
42
+ if (receipt?.principal_id) principalId = String(receipt.principal_id);
43
+ }
44
+ let principal = principalId ? (await db.query("SELECT principal_id, email, created_at FROM public.platform_human_principals WHERE principal_id = $1", [principalId])).rows[0] ?? null : null;
45
+ if (!contact && !principal) return null;
46
+ if (actor) {
47
+ if (principal) {
48
+ await db.query("SELECT pg_advisory_xact_lock(hashtext($1)::bigint)", [`operator_invitation:${String(principal.email).trim().toLowerCase()}`]);
49
+ }
50
+ await db.query("SELECT principal_id FROM public.platform_human_memberships WHERE principal_id = ANY($1::uuid[]) ORDER BY principal_id FOR UPDATE", [[actor.principalId, principalId].filter(Boolean)]);
51
+ if (contact) contact = (await db.query("SELECT * FROM public.clients WHERE id = $1 FOR UPDATE", [contact.id])).rows[0] ?? null;
52
+ if (principal) principal = (await db.query("SELECT principal_id, email, created_at FROM public.platform_human_principals WHERE principal_id = $1 FOR UPDATE", [principalId])).rows[0] ?? null;
53
+ }
54
+ const membership = principalId ? (await db.query("SELECT * FROM public.platform_human_memberships WHERE principal_id = $1", [principalId])).rows[0] ?? null : null;
55
+ const invitations = principalId ? (await db.query("SELECT * FROM public.identity_operator_invitations WHERE principal_id = $1", [principalId])).rows : [];
56
+ const invitationHistory = principalId ? (await db.query("SELECT * FROM public.identity_operator_invitation_events WHERE principal_id = $1 ORDER BY occurred_at, event_id", [principalId])).rows : [];
57
+ const membershipHistory = principalId ? (await db.query("SELECT * FROM public.platform_human_membership_audit_events WHERE target_principal_id = $1 ORDER BY occurred_at, id", [principalId])).rows : [];
58
+ return { contactId: contact ? String(contact.id) : null, principalId,
59
+ identity: { contact: json(contact), principal: json(principal), membership: json(membership), invitations: json(invitations), invitationHistory: json(invitationHistory), membershipHistory: json(membershipHistory) } };
60
+ },
61
+ async findReceipt(db, ref, key) {
62
+ const column = ref.kind === "contact" ? "client_id" : "principal_id";
63
+ const id = ref.kind === "contact" ? ref.contactId : ref.principalId;
64
+ const rows = (await db.query(`SELECT * FROM public.identity_privacy_erasures WHERE ${column} = $1 OR idempotency_key = $2::uuid`, [id, key ?? null])).rows;
65
+ if (rows.some(row => row.idempotency_key === key && row[column] !== id)) throw new PrivacyRefusal("PRIVACY_IDEMPOTENCY_CONFLICT");
66
+ const row = rows.find(row => row[column] === id);
67
+ return row ? erasureReceiptSchema.parse({ version: row.contract_version, erasureId: row.erasure_id, completedAt: row.completed_at instanceof Date ? row.completed_at.toISOString() : new Date(String(row.completed_at)).toISOString(), outcome: "ERASED", obligations: row.retained_obligations }) : null;
68
+ },
69
+ async eraseIdentity(db, subject, actor, pseudonym) {
70
+ await erasePostgresIdentity(db, subject, actor, pseudonym);
71
+ },
72
+ async saveReceipt(db, subject, actor, key, receipt, dispositions) {
73
+ await db.query(`INSERT INTO public.identity_privacy_erasures
74
+ (erasure_id, idempotency_key, client_id, principal_id, actor_principal_id, contract_version, contributor_versions, dispositions, retained_obligations, completed_at)
75
+ VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10::timestamptz)`,
76
+ [receipt.erasureId, key, subject.contactId, subject.principalId, actor.principalId, receipt.version,
77
+ JSON.stringify(Object.fromEntries(dispositions.map(d => [d.contributorId, d.version]))), JSON.stringify(dispositions), JSON.stringify(receipt.obligations), receipt.completedAt]);
78
+ },
79
+ };
80
+ }
81
+ async function erasePostgresIdentity(db: PgQueryExecutor, subject: ResolvedPrivacySubject, actor: AuthorizedHumanAdministrator, pseudonym: string): Promise<void> {
82
+ const id = subject.principalId;
83
+ if (id) {
84
+ if (id === actor.principalId || (await db.query("SELECT 1 FROM public.platform_control_operators WHERE principal_id = $1", [id])).rows.length) throw new PrivacyRefusal("PRIVACY_PROTECTED_OPERATOR");
85
+ const member = subject.identity.membership as Row | null;
86
+ if (member?.membership_state === "active") {
87
+ try { await db.query("SELECT public.platform_revoke_human_membership($1, $2, $3)", [actor.principalId, id, "identity erasure"]); }
88
+ catch (error) {
89
+ if (error && typeof error === "object" && "code" in error && ["P0001", "42501"].includes(String(error.code))) throw new PrivacyRefusal("PRIVACY_PROTECTED_OPERATOR");
90
+ throw error;
91
+ }
92
+ }
93
+ await db.query("DELETE FROM public.operator_identity_challenges WHERE principal_id = $1", [id]);
94
+ await db.query("DELETE FROM public.identity_operator_invitations WHERE principal_id = $1 AND phase <> 'accepted'", [id]);
95
+ await db.query("UPDATE public.identity_operator_invitations SET email_key = $2 WHERE principal_id = $1", [id, pseudonym]);
96
+ // Rebuild historical identity snapshots from lifecycle fields, never a recursive string replacement.
97
+ await db.query(`UPDATE public.identity_operator_invitation_events SET
98
+ old_value = old_value - 'email_key', new_value = new_value - 'email_key' WHERE principal_id = $1`, [id]);
99
+ await db.query(`UPDATE public.platform_human_membership_audit_events SET reason = NULL,
100
+ old_value = old_value - 'revocationReason', new_value = new_value - 'revocationReason' WHERE target_principal_id = $1`, [id]);
101
+ // The existing lifecycle guard also protects free-text redaction on a revoked row.
102
+ const previous = (await db.query("SELECT current_setting('app.platform_human_membership_revoke', true) AS previous")).rows[0]?.previous;
103
+ await db.query("SELECT set_config('app.platform_human_membership_revoke', 'true', true)");
104
+ await db.query("UPDATE public.platform_human_memberships SET membership_revocation_reason = NULL WHERE principal_id = $1 AND membership_state = 'revoked'", [id]);
105
+ await db.query("SELECT set_config('app.platform_human_membership_revoke', $1, true)", [previous ?? ""]);
106
+ await db.query("UPDATE public.platform_human_principals SET email = $2 WHERE principal_id = $1", [id, pseudonym]);
107
+ }
108
+ if (subject.contactId) {
109
+ await db.query("DELETE FROM public.customer_identity_challenges WHERE client_id = $1", [subject.contactId]);
110
+ await db.query(`UPDATE public.clients SET email = $2, first_name = NULL, last_name = NULL, phone = NULL,
111
+ country = NULL, principal_id = NULL, updated_at = clock_timestamp() WHERE id = $1`, [subject.contactId, pseudonym]);
112
+ }
113
+ }
114
+ export function createPostgresPrivacyUnitOfWork(env: PostgresDataGatewayEnv, options: PostgresDataGatewayOptions = {}) {
115
+ const lane = createPostgresRoleFreeTransactionLane(env, options);
116
+ return { run: <T>(work: (context: PgQueryExecutor) => Promise<T>) => lane.run(db => work(db as PgQueryExecutor)), close: lane.close };
117
+ }
@@ -0,0 +1,33 @@
1
+ import type { HttpHandler } from "../http/index.js";
2
+ import type { AdminAuthorizationResult } from "./ports.js";
3
+ import { createIdentityResponder, type IdentityResponseOptions } from "./response.js";
4
+ import { privacyExportRequestSchema, privacyEraseRequestSchema, privacyExportResponseSchema, privacyEraseResponseSchema } from "./contracts/privacy.js";
5
+ import { PrivacyRefusal, type PrivacyOperations } from "./privacyPorts.js";
6
+ export interface PrivacyHandlerDependencies extends IdentityResponseOptions { authorizeAdmin: () => Promise<AdminAuthorizationResult>; operations: PrivacyOperations }
7
+ function handler(deps: PrivacyHandlerDependencies, erase: boolean): HttpHandler {
8
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder(deps);
9
+ return async (req, res) => {
10
+ res.setHeader("Cache-Control", "no-store");
11
+ if (req.method !== "POST") return sendMethodNotAllowed(res, ["POST"]);
12
+ try {
13
+ const auth = await deps.authorizeAdmin();
14
+ if (!auth.ok) return sendBffError(res, auth.code, auth.message);
15
+ if (auth.role !== "admin" || auth.isMachineActor !== false) return sendBffError(res, "FORBIDDEN", "Human administrator required");
16
+ const parsed = (erase ? privacyEraseRequestSchema : privacyExportRequestSchema).safeParse(req.body);
17
+ if (!parsed.success) return sendBffError(res, "BAD_REQUEST", "Invalid privacy request");
18
+ const actor = { principalId: auth.principalId };
19
+ const result = erase ? privacyEraseResponseSchema.parse(await deps.operations.erase(actor, privacyEraseRequestSchema.parse(parsed.data)))
20
+ : privacyExportResponseSchema.parse(await deps.operations.export(actor, parsed.data.subject));
21
+ return sendBffSuccess(res, result);
22
+ } catch (error) {
23
+ if (error instanceof PrivacyRefusal) {
24
+ const code = error.code === "PRIVACY_SUBJECT_NOT_FOUND" ? "NOT_FOUND"
25
+ : ["PRIVACY_ACTIVE_SUBSCRIPTION", "PRIVACY_RESUMABLE_SUBSCRIPTION", "PRIVACY_PROTECTED_OPERATOR", "PRIVACY_IDEMPOTENCY_CONFLICT", "PRIVACY_SUBJECT_ERASED"].includes(error.code) ? "CONFLICT" : "UPSTREAM_UNAVAILABLE";
26
+ return sendBffError(res, code, "Privacy operation refused", { details: { privacyCode: error.code } });
27
+ }
28
+ return sendBffError(res, "UPSTREAM_UNAVAILABLE", "Privacy operation unavailable", { details: { privacyCode: "PRIVACY_ASSESSMENT_UNAVAILABLE" } });
29
+ }
30
+ };
31
+ }
32
+ export const createPrivacyExportHandler = (deps: PrivacyHandlerDependencies): HttpHandler => handler(deps, false);
33
+ export const createPrivacyEraseHandler = (deps: PrivacyHandlerDependencies): HttpHandler => handler(deps, true);
@@ -0,0 +1,52 @@
1
+ import type { PrivacySubjectRef, PrivacyExport, ErasureReceipt, PersonalDataDocument, PersonalDataObligation, PrivacyJson, PrivacyRefusalCode } from "./contracts/privacy.js";
2
+
3
+ export class PrivacyRefusal extends Error {
4
+ constructor(readonly code: PrivacyRefusalCode) { super(code); this.name = "PrivacyRefusal"; }
5
+ }
6
+ export interface AuthorizedHumanAdministrator { principalId: string }
7
+ export interface ResolvedPrivacySubject {
8
+ contactId: string | null;
9
+ principalId: string | null;
10
+ /** Only explicitly selected identity fields; never credentials. */
11
+ identity: { [key: string]: PrivacyJson };
12
+ }
13
+ export interface ErasureAssessment { obligations: PersonalDataObligation[] }
14
+ export interface ContributorDisposition { contributorId: string; version: string; obligations: PersonalDataObligation[] }
15
+ export interface PersonalDataContributor<Context> {
16
+ readonly id: string;
17
+ readonly version: string;
18
+ readonly ownedResources: readonly string[];
19
+ prepareErasure(context: Context): Promise<void>;
20
+ export(context: Context, subject: ResolvedPrivacySubject): Promise<PersonalDataDocument>;
21
+ assessErasure(context: Context, subject: ResolvedPrivacySubject): Promise<ErasureAssessment>;
22
+ eraseOrRetain(context: Context, subject: ResolvedPrivacySubject, assessment: ErasureAssessment, actor: AuthorizedHumanAdministrator): Promise<ContributorDisposition>;
23
+ }
24
+ export interface PersonalDataContributorRegistry<Context> {
25
+ readonly contributors: readonly PersonalDataContributor<Context>[];
26
+ readonly missingContributors: readonly string[];
27
+ }
28
+ export interface PrivacyUnitOfWork<Context> { run<T>(work: (context: Context) => Promise<T>): Promise<T> }
29
+ export interface PrivacyStore<Context> {
30
+ readonly erasureSupported: boolean;
31
+ prepareErasure(context: Context): Promise<void>;
32
+ authorizeErasure(context: Context, actor: AuthorizedHumanAdministrator): Promise<void>;
33
+ resolve(context: Context, subject: PrivacySubjectRef, actor?: AuthorizedHumanAdministrator): Promise<ResolvedPrivacySubject | null>;
34
+ findReceipt(context: Context, subject: PrivacySubjectRef, key?: string): Promise<ErasureReceipt | null>;
35
+ eraseIdentity(context: Context, subject: ResolvedPrivacySubject, actor: AuthorizedHumanAdministrator, pseudonym: string): Promise<void>;
36
+ saveReceipt(context: Context, subject: ResolvedPrivacySubject, actor: AuthorizedHumanAdministrator, key: string, receipt: ErasureReceipt, dispositions: ContributorDisposition[]): Promise<void>;
37
+ }
38
+ export interface PrivacyUseCaseDependencies<Context> {
39
+ unitOfWork: PrivacyUnitOfWork<Context>;
40
+ registry: PersonalDataContributorRegistry<Context>;
41
+ store: PrivacyStore<Context>;
42
+ now(): Date;
43
+ randomId(): string;
44
+ }
45
+ export interface PrivacyOperations {
46
+ export(actor: AuthorizedHumanAdministrator, subject: PrivacySubjectRef): Promise<PrivacyExport>;
47
+ erase(actor: AuthorizedHumanAdministrator, input: { subject: PrivacySubjectRef; idempotencyKey: string }): Promise<ErasureReceipt>;
48
+ }
49
+ /** A host may compose this only after a committed local erasure; never delete an identity. */
50
+ export interface ProviderIdentityErasurePort {
51
+ disableIdentity(input: { principalId: string; committedErasureId: string }): Promise<{ status: "disabled" | "already_disabled" }>;
52
+ }