@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.
- package/CHANGELOG.md +124 -0
- package/README.md +633 -6
- package/dist/communications/accountingDocumentDelivery.d.ts +35 -0
- package/dist/communications/accountingDocumentDelivery.js +33 -0
- package/dist/communications/authEmailError.d.ts +5 -0
- package/dist/communications/authEmailError.js +8 -0
- package/dist/communications/authEmailPorts.d.ts +2 -0
- package/dist/communications/authEmailPorts.js +1 -0
- package/dist/communications/authSendEmailHook.d.ts +35 -0
- package/dist/communications/authSendEmailHook.js +250 -0
- package/dist/communications/capturedTransactionalDelivery.d.ts +13 -0
- package/dist/communications/capturedTransactionalDelivery.js +14 -0
- package/dist/communications/communicationSync.d.ts +11 -0
- package/dist/communications/communicationSync.js +79 -0
- package/dist/communications/communicationSyncWorker.d.ts +68 -0
- package/dist/communications/communicationSyncWorker.js +218 -0
- package/dist/communications/consentPolicy.d.ts +19 -0
- package/dist/communications/consentPolicy.js +43 -0
- package/dist/communications/consentPurposeRegistry.d.ts +4 -0
- package/dist/communications/consentPurposeRegistry.js +24 -0
- package/dist/communications/contracts/abandonedCart.d.ts +27 -0
- package/dist/communications/contracts/abandonedCart.js +25 -0
- package/dist/communications/contracts/authEmail.d.ts +62 -0
- package/dist/communications/contracts/authEmail.js +1 -0
- package/dist/communications/contracts/backInStock.d.ts +28 -0
- package/dist/communications/contracts/backInStock.js +28 -0
- package/dist/communications/contracts/blocks.d.ts +24 -0
- package/dist/communications/contracts/blocks.js +23 -0
- package/dist/communications/contracts/checkoutExpired.d.ts +22 -0
- package/dist/communications/contracts/checkoutExpired.js +25 -0
- package/dist/communications/contracts/checkoutRecovery.d.ts +26 -0
- package/dist/communications/contracts/checkoutRecovery.js +37 -0
- package/dist/communications/contracts/commerceEmailContent.d.ts +176 -0
- package/dist/communications/contracts/commerceEmailContent.js +1 -0
- package/dist/communications/contracts/commerceEngagementEmailContent.d.ts +95 -0
- package/dist/communications/contracts/commerceEngagementEmailContent.js +1 -0
- package/dist/communications/contracts/consent.d.ts +12 -0
- package/dist/communications/contracts/consent.js +1 -0
- package/dist/communications/contracts/email.d.ts +182 -0
- package/dist/communications/contracts/email.js +1 -0
- package/dist/communications/contracts/guideCard.d.ts +3 -0
- package/dist/communications/contracts/guideCard.js +26 -0
- package/dist/communications/contracts/index.d.ts +51 -0
- package/dist/communications/contracts/index.js +42 -0
- package/dist/communications/contracts/inquiryContent.d.ts +32 -0
- package/dist/communications/contracts/inquiryContent.js +33 -0
- package/dist/communications/contracts/invoiceDocumentContent.d.ts +26 -0
- package/dist/communications/contracts/invoiceDocumentContent.js +36 -0
- package/dist/communications/contracts/marketingAutomation.d.ts +52 -0
- package/dist/communications/contracts/marketingAutomation.js +1 -0
- package/dist/communications/contracts/newsletter.d.ts +63 -0
- package/dist/communications/contracts/newsletter.js +1 -0
- package/dist/communications/contracts/newsletterWebhook.d.ts +26 -0
- package/dist/communications/contracts/newsletterWebhook.js +1 -0
- package/dist/communications/contracts/optOutContent.d.ts +11 -0
- package/dist/communications/contracts/optOutContent.js +17 -0
- package/dist/communications/contracts/orderCanceled.d.ts +25 -0
- package/dist/communications/contracts/orderCanceled.js +27 -0
- package/dist/communications/contracts/orderDraft.d.ts +46 -0
- package/dist/communications/contracts/orderDraft.js +46 -0
- package/dist/communications/contracts/orderPaid.d.ts +53 -0
- package/dist/communications/contracts/orderPaid.js +71 -0
- package/dist/communications/contracts/orderRefunded.d.ts +24 -0
- package/dist/communications/contracts/orderRefunded.js +27 -0
- package/dist/communications/contracts/paymentFailed.d.ts +24 -0
- package/dist/communications/contracts/paymentFailed.js +24 -0
- package/dist/communications/contracts/publicIntake.d.ts +49 -0
- package/dist/communications/contracts/publicIntake.js +1 -0
- package/dist/communications/contracts/render.d.ts +2 -0
- package/dist/communications/contracts/render.js +229 -0
- package/dist/communications/contracts/reorderReminder.d.ts +24 -0
- package/dist/communications/contracts/reorderReminder.js +24 -0
- package/dist/communications/contracts/returnEmails.d.ts +25 -0
- package/dist/communications/contracts/returnEmails.js +62 -0
- package/dist/communications/contracts/reviewEffects.d.ts +26 -0
- package/dist/communications/contracts/reviewEffects.js +25 -0
- package/dist/communications/contracts/reviewRequest.d.ts +26 -0
- package/dist/communications/contracts/reviewRequest.js +24 -0
- package/dist/communications/contracts/roleNotificationContent.d.ts +25 -0
- package/dist/communications/contracts/roleNotificationContent.js +46 -0
- package/dist/communications/contracts/shipmentDelivered.d.ts +23 -0
- package/dist/communications/contracts/shipmentDelivered.js +24 -0
- package/dist/communications/contracts/shipmentDispatched.d.ts +27 -0
- package/dist/communications/contracts/shipmentDispatched.js +35 -0
- package/dist/communications/contracts/shipmentException.d.ts +21 -0
- package/dist/communications/contracts/shipmentException.js +26 -0
- package/dist/communications/contracts/subscriptionActivationActionRequired.d.ts +17 -0
- package/dist/communications/contracts/subscriptionActivationActionRequired.js +22 -0
- package/dist/communications/contracts/subscriptionAddressChanged.d.ts +19 -0
- package/dist/communications/contracts/subscriptionAddressChanged.js +25 -0
- package/dist/communications/contracts/subscriptionCancelled.d.ts +19 -0
- package/dist/communications/contracts/subscriptionCancelled.js +25 -0
- package/dist/communications/contracts/subscriptionCycleSkipped.d.ts +25 -0
- package/dist/communications/contracts/subscriptionCycleSkipped.js +40 -0
- package/dist/communications/contracts/subscriptionDeliveryRescheduled.d.ts +25 -0
- package/dist/communications/contracts/subscriptionDeliveryRescheduled.js +39 -0
- package/dist/communications/contracts/subscriptionEmailContent.d.ts +145 -0
- package/dist/communications/contracts/subscriptionEmailContent.js +1 -0
- package/dist/communications/contracts/subscriptionPackageChanged.d.ts +20 -0
- package/dist/communications/contracts/subscriptionPackageChanged.js +26 -0
- package/dist/communications/contracts/subscriptionPauseReminder.d.ts +23 -0
- package/dist/communications/contracts/subscriptionPauseReminder.js +29 -0
- package/dist/communications/contracts/subscriptionPaused.d.ts +21 -0
- package/dist/communications/contracts/subscriptionPaused.js +25 -0
- package/dist/communications/contracts/subscriptionPaymentExpired.d.ts +34 -0
- package/dist/communications/contracts/subscriptionPaymentExpired.js +37 -0
- package/dist/communications/contracts/subscriptionPaymentFailed.d.ts +49 -0
- package/dist/communications/contracts/subscriptionPaymentFailed.js +56 -0
- package/dist/communications/contracts/subscriptionPaymentRecovered.d.ts +19 -0
- package/dist/communications/contracts/subscriptionPaymentRecovered.js +22 -0
- package/dist/communications/contracts/subscriptionRenewalAtRisk.d.ts +36 -0
- package/dist/communications/contracts/subscriptionRenewalAtRisk.js +36 -0
- package/dist/communications/contracts/subscriptionRenewalUpcoming.d.ts +49 -0
- package/dist/communications/contracts/subscriptionRenewalUpcoming.js +43 -0
- package/dist/communications/contracts/subscriptionResumed.d.ts +23 -0
- package/dist/communications/contracts/subscriptionResumed.js +25 -0
- package/dist/communications/contracts/subscriptionWelcome.d.ts +44 -0
- package/dist/communications/contracts/subscriptionWelcome.js +45 -0
- package/dist/communications/contracts/subscriptionWinback.d.ts +21 -0
- package/dist/communications/contracts/subscriptionWinback.js +28 -0
- package/dist/communications/contracts/transactionalDelivery.d.ts +42 -0
- package/dist/communications/contracts/transactionalDelivery.js +1 -0
- package/dist/communications/contracts/types.d.ts +46 -0
- package/dist/communications/contracts/types.js +1 -0
- package/dist/communications/contribution.d.ts +10 -0
- package/dist/communications/contribution.js +24 -0
- package/dist/communications/deliveryReconciliation.d.ts +64 -0
- package/dist/communications/deliveryReconciliation.js +60 -0
- package/dist/communications/emailEgress.d.ts +38 -0
- package/dist/communications/emailEgress.js +100 -0
- package/dist/communications/emailHealthAssessment.d.ts +39 -0
- package/dist/communications/emailHealthAssessment.js +59 -0
- package/dist/communications/emailHealthMetricsPort.d.ts +17 -0
- package/dist/communications/emailHealthMetricsPort.js +1 -0
- package/dist/communications/emailNotificationControl.d.ts +27 -0
- package/dist/communications/emailNotificationControl.js +10 -0
- package/dist/communications/emailObservation.d.ts +28 -0
- package/dist/communications/emailObservation.js +23 -0
- package/dist/communications/emailTransport.d.ts +82 -0
- package/dist/communications/emailTransport.js +95 -0
- package/dist/communications/errors.d.ts +15 -0
- package/dist/communications/errors.js +30 -0
- package/dist/communications/index.d.ts +46 -0
- package/dist/communications/index.js +38 -0
- package/dist/communications/integrationEventSignature.d.ts +15 -0
- package/dist/communications/integrationEventSignature.js +37 -0
- package/dist/communications/managedTransactionalDeliveryStore.d.ts +12 -0
- package/dist/communications/managedTransactionalDeliveryStore.js +49 -0
- package/dist/communications/nativeOperations.d.ts +79 -0
- package/dist/communications/nativeOperations.js +3 -0
- package/dist/communications/newsletterIntegrationContracts.d.ts +130 -0
- package/dist/communications/newsletterIntegrationContracts.js +128 -0
- package/dist/communications/newsletterWebhookPort.d.ts +11 -0
- package/dist/communications/newsletterWebhookPort.js +84 -0
- package/dist/communications/newsletterWebhookProcessing.d.ts +17 -0
- package/dist/communications/newsletterWebhookProcessing.js +71 -0
- package/dist/communications/originPolicy.d.ts +40 -0
- package/dist/communications/originPolicy.js +100 -0
- package/dist/communications/postgresCommunicationSync.d.ts +79 -0
- package/dist/communications/postgresCommunicationSync.js +111 -0
- package/dist/communications/postgresConsentEvaluator.d.ts +19 -0
- package/dist/communications/postgresConsentEvaluator.js +43 -0
- package/dist/communications/postgresDeliveryReconciliation.d.ts +3 -0
- package/dist/communications/postgresDeliveryReconciliation.js +113 -0
- package/dist/communications/postgresNativeOperations.d.ts +13 -0
- package/dist/communications/postgresNativeOperations.js +63 -0
- package/dist/communications/postgresNotificationRecipients.d.ts +37 -0
- package/dist/communications/postgresNotificationRecipients.js +60 -0
- package/dist/communications/postgresTransactionalDeliveryStore.d.ts +7 -0
- package/dist/communications/postgresTransactionalDeliveryStore.js +55 -0
- package/dist/communications/preferencesToken.d.ts +10 -0
- package/dist/communications/preferencesToken.js +40 -0
- package/dist/communications/publicPreferenceToken.d.ts +24 -0
- package/dist/communications/publicPreferenceToken.js +53 -0
- package/dist/communications/shipmentNotifications.d.ts +34 -0
- package/dist/communications/shipmentNotifications.js +61 -0
- package/dist/communications/sql.d.ts +26 -0
- package/dist/communications/sql.js +7 -0
- package/dist/communications/standardWebhooks.d.ts +14 -0
- package/dist/communications/standardWebhooks.js +73 -0
- package/dist/communications/status.d.ts +11 -0
- package/dist/communications/status.js +36 -0
- package/dist/communications/transactionalDelivery.d.ts +19 -0
- package/dist/communications/transactionalDelivery.js +96 -0
- package/dist/communications/transactionalDeliveryIdentifier.d.ts +3 -0
- package/dist/communications/transactionalDeliveryIdentifier.js +6 -0
- package/dist/communications/unsubscribeToken.d.ts +77 -0
- package/dist/communications/unsubscribeToken.js +150 -0
- package/dist/communications/validation.d.ts +2 -0
- package/dist/communications/validation.js +3 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/webhookSignature.d.ts +42 -0
- package/dist/http/webhookSignature.js +86 -0
- package/dist/identity/adminAuthBinding.d.ts +28 -0
- package/dist/identity/adminAuthBinding.js +66 -0
- package/dist/identity/adminAuthorization.d.ts +34 -0
- package/dist/identity/adminAuthorization.js +72 -0
- package/dist/identity/adminContracts.d.ts +70 -0
- package/dist/identity/adminContracts.js +43 -0
- package/dist/identity/adminMagicLinkHandler.d.ts +39 -0
- package/dist/identity/adminMagicLinkHandler.js +94 -0
- package/dist/identity/adminMeHandler.d.ts +15 -0
- package/dist/identity/adminMeHandler.js +49 -0
- package/dist/identity/adminPreflight.d.ts +16 -0
- package/dist/identity/adminPreflight.js +38 -0
- package/dist/identity/adminUserActionPorts.d.ts +31 -0
- package/dist/identity/adminUserActionPorts.js +51 -0
- package/dist/identity/adminUserHandlers.d.ts +38 -0
- package/dist/identity/adminUserHandlers.js +140 -0
- package/dist/identity/authVerifyUrl.d.ts +6 -0
- package/dist/identity/authVerifyUrl.js +54 -0
- package/dist/identity/contracts/contracts.d.ts +8 -0
- package/dist/identity/contracts/contracts.js +2 -0
- package/dist/identity/contracts/emailTrust.d.ts +3 -0
- package/dist/identity/contracts/emailTrust.js +4 -0
- package/dist/identity/contracts/index.d.ts +8 -0
- package/dist/identity/contracts/index.js +8 -0
- package/dist/identity/contracts/oidc.d.ts +3 -0
- package/dist/identity/contracts/oidc.js +9 -0
- package/dist/identity/contracts/operatorAuth.d.ts +35 -0
- package/dist/identity/contracts/operatorAuth.js +53 -0
- package/dist/identity/contracts/operatorInvitations.d.ts +37 -0
- package/dist/identity/contracts/operatorInvitations.js +1 -0
- package/dist/identity/contracts/ports.d.ts +22 -0
- package/dist/identity/contracts/ports.js +1 -0
- package/dist/identity/contracts/privacy.d.ts +62 -0
- package/dist/identity/contracts/privacy.js +99 -0
- package/dist/identity/contracts/providerList.d.ts +3 -0
- package/dist/identity/contracts/providerList.js +11 -0
- package/dist/identity/contracts/types.d.ts +16 -0
- package/dist/identity/contracts/types.js +4 -0
- package/dist/identity/contribution.d.ts +8 -0
- package/dist/identity/contribution.js +22 -0
- package/dist/identity/customerAuth.d.ts +8 -0
- package/dist/identity/customerAuth.js +1 -0
- package/dist/identity/customerAuthorization.d.ts +5 -0
- package/dist/identity/customerAuthorization.js +22 -0
- package/dist/identity/customerContracts.d.ts +42 -0
- package/dist/identity/customerContracts.js +43 -0
- package/dist/identity/customerIdentityBinding.d.ts +44 -0
- package/dist/identity/customerIdentityBinding.js +60 -0
- package/dist/identity/customerIdentityVerifier.d.ts +8 -0
- package/dist/identity/customerIdentityVerifier.js +101 -0
- package/dist/identity/customerMagicLinkHandler.d.ts +38 -0
- package/dist/identity/customerMagicLinkHandler.js +115 -0
- package/dist/identity/customerMeHandler.d.ts +11 -0
- package/dist/identity/customerMeHandler.js +46 -0
- package/dist/identity/customerPorts.d.ts +4 -0
- package/dist/identity/customerPorts.js +1 -0
- package/dist/identity/customerSessionBinding.d.ts +38 -0
- package/dist/identity/customerSessionBinding.js +80 -0
- package/dist/identity/customerSessionIssuer.d.ts +10 -0
- package/dist/identity/customerSessionIssuer.js +57 -0
- package/dist/identity/customerVerifyOtpHandler.d.ts +8 -0
- package/dist/identity/customerVerifyOtpHandler.js +41 -0
- package/dist/identity/directOperatorInvitationStore.d.ts +19 -0
- package/dist/identity/directOperatorInvitationStore.js +193 -0
- package/dist/identity/emailTrustPolicy.d.ts +14 -0
- package/dist/identity/emailTrustPolicy.js +16 -0
- package/dist/identity/identityPersonalDataContributor.d.ts +7 -0
- package/dist/identity/identityPersonalDataContributor.js +46 -0
- package/dist/identity/identityReadiness.d.ts +10 -0
- package/dist/identity/identityReadiness.js +25 -0
- package/dist/identity/index.d.ts +67 -0
- package/dist/identity/index.js +66 -0
- package/dist/identity/inviteAdminUserUseCase.d.ts +89 -0
- package/dist/identity/inviteAdminUserUseCase.js +75 -0
- package/dist/identity/jwtLibrary.d.ts +54 -0
- package/dist/identity/jwtLibrary.js +1 -0
- package/dist/identity/machineActorWriteFence.d.ts +4 -0
- package/dist/identity/machineActorWriteFence.js +13 -0
- package/dist/identity/magicLinkPorts.d.ts +28 -0
- package/dist/identity/magicLinkPorts.js +1 -0
- package/dist/identity/managedAccountLinkStore.d.ts +2 -0
- package/dist/identity/managedAccountLinkStore.js +93 -0
- package/dist/identity/managedAdminAuthVerifier.d.ts +7 -0
- package/dist/identity/managedAdminAuthVerifier.js +27 -0
- package/dist/identity/managedAdminUserGateways.d.ts +123 -0
- package/dist/identity/managedAdminUserGateways.js +127 -0
- package/dist/identity/managedClient.d.ts +90 -0
- package/dist/identity/managedClient.js +10 -0
- package/dist/identity/managedClientAuthLink.d.ts +3 -0
- package/dist/identity/managedClientAuthLink.js +115 -0
- package/dist/identity/managedCustomerMe.d.ts +13 -0
- package/dist/identity/managedCustomerMe.js +30 -0
- package/dist/identity/managedEmailTrustPolicy.d.ts +11 -0
- package/dist/identity/managedEmailTrustPolicy.js +105 -0
- package/dist/identity/managedIdentityVerifier.d.ts +2 -0
- package/dist/identity/managedIdentityVerifier.js +16 -0
- package/dist/identity/managedPrivacy.d.ts +4 -0
- package/dist/identity/managedPrivacy.js +39 -0
- package/dist/identity/messages.d.ts +29 -0
- package/dist/identity/messages.js +39 -0
- package/dist/identity/oidcIdentityVerifier.d.ts +45 -0
- package/dist/identity/oidcIdentityVerifier.js +309 -0
- package/dist/identity/oidcPrincipal.d.ts +2 -0
- package/dist/identity/oidcPrincipal.js +10 -0
- package/dist/identity/operatorAdminAuth.d.ts +6 -0
- package/dist/identity/operatorAdminAuth.js +13 -0
- package/dist/identity/operatorAuthHandlers.d.ts +18 -0
- package/dist/identity/operatorAuthHandlers.js +71 -0
- package/dist/identity/operatorIdentityVerifier.d.ts +8 -0
- package/dist/identity/operatorIdentityVerifier.js +19 -0
- package/dist/identity/operatorInvitationLifecycle.d.ts +7 -0
- package/dist/identity/operatorInvitationLifecycle.js +15 -0
- package/dist/identity/operatorInvitationPorts.d.ts +31 -0
- package/dist/identity/operatorInvitationPorts.js +1 -0
- package/dist/identity/operatorPorts.d.ts +37 -0
- package/dist/identity/operatorPorts.js +1 -0
- package/dist/identity/operatorSession.d.ts +13 -0
- package/dist/identity/operatorSession.js +31 -0
- package/dist/identity/operatorTokenAdminAuth.d.ts +15 -0
- package/dist/identity/operatorTokenAdminAuth.js +50 -0
- package/dist/identity/personalDataRegistry.d.ts +4 -0
- package/dist/identity/personalDataRegistry.js +11 -0
- package/dist/identity/platformAdminAuth.d.ts +15 -0
- package/dist/identity/platformAdminAuth.js +1 -0
- package/dist/identity/ports.d.ts +127 -0
- package/dist/identity/ports.js +38 -0
- package/dist/identity/postgresAccountLinkStore.d.ts +7 -0
- package/dist/identity/postgresAccountLinkStore.js +52 -0
- package/dist/identity/postgresCustomerMe.d.ts +3 -0
- package/dist/identity/postgresCustomerMe.js +21 -0
- package/dist/identity/postgresCustomerRecovery.d.ts +11 -0
- package/dist/identity/postgresCustomerRecovery.js +57 -0
- package/dist/identity/postgresOperatorIdentity.d.ts +5 -0
- package/dist/identity/postgresOperatorIdentity.js +137 -0
- package/dist/identity/postgresPrivacy.d.ts +7 -0
- package/dist/identity/postgresPrivacy.js +126 -0
- package/dist/identity/privacyHandlers.d.ts +10 -0
- package/dist/identity/privacyHandlers.js +35 -0
- package/dist/identity/privacyPorts.d.ts +72 -0
- package/dist/identity/privacyPorts.js +8 -0
- package/dist/identity/privacyUseCases.d.ts +5 -0
- package/dist/identity/privacyUseCases.js +55 -0
- package/dist/identity/reconcileAccountForPrincipal.d.ts +20 -0
- package/dist/identity/reconcileAccountForPrincipal.js +66 -0
- package/dist/identity/reconcileAccountHandler.d.ts +11 -0
- package/dist/identity/reconcileAccountHandler.js +30 -0
- package/dist/identity/removeAdminUserUseCase.d.ts +31 -0
- package/dist/identity/removeAdminUserUseCase.js +60 -0
- package/dist/identity/response.d.ts +3 -0
- package/dist/identity/response.js +2 -0
- package/dist/identity/sql.d.ts +44 -0
- package/dist/identity/sql.js +10 -0
- package/package.json +22 -2
- package/release-gates.json +63 -8
- package/sql/20260807190000_comms_outbox_rail.sql +329 -0
- package/sql/20260810120001_transactional_delivery_receipts.sql +209 -0
- package/sql/20260810180001_self_host_actor_auth.sql +40 -0
- package/sql/20260810204057_customer_identity_read.sql +31 -0
- package/sql/20260813120000_communications_control_plane.sql +568 -0
- package/sql/20260814090000_customer_selfservice_recovery.sql +477 -0
- package/sql/20260901143000_admin_membership_authority_foundation.sql +234 -0
- package/sql/20261018120000_identity_operator_invitations.sql +42 -0
- package/sql/20261018180000_identity_operator_challenges.sql +56 -0
- package/sql/20261018190000_identity_privacy_erasures.sql +15 -0
- package/sql/20261019200000_communications_native_operations.sql +68 -0
- package/src/communications/accountingDocumentDelivery.ts +63 -0
- package/src/communications/authEmailError.ts +10 -0
- package/src/communications/authEmailPorts.ts +2 -0
- package/src/communications/authSendEmailHook.ts +310 -0
- package/src/communications/capturedTransactionalDelivery.ts +29 -0
- package/src/communications/communicationSync.ts +96 -0
- package/src/communications/communicationSyncWorker.ts +302 -0
- package/src/communications/consentPolicy.ts +82 -0
- package/src/communications/consentPurposeRegistry.ts +31 -0
- package/src/communications/contracts/abandonedCart.ts +64 -0
- package/src/communications/contracts/authEmail.ts +73 -0
- package/src/communications/contracts/backInStock.ts +69 -0
- package/src/communications/contracts/blocks.ts +47 -0
- package/src/communications/contracts/checkoutExpired.ts +62 -0
- package/src/communications/contracts/checkoutRecovery.ts +68 -0
- package/src/communications/contracts/commerceEmailContent.ts +189 -0
- package/src/communications/contracts/commerceEngagementEmailContent.ts +106 -0
- package/src/communications/contracts/consent.ts +13 -0
- package/src/communications/contracts/email.ts +210 -0
- package/src/communications/contracts/guideCard.ts +34 -0
- package/src/communications/contracts/index.ts +51 -0
- package/src/communications/contracts/inquiryContent.ts +53 -0
- package/src/communications/contracts/invoiceDocumentContent.ts +57 -0
- package/src/communications/contracts/marketingAutomation.ts +45 -0
- package/src/communications/contracts/newsletter.ts +78 -0
- package/src/communications/contracts/newsletterWebhook.ts +23 -0
- package/src/communications/contracts/optOutContent.ts +23 -0
- package/src/communications/contracts/orderCanceled.ts +64 -0
- package/src/communications/contracts/orderDraft.ts +112 -0
- package/src/communications/contracts/orderPaid.ts +147 -0
- package/src/communications/contracts/orderRefunded.ts +63 -0
- package/src/communications/contracts/paymentFailed.ts +59 -0
- package/src/communications/contracts/publicIntake.ts +53 -0
- package/src/communications/contracts/render.ts +271 -0
- package/src/communications/contracts/reorderReminder.ts +59 -0
- package/src/communications/contracts/returnEmails.ts +113 -0
- package/src/communications/contracts/reviewEffects.ts +60 -0
- package/src/communications/contracts/reviewRequest.ts +59 -0
- package/src/communications/contracts/roleNotificationContent.ts +65 -0
- package/src/communications/contracts/shipmentDelivered.ts +59 -0
- package/src/communications/contracts/shipmentDispatched.ts +77 -0
- package/src/communications/contracts/shipmentException.ts +62 -0
- package/src/communications/contracts/subscriptionActivationActionRequired.ts +37 -0
- package/src/communications/contracts/subscriptionAddressChanged.ts +56 -0
- package/src/communications/contracts/subscriptionCancelled.ts +57 -0
- package/src/communications/contracts/subscriptionCycleSkipped.ts +80 -0
- package/src/communications/contracts/subscriptionDeliveryRescheduled.ts +79 -0
- package/src/communications/contracts/subscriptionEmailContent.ts +163 -0
- package/src/communications/contracts/subscriptionPackageChanged.ts +59 -0
- package/src/communications/contracts/subscriptionPauseReminder.ts +63 -0
- package/src/communications/contracts/subscriptionPaused.ts +58 -0
- package/src/communications/contracts/subscriptionPaymentExpired.ts +83 -0
- package/src/communications/contracts/subscriptionPaymentFailed.ts +117 -0
- package/src/communications/contracts/subscriptionPaymentRecovered.ts +50 -0
- package/src/communications/contracts/subscriptionRenewalAtRisk.ts +85 -0
- package/src/communications/contracts/subscriptionRenewalUpcoming.ts +103 -0
- package/src/communications/contracts/subscriptionResumed.ts +60 -0
- package/src/communications/contracts/subscriptionWelcome.ts +102 -0
- package/src/communications/contracts/subscriptionWinback.ts +62 -0
- package/src/communications/contracts/transactionalDelivery.ts +43 -0
- package/src/communications/contracts/types.ts +58 -0
- package/src/communications/contribution.ts +33 -0
- package/src/communications/deliveryReconciliation.ts +108 -0
- package/src/communications/emailEgress.ts +111 -0
- package/src/communications/emailHealthAssessment.ts +106 -0
- package/src/communications/emailHealthMetricsPort.ts +17 -0
- package/src/communications/emailNotificationControl.ts +28 -0
- package/src/communications/emailObservation.ts +43 -0
- package/src/communications/emailTransport.ts +167 -0
- package/src/communications/errors.ts +34 -0
- package/src/communications/index.ts +46 -0
- package/src/communications/integrationEventSignature.ts +49 -0
- package/src/communications/managedTransactionalDeliveryStore.ts +59 -0
- package/src/communications/nativeOperations.ts +40 -0
- package/src/communications/newsletterIntegrationContracts.ts +154 -0
- package/src/communications/newsletterWebhookPort.ts +104 -0
- package/src/communications/newsletterWebhookProcessing.ts +102 -0
- package/src/communications/originPolicy.ts +151 -0
- package/src/communications/postgresCommunicationSync.ts +127 -0
- package/src/communications/postgresConsentEvaluator.ts +49 -0
- package/src/communications/postgresDeliveryReconciliation.ts +101 -0
- package/src/communications/postgresNativeOperations.ts +55 -0
- package/src/communications/postgresNotificationRecipients.ts +57 -0
- package/src/communications/postgresTransactionalDeliveryStore.ts +79 -0
- package/src/communications/preferencesToken.ts +62 -0
- package/src/communications/publicPreferenceToken.ts +68 -0
- package/src/communications/shipmentNotifications.ts +78 -0
- package/src/communications/sql.ts +7 -0
- package/src/communications/standardWebhooks.ts +84 -0
- package/src/communications/status.ts +51 -0
- package/src/communications/transactionalDelivery.ts +114 -0
- package/src/communications/transactionalDeliveryIdentifier.ts +9 -0
- package/src/communications/unsubscribeToken.ts +186 -0
- package/src/communications/validation.ts +3 -0
- package/src/http/index.ts +1 -0
- package/src/http/webhookSignature.ts +96 -0
- package/src/identity/adminAuthBinding.ts +76 -0
- package/src/identity/adminAuthorization.ts +106 -0
- package/src/identity/adminContracts.ts +52 -0
- package/src/identity/adminMagicLinkHandler.ts +132 -0
- package/src/identity/adminMeHandler.ts +64 -0
- package/src/identity/adminPreflight.ts +46 -0
- package/src/identity/adminUserActionPorts.ts +78 -0
- package/src/identity/adminUserHandlers.ts +174 -0
- package/src/identity/authVerifyUrl.ts +56 -0
- package/src/identity/contracts/contracts.ts +15 -0
- package/src/identity/contracts/emailTrust.ts +5 -0
- package/src/identity/contracts/index.ts +8 -0
- package/src/identity/contracts/oidc.ts +10 -0
- package/src/identity/contracts/operatorAuth.ts +61 -0
- package/src/identity/contracts/operatorInvitations.ts +32 -0
- package/src/identity/contracts/ports.ts +25 -0
- package/src/identity/contracts/privacy.ts +86 -0
- package/src/identity/contracts/providerList.ts +13 -0
- package/src/identity/contracts/types.ts +25 -0
- package/src/identity/contribution.ts +29 -0
- package/src/identity/customerAuth.ts +3 -0
- package/src/identity/customerAuthorization.ts +38 -0
- package/src/identity/customerContracts.ts +52 -0
- package/src/identity/customerIdentityBinding.ts +125 -0
- package/src/identity/customerIdentityVerifier.ts +141 -0
- package/src/identity/customerMagicLinkHandler.ts +171 -0
- package/src/identity/customerMeHandler.ts +64 -0
- package/src/identity/customerPorts.ts +2 -0
- package/src/identity/customerSessionBinding.ts +114 -0
- package/src/identity/customerSessionIssuer.ts +71 -0
- package/src/identity/customerVerifyOtpHandler.ts +44 -0
- package/src/identity/directOperatorInvitationStore.ts +178 -0
- package/src/identity/emailTrustPolicy.ts +16 -0
- package/src/identity/identityPersonalDataContributor.ts +48 -0
- package/src/identity/identityReadiness.ts +26 -0
- package/src/identity/index.ts +69 -0
- package/src/identity/inviteAdminUserUseCase.ts +134 -0
- package/src/identity/jwtLibrary.ts +24 -0
- package/src/identity/machineActorWriteFence.ts +19 -0
- package/src/identity/magicLinkPorts.ts +7 -0
- package/src/identity/managedAccountLinkStore.ts +105 -0
- package/src/identity/managedAdminAuthVerifier.ts +40 -0
- package/src/identity/managedAdminUserGateways.ts +164 -0
- package/src/identity/managedClient.ts +58 -0
- package/src/identity/managedClientAuthLink.ts +120 -0
- package/src/identity/managedCustomerMe.ts +52 -0
- package/src/identity/managedEmailTrustPolicy.ts +90 -0
- package/src/identity/managedIdentityVerifier.ts +20 -0
- package/src/identity/managedPrivacy.ts +37 -0
- package/src/identity/messages.ts +41 -0
- package/src/identity/oidcIdentityVerifier.ts +229 -0
- package/src/identity/oidcPrincipal.ts +10 -0
- package/src/identity/operatorAdminAuth.ts +14 -0
- package/src/identity/operatorAuthHandlers.ts +52 -0
- package/src/identity/operatorIdentityVerifier.ts +19 -0
- package/src/identity/operatorInvitationLifecycle.ts +14 -0
- package/src/identity/operatorInvitationPorts.ts +11 -0
- package/src/identity/operatorPorts.ts +11 -0
- package/src/identity/operatorSession.ts +30 -0
- package/src/identity/operatorTokenAdminAuth.ts +79 -0
- package/src/identity/personalDataRegistry.ts +8 -0
- package/src/identity/platformAdminAuth.ts +7 -0
- package/src/identity/ports.ts +158 -0
- package/src/identity/postgresAccountLinkStore.ts +49 -0
- package/src/identity/postgresCustomerMe.ts +33 -0
- package/src/identity/postgresCustomerRecovery.ts +72 -0
- package/src/identity/postgresOperatorIdentity.ts +123 -0
- package/src/identity/postgresPrivacy.ts +117 -0
- package/src/identity/privacyHandlers.ts +33 -0
- package/src/identity/privacyPorts.ts +52 -0
- package/src/identity/privacyUseCases.ts +48 -0
- package/src/identity/reconcileAccountForPrincipal.ts +99 -0
- package/src/identity/reconcileAccountHandler.ts +36 -0
- package/src/identity/removeAdminUserUseCase.ts +85 -0
- package/src/identity/response.ts +3 -0
- package/src/identity/sql.ts +10 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createPostgresRoleFreeTransactionLane, type PgQueryExecutor, type PostgresDataGatewayEnv, type PostgresDataGatewayOptions } from "../data/index.js";
|
|
2
|
+
import type { OperatorInvitationStore } from "./operatorInvitationPorts.js";
|
|
3
|
+
import type { OperatorInvitationResult, OperatorRole } from "./contracts/operatorInvitations.js";
|
|
4
|
+
import { assertOperatorInvitationTtl, invitationFailure } from "./operatorInvitationLifecycle.js";
|
|
5
|
+
|
|
6
|
+
type Row = Record<string, unknown>;
|
|
7
|
+
const iso = (value: unknown): string => value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
8
|
+
const role = (value: unknown): OperatorRole => value === "distributor" ? "distributor" : "admin";
|
|
9
|
+
const snapshot = (row: Row) => ({ principalId: String(row.principal_id), role: role(row.role) });
|
|
10
|
+
function result(row: Row, code: OperatorInvitationResult["code"]): OperatorInvitationResult {
|
|
11
|
+
return { ...snapshot(row), code, membershipState: "invited", invitationId: String(row.invitation_id), invitedAt: iso(row.invited_at), expiresAt: iso(row.expires_at) };
|
|
12
|
+
}
|
|
13
|
+
async function event(db: PgQueryExecutor, action: string, before: Row | null, after: Row, actor: string | null): Promise<void> {
|
|
14
|
+
await db.query(`INSERT INTO public.identity_operator_invitation_events
|
|
15
|
+
(event_id, invitation_id, principal_id, action, actor_kind, actor_principal_id, old_value, new_value, occurred_at)
|
|
16
|
+
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, clock_timestamp())`,
|
|
17
|
+
[after.invitation_id, after.principal_id, action, actor ? "human" : "system", actor, JSON.stringify(before ?? {}), JSON.stringify(after)]);
|
|
18
|
+
}
|
|
19
|
+
async function lock(db: PgQueryExecutor, id: string): Promise<Row | null> {
|
|
20
|
+
const principal = (await db.query("SELECT principal_id, email FROM public.platform_human_principals WHERE principal_id = $1", [id])).rows[0];
|
|
21
|
+
if (!principal) return null;
|
|
22
|
+
await db.query("SELECT pg_advisory_xact_lock(hashtext($1)::bigint)", [`operator_invitation:${String(principal.email).trim().toLowerCase()}`]);
|
|
23
|
+
return principal;
|
|
24
|
+
}
|
|
25
|
+
async function membership(db: PgQueryExecutor, id: string): Promise<Row | null> {
|
|
26
|
+
return (await db.query("SELECT * FROM public.platform_human_memberships WHERE principal_id = $1 FOR UPDATE", [id])).rows[0] ?? null;
|
|
27
|
+
}
|
|
28
|
+
async function machine(db: PgQueryExecutor, id: string): Promise<boolean> {
|
|
29
|
+
return (await db.query("SELECT EXISTS (SELECT 1 FROM public.platform_control_operators WHERE principal_id = $1) AS present", [id])).rows[0]?.present === true;
|
|
30
|
+
}
|
|
31
|
+
async function lockCommandMemberships(db: PgQueryExecutor, actor: string, target: string): Promise<void> {
|
|
32
|
+
// Lock both rows in the same order before taking either actor or target locks.
|
|
33
|
+
await db.query("SELECT principal_id FROM public.platform_human_memberships WHERE principal_id = ANY($1::uuid[]) ORDER BY principal_id FOR UPDATE", [[actor, target]]);
|
|
34
|
+
}
|
|
35
|
+
async function administrator(db: PgQueryExecutor, id: string): Promise<void> {
|
|
36
|
+
const row = (await db.query("SELECT role, membership_state FROM public.platform_human_memberships WHERE principal_id = $1 FOR SHARE", [id])).rows[0];
|
|
37
|
+
if (row?.role !== "admin" || row.membership_state !== "active" || await machine(db, id)) throw invitationFailure("HUMAN_ADMIN_REQUIRED", "FORBIDDEN");
|
|
38
|
+
}
|
|
39
|
+
async function invitation(db: PgQueryExecutor, id: string): Promise<Row | null> {
|
|
40
|
+
const row = (await db.query("SELECT * FROM public.identity_operator_invitations WHERE principal_id = $1 FOR UPDATE", [id])).rows[0];
|
|
41
|
+
if (!row) return null;
|
|
42
|
+
// Capture the database clock after serialization; equality is already expired.
|
|
43
|
+
const expiredNow = (await db.query("SELECT clock_timestamp() >= $1::timestamptz AS expired", [row.expires_at])).rows[0]!.expired;
|
|
44
|
+
if (row.phase === "invited" && expiredNow === true) {
|
|
45
|
+
const expired = (await db.query("UPDATE public.identity_operator_invitations SET phase = 'expired', closed_at = expires_at WHERE principal_id = $1 RETURNING *", [id])).rows[0]!;
|
|
46
|
+
await event(db, "expire", row, expired, null);
|
|
47
|
+
return expired;
|
|
48
|
+
}
|
|
49
|
+
return row;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Owner-authority lane, never the actor gateway. One transaction per command. */
|
|
53
|
+
export function createDirectOperatorInvitationStore(env: PostgresDataGatewayEnv, options: PostgresDataGatewayOptions = {}): OperatorInvitationStore & { close(): Promise<void> } {
|
|
54
|
+
const lane = createPostgresRoleFreeTransactionLane(env, options);
|
|
55
|
+
const run = <T>(work: (db: PgQueryExecutor) => Promise<T>) => lane.run((db) => work(db as PgQueryExecutor));
|
|
56
|
+
return {
|
|
57
|
+
issue(input) {
|
|
58
|
+
assertOperatorInvitationTtl(input.ttlSeconds);
|
|
59
|
+
return run(async (db) => {
|
|
60
|
+
// Serialize same-target commands, then lock both memberships in order.
|
|
61
|
+
const principal = await lock(db, input.targetPrincipalId);
|
|
62
|
+
await lockCommandMemberships(db, input.actorPrincipalId, input.targetPrincipalId);
|
|
63
|
+
await administrator(db, input.actorPrincipalId);
|
|
64
|
+
if ((await db.query("SELECT 1 FROM public.identity_privacy_erasures WHERE principal_id = $1", [input.targetPrincipalId])).rows.length) return { code: "IDENTITY_CONFLICT" };
|
|
65
|
+
const email = input.email.trim().toLowerCase();
|
|
66
|
+
if (!principal || String(principal.email).trim().toLowerCase() !== email || await machine(db, input.targetPrincipalId)) return { code: "IDENTITY_CONFLICT" };
|
|
67
|
+
const member = await membership(db, input.targetPrincipalId);
|
|
68
|
+
if (member?.membership_state === "active") return { code: "MEMBERSHIP_ALREADY_ACTIVE" };
|
|
69
|
+
const previous = await invitation(db, input.targetPrincipalId);
|
|
70
|
+
if (previous?.phase === "invited") return previous.role === input.role ? result(previous, "ALREADY_INVITED") : { code: "INVITATION_ROLE_CONFLICT" };
|
|
71
|
+
const collision = (await db.query("SELECT principal_id FROM public.identity_operator_invitations WHERE email_key = $1", [email])).rows[0];
|
|
72
|
+
if (collision && collision.principal_id !== input.targetPrincipalId) return { code: "IDENTITY_CONFLICT" };
|
|
73
|
+
const row = (await db.query(`WITH stamp AS (SELECT clock_timestamp() AS at)
|
|
74
|
+
INSERT INTO public.identity_operator_invitations
|
|
75
|
+
(principal_id, invitation_id, email_key, role, phase, invited_by, invited_at, expires_at)
|
|
76
|
+
SELECT $1, gen_random_uuid(), $2, $3, 'invited', $4, stamp.at, stamp.at + $5 * interval '1 second' FROM stamp WHERE true
|
|
77
|
+
ON CONFLICT (principal_id) DO UPDATE SET invitation_id = EXCLUDED.invitation_id, email_key = EXCLUDED.email_key,
|
|
78
|
+
role = EXCLUDED.role, phase = 'invited', invited_by = EXCLUDED.invited_by, invited_at = EXCLUDED.invited_at,
|
|
79
|
+
expires_at = EXCLUDED.expires_at, accepted_at = NULL, closed_at = NULL RETURNING *`,
|
|
80
|
+
[input.targetPrincipalId, email, input.role, input.actorPrincipalId, input.ttlSeconds])).rows[0]!;
|
|
81
|
+
await event(db, previous || member ? "reissue" : "issue", previous, row, input.actorPrincipalId);
|
|
82
|
+
return result(row, previous || member ? "INVITATION_REISSUED" : "INVITATION_CREATED");
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
read(id) {
|
|
86
|
+
return run(async (db) => {
|
|
87
|
+
if (!await lock(db, id)) return null;
|
|
88
|
+
const member = await membership(db, id);
|
|
89
|
+
if (member?.membership_state === "active") return { ...snapshot(member), membershipState: "active" };
|
|
90
|
+
const pending = await invitation(db, id);
|
|
91
|
+
if (pending && pending.phase !== "accepted") return { ...snapshot(pending), membershipState: pending.phase === "invited" ? "invited" : pending.phase === "expired" ? "expired" : "revoked" };
|
|
92
|
+
return member ? { ...snapshot(member), membershipState: "revoked" } : null;
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
acceptOperatorInvitation(id) {
|
|
96
|
+
return run((db) => acceptDirectOperatorInvitationInTransaction(db, id));
|
|
97
|
+
},
|
|
98
|
+
remove(input) {
|
|
99
|
+
return run(async (db) => {
|
|
100
|
+
const principal = await lock(db, input.targetPrincipalId);
|
|
101
|
+
await lockCommandMemberships(db, input.actorPrincipalId, input.targetPrincipalId);
|
|
102
|
+
await administrator(db, input.actorPrincipalId);
|
|
103
|
+
if (input.actorPrincipalId === input.targetPrincipalId) throw invitationFailure("SELF_REVOKE_FORBIDDEN", "FORBIDDEN");
|
|
104
|
+
if (!principal || await machine(db, input.targetPrincipalId)) throw invitationFailure("IDENTITY_CONFLICT");
|
|
105
|
+
const member = await membership(db, input.targetPrincipalId);
|
|
106
|
+
const pending = await invitation(db, input.targetPrincipalId);
|
|
107
|
+
if (pending?.phase === "invited") {
|
|
108
|
+
const cancelled = (await db.query("UPDATE public.identity_operator_invitations SET phase = 'cancelled', closed_at = clock_timestamp() WHERE principal_id = $1 RETURNING *", [input.targetPrincipalId])).rows[0]!;
|
|
109
|
+
await event(db, "cancel", pending, cancelled, input.actorPrincipalId);
|
|
110
|
+
}
|
|
111
|
+
if (member?.membership_state === "active") await db.query("SELECT public.platform_revoke_human_membership($1, $2, $3)", [input.actorPrincipalId, input.targetPrincipalId, input.reason ?? null]);
|
|
112
|
+
if (!member && !pending) throw invitationFailure("ADMIN_NOT_FOUND", "BAD_REQUEST");
|
|
113
|
+
return { revoked: true };
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
close: lane.close,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Principal creation commits independently; an invitation failure leaves it reusable. */
|
|
121
|
+
export function createDirectOperatorIdentityDirectory(env: PostgresDataGatewayEnv, options: PostgresDataGatewayOptions = {}) {
|
|
122
|
+
const lane = createPostgresRoleFreeTransactionLane(env, options);
|
|
123
|
+
return {
|
|
124
|
+
ensurePrincipal(input: { email: string; redirectTo: string }): Promise<{ principalId: string; created: boolean }> {
|
|
125
|
+
const email = input.email.trim().toLowerCase();
|
|
126
|
+
return lane.run(async (executor) => {
|
|
127
|
+
const db = executor as PgQueryExecutor;
|
|
128
|
+
await db.query("SELECT pg_advisory_xact_lock(hashtext($1)::bigint)", [`operator_invitation:${email}`]);
|
|
129
|
+
const matches = (await db.query("SELECT principal_id FROM public.platform_human_principals WHERE lower(btrim(email)) = $1", [email])).rows;
|
|
130
|
+
if (matches.length > 1) throw invitationFailure("IDENTITY_CONFLICT");
|
|
131
|
+
if (matches[0]) {
|
|
132
|
+
const principalId = String(matches[0].principal_id);
|
|
133
|
+
if (await machine(db, principalId)) throw invitationFailure("IDENTITY_CONFLICT");
|
|
134
|
+
return { principalId, created: false };
|
|
135
|
+
}
|
|
136
|
+
const row = (await db.query("INSERT INTO public.platform_human_principals (principal_id, email) VALUES (gen_random_uuid(), $1) RETURNING principal_id", [email])).rows[0]!;
|
|
137
|
+
return { principalId: String(row.principal_id), created: true };
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
close: lane.close,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Trusted authentication only; caller owns the transaction, including challenge consumption. */
|
|
145
|
+
export async function acceptDirectOperatorInvitationInTransaction(db: PgQueryExecutor, id: string): Promise<import("./contracts/operatorInvitations.js").OperatorInvitationAcceptance> {
|
|
146
|
+
if (!await lock(db, id) || await machine(db, id)) return { code: "NO_INVITATION" };
|
|
147
|
+
if ((await db.query("SELECT 1 FROM public.identity_privacy_erasures WHERE principal_id = $1", [id])).rows.length) return { code: "INVITATION_REVOKED" };
|
|
148
|
+
const member = await membership(db, id);
|
|
149
|
+
if (member?.membership_state === "active") {
|
|
150
|
+
if (member.role !== "admin" && member.role !== "distributor") return { code: "NO_INVITATION" };
|
|
151
|
+
return { code: "ALREADY_ACTIVE", role: member.role };
|
|
152
|
+
}
|
|
153
|
+
const pending = await invitation(db, id);
|
|
154
|
+
if (!pending) return { code: "NO_INVITATION" };
|
|
155
|
+
if (pending.phase === "expired") return { code: "INVITATION_EXPIRED" };
|
|
156
|
+
if (pending.phase !== "invited" || (pending.role !== "admin" && pending.role !== "distributor")) return { code: "INVITATION_REVOKED" };
|
|
157
|
+
const clock = (await db.query("WITH stamp AS (SELECT clock_timestamp() AS at) SELECT at, at >= $1::timestamptz AS expired FROM stamp", [pending.expires_at])).rows[0]!;
|
|
158
|
+
const at = clock.at;
|
|
159
|
+
if (clock.expired === true) {
|
|
160
|
+
const expired = (await db.query("UPDATE public.identity_operator_invitations SET phase = 'expired', closed_at = expires_at WHERE principal_id = $1 RETURNING *", [id])).rows[0]!;
|
|
161
|
+
await event(db, "expire", pending, expired, null);
|
|
162
|
+
return { code: "INVITATION_EXPIRED" };
|
|
163
|
+
}
|
|
164
|
+
if (member) {
|
|
165
|
+
const setting = (await db.query("SELECT current_setting('app.platform_human_membership_revoke', true) AS previous")).rows[0]?.previous;
|
|
166
|
+
await db.query("SELECT set_config('app.platform_human_membership_revoke', 'true', true)");
|
|
167
|
+
await db.query(`UPDATE public.platform_human_memberships SET membership_state = 'active', membership_provenance = 'invitation',
|
|
168
|
+
membership_accepted_at = $2, membership_revoked_at = NULL, membership_revoked_by = NULL, membership_revocation_reason = NULL WHERE principal_id = $1`, [id, at]);
|
|
169
|
+
await db.query("UPDATE public.platform_human_memberships SET role = $2 WHERE principal_id = $1", [id, pending.role]);
|
|
170
|
+
await db.query("SELECT set_config('app.platform_human_membership_revoke', $1, true)", [setting ?? ""]);
|
|
171
|
+
} else {
|
|
172
|
+
await db.query(`INSERT INTO public.platform_human_memberships (principal_id, role, membership_state, membership_provenance, membership_accepted_at)
|
|
173
|
+
VALUES ($1, $2, 'active', 'invitation', $3)`, [id, pending.role, at]);
|
|
174
|
+
}
|
|
175
|
+
const accepted = (await db.query("UPDATE public.identity_operator_invitations SET phase = 'accepted', accepted_at = $2, closed_at = $2 WHERE principal_id = $1 RETURNING *", [id, at])).rows[0]!;
|
|
176
|
+
await event(db, "accept", pending, accepted, id);
|
|
177
|
+
return { code: "ACTIVATED", role: role(pending.role) };
|
|
178
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EmailTrustFailureCode } from "./contracts/emailTrust.js";
|
|
2
|
+
export type EmailTrustAssessment = { trusted: true } | { trusted: false; code: EmailTrustFailureCode };
|
|
3
|
+
export interface EmailTrustPolicy { assess(): Promise<EmailTrustAssessment> }
|
|
4
|
+
/** Explicitly retain verifier-only admission. This does not bypass mailbox verification. */
|
|
5
|
+
export function createVerifierResultOnlyEmailTrustPolicy(): EmailTrustPolicy {
|
|
6
|
+
return { async assess() { return { trusted: true }; } };
|
|
7
|
+
}
|
|
8
|
+
/** Invalid, absent or throwing JavaScript bindings fail closed, including invalid results. */
|
|
9
|
+
export async function assessEmailTrust(policy: EmailTrustPolicy): Promise<EmailTrustAssessment> {
|
|
10
|
+
try {
|
|
11
|
+
const result = await policy.assess();
|
|
12
|
+
if (result?.trusted === true) return { trusted: true };
|
|
13
|
+
if (result?.trusted === false && result.code === "EMAIL_TRUST_ENVELOPE_REFUSED") return result;
|
|
14
|
+
} catch { /* Do not expose policy or transport details. */ }
|
|
15
|
+
return { trusted: false, code: "EMAIL_TRUST_SETTINGS_UNAVAILABLE" };
|
|
16
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PersonalDataObligation, PrivacyJson } from "./contracts/privacy.js";
|
|
2
|
+
import { PrivacyRefusal, type PersonalDataContributor, type PrivacyStore } from "./privacyPorts.js";
|
|
3
|
+
|
|
4
|
+
export interface IdentityPrivacyPolicy {
|
|
5
|
+
/** Explicit dispositions for populated opaque contact fields; absence refuses erasure. */
|
|
6
|
+
retainedContactFields: Readonly<Record<string, PersonalDataObligation>>;
|
|
7
|
+
}
|
|
8
|
+
const owned = new Set(["id", "email", "first_name", "last_name", "phone", "country", "principal_id", "created_at", "updated_at"]);
|
|
9
|
+
const populated = (value: PrivacyJson) => value !== null && value !== "" && !(typeof value === "object" && Object.keys(value).length === 0);
|
|
10
|
+
export function createIdentityPersonalDataContributor<C>(store: PrivacyStore<C>, policy: IdentityPrivacyPolicy, randomId: () => string): PersonalDataContributor<C> {
|
|
11
|
+
function obligations(identity: { [key: string]: PrivacyJson }): PersonalDataObligation[] {
|
|
12
|
+
const contact = identity.contact;
|
|
13
|
+
const result: PersonalDataObligation[] = [{ owner: "identity", resource: "identity.audit-and-receipt", disposition: "retain", reasonCode: "PSEUDONYMOUS_AUDIT", remote: false }];
|
|
14
|
+
if (contact && typeof contact === "object" && !Array.isArray(contact)) {
|
|
15
|
+
for (const [key, value] of Object.entries(contact)) {
|
|
16
|
+
if (owned.has(key) || !populated(value)) continue;
|
|
17
|
+
const declaration = policy.retainedContactFields[key];
|
|
18
|
+
result.push(declaration?.disposition === "retain" && declaration.resource === `clients.${key}` ? declaration : { owner: "unassigned", resource: `clients.${key}`, disposition: "unresolved", reasonCode: "PRIVACY_OWNERSHIP_UNDECLARED", remote: false });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
id: "identity", version: "identity.personal-data.v1",
|
|
25
|
+
ownedResources: ["identity.contact-profile", "identity.principal", "identity.membership", "identity.invitations", "identity.audit-and-receipt"],
|
|
26
|
+
async prepareErasure() {},
|
|
27
|
+
async export(_context, subject) {
|
|
28
|
+
const unavailable = Array.isArray(subject.identity.unavailable) ? subject.identity.unavailable.map(String) : [];
|
|
29
|
+
const data = { ...subject.identity };
|
|
30
|
+
const contact = data.contact;
|
|
31
|
+
if (contact && typeof contact === "object" && !Array.isArray(contact)) {
|
|
32
|
+
data.contact = Object.fromEntries(Object.entries(contact).filter(([key]) => owned.has(key) || key === "auth_user_id"));
|
|
33
|
+
for (const key of Object.keys(contact)) if (!owned.has(key) && key !== "auth_user_id") unavailable.push(`contact.${key}`);
|
|
34
|
+
}
|
|
35
|
+
return { contributorId: "identity", version: "identity.personal-data.v1", data,
|
|
36
|
+
coverage: { complete: unavailable.length === 0, included: Object.keys(subject.identity).filter(k => k !== "unavailable"), unavailable }, obligations: obligations(subject.identity) };
|
|
37
|
+
},
|
|
38
|
+
async assessErasure(_context, subject) {
|
|
39
|
+
const declared = obligations(subject.identity);
|
|
40
|
+
if (declared.some(o => o.disposition === "unresolved")) throw new PrivacyRefusal("PRIVACY_OWNERSHIP_UNDECLARED");
|
|
41
|
+
return { obligations: declared };
|
|
42
|
+
},
|
|
43
|
+
async eraseOrRetain(context, subject, assessment, actor) {
|
|
44
|
+
await store.eraseIdentity(context, subject, actor, `erased+${randomId()}@identity.invalid`);
|
|
45
|
+
return { contributorId: "identity", version: "identity.personal-data.v1", obligations: assessment.obligations };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReadinessIssue } from "@openlup/core/readiness";
|
|
2
|
+
import { assessEmailTrust, type EmailTrustPolicy } from "./emailTrustPolicy.js";
|
|
3
|
+
|
|
4
|
+
/** The module field stays structural so older core readiness types still compile; no new core code is needed. */
|
|
5
|
+
export async function identityEmailTrustReadiness(policy: EmailTrustPolicy): Promise<readonly (ReadinessIssue & { module: "identity" })[]> {
|
|
6
|
+
const result = await assessEmailTrust(policy);
|
|
7
|
+
if (result.trusted) return [];
|
|
8
|
+
return [{
|
|
9
|
+
code: "OPENLUP_E_PORT_MISSING", package: "@openlup/platform", module: "identity",
|
|
10
|
+
subject: "emailTrustPolicy", observation: result.code,
|
|
11
|
+
fix: result.code === "EMAIL_TRUST_ENVELOPE_REFUSED"
|
|
12
|
+
? "Disable automatic confirmation and declare only providers that verify email."
|
|
13
|
+
: "Restore access to valid public identity settings or bind an explicit email-trust policy.",
|
|
14
|
+
}];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Discovery and fresh key availability are independent readiness observations. */
|
|
18
|
+
export async function identityOidcReadiness(verifier: import("./oidcIdentityVerifier.js").OidcIdentityVerifier): Promise<readonly (ReadinessIssue & { module: "identity" })[]> {
|
|
19
|
+
const report = await verifier.readiness();
|
|
20
|
+
if (report.state === "ready") return [];
|
|
21
|
+
return [
|
|
22
|
+
...(!report.discovery.valid ? ["oidcDiscovery"] : []),
|
|
23
|
+
...(!report.jwks.loaded ? ["oidcJwks"] : []),
|
|
24
|
+
].map(subject => ({ code: "OPENLUP_E_PORT_MISSING", package: "@openlup/platform", module: "identity", subject,
|
|
25
|
+
observation: report.code ?? "OIDC_JWKS_INVALID", fix: "Restore valid issuer discovery and fresh same-origin signing keys." }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export * from "./adminMagicLinkHandler.js";
|
|
2
|
+
export * from "./reconcileAccountForPrincipal.js";
|
|
3
|
+
export * from "./customerVerifyOtpHandler.js";
|
|
4
|
+
export * from "./authVerifyUrl.js";
|
|
5
|
+
export * from "./customerIdentityVerifier.js";
|
|
6
|
+
export * from "./operatorTokenAdminAuth.js";
|
|
7
|
+
export * from "./managedIdentityVerifier.js";
|
|
8
|
+
export * from "./managedAdminAuthVerifier.js";
|
|
9
|
+
export * from "./managedCustomerMe.js";
|
|
10
|
+
export * from "./sql.js";
|
|
11
|
+
export * from "./customerMeHandler.js";
|
|
12
|
+
export * from "./messages.js";
|
|
13
|
+
export * from "./managedAccountLinkStore.js";
|
|
14
|
+
export * from "./customerIdentityBinding.js";
|
|
15
|
+
export * from "./adminUserActionPorts.js";
|
|
16
|
+
export * from "./contribution.js";
|
|
17
|
+
export * from "./customerContracts.js";
|
|
18
|
+
export * from "./machineActorWriteFence.js";
|
|
19
|
+
export * from "./managedClient.js";
|
|
20
|
+
export * from "./customerAuthorization.js";
|
|
21
|
+
export * from "./customerSessionIssuer.js";
|
|
22
|
+
export * from "./managedClientAuthLink.js";
|
|
23
|
+
export * from "./customerAuth.js";
|
|
24
|
+
export * from "./reconcileAccountHandler.js";
|
|
25
|
+
export * from "./postgresCustomerMe.js";
|
|
26
|
+
export * from "./ports.js";
|
|
27
|
+
export * from "./postgresCustomerRecovery.js";
|
|
28
|
+
export * from "./customerMagicLinkHandler.js";
|
|
29
|
+
export * from "./customerSessionBinding.js";
|
|
30
|
+
export * from "./adminUserHandlers.js";
|
|
31
|
+
export * from "./adminPreflight.js";
|
|
32
|
+
export * from "./adminAuthBinding.js";
|
|
33
|
+
export * from "./customerPorts.js";
|
|
34
|
+
export * from "./magicLinkPorts.js";
|
|
35
|
+
export * from "./inviteAdminUserUseCase.js";
|
|
36
|
+
export * from "./adminAuthorization.js";
|
|
37
|
+
export * from "./removeAdminUserUseCase.js";
|
|
38
|
+
export * from "./adminMeHandler.js";
|
|
39
|
+
export * from "./managedAdminUserGateways.js";
|
|
40
|
+
export * from "./jwtLibrary.js";
|
|
41
|
+
export * from "./platformAdminAuth.js";
|
|
42
|
+
export * from "./adminContracts.js";
|
|
43
|
+
export * from "./contracts/emailTrust.js";
|
|
44
|
+
export { createVerifierResultOnlyEmailTrustPolicy } from "./emailTrustPolicy.js";
|
|
45
|
+
export type { EmailTrustAssessment, EmailTrustPolicy } from "./emailTrustPolicy.js";
|
|
46
|
+
export * from "./managedEmailTrustPolicy.js";
|
|
47
|
+
export * from "./identityReadiness.js";
|
|
48
|
+
|
|
49
|
+
export * from "./operatorInvitationPorts.js";
|
|
50
|
+
export * from "./operatorInvitationLifecycle.js";
|
|
51
|
+
export * from "./directOperatorInvitationStore.js";
|
|
52
|
+
export * from "./operatorPorts.js";
|
|
53
|
+
export * from "./operatorSession.js";
|
|
54
|
+
export * from "./operatorIdentityVerifier.js";
|
|
55
|
+
export * from "./operatorAdminAuth.js";
|
|
56
|
+
export * from "./operatorAuthHandlers.js";
|
|
57
|
+
export * from "./postgresOperatorIdentity.js";
|
|
58
|
+
export * from "./privacyPorts.js";
|
|
59
|
+
export * from "./personalDataRegistry.js";
|
|
60
|
+
export * from "./privacyUseCases.js";
|
|
61
|
+
export * from "./identityPersonalDataContributor.js";
|
|
62
|
+
export * from "./postgresPrivacy.js";
|
|
63
|
+
export * from "./managedPrivacy.js";
|
|
64
|
+
export * from "./privacyHandlers.js";
|
|
65
|
+
|
|
66
|
+
export * from "./oidcIdentityVerifier.js";
|
|
67
|
+
export * from "./oidcPrincipal.js";
|
|
68
|
+
export * from "./postgresAccountLinkStore.js";
|
|
69
|
+
export * from "./contracts/oidc.js";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ManagedOperatorMembershipResult, OperatorInvitationConflict } from "./contracts/operatorInvitations.js";
|
|
2
|
+
import type { OperatorInvitationStore } from "./operatorInvitationPorts.js";
|
|
3
|
+
import { assertOperatorInvitationTtl } from "./operatorInvitationLifecycle.js";
|
|
4
|
+
import { identityMessages, type IdentityMessages } from "./messages.js";
|
|
5
|
+
export type AdminUserInviteRole = "admin" | "distributor";
|
|
6
|
+
export const ADMIN_ROLE_NOTIFICATION_LOCALE = "en" as const;
|
|
7
|
+
export type AdminRoleNotificationSkipReason = "admin_disabled" | "egress_suppressed";
|
|
8
|
+
|
|
9
|
+
export type InviteAdminUserResponse = {
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
success?: boolean;
|
|
12
|
+
message?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
code?: string;
|
|
15
|
+
deliveryReference?: string | null;
|
|
16
|
+
deliveryReferencePresent?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface InviteAdminUserGateway {
|
|
20
|
+
getUser: (accessToken: string) => Promise<{
|
|
21
|
+
user: { id: string } | null;
|
|
22
|
+
error: unknown;
|
|
23
|
+
}>;
|
|
24
|
+
findAdminUser: (userId: string) => Promise<{ id: string; role: string; membership_state: string; is_machine_actor: boolean } | null>;
|
|
25
|
+
findAdminUserByEmail: (email: string) => Promise<{ id: string } | null>;
|
|
26
|
+
findAuthUserByEmail: (email: string) => Promise<{ id: string; email?: string | null } | null>;
|
|
27
|
+
inviteAuthUser: (input: {
|
|
28
|
+
email: string;
|
|
29
|
+
redirectTo: string;
|
|
30
|
+
role: AdminUserInviteRole;
|
|
31
|
+
}) => Promise<{
|
|
32
|
+
user: { id: string } | null;
|
|
33
|
+
error: { message: string } | null;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AdminRoleNotificationPort {
|
|
38
|
+
sendRoleGranted: (input: {
|
|
39
|
+
email: string;
|
|
40
|
+
authUserId: string;
|
|
41
|
+
role: AdminUserInviteRole;
|
|
42
|
+
locale: "pl" | "en";
|
|
43
|
+
invitationId?: string;
|
|
44
|
+
expiresAt?: string;
|
|
45
|
+
}) => Promise<{
|
|
46
|
+
ok: boolean;
|
|
47
|
+
providerMessageId: string | null;
|
|
48
|
+
providerError: string | null;
|
|
49
|
+
skipped: boolean;
|
|
50
|
+
skipReason: AdminRoleNotificationSkipReason | null;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runInviteAdminUserUseCase(
|
|
55
|
+
request: { accessToken: string | null; email: string; role: string },
|
|
56
|
+
deps: {
|
|
57
|
+
gateway: InviteAdminUserGateway;
|
|
58
|
+
roleNotificationPort: AdminRoleNotificationPort;
|
|
59
|
+
siteUrl: string;
|
|
60
|
+
|
|
61
|
+
messages?: IdentityMessages;
|
|
62
|
+
notificationLocale?: "pl" | "en";
|
|
63
|
+
} & (
|
|
64
|
+
| { invitationStore: OperatorInvitationStore; ttlSeconds: number }
|
|
65
|
+
| { activateMembership: (input: { principalId: string; email: string; role: AdminUserInviteRole }) => Promise<ManagedOperatorMembershipResult | { code: OperatorInvitationConflict }> }
|
|
66
|
+
),
|
|
67
|
+
): Promise<{ status: number; body: InviteAdminUserResponse }> {
|
|
68
|
+
const messages = deps.messages ?? identityMessages.en;
|
|
69
|
+
try {
|
|
70
|
+
if ("invitationStore" in deps) assertOperatorInvitationTtl(deps.ttlSeconds);
|
|
71
|
+
if (!request.accessToken) {
|
|
72
|
+
return { status: 401, body: { code: "UNAUTHORIZED", error: messages.unauthorized } };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { user, error: authError } = await deps.gateway.getUser(request.accessToken);
|
|
76
|
+
if (authError || !user) {
|
|
77
|
+
return { status: 401, body: { code: "INVALID_TOKEN", error: messages.invalidToken } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const callerAdmin = await deps.gateway.findAdminUser(user.id);
|
|
81
|
+
if (!callerAdmin || callerAdmin.role !== "admin" || callerAdmin.membership_state !== "active" || callerAdmin.is_machine_actor !== false) {
|
|
82
|
+
return { status: 403, body: { code: "INVITE_FORBIDDEN", error: messages.inviteForbidden } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!request.email) {
|
|
86
|
+
return { status: 400, body: { code: "EMAIL_REQUIRED", error: messages.emailRequired } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isInviteRole(request.role)) {
|
|
90
|
+
return { status: 400, body: { code: "INVALID_ROLE", error: messages.invalidRole } };
|
|
91
|
+
}
|
|
92
|
+
const role = request.role;
|
|
93
|
+
|
|
94
|
+
const email = request.email.trim().toLowerCase();
|
|
95
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return { status: 400, body: { code: "INVALID_EMAIL", error: "Invalid email" } };
|
|
96
|
+
const existingMembership = await deps.gateway.findAdminUserByEmail(email);
|
|
97
|
+
let principal = await deps.gateway.findAuthUserByEmail(email);
|
|
98
|
+
if (existingMembership && existingMembership.id !== principal?.id) return { status: 409, body: { code: "IDENTITY_CONFLICT", error: "IDENTITY_CONFLICT" } };
|
|
99
|
+
if (!principal) {
|
|
100
|
+
const invite = await deps.gateway.inviteAuthUser({ email, role, redirectTo: new URL("/admin/auth/callback", deps.siteUrl).toString() });
|
|
101
|
+
if (invite.error || !invite.user) {
|
|
102
|
+
// A provider duplicate race reuses the durable identity. Never delete it.
|
|
103
|
+
principal = await deps.gateway.findAuthUserByEmail(email);
|
|
104
|
+
if (!principal) throw new Error("operator_identity_creation_failed");
|
|
105
|
+
} else principal = invite.user;
|
|
106
|
+
}
|
|
107
|
+
const issued = "invitationStore" in deps
|
|
108
|
+
? await deps.invitationStore.issue({ actorPrincipalId: user.id, targetPrincipalId: principal.id, email, role, ttlSeconds: deps.ttlSeconds })
|
|
109
|
+
: await deps.activateMembership({ principalId: principal.id, email, role });
|
|
110
|
+
if (!("membershipState" in issued)) return { status: 409, body: { code: issued.code, error: issued.code } };
|
|
111
|
+
let delivery: Pick<InviteAdminUserResponse, "deliveryReference" | "deliveryReferencePresent"> = {};
|
|
112
|
+
if (issued.code !== "ALREADY_INVITED" && issued.code !== "ALREADY_ACTIVE") {
|
|
113
|
+
try {
|
|
114
|
+
const notification = await deps.roleNotificationPort.sendRoleGranted({ email, authUserId: principal.id, role, locale: deps.notificationLocale ?? ADMIN_ROLE_NOTIFICATION_LOCALE, ...(issued.membershipState === "invited" ? { invitationId: issued.invitationId, expiresAt: issued.expiresAt } : {}) });
|
|
115
|
+
if (!notification.ok) throw new Error("operator_invitation_notification_failed");
|
|
116
|
+
delivery = { deliveryReference: notification.skipped ? null : notification.providerMessageId, deliveryReferencePresent: !notification.skipped && Boolean(notification.providerMessageId) };
|
|
117
|
+
} catch {
|
|
118
|
+
return { status: 503, body: issued.membershipState === "invited"
|
|
119
|
+
? { code: "INVITATION_NOTIFICATION_FAILED", error: "Invitation persisted; notification unavailable", invitationId: issued.invitationId }
|
|
120
|
+
: { ...issued, code: "INVITATION_NOTIFICATION_FAILED", error: "Membership active; notification unavailable" } };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { status: 200, body: { success: true, ...issued, ...delivery, message: issued.membershipState === "invited" ? `Invitation pending until ${issued.expiresAt}` : "Operator membership active" } };
|
|
124
|
+
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const failure = err as { identityCode?: string; bffCode?: string };
|
|
127
|
+
if (failure.identityCode) return { status: failure.bffCode === "FORBIDDEN" ? 403 : 409, body: { code: failure.identityCode, error: failure.identityCode } };
|
|
128
|
+
return { status: 503, body: { code: "IDENTITY_ACTION_FAILED", error: String(err) } };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isInviteRole(value: string): value is AdminUserInviteRole {
|
|
133
|
+
return value === "admin" || value === "distributor";
|
|
134
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface IdentityJwk {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
kty?: string; alg?: string; crv?: string; kid?: string; use?: string; x?: string; y?: string; d?: string;
|
|
4
|
+
}
|
|
5
|
+
type Key = CryptoKey | Uint8Array;
|
|
6
|
+
interface Signer {
|
|
7
|
+
setProtectedHeader(header: { alg: string; kid: string; typ: string }): Signer;
|
|
8
|
+
setSubject(value: string): Signer; setIssuer(value: string): Signer;
|
|
9
|
+
setAudience(value: string): Signer; setIssuedAt(value: number): Signer;
|
|
10
|
+
setExpirationTime(value: number): Signer; sign(key: Key): Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
/** The host loads its selected JWT implementation only for the selected adapter. */
|
|
13
|
+
export interface IdentityJwtLibrary {
|
|
14
|
+
importJWK(jwk: IdentityJwk, alg: string): Promise<Key>;
|
|
15
|
+
jwtVerify(token: string, key: (header: { alg?: string; kid?: string }) => Promise<Key>, options: { algorithms: string[]; audience: string; issuer: string; currentDate?: Date; clockTolerance?: number }): Promise<{ payload: IdentityJwtClaims }>;
|
|
16
|
+
SignJWT: new (payload: Record<string, unknown>) => Signer;
|
|
17
|
+
}
|
|
18
|
+
export type IdentityJwtLibraryLoader = () => Promise<IdentityJwtLibrary>;
|
|
19
|
+
|
|
20
|
+
/** Claims returned only after signature verification by the host library. */
|
|
21
|
+
export interface IdentityJwtClaims {
|
|
22
|
+
[claim: string]: unknown;
|
|
23
|
+
sub?: string; iss?: string; aud?: string | string[]; exp?: number; iat?: number; nbf?: number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HttpResponse } from "../http/index.js";
|
|
2
|
+
import { createIdentityResponder, type IdentityResponseOptions } from "./response.js";
|
|
3
|
+
|
|
4
|
+
export function refusesMachineActorRequest(
|
|
5
|
+
route: string,
|
|
6
|
+
method: string | undefined,
|
|
7
|
+
isMachineActor: boolean | undefined,
|
|
8
|
+
writeRoutes: readonly string[] = [],
|
|
9
|
+
): boolean {
|
|
10
|
+
if (isMachineActor === false) return false;
|
|
11
|
+
const verb = (method ?? "").toUpperCase();
|
|
12
|
+
return verb !== "GET" && verb !== "HEAD" && !writeRoutes.includes(route);
|
|
13
|
+
}
|
|
14
|
+
export function sendMachineActorWriteRefused(res: HttpResponse, options: IdentityResponseOptions = {}): void {
|
|
15
|
+
const { sendBffError } = createIdentityResponder(options);
|
|
16
|
+
sendBffError(res, "FORBIDDEN", "Human operator required", {
|
|
17
|
+
details: { reason: "machine_actor_write_refused" },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface MagicLinkAuthClient {
|
|
2
|
+
auth: { signInWithOtp(input: { email: string; options: { shouldCreateUser: false; emailRedirectTo: string } }): Promise<{ error: { name?: string; message?: string } | null }> };
|
|
3
|
+
}
|
|
4
|
+
export interface MagicLinkRateLimitClient {
|
|
5
|
+
rpc(fn: string, args: Record<string, unknown>): Promise<{ data: unknown; error: { message?: string } | null }>;
|
|
6
|
+
}
|
|
7
|
+
export interface MagicLinkDecision { allowed: boolean; reason?: string }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ManagedIdentityClient, ManagedIdentityRow } from "./managedClient.js";
|
|
2
|
+
import type {
|
|
3
|
+
AccountLinkStore,
|
|
4
|
+
CreateAccountInput,
|
|
5
|
+
CustomerAccount,
|
|
6
|
+
LinkPrincipalResult,
|
|
7
|
+
} from "./ports.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function createSupabaseAccountLinkStore(rawClient: unknown): AccountLinkStore {
|
|
11
|
+
const client = rawClient as ManagedIdentityClient;
|
|
12
|
+
return {
|
|
13
|
+
async findAccountByPrincipalId(principalId: string): Promise<CustomerAccount | null> {
|
|
14
|
+
const { data, error } = await client
|
|
15
|
+
.from("clients")
|
|
16
|
+
.select("id, email, auth_user_id")
|
|
17
|
+
.eq("auth_user_id", principalId)
|
|
18
|
+
.maybeSingle();
|
|
19
|
+
if (error) throw error;
|
|
20
|
+
if (!data) return null;
|
|
21
|
+
return { id: data.id, email: data.email ?? null, principalId: data.auth_user_id ?? null };
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async findAccountByEmail(email: string): Promise<CustomerAccount | null> {
|
|
25
|
+
const rows = await findManagedContactsByEmail(client, email, false);
|
|
26
|
+
if (rows.length > 1) {
|
|
27
|
+
throw Object.assign(new Error("multiple rows returned"), { code: "PGRST116" });
|
|
28
|
+
}
|
|
29
|
+
const data = rows[0];
|
|
30
|
+
if (!data) return null;
|
|
31
|
+
return { id: data.id, email: data.email ?? null, principalId: data.auth_user_id ?? null };
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async isReservedPrincipal(principalId: string): Promise<boolean> {
|
|
35
|
+
const { data, error } = await client
|
|
36
|
+
.from("admin_users")
|
|
37
|
+
.select("id, is_machine_actor")
|
|
38
|
+
.eq("id", principalId)
|
|
39
|
+
.maybeSingle();
|
|
40
|
+
if (error) throw error;
|
|
41
|
+
if (!data) return false;
|
|
42
|
+
return data.is_machine_actor !== false;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async linkPrincipalToAccount(
|
|
46
|
+
accountId: string,
|
|
47
|
+
principalId: string,
|
|
48
|
+
): Promise<LinkPrincipalResult> {
|
|
49
|
+
const { data, error } = await client
|
|
50
|
+
.from("clients")
|
|
51
|
+
.update({ auth_user_id: principalId })
|
|
52
|
+
.eq("id", accountId)
|
|
53
|
+
.is("auth_user_id", null)
|
|
54
|
+
.select("auth_user_id")
|
|
55
|
+
.maybeSingle();
|
|
56
|
+
if (error) throw error;
|
|
57
|
+
if (data?.auth_user_id === principalId) {
|
|
58
|
+
return { ok: true, alreadyLinked: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { data: current, error: currentError } = await client
|
|
62
|
+
.from("clients")
|
|
63
|
+
.select("auth_user_id")
|
|
64
|
+
.eq("id", accountId)
|
|
65
|
+
.maybeSingle();
|
|
66
|
+
if (currentError) throw currentError;
|
|
67
|
+
if (current?.auth_user_id === principalId) {
|
|
68
|
+
return { ok: true, alreadyLinked: true };
|
|
69
|
+
}
|
|
70
|
+
return { ok: false, existingPrincipalId: current?.auth_user_id ?? null };
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async createAccount(input: CreateAccountInput): Promise<{ id: string }> {
|
|
74
|
+
const { data, error } = await client
|
|
75
|
+
.from("clients")
|
|
76
|
+
.insert({
|
|
77
|
+
email: input.email,
|
|
78
|
+
auth_user_id: input.principalId,
|
|
79
|
+
acquisition_source: input.acquisitionSource,
|
|
80
|
+
lifecycle_stage: input.lifecycleStage,
|
|
81
|
+
})
|
|
82
|
+
.select("id")
|
|
83
|
+
.single();
|
|
84
|
+
if (error) throw error;
|
|
85
|
+
return { id: data!.id };
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function findManagedContactsByEmail(
|
|
91
|
+
client: ManagedIdentityClient,
|
|
92
|
+
email: string,
|
|
93
|
+
oldestFirst: boolean,
|
|
94
|
+
): Promise<ManagedIdentityRow[]> {
|
|
95
|
+
const normalized = email.trim().toLowerCase();
|
|
96
|
+
// Escaping bounds the database prefilter; Unicode regex folding can still
|
|
97
|
+
// widen it, so only exact normalized addresses may cross this boundary.
|
|
98
|
+
let query = client.from("clients")
|
|
99
|
+
.select("id, email, auth_user_id")
|
|
100
|
+
.filter("email", "imatch", `^${normalized.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&")}$`);
|
|
101
|
+
if (oldestFirst) query = query.order("created_at", { ascending: true });
|
|
102
|
+
const { data, error } = await query.limit(10);
|
|
103
|
+
if (error) throw error;
|
|
104
|
+
return (data ?? []).filter((row) => row.email?.trim().toLowerCase() === normalized);
|
|
105
|
+
}
|