@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.
Files changed (395) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +204 -0
  3. package/README.md +471 -1
  4. package/dist/bff/dispatch.d.ts +27 -0
  5. package/dist/bff/dispatch.js +47 -0
  6. package/dist/bff/envelope.d.ts +46 -0
  7. package/dist/bff/envelope.js +80 -0
  8. package/dist/bff/health.d.ts +24 -0
  9. package/dist/bff/health.js +19 -0
  10. package/dist/bff/index.d.ts +6 -0
  11. package/dist/bff/index.js +6 -0
  12. package/dist/bff/lazyRoute.d.ts +4 -0
  13. package/dist/bff/lazyRoute.js +13 -0
  14. package/dist/bff/requestContext.d.ts +17 -0
  15. package/dist/bff/requestContext.js +26 -0
  16. package/dist/bff/routerRewrite.d.ts +2 -0
  17. package/dist/bff/routerRewrite.js +31 -0
  18. package/dist/blob/binding.d.ts +16 -0
  19. package/dist/blob/binding.js +37 -0
  20. package/dist/blob/filesystem.d.ts +8 -0
  21. package/dist/blob/filesystem.js +71 -0
  22. package/dist/blob/index.d.ts +6 -0
  23. package/dist/blob/index.js +6 -0
  24. package/dist/blob/s3.d.ts +32 -0
  25. package/dist/blob/s3.js +56 -0
  26. package/dist/blob/s3Client.d.ts +25 -0
  27. package/dist/blob/s3Client.js +76 -0
  28. package/dist/blob/supabase.d.ts +40 -0
  29. package/dist/blob/supabase.js +59 -0
  30. package/dist/blob/vercel.d.ts +63 -0
  31. package/dist/blob/vercel.js +69 -0
  32. package/dist/composition/composeBundle.d.ts +47 -0
  33. package/dist/composition/composeBundle.js +45 -0
  34. package/dist/composition/httpBinding.d.ts +8 -0
  35. package/dist/composition/httpBinding.js +18 -0
  36. package/dist/composition/index.d.ts +3 -0
  37. package/dist/composition/index.js +3 -0
  38. package/dist/composition/nodeHost.d.ts +103 -0
  39. package/dist/composition/nodeHost.js +145 -0
  40. package/dist/data/binding.d.ts +34 -0
  41. package/dist/data/binding.js +92 -0
  42. package/dist/data/index.d.ts +5 -0
  43. package/dist/data/index.js +5 -0
  44. package/dist/data/postgresGateway.d.ts +42 -0
  45. package/dist/data/postgresGateway.js +135 -0
  46. package/dist/data/queryBuilder.d.ts +54 -0
  47. package/dist/data/queryBuilder.js +240 -0
  48. package/dist/data/queryBuilderSql.d.ts +76 -0
  49. package/dist/data/queryBuilderSql.js +145 -0
  50. package/dist/data/supabaseClientFactory.d.ts +34 -0
  51. package/dist/data/supabaseClientFactory.js +56 -0
  52. package/dist/data/supabaseGateway.d.ts +22 -0
  53. package/dist/data/supabaseGateway.js +49 -0
  54. package/dist/http/deadline.d.ts +20 -0
  55. package/dist/http/deadline.js +93 -0
  56. package/dist/http/index.d.ts +6 -0
  57. package/dist/http/index.js +5 -0
  58. package/dist/http/requestAdapter.d.ts +38 -0
  59. package/dist/http/requestAdapter.js +203 -0
  60. package/dist/http/runtime.d.ts +83 -0
  61. package/dist/http/runtime.js +199 -0
  62. package/dist/http/security.d.ts +44 -0
  63. package/dist/http/security.js +114 -0
  64. package/dist/http/staticAssets.d.ts +3 -0
  65. package/dist/http/staticAssets.js +42 -0
  66. package/dist/http/types.d.ts +23 -0
  67. package/dist/http/types.js +6 -0
  68. package/dist/identity/adminAuthBinding.d.ts +28 -0
  69. package/dist/identity/adminAuthBinding.js +66 -0
  70. package/dist/identity/adminAuthorization.d.ts +34 -0
  71. package/dist/identity/adminAuthorization.js +72 -0
  72. package/dist/identity/adminContracts.d.ts +70 -0
  73. package/dist/identity/adminContracts.js +43 -0
  74. package/dist/identity/adminMagicLinkHandler.d.ts +39 -0
  75. package/dist/identity/adminMagicLinkHandler.js +94 -0
  76. package/dist/identity/adminMeHandler.d.ts +15 -0
  77. package/dist/identity/adminMeHandler.js +49 -0
  78. package/dist/identity/adminPreflight.d.ts +16 -0
  79. package/dist/identity/adminPreflight.js +38 -0
  80. package/dist/identity/adminUserActionPorts.d.ts +31 -0
  81. package/dist/identity/adminUserActionPorts.js +51 -0
  82. package/dist/identity/adminUserHandlers.d.ts +38 -0
  83. package/dist/identity/adminUserHandlers.js +140 -0
  84. package/dist/identity/authVerifyUrl.d.ts +6 -0
  85. package/dist/identity/authVerifyUrl.js +54 -0
  86. package/dist/identity/contracts/contracts.d.ts +8 -0
  87. package/dist/identity/contracts/contracts.js +2 -0
  88. package/dist/identity/contracts/emailTrust.d.ts +3 -0
  89. package/dist/identity/contracts/emailTrust.js +4 -0
  90. package/dist/identity/contracts/index.d.ts +8 -0
  91. package/dist/identity/contracts/index.js +8 -0
  92. package/dist/identity/contracts/oidc.d.ts +3 -0
  93. package/dist/identity/contracts/oidc.js +9 -0
  94. package/dist/identity/contracts/operatorAuth.d.ts +35 -0
  95. package/dist/identity/contracts/operatorAuth.js +53 -0
  96. package/dist/identity/contracts/operatorInvitations.d.ts +37 -0
  97. package/dist/identity/contracts/operatorInvitations.js +1 -0
  98. package/dist/identity/contracts/ports.d.ts +22 -0
  99. package/dist/identity/contracts/ports.js +1 -0
  100. package/dist/identity/contracts/privacy.d.ts +62 -0
  101. package/dist/identity/contracts/privacy.js +99 -0
  102. package/dist/identity/contracts/providerList.d.ts +3 -0
  103. package/dist/identity/contracts/providerList.js +11 -0
  104. package/dist/identity/contracts/types.d.ts +16 -0
  105. package/dist/identity/contracts/types.js +4 -0
  106. package/dist/identity/contribution.d.ts +8 -0
  107. package/dist/identity/contribution.js +22 -0
  108. package/dist/identity/customerAuth.d.ts +8 -0
  109. package/dist/identity/customerAuth.js +1 -0
  110. package/dist/identity/customerAuthorization.d.ts +5 -0
  111. package/dist/identity/customerAuthorization.js +22 -0
  112. package/dist/identity/customerContracts.d.ts +42 -0
  113. package/dist/identity/customerContracts.js +43 -0
  114. package/dist/identity/customerIdentityBinding.d.ts +44 -0
  115. package/dist/identity/customerIdentityBinding.js +60 -0
  116. package/dist/identity/customerIdentityVerifier.d.ts +8 -0
  117. package/dist/identity/customerIdentityVerifier.js +101 -0
  118. package/dist/identity/customerMagicLinkHandler.d.ts +38 -0
  119. package/dist/identity/customerMagicLinkHandler.js +115 -0
  120. package/dist/identity/customerMeHandler.d.ts +11 -0
  121. package/dist/identity/customerMeHandler.js +46 -0
  122. package/dist/identity/customerPorts.d.ts +4 -0
  123. package/dist/identity/customerPorts.js +1 -0
  124. package/dist/identity/customerSessionBinding.d.ts +38 -0
  125. package/dist/identity/customerSessionBinding.js +80 -0
  126. package/dist/identity/customerSessionIssuer.d.ts +10 -0
  127. package/dist/identity/customerSessionIssuer.js +57 -0
  128. package/dist/identity/customerVerifyOtpHandler.d.ts +8 -0
  129. package/dist/identity/customerVerifyOtpHandler.js +41 -0
  130. package/dist/identity/directOperatorInvitationStore.d.ts +19 -0
  131. package/dist/identity/directOperatorInvitationStore.js +193 -0
  132. package/dist/identity/emailTrustPolicy.d.ts +14 -0
  133. package/dist/identity/emailTrustPolicy.js +16 -0
  134. package/dist/identity/identityPersonalDataContributor.d.ts +7 -0
  135. package/dist/identity/identityPersonalDataContributor.js +46 -0
  136. package/dist/identity/identityReadiness.d.ts +10 -0
  137. package/dist/identity/identityReadiness.js +25 -0
  138. package/dist/identity/index.d.ts +67 -0
  139. package/dist/identity/index.js +66 -0
  140. package/dist/identity/inviteAdminUserUseCase.d.ts +89 -0
  141. package/dist/identity/inviteAdminUserUseCase.js +75 -0
  142. package/dist/identity/jwtLibrary.d.ts +54 -0
  143. package/dist/identity/jwtLibrary.js +1 -0
  144. package/dist/identity/machineActorWriteFence.d.ts +4 -0
  145. package/dist/identity/machineActorWriteFence.js +13 -0
  146. package/dist/identity/magicLinkPorts.d.ts +28 -0
  147. package/dist/identity/magicLinkPorts.js +1 -0
  148. package/dist/identity/managedAccountLinkStore.d.ts +2 -0
  149. package/dist/identity/managedAccountLinkStore.js +93 -0
  150. package/dist/identity/managedAdminAuthVerifier.d.ts +7 -0
  151. package/dist/identity/managedAdminAuthVerifier.js +27 -0
  152. package/dist/identity/managedAdminUserGateways.d.ts +123 -0
  153. package/dist/identity/managedAdminUserGateways.js +127 -0
  154. package/dist/identity/managedClient.d.ts +90 -0
  155. package/dist/identity/managedClient.js +10 -0
  156. package/dist/identity/managedClientAuthLink.d.ts +3 -0
  157. package/dist/identity/managedClientAuthLink.js +115 -0
  158. package/dist/identity/managedCustomerMe.d.ts +13 -0
  159. package/dist/identity/managedCustomerMe.js +30 -0
  160. package/dist/identity/managedEmailTrustPolicy.d.ts +11 -0
  161. package/dist/identity/managedEmailTrustPolicy.js +105 -0
  162. package/dist/identity/managedIdentityVerifier.d.ts +2 -0
  163. package/dist/identity/managedIdentityVerifier.js +16 -0
  164. package/dist/identity/managedPrivacy.d.ts +4 -0
  165. package/dist/identity/managedPrivacy.js +39 -0
  166. package/dist/identity/messages.d.ts +29 -0
  167. package/dist/identity/messages.js +39 -0
  168. package/dist/identity/oidcIdentityVerifier.d.ts +45 -0
  169. package/dist/identity/oidcIdentityVerifier.js +309 -0
  170. package/dist/identity/oidcPrincipal.d.ts +2 -0
  171. package/dist/identity/oidcPrincipal.js +10 -0
  172. package/dist/identity/operatorAdminAuth.d.ts +6 -0
  173. package/dist/identity/operatorAdminAuth.js +13 -0
  174. package/dist/identity/operatorAuthHandlers.d.ts +18 -0
  175. package/dist/identity/operatorAuthHandlers.js +71 -0
  176. package/dist/identity/operatorIdentityVerifier.d.ts +8 -0
  177. package/dist/identity/operatorIdentityVerifier.js +19 -0
  178. package/dist/identity/operatorInvitationLifecycle.d.ts +7 -0
  179. package/dist/identity/operatorInvitationLifecycle.js +15 -0
  180. package/dist/identity/operatorInvitationPorts.d.ts +31 -0
  181. package/dist/identity/operatorInvitationPorts.js +1 -0
  182. package/dist/identity/operatorPorts.d.ts +37 -0
  183. package/dist/identity/operatorPorts.js +1 -0
  184. package/dist/identity/operatorSession.d.ts +13 -0
  185. package/dist/identity/operatorSession.js +31 -0
  186. package/dist/identity/operatorTokenAdminAuth.d.ts +15 -0
  187. package/dist/identity/operatorTokenAdminAuth.js +50 -0
  188. package/dist/identity/personalDataRegistry.d.ts +4 -0
  189. package/dist/identity/personalDataRegistry.js +11 -0
  190. package/dist/identity/platformAdminAuth.d.ts +15 -0
  191. package/dist/identity/platformAdminAuth.js +1 -0
  192. package/dist/identity/ports.d.ts +127 -0
  193. package/dist/identity/ports.js +38 -0
  194. package/dist/identity/postgresAccountLinkStore.d.ts +7 -0
  195. package/dist/identity/postgresAccountLinkStore.js +52 -0
  196. package/dist/identity/postgresCustomerMe.d.ts +3 -0
  197. package/dist/identity/postgresCustomerMe.js +21 -0
  198. package/dist/identity/postgresCustomerRecovery.d.ts +11 -0
  199. package/dist/identity/postgresCustomerRecovery.js +57 -0
  200. package/dist/identity/postgresOperatorIdentity.d.ts +5 -0
  201. package/dist/identity/postgresOperatorIdentity.js +137 -0
  202. package/dist/identity/postgresPrivacy.d.ts +7 -0
  203. package/dist/identity/postgresPrivacy.js +126 -0
  204. package/dist/identity/privacyHandlers.d.ts +10 -0
  205. package/dist/identity/privacyHandlers.js +35 -0
  206. package/dist/identity/privacyPorts.d.ts +72 -0
  207. package/dist/identity/privacyPorts.js +8 -0
  208. package/dist/identity/privacyUseCases.d.ts +5 -0
  209. package/dist/identity/privacyUseCases.js +55 -0
  210. package/dist/identity/reconcileAccountForPrincipal.d.ts +20 -0
  211. package/dist/identity/reconcileAccountForPrincipal.js +66 -0
  212. package/dist/identity/reconcileAccountHandler.d.ts +11 -0
  213. package/dist/identity/reconcileAccountHandler.js +30 -0
  214. package/dist/identity/removeAdminUserUseCase.d.ts +31 -0
  215. package/dist/identity/removeAdminUserUseCase.js +60 -0
  216. package/dist/identity/response.d.ts +3 -0
  217. package/dist/identity/response.js +2 -0
  218. package/dist/identity/sql.d.ts +44 -0
  219. package/dist/identity/sql.js +10 -0
  220. package/dist/job-ledger/index.d.ts +5 -0
  221. package/dist/job-ledger/index.js +3 -0
  222. package/dist/job-ledger/ports.d.ts +10 -0
  223. package/dist/job-ledger/ports.js +7 -0
  224. package/dist/job-ledger/postgres.d.ts +10 -0
  225. package/dist/job-ledger/postgres.js +101 -0
  226. package/dist/job-ledger/sql.d.ts +7 -0
  227. package/dist/job-ledger/sql.js +10 -0
  228. package/dist/job-ledger/supabase.d.ts +77 -0
  229. package/dist/job-ledger/supabase.js +202 -0
  230. package/dist/kernel/index.d.ts +1 -0
  231. package/dist/kernel/index.js +1 -0
  232. package/dist/kernel/platformKernel.d.ts +34 -0
  233. package/dist/kernel/platformKernel.js +115 -0
  234. package/dist/migrations/index.d.ts +1 -0
  235. package/dist/migrations/index.js +1 -0
  236. package/dist/migrations/postgresRunner.d.ts +50 -0
  237. package/dist/migrations/postgresRunner.js +176 -0
  238. package/dist/scheduler/advisoryLock.d.ts +44 -0
  239. package/dist/scheduler/advisoryLock.js +114 -0
  240. package/dist/scheduler/cronGuard.d.ts +10 -0
  241. package/dist/scheduler/cronGuard.js +19 -0
  242. package/dist/scheduler/cronSecret.d.ts +5 -0
  243. package/dist/scheduler/cronSecret.js +23 -0
  244. package/dist/scheduler/handlers.d.ts +9 -0
  245. package/dist/scheduler/handlers.js +10 -0
  246. package/dist/scheduler/httpEntry.d.ts +16 -0
  247. package/dist/scheduler/httpEntry.js +31 -0
  248. package/dist/scheduler/index.d.ts +9 -0
  249. package/dist/scheduler/index.js +9 -0
  250. package/dist/scheduler/jobRegistry.d.ts +21 -0
  251. package/dist/scheduler/jobRegistry.js +39 -0
  252. package/dist/scheduler/jobs.d.ts +48 -0
  253. package/dist/scheduler/jobs.js +92 -0
  254. package/dist/scheduler/nodeScheduler.d.ts +42 -0
  255. package/dist/scheduler/nodeScheduler.js +73 -0
  256. package/dist/scheduler/vercelScheduler.d.ts +29 -0
  257. package/dist/scheduler/vercelScheduler.js +51 -0
  258. package/package.json +116 -3
  259. package/release-gates.json +143 -0
  260. package/sql/20260809150000_job_run_ledger.sql +301 -0
  261. package/sql/20260809150001_job_run_ledger_v3.sql +70 -0
  262. package/sql/20260810180001_self_host_actor_auth.sql +40 -0
  263. package/sql/20260810204057_customer_identity_read.sql +31 -0
  264. package/sql/20260814090000_customer_selfservice_recovery.sql +477 -0
  265. package/sql/20260901143000_admin_membership_authority_foundation.sql +234 -0
  266. package/sql/20261018120000_identity_operator_invitations.sql +42 -0
  267. package/sql/20261018180000_identity_operator_challenges.sql +56 -0
  268. package/sql/20261018190000_identity_privacy_erasures.sql +15 -0
  269. package/src/bff/dispatch.ts +81 -0
  270. package/src/bff/envelope.ts +142 -0
  271. package/src/bff/health.ts +37 -0
  272. package/src/bff/index.ts +30 -0
  273. package/src/bff/lazyRoute.ts +28 -0
  274. package/src/bff/requestContext.ts +48 -0
  275. package/src/bff/routerRewrite.ts +38 -0
  276. package/src/blob/binding.ts +55 -0
  277. package/src/blob/filesystem.ts +86 -0
  278. package/src/blob/index.ts +17 -0
  279. package/src/blob/s3.ts +87 -0
  280. package/src/blob/s3Client.ts +91 -0
  281. package/src/blob/supabase.ts +88 -0
  282. package/src/blob/vercel.ts +127 -0
  283. package/src/composition/composeBundle.ts +109 -0
  284. package/src/composition/httpBinding.ts +30 -0
  285. package/src/composition/index.ts +17 -0
  286. package/src/composition/nodeHost.ts +289 -0
  287. package/src/data/binding.ts +157 -0
  288. package/src/data/index.ts +33 -0
  289. package/src/data/postgresGateway.ts +186 -0
  290. package/src/data/queryBuilder.ts +300 -0
  291. package/src/data/queryBuilderSql.ts +183 -0
  292. package/src/data/supabaseClientFactory.ts +92 -0
  293. package/src/data/supabaseGateway.ts +76 -0
  294. package/src/http/deadline.ts +117 -0
  295. package/src/http/index.ts +36 -0
  296. package/src/http/requestAdapter.ts +215 -0
  297. package/src/http/runtime.ts +319 -0
  298. package/src/http/security.ts +155 -0
  299. package/src/http/staticAssets.ts +42 -0
  300. package/src/http/types.ts +31 -0
  301. package/src/identity/adminAuthBinding.ts +76 -0
  302. package/src/identity/adminAuthorization.ts +106 -0
  303. package/src/identity/adminContracts.ts +52 -0
  304. package/src/identity/adminMagicLinkHandler.ts +132 -0
  305. package/src/identity/adminMeHandler.ts +64 -0
  306. package/src/identity/adminPreflight.ts +46 -0
  307. package/src/identity/adminUserActionPorts.ts +78 -0
  308. package/src/identity/adminUserHandlers.ts +174 -0
  309. package/src/identity/authVerifyUrl.ts +56 -0
  310. package/src/identity/contracts/contracts.ts +15 -0
  311. package/src/identity/contracts/emailTrust.ts +5 -0
  312. package/src/identity/contracts/index.ts +8 -0
  313. package/src/identity/contracts/oidc.ts +10 -0
  314. package/src/identity/contracts/operatorAuth.ts +61 -0
  315. package/src/identity/contracts/operatorInvitations.ts +32 -0
  316. package/src/identity/contracts/ports.ts +25 -0
  317. package/src/identity/contracts/privacy.ts +86 -0
  318. package/src/identity/contracts/providerList.ts +13 -0
  319. package/src/identity/contracts/types.ts +25 -0
  320. package/src/identity/contribution.ts +29 -0
  321. package/src/identity/customerAuth.ts +3 -0
  322. package/src/identity/customerAuthorization.ts +38 -0
  323. package/src/identity/customerContracts.ts +52 -0
  324. package/src/identity/customerIdentityBinding.ts +125 -0
  325. package/src/identity/customerIdentityVerifier.ts +141 -0
  326. package/src/identity/customerMagicLinkHandler.ts +171 -0
  327. package/src/identity/customerMeHandler.ts +64 -0
  328. package/src/identity/customerPorts.ts +2 -0
  329. package/src/identity/customerSessionBinding.ts +114 -0
  330. package/src/identity/customerSessionIssuer.ts +71 -0
  331. package/src/identity/customerVerifyOtpHandler.ts +44 -0
  332. package/src/identity/directOperatorInvitationStore.ts +178 -0
  333. package/src/identity/emailTrustPolicy.ts +16 -0
  334. package/src/identity/identityPersonalDataContributor.ts +48 -0
  335. package/src/identity/identityReadiness.ts +26 -0
  336. package/src/identity/index.ts +69 -0
  337. package/src/identity/inviteAdminUserUseCase.ts +134 -0
  338. package/src/identity/jwtLibrary.ts +24 -0
  339. package/src/identity/machineActorWriteFence.ts +19 -0
  340. package/src/identity/magicLinkPorts.ts +7 -0
  341. package/src/identity/managedAccountLinkStore.ts +105 -0
  342. package/src/identity/managedAdminAuthVerifier.ts +40 -0
  343. package/src/identity/managedAdminUserGateways.ts +164 -0
  344. package/src/identity/managedClient.ts +58 -0
  345. package/src/identity/managedClientAuthLink.ts +120 -0
  346. package/src/identity/managedCustomerMe.ts +52 -0
  347. package/src/identity/managedEmailTrustPolicy.ts +90 -0
  348. package/src/identity/managedIdentityVerifier.ts +20 -0
  349. package/src/identity/managedPrivacy.ts +37 -0
  350. package/src/identity/messages.ts +41 -0
  351. package/src/identity/oidcIdentityVerifier.ts +229 -0
  352. package/src/identity/oidcPrincipal.ts +10 -0
  353. package/src/identity/operatorAdminAuth.ts +14 -0
  354. package/src/identity/operatorAuthHandlers.ts +52 -0
  355. package/src/identity/operatorIdentityVerifier.ts +19 -0
  356. package/src/identity/operatorInvitationLifecycle.ts +14 -0
  357. package/src/identity/operatorInvitationPorts.ts +11 -0
  358. package/src/identity/operatorPorts.ts +11 -0
  359. package/src/identity/operatorSession.ts +30 -0
  360. package/src/identity/operatorTokenAdminAuth.ts +79 -0
  361. package/src/identity/personalDataRegistry.ts +8 -0
  362. package/src/identity/platformAdminAuth.ts +7 -0
  363. package/src/identity/ports.ts +158 -0
  364. package/src/identity/postgresAccountLinkStore.ts +49 -0
  365. package/src/identity/postgresCustomerMe.ts +33 -0
  366. package/src/identity/postgresCustomerRecovery.ts +72 -0
  367. package/src/identity/postgresOperatorIdentity.ts +123 -0
  368. package/src/identity/postgresPrivacy.ts +117 -0
  369. package/src/identity/privacyHandlers.ts +33 -0
  370. package/src/identity/privacyPorts.ts +52 -0
  371. package/src/identity/privacyUseCases.ts +48 -0
  372. package/src/identity/reconcileAccountForPrincipal.ts +99 -0
  373. package/src/identity/reconcileAccountHandler.ts +36 -0
  374. package/src/identity/removeAdminUserUseCase.ts +85 -0
  375. package/src/identity/response.ts +3 -0
  376. package/src/identity/sql.ts +10 -0
  377. package/src/job-ledger/index.ts +25 -0
  378. package/src/job-ledger/ports.ts +27 -0
  379. package/src/job-ledger/postgres.ts +121 -0
  380. package/src/job-ledger/sql.ts +12 -0
  381. package/src/job-ledger/supabase.ts +294 -0
  382. package/src/kernel/index.ts +17 -0
  383. package/src/kernel/platformKernel.ts +153 -0
  384. package/src/migrations/index.ts +10 -0
  385. package/src/migrations/postgresRunner.ts +255 -0
  386. package/src/scheduler/advisoryLock.ts +145 -0
  387. package/src/scheduler/cronGuard.ts +30 -0
  388. package/src/scheduler/cronSecret.ts +26 -0
  389. package/src/scheduler/handlers.ts +24 -0
  390. package/src/scheduler/httpEntry.ts +49 -0
  391. package/src/scheduler/index.ts +47 -0
  392. package/src/scheduler/jobRegistry.ts +57 -0
  393. package/src/scheduler/jobs.ts +119 -0
  394. package/src/scheduler/nodeScheduler.ts +123 -0
  395. package/src/scheduler/vercelScheduler.ts +71 -0
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ import { createPlatformBundleRegistry, platformEnvSchema as corePlatformEnvSchema, createPlatformBundleIdGuard, } from "@openlup/core/platform-runtime";
3
+ /** The bundles a deployment can select with `PLATFORM_BUNDLE`. */
4
+ export const PLATFORM_BUNDLE_IDS = [
5
+ "vercel-supabase",
6
+ "node-supabase",
7
+ "node-postgres",
8
+ ];
9
+ /** The bundle selected when `PLATFORM_BUNDLE` is absent or unknown. */
10
+ export const DEFAULT_PLATFORM_BUNDLE = "vercel-supabase";
11
+ export const isPlatformBundleId = createPlatformBundleIdGuard(PLATFORM_BUNDLE_IDS);
12
+ /** The readiness code a bundle reports when a selected capability lacks its configuration. */
13
+ export const ENV_MISSING_CODE = "OPENLUP_E_ENV_MISSING";
14
+ /** The blob capabilities a deployment can select with `PLATFORM_BLOB`. */
15
+ export const BLOB_CAPABILITIES = ["filesystem", "s3", "supabase", "vercel-blob"];
16
+ /**
17
+ * The blob capability in effect: a valid `PLATFORM_BLOB` selection, else the bundle's own. An
18
+ * invalid selection keeps the bundle's own here and is refused by readiness.
19
+ */
20
+ export function resolveBlobCapability(descriptor, env) {
21
+ const selected = env.PLATFORM_BLOB?.trim();
22
+ return selected && BLOB_CAPABILITIES.includes(selected) ? selected : descriptor.capabilities.blob;
23
+ }
24
+ /** The settings each blob capability needs; an alternative name may stand in for the first. */
25
+ const BLOB_SETTINGS = {
26
+ "vercel-blob": [["BLOB_READ_WRITE_TOKEN"]],
27
+ supabase: [["SUPABASE_URL", "VITE_SUPABASE_URL"], ["SUPABASE_SERVICE_ROLE_KEY"], ["BLOB_SUPABASE_BUCKET"]],
28
+ filesystem: [["BLOB_FS_ROOT"], ["BLOB_FS_BASE_URL"]],
29
+ s3: [["BLOB_S3_BUCKET"], ["BLOB_S3_BASE_URL"]],
30
+ };
31
+ function present(env, name) {
32
+ return typeof env[name] === "string" && env[name].trim() !== "";
33
+ }
34
+ /**
35
+ * Per-capability readiness, in this order: an unknown PLATFORM_BUNDLE refuses (resolution itself
36
+ * keeps defaulting); a self-host bundle needs APP_BASE_URL; then every selected capability's
37
+ * missing settings are reported together under OPENLUP_E_ENV_MISSING: a scheduler needs
38
+ * CRON_SECRET, Node scheduling needs DATABASE_URL for its cluster lock (never a silent no-op lock),
39
+ * and the selected blob capability needs its settings. An unknown PLATFORM_BLOB refuses too.
40
+ */
41
+ function capabilityReadiness(descriptor, selfHost) {
42
+ return (env) => {
43
+ const raw = env.PLATFORM_BUNDLE;
44
+ if (raw !== undefined && raw.trim() !== "" && !isPlatformBundleId(raw)) {
45
+ return { ok: false, error: `unknown PLATFORM_BUNDLE "${raw}"; expected one of ${PLATFORM_BUNDLE_IDS.join(", ")}` };
46
+ }
47
+ const selectedBlob = env.PLATFORM_BLOB;
48
+ if (selectedBlob !== undefined && selectedBlob.trim() !== "" && !BLOB_CAPABILITIES.includes(selectedBlob.trim())) {
49
+ return { ok: false, error: `unknown PLATFORM_BLOB "${selectedBlob}"; expected one of ${BLOB_CAPABILITIES.join(", ")}` };
50
+ }
51
+ if (selfHost && !present(env, "APP_BASE_URL")) {
52
+ return { ok: false, error: "APP_BASE_URL is required for self-host bundles" };
53
+ }
54
+ const { scheduler } = descriptor.capabilities;
55
+ const blob = resolveBlobCapability(descriptor, env);
56
+ const missing = [];
57
+ if (scheduler === "vercel-cron" || scheduler === "node-cron") {
58
+ if (!present(env, "CRON_SECRET"))
59
+ missing.push("CRON_SECRET (scheduler)");
60
+ }
61
+ if (scheduler === "node-cron" && !present(env, "DATABASE_URL"))
62
+ missing.push("DATABASE_URL (scheduler lock)");
63
+ for (const names of BLOB_SETTINGS[blob] ?? []) {
64
+ if (!names.some((name) => present(env, name)))
65
+ missing.push(`${names[0]} (blob: ${blob})`);
66
+ }
67
+ return missing.length === 0 ? { ok: true } : { ok: false, error: `${ENV_MISSING_CODE}: ${missing.join(", ")}` };
68
+ };
69
+ }
70
+ function descriptor(id, capabilities, selfHost) {
71
+ return { id, capabilities, readReadiness: capabilityReadiness({ capabilities }, selfHost) };
72
+ }
73
+ const DESCRIPTORS = [
74
+ descriptor("vercel-supabase", {
75
+ http: "vercel",
76
+ scheduler: "vercel-cron",
77
+ blob: "vercel-blob",
78
+ data: "supabase",
79
+ migrations: "supabase",
80
+ analytics: "vercel",
81
+ transactional: "application",
82
+ }, false),
83
+ descriptor("node-supabase", {
84
+ http: "node",
85
+ scheduler: "node-cron",
86
+ blob: "supabase",
87
+ data: "supabase",
88
+ migrations: "supabase",
89
+ analytics: "gtm",
90
+ transactional: "node",
91
+ }, true),
92
+ descriptor("node-postgres", {
93
+ http: "node",
94
+ scheduler: "node-cron",
95
+ blob: "filesystem",
96
+ data: "postgres",
97
+ migrations: "postgres",
98
+ analytics: "noop",
99
+ transactional: "node",
100
+ }, true),
101
+ ];
102
+ const registry = createPlatformBundleRegistry({
103
+ descriptors: DESCRIPTORS,
104
+ defaultBundleId: DEFAULT_PLATFORM_BUNDLE,
105
+ });
106
+ export const PLATFORM_BUNDLE_DESCRIPTORS = registry.descriptors;
107
+ export function getBundleDescriptor(id) {
108
+ return registry.getBundleDescriptor(id);
109
+ }
110
+ export function resolveBundleId(env) {
111
+ return registry.resolveBundleId(env);
112
+ }
113
+ export const platformEnvSchema = corePlatformEnvSchema.extend({
114
+ PLATFORM_BUNDLE: z.enum(PLATFORM_BUNDLE_IDS).optional(),
115
+ });
@@ -0,0 +1 @@
1
+ export { createPostgresMigrationRunner, resolvePostgresMigrationRunnerEnv, type PlatformMigrationFile, type PlatformMigrationManifest, type PlatformMigrationManifestEntry, type PlatformMigrationSource, type PostgresMigrationRunnerEnv, type PostgresMigrationRunnerOptions, } from "./postgresRunner.js";
@@ -0,0 +1 @@
1
+ export { createPostgresMigrationRunner, resolvePostgresMigrationRunnerEnv, } from "./postgresRunner.js";
@@ -0,0 +1,50 @@
1
+ import type { Pool, PoolClient } from "pg";
2
+ import type { MigrationRunnerPort } from "@openlup/core/platform-runtime";
3
+ export interface PostgresMigrationRunnerEnv {
4
+ connectionString: string;
5
+ }
6
+ /** One migration file of the deployment's platform catalogue. */
7
+ export type PlatformMigrationFile = {
8
+ file: string;
9
+ content: string;
10
+ };
11
+ export type PlatformMigrationManifestEntry = {
12
+ file: string;
13
+ sha256: string;
14
+ };
15
+ /** The validated manifest: a baseline, its ordered forwards and the expected object inventory. */
16
+ export type PlatformMigrationManifest = {
17
+ schemaVersion: 1;
18
+ baseline: PlatformMigrationManifestEntry;
19
+ forward: PlatformMigrationManifestEntry[];
20
+ objectInventorySha256: string;
21
+ };
22
+ /**
23
+ * The deployment's migration catalogue and its validator. `evaluate` returns every refusal for a
24
+ * manifest against the catalogue files and, when given, the live object-inventory payload.
25
+ */
26
+ export interface PlatformMigrationSource {
27
+ readMigrations(root: string): {
28
+ migrations: PlatformMigrationFile[];
29
+ errors: string[];
30
+ };
31
+ readManifest(root: string): unknown | null;
32
+ evaluate(input: {
33
+ manifest: unknown;
34
+ migrations: readonly PlatformMigrationFile[];
35
+ objectInventoryPayload?: string;
36
+ }): string[];
37
+ }
38
+ export interface PostgresMigrationRunnerOptions {
39
+ /** Name of the PostgreSQL advisory lock that serializes migration sessions. */
40
+ lockName: string;
41
+ /** The deployment's migration catalogue. */
42
+ source: PlatformMigrationSource;
43
+ /** Serializes the live object inventory that the manifest's `objectInventorySha256` pins. */
44
+ catalogProjectionPayload: (client: PoolClient) => Promise<string>;
45
+ /** Catalogue root; defaults to process.cwd(). */
46
+ root?: string;
47
+ poolFactory?: (env: PostgresMigrationRunnerEnv) => Pool | Promise<Pool>;
48
+ }
49
+ export declare function createPostgresMigrationRunner(env: PostgresMigrationRunnerEnv, options: PostgresMigrationRunnerOptions): MigrationRunnerPort;
50
+ export declare function resolvePostgresMigrationRunnerEnv(env: Record<string, string | undefined>): PostgresMigrationRunnerEnv;
@@ -0,0 +1,176 @@
1
+ // Public-platform MigrationRunnerPort for the experimental node-postgres bundle.
2
+ // The manifest and its exact SQL bytes are validated before a pool is created.
3
+ // Applied rows are an exact prefix of that catalog; path, digest or position
4
+ // drift is fatal. Only the caller's catalogue is applied; no other migration
5
+ // history is selected or reachable through a default.
6
+ const LEDGER = "public.platform_schema_migrations";
7
+ const ACQUIRE_MIGRATION_LOCK = "SELECT pg_advisory_lock(hashtextextended($1, 0))";
8
+ const RELEASE_MIGRATION_LOCK = "SELECT pg_advisory_unlock(hashtextextended($1, 0)) AS unlocked";
9
+ export function createPostgresMigrationRunner(env, options) {
10
+ const root = options.root ?? process.cwd();
11
+ let pool = null;
12
+ async function getPool() {
13
+ if (pool)
14
+ return pool;
15
+ const factory = options.poolFactory ?? defaultPoolFactory;
16
+ pool = await factory(env);
17
+ return pool;
18
+ }
19
+ return {
20
+ async status() {
21
+ assertConnectionString(env);
22
+ const plan = loadPlan(root, options.source);
23
+ return withMigrationSession(await getPool(), options.lockName, async (client) => {
24
+ const applied = await readAppliedPrefix(client, plan.units);
25
+ if (applied.length === plan.units.length)
26
+ await assertObjectInventory(client, plan, options);
27
+ return {
28
+ pending: plan.units.slice(applied.length).map((unit) => unit.catalogPath),
29
+ applied: applied.map((unit) => unit.catalogPath),
30
+ };
31
+ });
32
+ },
33
+ async apply(opts) {
34
+ assertConnectionString(env);
35
+ const plan = loadPlan(root, options.source);
36
+ return withMigrationSession(await getPool(), options.lockName, async (client) => {
37
+ const already = await readAppliedPrefix(client, plan.units);
38
+ const pending = plan.units.slice(already.length);
39
+ if (opts?.dryRun) {
40
+ if (pending.length === 0)
41
+ await assertObjectInventory(client, plan, options);
42
+ return { applied: pending.map((unit) => unit.catalogPath) };
43
+ }
44
+ const appliedNow = [];
45
+ for (const unit of pending) {
46
+ await client.query("BEGIN");
47
+ try {
48
+ await client.query(unit.sql);
49
+ await client.query(`INSERT INTO ${LEDGER} (catalog_path, sha256, position) VALUES ($1, $2, $3)`, [unit.catalogPath, unit.sha256, unit.position]);
50
+ await client.query("COMMIT");
51
+ appliedNow.push(unit.catalogPath);
52
+ }
53
+ catch (error) {
54
+ await client.query("ROLLBACK").catch(() => { });
55
+ throw new Error(`platform migration ${unit.catalogPath} failed: ${errorMessage(error)}`);
56
+ }
57
+ }
58
+ const complete = await readAppliedPrefix(client, plan.units);
59
+ if (complete.length !== plan.units.length)
60
+ throw new Error("platform migration ledger is incomplete after apply");
61
+ await assertObjectInventory(client, plan, options);
62
+ return { applied: appliedNow };
63
+ });
64
+ },
65
+ };
66
+ }
67
+ async function withMigrationSession(pool, lockName, operation) {
68
+ const client = await pool.connect();
69
+ let locked = false;
70
+ let failed = false;
71
+ let operationFailure;
72
+ let result;
73
+ try {
74
+ await client.query(ACQUIRE_MIGRATION_LOCK, [lockName]);
75
+ locked = true;
76
+ result = await operation(client);
77
+ }
78
+ catch (error) {
79
+ failed = true;
80
+ operationFailure = error;
81
+ }
82
+ let unlockFailure;
83
+ if (locked) {
84
+ try {
85
+ const unlocked = await client.query(RELEASE_MIGRATION_LOCK, [lockName]);
86
+ if (unlocked.rows[0]?.unlocked !== true)
87
+ throw new Error("PostgreSQL did not release the advisory lock");
88
+ }
89
+ catch (error) {
90
+ unlockFailure = error;
91
+ }
92
+ }
93
+ client.release(unlockFailure !== undefined || (failed && !locked));
94
+ if (failed) {
95
+ if (unlockFailure !== undefined) {
96
+ throw new Error(`platform migration operation failed: ${errorMessage(operationFailure)}; advisory lock release failed: ${errorMessage(unlockFailure)}`);
97
+ }
98
+ throw operationFailure;
99
+ }
100
+ if (unlockFailure !== undefined) {
101
+ throw new Error(`platform migration advisory lock release failed: ${errorMessage(unlockFailure)}`);
102
+ }
103
+ return result;
104
+ }
105
+ function assertConnectionString(env) {
106
+ if (env.connectionString.trim() === "") {
107
+ throw new Error("DATABASE_URL is required for node-postgres migrations");
108
+ }
109
+ }
110
+ function loadPlan(root, source) {
111
+ const catalog = source.readMigrations(root);
112
+ const rawManifest = source.readManifest(root);
113
+ const errors = [...catalog.errors];
114
+ if (rawManifest === null)
115
+ errors.push("platform migration manifest is missing");
116
+ else
117
+ errors.push(...source.evaluate({ manifest: rawManifest, migrations: catalog.migrations }));
118
+ if (errors.length > 0)
119
+ throw new Error(`platform migration catalog rejected: ${errors.join("; ")}`);
120
+ const manifest = rawManifest;
121
+ const byPath = new Map(catalog.migrations.map((migration) => [migration.file, migration.content]));
122
+ const entries = [manifest.baseline, ...manifest.forward];
123
+ const units = entries.map((entry, position) => Object.freeze({
124
+ catalogPath: entry.file,
125
+ sha256: entry.sha256,
126
+ position,
127
+ sql: byPath.get(entry.file),
128
+ }));
129
+ return Object.freeze({ manifest, migrations: catalog.migrations, units: Object.freeze(units) });
130
+ }
131
+ async function readAppliedPrefix(client, units) {
132
+ const exists = await client.query("SELECT to_regclass($1) IS NOT NULL AS ledger_exists", [LEDGER]);
133
+ if (exists.rows[0]?.ledger_exists !== true)
134
+ return [];
135
+ const result = await client.query(`SELECT catalog_path, sha256, position FROM ${LEDGER} ORDER BY position`);
136
+ if (result.rows.length > units.length)
137
+ throw new Error("platform migration ledger contains an extra row");
138
+ for (let index = 0; index < result.rows.length; index += 1) {
139
+ const row = result.rows[index];
140
+ const unit = units[index];
141
+ if (row.catalog_path !== unit.catalogPath || row.sha256 !== unit.sha256 || row.position !== unit.position) {
142
+ throw new Error(`platform migration ledger is not the exact manifest prefix at position ${index}`);
143
+ }
144
+ }
145
+ return units.slice(0, result.rows.length);
146
+ }
147
+ async function assertObjectInventory(client, plan, options) {
148
+ const payload = await options.catalogProjectionPayload(client);
149
+ const errors = options.source.evaluate({
150
+ manifest: plan.manifest,
151
+ migrations: plan.migrations,
152
+ objectInventoryPayload: payload,
153
+ });
154
+ if (errors.length > 0)
155
+ throw new Error(`platform migration inventory rejected: ${errors.join("; ")}`);
156
+ }
157
+ function errorMessage(error) {
158
+ return error instanceof Error ? error.message : String(error);
159
+ }
160
+ async function defaultPoolFactory(env) {
161
+ const pg = await import("pg").catch(peerMissing);
162
+ const PoolCtor = (pg.default?.Pool ?? pg.Pool);
163
+ return new PoolCtor({ connectionString: env.connectionString });
164
+ }
165
+ export function resolvePostgresMigrationRunnerEnv(env) {
166
+ return { connectionString: env.DATABASE_URL ?? "" };
167
+ }
168
+ /** Translate only absence of this optional peer; preserve broken SDKs and other failures. */
169
+ function peerMissing(cause) {
170
+ if (cause instanceof Error && "code" in cause
171
+ && (cause.code === "ERR_MODULE_NOT_FOUND" || cause.code === "MODULE_NOT_FOUND")
172
+ && /^Cannot find (?:package|module) ['"]pg['"]/.test(cause.message)) {
173
+ throw new Error("OPENLUP_E_PEER_MISSING: pg is required by the Postgres migration runner; install it alongside @openlup/platform", { cause });
174
+ }
175
+ throw cause;
176
+ }
@@ -0,0 +1,44 @@
1
+ /** A cluster-wide, per-job mutex. Returns true iff THIS caller acquired the lock. */
2
+ export interface AdvisoryLock {
3
+ /** Explicit capability marker; a host that must not run a job twice requires a cluster lock. */
4
+ readonly durability?: "cluster" | "process";
5
+ /** Try to acquire the lock for `jobId` without blocking. */
6
+ tryAcquire(jobId: string): Promise<boolean>;
7
+ /** Release a previously-acquired lock for `jobId`. */
8
+ release(jobId: string): Promise<void>;
9
+ /** Release the dedicated session when the host stops; safe to call repeatedly. */
10
+ close(): Promise<void>;
11
+ }
12
+ /** Minimal pg-query surface (a `pg` Pool/Client satisfies this) — keeps `pg` out of the import graph. */
13
+ export interface AdvisoryLockQuerier {
14
+ query<T = unknown>(text: string, values?: unknown[]): Promise<{
15
+ rows: T[];
16
+ }>;
17
+ end?(): Promise<void>;
18
+ }
19
+ /**
20
+ * Deterministic 64-bit-ish key from a jobId for pg_try_advisory_lock(bigint).
21
+ * Postgres advisory locks key on bigint; we hash the job name to a stable signed-32-bit int
22
+ * (the single-arg `pg_try_advisory_lock(key bigint)` accepts it). Stable across processes.
23
+ */
24
+ export declare function advisoryLockKey(jobId: string): number;
25
+ /**
26
+ * pg-backed advisory lock. Uses `pg_try_advisory_lock` (session-scoped, non-blocking) on acquire
27
+ * and `pg_advisory_unlock` on release. NOTE: session-scoped locks require the same connection for
28
+ * acquire+release, so the supplied querier should be a single dedicated client/connection, not a
29
+ * pool that hands out a different connection per query.
30
+ */
31
+ export declare function createPgAdvisoryLock(client: AdvisoryLockQuerier, closeClient?: () => Promise<void>): AdvisoryLock;
32
+ /**
33
+ * Build the lock a long-running host should actually use, from the connection string the
34
+ * environment declares. Returns null when no connection string is configured, which leaves the
35
+ * scheduler on its always-granting default — the exact behaviour of a host with a single instance.
36
+ *
37
+ * The connection is opened LAZILY, on the first acquire, and then reused: a session-scoped
38
+ * try-lock must acquire and release on the SAME connection, and a host that never fires a timer
39
+ * must never open one. `pg` stays out of the static import graph (dynamic import), so the default
40
+ * hosted path and typecheck are untouched.
41
+ */
42
+ export declare function createEnvAdvisoryLock(env: Record<string, string | undefined>, connect?: (connectionString: string) => Promise<AdvisoryLockQuerier>): AdvisoryLock | null;
43
+ /** Lock that always grants (single-instance / tests). Never use in multi-instance Node deploys. */
44
+ export declare function createNoopAdvisoryLock(): AdvisoryLock;
@@ -0,0 +1,114 @@
1
+ // Per-job advisory lock for the Node scheduler.
2
+ //
3
+ // On Vercel exactly one cron timer fires per schedule. On a self-hosted Node bundle the SAME
4
+ // process image can run on MULTIPLE instances (horizontal scale, blue/green overlap, a stray
5
+ // second pod) — each with its own node-cron timer. Without coordination, every instance would
6
+ // fire the same job at the same minute => double-run (double emails, double polls, double
7
+ // dispatch). `pg_try_advisory_lock` gives us a cluster-wide, connection-scoped mutex keyed per job:
8
+ // only the instance that ACQUIRES the lock runs the job body; the rest skip and release nothing
9
+ // (try-lock returns false without blocking). The lock is released after the job body finishes.
10
+ //
11
+ // Injectable: the adapter depends on the AdvisoryLock interface, not on `pg` directly, so tests
12
+ // run without a database and a non-Supabase bundle can supply its own pg-backed implementation.
13
+ /**
14
+ * Deterministic 64-bit-ish key from a jobId for pg_try_advisory_lock(bigint).
15
+ * Postgres advisory locks key on bigint; we hash the job name to a stable signed-32-bit int
16
+ * (the single-arg `pg_try_advisory_lock(key bigint)` accepts it). Stable across processes.
17
+ */
18
+ export function advisoryLockKey(jobId) {
19
+ // FNV-1a 32-bit, then coerce to signed 32-bit so it fits a Postgres int safely.
20
+ let hash = 0x811c9dc5;
21
+ for (let i = 0; i < jobId.length; i += 1) {
22
+ hash ^= jobId.charCodeAt(i);
23
+ hash = Math.imul(hash, 0x01000193);
24
+ }
25
+ return hash | 0;
26
+ }
27
+ /**
28
+ * pg-backed advisory lock. Uses `pg_try_advisory_lock` (session-scoped, non-blocking) on acquire
29
+ * and `pg_advisory_unlock` on release. NOTE: session-scoped locks require the same connection for
30
+ * acquire+release, so the supplied querier should be a single dedicated client/connection, not a
31
+ * pool that hands out a different connection per query.
32
+ */
33
+ export function createPgAdvisoryLock(client, closeClient = () => client.end?.() ?? Promise.resolve()) {
34
+ let closePromise = null;
35
+ return {
36
+ durability: "cluster",
37
+ async tryAcquire(jobId) {
38
+ const key = advisoryLockKey(jobId);
39
+ const { rows } = await client.query("SELECT pg_try_advisory_lock($1) AS locked", [key]);
40
+ return rows[0]?.locked === true;
41
+ },
42
+ async release(jobId) {
43
+ const key = advisoryLockKey(jobId);
44
+ await client.query("SELECT pg_advisory_unlock($1)", [key]);
45
+ },
46
+ close() {
47
+ closePromise ??= closeClient();
48
+ return closePromise;
49
+ },
50
+ };
51
+ }
52
+ /**
53
+ * Build the lock a long-running host should actually use, from the connection string the
54
+ * environment declares. Returns null when no connection string is configured, which leaves the
55
+ * scheduler on its always-granting default — the exact behaviour of a host with a single instance.
56
+ *
57
+ * The connection is opened LAZILY, on the first acquire, and then reused: a session-scoped
58
+ * try-lock must acquire and release on the SAME connection, and a host that never fires a timer
59
+ * must never open one. `pg` stays out of the static import graph (dynamic import), so the default
60
+ * hosted path and typecheck are untouched.
61
+ */
62
+ export function createEnvAdvisoryLock(env, connect = connectDedicatedClient) {
63
+ const connectionString = env.DATABASE_URL?.trim() ?? "";
64
+ if (connectionString === "")
65
+ return null;
66
+ let pending = null;
67
+ let closed = false;
68
+ let closePromise = null;
69
+ const querier = {
70
+ async query(text, values) {
71
+ if (closed)
72
+ throw new Error("advisory_lock_closed");
73
+ pending ??= connect(connectionString);
74
+ const client = await pending;
75
+ return client.query(text, values);
76
+ },
77
+ };
78
+ return createPgAdvisoryLock(querier, () => {
79
+ closed = true;
80
+ closePromise ??= pending?.then((client) => client.end?.() ?? undefined) ?? Promise.resolve();
81
+ return closePromise;
82
+ });
83
+ }
84
+ async function connectDedicatedClient(connectionString) {
85
+ const pg = await import("pg").catch(peerMissing);
86
+ const ClientCtor = (pg.default?.Client ?? pg.Client);
87
+ const client = new ClientCtor({ connectionString });
88
+ await client.connect();
89
+ return client;
90
+ }
91
+ /** Lock that always grants (single-instance / tests). Never use in multi-instance Node deploys. */
92
+ export function createNoopAdvisoryLock() {
93
+ return {
94
+ durability: "process",
95
+ async tryAcquire() {
96
+ return true;
97
+ },
98
+ async release() {
99
+ /* no-op */
100
+ },
101
+ async close() {
102
+ /* no-op */
103
+ },
104
+ };
105
+ }
106
+ /** Translate only absence of this optional peer; preserve broken SDKs and other failures. */
107
+ function peerMissing(cause) {
108
+ if (cause instanceof Error && "code" in cause
109
+ && (cause.code === "ERR_MODULE_NOT_FOUND" || cause.code === "MODULE_NOT_FOUND")
110
+ && /^Cannot find (?:package|module) ['"]pg['"]/.test(cause.message)) {
111
+ throw new Error("OPENLUP_E_PEER_MISSING: pg is required by the Postgres scheduler advisory lock; install it alongside @openlup/platform", { cause });
112
+ }
113
+ throw cause;
114
+ }
@@ -0,0 +1,10 @@
1
+ import type { HttpRequest } from "../http/index.js";
2
+ import { bearerToken } from "./cronSecret.js";
3
+ export { bearerToken };
4
+ export interface CronAuthDenied {
5
+ status: number;
6
+ body: Record<string, unknown>;
7
+ }
8
+ export declare function authorizeCron(req: HttpRequest, env: {
9
+ CRON_SECRET?: string;
10
+ }): CronAuthDenied | null;
@@ -0,0 +1,19 @@
1
+ import { bearerToken, cronSecretMatches } from "./cronSecret.js";
2
+ export { bearerToken };
3
+ // Shared cron entrypoint guard: method allowlist + CRON_SECRET presence + constant-time bearer
4
+ // match (see ./cronSecret.ts). Returns a response to return verbatim when denied, or null when the
5
+ // request is authorized. Per-job flag kill-switches and provider/env readiness
6
+ // checks intentionally stay in each job — they differ per job and must run after
7
+ // this guard but before any ledger write.
8
+ export function authorizeCron(req, env) {
9
+ if (req.method !== "GET" && req.method !== "POST") {
10
+ return { status: 405, body: { ok: false, error: "method_not_allowed" } };
11
+ }
12
+ if (!env.CRON_SECRET) {
13
+ return { status: 503, body: { ok: false, error: "cron_secret_required" } };
14
+ }
15
+ if (!cronSecretMatches(bearerToken(req), env.CRON_SECRET)) {
16
+ return { status: 401, body: { ok: false, error: "unauthorized" } };
17
+ }
18
+ return null;
19
+ }
@@ -0,0 +1,5 @@
1
+ import type { PlatformHttpRequest } from "@openlup/core/platform-runtime";
2
+ /** Extract the Bearer token from a request Authorization header (case-insensitive). */
3
+ export declare function bearerToken(req: PlatformHttpRequest): string | null;
4
+ /** True when the presented token equals the configured secret, compared in constant time. */
5
+ export declare function cronSecretMatches(presented: string | null, expected: string): boolean;
@@ -0,0 +1,23 @@
1
+ // The one cron bearer check shared by the cron entrypoint guard and the scheduler verifier.
2
+ //
3
+ // Grammar: `Authorization: Bearer <token>`, scheme case-insensitive, the first value of a repeated
4
+ // header, the presented token trimmed and the configured secret compared as-is. The comparison takes
5
+ // the same time for every presented token of the secret's byte length, so a mismatch reveals no
6
+ // matching prefix; a token of another byte length (including any non-ASCII one) is refused first.
7
+ import { timingSafeEqual } from "node:crypto";
8
+ /** Extract the Bearer token from a request Authorization header (case-insensitive). */
9
+ export function bearerToken(req) {
10
+ const auth = req.headers.authorization;
11
+ const header = Array.isArray(auth) ? auth[0] : auth;
12
+ return header?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim() ?? null;
13
+ }
14
+ /** True when the presented token equals the configured secret, compared in constant time. */
15
+ export function cronSecretMatches(presented, expected) {
16
+ if (presented === null)
17
+ return false;
18
+ const presentedBytes = Buffer.from(presented, "utf8");
19
+ const expectedBytes = Buffer.from(expected, "utf8");
20
+ if (presentedBytes.length !== expectedBytes.length)
21
+ return false;
22
+ return timingSafeEqual(presentedBytes, expectedBytes);
23
+ }
@@ -0,0 +1,9 @@
1
+ export type ScheduledInvocationSource = "node_cron" | "scheduler";
2
+ export interface ScheduledJobInvocation {
3
+ jobId: string;
4
+ invocationSource: ScheduledInvocationSource;
5
+ }
6
+ export type ScheduledJobHandler = (invocation: ScheduledJobInvocation) => Promise<void> | void;
7
+ export type ScheduledJobHandlerRegistry = Readonly<Partial<Record<string, ScheduledJobHandler>>>;
8
+ /** Merge contributed bindings in order; a later contribution replaces an earlier one for the same job. */
9
+ export declare function mergeScheduledJobHandlers(...registries: ReadonlyArray<ScheduledJobHandlerRegistry | undefined>): ScheduledJobHandlerRegistry;
@@ -0,0 +1,10 @@
1
+ // Direct scheduled-job bindings for self-hosted runtimes.
2
+ //
3
+ // The Node host mounts every declared `/api/cron/<jobId>` route, so any job can be reached
4
+ // through the HTTP compatibility bridge. A direct binding skips that hop: it calls the
5
+ // application runtime in-process and records an honest invocation source instead of a
6
+ // hosted-cron one. Deployments and domains contribute bindings; this module only merges them.
7
+ /** Merge contributed bindings in order; a later contribution replaces an earlier one for the same job. */
8
+ export function mergeScheduledJobHandlers(...registries) {
9
+ return Object.assign({}, ...registries.filter((registry) => registry !== undefined));
10
+ }
@@ -0,0 +1,16 @@
1
+ import type { JobLeasePort } from "@openlup/core/platform-runtime";
2
+ import type { HttpRequest, HttpResponse } from "../http/index.js";
3
+ import { type ScheduledJob } from "./jobs.js";
4
+ export interface ScheduledJobHttpEntryOptions {
5
+ /** Environment whose CRON_SECRET authenticates the caller, read on each request. */
6
+ env: () => {
7
+ CRON_SECRET?: string;
8
+ };
9
+ /** Evidence recorded for HTTP invocations, for example the hosting platform's cron. */
10
+ invocationSource: string;
11
+ /** The job-lease port, required when the job declares a lease. */
12
+ lease?: JobLeasePort;
13
+ /** Error sink for a failed run (defaults to console.error). */
14
+ onError?: (jobId: string, error: unknown) => void;
15
+ }
16
+ export declare function createScheduledJobHttpEntry(job: ScheduledJob, options: ScheduledJobHttpEntryOptions): (req: HttpRequest, res: HttpResponse) => Promise<void>;
@@ -0,0 +1,31 @@
1
+ // Authenticated HTTP cron entry for a transport-neutral scheduled job.
2
+ //
3
+ // The entry is an adapter: it applies the cron guard (method, configured secret, bearer token) and
4
+ // only then runs the job with an HTTP invocation. A refusal answers the guard's own status and
5
+ // body. A run answers 200 with its result, a lease held elsewhere answers 200 as skipped, and a
6
+ // failed run answers 500 without error detail; the error goes to the host's error sink.
7
+ import { authorizeCron } from "./cronGuard.js";
8
+ import { runScheduledJob } from "./jobs.js";
9
+ export function createScheduledJobHttpEntry(job, options) {
10
+ const onError = options.onError ?? ((jobId, error) => console.error(`[scheduled-job] "${jobId}" failed`, error));
11
+ return async function scheduledJobEntry(req, res) {
12
+ const denied = authorizeCron(req, options.env());
13
+ if (denied) {
14
+ res.status(denied.status).json(denied.body);
15
+ return;
16
+ }
17
+ try {
18
+ const result = await runScheduledJob(job, {
19
+ invocation: { triggerKind: "scheduler", invocationSource: options.invocationSource },
20
+ lease: options.lease,
21
+ });
22
+ res.status(200).json(result.status === "skipped"
23
+ ? { ok: true, skipped: true, reason: result.reason }
24
+ : { ok: true, result: result.summary });
25
+ }
26
+ catch (error) {
27
+ onError(job.id, error);
28
+ res.status(500).json({ ok: false, error: "scheduled_job_failed" });
29
+ }
30
+ };
31
+ }
@@ -0,0 +1,9 @@
1
+ export { jobIdFromPath, jobRegistryFromConfig, loadJobRegistry, type PlatformRuntimeConfig, type ScheduledJobDeclaration, } from "./jobRegistry.js";
2
+ export { advisoryLockKey, createEnvAdvisoryLock, createNoopAdvisoryLock, createPgAdvisoryLock, type AdvisoryLock, type AdvisoryLockQuerier, } from "./advisoryLock.js";
3
+ export { createNodeScheduler, withAdvisoryLock, type CreateNodeSchedulerOptions, type CronScheduler, type NodeSchedulerAdapter, } from "./nodeScheduler.js";
4
+ export { bearerToken, createVercelScheduler, verifyCronAuth, type VercelSchedulerAdapter, } from "./vercelScheduler.js";
5
+ export { authorizeCron, type CronAuthDenied } from "./cronGuard.js";
6
+ export { cronSecretMatches } from "./cronSecret.js";
7
+ export { mergeScheduledJobHandlers, type ScheduledInvocationSource, type ScheduledJobHandler, type ScheduledJobHandlerRegistry, type ScheduledJobInvocation, } from "./handlers.js";
8
+ export { createScheduledJobRegistry, cronFromCadenceSeconds, runScheduledJob, scheduledJobsFromContribution, type ScheduledJob, type ScheduledJobContext, type ScheduledJobRunResult, } from "./jobs.js";
9
+ export { createScheduledJobHttpEntry, type ScheduledJobHttpEntryOptions } from "./httpEntry.js";