@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,229 @@
|
|
|
1
|
+
import type { IdentityJwtLibrary, IdentityJwtLibraryLoader, IdentityJwk } from "./jwtLibrary.js";
|
|
2
|
+
import type { IdentityVerifierPort, VerifiedPrincipal } from "./ports.js";
|
|
3
|
+
import type { OidcFailureCode } from "./contracts/oidc.js";
|
|
4
|
+
import { deriveOidcPrincipalId } from "./oidcPrincipal.js";
|
|
5
|
+
|
|
6
|
+
export interface OidcIdentityVerifierConfig {
|
|
7
|
+
issuer: string;
|
|
8
|
+
audience: string;
|
|
9
|
+
algorithms: readonly ("RS256" | "ES256")[];
|
|
10
|
+
maxTokenLifetimeSeconds: number;
|
|
11
|
+
requireSingleAudience?: boolean;
|
|
12
|
+
/** Conformance only: literal loopback addresses, never arbitrary HTTP hosts. */
|
|
13
|
+
allowLoopbackHttp?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export type OidcVerificationResult = { ok: true; principal: VerifiedPrincipal; expiresAt: string }
|
|
16
|
+
| { ok: false; code: OidcFailureCode };
|
|
17
|
+
export interface OidcReadinessReport {
|
|
18
|
+
state: "ready" | "unknown" | "unsatisfied";
|
|
19
|
+
discovery: { reachable: boolean; valid: boolean; checkedAt: string | null; expiresAt: string | null };
|
|
20
|
+
jwks: { loaded: boolean; checkedAt: string | null; expiresAt: string | null };
|
|
21
|
+
code?: OidcFailureCode;
|
|
22
|
+
}
|
|
23
|
+
export interface OidcIdentityVerifier extends IdentityVerifierPort {
|
|
24
|
+
verifyToken(token: string): Promise<OidcVerificationResult>;
|
|
25
|
+
readiness(): Promise<OidcReadinessReport>;
|
|
26
|
+
}
|
|
27
|
+
class Refusal extends Error {
|
|
28
|
+
constructor(readonly code: OidcFailureCode) { super(code); }
|
|
29
|
+
}
|
|
30
|
+
const refuse = (code: OidcFailureCode): never => { throw new Refusal(code); };
|
|
31
|
+
const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
const bounded = (value: unknown, max: number): value is string => typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= max && !Array.from(value).some(character => character.charCodeAt(0) <= 0x20 || character.charCodeAt(0) === 0x7f);
|
|
33
|
+
const stamp = (value: number | null) => value === null ? null : new Date(value).toISOString();
|
|
34
|
+
const unavailable = (code: OidcFailureCode) => code.endsWith("_UNAVAILABLE") || code === "OIDC_JWKS_REFRESH_RATE_LIMITED";
|
|
35
|
+
|
|
36
|
+
/** One binding per host/configuration. Imports and construction perform no I/O or JWT loading. */
|
|
37
|
+
export function createOidcIdentityVerifier(config: OidcIdentityVerifierConfig, dependencies: {
|
|
38
|
+
loadJwt: IdentityJwtLibraryLoader; fetch?: typeof globalThis.fetch; now?: () => number;
|
|
39
|
+
}): OidcIdentityVerifier {
|
|
40
|
+
const settings = { ...config, algorithms: [...config.algorithms] };
|
|
41
|
+
const now = dependencies.now ?? Date.now;
|
|
42
|
+
const transport = dependencies.fetch ?? globalThis.fetch;
|
|
43
|
+
function validUrl(value: string) {
|
|
44
|
+
const url = new URL(value);
|
|
45
|
+
if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:"
|
|
46
|
+
&& !(settings.allowLoopbackHttp && url.protocol === "http:" && ["127.0.0.1", "[::1]"].includes(url.hostname)))) throw new Error();
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
let origin = "", configurationValid = true;
|
|
50
|
+
try {
|
|
51
|
+
if (!bounded(settings.issuer, 2048) || !bounded(settings.audience, 256)
|
|
52
|
+
|| !settings.algorithms.length || settings.algorithms.some(alg => alg !== "RS256" && alg !== "ES256")
|
|
53
|
+
|| !Number.isInteger(settings.maxTokenLifetimeSeconds) || settings.maxTokenLifetimeSeconds < 1 || settings.maxTokenLifetimeSeconds > 3600) throw new Error();
|
|
54
|
+
origin = validUrl(settings.issuer).origin;
|
|
55
|
+
} catch { configurationValid = false; }
|
|
56
|
+
let library: Promise<IdentityJwtLibrary> | undefined;
|
|
57
|
+
const jwt = () => library ??= Promise.resolve().then(dependencies.loadJwt).catch(() => { library = undefined; return refuse("OIDC_JWT_LIBRARY_UNAVAILABLE"); });
|
|
58
|
+
let discovery: { jwksUrl: string; expires: number } | undefined;
|
|
59
|
+
let keys: { values: IdentityJwk[]; expires: number } | undefined;
|
|
60
|
+
let discoveryCheck: number | null = null, keyCheck: number | null = null, lastKeyAttempt = -Infinity;
|
|
61
|
+
let reachable = false, discoveryValid = false;
|
|
62
|
+
let discoveryFlight: Promise<NonNullable<typeof discovery>> | undefined;
|
|
63
|
+
let keyFlight: Promise<NonNullable<typeof keys>> | undefined;
|
|
64
|
+
let keyFailure: OidcFailureCode | undefined;
|
|
65
|
+
|
|
66
|
+
async function document(url: string, kind: "DISCOVERY" | "JWKS", maxAge: number) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
69
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
const deadline = new Promise<never>((_, reject) => { timer = setTimeout(() => {
|
|
71
|
+
controller.abort(); void reader?.cancel().catch(() => {});
|
|
72
|
+
reject(new Refusal(`OIDC_${kind}_UNAVAILABLE`));
|
|
73
|
+
}, 3000); });
|
|
74
|
+
try {
|
|
75
|
+
return await Promise.race([deadline, (async () => {
|
|
76
|
+
const response = await transport(url, { redirect: "error", signal: controller.signal, headers: { accept: "application/json" } });
|
|
77
|
+
if (kind === "DISCOVERY") reachable = true;
|
|
78
|
+
if (!response.ok) return refuse(`OIDC_${kind}_UNAVAILABLE`);
|
|
79
|
+
if (response.redirected || (response.url && response.url !== url)) return refuse(`OIDC_${kind}_INVALID`);
|
|
80
|
+
const length = response.headers.get("content-length");
|
|
81
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > 65536)) return refuse(`OIDC_${kind}_INVALID`);
|
|
82
|
+
if (!response.body) return refuse(`OIDC_${kind}_INVALID`);
|
|
83
|
+
reader = response.body.getReader();
|
|
84
|
+
const chunks: Uint8Array[] = []; let size = 0;
|
|
85
|
+
for (;;) {
|
|
86
|
+
const part = await reader.read(); if (part.done) break;
|
|
87
|
+
size += part.value.byteLength;
|
|
88
|
+
if (size > 65536) { void reader.cancel().catch(() => {}); return refuse(`OIDC_${kind}_INVALID`); }
|
|
89
|
+
chunks.push(part.value);
|
|
90
|
+
}
|
|
91
|
+
let value: unknown;
|
|
92
|
+
try { value = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { return refuse(`OIDC_${kind}_INVALID`); }
|
|
93
|
+
const control = response.headers.get("cache-control") ?? "";
|
|
94
|
+
const ages = [...control.matchAll(/(?:^|,)\s*max-age\s*=\s*"?(\d+)"?/gi)].map(match => Number(match[1]));
|
|
95
|
+
const age = Number(response.headers.get("age") ?? "0");
|
|
96
|
+
// Revalidate no-cache documents at the next permitted JWKS refresh.
|
|
97
|
+
const cacheBound = /(?:^|,)\s*no-cache(?:\s|,|$)/i.test(control) ? 30 : maxAge;
|
|
98
|
+
const seconds = /(?:^|,)\s*no-store(?:\s|,|$)/i.test(control) ? 0
|
|
99
|
+
: Math.max(0, Math.min(maxAge, cacheBound, ...ages) - (Number.isFinite(age) && age >= 0 ? age : maxAge));
|
|
100
|
+
return { value, expires: now() + seconds * 1000 };
|
|
101
|
+
})()]);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof Refusal) throw error;
|
|
104
|
+
return refuse(`OIDC_${kind}_UNAVAILABLE`);
|
|
105
|
+
} finally { clearTimeout(timer); controller.abort(); }
|
|
106
|
+
}
|
|
107
|
+
async function metadata() {
|
|
108
|
+
if (!configurationValid) return refuse("OIDC_CONFIGURATION_INVALID");
|
|
109
|
+
if (discovery && discovery.expires > now()) return discovery;
|
|
110
|
+
if (discoveryFlight) return discoveryFlight;
|
|
111
|
+
discoveryFlight = (async () => {
|
|
112
|
+
discoveryCheck = now(); reachable = false; discoveryValid = false;
|
|
113
|
+
const fetched = await document(`${settings.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`, "DISCOVERY", 3600);
|
|
114
|
+
if (!record(fetched.value) || fetched.value.issuer !== settings.issuer || typeof fetched.value.jwks_uri !== "string") return refuse("OIDC_DISCOVERY_INVALID");
|
|
115
|
+
try { if (validUrl(fetched.value.jwks_uri).origin !== origin) throw new Error(); }
|
|
116
|
+
catch { return refuse("OIDC_DISCOVERY_INVALID"); }
|
|
117
|
+
if (discovery?.jwksUrl !== fetched.value.jwks_uri) keys = undefined;
|
|
118
|
+
discoveryValid = true;
|
|
119
|
+
return discovery = { jwksUrl: fetched.value.jwks_uri, expires: fetched.expires };
|
|
120
|
+
})();
|
|
121
|
+
try { return await discoveryFlight; } finally { discoveryFlight = undefined; }
|
|
122
|
+
}
|
|
123
|
+
async function keySet(force = false) {
|
|
124
|
+
const meta = await metadata();
|
|
125
|
+
if (!force && keys && keys.expires > now()) return keys;
|
|
126
|
+
if (keyFlight) return keyFlight;
|
|
127
|
+
if (now() - lastKeyAttempt < 30000) return refuse("OIDC_JWKS_REFRESH_RATE_LIMITED");
|
|
128
|
+
lastKeyAttempt = now(); keyCheck = now();
|
|
129
|
+
keyFlight = (async () => {
|
|
130
|
+
const fetched = await document(meta.jwksUrl, "JWKS", 300);
|
|
131
|
+
const value = fetched.value;
|
|
132
|
+
if (!record(value) || !Array.isArray(value.keys) || !value.keys.length || value.keys.length > 64) return refuse("OIDC_JWKS_INVALID");
|
|
133
|
+
const eligible: IdentityJwk[] = [];
|
|
134
|
+
for (const key of value.keys) {
|
|
135
|
+
if (!record(key) || ["d", "p", "q", "dp", "dq", "qi", "oth", "k"].some(field => field in key)) return refuse("OIDC_JWKS_INVALID");
|
|
136
|
+
if ((key.use !== undefined && key.use !== "sig") || (key.key_ops !== undefined && (!Array.isArray(key.key_ops) || !key.key_ops.length || !key.key_ops.every(operation => operation === "verify")))) continue;
|
|
137
|
+
const alg = key.alg === undefined ? (key.kty === "RSA" ? "RS256" : key.kty === "EC" && key.crv === "P-256" ? "ES256" : "") : key.alg;
|
|
138
|
+
if (!settings.algorithms.includes(alg as "RS256" | "ES256")
|
|
139
|
+
|| (alg === "RS256" ? key.kty !== "RSA" : key.kty !== "EC" || key.crv !== "P-256")) continue;
|
|
140
|
+
if (!bounded(key.kid, 128) || (alg === "RS256" ? !bounded(key.n, 2048) || !bounded(key.e, 16)
|
|
141
|
+
: !bounded(key.x, 128) || !bounded(key.y, 128))) return refuse("OIDC_JWKS_INVALID");
|
|
142
|
+
eligible.push({ ...key, alg: String(alg) });
|
|
143
|
+
}
|
|
144
|
+
if (!eligible.length || new Set(eligible.map(key => key.kid)).size !== eligible.length) return refuse("OIDC_JWKS_INVALID");
|
|
145
|
+
const lib = await jwt();
|
|
146
|
+
try { await Promise.all(eligible.map(key => lib.importJWK(key, key.alg!))); }
|
|
147
|
+
catch { return refuse("OIDC_JWKS_INVALID"); }
|
|
148
|
+
keyFailure = undefined;
|
|
149
|
+
return keys = { values: eligible, expires: fetched.expires };
|
|
150
|
+
})();
|
|
151
|
+
try { return await keyFlight; }
|
|
152
|
+
catch (error) { keyFailure = error instanceof Refusal ? error.code : "OIDC_JWKS_UNAVAILABLE"; throw error; }
|
|
153
|
+
finally { keyFlight = undefined; }
|
|
154
|
+
}
|
|
155
|
+
async function verifyToken(token: string): Promise<OidcVerificationResult> {
|
|
156
|
+
try {
|
|
157
|
+
if (!configurationValid) return refuse("OIDC_CONFIGURATION_INVALID");
|
|
158
|
+
if (typeof token !== "string" || Buffer.byteLength(token) > 16384 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) return refuse("OIDC_TOKEN_INVALID");
|
|
159
|
+
let header: unknown, unverifiedPayload: unknown;
|
|
160
|
+
try {
|
|
161
|
+
const parts = token.split(".").map(part => {
|
|
162
|
+
const bytes = Buffer.from(part, "base64url");
|
|
163
|
+
if (bytes.toString("base64url") !== part) return refuse("OIDC_TOKEN_INVALID");
|
|
164
|
+
return bytes;
|
|
165
|
+
});
|
|
166
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
167
|
+
header = JSON.parse(decoder.decode(parts[0]!));
|
|
168
|
+
unverifiedPayload = JSON.parse(decoder.decode(parts[1]!));
|
|
169
|
+
} catch { return refuse("OIDC_TOKEN_INVALID"); }
|
|
170
|
+
if (!record(header) || !record(unverifiedPayload) || !bounded(header.kid, 128)
|
|
171
|
+
|| !settings.algorithms.includes(header.alg as "RS256" | "ES256")) return refuse("OIDC_TOKEN_INVALID");
|
|
172
|
+
// Unverified claims may refuse before I/O; only verified claims can admit.
|
|
173
|
+
if (unverifiedPayload.iss !== settings.issuer) return refuse("OIDC_ISSUER_MISMATCH");
|
|
174
|
+
let set = await keySet();
|
|
175
|
+
const select = () => set.values.find(key => key.kid === header.kid && key.alg === header.alg);
|
|
176
|
+
if (!select()) { set = await keySet(true); if (!select()) return refuse("OIDC_KID_UNKNOWN"); }
|
|
177
|
+
const lib = await jwt();
|
|
178
|
+
const selected = select()!;
|
|
179
|
+
let payload;
|
|
180
|
+
try {
|
|
181
|
+
const key = await lib.importJWK(selected, String(header.alg));
|
|
182
|
+
({ payload } = await lib.jwtVerify(token, async protectedHeader => {
|
|
183
|
+
if (protectedHeader.alg !== header.alg || protectedHeader.kid !== header.kid) return refuse("OIDC_TOKEN_INVALID");
|
|
184
|
+
return key;
|
|
185
|
+
}, { algorithms: settings.algorithms, audience: settings.audience, issuer: settings.issuer, currentDate: new Date(now()), clockTolerance: 0 }));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (record(error)) {
|
|
188
|
+
if (error.code === "ERR_JWT_EXPIRED") return refuse("OIDC_TOKEN_EXPIRED");
|
|
189
|
+
if (error.code === "ERR_JWT_CLAIM_VALIDATION_FAILED") {
|
|
190
|
+
if (error.claim === "iss") return refuse("OIDC_ISSUER_MISMATCH");
|
|
191
|
+
if (error.claim === "aud") return refuse("OIDC_AUDIENCE_MISMATCH");
|
|
192
|
+
if (error.claim === "nbf") return refuse("OIDC_TOKEN_NOT_ACTIVE");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return refuse("OIDC_TOKEN_INVALID");
|
|
196
|
+
}
|
|
197
|
+
if (payload.iss !== settings.issuer) return refuse("OIDC_ISSUER_MISMATCH");
|
|
198
|
+
if (settings.requireSingleAudience ? payload.aud !== settings.audience
|
|
199
|
+
: !(payload.aud === settings.audience || (Array.isArray(payload.aud) && payload.aud.every(aud => typeof aud === "string") && payload.aud.includes(settings.audience)))) return refuse("OIDC_AUDIENCE_MISMATCH");
|
|
200
|
+
const seconds = now() / 1000;
|
|
201
|
+
if (!Number.isSafeInteger(payload.iat) || !Number.isSafeInteger(payload.exp) || payload.exp! <= payload.iat!
|
|
202
|
+
|| payload.exp! - payload.iat! > settings.maxTokenLifetimeSeconds || payload.iat! > seconds) return refuse("OIDC_TOKEN_LIFETIME_INVALID");
|
|
203
|
+
if (payload.exp! <= seconds) return refuse("OIDC_TOKEN_EXPIRED");
|
|
204
|
+
if (payload.nbf !== undefined && (!Number.isSafeInteger(payload.nbf) || payload.nbf > seconds)) return refuse("OIDC_TOKEN_NOT_ACTIVE");
|
|
205
|
+
if (!bounded(payload.sub, 1024)) return refuse("OIDC_SUBJECT_INVALID");
|
|
206
|
+
if (!bounded(payload.email, 254) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(payload.email)) return refuse("OIDC_EMAIL_INVALID");
|
|
207
|
+
if (payload.email_verified !== true) return refuse("OIDC_EMAIL_UNVERIFIED");
|
|
208
|
+
return { ok: true, principal: { principalId: deriveOidcPrincipalId(settings.issuer, payload.sub), email: payload.email.trim().toLowerCase(), emailVerified: true }, expiresAt: stamp(payload.exp! * 1000)! };
|
|
209
|
+
} catch (error) { return { ok: false, code: error instanceof Refusal ? error.code : "OIDC_TOKEN_INVALID" }; }
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
verifyToken,
|
|
213
|
+
async verifyAccessToken(token) {
|
|
214
|
+
const result = await verifyToken(token);
|
|
215
|
+
if (result.ok) return result.principal;
|
|
216
|
+
if (unavailable(result.code)) throw new Error("OIDC identity verification unavailable");
|
|
217
|
+
return null;
|
|
218
|
+
},
|
|
219
|
+
async readiness() {
|
|
220
|
+
let code: OidcFailureCode | undefined;
|
|
221
|
+
try { await keySet(); } catch (error) { code = error instanceof Refusal ? error.code : "OIDC_JWKS_UNAVAILABLE"; }
|
|
222
|
+
if (code === "OIDC_JWKS_REFRESH_RATE_LIMITED" && keyFailure) code = keyFailure;
|
|
223
|
+
const fresh = !!keys && keys.expires > now();
|
|
224
|
+
return { state: code ? unavailable(code) ? "unknown" : "unsatisfied" : fresh ? "ready" : "unsatisfied",
|
|
225
|
+
discovery: { reachable, valid: discoveryValid && !!discovery && discovery.expires > now(), checkedAt: stamp(discoveryCheck), expiresAt: stamp(discovery?.expires ?? null) },
|
|
226
|
+
jwks: { loaded: fresh, checkedAt: stamp(keyCheck), expiresAt: stamp(keys?.expires ?? null) }, ...(code ? { code } : {}) };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Frozen UTF-8, domain-separated SHA-256 UUIDv8 mapping of the exact issuer/subject pair. */
|
|
3
|
+
export function deriveOidcPrincipalId(issuer: string, subject: string): string {
|
|
4
|
+
const bytes = createHash("sha256").update("openlup:oidc-principal:v1\0", "utf8")
|
|
5
|
+
.update(JSON.stringify([issuer, subject]), "utf8").digest().subarray(0, 16);
|
|
6
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x80;
|
|
7
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
8
|
+
const hex = bytes.toString("hex");
|
|
9
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AdminAuthPort, IdentityVerifierPort } from "./ports.js";
|
|
2
|
+
import type { OperatorIdentityStore } from "./operatorPorts.js";
|
|
3
|
+
export function createOperatorAdminAuth(options: { verifier: IdentityVerifierPort; authority: Pick<OperatorIdentityStore, "findMembership"> }): AdminAuthPort {
|
|
4
|
+
return { async authorize(token, restrictions) {
|
|
5
|
+
const principal = token ? await options.verifier.verifyAccessToken(token) : null;
|
|
6
|
+
if (!principal) return { ok: false, code: "UNAUTHORIZED", message: "Operator authentication required" };
|
|
7
|
+
const member = await options.authority.findMembership(principal.principalId);
|
|
8
|
+
if (!member || member.membershipState !== "active" || (member.role !== "admin" && member.role !== "distributor")
|
|
9
|
+
|| (restrictions?.allowedRoles && !restrictions.allowedRoles.includes(member.role))) {
|
|
10
|
+
return { ok: false, code: "FORBIDDEN", message: "Operator access denied" };
|
|
11
|
+
}
|
|
12
|
+
return { ok: true, principalId: principal.principalId, role: member.role, isMachineActor: false };
|
|
13
|
+
} };
|
|
14
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { operatorChallengeRequestSchema, operatorChallengeVerificationSchema, operatorSessionSchema } from "./contracts/operatorAuth.js";
|
|
2
|
+
import { createMagicLinkOriginResolver } from "./authVerifyUrl.js";
|
|
3
|
+
import { createIdentityResponder, type IdentityResponseOptions } from "./response.js";
|
|
4
|
+
import type { HttpRequest, HttpResponse } from "../http/index.js";
|
|
5
|
+
import type { createOperatorSessionService } from "./operatorSession.js";
|
|
6
|
+
export interface OperatorAuthMessages { invalidRequest: string; forbidden: string; unauthorized: string; unavailable: string }
|
|
7
|
+
export function createOperatorAuthHandlers(options: IdentityResponseOptions & {
|
|
8
|
+
sessions: ReturnType<typeof createOperatorSessionService>; origin: string; messages: OperatorAuthMessages;
|
|
9
|
+
padResponseTime(startedAt: number, minimumMs: number): Promise<void>;
|
|
10
|
+
}) {
|
|
11
|
+
const responder = createIdentityResponder(options);
|
|
12
|
+
const origin = createMagicLinkOriginResolver([options.origin]);
|
|
13
|
+
function admitted(req: HttpRequest, res: HttpResponse): boolean {
|
|
14
|
+
if (req.method !== "POST") { responder.sendMethodNotAllowed(res, ["POST"]); return false; }
|
|
15
|
+
const configured = origin(req);
|
|
16
|
+
if (!configured) { responder.sendBffError(res, "UPSTREAM_UNAVAILABLE", options.messages.unavailable); return false; }
|
|
17
|
+
const supplied = req.headers.origin;
|
|
18
|
+
const contentType = req.headers["content-type"];
|
|
19
|
+
if (supplied !== configured || typeof contentType !== "string" || !/^application\/json(?:;|$)/i.test(contentType)) {
|
|
20
|
+
responder.sendBffError(res, "FORBIDDEN", options.messages.forbidden); return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
async requestChallenge(req: HttpRequest, res: HttpResponse): Promise<void> {
|
|
26
|
+
if (!admitted(req, res)) return;
|
|
27
|
+
const parsed = operatorChallengeRequestSchema.safeParse(req.body);
|
|
28
|
+
if (!parsed.success) { responder.sendBffError(res, "BAD_REQUEST", options.messages.invalidRequest); return; }
|
|
29
|
+
const started = Date.now();
|
|
30
|
+
try {
|
|
31
|
+
await options.sessions.requestChallenge(parsed.data.email, async () => {
|
|
32
|
+
await options.padResponseTime(started, 400);
|
|
33
|
+
responder.sendBffSuccess(res, { accepted: true });
|
|
34
|
+
});
|
|
35
|
+
} catch { responder.sendBffError(res, "UPSTREAM_UNAVAILABLE", options.messages.unavailable); }
|
|
36
|
+
},
|
|
37
|
+
async verifyChallenge(req: HttpRequest, res: HttpResponse): Promise<void> {
|
|
38
|
+
if (!admitted(req, res)) return;
|
|
39
|
+
// A token-shaped refusal is authentication failure; an invalid envelope is a bad request.
|
|
40
|
+
if (!req.body || typeof req.body !== "object" || Object.keys(req.body).length !== 1 || typeof (req.body as { token?: unknown }).token !== "string") {
|
|
41
|
+
responder.sendBffError(res, "BAD_REQUEST", options.messages.invalidRequest); return;
|
|
42
|
+
}
|
|
43
|
+
const parsed = operatorChallengeVerificationSchema.safeParse(req.body);
|
|
44
|
+
if (!parsed.success) { responder.sendBffError(res, "UNAUTHORIZED", options.messages.unauthorized); return; }
|
|
45
|
+
try {
|
|
46
|
+
const session = await options.sessions.verifyChallenge(parsed.data.token);
|
|
47
|
+
if (!session) { responder.sendBffError(res, "UNAUTHORIZED", options.messages.unauthorized); return; }
|
|
48
|
+
responder.sendBffSuccess(res, operatorSessionSchema.parse(session));
|
|
49
|
+
} catch { responder.sendBffError(res, "UPSTREAM_UNAVAILABLE", options.messages.unavailable); }
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createCustomerIdentityVerifier } from "./customerIdentityVerifier.js";
|
|
2
|
+
import type { IdentityJwtLibraryLoader } from "./jwtLibrary.js";
|
|
3
|
+
import type { IdentityVerifierPort } from "./ports.js";
|
|
4
|
+
export function createOperatorIdentityVerifier(config: { issuer: string; audience: string; customerAudience: string; jwks: string }, loadJwt: IdentityJwtLibraryLoader): IdentityVerifierPort {
|
|
5
|
+
if (!config.customerAudience.trim() || config.audience === config.customerAudience) throw new Error("operator_audience_must_be_distinct");
|
|
6
|
+
// Inspect the payload returned by the same cryptographic verification, never an unverified decode.
|
|
7
|
+
return createCustomerIdentityVerifier(config, async () => {
|
|
8
|
+
const library = await loadJwt();
|
|
9
|
+
return { ...library, async jwtVerify(token, key, options) {
|
|
10
|
+
const verified = await library.jwtVerify(token, key, options);
|
|
11
|
+
const payload = verified.payload as Record<string, unknown>;
|
|
12
|
+
const { iat, exp } = payload;
|
|
13
|
+
if (payload.aud !== config.audience || typeof iat !== "number" || typeof exp !== "number"
|
|
14
|
+
|| !Number.isInteger(iat) || !Number.isInteger(exp) || exp <= iat || exp - iat > 900
|
|
15
|
+
|| iat > Math.floor(Date.now() / 1000) || exp <= Math.floor(Date.now() / 1000)) throw new Error("operator_token_profile_invalid");
|
|
16
|
+
return verified;
|
|
17
|
+
} };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function assertOperatorInvitationTtl(value: number): void {
|
|
2
|
+
if (!Number.isInteger(value) || value < 60 || value > 2592000) throw new Error("operator_invitation_ttl_invalid");
|
|
3
|
+
}
|
|
4
|
+
export function invitationFailure(identityCode: string, bffCode = "CONFLICT"): Error & { identityCode: string; bffCode: string } {
|
|
5
|
+
return Object.assign(new Error(identityCode), { identityCode, bffCode });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Pure parser; direct hosts call it before effects and pass the number to identity. */
|
|
9
|
+
export function parseOperatorInvitationTtl(raw?: string): number {
|
|
10
|
+
if (raw !== undefined && !/^\d+$/.test(raw)) throw new Error("operator_invitation_ttl_invalid");
|
|
11
|
+
const value = raw === undefined ? 604800 : Number(raw);
|
|
12
|
+
assertOperatorInvitationTtl(value);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OperatorInvitationAcceptance, OperatorInvitationConflict, OperatorInvitationResult, OperatorMembershipSnapshot, OperatorRole } from "./contracts/operatorInvitations.js";
|
|
2
|
+
export interface OperatorIdentityDirectoryPort {
|
|
3
|
+
ensurePrincipal(input: { email: string; redirectTo: string }): Promise<{ principalId: string; created: boolean }>;
|
|
4
|
+
}
|
|
5
|
+
export interface OperatorInvitationStore {
|
|
6
|
+
issue(input: { actorPrincipalId: string; targetPrincipalId: string; email: string; role: OperatorRole; ttlSeconds: number }): Promise<OperatorInvitationResult | { code: OperatorInvitationConflict }>;
|
|
7
|
+
read(principalId: string): Promise<OperatorMembershipSnapshot | null>;
|
|
8
|
+
/** Trusted server input only: principal must come from successful authentication. */
|
|
9
|
+
acceptOperatorInvitation(principalId: string): Promise<OperatorInvitationAcceptance>;
|
|
10
|
+
remove(input: { actorPrincipalId: string; targetPrincipalId: string; reason?: string }): Promise<{ revoked: true }>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface OperatorIdentity { principalId: string; email: string }
|
|
2
|
+
export interface OperatorMembership extends OperatorIdentity { membershipState: string; role: unknown }
|
|
3
|
+
export interface OperatorIdentityStore {
|
|
4
|
+
issue(input: { email: string; tokenHash: string }): Promise<{ deliverable: false } | { deliverable: true; email: string; expiresAt: string }>;
|
|
5
|
+
/** Activation and consumption commit together before signing. */
|
|
6
|
+
redeem(input: { tokenHash: string }): Promise<OperatorIdentity | null>;
|
|
7
|
+
findMembership(principalId: string): Promise<OperatorMembership | null>;
|
|
8
|
+
bootstrapFirstAdministrator(input: { email: string }): Promise<{ principalId: string; membershipState: "invited" }>;
|
|
9
|
+
pruneChallenges(input: { limit: number }): Promise<{ deleted: number }>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { CustomerChallengeDeliveryPort, CustomerSessionIssuerPort } from "./ports.js";
|
|
3
|
+
import type { OperatorIdentityStore } from "./operatorPorts.js";
|
|
4
|
+
import type { OperatorSession } from "./contracts/operatorAuth.js";
|
|
5
|
+
export function createOperatorSessionService(options: {
|
|
6
|
+
store: OperatorIdentityStore; delivery: CustomerChallengeDeliveryPort; issuer: CustomerSessionIssuerPort;
|
|
7
|
+
generateToken?: () => string;
|
|
8
|
+
deliveryFailed?: (code: "operator_challenge_delivery_failed") => void;
|
|
9
|
+
}) {
|
|
10
|
+
const hash = (value: string) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
11
|
+
return {
|
|
12
|
+
async requestChallenge(email: string, accept?: () => Promise<void>): Promise<void> {
|
|
13
|
+
const token = options.generateToken?.() ?? randomBytes(32).toString("base64url");
|
|
14
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(token)) throw new Error("operator_challenge_generator_invalid");
|
|
15
|
+
const result = await options.store.issue({ email: email.trim().toLowerCase(), tokenHash: hash(token) });
|
|
16
|
+
// Send the generic acceptance before mailbox transport, for every eligibility result.
|
|
17
|
+
await accept?.();
|
|
18
|
+
if (!result.deliverable) return;
|
|
19
|
+
try { await options.delivery.deliver({ email: result.email, expiresAt: result.expiresAt, token }); }
|
|
20
|
+
catch { options.deliveryFailed?.("operator_challenge_delivery_failed"); }
|
|
21
|
+
},
|
|
22
|
+
async verifyChallenge(token: string): Promise<OperatorSession | null> {
|
|
23
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(token)) return null;
|
|
24
|
+
const identity = await options.store.redeem({ tokenHash: hash(token) });
|
|
25
|
+
if (!identity) return null;
|
|
26
|
+
const session = await options.issuer.issue(identity);
|
|
27
|
+
return { ...session, user: { id: identity.principalId, email: identity.email } };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AdminAuthorizationResult,
|
|
5
|
+
AdminAuthPort,
|
|
6
|
+
AdminPrincipalRole,
|
|
7
|
+
} from "./ports.js";
|
|
8
|
+
|
|
9
|
+
const MIN_TOKEN_BYTES = 32;
|
|
10
|
+
const MAX_TOKEN_BYTES = 512;
|
|
11
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
|
+
|
|
13
|
+
export type PlatformOperatorAuthConfig = {
|
|
14
|
+
readonly operatorToken: string;
|
|
15
|
+
readonly operatorId: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PlatformOperatorAllowlistCheck = (principalId: string) => Promise<boolean>;
|
|
19
|
+
|
|
20
|
+
export type PlatformOperatorAuthConfigResolution =
|
|
21
|
+
| { readonly config: PlatformOperatorAuthConfig; readonly error?: undefined }
|
|
22
|
+
| { readonly config?: undefined; readonly error: "platform_operator_token_required" | "platform_operator_token_invalid" | "platform_operator_id_required" | "platform_operator_id_invalid" };
|
|
23
|
+
|
|
24
|
+
function byteLength(value: string): number {
|
|
25
|
+
return Buffer.byteLength(value, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export function resolvePlatformOperatorAuthConfig(
|
|
30
|
+
env: Record<string, string | undefined>,
|
|
31
|
+
): PlatformOperatorAuthConfigResolution {
|
|
32
|
+
const operatorToken = env.PLATFORM_OPERATOR_TOKEN?.trim() ?? "";
|
|
33
|
+
if (!operatorToken) return { error: "platform_operator_token_required" };
|
|
34
|
+
const tokenBytes = byteLength(operatorToken);
|
|
35
|
+
if (tokenBytes < MIN_TOKEN_BYTES || tokenBytes > MAX_TOKEN_BYTES) {
|
|
36
|
+
return { error: "platform_operator_token_invalid" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const operatorId = env.PLATFORM_OPERATOR_ID?.trim() ?? "";
|
|
40
|
+
if (!operatorId) return { error: "platform_operator_id_required" };
|
|
41
|
+
if (!UUID_PATTERN.test(operatorId)) return { error: "platform_operator_id_invalid" };
|
|
42
|
+
return { config: { operatorToken, operatorId: operatorId.toLowerCase() } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function matchesOperatorToken(candidate: string | null, expected: string): boolean {
|
|
46
|
+
if (!candidate || byteLength(candidate) > MAX_TOKEN_BYTES) return false;
|
|
47
|
+
const actualDigest = createHash("sha256").update(candidate, "utf8").digest();
|
|
48
|
+
const expectedDigest = createHash("sha256").update(expected, "utf8").digest();
|
|
49
|
+
return timingSafeEqual(actualDigest, expectedDigest);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
export function createOperatorTokenAdminAuth(
|
|
54
|
+
config: PlatformOperatorAuthConfig,
|
|
55
|
+
isOperatorAllowed: PlatformOperatorAllowlistCheck,
|
|
56
|
+
): AdminAuthPort {
|
|
57
|
+
return {
|
|
58
|
+
async authorize(
|
|
59
|
+
accessToken: string | null,
|
|
60
|
+
options: { allowedRoles?: readonly AdminPrincipalRole[] } = {},
|
|
61
|
+
): Promise<AdminAuthorizationResult> {
|
|
62
|
+
if (!matchesOperatorToken(accessToken, config.operatorToken)) {
|
|
63
|
+
return { ok: false, code: "UNAUTHORIZED", message: "Admin session required" };
|
|
64
|
+
}
|
|
65
|
+
if (options.allowedRoles && !options.allowedRoles.includes("admin")) {
|
|
66
|
+
return { ok: false, code: "FORBIDDEN", message: "Admin role required" };
|
|
67
|
+
}
|
|
68
|
+
if (!(await isOperatorAllowed(config.operatorId))) {
|
|
69
|
+
return { ok: false, code: "FORBIDDEN", message: "Admin role required" };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
principalId: config.operatorId,
|
|
74
|
+
role: "admin",
|
|
75
|
+
isMachineActor: true,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PrivacyRefusal, type PersonalDataContributor, type PersonalDataContributorRegistry } from "./privacyPorts.js";
|
|
2
|
+
export function createPersonalDataContributorRegistry<Context>(contributors: readonly PersonalDataContributor<Context>[], options: { requiredContributorIds: readonly string[] }): PersonalDataContributorRegistry<Context> {
|
|
3
|
+
const ids = contributors.map(c => c.id);
|
|
4
|
+
if (new Set(ids).size !== ids.length) throw new PrivacyRefusal("PRIVACY_CONTRIBUTOR_DUPLICATE");
|
|
5
|
+
if (contributors.some(c => !/^[a-z][a-z0-9.-]{0,99}$/.test(c.id) || !/^[a-z][a-z0-9.-]*\.v[1-9][0-9]*$/.test(c.version))) throw new PrivacyRefusal("PRIVACY_OWNERSHIP_UNDECLARED");
|
|
6
|
+
if (contributors.some(c => !Array.isArray(c.ownedResources) || c.ownedResources.length === 0 || c.ownedResources.some(resource => !resource.trim()))) throw new PrivacyRefusal("PRIVACY_OWNERSHIP_UNDECLARED");
|
|
7
|
+
return Object.freeze({ contributors: Object.freeze([...contributors]), missingContributors: Object.freeze(options.requiredContributorIds.filter(id => !ids.includes(id))) });
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type PlatformAdminAuthorizationResult =
|
|
2
|
+
| { ok: true }
|
|
3
|
+
| { ok: false; code: "UNAUTHORIZED" | "FORBIDDEN"; message: string };
|
|
4
|
+
|
|
5
|
+
export type PlatformUserAuthenticationResult =
|
|
6
|
+
| { ok: true; userId: string }
|
|
7
|
+
| { ok: false; code: "UNAUTHORIZED"; message: string };
|