alepha 0.21.2 → 0.22.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 (463) hide show
  1. package/README.md +0 -1
  2. package/dist/api/audits/index.browser.js.map +1 -1
  3. package/dist/api/audits/index.d.ts +393 -403
  4. package/dist/api/audits/index.d.ts.map +1 -1
  5. package/dist/api/audits/index.js +25 -56
  6. package/dist/api/audits/index.js.map +1 -1
  7. package/dist/api/files/index.browser.js +31 -1
  8. package/dist/api/files/index.browser.js.map +1 -1
  9. package/dist/api/files/index.d.ts +313 -208
  10. package/dist/api/files/index.d.ts.map +1 -1
  11. package/dist/api/files/index.js +152 -42
  12. package/dist/api/files/index.js.map +1 -1
  13. package/dist/api/jobs/index.browser.js +2 -2
  14. package/dist/api/jobs/index.browser.js.map +1 -1
  15. package/dist/api/jobs/index.d.ts +289 -292
  16. package/dist/api/jobs/index.d.ts.map +1 -1
  17. package/dist/api/jobs/index.js +39 -33
  18. package/dist/api/jobs/index.js.map +1 -1
  19. package/dist/api/keys/index.d.ts +211 -216
  20. package/dist/api/keys/index.d.ts.map +1 -1
  21. package/dist/api/keys/index.js.map +1 -1
  22. package/dist/api/notifications/index.browser.js.map +1 -1
  23. package/dist/api/notifications/index.d.ts +188 -195
  24. package/dist/api/notifications/index.d.ts.map +1 -1
  25. package/dist/api/notifications/index.js.map +1 -1
  26. package/dist/api/oauth/index.d.ts +71 -76
  27. package/dist/api/oauth/index.d.ts.map +1 -1
  28. package/dist/api/oauth/index.js.map +1 -1
  29. package/dist/api/organizations/index.browser.js.map +1 -1
  30. package/dist/api/organizations/index.d.ts +104 -109
  31. package/dist/api/organizations/index.d.ts.map +1 -1
  32. package/dist/api/organizations/index.js.map +1 -1
  33. package/dist/api/parameters/index.browser.js +43 -16
  34. package/dist/api/parameters/index.browser.js.map +1 -1
  35. package/dist/api/parameters/index.d.ts +488 -344
  36. package/dist/api/parameters/index.d.ts.map +1 -1
  37. package/dist/api/parameters/index.js +175 -35
  38. package/dist/api/parameters/index.js.map +1 -1
  39. package/dist/api/payments/index.d.ts +396 -402
  40. package/dist/api/payments/index.d.ts.map +1 -1
  41. package/dist/api/payments/index.js.map +1 -1
  42. package/dist/api/subscriptions/index.d.ts +644 -652
  43. package/dist/api/subscriptions/index.d.ts.map +1 -1
  44. package/dist/api/subscriptions/index.js +1 -1
  45. package/dist/api/subscriptions/index.js.map +1 -1
  46. package/dist/api/users/index.browser.js +7 -0
  47. package/dist/api/users/index.browser.js.map +1 -1
  48. package/dist/api/users/index.d.ts +1073 -1006
  49. package/dist/api/users/index.d.ts.map +1 -1
  50. package/dist/api/users/index.js +283 -61
  51. package/dist/api/users/index.js.map +1 -1
  52. package/dist/api/verifications/index.browser.js.map +1 -1
  53. package/dist/api/verifications/index.d.ts +134 -140
  54. package/dist/api/verifications/index.d.ts.map +1 -1
  55. package/dist/api/verifications/index.js.map +1 -1
  56. package/dist/background/index.d.ts +95 -0
  57. package/dist/background/index.d.ts.map +1 -0
  58. package/dist/background/index.js +121 -0
  59. package/dist/background/index.js.map +1 -0
  60. package/dist/background/index.workerd.js +110 -0
  61. package/dist/background/index.workerd.js.map +1 -0
  62. package/dist/batch/index.d.ts +5 -7
  63. package/dist/batch/index.d.ts.map +1 -1
  64. package/dist/batch/index.js.map +1 -1
  65. package/dist/bin/index.js.map +1 -1
  66. package/dist/bucket/index.d.ts +76 -54
  67. package/dist/bucket/index.d.ts.map +1 -1
  68. package/dist/bucket/index.js +58 -11
  69. package/dist/bucket/index.js.map +1 -1
  70. package/dist/bucket/index.workerd.js +200 -5
  71. package/dist/bucket/index.workerd.js.map +1 -1
  72. package/dist/cache/core/index.d.ts +7 -10
  73. package/dist/cache/core/index.d.ts.map +1 -1
  74. package/dist/cache/core/index.js.map +1 -1
  75. package/dist/cache/core/index.workerd.js.map +1 -1
  76. package/dist/cache/database/index.d.ts +22 -26
  77. package/dist/cache/database/index.d.ts.map +1 -1
  78. package/dist/cache/database/index.js.map +1 -1
  79. package/dist/cache/redis/index.d.ts +4 -7
  80. package/dist/cache/redis/index.d.ts.map +1 -1
  81. package/dist/cache/redis/index.js.map +1 -1
  82. package/dist/captcha/index.d.ts +3 -6
  83. package/dist/captcha/index.d.ts.map +1 -1
  84. package/dist/captcha/index.js.map +1 -1
  85. package/dist/cli/config/index.d.ts.map +1 -1
  86. package/dist/cli/config/index.js.map +1 -1
  87. package/dist/cli/core/index.d.ts +417 -214
  88. package/dist/cli/core/index.d.ts.map +1 -1
  89. package/dist/cli/core/index.js +325 -563
  90. package/dist/cli/core/index.js.map +1 -1
  91. package/dist/cli/devtools/index.d.ts +3 -5
  92. package/dist/cli/devtools/index.d.ts.map +1 -1
  93. package/dist/cli/devtools/index.js.map +1 -1
  94. package/dist/cli/i18n/index.d.ts +8 -12
  95. package/dist/cli/i18n/index.d.ts.map +1 -1
  96. package/dist/cli/i18n/index.js.map +1 -1
  97. package/dist/cli/platform/index.d.ts +126 -1342
  98. package/dist/cli/platform/index.d.ts.map +1 -1
  99. package/dist/cli/platform/index.js +136 -2374
  100. package/dist/cli/platform/index.js.map +1 -1
  101. package/dist/cli/platform-lib/index.d.ts +1446 -0
  102. package/dist/cli/platform-lib/index.d.ts.map +1 -0
  103. package/dist/cli/platform-lib/index.js +2597 -0
  104. package/dist/cli/platform-lib/index.js.map +1 -0
  105. package/dist/cli/vendor/index.d.ts +17 -21
  106. package/dist/cli/vendor/index.d.ts.map +1 -1
  107. package/dist/cli/vendor/index.js.map +1 -1
  108. package/dist/command/index.d.ts +21 -20
  109. package/dist/command/index.d.ts.map +1 -1
  110. package/dist/command/index.js +39 -10
  111. package/dist/command/index.js.map +1 -1
  112. package/dist/{containers → container}/core/index.d.ts +13 -15
  113. package/dist/container/core/index.d.ts.map +1 -0
  114. package/dist/{containers → container}/core/index.js +23 -14
  115. package/dist/container/core/index.js.map +1 -0
  116. package/dist/{containers → container}/core/index.workerd.js +37 -22
  117. package/dist/container/core/index.workerd.js.map +1 -0
  118. package/dist/core/index.browser.js +27 -1
  119. package/dist/core/index.browser.js.map +1 -1
  120. package/dist/core/index.d.ts +48 -24
  121. package/dist/core/index.d.ts.map +1 -1
  122. package/dist/core/index.js +27 -1
  123. package/dist/core/index.js.map +1 -1
  124. package/dist/core/index.native.js +27 -1
  125. package/dist/core/index.native.js.map +1 -1
  126. package/dist/core/index.workerd.js +27 -1
  127. package/dist/core/index.workerd.js.map +1 -1
  128. package/dist/crypto/index.browser.js.map +1 -1
  129. package/dist/crypto/index.d.ts +5 -8
  130. package/dist/crypto/index.d.ts.map +1 -1
  131. package/dist/crypto/index.js.map +1 -1
  132. package/dist/datetime/index.d.ts +3 -4
  133. package/dist/datetime/index.d.ts.map +1 -1
  134. package/dist/datetime/index.js.map +1 -1
  135. package/dist/email/brevo/index.d.ts +2 -4
  136. package/dist/email/brevo/index.d.ts.map +1 -1
  137. package/dist/email/brevo/index.js.map +1 -1
  138. package/dist/email/cloudflare/index.d.ts +20 -7
  139. package/dist/email/cloudflare/index.d.ts.map +1 -1
  140. package/dist/email/cloudflare/index.js +46 -9
  141. package/dist/email/cloudflare/index.js.map +1 -1
  142. package/dist/email/core/index.d.ts +6 -9
  143. package/dist/email/core/index.d.ts.map +1 -1
  144. package/dist/email/core/index.js.map +1 -1
  145. package/dist/email/core/index.workerd.js.map +1 -1
  146. package/dist/email/smtp/index.d.ts +10 -13
  147. package/dist/email/smtp/index.d.ts.map +1 -1
  148. package/dist/email/smtp/index.js +107 -32
  149. package/dist/email/smtp/index.js.map +1 -1
  150. package/dist/fake/index.d.ts +1 -2
  151. package/dist/fake/index.d.ts.map +1 -1
  152. package/dist/fake/index.js.map +1 -1
  153. package/dist/lock/core/index.d.ts +9 -14
  154. package/dist/lock/core/index.d.ts.map +1 -1
  155. package/dist/lock/core/index.js.map +1 -1
  156. package/dist/lock/redis/index.d.ts +2 -4
  157. package/dist/lock/redis/index.d.ts.map +1 -1
  158. package/dist/lock/redis/index.js.map +1 -1
  159. package/dist/logger/index.d.ts +105 -76
  160. package/dist/logger/index.d.ts.map +1 -1
  161. package/dist/logger/index.js +196 -174
  162. package/dist/logger/index.js.map +1 -1
  163. package/dist/mcp/index.d.ts +16 -20
  164. package/dist/mcp/index.d.ts.map +1 -1
  165. package/dist/mcp/index.js.map +1 -1
  166. package/dist/orm/core/index.browser.js.map +1 -1
  167. package/dist/orm/core/index.bun.js +19 -1
  168. package/dist/orm/core/index.bun.js.map +1 -1
  169. package/dist/orm/core/index.d.ts +76 -62
  170. package/dist/orm/core/index.d.ts.map +1 -1
  171. package/dist/orm/core/index.js +20 -2
  172. package/dist/orm/core/index.js.map +1 -1
  173. package/dist/orm/postgres/index.bun.js.map +1 -1
  174. package/dist/orm/postgres/index.d.ts +28 -20
  175. package/dist/orm/postgres/index.d.ts.map +1 -1
  176. package/dist/orm/postgres/index.js.map +1 -1
  177. package/dist/queue/core/index.d.ts +12 -15
  178. package/dist/queue/core/index.d.ts.map +1 -1
  179. package/dist/queue/core/index.js.map +1 -1
  180. package/dist/queue/core/index.workerd.js.map +1 -1
  181. package/dist/queue/redis/index.d.ts +3 -5
  182. package/dist/queue/redis/index.d.ts.map +1 -1
  183. package/dist/queue/redis/index.js.map +1 -1
  184. package/dist/react/auth/index.browser.js +9 -2
  185. package/dist/react/auth/index.browser.js.map +1 -1
  186. package/dist/react/auth/index.d.ts +14 -9
  187. package/dist/react/auth/index.d.ts.map +1 -1
  188. package/dist/react/auth/index.js +9 -2
  189. package/dist/react/auth/index.js.map +1 -1
  190. package/dist/react/core/index.d.ts +7 -8
  191. package/dist/react/core/index.d.ts.map +1 -1
  192. package/dist/react/core/index.js +6 -3
  193. package/dist/react/core/index.js.map +1 -1
  194. package/dist/react/form/index.d.ts +2 -4
  195. package/dist/react/form/index.d.ts.map +1 -1
  196. package/dist/react/form/index.js.map +1 -1
  197. package/dist/react/head/index.browser.js.map +1 -1
  198. package/dist/react/head/index.d.ts +2 -4
  199. package/dist/react/head/index.d.ts.map +1 -1
  200. package/dist/react/head/index.js.map +1 -1
  201. package/dist/react/i18n/index.d.ts +47 -11
  202. package/dist/react/i18n/index.d.ts.map +1 -1
  203. package/dist/react/i18n/index.js +33 -1
  204. package/dist/react/i18n/index.js.map +1 -1
  205. package/dist/react/intro/index.d.ts +1 -2
  206. package/dist/react/intro/index.d.ts.map +1 -1
  207. package/dist/react/intro/index.js +2 -2
  208. package/dist/react/intro/index.js.map +1 -1
  209. package/dist/react/router/index.browser.js +65 -19
  210. package/dist/react/router/index.browser.js.map +1 -1
  211. package/dist/react/router/index.d.ts +327 -222
  212. package/dist/react/router/index.d.ts.map +1 -1
  213. package/dist/react/router/index.js +65 -29
  214. package/dist/react/router/index.js.map +1 -1
  215. package/dist/react/testing/index.d.ts +1 -2
  216. package/dist/react/testing/index.d.ts.map +1 -1
  217. package/dist/react/testing/index.js +16 -17
  218. package/dist/react/testing/index.js.map +1 -1
  219. package/dist/react/ui/index.d.ts +20 -25
  220. package/dist/react/ui/index.d.ts.map +1 -1
  221. package/dist/react/ui/index.js.map +1 -1
  222. package/dist/redis/index.bun.js.map +1 -1
  223. package/dist/redis/index.d.ts +17 -19
  224. package/dist/redis/index.d.ts.map +1 -1
  225. package/dist/redis/index.js.map +1 -1
  226. package/dist/retry/index.d.ts +2 -4
  227. package/dist/retry/index.d.ts.map +1 -1
  228. package/dist/retry/index.js.map +1 -1
  229. package/dist/router/index.d.ts.map +1 -1
  230. package/dist/router/index.js.map +1 -1
  231. package/dist/scheduler/index.d.ts +10 -13
  232. package/dist/scheduler/index.d.ts.map +1 -1
  233. package/dist/scheduler/index.js.map +1 -1
  234. package/dist/scheduler/index.workerd.js.map +1 -1
  235. package/dist/security/index.browser.js.map +1 -1
  236. package/dist/security/index.d.ts +45 -48
  237. package/dist/security/index.d.ts.map +1 -1
  238. package/dist/security/index.js.map +1 -1
  239. package/dist/server/auth/index.browser.js.map +1 -1
  240. package/dist/server/auth/index.d.ts +167 -172
  241. package/dist/server/auth/index.d.ts.map +1 -1
  242. package/dist/server/auth/index.js +4 -8
  243. package/dist/server/auth/index.js.map +1 -1
  244. package/dist/server/cookies/index.browser.js.map +1 -1
  245. package/dist/server/cookies/index.d.ts +5 -7
  246. package/dist/server/cookies/index.d.ts.map +1 -1
  247. package/dist/server/cookies/index.js.map +1 -1
  248. package/dist/server/core/index.browser.js.map +1 -1
  249. package/dist/server/core/index.d.ts +88 -73
  250. package/dist/server/core/index.d.ts.map +1 -1
  251. package/dist/server/core/index.js +19 -0
  252. package/dist/server/core/index.js.map +1 -1
  253. package/dist/server/cors/index.d.ts +11 -14
  254. package/dist/server/cors/index.d.ts.map +1 -1
  255. package/dist/server/cors/index.js.map +1 -1
  256. package/dist/server/etag/index.d.ts +6 -9
  257. package/dist/server/etag/index.d.ts.map +1 -1
  258. package/dist/server/etag/index.js.map +1 -1
  259. package/dist/server/health/index.d.ts +18 -21
  260. package/dist/server/health/index.d.ts.map +1 -1
  261. package/dist/server/health/index.js.map +1 -1
  262. package/dist/server/links/index.browser.js +2 -0
  263. package/dist/server/links/index.browser.js.map +1 -1
  264. package/dist/server/links/index.d.ts +63 -67
  265. package/dist/server/links/index.d.ts.map +1 -1
  266. package/dist/server/links/index.js +2 -0
  267. package/dist/server/links/index.js.map +1 -1
  268. package/dist/server/metrics/index.d.ts +5 -7
  269. package/dist/server/metrics/index.d.ts.map +1 -1
  270. package/dist/server/metrics/index.js.map +1 -1
  271. package/dist/server/proxy/index.d.ts +3 -5
  272. package/dist/server/proxy/index.d.ts.map +1 -1
  273. package/dist/server/proxy/index.js.map +1 -1
  274. package/dist/server/rate-limit/index.d.ts +10 -13
  275. package/dist/server/rate-limit/index.d.ts.map +1 -1
  276. package/dist/server/rate-limit/index.js.map +1 -1
  277. package/dist/server/static/index.d.ts +3 -5
  278. package/dist/server/static/index.d.ts.map +1 -1
  279. package/dist/server/static/index.js.map +1 -1
  280. package/dist/server/swagger/index.d.ts +5 -8
  281. package/dist/server/swagger/index.d.ts.map +1 -1
  282. package/dist/server/swagger/index.js.map +1 -1
  283. package/dist/sms/index.d.ts +3 -5
  284. package/dist/sms/index.d.ts.map +1 -1
  285. package/dist/sms/index.js.map +1 -1
  286. package/dist/system/index.browser.js.map +1 -1
  287. package/dist/system/index.d.ts +2 -4
  288. package/dist/system/index.d.ts.map +1 -1
  289. package/dist/system/index.js.map +1 -1
  290. package/dist/system/index.workerd.js.map +1 -1
  291. package/dist/topic/core/index.d.ts +4 -6
  292. package/dist/topic/core/index.d.ts.map +1 -1
  293. package/dist/topic/core/index.js.map +1 -1
  294. package/dist/topic/redis/index.d.ts +5 -8
  295. package/dist/topic/redis/index.d.ts.map +1 -1
  296. package/dist/topic/redis/index.js.map +1 -1
  297. package/package.json +45 -22
  298. package/src/api/audits/__tests__/AuditService.spec.ts +18 -110
  299. package/src/api/audits/controllers/AdminAuditController.ts +14 -0
  300. package/src/api/audits/services/AuditService.ts +21 -88
  301. package/src/api/files/__tests__/FileService.spec.ts +207 -2
  302. package/src/api/files/index.ts +3 -0
  303. package/src/api/files/schemas/fileCreatorSummarySchema.ts +22 -0
  304. package/src/api/files/schemas/fileResourceSchema.ts +10 -1
  305. package/src/api/files/services/FileService.ts +170 -72
  306. package/src/api/jobs/__tests__/$job.spec.ts +24 -1
  307. package/src/api/jobs/index.ts +4 -3
  308. package/src/api/jobs/primitives/$job.ts +7 -3
  309. package/src/api/jobs/providers/DirectJobDispatcher.ts +17 -36
  310. package/src/api/jobs/providers/JobProvider.ts +53 -24
  311. package/src/api/jobs/schemas/jobConfigAtom.ts +1 -1
  312. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +4 -1
  313. package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +3 -1
  314. package/src/api/parameters/__tests__/$parameter.spec.ts +19 -2
  315. package/src/api/parameters/audits/ParameterAudits.ts +17 -0
  316. package/src/api/parameters/controllers/AdminParameterController.ts +95 -19
  317. package/src/api/parameters/index.ts +3 -0
  318. package/src/api/parameters/schemas/activateParameterBodySchema.ts +3 -3
  319. package/src/api/parameters/schemas/createParameterVersionBodySchema.ts +3 -2
  320. package/src/api/parameters/schemas/parameterCreatorSummarySchema.ts +25 -0
  321. package/src/api/parameters/schemas/parameterResponseSchema.ts +5 -0
  322. package/src/api/parameters/schemas/rollbackParameterBodySchema.ts +4 -2
  323. package/src/api/parameters/services/ParameterProvider.ts +69 -6
  324. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +1 -1
  325. package/src/api/users/__tests__/AdminSessionController.spec.ts +37 -0
  326. package/src/api/users/audits/SessionAudits.ts +33 -0
  327. package/src/api/users/audits/UserAudits.ts +19 -43
  328. package/src/api/users/controllers/AdminUserController.ts +66 -1
  329. package/src/api/users/entities/sessions.ts +6 -0
  330. package/src/api/users/entities/users.ts +2 -0
  331. package/src/api/users/index.ts +9 -1
  332. package/src/api/users/primitives/$realm.ts +3 -0
  333. package/src/api/users/schemas/sessionResourceSchema.ts +16 -0
  334. package/src/api/users/schemas/updateUserSchema.ts +1 -8
  335. package/src/api/users/schemas/userQuerySchema.ts +7 -0
  336. package/src/api/users/services/CredentialService.ts +15 -6
  337. package/src/api/users/services/IdentityService.ts +2 -1
  338. package/src/api/users/services/RegistrationService.ts +2 -1
  339. package/src/api/users/services/SessionCrudService.ts +19 -2
  340. package/src/api/users/services/SessionService.ts +39 -19
  341. package/src/api/users/services/UserService.ts +106 -8
  342. package/src/background/__tests__/BackgroundTaskProvider.spec.ts +96 -0
  343. package/src/background/index.ts +37 -0
  344. package/src/background/index.workerd.ts +28 -0
  345. package/src/background/providers/BackgroundTaskProvider.ts +70 -0
  346. package/src/background/providers/WorkerdBackgroundTaskProvider.ts +43 -0
  347. package/src/bucket/__tests__/$bucket.spec.ts +18 -0
  348. package/src/bucket/__tests__/LocalFileStorageProvider.spec.ts +5 -0
  349. package/src/bucket/__tests__/MemoryFileStorageProvider.spec.ts +5 -0
  350. package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +23 -4
  351. package/src/bucket/__tests__/shared.ts +30 -0
  352. package/src/bucket/index.ts +5 -5
  353. package/src/bucket/index.workerd.ts +11 -4
  354. package/src/bucket/primitives/$bucket.ts +27 -0
  355. package/src/bucket/providers/FileStorageProvider.ts +13 -0
  356. package/src/bucket/providers/LocalFileStorageProvider.ts +17 -1
  357. package/src/bucket/providers/MemoryFileStorageProvider.ts +7 -0
  358. package/src/bucket/providers/{CloudflareR2Provider.ts → R2FileStorageProvider.ts} +10 -1
  359. package/src/bucket/providers/{NodeS3BucketProvider.ts → S3FileStorageProvider.ts} +27 -5
  360. package/src/cli/core/__tests__/BuildDockerTask.spec.ts +25 -1
  361. package/src/cli/core/__tests__/init.spec.ts +0 -219
  362. package/src/cli/core/commands/__tests__/BuildCommand.spec.ts +43 -0
  363. package/src/cli/core/commands/build.ts +108 -30
  364. package/src/cli/core/commands/init.ts +0 -12
  365. package/src/cli/core/commands/pack.ts +133 -0
  366. package/src/cli/core/index.ts +3 -0
  367. package/src/cli/core/providers/ViteDevServerProvider.ts +40 -16
  368. package/src/cli/core/services/PackageManagerUtils.ts +0 -16
  369. package/src/cli/core/services/ProjectScaffolder.ts +29 -291
  370. package/src/cli/core/tasks/BuildCloudflareTask.ts +353 -47
  371. package/src/cli/core/tasks/BuildDockerTask.ts +33 -3
  372. package/src/cli/core/tasks/BuildTask.ts +34 -0
  373. package/src/cli/core/templates/apiIndexTs.ts +1 -22
  374. package/src/cli/core/templates/mainCss.ts +0 -1
  375. package/src/cli/core/templates/webAppRouterTs.ts +0 -99
  376. package/src/cli/core/templates/webIndexTs.ts +1 -22
  377. package/src/cli/platform/__tests__/SecretsCommand.spec.ts +5 -3
  378. package/src/cli/platform/commands/SecretsCommand.ts +8 -6
  379. package/src/cli/platform/commands/platform.ts +192 -46
  380. package/src/cli/platform/index.ts +12 -52
  381. package/src/cli/{platform → platform-lib}/__tests__/CloudflareAdapter.spec.ts +426 -169
  382. package/src/cli/{platform → platform-lib}/__tests__/NamingService.spec.ts +91 -4
  383. package/src/cli/{platform → platform-lib}/__tests__/VercelAdapter.spec.ts +56 -85
  384. package/src/cli/{platform → platform-lib}/adapters/CloudflareAdapter.ts +402 -165
  385. package/src/cli/{platform → platform-lib}/adapters/PlatformAdapter.ts +62 -35
  386. package/src/cli/{platform → platform-lib}/adapters/VercelAdapter.ts +6 -10
  387. package/src/cli/{platform → platform-lib}/atoms/platformOptions.ts +34 -1
  388. package/src/cli/platform-lib/index.ts +67 -0
  389. package/src/cli/platform-lib/services/NamingService.ts +136 -0
  390. package/src/cli/{platform → platform-lib}/services/PlatformInspector.ts +60 -13
  391. package/src/cli/{platform → platform-lib}/services/PlatformOrchestrator.ts +54 -43
  392. package/src/cli/{platform → platform-lib}/services/WranglerApi.ts +4 -2
  393. package/src/command/__tests__/Runner.spec.ts +20 -0
  394. package/src/command/helpers/EnvUtils.ts +19 -3
  395. package/src/command/helpers/Runner.ts +12 -2
  396. package/src/command/providers/CliProvider.ts +34 -1
  397. package/src/{containers → container}/core/__tests__/$container.spec.ts +5 -5
  398. package/src/{containers → container}/core/index.ts +4 -4
  399. package/src/{containers → container}/core/index.workerd.ts +19 -3
  400. package/src/{containers → container}/core/primitives/$container.ts +1 -1
  401. package/src/{containers → container}/core/providers/CloudflareContainerProvider.ts +17 -19
  402. package/src/{containers → container}/core/providers/ContainerProvider.ts +16 -2
  403. package/src/{containers → container}/core/providers/MockContainerProvider.ts +1 -1
  404. package/src/core/Alepha.ts +49 -1
  405. package/src/core/__tests__/$env.spec.ts +42 -0
  406. package/src/core/__tests__/dump.spec.ts +47 -0
  407. package/src/email/cloudflare/__tests__/CloudflareEmailProvider.spec.ts +42 -10
  408. package/src/email/cloudflare/index.ts +14 -5
  409. package/src/email/cloudflare/providers/CloudflareEmailProvider.ts +54 -9
  410. package/src/logger/__tests__/Logger.spec.ts +55 -0
  411. package/src/logger/index.ts +13 -0
  412. package/src/logger/services/Logger.ts +31 -1
  413. package/src/orm/__tests__/orm-showcase-tests.ts +27 -0
  414. package/src/orm/__tests__/orm-showcase.spec.ts +12 -0
  415. package/src/orm/core/interfaces/PgQuery.ts +4 -1
  416. package/src/orm/core/services/Repository.ts +27 -11
  417. package/src/react/auth/hooks/useAuth.ts +10 -5
  418. package/src/react/core/__tests__/useQuery.browser.spec.tsx +25 -0
  419. package/src/react/core/hooks/useAction.ts +14 -3
  420. package/src/react/core/hooks/useQuery.ts +24 -4
  421. package/src/react/i18n/components/Translate.tsx +47 -0
  422. package/src/react/i18n/index.ts +2 -0
  423. package/src/react/intro/components/GettingStartedAdminSlide.tsx +2 -2
  424. package/src/react/router/__tests__/$page.spec.tsx +3 -2
  425. package/src/react/router/__tests__/page-can.spec.ts +18 -13
  426. package/src/react/router/hooks/useQueryParams.ts +114 -14
  427. package/src/react/router/primitives/$page.ts +85 -4
  428. package/src/react/router/providers/ReactBrowserRouterProvider.ts +3 -7
  429. package/src/react/router/providers/ReactServerProvider.ts +4 -13
  430. package/src/react/ui/services/SchemaControl.ts +3 -4
  431. package/src/server/core/providers/ServerMultipartProvider.ts +19 -0
  432. package/src/server/links/providers/LinkProvider.ts +10 -0
  433. package/dist/containers/core/index.d.ts.map +0 -1
  434. package/dist/containers/core/index.js.map +0 -1
  435. package/dist/containers/core/index.workerd.js.map +0 -1
  436. package/src/cli/core/templates/componentsJsonTs.ts +0 -39
  437. package/src/cli/core/templates/saasAdminLayoutTsx.ts +0 -77
  438. package/src/cli/core/templates/saasAdminPagesTsx.ts +0 -26
  439. package/src/cli/core/templates/saasAuthLayoutTsx.ts +0 -22
  440. package/src/cli/core/templates/saasAuthPagesTsx.ts +0 -62
  441. package/src/cli/core/templates/saasRealmProviderTs.ts +0 -52
  442. package/src/cli/platform/services/NamingService.ts +0 -54
  443. /package/dist/orm/core/{chunk-o8xxKEmq.js → chunk-B4FMCO8f.js} +0 -0
  444. /package/dist/react/testing/{chunk-6Ep1yQYe.js → chunk-BpyX8vjI.js} +0 -0
  445. /package/src/cli/{platform → platform-lib}/__tests__/GitHubSecretStore.spec.ts +0 -0
  446. /package/src/cli/{platform → platform-lib}/__tests__/PlatformCacheProvider.spec.ts +0 -0
  447. /package/src/cli/{platform → platform-lib}/__tests__/PlatformInspector.spec.ts +0 -0
  448. /package/src/cli/{platform → platform-lib}/__tests__/PlatformOrchestrator.spec.ts +0 -0
  449. /package/src/cli/{platform → platform-lib}/__tests__/SecretFilterService.spec.ts +0 -0
  450. /package/src/cli/{platform → platform-lib}/__tests__/detectResources.spec.ts +0 -0
  451. /package/src/cli/{platform → platform-lib}/providers/GitHubSecretStore.ts +0 -0
  452. /package/src/cli/{platform → platform-lib}/providers/MemorySecretStore.ts +0 -0
  453. /package/src/cli/{platform → platform-lib}/providers/PlatformCacheProvider.ts +0 -0
  454. /package/src/cli/{platform → platform-lib}/providers/SecretStoreProvider.ts +0 -0
  455. /package/src/cli/{platform → platform-lib}/schemas/cloudflare.ts +0 -0
  456. /package/src/cli/{platform → platform-lib}/schemas/platform.ts +0 -0
  457. /package/src/cli/{platform → platform-lib}/schemas/vercel.ts +0 -0
  458. /package/src/cli/{platform → platform-lib}/services/CloudflareApi.ts +0 -0
  459. /package/src/cli/{platform → platform-lib}/services/SecretFilterService.ts +0 -0
  460. /package/src/cli/{platform → platform-lib}/services/VercelApi.ts +0 -0
  461. /package/src/cli/{platform → platform-lib}/services/VercelCli.ts +0 -0
  462. /package/src/{containers → container}/core/interfaces/ContainerOptions.ts +0 -0
  463. /package/src/{containers → container}/core/providers/NodeContainerProvider.ts +0 -0
