@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,59 @@
|
|
|
1
|
+
// Supabase Storage adapter for the platform-runtime BlobStoragePort.
|
|
2
|
+
//
|
|
3
|
+
// Wraps `client.storage.from(bucket)` 1:1 with the existing application storage usage:
|
|
4
|
+
// upsert uploads, public-URL resolution via getPublicUrl, and bucket-scoped list/remove. Keys
|
|
5
|
+
// are bucket-relative paths. Behavior matches the current inline storage calls so the default
|
|
6
|
+
// vercel-supabase bundle (which uses this for storage buckets) is byte-identical.
|
|
7
|
+
export function createSupabaseBlobStorage(client, bucket, options = {}) {
|
|
8
|
+
const from = () => client.storage.from(bucket);
|
|
9
|
+
const defaultContentType = options.defaultContentType ?? "application/octet-stream";
|
|
10
|
+
return {
|
|
11
|
+
urlFor(key) {
|
|
12
|
+
return from().getPublicUrl(key).data.publicUrl;
|
|
13
|
+
},
|
|
14
|
+
async upload(key, data, opts) {
|
|
15
|
+
const bucketApi = from();
|
|
16
|
+
const result = await bucketApi.upload(key, data, {
|
|
17
|
+
contentType: opts?.contentType ?? defaultContentType,
|
|
18
|
+
upsert: true,
|
|
19
|
+
});
|
|
20
|
+
if (result.error) {
|
|
21
|
+
throw new Error(`supabase_storage_upload_failed:${result.error.message ?? "unknown"}`);
|
|
22
|
+
}
|
|
23
|
+
return { url: bucketApi.getPublicUrl(key).data.publicUrl, pathname: key };
|
|
24
|
+
},
|
|
25
|
+
async exists(key) {
|
|
26
|
+
// Supabase has no head(); list the parent folder and match the leaf name.
|
|
27
|
+
const slash = key.lastIndexOf("/");
|
|
28
|
+
const folder = slash >= 0 ? key.slice(0, slash) : "";
|
|
29
|
+
const leaf = slash >= 0 ? key.slice(slash + 1) : key;
|
|
30
|
+
const result = await from().list(folder || undefined);
|
|
31
|
+
if (result.error)
|
|
32
|
+
return false;
|
|
33
|
+
return (result.data ?? []).some((entry) => entry.name === leaf);
|
|
34
|
+
},
|
|
35
|
+
async delete(keyOrUrl) {
|
|
36
|
+
const result = await from().remove([toBucketKey(keyOrUrl, bucket)]);
|
|
37
|
+
if (result.error) {
|
|
38
|
+
throw new Error(`supabase_storage_remove_failed:${result.error.message ?? "unknown"}`);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async list(prefix) {
|
|
42
|
+
const result = await from().list(prefix);
|
|
43
|
+
if (result.error) {
|
|
44
|
+
throw new Error(`supabase_storage_list_failed:${result.error.message ?? "unknown"}`);
|
|
45
|
+
}
|
|
46
|
+
const base = prefix ? `${prefix.replace(/\/$/, "")}/` : "";
|
|
47
|
+
return { keys: (result.data ?? []).map((entry) => `${base}${entry.name}`) };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Accept either a bucket-relative key or a full public URL; strip everything up to and
|
|
52
|
+
// including the bucket segment so remove() always gets a bucket-relative path.
|
|
53
|
+
function toBucketKey(keyOrUrl, bucket) {
|
|
54
|
+
const marker = `/storage/v1/object/public/${bucket}/`;
|
|
55
|
+
const idx = keyOrUrl.indexOf(marker);
|
|
56
|
+
if (idx >= 0)
|
|
57
|
+
return keyOrUrl.slice(idx + marker.length);
|
|
58
|
+
return keyOrUrl;
|
|
59
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BlobStoragePort } from "@openlup/core/platform-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The `@vercel/blob` functions this adapter calls, supplied by the application (pass the module
|
|
4
|
+
* itself). The SDK reads its own credentials from the environment, as before.
|
|
5
|
+
*/
|
|
6
|
+
export interface VercelBlobSdk {
|
|
7
|
+
put(pathname: string, body: Buffer, options: {
|
|
8
|
+
access: "public";
|
|
9
|
+
contentType: string;
|
|
10
|
+
addRandomSuffix: boolean;
|
|
11
|
+
cacheControlMaxAge: number;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
url: string;
|
|
14
|
+
pathname: string;
|
|
15
|
+
}>;
|
|
16
|
+
head(url: string): Promise<{
|
|
17
|
+
url?: string;
|
|
18
|
+
} | null | undefined>;
|
|
19
|
+
del(url: string): Promise<unknown>;
|
|
20
|
+
list(options: {
|
|
21
|
+
prefix?: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
blobs: Array<{
|
|
24
|
+
pathname: string;
|
|
25
|
+
url: string;
|
|
26
|
+
uploadedAt: Date;
|
|
27
|
+
}>;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export interface VercelBlobAdapterOptions {
|
|
31
|
+
/** The `@vercel/blob` module. */
|
|
32
|
+
sdk: VercelBlobSdk;
|
|
33
|
+
/** Environment whose BLOB_READ_WRITE_TOKEN is read at each call when `token` is absent. */
|
|
34
|
+
env: Record<string, string | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Explicit token for deriving the public store URL (`urlFor`, `exists`); takes precedence over
|
|
37
|
+
* `env`. Upload, list and delete use the credentials the injected SDK reads itself, so `env` and
|
|
38
|
+
* `token` must name the same store as the SDK's own configuration.
|
|
39
|
+
*/
|
|
40
|
+
token?: string;
|
|
41
|
+
/** Default content-type when an upload omits one (image/png when absent). */
|
|
42
|
+
defaultContentType?: string;
|
|
43
|
+
cacheControlMaxAge?: number;
|
|
44
|
+
}
|
|
45
|
+
/** A listed blob plus the Vercel-specific metadata (uploadedAt) used by TTL cleanup. */
|
|
46
|
+
export interface VercelBlobEntry {
|
|
47
|
+
pathname: string;
|
|
48
|
+
url: string;
|
|
49
|
+
uploadedAt: Date;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Vercel-specific extension over the neutral BlobStoragePort.list(): also surfaces uploadedAt
|
|
53
|
+
* so the TTL cron can prune by age. Not part of the port contract (other backends carry no
|
|
54
|
+
* equivalent metadata); consumers needing it depend on this adapter shape explicitly.
|
|
55
|
+
*/
|
|
56
|
+
export interface VercelBlobStorage extends BlobStoragePort {
|
|
57
|
+
listDetailed(prefix?: string): Promise<{
|
|
58
|
+
blobs: VercelBlobEntry[];
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
/** Parse the public store id from a `vercel_blob_rw_{storeId}_{secret}` token. */
|
|
62
|
+
export declare function parseVercelStoreId(token: string | undefined): string;
|
|
63
|
+
export declare function createVercelBlobStorage(options: VercelBlobAdapterOptions): VercelBlobStorage;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Vercel Blob adapter for the platform-runtime BlobStoragePort.
|
|
2
|
+
//
|
|
3
|
+
// Wraps the CURRENT `@vercel/blob` behavior 1:1 so the default vercel-supabase bundle is
|
|
4
|
+
// byte-identical: public access, image content-type pass-through, no random suffix, the same
|
|
5
|
+
// 30-day cache-control, O(1) head() existence checks, and the deterministic public CDN URL
|
|
6
|
+
// derived from the store id encoded in BLOB_READ_WRITE_TOKEN. Operates on FULL keys
|
|
7
|
+
// (pathnames); any key prefix or suffix stays in the consuming facade.
|
|
8
|
+
const DEFAULT_CACHE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days
|
|
9
|
+
/** Parse the public store id from a `vercel_blob_rw_{storeId}_{secret}` token. */
|
|
10
|
+
export function parseVercelStoreId(token) {
|
|
11
|
+
if (!token)
|
|
12
|
+
throw new Error("BLOB_READ_WRITE_TOKEN missing");
|
|
13
|
+
const m = token.match(/^vercel_blob_rw_([a-zA-Z0-9]+)_/);
|
|
14
|
+
if (!m)
|
|
15
|
+
throw new Error("invalid BLOB_READ_WRITE_TOKEN format");
|
|
16
|
+
return m[1].toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
export function createVercelBlobStorage(options) {
|
|
19
|
+
const resolveToken = () => options.token ?? options.env.BLOB_READ_WRITE_TOKEN;
|
|
20
|
+
const cacheControlMaxAge = options.cacheControlMaxAge ?? DEFAULT_CACHE_MAX_AGE_SECONDS;
|
|
21
|
+
const defaultContentType = options.defaultContentType ?? "image/png";
|
|
22
|
+
return {
|
|
23
|
+
urlFor(key) {
|
|
24
|
+
const storeId = parseVercelStoreId(resolveToken());
|
|
25
|
+
return `https://${storeId}.public.blob.vercel-storage.com/${key}`;
|
|
26
|
+
},
|
|
27
|
+
async upload(key, data, opts) {
|
|
28
|
+
const result = await options.sdk.put(key, toUploadBody(data), {
|
|
29
|
+
access: "public",
|
|
30
|
+
contentType: opts?.contentType ?? defaultContentType,
|
|
31
|
+
addRandomSuffix: false,
|
|
32
|
+
cacheControlMaxAge,
|
|
33
|
+
});
|
|
34
|
+
return { url: result.url, pathname: result.pathname };
|
|
35
|
+
},
|
|
36
|
+
async exists(key) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await options.sdk.head(this.urlFor(key));
|
|
39
|
+
return Boolean(result?.url);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async delete(keyOrUrl) {
|
|
46
|
+
// del() accepts a full blob URL or pathname; pass through unchanged.
|
|
47
|
+
await options.sdk.del(keyOrUrl);
|
|
48
|
+
},
|
|
49
|
+
async list(prefix) {
|
|
50
|
+
const result = await options.sdk.list(prefix ? { prefix } : {});
|
|
51
|
+
return { keys: result.blobs.map((blob) => blob.pathname) };
|
|
52
|
+
},
|
|
53
|
+
async listDetailed(prefix) {
|
|
54
|
+
const result = await options.sdk.list(prefix ? { prefix } : {});
|
|
55
|
+
return {
|
|
56
|
+
blobs: result.blobs.map((blob) => ({
|
|
57
|
+
pathname: blob.pathname,
|
|
58
|
+
url: blob.url,
|
|
59
|
+
uploadedAt: blob.uploadedAt,
|
|
60
|
+
})),
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// @vercel/blob's put() accepts string | Buffer | Blob | ReadableStream etc. Buffer is a
|
|
66
|
+
// Uint8Array subclass, so both port input shapes pass straight through.
|
|
67
|
+
function toUploadBody(data) {
|
|
68
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AnalyticsPort, BlobStoragePort, DataGatewayPort, HttpRuntimePort, MigrationRunnerPort, SchedulerPort, TransactionalRuntimePort } from "@openlup/core/platform-runtime";
|
|
2
|
+
import { type PlatformBundleDescriptor, type PlatformBundleId } from "../kernel/index.js";
|
|
3
|
+
type Env = Record<string, string | undefined>;
|
|
4
|
+
export interface PlatformBundlePorts {
|
|
5
|
+
http: HttpRuntimePort | null;
|
|
6
|
+
scheduler: SchedulerPort | null;
|
|
7
|
+
blob: BlobStoragePort | null;
|
|
8
|
+
data: DataGatewayPort | null;
|
|
9
|
+
migrations: MigrationRunnerPort | null;
|
|
10
|
+
analytics: AnalyticsPort | null;
|
|
11
|
+
transactional: TransactionalRuntimePort | null;
|
|
12
|
+
}
|
|
13
|
+
export interface PlatformBundle {
|
|
14
|
+
id: PlatformBundleId;
|
|
15
|
+
descriptor: PlatformBundleDescriptor;
|
|
16
|
+
ports: PlatformBundlePorts;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One binder per port slot. Each receives the slot's capability and the environment; the blob
|
|
20
|
+
* binder receives the selected blob capability (`PLATFORM_BLOB`, else the bundle's own), and the
|
|
21
|
+
* transactional binder also receives the bundle's data capability.
|
|
22
|
+
*/
|
|
23
|
+
export interface PlatformBundleBinders {
|
|
24
|
+
/**
|
|
25
|
+
* The supabase-js `createClient` used by the default Supabase Storage blob binder. Declared
|
|
26
|
+
* structurally here so the composition entry's declarations reference no data-module type.
|
|
27
|
+
*/
|
|
28
|
+
createSupabaseClient?: (url: string, key: string, options?: {
|
|
29
|
+
auth?: {
|
|
30
|
+
persistSession?: boolean;
|
|
31
|
+
autoRefreshToken?: boolean;
|
|
32
|
+
};
|
|
33
|
+
}) => unknown;
|
|
34
|
+
http?: (capability: string, env: Env) => HttpRuntimePort | null;
|
|
35
|
+
scheduler?: (capability: string, env: Env) => SchedulerPort | null;
|
|
36
|
+
/** Defaults to `bindBlobPort`. */
|
|
37
|
+
blob?: (capability: string, env: Env) => BlobStoragePort | null;
|
|
38
|
+
/** For example `createDataBindings(...).bindDataPort`. */
|
|
39
|
+
data?: (capability: string, env: Env) => DataGatewayPort | null;
|
|
40
|
+
migrations?: (capability: string, env: Env) => MigrationRunnerPort | null;
|
|
41
|
+
analytics?: (capability: string, env: Env) => AnalyticsPort | null;
|
|
42
|
+
transactional?: (capability: string, dataCapability: string, env: Env) => TransactionalRuntimePort | null;
|
|
43
|
+
}
|
|
44
|
+
export type ComposeBundle = (env: Env) => PlatformBundle;
|
|
45
|
+
/** Build the deployment's composition root from its port binders. */
|
|
46
|
+
export declare function createBundleComposer(binders?: PlatformBundleBinders): ComposeBundle;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Platform-runtime bundle composition.
|
|
2
|
+
//
|
|
3
|
+
// Resolves the active bundle from the environment and returns its descriptor and port table. Each
|
|
4
|
+
// port slot is filled by one binder, keyed by the bundle's declared capability for that slot. A
|
|
5
|
+
// slot without a binder, or whose binder declines the capability, stays null: callers treat an
|
|
6
|
+
// unbound port as "use the deployment's direct path", never as a silent substitute.
|
|
7
|
+
//
|
|
8
|
+
// The package supplies a default binder for the blob slot. The HTTP, scheduler, data, migrations,
|
|
9
|
+
// analytics and transactional slots carry deployment values (route tables, jobs, the database
|
|
10
|
+
// client factory, migration catalogue, analytics provider, delivery runtime), so the deployment
|
|
11
|
+
// supplies them; `createDataBindings` builds the data binder.
|
|
12
|
+
import { bindBlobPort } from "../blob/index.js";
|
|
13
|
+
import { createServiceClient, readSupabaseDataGatewayEnv } from "../data/index.js";
|
|
14
|
+
import { getBundleDescriptor, resolveBlobCapability, resolveBundleId, } from "../kernel/index.js";
|
|
15
|
+
/** Build the deployment's composition root from its port binders. */
|
|
16
|
+
export function createBundleComposer(binders = {}) {
|
|
17
|
+
const bindBlob = binders.blob ?? ((capability, env) => {
|
|
18
|
+
const settings = capability === "supabase" ? readSupabaseDataGatewayEnv({
|
|
19
|
+
...env,
|
|
20
|
+
SUPABASE_URL: env.SUPABASE_URL?.trim() || env.VITE_SUPABASE_URL?.trim(),
|
|
21
|
+
}) : null;
|
|
22
|
+
const createSupabaseClient = binders.createSupabaseClient;
|
|
23
|
+
return bindBlobPort(capability, env, {
|
|
24
|
+
supabaseStorageClient: settings && createSupabaseClient
|
|
25
|
+
? () => createServiceClient(settings, createSupabaseClient)
|
|
26
|
+
: undefined,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
return function composeBundle(env) {
|
|
30
|
+
const id = resolveBundleId(env);
|
|
31
|
+
const descriptor = getBundleDescriptor(id);
|
|
32
|
+
const { capabilities } = descriptor;
|
|
33
|
+
const ports = {
|
|
34
|
+
http: binders.http?.(capabilities.http, env) ?? null,
|
|
35
|
+
scheduler: null,
|
|
36
|
+
blob: bindBlob(resolveBlobCapability(descriptor, env), env),
|
|
37
|
+
data: binders.data?.(capabilities.data, env) ?? null,
|
|
38
|
+
migrations: binders.migrations?.(capabilities.migrations, env) ?? null,
|
|
39
|
+
analytics: binders.analytics?.(capabilities.analytics, env) ?? null,
|
|
40
|
+
transactional: binders.transactional?.(capabilities.transactional, capabilities.data, env) ?? null,
|
|
41
|
+
};
|
|
42
|
+
ports.scheduler = binders.scheduler?.(capabilities.scheduler, env) ?? null;
|
|
43
|
+
return { id, descriptor, ports };
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HttpRuntimePort } from "@openlup/core/platform-runtime";
|
|
2
|
+
import type { EntrypointLoader } from "../http/index.js";
|
|
3
|
+
import { type NodeHttpRuntimeOptions } from "../http/index.js";
|
|
4
|
+
import type { ScheduledJobDeclaration } from "../scheduler/index.js";
|
|
5
|
+
/** Mount only the scheduled routes declared by the job registry. */
|
|
6
|
+
export declare function createScheduledEntrypointResolver(jobs: readonly ScheduledJobDeclaration[], load: (jobId: string) => ReturnType<EntrypointLoader>): (jobId: string) => EntrypointLoader | null;
|
|
7
|
+
/** Build the http port for a bundle capability ("node" => Node runtime; else null). */
|
|
8
|
+
export declare function bindNodeHttpPort(capability: string, options: () => NodeHttpRuntimeOptions): HttpRuntimePort | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// HTTP slot binding for the Node runtime.
|
|
2
|
+
//
|
|
3
|
+
// Only a bundle whose `http` capability is "node" receives a Node runtime; a hosted bundle keeps
|
|
4
|
+
// `http: null` and serves its route table through its own function entrypoint. The scheduled-route
|
|
5
|
+
// resolver mounts exactly the jobs the deployment declared, so an undeclared `/api/cron/<id>`
|
|
6
|
+
// path stays a 404 and a declared job without an entrypoint fails visibly when called.
|
|
7
|
+
import { createNodeHttpRuntime } from "../http/index.js";
|
|
8
|
+
/** Mount only the scheduled routes declared by the job registry. */
|
|
9
|
+
export function createScheduledEntrypointResolver(jobs, load) {
|
|
10
|
+
const declared = new Set(jobs.map((job) => job.jobId));
|
|
11
|
+
return (jobId) => declared.has(jobId) ? () => load(jobId) : null;
|
|
12
|
+
}
|
|
13
|
+
/** Build the http port for a bundle capability ("node" => Node runtime; else null). */
|
|
14
|
+
export function bindNodeHttpPort(capability, options) {
|
|
15
|
+
if (capability !== "node")
|
|
16
|
+
return null;
|
|
17
|
+
return createNodeHttpRuntime(options());
|
|
18
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createBundleComposer, type ComposeBundle, type PlatformBundle, type PlatformBundleBinders, type PlatformBundlePorts, } from "./composeBundle.js";
|
|
2
|
+
export { bindNodeHttpPort, createScheduledEntrypointResolver } from "./httpBinding.js";
|
|
3
|
+
export { createNodeHost, type NodeHost, type NodeHostAdmission, type NodeHostDependencies, type NodeSchedulerProfile, type ServeNodeOptions, type ServeNodeResult, } from "./nodeHost.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { HttpRuntimePort, JobLeasePort } from "@openlup/core/platform-runtime";
|
|
2
|
+
import type { StaticAssetReader } from "../http/index.js";
|
|
3
|
+
import { type AdvisoryLock, type CronScheduler, type ScheduledJob, type ScheduledJobDeclaration, type ScheduledJobHandler, type ScheduledJobHandlerRegistry } from "../scheduler/index.js";
|
|
4
|
+
import type { ComposeBundle, PlatformBundlePorts } from "./composeBundle.js";
|
|
5
|
+
type Env = Record<string, string | undefined>;
|
|
6
|
+
/** What the host hands its admission check before it serves or schedules anything. */
|
|
7
|
+
export interface NodeHostAdmission {
|
|
8
|
+
kind: "node";
|
|
9
|
+
nodeJobHandlers: ScheduledJobHandlerRegistry;
|
|
10
|
+
/** The path-declared jobs this process will time; empty when it does not run the scheduler. */
|
|
11
|
+
nodeJobs: readonly ScheduledJobDeclaration[];
|
|
12
|
+
/** The registry jobs this process will run directly; empty when it does not run the scheduler. */
|
|
13
|
+
scheduledJobs: readonly ScheduledJob[];
|
|
14
|
+
}
|
|
15
|
+
export interface NodeHostDependencies {
|
|
16
|
+
/** The deployment's composition root (see `createBundleComposer`). */
|
|
17
|
+
composeBundle: ComposeBundle;
|
|
18
|
+
/**
|
|
19
|
+
* Build the HTTP runtime this host serves, for the declared jobs and its static reader. The
|
|
20
|
+
* bundle's `http` port only proves that the bundle is a Node bundle at readiness time.
|
|
21
|
+
*/
|
|
22
|
+
createHttpRuntime: (input: {
|
|
23
|
+
schedulerJobs: readonly ScheduledJobDeclaration[];
|
|
24
|
+
staticReader: StaticAssetReader;
|
|
25
|
+
/** The composed ports (blob storage, data gateway and the rest) for routes to use. */
|
|
26
|
+
ports: PlatformBundlePorts;
|
|
27
|
+
}) => HttpRuntimePort;
|
|
28
|
+
/** The deployment's declared jobs, used when the caller passes none. */
|
|
29
|
+
loadSchedulerJobs: () => readonly ScheduledJobDeclaration[];
|
|
30
|
+
/** Transport-neutral jobs contributed to the registry; run in-process with no HTTP secret. */
|
|
31
|
+
scheduledJobs?: (env: Env, ports: PlatformBundlePorts) => readonly ScheduledJob[];
|
|
32
|
+
/** The job-lease port for registry jobs that declare a lease. */
|
|
33
|
+
jobLease?: (env: Env) => JobLeasePort;
|
|
34
|
+
/** Direct bindings the deployment and its domains contribute; caller overrides win. */
|
|
35
|
+
scheduledJobHandlers?: (env: Env) => ScheduledJobHandlerRegistry;
|
|
36
|
+
/** Runs first, before readiness (for example, process-wide configuration checks). */
|
|
37
|
+
prepareHost?: (env: Env) => void;
|
|
38
|
+
/** Admits the host before it serves or schedules; a rejection stops the boot. */
|
|
39
|
+
admitHost?: (env: Env, admission: NodeHostAdmission) => Promise<void>;
|
|
40
|
+
/** Operational log sink (defaults to console.log). */
|
|
41
|
+
log?: (message: string) => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A deployment's scheduler profile: a fenced projection of the declared jobs with its own
|
|
45
|
+
* validation, applied at the host's boot boundaries. Each hook may throw to stop the boot.
|
|
46
|
+
*/
|
|
47
|
+
export interface NodeSchedulerProfile {
|
|
48
|
+
/** Replaces the declared jobs with this profile's projection. Runs after any migration replay. */
|
|
49
|
+
projectJobs(env: Env, jobs: readonly ScheduledJobDeclaration[]): readonly ScheduledJobDeclaration[];
|
|
50
|
+
/** Direct bindings the profile adds; they replace the deployment's and the caller's for a job. */
|
|
51
|
+
handlers?(env: Env): ScheduledJobHandlerRegistry;
|
|
52
|
+
/** Runs once the advisory lock is resolved and before any job is registered. */
|
|
53
|
+
validateSchedule?(input: {
|
|
54
|
+
lock: AdvisoryLock | undefined;
|
|
55
|
+
jobs: readonly ScheduledJobDeclaration[];
|
|
56
|
+
overrides: ScheduledJobHandlerRegistry | undefined;
|
|
57
|
+
}): void;
|
|
58
|
+
/**
|
|
59
|
+
* Runs for each job, in order, immediately before it is registered, with its direct binding if
|
|
60
|
+
* any. A refusal stops the boot; jobs registered before it keep their timers, as the boot fails.
|
|
61
|
+
*/
|
|
62
|
+
validateJob?(job: ScheduledJobDeclaration, directHandler: ScheduledJobHandler | undefined): void;
|
|
63
|
+
}
|
|
64
|
+
export interface ServeNodeOptions {
|
|
65
|
+
/** The process environment of this host. */
|
|
66
|
+
env: Env;
|
|
67
|
+
/** dist/ root for static serving (defaults to <cwd>/dist). */
|
|
68
|
+
distDir?: string;
|
|
69
|
+
/** node-cron instance; injected so the dependency stays optional for typecheck and tests. */
|
|
70
|
+
cron?: CronScheduler;
|
|
71
|
+
/** Direct bindings added over the deployment's own for this boot. */
|
|
72
|
+
scheduledJobHandlers?: ScheduledJobHandlerRegistry;
|
|
73
|
+
/** Explicit scheduler projection; production uses the deployment's job registry. */
|
|
74
|
+
schedulerJobs?: readonly ScheduledJobDeclaration[];
|
|
75
|
+
/** Optional scheduler profile applied to the declared jobs (see `NodeSchedulerProfile`). */
|
|
76
|
+
schedulerProfile?: NodeSchedulerProfile;
|
|
77
|
+
/**
|
|
78
|
+
* Per-job cluster mutex. Defaults to the one the environment's connection string describes, so a
|
|
79
|
+
* host that runs more than one instance stops executing every timer once per instance.
|
|
80
|
+
*/
|
|
81
|
+
advisoryLock?: AdvisoryLock;
|
|
82
|
+
/** Skip binding the network listener (unit tests build the runtime without opening a socket). */
|
|
83
|
+
listen?: boolean;
|
|
84
|
+
port?: number;
|
|
85
|
+
host?: string;
|
|
86
|
+
}
|
|
87
|
+
export interface ServeNodeResult {
|
|
88
|
+
bundleId: string;
|
|
89
|
+
httpRuntime: HttpRuntimePort;
|
|
90
|
+
registeredJobs: string[];
|
|
91
|
+
/** Stop timers and the local listener after a host signal; safe to call repeatedly. */
|
|
92
|
+
stop(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export interface NodeHost {
|
|
95
|
+
/** Readiness gate: the active bundle's descriptor decides, and the bundle must bind a Node runtime. */
|
|
96
|
+
assertBundleReady(env: Env): {
|
|
97
|
+
bundleId: string;
|
|
98
|
+
};
|
|
99
|
+
/** Build (and optionally start) the Node host. */
|
|
100
|
+
serveNode(options: ServeNodeOptions): Promise<ServeNodeResult>;
|
|
101
|
+
}
|
|
102
|
+
export declare function createNodeHost(deps: NodeHostDependencies): NodeHost;
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Node (self-hosted) host bootstrap.
|
|
2
|
+
//
|
|
3
|
+
// Boots a Node platform bundle: prepare the host, check the bundle's readiness, apply the
|
|
4
|
+
// node-postgres migration replay before serving, admit the host, build the HTTP runtime, register
|
|
5
|
+
// the declared jobs with node-cron, and listen. A bundle whose migrations are managed outside the
|
|
6
|
+
// process (node-supabase) is expected to be migrated before the container starts.
|
|
7
|
+
//
|
|
8
|
+
// The deployment supplies its composition root, HTTP runtime options, job registry, direct job
|
|
9
|
+
// bindings and host admission; this module owns the order and the failure modes.
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { createDistStaticReader } from "../http/index.js";
|
|
12
|
+
import { createEnvAdvisoryLock, createNodeScheduler, mergeScheduledJobHandlers, runScheduledJob, } from "../scheduler/index.js";
|
|
13
|
+
export function createNodeHost(deps) {
|
|
14
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
15
|
+
function assertBundleReady(env) {
|
|
16
|
+
const bundle = deps.composeBundle(env);
|
|
17
|
+
const readiness = bundle.descriptor.readReadiness(env);
|
|
18
|
+
if (!readiness.ok) {
|
|
19
|
+
throw new Error(`[serve.node] bundle "${bundle.id}" not ready: ${readiness.error}`);
|
|
20
|
+
}
|
|
21
|
+
if (!bundle.ports.http) {
|
|
22
|
+
throw new Error(`[serve.node] bundle "${bundle.id}" has no http runtime (expected node bundle)`);
|
|
23
|
+
}
|
|
24
|
+
return { bundleId: bundle.id };
|
|
25
|
+
}
|
|
26
|
+
async function serveNode(options) {
|
|
27
|
+
const env = options.env;
|
|
28
|
+
deps.prepareHost?.(env);
|
|
29
|
+
const { bundleId } = assertBundleReady(env);
|
|
30
|
+
// Deploy parity (migrate -> health -> serve): the node-postgres bundle has no external migration
|
|
31
|
+
// step, so it applies the composed MigrationRunnerPort BEFORE serving. Gated on the data
|
|
32
|
+
// capability so managed-schema bundles are untouched. A listening node-postgres process has no
|
|
33
|
+
// bypass: unit tests that need no database use the non-listening seam.
|
|
34
|
+
const bundle = deps.composeBundle(env);
|
|
35
|
+
const shouldMigrate = options.listen !== false &&
|
|
36
|
+
bundle.descriptor.capabilities.data === "postgres" &&
|
|
37
|
+
bundle.ports.migrations !== null;
|
|
38
|
+
if (shouldMigrate) {
|
|
39
|
+
const { applied } = await bundle.ports.migrations.apply();
|
|
40
|
+
log(`[serve.node] bundle=${bundleId} applied ${applied.length} migration(s) before serve`);
|
|
41
|
+
}
|
|
42
|
+
const distDir = options.distDir ?? resolve(process.cwd(), "dist");
|
|
43
|
+
const staticReader = createDistStaticReader(distDir);
|
|
44
|
+
const declaredSchedulerJobs = options.schedulerJobs ?? deps.loadSchedulerJobs();
|
|
45
|
+
const profile = options.schedulerProfile;
|
|
46
|
+
const schedulerJobs = profile ? profile.projectJobs(env, declaredSchedulerJobs) : declaredSchedulerJobs;
|
|
47
|
+
const handlers = mergeScheduledJobHandlers(deps.scheduledJobHandlers?.(env), options.scheduledJobHandlers, profile?.handlers?.(env));
|
|
48
|
+
const registryJobs = deps.scheduledJobs?.(env, bundle.ports) ?? [];
|
|
49
|
+
for (const job of registryJobs) {
|
|
50
|
+
if (schedulerJobs.some((declared) => declared.jobId === job.id)) {
|
|
51
|
+
throw new Error(`scheduled_job_declared_twice:${job.id}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (deps.admitHost && (options.listen !== false || options.cron)) {
|
|
55
|
+
await deps.admitHost(env, {
|
|
56
|
+
kind: "node",
|
|
57
|
+
nodeJobHandlers: handlers,
|
|
58
|
+
nodeJobs: options.cron ? schedulerJobs : [],
|
|
59
|
+
scheduledJobs: options.cron ? registryJobs : [],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const httpRuntime = deps.createHttpRuntime({ schedulerJobs, staticReader, ports: bundle.ports });
|
|
63
|
+
// Node cron invokes a direct binding where one exists. Every other job takes the HTTP
|
|
64
|
+
// compatibility bridge to its own entrypoint, which the runtime mounts from the same registry,
|
|
65
|
+
// so a routing miss cannot masquerade as a scheduler failure and a job without an entrypoint
|
|
66
|
+
// fails visibly. The scheduler holds the cluster mutex: without it every instance of a
|
|
67
|
+
// horizontally scaled host fires the same timer in the same minute.
|
|
68
|
+
const registeredJobs = [];
|
|
69
|
+
let scheduler = null;
|
|
70
|
+
let schedulerLock;
|
|
71
|
+
if (options.cron) {
|
|
72
|
+
schedulerLock = options.advisoryLock ?? createEnvAdvisoryLock(env) ?? undefined;
|
|
73
|
+
scheduler = createNodeScheduler({
|
|
74
|
+
cron: options.cron,
|
|
75
|
+
env,
|
|
76
|
+
jobs: schedulerJobs,
|
|
77
|
+
lock: schedulerLock,
|
|
78
|
+
});
|
|
79
|
+
profile?.validateSchedule?.({
|
|
80
|
+
lock: schedulerLock,
|
|
81
|
+
jobs: scheduler.jobs,
|
|
82
|
+
overrides: options.scheduledJobHandlers,
|
|
83
|
+
});
|
|
84
|
+
const baseUrl = env.APP_BASE_URL ?? "http://localhost:3000";
|
|
85
|
+
const cronSecret = env.CRON_SECRET ?? "";
|
|
86
|
+
for (const job of scheduler.jobs) {
|
|
87
|
+
const directHandler = handlers[job.jobId];
|
|
88
|
+
profile?.validateJob?.(job, directHandler);
|
|
89
|
+
scheduler.register({
|
|
90
|
+
jobId: job.jobId,
|
|
91
|
+
schedule: job.schedule,
|
|
92
|
+
handler: async () => {
|
|
93
|
+
if (directHandler) {
|
|
94
|
+
await directHandler({ jobId: job.jobId, invocationSource: "node_cron" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const response = await fetch(new URL(job.path, baseUrl).toString(), {
|
|
98
|
+
method: "GET",
|
|
99
|
+
headers: cronSecret ? { Authorization: `Bearer ${cronSecret}` } : {},
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`node scheduler compatibility bridge for "${job.jobId}" returned HTTP ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
registeredJobs.push(job.jobId);
|
|
107
|
+
}
|
|
108
|
+
const lease = registryJobs.some((job) => job.lease) ? deps.jobLease?.(env) : undefined;
|
|
109
|
+
for (const job of registryJobs) {
|
|
110
|
+
scheduler.register({
|
|
111
|
+
jobId: job.id,
|
|
112
|
+
schedule: job.schedule,
|
|
113
|
+
handler: async () => {
|
|
114
|
+
await runScheduledJob(job, { invocation: { triggerKind: "scheduler", invocationSource: "node_cron" }, lease });
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
registeredJobs.push(job.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (options.listen !== false) {
|
|
121
|
+
const port = options.port ?? Number(env.PORT ?? 3000);
|
|
122
|
+
await httpRuntime.listen?.({ port, host: options.host ?? "0.0.0.0" });
|
|
123
|
+
log(`[serve.node] bundle=${bundleId} listening on :${port} (${registeredJobs.length} cron jobs)`);
|
|
124
|
+
}
|
|
125
|
+
let stopPromise = null;
|
|
126
|
+
return {
|
|
127
|
+
bundleId,
|
|
128
|
+
httpRuntime,
|
|
129
|
+
registeredJobs,
|
|
130
|
+
stop: () => (stopPromise ??= stopNodeHost(scheduler, schedulerLock, httpRuntime)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { assertBundleReady, serveNode };
|
|
134
|
+
}
|
|
135
|
+
async function stopNodeHost(scheduler, advisoryLock, httpRuntime) {
|
|
136
|
+
const timers = Promise.resolve().then(() => scheduler?.stopAll());
|
|
137
|
+
const server = httpRuntime.server;
|
|
138
|
+
const listener = server
|
|
139
|
+
? new Promise((resolveClose, reject) => server.close((error) => error ? reject(error) : resolveClose()))
|
|
140
|
+
: Promise.resolve();
|
|
141
|
+
const results = await Promise.allSettled([timers, advisoryLock?.close(), listener]);
|
|
142
|
+
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
143
|
+
if (failures.length > 0)
|
|
144
|
+
throw new AggregateError(failures, "node_host_shutdown_failed");
|
|
145
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type PostgresDataGatewayEnv } from "./postgresGateway.js";
|
|
2
|
+
import type { DataGatewayPort } from "@openlup/core/platform-runtime";
|
|
3
|
+
import type { SupabaseCreateClient } from "./supabaseClientFactory.js";
|
|
4
|
+
type Env = Record<string, string | undefined>;
|
|
5
|
+
type ActorClaims = {
|
|
6
|
+
sub?: string;
|
|
7
|
+
role?: string;
|
|
8
|
+
} | null;
|
|
9
|
+
/** Actor-only data access for one request. */
|
|
10
|
+
export type ActorOnlyDataPort = {
|
|
11
|
+
asActor: <T>(claims: ActorClaims, work: (gateway: unknown) => Promise<T>) => Promise<T>;
|
|
12
|
+
};
|
|
13
|
+
export interface DataPortBindingOptions {
|
|
14
|
+
/** Replaces the bindings' supabase-js `createClient` for this call (tests). */
|
|
15
|
+
createClientImpl?: SupabaseCreateClient;
|
|
16
|
+
resolveAccessToken?: (claims: ActorClaims) => string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface DataBindingDependencies {
|
|
19
|
+
/** The supabase-js `createClient` used by every supabase gateway these bindings build. */
|
|
20
|
+
createSupabaseClient: SupabaseCreateClient;
|
|
21
|
+
/** Builds the direct-PostgreSQL gateway; defaults to `createPostgresDataGateway`. */
|
|
22
|
+
createPostgresGateway?: (env: PostgresDataGatewayEnv) => DataGatewayPort;
|
|
23
|
+
}
|
|
24
|
+
export interface DataBindings {
|
|
25
|
+
/** Resolve the DataGatewayPort for a bundle's declared data capability kind. */
|
|
26
|
+
bindDataPort(dataKind: string, env: Env, requestOptions?: DataPortBindingOptions): DataGatewayPort | null;
|
|
27
|
+
/** Bind one request to actor-only data access. */
|
|
28
|
+
bindRequestActorDataPort(accessToken: string, env: Env, requestOptions?: DataPortBindingOptions): ActorOnlyDataPort | null;
|
|
29
|
+
/** Resolve and bind the active bundle's data gateway, cached per bundle and data configuration. */
|
|
30
|
+
bindBundleDataPort(env: Env): DataGatewayPort | null;
|
|
31
|
+
}
|
|
32
|
+
/** Create one set of data bindings; the bundle data port cache belongs to this set. */
|
|
33
|
+
export declare function createDataBindings(deps: DataBindingDependencies): DataBindings;
|
|
34
|
+
export {};
|