@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,203 @@
1
+ // Node request/response adaptation to the hosted-function request shape for the HttpRuntimePort.
2
+ //
3
+ // Owns the RAW-BODY INVARIANT: every buffered request gets the exact bytes re-exposed as an
4
+ // async-iterable (so raw-body signature verification works for ANY route, including ones outside a
5
+ // `/webhooks/` segment) while still setting a parsed JSON req.body; stream-first standalone
6
+ // entrypoints keep their live stream and NO req.body.
7
+ export const DEFAULT_NODE_BUFFERED_BODY_LIMIT_BYTES = 1_048_576;
8
+ export class NodeRequestBodyTooLargeError extends Error {
9
+ constructor() {
10
+ super("node_request_body_too_large");
11
+ }
12
+ }
13
+ export class NodeRequestAbortedError extends Error {
14
+ constructor() {
15
+ super("node_request_aborted");
16
+ }
17
+ }
18
+ /** A path is a webhook iff it contains a `/webhooks/` segment. */
19
+ export function isWebhookPath(pathname) {
20
+ return pathname.includes("/webhooks/");
21
+ }
22
+ /**
23
+ * The PLATFORM's stream-first standalone entrypoints: they read the raw stream
24
+ * themselves, so the runtime must never pre-parse their body.
25
+ *
26
+ * A deployment's own stream-first endpoint is NOT listed here. It declares
27
+ * `rawStream: true` on its `HttpEntrypointRegistration` and the runtime passes that
28
+ * decision to `adaptRequest` directly — which is what stops this publishable list
29
+ * from having to name a path only that deployment owns.
30
+ */
31
+ export function isRawStreamEntrypoint(pathname) {
32
+ return pathname === "/api/csp-report";
33
+ }
34
+ export function pathnameOf(req) {
35
+ return new URL(req.url ?? "/", "http://node.local").pathname;
36
+ }
37
+ function parseQuery(req) {
38
+ const url = new URL(req.url ?? "/", "http://node.local");
39
+ const query = {};
40
+ for (const key of url.searchParams.keys()) {
41
+ if (key in query)
42
+ continue;
43
+ const all = url.searchParams.getAll(key);
44
+ query[key] = all.length > 1 ? all : all[0];
45
+ }
46
+ return query;
47
+ }
48
+ function parseCookies(req) {
49
+ const header = req.headers.cookie;
50
+ if (!header)
51
+ return {};
52
+ const out = {};
53
+ for (const part of header.split(";")) {
54
+ const idx = part.indexOf("=");
55
+ if (idx < 0)
56
+ continue;
57
+ const key = part.slice(0, idx).trim();
58
+ const value = part.slice(idx + 1).trim();
59
+ if (key)
60
+ out[key] = decodeURIComponent(value);
61
+ }
62
+ return out;
63
+ }
64
+ /** Buffer the request stream once into a single Buffer (GET/HEAD => empty). */
65
+ async function bufferBody(req, maxBytes) {
66
+ const method = (req.method ?? "GET").toUpperCase();
67
+ if (method === "GET" || method === "HEAD")
68
+ return Buffer.alloc(0);
69
+ return new Promise((resolve, reject) => {
70
+ const chunks = [];
71
+ let bytes = 0;
72
+ let settled = false;
73
+ const finish = (result) => {
74
+ if (settled)
75
+ return;
76
+ settled = true;
77
+ req.removeListener("data", onData);
78
+ req.removeListener("end", onEnd);
79
+ req.removeListener("error", onError);
80
+ req.abortSignal?.removeEventListener("abort", onAbort);
81
+ if (result instanceof Error)
82
+ reject(result);
83
+ else
84
+ resolve(result);
85
+ };
86
+ const onData = (chunk) => {
87
+ const bytesChunk = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
88
+ bytes += bytesChunk.length;
89
+ if (bytes > maxBytes) {
90
+ finish(new NodeRequestBodyTooLargeError());
91
+ return;
92
+ }
93
+ chunks.push(bytesChunk);
94
+ };
95
+ const onEnd = () => finish(Buffer.concat(chunks));
96
+ const onError = (error) => finish(error);
97
+ const onAbort = () => finish(new NodeRequestAbortedError());
98
+ if (req.abortSignal?.aborted)
99
+ return onAbort();
100
+ req.on("data", onData);
101
+ req.once("end", onEnd);
102
+ req.once("error", onError);
103
+ req.abortSignal?.addEventListener("abort", onAbort, { once: true });
104
+ });
105
+ }
106
+ /** Re-expose a fixed Buffer as the request's async-iterable so readRawBody sees the EXACT bytes. */
107
+ function reexposeRawStream(req, raw) {
108
+ req[Symbol.asyncIterator] =
109
+ async function* () {
110
+ yield raw;
111
+ };
112
+ }
113
+ /** Best-effort JSON parse of a buffered body for handlers that read `req.body`. */
114
+ function parseJsonBody(raw, contentType) {
115
+ if (raw.length === 0)
116
+ return undefined;
117
+ const text = raw.toString("utf8").trim();
118
+ if (!text)
119
+ return undefined;
120
+ if (contentType.includes("application/json") || text.startsWith("{") || text.startsWith("[")) {
121
+ try {
122
+ return JSON.parse(text);
123
+ }
124
+ catch {
125
+ return text;
126
+ }
127
+ }
128
+ return text;
129
+ }
130
+ /**
131
+ * Decorate a Node response with Vercel's status/json/send/redirect sugar (productionized from the
132
+ * dev shim's decorateResponse — identical semantics).
133
+ */
134
+ export function decorateResponse(res) {
135
+ const vres = res;
136
+ vres.status = function status(code) {
137
+ res.statusCode = code;
138
+ return vres;
139
+ };
140
+ vres.json = function json(body) {
141
+ if (!res.hasHeader("content-type")) {
142
+ res.setHeader("content-type", "application/json; charset=utf-8");
143
+ }
144
+ res.end(JSON.stringify(body));
145
+ return vres;
146
+ };
147
+ vres.send = function send(body) {
148
+ if (Buffer.isBuffer(body) || typeof body === "string") {
149
+ res.end(body);
150
+ return vres;
151
+ }
152
+ if (!res.hasHeader("content-type")) {
153
+ res.setHeader("content-type", "application/json; charset=utf-8");
154
+ }
155
+ res.end(JSON.stringify(body));
156
+ return vres;
157
+ };
158
+ vres.redirect = function redirect(statusOrUrl, maybeUrl) {
159
+ const code = typeof statusOrUrl === "number" ? statusOrUrl : 302;
160
+ const location = typeof statusOrUrl === "number" ? maybeUrl ?? "/" : statusOrUrl;
161
+ res.statusCode = code;
162
+ res.setHeader("location", location);
163
+ res.end();
164
+ return vres;
165
+ };
166
+ return vres;
167
+ }
168
+ /**
169
+ * Adapt a Node IncomingMessage into the Vercel request shape, honoring the raw-body invariant:
170
+ * - raw-stream entrypoints (process/csp/feedback): stream left intact, NO req.body;
171
+ * - everything else: raw bytes buffered + re-exposed as async-iterable (so readRawBody sees the
172
+ * EXACT bytes for any signature-verifying route, webhook-segment or not) AND req.body = parsed JSON.
173
+ */
174
+ export async function adaptRequest(req, pathname,
175
+ /** Explicit override for a caller that knows the entrypoint's body contract (see above). */
176
+ rawStream = isRawStreamEntrypoint(pathname), bufferedBodyLimitBytes = DEFAULT_NODE_BUFFERED_BODY_LIMIT_BYTES) {
177
+ const vreq = req;
178
+ vreq.query = parseQuery(req);
179
+ vreq.cookies = parseCookies(req);
180
+ if (rawStream) {
181
+ // Leave the live stream untouched; the handler reads it itself (formidable / custom readBody).
182
+ return vreq;
183
+ }
184
+ const raw = await bufferBody(vreq, bufferedBodyLimitBytes);
185
+ const contentType = String(req.headers["content-type"] ?? "");
186
+ // Re-expose the EXACT bytes as an async-iterable for EVERY buffered request, not only
187
+ // `/webhooks/` paths. Some signature-verifying routes live outside that segment — e.g.
188
+ // `/api/bff/communications/integrations/events` calls readRawBody for HMAC verification.
189
+ // readRawBody is stream-first (isAsyncIterable), so any handler that needs the raw bytes
190
+ // works; handlers that read req.body still get parsed JSON. The re-exposed iterator is
191
+ // simply never consumed for routes that don't call readRawBody, so this is a safe superset
192
+ // of the old `/webhooks/`-only behavior (and not dependent on a fragile path convention).
193
+ reexposeRawStream(req, raw);
194
+ vreq.body = parseJsonBody(raw, contentType);
195
+ return vreq;
196
+ }
197
+ /** Narrow a raw IncomingMessage to HttpRequest at the server boundary (query/cookies set in adapt). */
198
+ export function decorateRequest(req) {
199
+ const vreq = req;
200
+ if (!vreq.query)
201
+ vreq.query = {};
202
+ return vreq;
203
+ }
@@ -0,0 +1,83 @@
1
+ import { type Server } from "node:http";
2
+ import type { HttpRuntimePort } from "@openlup/core/platform-runtime";
3
+ import type { EntrypointLoader } from "./types.js";
4
+ import type { BffDispatch, RouteEntry } from "../bff/index.js";
5
+ import { type NodeSecurityHeaderPolicy, type StaticAssetReader } from "./security.js";
6
+ /** One deployment-specific standalone entrypoint, matched by its exact path. */
7
+ export interface HttpEntrypointRegistration {
8
+ path: string;
9
+ rawStream?: boolean;
10
+ maxDurationMs?: number;
11
+ load: EntrypointLoader;
12
+ }
13
+ /**
14
+ * One deployment-specific entrypoint matched by a path rule. `match` returns the query values the
15
+ * entrypoint receives for this path, or null when the path is not its own. The body is buffered.
16
+ */
17
+ export interface HttpPathEntrypoint {
18
+ match(pathname: string): Record<string, string> | null;
19
+ load: EntrypointLoader;
20
+ /** Request deadline; defaults to 10 seconds. */
21
+ maxDurationMs?: number;
22
+ }
23
+ export interface NodeLegacyRedirect {
24
+ source: string;
25
+ destination: string;
26
+ permanent: boolean;
27
+ }
28
+ export interface NodeCsrFallback {
29
+ exactPaths: readonly string[];
30
+ routeFamilies: readonly string[];
31
+ }
32
+ export interface NodeHttpRuntimeOptions {
33
+ /** The deployment's BFF route table. */
34
+ routes: readonly RouteEntry[];
35
+ /** The dispatcher that matches `routes` (see `createBffDispatcher`). */
36
+ dispatch: BffDispatch;
37
+ /** Validated public-route contract supplied by the host composition root. */
38
+ routing?: {
39
+ legacyRedirects: readonly NodeLegacyRedirect[];
40
+ ssgPaths: readonly string[];
41
+ csrFallback: NodeCsrFallback;
42
+ };
43
+ /** Deployment header policy, including the content security policy. */
44
+ security: NodeSecurityHeaderPolicy;
45
+ /** robots.txt body for a request host; without it `/robots.txt` is served like any other path. */
46
+ robotsTxt?: (host: string | undefined) => string;
47
+ /**
48
+ * Platform-path standalone handlers: the CSP report sink at `/api/csp-report` and the hosted
49
+ * BFF router at `/api/bff-router`. A path without a handler is treated like any other path.
50
+ */
51
+ entrypoints?: {
52
+ cspReport?: EntrypointLoader;
53
+ bffRouter?: EntrypointLoader;
54
+ };
55
+ /**
56
+ * Deployment exact-path handlers, checked after the CSP report entrypoint and before the BFF
57
+ * router; a later registration replaces an earlier one with the same path.
58
+ */
59
+ extraEntrypoints?: readonly HttpEntrypointRegistration[];
60
+ /** Deployment path-rule handlers checked before the BFF dispatcher, first match wins. */
61
+ pathEntrypoints?: readonly HttpPathEntrypoint[];
62
+ /** Deployment path-rule handlers checked after legacy redirects and before static documents. */
63
+ fallbackPathEntrypoints?: readonly HttpPathEntrypoint[];
64
+ /** Registry-derived allowlist; undeclared job ids must resolve null. */
65
+ scheduledEntrypoint?: (jobId: string) => EntrypointLoader | null;
66
+ /** Explicit host deadlines; missing cells use the bounded defaults below. */
67
+ durations?: {
68
+ bff?: number;
69
+ cspReport?: number;
70
+ bffRouter?: number;
71
+ scheduled?: (jobId: string) => number;
72
+ };
73
+ /** Static SPA reader; without one, unmatched non-API paths answer 404. */
74
+ staticReader?: StaticAssetReader;
75
+ /** Structured error sink (defaults to console.error). */
76
+ onError?: (context: string, error: unknown) => void;
77
+ }
78
+ /** Build the runtime's request handler (exported for unit tests; wraps every dispatch path). */
79
+ export declare function createRequestHandler(options: NodeHttpRuntimeOptions): HttpRuntimePort["handle"];
80
+ /** Create the Node HttpRuntimePort: `handle` for one request, `listen` to bind the server. */
81
+ export declare function createNodeHttpRuntime(options: NodeHttpRuntimeOptions): HttpRuntimePort & {
82
+ server: Server | null;
83
+ };
@@ -0,0 +1,199 @@
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
+ import { createServer } from "node:http";
9
+ import { adaptRequest, decorateRequest, decorateResponse, NodeRequestAbortedError, NodeRequestBodyTooLargeError, } from "./requestAdapter.js";
10
+ import { closeInputAfterResponse, runWithHttpDeadline } from "./deadline.js";
11
+ import { applySecurityHeaders, serveSpaStatic, } from "./security.js";
12
+ const SCHEDULED_PREFIX = "/api/cron/";
13
+ const ROBOTS_CONTENT_TYPE = "text/plain; charset=utf-8";
14
+ const EMPTY_ROUTING = { legacyRedirects: [], ssgPaths: [], csrFallback: { exactPaths: [], routeFamilies: [] } };
15
+ function requestUrl(req) {
16
+ return new URL(req.url ?? "/", "http://node.local");
17
+ }
18
+ function capturesFor(source, pathname) {
19
+ const sourceParts = source.split("/").slice(1);
20
+ const pathParts = pathname.split("/").slice(1);
21
+ const captures = {};
22
+ let cursor = 0;
23
+ for (const sourcePart of sourceParts) {
24
+ if (!sourcePart.startsWith(":")) {
25
+ if (pathParts[cursor++] !== sourcePart)
26
+ return null;
27
+ continue;
28
+ }
29
+ const name = sourcePart.slice(1).replace(/\*$/, "");
30
+ if (sourcePart.endsWith("*")) {
31
+ captures[name] = pathParts.slice(cursor).join("/");
32
+ cursor = pathParts.length;
33
+ break;
34
+ }
35
+ const value = pathParts[cursor++];
36
+ if (!value)
37
+ return null;
38
+ captures[name] = value;
39
+ }
40
+ return cursor === pathParts.length ? captures : null;
41
+ }
42
+ function legacyRedirect(pathname, redirects) {
43
+ for (const redirect of redirects) {
44
+ const captures = capturesFor(redirect.source, pathname);
45
+ if (!captures)
46
+ continue;
47
+ return redirect.destination.replace(/:([A-Za-z][A-Za-z0-9_]*)\*?/g, (_match, name) => captures[name] ?? "");
48
+ }
49
+ return null;
50
+ }
51
+ function isCsrFallback(pathname, csrFallback) {
52
+ return csrFallback.exactPaths.includes(pathname)
53
+ || csrFallback.routeFamilies.some((path) => pathname === path || pathname.startsWith(`${path}/`));
54
+ }
55
+ function finalize(res) {
56
+ if (!res.writableEnded && !res.destroyed)
57
+ res.end();
58
+ }
59
+ function matchPathEntrypoint(entrypoints, pathname) {
60
+ for (const entrypoint of entrypoints ?? []) {
61
+ const query = entrypoint.match(pathname);
62
+ if (query)
63
+ return { entrypoint, query };
64
+ }
65
+ return null;
66
+ }
67
+ /** Build the runtime's request handler (exported for unit tests; wraps every dispatch path). */
68
+ export function createRequestHandler(options) {
69
+ const onError = options.onError ?? ((ctx, err) => console.error(`[node-http] ${ctx}`, err));
70
+ const staticReader = options.staticReader;
71
+ const routing = options.routing ?? EMPTY_ROUTING;
72
+ const ssgPaths = new Set(routing.ssgPaths);
73
+ // Built once; a deployment can deliberately replace an inherited path.
74
+ const extra = new Map((options.extraEntrypoints ?? []).map((entry) => [entry.path, entry]));
75
+ return async function handle(req, res) {
76
+ const url = requestUrl(req);
77
+ const pathname = url.pathname;
78
+ const host = req.headers.host;
79
+ const bounded = (durationMs, run) => runWithHttpDeadline({ req, res, durationMs, run, onLateError: (error) => onError(`${pathname}:late`, error) });
80
+ const invoke = (loader, durationMs, rawStream, query) => bounded(durationMs, async () => {
81
+ const [mod, vreq] = await Promise.all([loader(), adaptRequest(req, pathname, rawStream)]);
82
+ if (query)
83
+ vreq.query = { ...vreq.query, ...query };
84
+ await mod.default(vreq, res);
85
+ });
86
+ try {
87
+ applySecurityHeaders(res, pathname, host, options.security);
88
+ if (pathname.length > 1 && pathname.endsWith("/")) {
89
+ res.statusCode = 308;
90
+ res.setHeader("location", `${pathname.replace(/\/+$/, "")}${url.search}`);
91
+ return finalize(res);
92
+ }
93
+ // 1. Standalone entrypoints (filesystem-parity with api/*.ts).
94
+ const cspReport = options.entrypoints?.cspReport;
95
+ if (pathname === "/api/csp-report" && cspReport) {
96
+ await invoke(cspReport, options.durations?.cspReport ?? 30_000);
97
+ return finalize(res);
98
+ }
99
+ // 1b. Deployment entrypoints: exact path, lazy load, owner-declared body policy.
100
+ const registered = extra.get(pathname);
101
+ if (registered) {
102
+ await invoke(registered.load, registered.maxDurationMs ?? 30_000, registered.rawStream);
103
+ return finalize(res);
104
+ }
105
+ const bffRouter = options.entrypoints?.bffRouter;
106
+ if (pathname === "/api/bff-router" && bffRouter) {
107
+ await invoke(bffRouter, options.durations?.bffRouter ?? 30_000);
108
+ return finalize(res);
109
+ }
110
+ // 1c. Deployment path-rule entrypoints, before the BFF dispatcher.
111
+ const routed = matchPathEntrypoint(options.pathEntrypoints, pathname);
112
+ if (routed) {
113
+ await invoke(routed.entrypoint.load, routed.entrypoint.maxDurationMs ?? 10_000, false, routed.query);
114
+ return finalize(res);
115
+ }
116
+ // 2. The BFF dispatcher (same routes + dispatch as the hosted entrypoint).
117
+ if (pathname === "/api/bff" || pathname.startsWith("/api/bff/")) {
118
+ await bounded(options.durations?.bff ?? 30_000, async () => options.dispatch(options.routes, await adaptRequest(req, pathname), res));
119
+ return finalize(res);
120
+ }
121
+ if (pathname === "/robots.txt" && options.robotsTxt) {
122
+ res.statusCode = 200;
123
+ res.setHeader("content-type", ROBOTS_CONTENT_TYPE);
124
+ res.setHeader("cache-control", "public, max-age=0, s-maxage=300, stale-while-revalidate=3600");
125
+ res.end(options.robotsTxt(host));
126
+ return;
127
+ }
128
+ // 3. Scheduled routes share the scheduler's single-source allowlist.
129
+ if (pathname.startsWith(SCHEDULED_PREFIX)) {
130
+ const jobId = pathname.slice(SCHEDULED_PREFIX.length);
131
+ const loader = options.scheduledEntrypoint?.(jobId);
132
+ if (loader) {
133
+ await invoke(loader, options.durations?.scheduled?.(jobId) ?? 60_000);
134
+ return finalize(res);
135
+ }
136
+ }
137
+ // 4. Unknown /api/* => 404 JSON (never fall through to the SPA).
138
+ if (pathname.startsWith("/api/")) {
139
+ res.statusCode = 404;
140
+ res.setHeader("content-type", "application/json; charset=utf-8");
141
+ res.end(JSON.stringify({ ok: false, error: { code: "NOT_FOUND", message: "Not found" } }));
142
+ return;
143
+ }
144
+ const legacyLocation = legacyRedirect(pathname, routing.legacyRedirects);
145
+ if (legacyLocation) {
146
+ res.statusCode = 308;
147
+ res.setHeader("location", `${legacyLocation}${url.search}`);
148
+ return finalize(res);
149
+ }
150
+ const fallback = matchPathEntrypoint(options.fallbackPathEntrypoints, pathname);
151
+ if (fallback) {
152
+ await invoke(fallback.entrypoint.load, fallback.entrypoint.maxDurationMs ?? 10_000, false, fallback.query);
153
+ return finalize(res);
154
+ }
155
+ // 5. Static documents and declared CSR fallback. Unknown extensionless routes are 404.
156
+ if (staticReader) {
157
+ await serveSpaStatic(req, res, pathname, staticReader, ssgPaths.has(pathname), isCsrFallback(pathname, routing.csrFallback));
158
+ return;
159
+ }
160
+ // No static reader configured (pure-API smoke): respond 404 rather than hang.
161
+ res.statusCode = 404;
162
+ res.end("Not Found");
163
+ }
164
+ catch (error) {
165
+ if (error instanceof NodeRequestAbortedError)
166
+ return;
167
+ onError(pathname, error);
168
+ const tooLarge = error instanceof NodeRequestBodyTooLargeError;
169
+ if (tooLarge)
170
+ closeInputAfterResponse(req, res);
171
+ if (!res.headersSent && !res.destroyed) {
172
+ res.statusCode = tooLarge ? 413 : 500;
173
+ res.setHeader("content-type", "application/json; charset=utf-8");
174
+ }
175
+ if (!res.writableEnded && !res.destroyed) {
176
+ res.end(JSON.stringify({ ok: false, error: {
177
+ code: tooLarge ? "REQUEST_BODY_TOO_LARGE" : "NODE_HTTP_ERROR",
178
+ message: tooLarge ? "Request body too large" : "Internal server error",
179
+ } }));
180
+ }
181
+ }
182
+ };
183
+ }
184
+ /** Create the Node HttpRuntimePort: `handle` for one request, `listen` to bind the server. */
185
+ export function createNodeHttpRuntime(options) {
186
+ const handle = createRequestHandler(options);
187
+ let server = null;
188
+ return {
189
+ server,
190
+ handle,
191
+ async listen({ port, host }) {
192
+ server = createServer((req, res) => {
193
+ void handle(decorateRequest(req), decorateResponse(res));
194
+ });
195
+ this.server = server;
196
+ await new Promise((resolve) => server.listen(port, host ?? "0.0.0.0", resolve));
197
+ },
198
+ };
199
+ }
@@ -0,0 +1,44 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { HttpResponse } from "./types.js";
3
+ /** Deployment-owned header policy applied to every response of the Node runtime. */
4
+ export interface NodeSecurityHeaderPolicy {
5
+ /** Headers set on every response unless the handler already set them, in this order. */
6
+ readonly globalHeaders: ReadonlyArray<readonly [string, string]>;
7
+ /** Static-asset path prefixes that receive the long-lived asset Cache-Control. */
8
+ readonly assetCachePrefixes: readonly string[];
9
+ /** Path families (the path or anything below it) that receive `X-Robots-Tag: noindex, nofollow`. */
10
+ readonly noindexFamilies: readonly string[];
11
+ /** Exact paths that receive `X-Robots-Tag: noindex, nofollow`. */
12
+ readonly noindexExacts: readonly string[];
13
+ /** True for a production host; every other host receives the strict non-production X-Robots-Tag. */
14
+ readonly isProductionHost: (host: string | undefined) => boolean;
15
+ }
16
+ /** Apply the deployment's global and path-specific headers for this route. */
17
+ export declare function applySecurityHeaders(res: ServerResponse, pathname: string, host: string | undefined, policy: NodeSecurityHeaderPolicy): void;
18
+ /** Pluggable static reader so tests can inject an in-memory dist/ without touching the filesystem. */
19
+ export interface StaticAssetReader {
20
+ /** Read a built asset (returns null if it does not exist). */
21
+ read(relativePath: string): Promise<{
22
+ body: Buffer;
23
+ contentType: string;
24
+ } | null>;
25
+ /** The SPA fallback document (dist/index.html). */
26
+ readIndex(): Promise<{
27
+ body: Buffer;
28
+ contentType: string;
29
+ }>;
30
+ }
31
+ export declare function contentTypeFor(pathname: string): string;
32
+ /** A path "looks like" a built asset if it has a file extension (so we 404 missing assets, not SPA). */
33
+ export declare function looksLikeAsset(pathname: string): boolean;
34
+ /**
35
+ * Serve a built SPA asset if present, otherwise the document the build produced for this route,
36
+ * otherwise the declared CSR fallback for client routes. Missing real assets (a hashed bundle that
37
+ * does not exist) and unknown extensionless paths return 404 rather than the SPA shell.
38
+ *
39
+ * The middle step is not decoration. The build writes each pre-rendered route as
40
+ * `<route>/index.html` and writes the HOME document to `index.html`; skipping that lookup hands
41
+ * every pre-rendered URL the home document instead of its own. The hosted platform does the same
42
+ * thing declaratively, one rewrite per route, which is why this host had no equivalent code.
43
+ */
44
+ export declare function serveSpaStatic(_req: IncomingMessage, res: HttpResponse, pathname: string, reader: StaticAssetReader, allowRouteDocument?: boolean, allowFallback?: boolean): Promise<void>;
@@ -0,0 +1,114 @@
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
+ import { extname } from "node:path";
12
+ const ASSET_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800";
13
+ function matchesNoindex(policy, pathname) {
14
+ if (policy.noindexExacts.includes(pathname))
15
+ return true;
16
+ return policy.noindexFamilies.some((path) => pathname === path || pathname.startsWith(`${path}/`));
17
+ }
18
+ /** Apply the deployment's global and path-specific headers for this route. */
19
+ export function applySecurityHeaders(res, pathname, host, policy) {
20
+ for (const [key, value] of policy.globalHeaders) {
21
+ if (!res.hasHeader(key))
22
+ res.setHeader(key, value);
23
+ }
24
+ if (policy.assetCachePrefixes.some((p) => pathname.startsWith(p))) {
25
+ res.setHeader("Cache-Control", ASSET_CACHE_CONTROL);
26
+ }
27
+ if (pathname === "/.well-known/security.txt") {
28
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
29
+ }
30
+ if (pathname === "/ssg-manifest.json" || pathname === "/spa-fallback.html") {
31
+ res.setHeader("Cache-Control", "public, max-age=0, s-maxage=0, must-revalidate");
32
+ }
33
+ if (matchesNoindex(policy, pathname)) {
34
+ res.setHeader("X-Robots-Tag", "noindex, nofollow");
35
+ }
36
+ if (!policy.isProductionHost(host)) {
37
+ res.setHeader("X-Robots-Tag", "noindex, nofollow, noarchive, nosnippet, noimageindex");
38
+ }
39
+ }
40
+ const CONTENT_TYPES = {
41
+ ".html": "text/html; charset=utf-8",
42
+ ".js": "text/javascript; charset=utf-8",
43
+ ".mjs": "text/javascript; charset=utf-8",
44
+ ".css": "text/css; charset=utf-8",
45
+ ".json": "application/json; charset=utf-8",
46
+ ".svg": "image/svg+xml",
47
+ ".png": "image/png",
48
+ ".jpg": "image/jpeg",
49
+ ".jpeg": "image/jpeg",
50
+ ".webp": "image/webp",
51
+ ".ico": "image/x-icon",
52
+ ".woff": "font/woff",
53
+ ".woff2": "font/woff2",
54
+ ".txt": "text/plain; charset=utf-8",
55
+ ".webmanifest": "application/manifest+json",
56
+ };
57
+ export function contentTypeFor(pathname) {
58
+ return CONTENT_TYPES[extname(pathname).toLowerCase()] ?? "application/octet-stream";
59
+ }
60
+ /** A path "looks like" a built asset if it has a file extension (so we 404 missing assets, not SPA). */
61
+ export function looksLikeAsset(pathname) {
62
+ return extname(pathname) !== "";
63
+ }
64
+ /**
65
+ * Serve a built SPA asset if present, otherwise the document the build produced for this route,
66
+ * otherwise the declared CSR fallback for client routes. Missing real assets (a hashed bundle that
67
+ * does not exist) and unknown extensionless paths return 404 rather than the SPA shell.
68
+ *
69
+ * The middle step is not decoration. The build writes each pre-rendered route as
70
+ * `<route>/index.html` and writes the HOME document to `index.html`; skipping that lookup hands
71
+ * every pre-rendered URL the home document instead of its own. The hosted platform does the same
72
+ * thing declaratively, one rewrite per route, which is why this host had no equivalent code.
73
+ */
74
+ export async function serveSpaStatic(_req, res, pathname, reader, allowRouteDocument = false, allowFallback = false) {
75
+ const relative = pathname === "/" ? "/index.html" : pathname;
76
+ const asset = await reader.read(relative);
77
+ if (asset) {
78
+ if (!res.hasHeader("content-type"))
79
+ res.setHeader("content-type", asset.contentType);
80
+ res.statusCode = 200;
81
+ res.end(asset.body);
82
+ return;
83
+ }
84
+ if (looksLikeAsset(pathname)) {
85
+ // A concrete asset request that does not exist -> genuine 404 (not the SPA shell).
86
+ res.statusCode = 404;
87
+ res.setHeader("content-type", "text/plain; charset=utf-8");
88
+ res.end("Not Found");
89
+ return;
90
+ }
91
+ // A manifest-declared SSG route -> its own pre-rendered document.
92
+ if (allowRouteDocument) {
93
+ const document = await reader.read(`${pathname.replace(/\/+$/, "")}/index.html`);
94
+ if (document) {
95
+ if (!res.hasHeader("content-type"))
96
+ res.setHeader("content-type", document.contentType);
97
+ res.statusCode = 200;
98
+ res.end(document.body);
99
+ return;
100
+ }
101
+ }
102
+ if (!allowFallback) {
103
+ res.statusCode = 404;
104
+ res.setHeader("content-type", "text/plain; charset=utf-8");
105
+ res.end("Not Found");
106
+ return;
107
+ }
108
+ // Declared client route -> SPA fallback.
109
+ const index = await reader.readIndex();
110
+ res.statusCode = 200;
111
+ if (!res.hasHeader("content-type"))
112
+ res.setHeader("content-type", index.contentType);
113
+ res.end(index.body);
114
+ }
@@ -0,0 +1,3 @@
1
+ import { type StaticAssetReader } from "./security.js";
2
+ /** Read files below `distDir` (path-traversal safe; null for a missing file or a directory). */
3
+ export declare function createDistStaticReader(distDir: string): StaticAssetReader;