@@ -11,10 +11,11 @@ import {
11
11
  type DurationLike,
12
12
  } from "alepha/datetime";
13
13
  import { $logger } from "alepha/logger";
14
- import { $repository, type Page } from "alepha/orm";
14
+ import { $repository, type Page, RepositoryProvider } from "alepha/orm";
15
15
  import type { UserAccountToken } from "alepha/security";
16
16
  import type { Ok } from "alepha/server";
17
17
  import { NotFoundError } from "alepha/server";
18
+ import { FileSystemProvider } from "alepha/system";
18
19
  import { type FileEntity, files } from "../entities/files.ts";
19
20
  import type { FileQuery } from "../schemas/fileQuerySchema.ts";
20
21
  import type { FileResource } from "../schemas/fileResourceSchema.ts";
@@ -24,9 +25,39 @@ export class FileService {
24
25
  protected readonly alepha = $inject(Alepha);
25
26
  protected readonly log = $logger();
26
27
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
28
+ protected readonly repositoryProvider = $inject(RepositoryProvider);
29
+ protected readonly fileSystem = $inject(FileSystemProvider);
27
30
  protected readonly defaultBucket = $bucket({ name: "default" });
28
31
  public readonly fileRepository = $repository(files);
29
32
 
33
+ /**
34
+ * Best-effort left join embedding the uploader on every file row, so the
35
+ * admin UI can render `user.email` instead of the bare `creator` UUID.
36
+ * Joins `files.creator` → `users.id`. Only applied when the `users` table is
37
+ * actually registered in the app (see `findFiles`) — the files module stays
38
+ * usable standalone, without `alepha/api/users`. Targets the default `users`
39
+ * table, so creators in non-default realms come back unmatched (`user`
40
+ * undefined), which the UI handles by falling back to `creatorName`.
41
+ *
42
+ * The `users` entity is resolved from the repository registry at runtime
43
+ * rather than imported: the users module already depends on the files module
44
+ * (avatars), so a compile-time import here would form a circular dependency.
45
+ */
46
+ protected resolveCreatorJoin() {
47
+ const usersEntity = this.repositoryProvider
48
+ .getRepositories()
49
+ .find((repo) => repo.entity.name === "users")?.entity;
50
+ if (!usersEntity) {
51
+ return undefined;
52
+ }
53
+ return {
54
+ user: {
55
+ join: usersEntity,
56
+ on: ["creator", usersEntity.cols.id] as ["creator", { name: string }],
57
+ },
58
+ };
59
+ }
60
+
30
61
  protected onUploadFile = $hook({
31
62
  on: "bucket:file:uploaded",
32
63
  handler: async ({ file, bucket, options, id }) => {
@@ -36,18 +67,20 @@ export class FileService {
36
67
 
37
68
  const checksum = await this.calculateChecksum(file);
38
69
 
39
- await this.fileRepository.create({
40
- blobId: id,
41
- mimeType: file.type,
42
- name: file.name,
43
- originalName: file.name,
44
- size: file.size,
45
- creator: options.user?.id,
46
- creatorRealm: options.user?.realm,
47
- expirationDate: this.getExpirationDate(options.ttl),
48
- bucket: bucket.name,
49
- checksum,
50
- });
70
+ await this.persistBlobMetadata(bucket, id, () =>
71
+ this.fileRepository.create({
72
+ blobId: id,
73
+ mimeType: file.type,
74
+ name: file.name,
75
+ originalName: file.name,
76
+ size: file.size,
77
+ creator: options.user?.id,
78
+ creatorRealm: options.user?.realm,
79
+ expirationDate: this.getExpirationDate(options.ttl),
80
+ bucket: bucket.name,
81
+ checksum,
82
+ }),
83
+ );
51
84
  },
52
85
  });
53
86
 
@@ -66,15 +99,28 @@ export class FileService {
66
99
  /**
67
100
  * Calculates SHA-256 checksum of a file.
68
101
  *
102
+ * Reads the whole file into memory. Callers that already hold the bytes
103
+ * should use {@link hashBuffer} instead to avoid re-reading — re-reading a
104
+ * one-shot stream either yields the wrong hash or drains a stream another
105
+ * step still needs.
106
+ *
69
107
  * @param file - The file to calculate checksum for
70
108
  * @returns Hexadecimal string representation of the SHA-256 hash
71
109
  * @protected
72
110
  */
73
111
  protected async calculateChecksum(file: FileLike): Promise<string> {
74
- const buffer = await file.arrayBuffer();
75
- const hash = createHash("sha256");
76
- hash.update(Buffer.from(buffer));
77
- return hash.digest("hex");
112
+ return this.hashBuffer(await file.arrayBuffer());
113
+ }
114
+
115
+ /**
116
+ * Calculates the SHA-256 checksum of an already-read body.
117
+ *
118
+ * @param data - The file bytes
119
+ * @returns Hexadecimal string representation of the SHA-256 hash
120
+ * @protected
121
+ */
122
+ protected hashBuffer(data: ArrayBuffer): string {
123
+ return createHash("sha256").update(Buffer.from(data)).digest("hex");
78
124
  }
79
125
 
80
126
  /**
@@ -141,8 +187,17 @@ export class FileService {
141
187
  where.createdAt = { lte: q.createdBefore };
142
188
  }
143
189
 
190
+ // The creator join requires the `users` table. Only opt in when a users
191
+ // repository is registered (i.e. `alepha/api/users` is loaded) so the
192
+ // files module — and its standalone tests — keep working without it.
193
+ const withCreator = this.resolveCreatorJoin();
194
+
144
195
  return await this.fileRepository
145
- .paginate(q, { where }, { count: true })
196
+ .paginate(
197
+ q,
198
+ { where, ...(withCreator ? { with: withCreator } : {}) },
199
+ { count: true },
200
+ )
146
201
  .then((page) => {
147
202
  return {
148
203
  ...page,
@@ -206,7 +261,20 @@ export class FileService {
206
261
  ): Promise<FileEntity> {
207
262
  const bucket = this.bucket(options.bucket);
208
263
 
209
- const checksum = await this.calculateChecksum(file);
264
+ // Read the source exactly once. The checksum and the stored bytes are
265
+ // both derived from this single buffer, so a one-shot stream is never
266
+ // read twice — the previous code checksummed the file and then let
267
+ // bucket.upload read it again, which drained the stream and stored an
268
+ // empty blob. Uploads are size-capped (bucket maxSize / multipart
269
+ // limits), so buffering here is intentional and bounded.
270
+ const data = await file.arrayBuffer();
271
+ const checksum = this.hashBuffer(data);
272
+ file = this.fileSystem.createFile({
273
+ arrayBuffer: data,
274
+ name: file.name,
275
+ type: file.type,
276
+ });
277
+
210
278
  const blobId = await bucket.upload(file, { persist: false });
211
279
 
212
280
  let expirationDate: string | undefined;
@@ -218,20 +286,58 @@ export class FileService {
218
286
  expirationDate = this.getExpirationDate(bucket.options.ttl);
219
287
  }
220
288
 
221
- return await this.fileRepository.create({
222
- blobId: blobId,
223
- mimeType: file.type,
224
- name: file.name,
225
- originalName: file.name,
226
- size: file.size,
227
- creator: options.user?.id,
228
- creatorRealm: options.user?.realm,
229
- creatorName: options.user?.name,
230
- expirationDate,
231
- bucket: bucket.name,
232
- tags: options.tags,
233
- checksum,
234
- });
289
+ return await this.persistBlobMetadata(bucket, blobId, () =>
290
+ this.fileRepository.create({
291
+ blobId: blobId,
292
+ mimeType: file.type,
293
+ name: file.name,
294
+ originalName: file.name,
295
+ size: file.size,
296
+ creator: options.user?.id,
297
+ creatorRealm: options.user?.realm,
298
+ creatorName: options.user?.name,
299
+ expirationDate,
300
+ bucket: bucket.name,
301
+ tags: options.tags,
302
+ checksum,
303
+ }),
304
+ );
305
+ }
306
+
307
+ /**
308
+ * Persists the metadata row for an already-uploaded blob, deleting the
309
+ * blob if the insert fails. Uploads are not atomic: the blob is written to
310
+ * storage first (the row needs the returned `blobId`), so a failed insert
311
+ * would otherwise leak the blob — an orphaned blob with no DB row. This
312
+ * compensates by removing the blob, favouring the recoverable failure
313
+ * (a missing blob) over the worse one (a row pointing at nothing).
314
+ *
315
+ * Best-effort: cleanup runs with `skipHook` so it neither re-emits
316
+ * `bucket:file:deleted` nor touches the (non-existent) DB row, and a failed
317
+ * cleanup is logged rather than thrown. The original write error is always
318
+ * rethrown so callers still see the real failure.
319
+ *
320
+ * @param bucket - The bucket the blob was uploaded to
321
+ * @param blobId - The id returned by `bucket.upload`
322
+ * @param insert - Thunk performing the metadata insert
323
+ * @returns The created file entity
324
+ */
325
+ protected async persistBlobMetadata(
326
+ bucket: BucketPrimitive,
327
+ blobId: string,
328
+ insert: () => Promise<FileEntity>,
329
+ ): Promise<FileEntity> {
330
+ try {
331
+ return await insert();
332
+ } catch (error) {
333
+ await bucket.delete(blobId, true).catch((cleanupError) => {
334
+ this.log.warn(
335
+ `Failed to remove orphaned blob ${blobId} from bucket ${bucket.name} after a metadata write failure`,
336
+ cleanupError,
337
+ );
338
+ });
339
+ throw error;
340
+ }
235
341
  }
236
342
 
237
343
  /**
@@ -388,50 +494,42 @@ export class FileService {
388
494
  /**
389
495
  * Gets storage statistics including total size, file count, and breakdowns by bucket and MIME type.
390
496
  *
497
+ * Aggregated in SQL (`SUM`/`COUNT` + `GROUP BY`) rather than by loading
498
+ * every row into memory — the table can hold millions of files, and this
499
+ * endpoint must stay O(groups), not O(rows). Totals are derived from the
500
+ * per-bucket groups (every row has exactly one bucket), so no extra query
501
+ * is needed.
502
+ *
391
503
  * @returns Storage statistics with aggregated data
392
504
  */
393
505
  public async getStorageStats(): Promise<StorageStats> {
394
- const allFiles = await this.fileRepository.findMany({});
395
-
396
- const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0);
397
- const totalFiles = allFiles.length;
398
-
399
- // Group by bucket
400
- const bucketMap = new Map<
401
- string,
402
- { totalSize: number; fileCount: number }
403
- >();
404
- for (const file of allFiles) {
405
- const existing = bucketMap.get(file.bucket) || {
406
- totalSize: 0,
407
- fileCount: 0,
408
- };
409
- existing.totalSize += file.size;
410
- existing.fileCount += 1;
411
- bucketMap.set(file.bucket, existing);
412
- }
413
-
414
- // Group by MIME type
415
- const mimeTypeMap = new Map<string, number>();
416
- for (const file of allFiles) {
417
- const existing = mimeTypeMap.get(file.mimeType) || 0;
418
- mimeTypeMap.set(file.mimeType, existing + 1);
419
- }
506
+ const [byBucketRows, byMimeTypeRows] = await Promise.all([
507
+ this.fileRepository.aggregate({
508
+ select: { bucket: true, size: { sum: true, count: true } },
509
+ groupBy: ["bucket"],
510
+ }),
511
+ this.fileRepository.aggregate({
512
+ select: { mimeType: true, size: { count: true } },
513
+ groupBy: ["mimeType"],
514
+ }),
515
+ ]);
516
+
517
+ const byBucket = byBucketRows.map((row) => ({
518
+ bucket: row.bucket,
519
+ totalSize: row.size.sum,
520
+ fileCount: row.size.count,
521
+ }));
522
+
523
+ const byMimeType = byMimeTypeRows.map((row) => ({
524
+ mimeType: row.mimeType,
525
+ fileCount: row.size.count,
526
+ }));
420
527
 
421
528
  return {
422
- totalSize,
423
- totalFiles,
424
- byBucket: Array.from(bucketMap.entries()).map(([bucket, stats]) => ({
425
- bucket,
426
- totalSize: stats.totalSize,
427
- fileCount: stats.fileCount,
428
- })),
429
- byMimeType: Array.from(mimeTypeMap.entries()).map(
430
- ([mimeType, fileCount]) => ({
431
- mimeType,
432
- fileCount,
433
- }),
434
- ),
529
+ totalSize: byBucket.reduce((sum, b) => sum + b.totalSize, 0),
530
+ totalFiles: byBucket.reduce((sum, b) => sum + b.fileCount, 0),
531
+ byBucket,
532
+ byMimeType,
435
533
  };
436
534
  }
437
535
 
@@ -71,7 +71,7 @@ describe("$job — registration validation", () => {
71
71
  // ---------------------------------------------------------------------------
72
72
 
73
73
  describe("$job — cron mode", () => {
74
- it("runs handler inline on trigger, no DB row on success by default", async ({
74
+ it("runs handler inline on trigger and keeps the last successful run", async ({
75
75
  expect,
76
76
  }) => {
77
77
  const alepha = makeApp();
@@ -89,6 +89,29 @@ describe("$job — cron mode", () => {
89
89
  await alepha.start();
90
90
  await app.tick.trigger();
91
91
  expect(calls).toBe(1);
92
+ // Cron jobs keep their last successful run by default so "Last run" works.
93
+ const rows = await app.executions.findMany({
94
+ where: { jobName: { eq: "App.tick" } },
95
+ });
96
+ expect(rows).toHaveLength(1);
97
+ expect(rows[0].status).toBe("ok");
98
+ });
99
+
100
+ it("records no row on success when record is 'error' (opt-out)", async ({
101
+ expect,
102
+ }) => {
103
+ const alepha = makeApp();
104
+ class App {
105
+ executions = $repository(jobExecutionEntity);
106
+ tick = $job({
107
+ cron: "0 0 * * *",
108
+ record: "error",
109
+ handler: async () => {},
110
+ });
111
+ }
112
+ const app = alepha.inject(App);
113
+ await alepha.start();
114
+ await app.tick.trigger();
92
115
  const rows = await app.executions.findMany({
93
116
  where: { jobName: { eq: "App.tick" } },
94
117
  });
@@ -1,4 +1,5 @@
1
1
  import { $module } from "alepha";
2
+ import { AlephaBackground } from "alepha/background";
2
3
  import type { DateTime } from "alepha/datetime";
3
4
  import { AlephaLock } from "alepha/lock";
4
5
  import { AlephaQueue } from "alepha/queue";
@@ -62,8 +63,8 @@ declare module "alepha" {
62
63
  * is overkill.
63
64
  *
64
65
  * **Retries** are sweep-driven across all modes (no exponential backoff).
65
- * Granularity is bounded by `sweepCron` (default 5 min). The first retry
66
- * may land anywhere from a few seconds to ~5 min later depending on when
66
+ * Granularity is bounded by `sweepCron` (default 15 min). The first retry
67
+ * may land anywhere from a few seconds to ~15 min later depending on when
67
68
  * the next sweep tick fires. Cron jobs that declare `retry` go through
68
69
  * the same sweep path — a transient failure no longer means waiting for
69
70
  * the next cron tick (useful for once-daily jobs).
@@ -86,7 +87,7 @@ declare module "alepha" {
86
87
  export const AlephaApiJobs = $module({
87
88
  name: "alepha.api.jobs",
88
89
  primitives: [$job],
89
- imports: [AlephaScheduler, AlephaLock],
90
+ imports: [AlephaScheduler, AlephaLock, AlephaBackground],
90
91
  services: [JobProvider, JobService, AdminJobController, DirectJobDispatcher],
91
92
  });
92
93
 
@@ -77,8 +77,8 @@ export interface JobPrimitiveOptions<T extends TSchema = TSchema>
77
77
  * Cron-mode jobs do not retry — the next tick re-runs.
78
78
  *
79
79
  * Retries are picked up by the reconciliation sweep, so retry granularity
80
- * is bounded by `sweepCron` (default 5 minutes). The first retry may run
81
- * earlier than 5 minutes if the sweep tick happens sooner.
80
+ * is bounded by `sweepCron` (default 15 minutes). The first retry may run
81
+ * earlier than 15 minutes if the sweep tick happens sooner.
82
82
  */
83
83
  retry?: JobRetryOptions;
84
84
 
@@ -115,10 +115,14 @@ export interface JobPrimitiveOptions<T extends TSchema = TSchema>
115
115
  /**
116
116
  * Whether to record successful executions.
117
117
  *
118
- * - `"error"` (default for cron, default for queue): only error/cancelled rows kept
118
+ * - `"error"` (default for queue): only error/cancelled rows kept
119
119
  * - `"all"`: keep success rows too (bounded by `keepLastSuccess`)
120
120
  * - `"none"`: fire-and-forget, no row even on error
121
121
  *
122
+ * **Cron jobs default to keeping their last successful run** (`record: "all"`
123
+ * with `keep.ok = 1`) so the admin "Last run" is accurate — set
124
+ * `record: "error"` to opt out (e.g. for very high-frequency crons).
125
+ *
122
126
  * Note: queue-mode jobs always write a `pending` row at push time (outbox).
123
127
  * This setting controls whether that row is kept on success.
124
128
  */
@@ -1,4 +1,5 @@
1
1
  import { $inject, Alepha } from "alepha";
2
+ import { BackgroundTaskProvider } from "alepha/background";
2
3
  import { $logger } from "alepha/logger";
3
4
  import { JobDispatcher } from "./JobDispatcher.ts";
4
5
  import { JobProvider } from "./JobProvider.ts";
@@ -12,20 +13,17 @@ import { JobProvider } from "./JobProvider.ts";
12
13
  * if the process dies before the handler finishes, the next sweep tick
13
14
  * picks the row up and re-dispatches.
14
15
  *
15
- * **Cloudflare Workers** when an `executionCtx.waitUntil` is available
16
- * in the alepha store at `cloudflare.waitUntil`, the dispatch wraps the
17
- * background promise with `waitUntil` so the runtime keeps the isolate
18
- * alive past the HTTP response. Without this, the handler would be
19
- * terminated when the response is returned and only the next sweep
20
- * (every 5 min by default) would re-dispatch.
21
- *
22
- * **Vercel / single-Node** — on long-running runtimes the event loop
23
- * keeps the promise alive naturally; no special wiring is required.
16
+ * Keeping the isolate alive past the HTTP response (Cloudflare Workers) vs.
17
+ * relying on the event loop (Node/Vercel) is delegated to
18
+ * {@link BackgroundTaskProvider.defer} this dispatcher stays
19
+ * platform-agnostic. The DB outbox row remains the durability guarantee: if
20
+ * the process dies mid-handler, the next sweep re-dispatches.
24
21
  */
25
22
  export class DirectJobDispatcher extends JobDispatcher {
26
23
  public readonly kind = "direct" as const;
27
24
 
28
25
  protected readonly alepha = $inject(Alepha);
26
+ protected readonly background = $inject(BackgroundTaskProvider);
29
27
  protected readonly log = $logger();
30
28
 
31
29
  // Lazy: resolved on first dispatch to break the JobProvider ↔ Dispatcher
@@ -40,32 +38,15 @@ export class DirectJobDispatcher extends JobDispatcher {
40
38
  }
41
39
 
42
40
  public async dispatch(jobName: string, executionId: string): Promise<void> {
43
- const promise = this.getJobProvider()
44
- .processExecution(jobName, executionId)
45
- .catch((err) => {
46
- this.log.warn(
47
- `Direct execution failed for '${jobName}' (sweep will retry)`,
48
- err,
49
- );
50
- });
51
-
52
- // Cloudflare Workers: keep the isolate alive past the HTTP response so
53
- // the handler actually finishes. Outside CF this read returns undefined
54
- // and we fall through to plain fire-and-track.
55
- const waitUntil = this.alepha.store.get("cloudflare.waitUntil") as
56
- | ((p: Promise<unknown>) => void)
57
- | undefined;
58
- if (typeof waitUntil === "function") {
59
- try {
60
- waitUntil(promise);
61
- } catch (e) {
62
- // The runtime may reject waitUntil if called outside a request scope.
63
- // Promise still runs; just log.
64
- this.log.debug(
65
- "waitUntil rejected — falling back to fire-and-track",
66
- e,
67
- );
68
- }
69
- }
41
+ this.background.defer(() =>
42
+ this.getJobProvider()
43
+ .processExecution(jobName, executionId)
44
+ .catch((err) => {
45
+ this.log.warn(
46
+ `Direct execution failed for '${jobName}' (sweep will retry)`,
47
+ err,
48
+ );
49
+ }),
50
+ );
70
51
  }
71
52
  }
@@ -152,6 +152,38 @@ export class JobProvider {
152
152
  protected readonly perExecutionLogs = new Map<string, LogEntry[]>();
153
153
  protected stopping = false;
154
154
 
155
+ constructor() {
156
+ // Register the sweep + trim crons eagerly so the wrangler build
157
+ // step (which reads `CronProvider.getCronJobs()` without running
158
+ // any lifecycle hooks) sees them and emits the matching cron
159
+ // triggers into wrangler.jsonc. CronProvider's `start` hook will
160
+ // boot whatever is in `cronJobs` at runtime — registering here
161
+ // (constructor, runs at inject time) is equivalent to registering
162
+ // in `onStart` from CronProvider's POV but visible to build-time
163
+ // introspection.
164
+ this.cronProvider.createCronJob(
165
+ "api:jobs:sweep",
166
+ this.config.sweepCron,
167
+ async () => {
168
+ await this.sweep();
169
+ },
170
+ true,
171
+ );
172
+ this.cronProvider.createCronJob(
173
+ "api:jobs:trim",
174
+ this.config.trimCron,
175
+ async () => {
176
+ if (this.stopping) return;
177
+ try {
178
+ await this.trimRingBuffers();
179
+ } catch (e) {
180
+ this.log.error("Trim failed", { error: e });
181
+ }
182
+ },
183
+ true,
184
+ );
185
+ }
186
+
155
187
  // --- Registration -----------------------------------------------------------------------------------------------
156
188
 
157
189
  public registerJob(name: string, options: JobPrimitiveOptions): void {
@@ -170,6 +202,21 @@ export class JobProvider {
170
202
  }
171
203
 
172
204
  const kind: "cron" | "queue" = options.cron ? "cron" : "queue";
205
+
206
+ // Cron jobs keep their last successful run by default, so the admin
207
+ // "Last run" reflects reality — a routine cron success is otherwise
208
+ // unrecorded (only failures are), making every healthy cron look like it
209
+ // "never" ran. Bounded to the latest row (keep.ok=1) to avoid unbounded
210
+ // growth from recurring ticks. Opt out of recording successes entirely
211
+ // with `record: "error"` (recommended for very high-frequency crons).
212
+ if (kind === "cron" && options.record === undefined) {
213
+ options = {
214
+ ...options,
215
+ record: "all",
216
+ keep: { ...options.keep, ok: options.keep?.ok ?? 1 },
217
+ };
218
+ }
219
+
173
220
  this.jobs.set(name, { name, options, kind });
174
221
  this.log.debug(`Registered ${kind} job '${name}'`, {
175
222
  cron: options.cron,
@@ -996,8 +1043,8 @@ export class JobProvider {
996
1043
  if (canRetry) {
997
1044
  // Retries are sweep-driven: write the row as `scheduled` with
998
1045
  // `scheduledAt = now`. The next sweep tick (every `sweepCron`,
999
- // default 5 minutes) re-dispatches it. This means the first retry
1000
- // can land anywhere from a few seconds to ~5 minutes later — the
1046
+ // default 15 minutes) re-dispatches it. This means the first retry
1047
+ // can land anywhere from a few seconds to ~15 minutes later — the
1001
1048
  // exact moment depends on when the next sweep tick fires.
1002
1049
  //
1003
1050
  // We deliberately do NOT use exponential backoff or a local timer.
@@ -1246,28 +1293,10 @@ export class JobProvider {
1246
1293
  await this.sweep();
1247
1294
  }
1248
1295
 
1249
- this.cronProvider.createCronJob(
1250
- "api:jobs:sweep",
1251
- this.config.sweepCron,
1252
- async () => {
1253
- await this.sweep();
1254
- },
1255
- true,
1256
- );
1257
-
1258
- this.cronProvider.createCronJob(
1259
- "api:jobs:trim",
1260
- this.config.trimCron,
1261
- async () => {
1262
- if (this.stopping) return;
1263
- try {
1264
- await this.trimRingBuffers();
1265
- } catch (e) {
1266
- this.log.error("Trim failed", { error: e });
1267
- }
1268
- },
1269
- true,
1270
- );
1296
+ // Sweep + trim cron registrations live in the constructor — see
1297
+ // the note there. CronProvider's `start` hook (which runs before
1298
+ // ours via DI order) has already booted them by the time we get
1299
+ // here.
1271
1300
  },
1272
1301
  });
1273
1302
 
@@ -34,7 +34,7 @@ export const jobConfig = $atom({
34
34
  }),
35
35
  }),
36
36
  default: {
37
- sweepCron: "*/5 * * * *",
37
+ sweepCron: "*/15 * * * *",
38
38
  trimCron: "0 * * * *",
39
39
  staleThreshold: 300_000,
40
40
  runTimeout: 1_800_000,
@@ -12,7 +12,10 @@ import { jobExecutionEntity } from "../entities/jobExecutionEntity.ts";
12
12
  * - `can` derives the available admin actions from the row's status.
13
13
  */
14
14
  export const jobExecutionResourceSchema = t.extend(
15
- jobExecutionEntity.schema,
15
+ // `t.extend` composes (interface-extends), it does not override: the base
16
+ // `priority` (integer) would still be enforced alongside the enum below and
17
+ // reject the int→string transform. Drop it from the base first.
18
+ t.omit(jobExecutionEntity.schema, ["priority"]),
16
19
  {
17
20
  priority: t.enum(["critical", "high", "normal", "low"]),
18
21
  can: t.object({
@@ -1,4 +1,4 @@
1
- import { t } from "alepha";
1
+ import { type Static, t } from "alepha";
2
2
 
3
3
  export const adminApiKeyResourceSchema = t.object({
4
4
  id: t.uuid(),
@@ -15,3 +15,5 @@ export const adminApiKeyResourceSchema = t.object({
15
15
  revokedAt: t.optional(t.datetime()),
16
16
  usageCount: t.integer(),
17
17
  });
18
+
19
+ export type AdminApiKeyResource = Static<typeof adminApiKeyResourceSchema>;
@@ -1321,7 +1321,7 @@ describe("getCurrentWithDefault", () => {
1321
1321
  expect(result.schema).toBeNull();
1322
1322
  });
1323
1323
 
1324
- it("should return default when primitive registered but no DB value", async () => {
1324
+ it("should lazy-seed v1 from defaults when primitive registered but no DB value", async () => {
1325
1325
  class AppConfig {
1326
1326
  features = $parameter({
1327
1327
  name: "gwd.default.only",
@@ -1338,7 +1338,19 @@ describe("getCurrentWithDefault", () => {
1338
1338
  const provider = alepha.inject(ParameterProvider);
1339
1339
  const result = await provider.getCurrentWithDefault("gwd.default.only");
1340
1340
 
1341
- expect(result.current).toBeNull();
1341
+ // Auto-seed: getCurrentWithDefault materializes v1 from the compiled
1342
+ // defaults when nothing is in the DB yet, so the admin UI always has a
1343
+ // concrete current row to edit / roll back / compare against.
1344
+ expect(result.current).not.toBeNull();
1345
+ expect(result.current!.version).toBe(1);
1346
+ expect(result.current!.status).toBe("current");
1347
+ expect(result.current!.content).toEqual({
1348
+ enableBeta: false,
1349
+ maxUploadSize: 10485760,
1350
+ });
1351
+ expect(result.current!.changeDescription).toBe(
1352
+ "Auto-seeded from compiled defaults",
1353
+ );
1342
1354
  expect(result.next).toBeNull();
1343
1355
  expect(result.defaultValue).toEqual({
1344
1356
  enableBeta: false,
@@ -1349,6 +1361,11 @@ describe("getCurrentWithDefault", () => {
1349
1361
  maxUploadSize: 10485760,
1350
1362
  });
1351
1363
  expect(result.schema).not.toBeNull();
1364
+
1365
+ // Idempotent: a second call returns the same row without re-creating.
1366
+ const second = await provider.getCurrentWithDefault("gwd.default.only");
1367
+ expect(second.current!.id).toBe(result.current!.id);
1368
+ expect(second.current!.version).toBe(1);
1352
1369
  });
1353
1370
 
1354
1371
  it("should return current, next, and defaults when all exist", async () => {