@openlup/platform 0.0.0 → 0.16.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 +59 -0
- package/LICENSE +204 -0
- package/README.md +471 -1
- package/dist/bff/dispatch.d.ts +27 -0
- package/dist/bff/dispatch.js +47 -0
- package/dist/bff/envelope.d.ts +46 -0
- package/dist/bff/envelope.js +80 -0
- package/dist/bff/health.d.ts +24 -0
- package/dist/bff/health.js +19 -0
- package/dist/bff/index.d.ts +6 -0
- package/dist/bff/index.js +6 -0
- package/dist/bff/lazyRoute.d.ts +4 -0
- package/dist/bff/lazyRoute.js +13 -0
- package/dist/bff/requestContext.d.ts +17 -0
- package/dist/bff/requestContext.js +26 -0
- package/dist/bff/routerRewrite.d.ts +2 -0
- package/dist/bff/routerRewrite.js +31 -0
- package/dist/blob/binding.d.ts +16 -0
- package/dist/blob/binding.js +37 -0
- package/dist/blob/filesystem.d.ts +8 -0
- package/dist/blob/filesystem.js +71 -0
- package/dist/blob/index.d.ts +6 -0
- package/dist/blob/index.js +6 -0
- package/dist/blob/s3.d.ts +32 -0
- package/dist/blob/s3.js +56 -0
- package/dist/blob/s3Client.d.ts +25 -0
- package/dist/blob/s3Client.js +76 -0
- package/dist/blob/supabase.d.ts +40 -0
- package/dist/blob/supabase.js +59 -0
- package/dist/blob/vercel.d.ts +63 -0
- package/dist/blob/vercel.js +69 -0
- package/dist/composition/composeBundle.d.ts +47 -0
- package/dist/composition/composeBundle.js +45 -0
- package/dist/composition/httpBinding.d.ts +8 -0
- package/dist/composition/httpBinding.js +18 -0
- package/dist/composition/index.d.ts +3 -0
- package/dist/composition/index.js +3 -0
- package/dist/composition/nodeHost.d.ts +103 -0
- package/dist/composition/nodeHost.js +145 -0
- package/dist/data/binding.d.ts +34 -0
- package/dist/data/binding.js +92 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +5 -0
- package/dist/data/postgresGateway.d.ts +42 -0
- package/dist/data/postgresGateway.js +135 -0
- package/dist/data/queryBuilder.d.ts +54 -0
- package/dist/data/queryBuilder.js +240 -0
- package/dist/data/queryBuilderSql.d.ts +76 -0
- package/dist/data/queryBuilderSql.js +145 -0
- package/dist/data/supabaseClientFactory.d.ts +34 -0
- package/dist/data/supabaseClientFactory.js +56 -0
- package/dist/data/supabaseGateway.d.ts +22 -0
- package/dist/data/supabaseGateway.js +49 -0
- package/dist/http/deadline.d.ts +20 -0
- package/dist/http/deadline.js +93 -0
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +5 -0
- package/dist/http/requestAdapter.d.ts +38 -0
- package/dist/http/requestAdapter.js +203 -0
- package/dist/http/runtime.d.ts +83 -0
- package/dist/http/runtime.js +199 -0
- package/dist/http/security.d.ts +44 -0
- package/dist/http/security.js +114 -0
- package/dist/http/staticAssets.d.ts +3 -0
- package/dist/http/staticAssets.js +42 -0
- package/dist/http/types.d.ts +23 -0
- package/dist/http/types.js +6 -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/dist/job-ledger/index.d.ts +5 -0
- package/dist/job-ledger/index.js +3 -0
- package/dist/job-ledger/ports.d.ts +10 -0
- package/dist/job-ledger/ports.js +7 -0
- package/dist/job-ledger/postgres.d.ts +10 -0
- package/dist/job-ledger/postgres.js +101 -0
- package/dist/job-ledger/sql.d.ts +7 -0
- package/dist/job-ledger/sql.js +10 -0
- package/dist/job-ledger/supabase.d.ts +77 -0
- package/dist/job-ledger/supabase.js +202 -0
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.js +1 -0
- package/dist/kernel/platformKernel.d.ts +34 -0
- package/dist/kernel/platformKernel.js +115 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.js +1 -0
- package/dist/migrations/postgresRunner.d.ts +50 -0
- package/dist/migrations/postgresRunner.js +176 -0
- package/dist/scheduler/advisoryLock.d.ts +44 -0
- package/dist/scheduler/advisoryLock.js +114 -0
- package/dist/scheduler/cronGuard.d.ts +10 -0
- package/dist/scheduler/cronGuard.js +19 -0
- package/dist/scheduler/cronSecret.d.ts +5 -0
- package/dist/scheduler/cronSecret.js +23 -0
- package/dist/scheduler/handlers.d.ts +9 -0
- package/dist/scheduler/handlers.js +10 -0
- package/dist/scheduler/httpEntry.d.ts +16 -0
- package/dist/scheduler/httpEntry.js +31 -0
- package/dist/scheduler/index.d.ts +9 -0
- package/dist/scheduler/index.js +9 -0
- package/dist/scheduler/jobRegistry.d.ts +21 -0
- package/dist/scheduler/jobRegistry.js +39 -0
- package/dist/scheduler/jobs.d.ts +48 -0
- package/dist/scheduler/jobs.js +92 -0
- package/dist/scheduler/nodeScheduler.d.ts +42 -0
- package/dist/scheduler/nodeScheduler.js +73 -0
- package/dist/scheduler/vercelScheduler.d.ts +29 -0
- package/dist/scheduler/vercelScheduler.js +51 -0
- package/package.json +116 -3
- package/release-gates.json +143 -0
- package/sql/20260809150000_job_run_ledger.sql +301 -0
- package/sql/20260809150001_job_run_ledger_v3.sql +70 -0
- package/sql/20260810180001_self_host_actor_auth.sql +40 -0
- package/sql/20260810204057_customer_identity_read.sql +31 -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/src/bff/dispatch.ts +81 -0
- package/src/bff/envelope.ts +142 -0
- package/src/bff/health.ts +37 -0
- package/src/bff/index.ts +30 -0
- package/src/bff/lazyRoute.ts +28 -0
- package/src/bff/requestContext.ts +48 -0
- package/src/bff/routerRewrite.ts +38 -0
- package/src/blob/binding.ts +55 -0
- package/src/blob/filesystem.ts +86 -0
- package/src/blob/index.ts +17 -0
- package/src/blob/s3.ts +87 -0
- package/src/blob/s3Client.ts +91 -0
- package/src/blob/supabase.ts +88 -0
- package/src/blob/vercel.ts +127 -0
- package/src/composition/composeBundle.ts +109 -0
- package/src/composition/httpBinding.ts +30 -0
- package/src/composition/index.ts +17 -0
- package/src/composition/nodeHost.ts +289 -0
- package/src/data/binding.ts +157 -0
- package/src/data/index.ts +33 -0
- package/src/data/postgresGateway.ts +186 -0
- package/src/data/queryBuilder.ts +300 -0
- package/src/data/queryBuilderSql.ts +183 -0
- package/src/data/supabaseClientFactory.ts +92 -0
- package/src/data/supabaseGateway.ts +76 -0
- package/src/http/deadline.ts +117 -0
- package/src/http/index.ts +36 -0
- package/src/http/requestAdapter.ts +215 -0
- package/src/http/runtime.ts +319 -0
- package/src/http/security.ts +155 -0
- package/src/http/staticAssets.ts +42 -0
- package/src/http/types.ts +31 -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
- package/src/job-ledger/index.ts +25 -0
- package/src/job-ledger/ports.ts +27 -0
- package/src/job-ledger/postgres.ts +121 -0
- package/src/job-ledger/sql.ts +12 -0
- package/src/job-ledger/supabase.ts +294 -0
- package/src/kernel/index.ts +17 -0
- package/src/kernel/platformKernel.ts +153 -0
- package/src/migrations/index.ts +10 -0
- package/src/migrations/postgresRunner.ts +255 -0
- package/src/scheduler/advisoryLock.ts +145 -0
- package/src/scheduler/cronGuard.ts +30 -0
- package/src/scheduler/cronSecret.ts +26 -0
- package/src/scheduler/handlers.ts +24 -0
- package/src/scheduler/httpEntry.ts +49 -0
- package/src/scheduler/index.ts +47 -0
- package/src/scheduler/jobRegistry.ts +57 -0
- package/src/scheduler/jobs.ts +119 -0
- package/src/scheduler/nodeScheduler.ts +123 -0
- package/src/scheduler/vercelScheduler.ts +71 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { HttpRequest } from "../http/index.js";
|
|
2
|
+
|
|
3
|
+
function authVerifyType(actionType: string): string {
|
|
4
|
+
return actionType === "email_change_current" || actionType === "email_change_new"
|
|
5
|
+
? "email_change"
|
|
6
|
+
: actionType;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function qpSafeTokenValue(token: string): string {
|
|
11
|
+
if (token.length === 0) return token;
|
|
12
|
+
return `%${token.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}${encodeURIComponent(token.slice(1))}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildAuthVerifyUrl(authServiceUrl: string, actionType: string, tokenHash: string, redirectTo: string): string {
|
|
16
|
+
const params = new URLSearchParams({ type: authVerifyType(actionType), redirect_to: redirectTo });
|
|
17
|
+
return `${authServiceUrl.replace(/\/+$/, "")}/auth/v1/verify?token=${qpSafeTokenValue(tokenHash)}&${params.toString()}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Only configured HTTP(S) origins authorize the default magic-link destination. */
|
|
21
|
+
export function createMagicLinkOriginResolver(
|
|
22
|
+
configuredOrigins: readonly string[],
|
|
23
|
+
): (req: HttpRequest) => string | null {
|
|
24
|
+
const origins = configuredOrigins.map(normalizeOrigin);
|
|
25
|
+
const origin = origins.length > 0 && origins.every((value) => value !== null)
|
|
26
|
+
? (origins[0] ?? null)
|
|
27
|
+
: null;
|
|
28
|
+
return () => origin;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeOrigin(value: string): string | null {
|
|
32
|
+
const candidate = value.trim();
|
|
33
|
+
// URL parsing alone accepts paths, credentials and repaired backslashes.
|
|
34
|
+
if (!/^https?:\/\/[^/?#\\\s]+\/?$/i.test(candidate)) return null;
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(candidate);
|
|
37
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
|
|
38
|
+
return url.origin;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Invalid return targets use the callback's default landing page. */
|
|
45
|
+
export function normalizeMagicLinkReturnPath(value: string | undefined, origin: string): string | undefined {
|
|
46
|
+
if (!value || !value.startsWith("/") || value.startsWith("//")) return undefined;
|
|
47
|
+
// Reject encoded separators, controls and nested escapes before any later decoding.
|
|
48
|
+
if (value.includes("\\") || Array.from(value).some((character) => character.charCodeAt(0) <= 0x20 || character.charCodeAt(0) === 0x7f) || /%(?:2f|5c|0[0-9a-f]|1[0-9a-f]|7f|25)/i.test(value)) return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(value, origin);
|
|
51
|
+
if (url.origin !== new URL(origin).origin || url.pathname.startsWith("//")) return undefined;
|
|
52
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EmailTrustFailureCode } from "./emailTrust.js";
|
|
2
|
+
export const AUTH_RECONCILE_CONTRACT_VERSION = "auth.reconcile.v1" as const;
|
|
3
|
+
export interface AuthReconcileResponse {
|
|
4
|
+
accountId: string;
|
|
5
|
+
created: boolean;
|
|
6
|
+
linked: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type AuthReconcileErrorCode =
|
|
9
|
+
| EmailTrustFailureCode
|
|
10
|
+
| "NO_VERIFIED_EMAIL"
|
|
11
|
+
| "RESERVED_PRINCIPAL_REFUSED"
|
|
12
|
+
| "ACCOUNT_LINK_CONFLICT"
|
|
13
|
+
| "EMAIL_OWNED_BY_OTHER_PRINCIPAL"
|
|
14
|
+
| "PRINCIPAL_EMAIL_MISMATCH";
|
|
15
|
+
// Runtime schemas remain in reference composition until the validator boundary is resolved.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./ports.js";
|
|
3
|
+
export * from "./providerList.js";
|
|
4
|
+
export * from "./contracts.js";
|
|
5
|
+
export * from "./emailTrust.js";
|
|
6
|
+
export * from "./operatorInvitations.js";
|
|
7
|
+
export * from "./operatorAuth.js";
|
|
8
|
+
export * from "./privacy.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Credential and dependency refusals; no token or transport details are exposed. */
|
|
2
|
+
export const OIDC_FAILURE_CODES = [
|
|
3
|
+
"OIDC_CONFIGURATION_INVALID", "OIDC_DISCOVERY_UNAVAILABLE", "OIDC_DISCOVERY_INVALID",
|
|
4
|
+
"OIDC_JWKS_UNAVAILABLE", "OIDC_JWKS_INVALID", "OIDC_JWKS_REFRESH_RATE_LIMITED",
|
|
5
|
+
"OIDC_JWT_LIBRARY_UNAVAILABLE", "OIDC_TOKEN_INVALID", "OIDC_KID_UNKNOWN",
|
|
6
|
+
"OIDC_ISSUER_MISMATCH", "OIDC_AUDIENCE_MISMATCH", "OIDC_TOKEN_EXPIRED",
|
|
7
|
+
"OIDC_TOKEN_NOT_ACTIVE", "OIDC_TOKEN_LIFETIME_INVALID", "OIDC_SUBJECT_INVALID",
|
|
8
|
+
"OIDC_EMAIL_INVALID", "OIDC_EMAIL_UNVERIFIED", "OIDC_NATIVE_SESSION_REQUIRED",
|
|
9
|
+
] as const;
|
|
10
|
+
export type OidcFailureCode = typeof OIDC_FAILURE_CODES[number];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface OperatorSession {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
expiresAt: string;
|
|
4
|
+
user: { id: string; email: string };
|
|
5
|
+
}
|
|
6
|
+
export interface OperatorChallengeRequest { email: string }
|
|
7
|
+
export interface OperatorChallengeVerification { token: string }
|
|
8
|
+
export interface OperatorBrowserAuthPort {
|
|
9
|
+
getSession(): Promise<OperatorSession | null>;
|
|
10
|
+
requestChallenge(email: string): Promise<void>;
|
|
11
|
+
verifyChallenge(token: string): Promise<OperatorSession>;
|
|
12
|
+
signOut(): Promise<void>;
|
|
13
|
+
onAuthStateChange(listener: (session: OperatorSession | null) => void): () => void;
|
|
14
|
+
}
|
|
15
|
+
/** Strict parsers keep the contracts entry independent of the server validator. */
|
|
16
|
+
export interface OperatorSchema<T> {
|
|
17
|
+
parse(value: unknown): T;
|
|
18
|
+
safeParse(value: unknown): { success: true; data: T } | { success: false; error: Error };
|
|
19
|
+
}
|
|
20
|
+
function schema<T>(parse: (value: unknown) => T): OperatorSchema<T> {
|
|
21
|
+
return { parse, safeParse(value) {
|
|
22
|
+
try { return { success: true, data: parse(value) }; }
|
|
23
|
+
catch { return { success: false, error: new Error("operator_contract_invalid") }; }
|
|
24
|
+
} };
|
|
25
|
+
}
|
|
26
|
+
function fields(value: unknown, keys: string[]): Record<string, unknown> {
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
28
|
+
|| Object.keys(value).length !== keys.length || !keys.every(key => Object.hasOwn(value, key))) throw new Error("operator_contract_invalid");
|
|
29
|
+
return value as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
function email(value: unknown): string {
|
|
32
|
+
if (typeof value !== "string" || value.length > 320 || !/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i.test(value)) throw new Error("operator_email_invalid");
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export const operatorChallengeRequestSchema: OperatorSchema<OperatorChallengeRequest> = schema(value => {
|
|
36
|
+
const row = fields(value, ["email"]);
|
|
37
|
+
if (typeof row.email !== "string") throw new Error("operator_email_invalid");
|
|
38
|
+
return { email: email(row.email.trim().toLowerCase()) };
|
|
39
|
+
});
|
|
40
|
+
export const operatorChallengeVerificationSchema: OperatorSchema<OperatorChallengeVerification> = schema(value => {
|
|
41
|
+
const row = fields(value, ["token"]);
|
|
42
|
+
if (typeof row.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(row.token)) throw new Error("operator_token_invalid");
|
|
43
|
+
return { token: row.token };
|
|
44
|
+
});
|
|
45
|
+
function timestamp(value: unknown): value is string {
|
|
46
|
+
if (typeof value !== "string") return false;
|
|
47
|
+
const parts = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.exec(value);
|
|
48
|
+
if (!parts || !Number.isFinite(Date.parse(value))) return false;
|
|
49
|
+
const [, year, month, day, hour, minute, second] = parts.map(Number);
|
|
50
|
+
return year! >= 1970 && month! >= 1 && month! <= 12 && day! >= 1
|
|
51
|
+
&& day! <= new Date(Date.UTC(year!, month!, 0)).getUTCDate()
|
|
52
|
+
&& hour! <= 23 && minute! <= 59 && second! <= 59;
|
|
53
|
+
}
|
|
54
|
+
export const operatorSessionSchema: OperatorSchema<OperatorSession> = schema(value => {
|
|
55
|
+
const row = fields(value, ["accessToken", "expiresAt", "user"]);
|
|
56
|
+
const user = fields(row.user, ["id", "email"]);
|
|
57
|
+
if (typeof row.accessToken !== "string" || !row.accessToken || row.accessToken.length > 8192
|
|
58
|
+
|| !timestamp(row.expiresAt)
|
|
59
|
+
|| typeof user.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(user.id)) throw new Error("operator_session_invalid");
|
|
60
|
+
return { accessToken: row.accessToken, expiresAt: row.expiresAt, user: { id: user.id, email: email(user.email) } };
|
|
61
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Pending invitations confer no authority; only accepted native memberships do. */
|
|
2
|
+
export type OperatorRole = "admin" | "distributor";
|
|
3
|
+
export type OperatorMembershipState = "invited" | "active" | "expired" | "revoked";
|
|
4
|
+
export type OperatorInvitationOutcome = "INVITATION_CREATED" | "ALREADY_INVITED" | "INVITATION_REISSUED";
|
|
5
|
+
export type OperatorInvitationConflict = "MEMBERSHIP_ALREADY_ACTIVE" | "INVITATION_ROLE_CONFLICT" | "IDENTITY_CONFLICT" | "MANAGED_REINVITE_UNSUPPORTED";
|
|
6
|
+
export interface OperatorInvitation {
|
|
7
|
+
principalId: string;
|
|
8
|
+
invitationId: string;
|
|
9
|
+
membershipState: "invited";
|
|
10
|
+
role: OperatorRole;
|
|
11
|
+
invitedAt: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
}
|
|
14
|
+
export interface OperatorInvitationResult extends OperatorInvitation { code: OperatorInvitationOutcome; message?: string }
|
|
15
|
+
export interface OperatorMembershipSnapshot {
|
|
16
|
+
principalId: string;
|
|
17
|
+
membershipState: OperatorMembershipState;
|
|
18
|
+
role: OperatorRole;
|
|
19
|
+
}
|
|
20
|
+
export type OperatorInvitationAcceptance =
|
|
21
|
+
| { code: "ACTIVATED" | "ALREADY_ACTIVE"; role: OperatorRole }
|
|
22
|
+
| { code: "INVITATION_EXPIRED" | "INVITATION_REVOKED" | "NO_INVITATION" };
|
|
23
|
+
|
|
24
|
+
/** Managed invitations activate membership immediately; no pending attempt is stored. */
|
|
25
|
+
export interface ManagedOperatorMembershipResult {
|
|
26
|
+
principalId: string;
|
|
27
|
+
membershipState: "active";
|
|
28
|
+
role: OperatorRole;
|
|
29
|
+
code: "MEMBERSHIP_CREATED" | "ALREADY_ACTIVE";
|
|
30
|
+
message?: string;
|
|
31
|
+
}
|
|
32
|
+
export type OperatorInviteResult = OperatorInvitationResult | ManagedOperatorMembershipResult;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthActionResult,
|
|
3
|
+
AuthSession,
|
|
4
|
+
AuthStateListener,
|
|
5
|
+
OAuthProviderKind,
|
|
6
|
+
Unsubscribe,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export interface CustomerAuthPort {
|
|
11
|
+
getSession(): Promise<AuthSession | null>;
|
|
12
|
+
onAuthStateChange(listener: AuthStateListener): Unsubscribe;
|
|
13
|
+
signInWithOtp(input: { email: string; locale: string }): Promise<AuthActionResult>;
|
|
14
|
+
|
|
15
|
+
verifyOtpCode(input: { email: string; token: string }): Promise<AuthActionResult>;
|
|
16
|
+
signInWithOAuth(input: {
|
|
17
|
+
provider: OAuthProviderKind;
|
|
18
|
+
redirectTo: string;
|
|
19
|
+
}): Promise<AuthActionResult>;
|
|
20
|
+
linkIdentity(input: {
|
|
21
|
+
provider: OAuthProviderKind;
|
|
22
|
+
redirectTo: string;
|
|
23
|
+
}): Promise<AuthActionResult>;
|
|
24
|
+
signOut(): Promise<void>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { OperatorSchema } from "./operatorAuth.js";
|
|
2
|
+
|
|
3
|
+
export type PrivacySubjectRef = { kind: "contact"; contactId: string } | { kind: "principal"; principalId: string };
|
|
4
|
+
export type PrivacyJson = null | boolean | number | string | PrivacyJson[] | { [key: string]: PrivacyJson };
|
|
5
|
+
export interface PersonalDataObligation { owner: string; resource: string; disposition: "erase" | "retain" | "unresolved"; reasonCode: string; remote: boolean }
|
|
6
|
+
export interface PersonalDataDocument {
|
|
7
|
+
contributorId: string; version: string; data: PrivacyJson;
|
|
8
|
+
coverage: { complete: boolean; included: string[]; unavailable: string[] };
|
|
9
|
+
obligations: PersonalDataObligation[];
|
|
10
|
+
}
|
|
11
|
+
export interface ErasureReceipt { version: "identity.erasure.v1"; erasureId: string; completedAt: string; outcome: "ERASED" | "ALREADY_ERASED"; obligations: PersonalDataObligation[] }
|
|
12
|
+
export interface PrivacyEraseRequest { subject: PrivacySubjectRef; idempotencyKey: string }
|
|
13
|
+
export interface PrivacyExport { format: "personal-data.v1"; generatedAt: string; subject: PrivacySubjectRef; documents: PersonalDataDocument[]; missingContributors: string[]; erasure: ErasureReceipt | null }
|
|
14
|
+
function invalid(): never { throw new Error("privacy_contract_invalid"); }
|
|
15
|
+
function schema<T>(parse: (value: unknown) => T): OperatorSchema<T> {
|
|
16
|
+
return { parse, safeParse(value) { try { return { success: true, data: parse(value) }; } catch { return { success: false, error: new Error("privacy_contract_invalid") }; } } };
|
|
17
|
+
}
|
|
18
|
+
function fields(value: unknown, keys: string[]): Record<string, unknown> {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype
|
|
20
|
+
|| Object.keys(value).length !== keys.length || !keys.every(key => Object.hasOwn(value, key))) invalid();
|
|
21
|
+
return value as Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
function text(value: unknown, max = 200): string {
|
|
24
|
+
if (typeof value !== "string" || !value.length || value.length > max) invalid();
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function uuid(value: unknown): string {
|
|
28
|
+
const result = text(value, 36);
|
|
29
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(result)) invalid();
|
|
30
|
+
return result.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
function timestamp(value: unknown): string {
|
|
33
|
+
const result = text(value, 30);
|
|
34
|
+
if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(result) || !Number.isFinite(Date.parse(result))
|
|
35
|
+
|| new Date(result).toISOString() !== result.replace(/Z$/, result.length === 20 ? ".000Z" : "Z")) invalid();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
function list<T>(value: unknown, parse: (element: unknown) => T): T[] {
|
|
39
|
+
if (!Array.isArray(value) || value.length > 10000) invalid();
|
|
40
|
+
return value.map(element => parse(element));
|
|
41
|
+
}
|
|
42
|
+
function boolean(value: unknown): boolean { if (typeof value !== "boolean") invalid(); return value; }
|
|
43
|
+
function choice<T extends string>(value: unknown, allowed: readonly T[]): T { if (!allowed.includes(value as T)) invalid(); return value as T; }
|
|
44
|
+
function json(value: unknown, depth = 0): PrivacyJson {
|
|
45
|
+
if (depth > 32) invalid();
|
|
46
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") { if (typeof value === "string" && value.length > 1000000) invalid(); return value; }
|
|
47
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
48
|
+
if (Array.isArray(value)) return list(value, element => json(element, depth + 1));
|
|
49
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) invalid();
|
|
50
|
+
const forbidden = new Set(["__proto__", "constructor", "prototype", "password", "password_hash", "token_hash", "access_token", "refresh_token", "accessToken", "refreshToken", "secret", "credentials"]);
|
|
51
|
+
const entries = Object.entries(value);
|
|
52
|
+
if (entries.length > 10000 || entries.some(([key]) => forbidden.has(key))) invalid();
|
|
53
|
+
return Object.fromEntries(entries.map(([key, child]) => [key, json(child, depth + 1)]));
|
|
54
|
+
}
|
|
55
|
+
export const privacySubjectSchema: OperatorSchema<PrivacySubjectRef> = schema(value => {
|
|
56
|
+
if (!value || typeof value !== "object" || !("kind" in value)) invalid();
|
|
57
|
+
if (value.kind === "contact") { const row = fields(value, ["kind", "contactId"]); return { kind: "contact", contactId: uuid(row.contactId) }; }
|
|
58
|
+
const row = fields(value, ["kind", "principalId"]);
|
|
59
|
+
return { kind: choice(row.kind, ["principal"]), principalId: uuid(row.principalId) };
|
|
60
|
+
});
|
|
61
|
+
export const personalDataObligationSchema: OperatorSchema<PersonalDataObligation> = schema(value => {
|
|
62
|
+
const row = fields(value, ["owner", "resource", "disposition", "reasonCode", "remote"]);
|
|
63
|
+
return { owner: text(row.owner, 100), resource: text(row.resource), disposition: choice(row.disposition, ["erase", "retain", "unresolved"]), reasonCode: text(row.reasonCode, 100), remote: boolean(row.remote) };
|
|
64
|
+
});
|
|
65
|
+
export const personalDataDocumentSchema: OperatorSchema<PersonalDataDocument> = schema(value => {
|
|
66
|
+
const row = fields(value, ["contributorId", "version", "data", "coverage", "obligations"]), coverage = fields(row.coverage, ["complete", "included", "unavailable"]);
|
|
67
|
+
const contributorId = text(row.contributorId, 100), version = text(row.version);
|
|
68
|
+
if (!/^[a-z][a-z0-9.-]{0,99}$/.test(contributorId) || !/^[a-z][a-z0-9.-]*\.v[1-9][0-9]*$/.test(version)) invalid();
|
|
69
|
+
return { contributorId, version, data: json(row.data), coverage: { complete: boolean(coverage.complete), included: list(coverage.included, text), unavailable: list(coverage.unavailable, text) }, obligations: list(row.obligations, personalDataObligationSchema.parse) };
|
|
70
|
+
});
|
|
71
|
+
export const erasureReceiptSchema: OperatorSchema<ErasureReceipt> = schema(value => {
|
|
72
|
+
const row = fields(value, ["version", "erasureId", "completedAt", "outcome", "obligations"]);
|
|
73
|
+
return { version: choice(row.version, ["identity.erasure.v1"]), erasureId: uuid(row.erasureId), completedAt: timestamp(row.completedAt), outcome: choice(row.outcome, ["ERASED", "ALREADY_ERASED"]), obligations: list(row.obligations, personalDataObligationSchema.parse) };
|
|
74
|
+
});
|
|
75
|
+
export const privacyExportRequestSchema: OperatorSchema<{ subject: PrivacySubjectRef }> = schema(value => ({ subject: privacySubjectSchema.parse(fields(value, ["subject"]).subject) }));
|
|
76
|
+
export const privacyEraseRequestSchema: OperatorSchema<PrivacyEraseRequest> = schema(value => {
|
|
77
|
+
const row = fields(value, ["subject", "idempotencyKey"]); return { subject: privacySubjectSchema.parse(row.subject), idempotencyKey: uuid(row.idempotencyKey) };
|
|
78
|
+
});
|
|
79
|
+
export const privacyExportResponseSchema: OperatorSchema<PrivacyExport> = schema(value => {
|
|
80
|
+
const row = fields(value, ["format", "generatedAt", "subject", "documents", "missingContributors", "erasure"]);
|
|
81
|
+
return { format: choice(row.format, ["personal-data.v1"]), generatedAt: timestamp(row.generatedAt), subject: privacySubjectSchema.parse(row.subject), documents: list(row.documents, personalDataDocumentSchema.parse), missingContributors: list(row.missingContributors, text), erasure: row.erasure === null ? null : erasureReceiptSchema.parse(row.erasure) };
|
|
82
|
+
});
|
|
83
|
+
export const privacyEraseResponseSchema = erasureReceiptSchema;
|
|
84
|
+
const refusalCodes = ["PRIVACY_SUBJECT_NOT_FOUND", "PRIVACY_CONTRIBUTOR_DUPLICATE", "PRIVACY_CONTRIBUTOR_MISSING", "PRIVACY_ASSESSMENT_UNAVAILABLE", "PRIVACY_OWNERSHIP_UNDECLARED", "PRIVACY_ACTIVE_SUBSCRIPTION", "PRIVACY_RESUMABLE_SUBSCRIPTION", "PRIVACY_PROTECTED_OPERATOR", "PRIVACY_IDEMPOTENCY_CONFLICT", "PRIVACY_SUBJECT_ERASED", "PRIVACY_STORE_PRIVILEGES_UNSAFE", "MANAGED_PRIVACY_ERASURE_UNAVAILABLE"] as const;
|
|
85
|
+
export type PrivacyRefusalCode = typeof refusalCodes[number];
|
|
86
|
+
export const privacyRefusalCodeSchema: OperatorSchema<PrivacyRefusalCode> = schema(value => choice(value, refusalCodes));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isOAuthProviderKind, type OAuthProviderKind } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SOCIAL_PROVIDERS: readonly OAuthProviderKind[] = ["google"];
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function parseSocialProviders(raw: string | undefined): OAuthProviderKind[] {
|
|
7
|
+
if (raw === undefined) return [...DEFAULT_SOCIAL_PROVIDERS];
|
|
8
|
+
const kinds = raw
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((token) => token.trim().toLowerCase())
|
|
11
|
+
.filter((token) => isOAuthProviderKind(token));
|
|
12
|
+
return [...new Set(kinds)];
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const OAUTH_PROVIDER_KINDS = ["google", "apple", "facebook"] as const;
|
|
2
|
+
export type OAuthProviderKind = (typeof OAUTH_PROVIDER_KINDS)[number];
|
|
3
|
+
|
|
4
|
+
export function isOAuthProviderKind(value: string): value is OAuthProviderKind {
|
|
5
|
+
return (OAUTH_PROVIDER_KINDS as readonly string[]).includes(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export interface AuthUser {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AuthSession {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
user: AuthUser;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AuthStateListener = (session: AuthSession | null) => void;
|
|
20
|
+
export type Unsubscribe = () => void;
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export interface AuthActionResult {
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
|
|
2
|
+
|
|
3
|
+
/** Host-selected schema inventory: managed and direct installations have different objects. */
|
|
4
|
+
export function createIdentityContribution(options: {
|
|
5
|
+
version: string;
|
|
6
|
+
requiredSchema: RequiredSchema;
|
|
7
|
+
routes?: Contribution["routes"];
|
|
8
|
+
requiredPorts?: readonly string[];
|
|
9
|
+
}): Contribution {
|
|
10
|
+
const routes = options.routes ?? [];
|
|
11
|
+
return {
|
|
12
|
+
manifest: {
|
|
13
|
+
name: "@openlup/platform", version: options.version, kind: "rail",
|
|
14
|
+
module: {
|
|
15
|
+
id: "identity", kind: "capability", provides: ["./identity", "./identity/contracts"],
|
|
16
|
+
dependsOn: [
|
|
17
|
+
{ module: "bff", entries: ["./bff"] },
|
|
18
|
+
{ module: "data", entries: ["./data"] },
|
|
19
|
+
{ module: "kernel", entries: ["./kernel"] },
|
|
20
|
+
{ module: "http", entries: ["./http"] },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
requiredSchema: options.requiredSchema,
|
|
24
|
+
emits: [], handles: [], schedules: [], routes: routes.map((route) => route.id),
|
|
25
|
+
requiredPorts: [...new Set(["emailTrustPolicy", ...(options.requiredPorts ?? [])])], env: [],
|
|
26
|
+
},
|
|
27
|
+
handlers: [], schedules: [], routes,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CustomerUserAuthenticationResult } from "./customerAuth.js";
|
|
2
|
+
import type { IdentityVerifierPort } from "./ports.js";
|
|
3
|
+
import type { ManagedIdentityClient } from "./managedClient.js";
|
|
4
|
+
import { createSupabaseIdentityVerifier } from "./managedIdentityVerifier.js";
|
|
5
|
+
export async function authenticateCustomerUserWith(
|
|
6
|
+
verifier: IdentityVerifierPort,
|
|
7
|
+
accessToken: string | null,
|
|
8
|
+
): Promise<CustomerUserAuthenticationResult> {
|
|
9
|
+
if (!accessToken) {
|
|
10
|
+
return { ok: false, code: "UNAUTHORIZED", message: "Customer session required" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const principal = await verifier.verifyAccessToken(accessToken);
|
|
14
|
+
if (!principal) {
|
|
15
|
+
return { ok: false, code: "UNAUTHORIZED", message: "Customer session required" };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return { ok: true, userId: principal.principalId };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
export async function optionalCustomerSubject(
|
|
23
|
+
rawClient: unknown,
|
|
24
|
+
accessToken: string | null,
|
|
25
|
+
): Promise<string | null> {
|
|
26
|
+
const client = rawClient as ManagedIdentityClient;
|
|
27
|
+
if (!accessToken) return null;
|
|
28
|
+
const result = await authenticateCustomerUser(client, accessToken);
|
|
29
|
+
return result.ok ? result.userId : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function authenticateCustomerUser(
|
|
33
|
+
rawClient: unknown,
|
|
34
|
+
accessToken: string | null,
|
|
35
|
+
): Promise<CustomerUserAuthenticationResult> {
|
|
36
|
+
const client = rawClient as ManagedIdentityClient;
|
|
37
|
+
return authenticateCustomerUserWith(createSupabaseIdentityVerifier(client), accessToken);
|
|
38
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
z.config({ jitless: true });
|
|
3
|
+
const platformCurrencySchema = z.string().regex(/^[A-Z]{3}$/);
|
|
4
|
+
export const customerLifecycleStageSchema = z.enum([
|
|
5
|
+
"lead",
|
|
6
|
+
"tester",
|
|
7
|
+
"customer",
|
|
8
|
+
"inactive",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export const customerMeRequestSchema = z.object({}).strict();
|
|
12
|
+
|
|
13
|
+
export const customerMeResponseSchema = z.object({
|
|
14
|
+
clientId: z.string().min(1),
|
|
15
|
+
email: z.string().min(1),
|
|
16
|
+
firstName: z.string().nullable(),
|
|
17
|
+
lastName: z.string().nullable(),
|
|
18
|
+
lifecycleStage: customerLifecycleStageSchema,
|
|
19
|
+
subscriptions: z
|
|
20
|
+
.array(
|
|
21
|
+
z
|
|
22
|
+
.object({
|
|
23
|
+
subscriptionId: z.guid(),
|
|
24
|
+
status: z.string().min(1),
|
|
25
|
+
cadenceDays: z.number().int().positive(),
|
|
26
|
+
nextCycleAt: z.string().datetime({ offset: true }).nullable(),
|
|
27
|
+
paymentMethodKind: z.string().nullable(),
|
|
28
|
+
})
|
|
29
|
+
.strict(),
|
|
30
|
+
)
|
|
31
|
+
.optional(),
|
|
32
|
+
recentOrders: z
|
|
33
|
+
.array(
|
|
34
|
+
z
|
|
35
|
+
.object({
|
|
36
|
+
orderId: z.guid(),
|
|
37
|
+
orderRef: z.string().regex(/^order_[a-z0-9-]+$/),
|
|
38
|
+
status: z.string().min(1),
|
|
39
|
+
mode: z.enum(["one_time", "subscription_cycle"]),
|
|
40
|
+
total: z.object({
|
|
41
|
+
amountMinor: z.number().int().nonnegative(),
|
|
42
|
+
currency: platformCurrencySchema,
|
|
43
|
+
}),
|
|
44
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
45
|
+
})
|
|
46
|
+
.strict(),
|
|
47
|
+
)
|
|
48
|
+
.optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
export type CustomerMeResponse = z.infer<typeof customerMeResponseSchema>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { IdentityJwtLibraryLoader } from "./jwtLibrary.js";
|
|
2
|
+
import type { ManagedIdentityClient } from "./managedClient.js";
|
|
3
|
+
import {
|
|
4
|
+
authenticateCustomerUser,
|
|
5
|
+
authenticateCustomerUserWith,
|
|
6
|
+
} from "./customerAuthorization.js";
|
|
7
|
+
import {
|
|
8
|
+
readBearerToken,
|
|
9
|
+
} from "./adminAuthorization.js";
|
|
10
|
+
import { createPostgresCustomerMePort } from "./postgresCustomerMe.js";
|
|
11
|
+
import type { PgGatewayClient } from "../data/index.js";
|
|
12
|
+
import { createSupabaseCustomerMePort as createManagedCustomerMePort } from "./managedCustomerMe.js";
|
|
13
|
+
import { createCustomerIdentityVerifier } from "./customerIdentityVerifier.js";
|
|
14
|
+
import type { CustomerMeDeps } from "./customerMeHandler.js";
|
|
15
|
+
import type { CustomerMePort } from "./customerPorts.js";
|
|
16
|
+
import { resolveBundleId } from "../kernel/index.js";
|
|
17
|
+
|
|
18
|
+
type Env = Record<string, string | undefined>;
|
|
19
|
+
type CustomerRequest = Parameters<typeof readBearerToken>[0];
|
|
20
|
+
type ManagedReadEnv = { url: string; anonKey: string };
|
|
21
|
+
type CreateCustomerClient = (env: ManagedReadEnv, token: string | null) => unknown;
|
|
22
|
+
type ActorGateway = {
|
|
23
|
+
asActor<T>(
|
|
24
|
+
claims: { role: "authenticated"; sub: string },
|
|
25
|
+
work: (gateway: unknown) => Promise<T>,
|
|
26
|
+
): Promise<T>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CustomerIdentityBindingOptions = {
|
|
30
|
+
authenticateManaged?: typeof authenticateCustomerUser;
|
|
31
|
+
createManagedClient?: CreateCustomerClient;
|
|
32
|
+
createManagedPort?: typeof createManagedCustomerMePort;
|
|
33
|
+
readManagedEnv?: () => ManagedReadEnv | null;
|
|
34
|
+
readToken?: typeof readBearerToken;
|
|
35
|
+
createPostgresGateway?: (env: Env) => ActorGateway;
|
|
36
|
+
createPostgresPort?: typeof createPostgresCustomerMePort;
|
|
37
|
+
createVerifier?: (config: Parameters<typeof createCustomerIdentityVerifier>[0]) => ReturnType<typeof createCustomerIdentityVerifier>;
|
|
38
|
+
resolveBundle?: typeof resolveBundleId;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Select the customer identity/read composition for one request. The direct
|
|
43
|
+
* path constructs its profile port only inside one actor-scoped transaction;
|
|
44
|
+
* it never uses a generic service lane or caller-provided token role.
|
|
45
|
+
*/
|
|
46
|
+
export function createCustomerIdentityBindingFactory(defaults: {
|
|
47
|
+
loadJwt: IdentityJwtLibraryLoader;
|
|
48
|
+
createManagedClient: CreateCustomerClient;
|
|
49
|
+
readManagedEnv: (env: Env) => ManagedReadEnv | null;
|
|
50
|
+
bindRequestActorDataPort: (token: string, env: Env) => ActorGateway | null;
|
|
51
|
+
}) {
|
|
52
|
+
return function createCustomerIdentityBinding(
|
|
53
|
+
req: CustomerRequest,
|
|
54
|
+
env: Env,
|
|
55
|
+
options: CustomerIdentityBindingOptions = {},
|
|
56
|
+
): Omit<CustomerMeDeps, "responseSchema"> | null {
|
|
57
|
+
const accessToken = (options.readToken ?? readBearerToken)(req);
|
|
58
|
+
const bundleId = (options.resolveBundle ?? resolveBundleId)(env);
|
|
59
|
+
|
|
60
|
+
if (bundleId !== "node-postgres") {
|
|
61
|
+
const readEnv = options.readManagedEnv ?? (() => defaults.readManagedEnv(env));
|
|
62
|
+
const managedEnv = readEnv();
|
|
63
|
+
if (!managedEnv) return null;
|
|
64
|
+
const client = (options.createManagedClient ?? defaults.createManagedClient)(
|
|
65
|
+
managedEnv,
|
|
66
|
+
accessToken,
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
authenticateUser: () =>
|
|
70
|
+
(options.authenticateManaged ?? authenticateCustomerUser)(
|
|
71
|
+
client,
|
|
72
|
+
accessToken,
|
|
73
|
+
),
|
|
74
|
+
mePort: (options.createManagedPort ?? createManagedCustomerMePort)(
|
|
75
|
+
client,
|
|
76
|
+
),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const gateway = (options.createPostgresGateway ?? createActorGateway)(env);
|
|
81
|
+
const createPort = options.createPostgresPort ?? createPostgresCustomerMePort;
|
|
82
|
+
const createVerifier =
|
|
83
|
+
options.createVerifier ?? ((config) => createCustomerIdentityVerifier(config, defaults.loadJwt));
|
|
84
|
+
const verifierConfig = {
|
|
85
|
+
audience: env.CUSTOMER_IDENTITY_AUDIENCE ?? "",
|
|
86
|
+
issuer: env.CUSTOMER_IDENTITY_ISSUER ?? "",
|
|
87
|
+
jwks: env.CUSTOMER_IDENTITY_JWKS ?? "",
|
|
88
|
+
};
|
|
89
|
+
let verifiedPrincipalId: string | null = null;
|
|
90
|
+
const mePort: CustomerMePort = {
|
|
91
|
+
getCustomerMe: (principalId) => {
|
|
92
|
+
if (principalId !== verifiedPrincipalId) {
|
|
93
|
+
return Promise.reject(new Error("customer_principal_mismatch"));
|
|
94
|
+
}
|
|
95
|
+
return gateway.asActor(
|
|
96
|
+
{ role: "authenticated", sub: verifiedPrincipalId },
|
|
97
|
+
(actorGateway) =>
|
|
98
|
+
createPort(actorGateway as PgGatewayClient).getCustomerMe(
|
|
99
|
+
principalId,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
async authenticateUser() {
|
|
107
|
+
const authentication = await authenticateCustomerUserWith(
|
|
108
|
+
createVerifier(verifierConfig),
|
|
109
|
+
accessToken,
|
|
110
|
+
);
|
|
111
|
+
if (authentication.ok) verifiedPrincipalId = authentication.userId;
|
|
112
|
+
return authentication;
|
|
113
|
+
},
|
|
114
|
+
mePort,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createActorGateway(env: Env): ActorGateway {
|
|
119
|
+
const gateway = defaults.bindRequestActorDataPort("", env);
|
|
120
|
+
if (!gateway)
|
|
121
|
+
throw new Error("customer identity data gateway is unavailable");
|
|
122
|
+
return gateway as ActorGateway;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
}
|