@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,234 @@
1
+ -- Dormant direct human membership authority.
2
+ --
3
+ -- This forward introduces a provider-neutral human principal and its separate
4
+ -- current-panel membership for the direct bundle. It mirrors the managed role,
5
+ -- active/revoked state, provenance, acceptance, revocation, retained identity,
6
+ -- last-human-administrator refusal, and one-row revoke audit semantics. It does
7
+ -- not verify a login, grant browser access, seed a person, or link to the
8
+ -- existing platform_control_operators machine authority. The direct host owns
9
+ -- activation of a future human-login adapter; until then these relations are
10
+ -- deliberately dormant and default-deny for the actor roles.
11
+
12
+ CREATE TABLE public.platform_human_principals (
13
+ principal_id uuid PRIMARY KEY,
14
+ email text NOT NULL UNIQUE,
15
+ created_at timestamptz NOT NULL DEFAULT now()
16
+ );
17
+
18
+ CREATE TABLE public.platform_human_memberships (
19
+ principal_id uuid PRIMARY KEY REFERENCES public.platform_human_principals(principal_id) ON DELETE RESTRICT,
20
+ role text NOT NULL CHECK (role IN ('admin', 'distributor')),
21
+ membership_state text NOT NULL CHECK (membership_state IN ('active', 'revoked')),
22
+ membership_provenance text NOT NULL CHECK (membership_provenance IN ('legacy', 'invitation')),
23
+ membership_accepted_at timestamptz NOT NULL,
24
+ membership_revoked_at timestamptz,
25
+ membership_revoked_by uuid REFERENCES public.platform_human_principals(principal_id) ON DELETE RESTRICT,
26
+ membership_revocation_reason text,
27
+ CONSTRAINT platform_human_memberships_lifecycle_check CHECK (
28
+ (membership_state = 'active'
29
+ AND membership_revoked_at IS NULL
30
+ AND membership_revoked_by IS NULL
31
+ AND membership_revocation_reason IS NULL)
32
+ OR
33
+ (membership_state = 'revoked'
34
+ AND membership_revoked_at IS NOT NULL
35
+ AND membership_revoked_by IS NOT NULL)
36
+ ),
37
+ CONSTRAINT platform_human_memberships_distinct_revoker_check
38
+ CHECK (membership_revoked_by IS NULL OR membership_revoked_by <> principal_id),
39
+ CONSTRAINT platform_human_memberships_reason_length_check
40
+ CHECK (membership_revocation_reason IS NULL OR char_length(membership_revocation_reason) <= 1000)
41
+ );
42
+
43
+ CREATE TABLE public.platform_human_membership_audit_events (
44
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
45
+ actor_principal_id uuid NOT NULL REFERENCES public.platform_human_principals(principal_id) ON DELETE RESTRICT,
46
+ target_principal_id uuid NOT NULL REFERENCES public.platform_human_principals(principal_id) ON DELETE RESTRICT,
47
+ action text NOT NULL CHECK (action = 'revoke'),
48
+ old_value jsonb NOT NULL,
49
+ new_value jsonb NOT NULL,
50
+ reason text,
51
+ occurred_at timestamptz NOT NULL DEFAULT now()
52
+ );
53
+
54
+ CREATE INDEX platform_human_membership_audit_target_idx
55
+ ON public.platform_human_membership_audit_events(target_principal_id, occurred_at DESC);
56
+
57
+ REVOKE ALL ON TABLE
58
+ public.platform_human_principals,
59
+ public.platform_human_memberships,
60
+ public.platform_human_membership_audit_events
61
+ FROM PUBLIC, anon, authenticated;
62
+
63
+ CREATE FUNCTION public.platform_human_membership_is_active(p_principal_id uuid)
64
+ RETURNS boolean
65
+ LANGUAGE sql
66
+ STABLE
67
+ SET search_path = pg_catalog
68
+ AS $$
69
+ SELECT EXISTS (
70
+ SELECT 1
71
+ FROM public.platform_human_memberships AS membership_row
72
+ WHERE membership_row.principal_id = p_principal_id
73
+ AND membership_row.membership_state = 'active'
74
+ );
75
+ $$;
76
+
77
+ CREATE FUNCTION public.platform_human_membership_is_administrator(p_principal_id uuid)
78
+ RETURNS boolean
79
+ LANGUAGE sql
80
+ STABLE
81
+ SET search_path = pg_catalog
82
+ AS $$
83
+ SELECT EXISTS (
84
+ SELECT 1
85
+ FROM public.platform_human_memberships AS membership_row
86
+ WHERE membership_row.principal_id = p_principal_id
87
+ AND membership_row.membership_state = 'active'
88
+ AND membership_row.role = 'admin'
89
+ );
90
+ $$;
91
+
92
+ CREATE FUNCTION public.platform_human_memberships_guard()
93
+ RETURNS trigger
94
+ LANGUAGE plpgsql
95
+ SET search_path = pg_catalog, public
96
+ AS $$
97
+ DECLARE
98
+ v_removes_active_human_admin boolean := false;
99
+ v_other_active_human_admins integer;
100
+ BEGIN
101
+ IF TG_OP = 'DELETE' THEN
102
+ v_removes_active_human_admin := OLD.membership_state = 'active' AND OLD.role = 'admin';
103
+ ELSE
104
+ IF (OLD.membership_state, OLD.membership_provenance, OLD.membership_accepted_at,
105
+ OLD.membership_revoked_at, OLD.membership_revoked_by,
106
+ OLD.membership_revocation_reason)
107
+ IS DISTINCT FROM
108
+ (NEW.membership_state, NEW.membership_provenance, NEW.membership_accepted_at,
109
+ NEW.membership_revoked_at, NEW.membership_revoked_by,
110
+ NEW.membership_revocation_reason)
111
+ AND current_setting('app.platform_human_membership_revoke', true) IS DISTINCT FROM 'true' THEN
112
+ RAISE EXCEPTION 'platform_human_membership_lifecycle_update_forbidden' USING ERRCODE = '42501';
113
+ END IF;
114
+
115
+ IF OLD.membership_state <> 'active' AND NEW.role IS DISTINCT FROM OLD.role THEN
116
+ RAISE EXCEPTION 'platform_human_target_membership_inactive' USING ERRCODE = 'P0001';
117
+ END IF;
118
+
119
+ v_removes_active_human_admin := OLD.membership_state = 'active'
120
+ AND OLD.role = 'admin'
121
+ AND (NEW.membership_state <> 'active' OR NEW.role <> 'admin');
122
+ END IF;
123
+
124
+ IF v_removes_active_human_admin THEN
125
+ PERFORM pg_advisory_xact_lock(hashtext('platform_human_memberships_active_admin')::bigint);
126
+ SELECT count(*)
127
+ INTO v_other_active_human_admins
128
+ FROM public.platform_human_memberships
129
+ WHERE principal_id <> OLD.principal_id
130
+ AND membership_state = 'active'
131
+ AND role = 'admin';
132
+
133
+ IF v_other_active_human_admins < 1 THEN
134
+ RAISE EXCEPTION 'platform_human_last_admin_lockout' USING ERRCODE = 'P0001';
135
+ END IF;
136
+ END IF;
137
+
138
+ IF TG_OP = 'DELETE' THEN
139
+ RAISE EXCEPTION 'platform_human_membership_delete_forbidden' USING ERRCODE = '42501';
140
+ END IF;
141
+
142
+ RETURN NEW;
143
+ END;
144
+ $$;
145
+
146
+ CREATE TRIGGER trg_platform_human_memberships_guard
147
+ BEFORE UPDATE OR DELETE ON public.platform_human_memberships
148
+ FOR EACH ROW EXECUTE FUNCTION public.platform_human_memberships_guard();
149
+
150
+ CREATE FUNCTION public.platform_revoke_human_membership(
151
+ p_actor_principal_id uuid,
152
+ p_target_principal_id uuid,
153
+ p_reason text DEFAULT NULL
154
+ )
155
+ RETURNS public.platform_human_memberships
156
+ LANGUAGE plpgsql
157
+ SET search_path = pg_catalog, public
158
+ AS $$
159
+ DECLARE
160
+ v_target public.platform_human_memberships;
161
+ v_reason text := NULLIF(btrim(p_reason), '');
162
+ v_revoked_at timestamptz := clock_timestamp();
163
+ BEGIN
164
+ IF NOT public.platform_human_membership_is_administrator(p_actor_principal_id) THEN
165
+ RAISE EXCEPTION 'platform_human_revoke_forbidden' USING ERRCODE = '42501';
166
+ END IF;
167
+
168
+ IF p_target_principal_id IS NULL THEN
169
+ RAISE EXCEPTION 'platform_human_target_required' USING ERRCODE = '22023';
170
+ END IF;
171
+
172
+ IF p_target_principal_id = p_actor_principal_id THEN
173
+ RAISE EXCEPTION 'platform_human_self_revoke_forbidden' USING ERRCODE = 'P0001';
174
+ END IF;
175
+
176
+ IF v_reason IS NOT NULL AND char_length(v_reason) > 1000 THEN
177
+ RAISE EXCEPTION 'platform_human_revocation_reason_too_long' USING ERRCODE = '22023';
178
+ END IF;
179
+
180
+ SELECT * INTO v_target
181
+ FROM public.platform_human_memberships
182
+ WHERE principal_id = p_target_principal_id
183
+ FOR UPDATE;
184
+
185
+ IF NOT FOUND THEN
186
+ RAISE EXCEPTION 'platform_human_target_not_found' USING ERRCODE = 'P0002';
187
+ END IF;
188
+
189
+ IF v_target.membership_state = 'revoked' THEN
190
+ RETURN v_target;
191
+ END IF;
192
+
193
+ PERFORM set_config('app.platform_human_membership_revoke', 'true', true);
194
+
195
+ UPDATE public.platform_human_memberships
196
+ SET membership_state = 'revoked',
197
+ membership_revoked_at = v_revoked_at,
198
+ membership_revoked_by = p_actor_principal_id,
199
+ membership_revocation_reason = v_reason
200
+ WHERE principal_id = p_target_principal_id
201
+ RETURNING * INTO v_target;
202
+
203
+ INSERT INTO public.platform_human_membership_audit_events
204
+ (actor_principal_id, target_principal_id, action, old_value, new_value, reason, occurred_at)
205
+ VALUES (
206
+ p_actor_principal_id,
207
+ p_target_principal_id,
208
+ 'revoke',
209
+ jsonb_build_object('membershipState', 'active', 'role', v_target.role),
210
+ jsonb_build_object(
211
+ 'membershipState', 'revoked',
212
+ 'revokedAt', v_revoked_at,
213
+ 'revokedBy', p_actor_principal_id,
214
+ 'revocationReason', v_reason
215
+ ),
216
+ v_reason,
217
+ v_revoked_at
218
+ );
219
+
220
+ RETURN v_target;
221
+ END;
222
+ $$;
223
+
224
+ REVOKE ALL ON FUNCTION public.platform_human_membership_is_active(uuid)
225
+ FROM PUBLIC, anon, authenticated;
226
+ REVOKE ALL ON FUNCTION public.platform_human_membership_is_administrator(uuid)
227
+ FROM PUBLIC, anon, authenticated;
228
+ REVOKE ALL ON FUNCTION public.platform_human_memberships_guard()
229
+ FROM PUBLIC, anon, authenticated;
230
+ REVOKE ALL ON FUNCTION public.platform_revoke_human_membership(uuid, uuid, text)
231
+ FROM PUBLIC, anon, authenticated;
232
+
233
+ COMMENT ON FUNCTION public.platform_revoke_human_membership(uuid, uuid, text) IS
234
+ 'Dormant direct-bundle revoke command. Requires an active human administrator, retains the target principal/membership, refuses self and last-admin removal, and records one atomic revoke event.';
@@ -0,0 +1,42 @@
1
+ -- Pending direct operator invitations. Lifecycle commands use the trusted
2
+ -- server transaction lane; this forward adds no function or actor privilege.
3
+ CREATE TABLE public.identity_operator_invitations (
4
+ principal_id uuid PRIMARY KEY REFERENCES public.platform_human_principals(principal_id),
5
+ invitation_id uuid NOT NULL UNIQUE,
6
+ email_key text NOT NULL UNIQUE,
7
+ role text NOT NULL CHECK (role IN ('admin', 'distributor')),
8
+ phase text NOT NULL CHECK (phase IN ('invited', 'accepted', 'expired', 'cancelled')),
9
+ invited_by uuid NOT NULL REFERENCES public.platform_human_principals(principal_id),
10
+ invited_at timestamptz NOT NULL,
11
+ expires_at timestamptz NOT NULL,
12
+ accepted_at timestamptz,
13
+ closed_at timestamptz,
14
+ CHECK (email_key = lower(btrim(email_key)) AND NOT email_key = ''),
15
+ CHECK (expires_at BETWEEN invited_at + interval '60 seconds' AND invited_at + interval '30 days'),
16
+ CHECK (
17
+ (phase = 'invited' AND accepted_at IS NULL AND closed_at IS NULL)
18
+ OR (phase = 'accepted' AND accepted_at IS NOT NULL
19
+ AND accepted_at BETWEEN invited_at AND expires_at
20
+ AND NOT accepted_at = expires_at AND closed_at = accepted_at)
21
+ OR (phase = 'expired' AND accepted_at IS NULL AND closed_at = expires_at)
22
+ OR (phase = 'cancelled' AND accepted_at IS NULL AND closed_at IS NOT NULL)
23
+ )
24
+ );
25
+
26
+ CREATE TABLE public.identity_operator_invitation_events (
27
+ event_id uuid PRIMARY KEY,
28
+ invitation_id uuid NOT NULL,
29
+ principal_id uuid NOT NULL REFERENCES public.platform_human_principals(principal_id),
30
+ action text NOT NULL CHECK (action IN ('issue', 'reissue', 'accept', 'expire', 'cancel')),
31
+ actor_kind text NOT NULL CHECK (actor_kind IN ('human', 'system')),
32
+ actor_principal_id uuid REFERENCES public.platform_human_principals(principal_id),
33
+ old_value jsonb NOT NULL,
34
+ new_value jsonb NOT NULL,
35
+ occurred_at timestamptz NOT NULL,
36
+ UNIQUE (invitation_id, action),
37
+ CHECK ((actor_kind = 'human' AND actor_principal_id IS NOT NULL)
38
+ OR (actor_kind = 'system' AND actor_principal_id IS NULL))
39
+ );
40
+
41
+ CREATE INDEX identity_operator_invitation_events_target_idx
42
+ ON public.identity_operator_invitation_events(principal_id, occurred_at DESC);
@@ -0,0 +1,56 @@
1
+ CREATE UNIQUE INDEX platform_human_principals_normalized_email_key
2
+ ON public.platform_human_principals (lower(btrim(email)));
3
+
4
+ CREATE TABLE public.operator_identity_challenges (
5
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ principal_id uuid NOT NULL
7
+ REFERENCES public.platform_human_principals(principal_id),
8
+ email_fingerprint text NOT NULL,
9
+ token_hash text NOT NULL,
10
+ created_at timestamptz NOT NULL DEFAULT now(),
11
+ expires_at timestamptz NOT NULL,
12
+ used_at timestamptz,
13
+
14
+ CONSTRAINT operator_identity_challenges_email_fingerprint_check
15
+ CHECK (
16
+ length(email_fingerprint) = 64
17
+ AND translate(
18
+ email_fingerprint,
19
+ '0123456789abcdef',
20
+ ''
21
+ ) = ''
22
+ ),
23
+
24
+ CONSTRAINT operator_identity_challenges_token_hash_check
25
+ CHECK (
26
+ length(token_hash) = 64
27
+ AND translate(
28
+ token_hash,
29
+ '0123456789abcdef',
30
+ ''
31
+ ) = ''
32
+ ),
33
+
34
+ CONSTRAINT operator_identity_challenges_token_hash_key
35
+ UNIQUE (token_hash),
36
+
37
+ CONSTRAINT operator_identity_challenges_expiry_check
38
+ CHECK (
39
+ expires_at BETWEEN
40
+ created_at + INTERVAL '1 second'
41
+ AND created_at + INTERVAL '10 minutes'
42
+ ),
43
+
44
+ CONSTRAINT operator_identity_challenges_used_at_check
45
+ CHECK (
46
+ used_at IS NULL
47
+ OR used_at BETWEEN created_at AND expires_at
48
+ )
49
+ );
50
+
51
+ CREATE INDEX operator_identity_challenges_principal_created_idx
52
+ ON public.operator_identity_challenges
53
+ (principal_id, created_at DESC);
54
+
55
+ CREATE INDEX operator_identity_challenges_expiry_idx
56
+ ON public.operator_identity_challenges (expires_at, id);
@@ -0,0 +1,15 @@
1
+ -- Direct identity erasure receipts. Audit and idempotency share one retained
2
+ -- pseudonymous record; no personal export snapshot or actor grants are stored.
3
+ CREATE TABLE public.identity_privacy_erasures (
4
+ erasure_id uuid PRIMARY KEY,
5
+ idempotency_key uuid NOT NULL UNIQUE,
6
+ client_id uuid UNIQUE REFERENCES public.clients(id),
7
+ principal_id uuid UNIQUE,
8
+ actor_principal_id uuid NOT NULL REFERENCES public.platform_human_principals(principal_id),
9
+ contract_version text NOT NULL CHECK (contract_version = 'identity.erasure.v1'),
10
+ contributor_versions jsonb NOT NULL,
11
+ dispositions jsonb NOT NULL,
12
+ retained_obligations jsonb NOT NULL,
13
+ completed_at timestamptz NOT NULL,
14
+ CHECK (client_id IS NOT NULL OR principal_id IS NOT NULL)
15
+ );
@@ -0,0 +1,81 @@
1
+ // BFF dispatch mechanism.
2
+ //
3
+ // The route table and its handlers belong to the deployment. This module owns the
4
+ // platform-neutral matching loop, so every HttpRuntimePort adapter (a hosted function or the Node
5
+ // runtime of this package) mounts the same route table without duplicating dispatch logic.
6
+ //
7
+ // Per request, in order: the deployment's `beforeDispatch` hook, path normalization, the first
8
+ // matching route, its availability, its decoded path parameters, the deployment's `preflight`
9
+ // hook, then the handler. An unmatched or unavailable path goes to the deployment's `notFound`.
10
+
11
+ import type { HttpRequest, HttpResponse } from "../http/index.js";
12
+ import { getBundleDescriptor, resolveBundleId } from "../kernel/index.js";
13
+
14
+ export type RouteEntry = {
15
+ route: string;
16
+ pattern: RegExp;
17
+ params: string[];
18
+ handler: (req: HttpRequest, res: HttpResponse) => unknown;
19
+ /** Fail unsupported hosted bundles closed before auth without weakening direct-route auth. */
20
+ availability?: "direct-postgres";
21
+ };
22
+
23
+ /** Match `routes` against one request and run the handler, or answer not found. */
24
+ export type BffDispatch = (
25
+ routes: readonly RouteEntry[],
26
+ req: HttpRequest,
27
+ res: HttpResponse,
28
+ ) => Promise<unknown>;
29
+
30
+ export interface BffDispatcherOptions {
31
+ /** Environment read on each request to decide a route's bundle availability. */
32
+ env: () => Record<string, string | undefined>;
33
+ /** Writes the deployment's not-found response for an unmatched or unavailable path. */
34
+ notFound: (res: HttpResponse, pathname: string) => void;
35
+ /** Runs once per request before matching (for example, ambient request configuration). */
36
+ beforeDispatch?: () => void;
37
+ /**
38
+ * Runs after matching and before the handler, for example route authorization; resolving false
39
+ * means it already answered. Required, so no composition skips it by omission; a deployment
40
+ * without guarded routes passes `() => true`.
41
+ */
42
+ preflight: (req: HttpRequest, res: HttpResponse, route: string) => Promise<boolean> | boolean;
43
+ }
44
+
45
+ // Only the pathname is read, and it does not depend on the URL base; this host only completes a
46
+ // request that carries no Host header.
47
+ const FALLBACK_HOST = "bff.invalid";
48
+
49
+ function requestPath(req: HttpRequest): string {
50
+ const rawUrl = typeof req.url === "string" ? req.url : "/";
51
+ const host = typeof req.headers?.host === "string" ? req.headers.host : FALLBACK_HOST;
52
+ return new URL(rawUrl, "https://" + host).pathname.replace(/\/+$/, "") || "/";
53
+ }
54
+
55
+ /** Build the dispatcher a deployment mounts on its hosted and Node entrypoints alike. */
56
+ export function createBffDispatcher(options: BffDispatcherOptions): BffDispatch {
57
+ return async function dispatch(routes, req, res) {
58
+ options.beforeDispatch?.();
59
+ const pathname = requestPath(req);
60
+ for (const entry of routes) {
61
+ const match = entry.pattern.exec(pathname);
62
+ if (!match) continue;
63
+ if (entry.availability === "direct-postgres"
64
+ && getBundleDescriptor(resolveBundleId(options.env())).capabilities.data !== "postgres") {
65
+ options.notFound(res, pathname);
66
+ return undefined;
67
+ }
68
+ if (entry.params.length > 0) {
69
+ const query = { ...(req.query ?? {}) };
70
+ entry.params.forEach((param, index) => {
71
+ query[param] = decodeURIComponent(match[index + 1] ?? "");
72
+ });
73
+ req.query = query;
74
+ }
75
+ if (!(await options.preflight(req, res, entry.route))) return undefined;
76
+ return entry.handler(req, res);
77
+ }
78
+ options.notFound(res, pathname);
79
+ return undefined;
80
+ };
81
+ }
@@ -0,0 +1,142 @@
1
+ // BFF response envelope.
2
+ //
3
+ // Every BFF response is JSON `{ ok: true, data, meta? }` or `{ ok: false, error: { code, message,
4
+ // details? }, meta? }`. A private response carries the request reference in the `x-request-id`
5
+ // header and, for errors or responses with meta, in `meta.requestId`; a shared-cache response
6
+ // (public or s-maxage) carries neither. `undefined` members are stripped before serialization.
7
+
8
+ import type { HttpResponse } from "../http/index.js";
9
+
10
+ export const BFF_REQUEST_REFERENCE_HEADER = "x-request-id";
11
+
12
+ export const BFF_ERROR_CODES = [
13
+ "BAD_REQUEST",
14
+ "UNAUTHORIZED",
15
+ "FORBIDDEN",
16
+ "NOT_FOUND",
17
+ "METHOD_NOT_ALLOWED",
18
+ "CONFLICT",
19
+ "RATE_LIMITED",
20
+ "UPSTREAM_UNAVAILABLE",
21
+ "INTERNAL",
22
+ "INVALID_RESPONSE",
23
+ ] as const;
24
+
25
+ export type BffErrorCode = (typeof BFF_ERROR_CODES)[number];
26
+
27
+ export type BffMeta = { requestId?: string; contractVersion?: string; [key: string]: unknown };
28
+
29
+ export type BffError = { code: BffErrorCode; message: string; details?: unknown };
30
+
31
+ export type BffSuccessResponse<T> = { ok: true; data: T; meta?: BffMeta };
32
+
33
+ export type BffErrorResponse = { ok: false; error: BffError; meta?: BffMeta };
34
+
35
+ export type BffResponse<T> = BffSuccessResponse<T> | BffErrorResponse;
36
+
37
+ /** The HTTP status each error code answers with unless the caller names another. */
38
+ export const BFF_ERROR_STATUS: Readonly<Record<BffErrorCode, number>> = {
39
+ BAD_REQUEST: 400,
40
+ UNAUTHORIZED: 401,
41
+ FORBIDDEN: 403,
42
+ NOT_FOUND: 404,
43
+ METHOD_NOT_ALLOWED: 405,
44
+ CONFLICT: 409,
45
+ RATE_LIMITED: 429,
46
+ UPSTREAM_UNAVAILABLE: 503,
47
+ INTERNAL: 500,
48
+ INVALID_RESPONSE: 502,
49
+ };
50
+
51
+ export interface BffErrorOptions {
52
+ status?: number;
53
+ details?: unknown;
54
+ meta?: BffMeta;
55
+ }
56
+
57
+ export interface BffSuccessOptions {
58
+ cacheControl?: string;
59
+ }
60
+
61
+ export interface BffResponderOptions {
62
+ /** The request reference observed for this response, if the deployment records one. */
63
+ responseRequestId?: (res: HttpResponse) => string | undefined;
64
+ }
65
+
66
+ export interface BffResponder {
67
+ sendBffSuccess<T>(res: HttpResponse, data: T, meta?: BffMeta, options?: BffSuccessOptions): void;
68
+ sendBffError(res: HttpResponse, code: BffErrorCode, message: string, options?: BffErrorOptions): void;
69
+ sendMethodNotAllowed(res: HttpResponse, allowedMethods: readonly string[]): void;
70
+ }
71
+
72
+ /** Build the envelope writers a deployment's handlers share. */
73
+ export function createBffResponder(options: BffResponderOptions = {}): BffResponder {
74
+ function sendBffSuccess<T>(
75
+ res: HttpResponse,
76
+ data: T,
77
+ meta?: BffMeta,
78
+ successOptions: BffSuccessOptions = {},
79
+ ): void {
80
+ sendJson(res, 200, { ok: true, data, meta }, successOptions.cacheControl);
81
+ }
82
+
83
+ function sendBffError(
84
+ res: HttpResponse,
85
+ code: BffErrorCode,
86
+ message: string,
87
+ errorOptions: BffErrorOptions = {},
88
+ ): void {
89
+ sendJson(res, errorOptions.status ?? BFF_ERROR_STATUS[code], {
90
+ ok: false,
91
+ error: { code, message, details: errorOptions.details },
92
+ meta: errorOptions.meta,
93
+ });
94
+ }
95
+
96
+ function sendMethodNotAllowed(res: HttpResponse, allowedMethods: readonly string[]): void {
97
+ res.setHeader("Allow", allowedMethods.join(", "));
98
+ sendBffError(res, "METHOD_NOT_ALLOWED", "Method not allowed", { status: 405 });
99
+ }
100
+
101
+ function sendJson<T>(
102
+ res: HttpResponse,
103
+ status: number,
104
+ body: BffResponse<T>,
105
+ cacheControl = "no-store",
106
+ ): void {
107
+ const shared = /(?:^|,)\s*(?:public\b|s-maxage\s*=)/i.test(cacheControl);
108
+ const requestId = shared ? undefined : options.responseRequestId?.(res);
109
+ const responseBody = withRequestReference(body, requestId, shared);
110
+ res.setHeader("Content-Type", "application/json");
111
+ res.setHeader("Cache-Control", cacheControl);
112
+ if (shared && typeof res.removeHeader === "function") res.removeHeader(BFF_REQUEST_REFERENCE_HEADER);
113
+ else if (requestId) res.setHeader(BFF_REQUEST_REFERENCE_HEADER, requestId);
114
+ res.status(status).json(stripUndefined(responseBody));
115
+ }
116
+
117
+ return { sendBffSuccess, sendBffError, sendMethodNotAllowed };
118
+ }
119
+
120
+ function withRequestReference<T>(
121
+ body: BffResponse<T>,
122
+ requestId: string | undefined,
123
+ omitRequestId: boolean,
124
+ ): BffResponse<T> {
125
+ if (requestId && (body.ok === false || body.meta)) {
126
+ return { ...body, meta: { ...body.meta, requestId } };
127
+ }
128
+ if (!omitRequestId || !body.meta || !("requestId" in body.meta)) return body;
129
+ const { requestId: _omitted, ...meta } = body.meta;
130
+ return { ...body, meta: Object.keys(meta).length > 0 ? meta : undefined };
131
+ }
132
+
133
+ function stripUndefined(value: unknown): unknown {
134
+ if (Array.isArray(value)) return value.map(stripUndefined);
135
+ if (!value || typeof value !== "object") return value;
136
+
137
+ return Object.fromEntries(
138
+ Object.entries(value)
139
+ .filter(([, entryValue]) => entryValue !== undefined)
140
+ .map(([key, entryValue]) => [key, stripUndefined(entryValue)]),
141
+ );
142
+ }
@@ -0,0 +1,37 @@
1
+ // BFF health route.
2
+ //
3
+ // GET answers the deployment's health payload, validated by the deployment's strict contract, with
4
+ // the payload's contract version in `meta`; any other method answers 405 with `Allow: GET`. A
5
+ // payload that fails its own contract answers INTERNAL rather than an unchecked body.
6
+
7
+ import type { HttpRequest, HttpResponse } from "../http/index.js";
8
+ import type { BffResponder } from "./envelope.js";
9
+
10
+ export interface HealthContract<T extends { contractVersion: string }> {
11
+ safeParse(value: unknown): { success: true; data: T } | { success: false };
12
+ }
13
+
14
+ export interface HealthHandlerOptions<T extends { contractVersion: string }> {
15
+ responder: BffResponder;
16
+ /** The deployment's strict health contract. */
17
+ contract: HealthContract<T>;
18
+ /** Builds this request's health payload: service identity, release provenance and the rest. */
19
+ read: () => unknown;
20
+ }
21
+
22
+ export function createHealthHandler<T extends { contractVersion: string }>(
23
+ options: HealthHandlerOptions<T>,
24
+ ): (req: HttpRequest, res: HttpResponse) => void {
25
+ return function health(req, res) {
26
+ if (req.method !== "GET") {
27
+ options.responder.sendMethodNotAllowed(res, ["GET"]);
28
+ return;
29
+ }
30
+ const result = options.contract.safeParse(options.read());
31
+ if (!result.success) {
32
+ options.responder.sendBffError(res, "INTERNAL", "Health contract is invalid");
33
+ return;
34
+ }
35
+ options.responder.sendBffSuccess(res, result.data, { contractVersion: result.data.contractVersion });
36
+ };
37
+ }
@@ -0,0 +1,30 @@
1
+ export { createBffDispatcher, type BffDispatch, type BffDispatcherOptions, type RouteEntry } from "./dispatch.js";
2
+ export { lazyBffRoute } from "./lazyRoute.js";
3
+ export { rewriteBffRouterRequestUrl } from "./routerRewrite.js";
4
+ export {
5
+ BFF_ERROR_CODES,
6
+ BFF_ERROR_STATUS,
7
+ BFF_REQUEST_REFERENCE_HEADER,
8
+ createBffResponder,
9
+ type BffError,
10
+ type BffErrorCode,
11
+ type BffErrorOptions,
12
+ type BffErrorResponse,
13
+ type BffMeta,
14
+ type BffResponder,
15
+ type BffResponderOptions,
16
+ type BffResponse,
17
+ type BffSuccessOptions,
18
+ type BffSuccessResponse,
19
+ } from "./envelope.js";
20
+ export {
21
+ createHealthHandler,
22
+ type HealthContract,
23
+ type HealthHandlerOptions,
24
+ } from "./health.js";
25
+ export {
26
+ createRequestContextStore,
27
+ type ObservedRequestContext,
28
+ type RequestContextOptions,
29
+ type RequestContextStore,
30
+ } from "./requestContext.js";
@@ -0,0 +1,28 @@
1
+ // Loads a BFF route module on its first request instead of at cold start.
2
+ //
3
+ // A route table binds each route as `lazyBffRoute(() => import("<specifier>"))`, so the
4
+ // specifier stays a static literal that bundlers and inventories can read, and the load's type
5
+ // keeps every module's default export checked against `RouteEntry["handler"]`.
6
+ //
7
+ // Concurrent first calls share one load. A rejected load is not kept: the requests that awaited
8
+ // it reject as a throwing handler does, and the next request loads again. A module that threw
9
+ // during evaluation stays errored in Node's ESM loader; the retry helps only a failure before
10
+ // evaluation (a missing file or a read error). Once loaded, the module's handler is called
11
+ // synchronously and its result returned.
12
+ import type { RouteEntry } from "./dispatch.js";
13
+
14
+ export function lazyBffRoute(load: () => Promise<{ default: RouteEntry["handler"] }>): RouteEntry["handler"] {
15
+ let handler: RouteEntry["handler"] | undefined;
16
+ let loading: Promise<RouteEntry["handler"]> | undefined;
17
+ return (req, res) => {
18
+ if (handler) return handler(req, res);
19
+ loading ??= load().then(
20
+ (module) => (handler = module.default),
21
+ (error: unknown) => {
22
+ loading = undefined;
23
+ throw error;
24
+ },
25
+ );
26
+ return loading.then((loaded) => loaded(req, res));
27
+ };
28
+ }