@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,319 @@
1
+ // Native Node adapter for manifest-driven public routing and API topology.
2
+ //
3
+ // The request order is fixed: trailing-slash redirect, the CSP report entrypoint, deployment
4
+ // exact-path entrypoints, the BFF router entrypoint, deployment path entrypoints, the BFF
5
+ // dispatcher, robots.txt, declared scheduled routes, the API 404, legacy redirects, deployment
6
+ // fallback path entrypoints, then static documents. Every deployment value (routes, entrypoints,
7
+ // header policy, robots policy, deadlines) is supplied by the composition root.
8
+
9
+ import { createServer, type Server } from "node:http";
10
+
11
+ import type { HttpRuntimePort } from "@openlup/core/platform-runtime";
12
+ import type { EntrypointLoader, HttpRequest, HttpResponse } from "./types.js";
13
+ import type { BffDispatch, RouteEntry } from "../bff/index.js";
14
+ import {
15
+ adaptRequest,
16
+ decorateRequest,
17
+ decorateResponse,
18
+ NodeRequestAbortedError,
19
+ NodeRequestBodyTooLargeError,
20
+ } from "./requestAdapter.js";
21
+ import { closeInputAfterResponse, runWithHttpDeadline } from "./deadline.js";
22
+ import {
23
+ applySecurityHeaders,
24
+ serveSpaStatic,
25
+ type NodeSecurityHeaderPolicy,
26
+ type StaticAssetReader,
27
+ } from "./security.js";
28
+
29
+ const SCHEDULED_PREFIX = "/api/cron/";
30
+ const ROBOTS_CONTENT_TYPE = "text/plain; charset=utf-8";
31
+ const EMPTY_ROUTING = { legacyRedirects: [], ssgPaths: [], csrFallback: { exactPaths: [], routeFamilies: [] } };
32
+
33
+ function requestUrl(req: HttpRequest): URL {
34
+ return new URL(req.url ?? "/", "http://node.local");
35
+ }
36
+
37
+ function capturesFor(source: string, pathname: string): Record<string, string> | null {
38
+ const sourceParts = source.split("/").slice(1);
39
+ const pathParts = pathname.split("/").slice(1);
40
+ const captures: Record<string, string> = {};
41
+ let cursor = 0;
42
+ for (const sourcePart of sourceParts) {
43
+ if (!sourcePart.startsWith(":")) {
44
+ if (pathParts[cursor++] !== sourcePart) return null;
45
+ continue;
46
+ }
47
+ const name = sourcePart.slice(1).replace(/\*$/, "");
48
+ if (sourcePart.endsWith("*")) {
49
+ captures[name] = pathParts.slice(cursor).join("/");
50
+ cursor = pathParts.length;
51
+ break;
52
+ }
53
+ const value = pathParts[cursor++];
54
+ if (!value) return null;
55
+ captures[name] = value;
56
+ }
57
+ return cursor === pathParts.length ? captures : null;
58
+ }
59
+
60
+ function legacyRedirect(pathname: string, redirects: readonly NodeLegacyRedirect[]): string | null {
61
+ for (const redirect of redirects) {
62
+ const captures = capturesFor(redirect.source, pathname);
63
+ if (!captures) continue;
64
+ return redirect.destination.replace(/:([A-Za-z][A-Za-z0-9_]*)\*?/g, (_match, name: string) => captures[name] ?? "");
65
+ }
66
+ return null;
67
+ }
68
+
69
+ function isCsrFallback(pathname: string, csrFallback: NodeCsrFallback): boolean {
70
+ return csrFallback.exactPaths.includes(pathname)
71
+ || csrFallback.routeFamilies.some((path) => pathname === path || pathname.startsWith(`${path}/`));
72
+ }
73
+
74
+ /** One deployment-specific standalone entrypoint, matched by its exact path. */
75
+ export interface HttpEntrypointRegistration {
76
+ path: string;
77
+ rawStream?: boolean;
78
+ maxDurationMs?: number;
79
+ load: EntrypointLoader;
80
+ }
81
+
82
+ /**
83
+ * One deployment-specific entrypoint matched by a path rule. `match` returns the query values the
84
+ * entrypoint receives for this path, or null when the path is not its own. The body is buffered.
85
+ */
86
+ export interface HttpPathEntrypoint {
87
+ match(pathname: string): Record<string, string> | null;
88
+ load: EntrypointLoader;
89
+ /** Request deadline; defaults to 10 seconds. */
90
+ maxDurationMs?: number;
91
+ }
92
+
93
+ export interface NodeLegacyRedirect {
94
+ source: string;
95
+ destination: string;
96
+ permanent: boolean;
97
+ }
98
+
99
+ export interface NodeCsrFallback {
100
+ exactPaths: readonly string[];
101
+ routeFamilies: readonly string[];
102
+ }
103
+
104
+ export interface NodeHttpRuntimeOptions {
105
+ /** The deployment's BFF route table. */
106
+ routes: readonly RouteEntry[];
107
+ /** The dispatcher that matches `routes` (see `createBffDispatcher`). */
108
+ dispatch: BffDispatch;
109
+ /** Validated public-route contract supplied by the host composition root. */
110
+ routing?: {
111
+ legacyRedirects: readonly NodeLegacyRedirect[];
112
+ ssgPaths: readonly string[];
113
+ csrFallback: NodeCsrFallback;
114
+ };
115
+ /** Deployment header policy, including the content security policy. */
116
+ security: NodeSecurityHeaderPolicy;
117
+ /** robots.txt body for a request host; without it `/robots.txt` is served like any other path. */
118
+ robotsTxt?: (host: string | undefined) => string;
119
+ /**
120
+ * Platform-path standalone handlers: the CSP report sink at `/api/csp-report` and the hosted
121
+ * BFF router at `/api/bff-router`. A path without a handler is treated like any other path.
122
+ */
123
+ entrypoints?: {
124
+ cspReport?: EntrypointLoader;
125
+ bffRouter?: EntrypointLoader;
126
+ };
127
+ /**
128
+ * Deployment exact-path handlers, checked after the CSP report entrypoint and before the BFF
129
+ * router; a later registration replaces an earlier one with the same path.
130
+ */
131
+ extraEntrypoints?: readonly HttpEntrypointRegistration[];
132
+ /** Deployment path-rule handlers checked before the BFF dispatcher, first match wins. */
133
+ pathEntrypoints?: readonly HttpPathEntrypoint[];
134
+ /** Deployment path-rule handlers checked after legacy redirects and before static documents. */
135
+ fallbackPathEntrypoints?: readonly HttpPathEntrypoint[];
136
+ /** Registry-derived allowlist; undeclared job ids must resolve null. */
137
+ scheduledEntrypoint?: (jobId: string) => EntrypointLoader | null;
138
+ /** Explicit host deadlines; missing cells use the bounded defaults below. */
139
+ durations?: {
140
+ bff?: number;
141
+ cspReport?: number;
142
+ bffRouter?: number;
143
+ scheduled?: (jobId: string) => number;
144
+ };
145
+ /** Static SPA reader; without one, unmatched non-API paths answer 404. */
146
+ staticReader?: StaticAssetReader;
147
+ /** Structured error sink (defaults to console.error). */
148
+ onError?: (context: string, error: unknown) => void;
149
+ }
150
+
151
+ function finalize(res: HttpResponse): void {
152
+ if (!res.writableEnded && !res.destroyed) res.end();
153
+ }
154
+
155
+ function matchPathEntrypoint(
156
+ entrypoints: readonly HttpPathEntrypoint[] | undefined,
157
+ pathname: string,
158
+ ): { entrypoint: HttpPathEntrypoint; query: Record<string, string> } | null {
159
+ for (const entrypoint of entrypoints ?? []) {
160
+ const query = entrypoint.match(pathname);
161
+ if (query) return { entrypoint, query };
162
+ }
163
+ return null;
164
+ }
165
+
166
+ /** Build the runtime's request handler (exported for unit tests; wraps every dispatch path). */
167
+ export function createRequestHandler(options: NodeHttpRuntimeOptions): HttpRuntimePort["handle"] {
168
+ const onError = options.onError ?? ((ctx, err) => console.error(`[node-http] ${ctx}`, err));
169
+ const staticReader = options.staticReader;
170
+ const routing = options.routing ?? EMPTY_ROUTING;
171
+ const ssgPaths = new Set(routing.ssgPaths);
172
+ // Built once; a deployment can deliberately replace an inherited path.
173
+ const extra = new Map((options.extraEntrypoints ?? []).map((entry) => [entry.path, entry]));
174
+
175
+ return async function handle(req: HttpRequest, res: HttpResponse): Promise<void> {
176
+ const url = requestUrl(req);
177
+ const pathname = url.pathname;
178
+ const host = req.headers.host;
179
+ const bounded = (durationMs: number, run: () => Promise<unknown> | unknown) =>
180
+ runWithHttpDeadline({ req, res, durationMs, run, onLateError: (error) => onError(`${pathname}:late`, error) });
181
+ const invoke = (
182
+ loader: EntrypointLoader,
183
+ durationMs: number,
184
+ rawStream?: boolean,
185
+ query?: Record<string, string>,
186
+ ) => bounded(durationMs, async () => {
187
+ const [mod, vreq] = await Promise.all([loader(), adaptRequest(req, pathname, rawStream)]);
188
+ if (query) vreq.query = { ...vreq.query, ...query };
189
+ await mod.default(vreq, res);
190
+ });
191
+ try {
192
+ applySecurityHeaders(res, pathname, host, options.security);
193
+ if (pathname.length > 1 && pathname.endsWith("/")) {
194
+ res.statusCode = 308;
195
+ res.setHeader("location", `${pathname.replace(/\/+$/, "")}${url.search}`);
196
+ return finalize(res);
197
+ }
198
+ // 1. Standalone entrypoints (filesystem-parity with api/*.ts).
199
+ const cspReport = options.entrypoints?.cspReport;
200
+ if (pathname === "/api/csp-report" && cspReport) {
201
+ await invoke(cspReport, options.durations?.cspReport ?? 30_000);
202
+ return finalize(res);
203
+ }
204
+ // 1b. Deployment entrypoints: exact path, lazy load, owner-declared body policy.
205
+ const registered = extra.get(pathname);
206
+ if (registered) {
207
+ await invoke(registered.load, registered.maxDurationMs ?? 30_000, registered.rawStream);
208
+ return finalize(res);
209
+ }
210
+ const bffRouter = options.entrypoints?.bffRouter;
211
+ if (pathname === "/api/bff-router" && bffRouter) {
212
+ await invoke(bffRouter, options.durations?.bffRouter ?? 30_000);
213
+ return finalize(res);
214
+ }
215
+ // 1c. Deployment path-rule entrypoints, before the BFF dispatcher.
216
+ const routed = matchPathEntrypoint(options.pathEntrypoints, pathname);
217
+ if (routed) {
218
+ await invoke(routed.entrypoint.load, routed.entrypoint.maxDurationMs ?? 10_000, false, routed.query);
219
+ return finalize(res);
220
+ }
221
+
222
+ // 2. The BFF dispatcher (same routes + dispatch as the hosted entrypoint).
223
+ if (pathname === "/api/bff" || pathname.startsWith("/api/bff/")) {
224
+ await bounded(options.durations?.bff ?? 30_000, async () =>
225
+ options.dispatch(options.routes, await adaptRequest(req, pathname), res));
226
+ return finalize(res);
227
+ }
228
+
229
+ if (pathname === "/robots.txt" && options.robotsTxt) {
230
+ res.statusCode = 200;
231
+ res.setHeader("content-type", ROBOTS_CONTENT_TYPE);
232
+ res.setHeader("cache-control", "public, max-age=0, s-maxage=300, stale-while-revalidate=3600");
233
+ res.end(options.robotsTxt(host));
234
+ return;
235
+ }
236
+
237
+ // 3. Scheduled routes share the scheduler's single-source allowlist.
238
+ if (pathname.startsWith(SCHEDULED_PREFIX)) {
239
+ const jobId = pathname.slice(SCHEDULED_PREFIX.length);
240
+ const loader = options.scheduledEntrypoint?.(jobId);
241
+ if (loader) {
242
+ await invoke(loader, options.durations?.scheduled?.(jobId) ?? 60_000);
243
+ return finalize(res);
244
+ }
245
+ }
246
+
247
+ // 4. Unknown /api/* => 404 JSON (never fall through to the SPA).
248
+ if (pathname.startsWith("/api/")) {
249
+ res.statusCode = 404;
250
+ res.setHeader("content-type", "application/json; charset=utf-8");
251
+ res.end(JSON.stringify({ ok: false, error: { code: "NOT_FOUND", message: "Not found" } }));
252
+ return;
253
+ }
254
+
255
+ const legacyLocation = legacyRedirect(pathname, routing.legacyRedirects);
256
+ if (legacyLocation) {
257
+ res.statusCode = 308;
258
+ res.setHeader("location", `${legacyLocation}${url.search}`);
259
+ return finalize(res);
260
+ }
261
+ const fallback = matchPathEntrypoint(options.fallbackPathEntrypoints, pathname);
262
+ if (fallback) {
263
+ await invoke(fallback.entrypoint.load, fallback.entrypoint.maxDurationMs ?? 10_000, false, fallback.query);
264
+ return finalize(res);
265
+ }
266
+
267
+ // 5. Static documents and declared CSR fallback. Unknown extensionless routes are 404.
268
+ if (staticReader) {
269
+ await serveSpaStatic(
270
+ req,
271
+ res,
272
+ pathname,
273
+ staticReader,
274
+ ssgPaths.has(pathname),
275
+ isCsrFallback(pathname, routing.csrFallback),
276
+ );
277
+ return;
278
+ }
279
+ // No static reader configured (pure-API smoke): respond 404 rather than hang.
280
+ res.statusCode = 404;
281
+ res.end("Not Found");
282
+ } catch (error) {
283
+ if (error instanceof NodeRequestAbortedError) return;
284
+ onError(pathname, error);
285
+ const tooLarge = error instanceof NodeRequestBodyTooLargeError;
286
+ if (tooLarge) closeInputAfterResponse(req, res);
287
+ if (!res.headersSent && !res.destroyed) {
288
+ res.statusCode = tooLarge ? 413 : 500;
289
+ res.setHeader("content-type", "application/json; charset=utf-8");
290
+ }
291
+ if (!res.writableEnded && !res.destroyed) {
292
+ res.end(JSON.stringify({ ok: false, error: {
293
+ code: tooLarge ? "REQUEST_BODY_TOO_LARGE" : "NODE_HTTP_ERROR",
294
+ message: tooLarge ? "Request body too large" : "Internal server error",
295
+ } }));
296
+ }
297
+ }
298
+ };
299
+ }
300
+
301
+ /** Create the Node HttpRuntimePort: `handle` for one request, `listen` to bind the server. */
302
+ export function createNodeHttpRuntime(options: NodeHttpRuntimeOptions): HttpRuntimePort & {
303
+ server: Server | null;
304
+ } {
305
+ const handle = createRequestHandler(options);
306
+ let server: Server | null = null;
307
+
308
+ return {
309
+ server,
310
+ handle,
311
+ async listen({ port, host }): Promise<void> {
312
+ server = createServer((req, res) => {
313
+ void handle(decorateRequest(req), decorateResponse(res));
314
+ });
315
+ this.server = server;
316
+ await new Promise<void>((resolve) => server!.listen(port, host ?? "0.0.0.0", resolve));
317
+ },
318
+ };
319
+ }
@@ -0,0 +1,155 @@
1
+ // Security headers + SPA static middleware for the Node HttpRuntimePort.
2
+ //
3
+ // Replicates, as Node middleware, a hosted response surface whose values the deployment owns:
4
+ // the global headers (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
5
+ // Permissions-Policy, Reporting-Endpoints, the content security policy), the path-specific
6
+ // Cache-Control and X-Robots-Tag rules, and which hosts are production. The caller supplies all of
7
+ // them through `NodeSecurityHeaderPolicy`; this module only applies them in a fixed order.
8
+ //
9
+ // SPA fallback boundary: the runtime handles its API and entrypoint branches before static
10
+ // serving, so those paths never fall through to a document.
11
+
12
+ import { extname } from "node:path";
13
+ import type { IncomingMessage, ServerResponse } from "node:http";
14
+
15
+ import type { HttpResponse } from "./types.js";
16
+
17
+ /** Deployment-owned header policy applied to every response of the Node runtime. */
18
+ export interface NodeSecurityHeaderPolicy {
19
+ /** Headers set on every response unless the handler already set them, in this order. */
20
+ readonly globalHeaders: ReadonlyArray<readonly [string, string]>;
21
+ /** Static-asset path prefixes that receive the long-lived asset Cache-Control. */
22
+ readonly assetCachePrefixes: readonly string[];
23
+ /** Path families (the path or anything below it) that receive `X-Robots-Tag: noindex, nofollow`. */
24
+ readonly noindexFamilies: readonly string[];
25
+ /** Exact paths that receive `X-Robots-Tag: noindex, nofollow`. */
26
+ readonly noindexExacts: readonly string[];
27
+ /** True for a production host; every other host receives the strict non-production X-Robots-Tag. */
28
+ readonly isProductionHost: (host: string | undefined) => boolean;
29
+ }
30
+
31
+ const ASSET_CACHE_CONTROL =
32
+ "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800";
33
+
34
+ function matchesNoindex(policy: NodeSecurityHeaderPolicy, pathname: string): boolean {
35
+ if (policy.noindexExacts.includes(pathname)) return true;
36
+ return policy.noindexFamilies.some((path) => pathname === path || pathname.startsWith(`${path}/`));
37
+ }
38
+
39
+ /** Apply the deployment's global and path-specific headers for this route. */
40
+ export function applySecurityHeaders(
41
+ res: ServerResponse,
42
+ pathname: string,
43
+ host: string | undefined,
44
+ policy: NodeSecurityHeaderPolicy,
45
+ ): void {
46
+ for (const [key, value] of policy.globalHeaders) {
47
+ if (!res.hasHeader(key)) res.setHeader(key, value);
48
+ }
49
+ if (policy.assetCachePrefixes.some((p) => pathname.startsWith(p))) {
50
+ res.setHeader("Cache-Control", ASSET_CACHE_CONTROL);
51
+ }
52
+ if (pathname === "/.well-known/security.txt") {
53
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
54
+ }
55
+ if (pathname === "/ssg-manifest.json" || pathname === "/spa-fallback.html") {
56
+ res.setHeader("Cache-Control", "public, max-age=0, s-maxage=0, must-revalidate");
57
+ }
58
+ if (matchesNoindex(policy, pathname)) {
59
+ res.setHeader("X-Robots-Tag", "noindex, nofollow");
60
+ }
61
+ if (!policy.isProductionHost(host)) {
62
+ res.setHeader("X-Robots-Tag", "noindex, nofollow, noarchive, nosnippet, noimageindex");
63
+ }
64
+ }
65
+
66
+ /** Pluggable static reader so tests can inject an in-memory dist/ without touching the filesystem. */
67
+ export interface StaticAssetReader {
68
+ /** Read a built asset (returns null if it does not exist). */
69
+ read(relativePath: string): Promise<{ body: Buffer; contentType: string } | null>;
70
+ /** The SPA fallback document (dist/index.html). */
71
+ readIndex(): Promise<{ body: Buffer; contentType: string }>;
72
+ }
73
+
74
+ const CONTENT_TYPES: Record<string, string> = {
75
+ ".html": "text/html; charset=utf-8",
76
+ ".js": "text/javascript; charset=utf-8",
77
+ ".mjs": "text/javascript; charset=utf-8",
78
+ ".css": "text/css; charset=utf-8",
79
+ ".json": "application/json; charset=utf-8",
80
+ ".svg": "image/svg+xml",
81
+ ".png": "image/png",
82
+ ".jpg": "image/jpeg",
83
+ ".jpeg": "image/jpeg",
84
+ ".webp": "image/webp",
85
+ ".ico": "image/x-icon",
86
+ ".woff": "font/woff",
87
+ ".woff2": "font/woff2",
88
+ ".txt": "text/plain; charset=utf-8",
89
+ ".webmanifest": "application/manifest+json",
90
+ };
91
+
92
+ export function contentTypeFor(pathname: string): string {
93
+ return CONTENT_TYPES[extname(pathname).toLowerCase()] ?? "application/octet-stream";
94
+ }
95
+
96
+ /** A path "looks like" a built asset if it has a file extension (so we 404 missing assets, not SPA). */
97
+ export function looksLikeAsset(pathname: string): boolean {
98
+ return extname(pathname) !== "";
99
+ }
100
+
101
+ /**
102
+ * Serve a built SPA asset if present, otherwise the document the build produced for this route,
103
+ * otherwise the declared CSR fallback for client routes. Missing real assets (a hashed bundle that
104
+ * does not exist) and unknown extensionless paths return 404 rather than the SPA shell.
105
+ *
106
+ * The middle step is not decoration. The build writes each pre-rendered route as
107
+ * `<route>/index.html` and writes the HOME document to `index.html`; skipping that lookup hands
108
+ * every pre-rendered URL the home document instead of its own. The hosted platform does the same
109
+ * thing declaratively, one rewrite per route, which is why this host had no equivalent code.
110
+ */
111
+ export async function serveSpaStatic(
112
+ _req: IncomingMessage,
113
+ res: HttpResponse,
114
+ pathname: string,
115
+ reader: StaticAssetReader,
116
+ allowRouteDocument = false,
117
+ allowFallback = false,
118
+ ): Promise<void> {
119
+ const relative = pathname === "/" ? "/index.html" : pathname;
120
+ const asset = await reader.read(relative);
121
+ if (asset) {
122
+ if (!res.hasHeader("content-type")) res.setHeader("content-type", asset.contentType);
123
+ res.statusCode = 200;
124
+ res.end(asset.body);
125
+ return;
126
+ }
127
+ if (looksLikeAsset(pathname)) {
128
+ // A concrete asset request that does not exist -> genuine 404 (not the SPA shell).
129
+ res.statusCode = 404;
130
+ res.setHeader("content-type", "text/plain; charset=utf-8");
131
+ res.end("Not Found");
132
+ return;
133
+ }
134
+ // A manifest-declared SSG route -> its own pre-rendered document.
135
+ if (allowRouteDocument) {
136
+ const document = await reader.read(`${pathname.replace(/\/+$/, "")}/index.html`);
137
+ if (document) {
138
+ if (!res.hasHeader("content-type")) res.setHeader("content-type", document.contentType);
139
+ res.statusCode = 200;
140
+ res.end(document.body);
141
+ return;
142
+ }
143
+ }
144
+ if (!allowFallback) {
145
+ res.statusCode = 404;
146
+ res.setHeader("content-type", "text/plain; charset=utf-8");
147
+ res.end("Not Found");
148
+ return;
149
+ }
150
+ // Declared client route -> SPA fallback.
151
+ const index = await reader.readIndex();
152
+ res.statusCode = 200;
153
+ if (!res.hasHeader("content-type")) res.setHeader("content-type", index.contentType);
154
+ res.end(index.body);
155
+ }
@@ -0,0 +1,42 @@
1
+ // Filesystem reader for a built static site, used by the Node runtime's static branch.
2
+
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { join, normalize, resolve } from "node:path";
5
+
6
+ import { contentTypeFor, type StaticAssetReader } from "./security.js";
7
+
8
+ const SPA_FALLBACK_DOCUMENT = "spa-fallback.html";
9
+
10
+ /** Read files below `distDir` (path-traversal safe; null for a missing file or a directory). */
11
+ export function createDistStaticReader(distDir: string): StaticAssetReader {
12
+ const root = resolve(distDir);
13
+ const safe = (relative: string): string | null => {
14
+ const full = normalize(join(root, relative));
15
+ return full.startsWith(root) ? full : null;
16
+ };
17
+ return {
18
+ async read(relative) {
19
+ const full = safe(relative);
20
+ if (!full) return null;
21
+ try {
22
+ const info = await stat(full);
23
+ if (!info.isFile()) return null;
24
+ const body = await readFile(full);
25
+ return { body, contentType: contentTypeFor(relative) };
26
+ } catch {
27
+ return null;
28
+ }
29
+ },
30
+ async readIndex() {
31
+ // Prefer the client fallback document; a build without a pre-render step has only
32
+ // index.html, which then serves as the fallback.
33
+ try {
34
+ const body = await readFile(join(root, SPA_FALLBACK_DOCUMENT));
35
+ return { body, contentType: "text/html; charset=utf-8" };
36
+ } catch {
37
+ const body = await readFile(join(root, "index.html"));
38
+ return { body, contentType: "text/html; charset=utf-8" };
39
+ }
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,31 @@
1
+ // Host-neutral request and response shapes for every HTTP entrypoint.
2
+ //
3
+ // They are defined over node:http so that a handler runs unchanged on a hosted
4
+ // serverless function, the Node runtime of this package, or a test double.
5
+ // No host SDK is required to name them.
6
+
7
+ import type { IncomingMessage, ServerResponse } from "node:http";
8
+
9
+ /** A Node request decorated with the parsed query, cookies and body. */
10
+ export interface HttpRequest extends IncomingMessage {
11
+ query: Record<string, string | string[] | undefined>;
12
+ cookies?: Record<string, string>;
13
+ body?: unknown;
14
+ /** Cooperative cancellation owned by the portable host adapter. */
15
+ abortSignal?: AbortSignal;
16
+ }
17
+
18
+ /** A Node response decorated with the helper methods hosted functions provide. */
19
+ export interface HttpResponse extends ServerResponse {
20
+ status(code: number): this;
21
+ json(body: unknown): this;
22
+ send(body: string | Buffer | object): this;
23
+ redirect(url: string): this;
24
+ redirect(status: number, url: string): this;
25
+ }
26
+
27
+ /** A default-export handler shared by every physical API entrypoint. */
28
+ export type HttpHandler = (req: HttpRequest, res: HttpResponse) => Promise<unknown> | unknown;
29
+
30
+ /** A lazily loaded module exposing one default-export handler. */
31
+ export type EntrypointLoader = () => Promise<{ default: HttpHandler }>;
@@ -0,0 +1,76 @@
1
+ import { createOperatorTokenAdminAuth, resolvePlatformOperatorAuthConfig } from "./operatorTokenAdminAuth.js";
2
+ import { resolveBundleId } from "../kernel/index.js";
3
+ import type { AdminAuthPort } from "./ports.js";
4
+ import type { AdminAuthBinding } from "./adminPreflight.js";
5
+ type Env = Record<string, string | undefined>;
6
+ export interface PlatformOperatorChecker { isOperatorAllowed(principalId: string): Promise<boolean>; close(): Promise<void> }
7
+ export type PlatformOperatorCheckerFactory = (env: Env) => PlatformOperatorChecker | Promise<PlatformOperatorChecker>;
8
+ export type AdminAuthBindingResolution =
9
+ | { readonly binding: AdminAuthBinding; readonly error?: undefined }
10
+ | { readonly binding?: undefined; readonly error: "managed_admin_auth_env_required" | "direct_admin_auth_config_invalid" };
11
+ export interface AdminAuthBindingOptions {
12
+ humanAuthFactory?: (env: Env) => AdminAuthPort & { close(): Promise<void> };
13
+ checkerFactory?: PlatformOperatorCheckerFactory;
14
+ managedAuthFactory?: (env: { url: string; anonKey: string }, accessToken: string | null) => AdminAuthPort;
15
+ }
16
+ function managedAuthEnv(env: Env): { url: string; anonKey: string } | null {
17
+ const url = env.SUPABASE_URL ?? env.VITE_SUPABASE_URL;
18
+ const anonKey = env.VITE_SUPABASE_PUBLISHABLE_KEY ?? env.VITE_SUPABASE_ANON_KEY ?? env.SUPABASE_ANON_KEY;
19
+ return url && anonKey ? { url, anonKey } : null;
20
+ }
21
+
22
+ /** Bind the admin authorizer selected by PLATFORM_BUNDLE. */
23
+ export function createAdminAuthBindingResolver(defaults: Required<Pick<AdminAuthBindingOptions, "checkerFactory" | "managedAuthFactory">> & Pick<AdminAuthBindingOptions, "humanAuthFactory">) {
24
+ return function resolveAdminAuthBinding(
25
+ env: Env,
26
+ options: AdminAuthBindingOptions = {},
27
+ ): AdminAuthBindingResolution {
28
+ const bundleId = resolveBundleId(env);
29
+ if (bundleId === "node-postgres") {
30
+ const config = resolvePlatformOperatorAuthConfig(env);
31
+ const humanFactory = options.humanAuthFactory ?? defaults.humanAuthFactory;
32
+ const humanEnabled = !!env.OPERATOR_IDENTITY_AUDIENCE && !!humanFactory;
33
+ if (!config.config && !humanEnabled) return { error: "direct_admin_auth_config_invalid" };
34
+ const checkerFactory = options.checkerFactory ?? defaults.checkerFactory;
35
+ return {
36
+ binding: {
37
+ identity: bundleId,
38
+ async run(accessToken, work) {
39
+ const scope: { checker?: PlatformOperatorChecker } = {};
40
+ const machine = config.config ? createOperatorTokenAdminAuth(config.config, async (principalId) => {
41
+ scope.checker ??= await checkerFactory(env);
42
+ return scope.checker.isOperatorAllowed(principalId);
43
+ }) : null;
44
+ let human: (AdminAuthPort & { close(): Promise<void> }) | undefined;
45
+ const auth: AdminAuthPort = { async authorize(token, restrictions) {
46
+ // Only a credential mismatch may select human verification; machine
47
+ // revocation and database errors retain their existing result.
48
+ const result = machine ? await machine.authorize(token, restrictions) : null;
49
+ if (result && (result.ok || result.code !== "UNAUTHORIZED")) return result;
50
+ if (!humanEnabled || !token) return result ?? { ok: false, code: "UNAUTHORIZED", message: "Operator authentication required" };
51
+ human ??= humanFactory!(env);
52
+ return human.authorize(token, restrictions);
53
+ } };
54
+ try {
55
+ return await work(auth);
56
+ } finally {
57
+ try { await scope.checker?.close(); } finally { await human?.close(); }
58
+ }
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ const authEnv = managedAuthEnv(env);
65
+ if (!authEnv) return { error: "managed_admin_auth_env_required" };
66
+ const managedAuthFactory = options.managedAuthFactory ?? defaults.managedAuthFactory;
67
+ return {
68
+ binding: {
69
+ identity: bundleId,
70
+ run: (accessToken, work) => work(managedAuthFactory(authEnv, accessToken)),
71
+ },
72
+ };
73
+ }
74
+
75
+
76
+ }