alepha 0.21.2 → 0.23.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 (519) 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 +282 -285
  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 +217 -222
  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 +1106 -1005
  49. package/dist/api/users/index.d.ts.map +1 -1
  50. package/dist/api/users/index.js +307 -64
  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 +137 -143
  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 +458 -249
  88. package/dist/cli/core/index.d.ts.map +1 -1
  89. package/dist/cli/core/index.js +372 -660
  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 +20 -17
  95. package/dist/cli/i18n/index.d.ts.map +1 -1
  96. package/dist/cli/i18n/index.js +45 -11
  97. package/dist/cli/i18n/index.js.map +1 -1
  98. package/dist/cli/platform/index.d.ts +126 -1342
  99. package/dist/cli/platform/index.d.ts.map +1 -1
  100. package/dist/cli/platform/index.js +136 -2374
  101. package/dist/cli/platform/index.js.map +1 -1
  102. package/dist/cli/platform-lib/index.d.ts +1472 -0
  103. package/dist/cli/platform-lib/index.d.ts.map +1 -0
  104. package/dist/cli/platform-lib/index.js +2660 -0
  105. package/dist/cli/platform-lib/index.js.map +1 -0
  106. package/dist/cli/vendor/index.d.ts +17 -21
  107. package/dist/cli/vendor/index.d.ts.map +1 -1
  108. package/dist/cli/vendor/index.js.map +1 -1
  109. package/dist/command/index.d.ts +20 -19
  110. package/dist/command/index.d.ts.map +1 -1
  111. package/dist/command/index.js +39 -10
  112. package/dist/command/index.js.map +1 -1
  113. package/dist/{containers → container}/core/index.d.ts +13 -15
  114. package/dist/container/core/index.d.ts.map +1 -0
  115. package/dist/{containers → container}/core/index.js +23 -14
  116. package/dist/container/core/index.js.map +1 -0
  117. package/dist/{containers → container}/core/index.workerd.js +37 -22
  118. package/dist/container/core/index.workerd.js.map +1 -0
  119. package/dist/core/index.browser.js +27 -1
  120. package/dist/core/index.browser.js.map +1 -1
  121. package/dist/core/index.d.ts +48 -24
  122. package/dist/core/index.d.ts.map +1 -1
  123. package/dist/core/index.js +27 -1
  124. package/dist/core/index.js.map +1 -1
  125. package/dist/core/index.native.js +27 -1
  126. package/dist/core/index.native.js.map +1 -1
  127. package/dist/core/index.workerd.js +27 -1
  128. package/dist/core/index.workerd.js.map +1 -1
  129. package/dist/crypto/index.browser.js.map +1 -1
  130. package/dist/crypto/index.d.ts +5 -8
  131. package/dist/crypto/index.d.ts.map +1 -1
  132. package/dist/crypto/index.js.map +1 -1
  133. package/dist/datetime/index.d.ts +3 -4
  134. package/dist/datetime/index.d.ts.map +1 -1
  135. package/dist/datetime/index.js.map +1 -1
  136. package/dist/email/brevo/index.d.ts +2 -4
  137. package/dist/email/brevo/index.d.ts.map +1 -1
  138. package/dist/email/brevo/index.js.map +1 -1
  139. package/dist/email/cloudflare/index.d.ts +20 -7
  140. package/dist/email/cloudflare/index.d.ts.map +1 -1
  141. package/dist/email/cloudflare/index.js +46 -9
  142. package/dist/email/cloudflare/index.js.map +1 -1
  143. package/dist/email/core/index.d.ts +6 -9
  144. package/dist/email/core/index.d.ts.map +1 -1
  145. package/dist/email/core/index.js.map +1 -1
  146. package/dist/email/core/index.workerd.js.map +1 -1
  147. package/dist/email/smtp/index.d.ts +10 -13
  148. package/dist/email/smtp/index.d.ts.map +1 -1
  149. package/dist/email/smtp/index.js +107 -32
  150. package/dist/email/smtp/index.js.map +1 -1
  151. package/dist/fake/index.d.ts +1 -2
  152. package/dist/fake/index.d.ts.map +1 -1
  153. package/dist/fake/index.js.map +1 -1
  154. package/dist/lock/core/index.d.ts +9 -14
  155. package/dist/lock/core/index.d.ts.map +1 -1
  156. package/dist/lock/core/index.js.map +1 -1
  157. package/dist/lock/redis/index.d.ts +2 -4
  158. package/dist/lock/redis/index.d.ts.map +1 -1
  159. package/dist/lock/redis/index.js.map +1 -1
  160. package/dist/logger/index.d.ts +105 -76
  161. package/dist/logger/index.d.ts.map +1 -1
  162. package/dist/logger/index.js +196 -174
  163. package/dist/logger/index.js.map +1 -1
  164. package/dist/mcp/index.d.ts +25 -20
  165. package/dist/mcp/index.d.ts.map +1 -1
  166. package/dist/mcp/index.js +23 -0
  167. package/dist/mcp/index.js.map +1 -1
  168. package/dist/orm/core/index.browser.js.map +1 -1
  169. package/dist/orm/core/index.bun.js +19 -1
  170. package/dist/orm/core/index.bun.js.map +1 -1
  171. package/dist/orm/core/index.d.ts +76 -62
  172. package/dist/orm/core/index.d.ts.map +1 -1
  173. package/dist/orm/core/index.js +20 -2
  174. package/dist/orm/core/index.js.map +1 -1
  175. package/dist/orm/postgres/index.bun.js.map +1 -1
  176. package/dist/orm/postgres/index.d.ts +28 -20
  177. package/dist/orm/postgres/index.d.ts.map +1 -1
  178. package/dist/orm/postgres/index.js.map +1 -1
  179. package/dist/queue/core/index.d.ts +12 -15
  180. package/dist/queue/core/index.d.ts.map +1 -1
  181. package/dist/queue/core/index.js.map +1 -1
  182. package/dist/queue/core/index.workerd.js.map +1 -1
  183. package/dist/queue/redis/index.d.ts +3 -5
  184. package/dist/queue/redis/index.d.ts.map +1 -1
  185. package/dist/queue/redis/index.js.map +1 -1
  186. package/dist/react/auth/index.browser.js +9 -2
  187. package/dist/react/auth/index.browser.js.map +1 -1
  188. package/dist/react/auth/index.d.ts +14 -9
  189. package/dist/react/auth/index.d.ts.map +1 -1
  190. package/dist/react/auth/index.js +9 -2
  191. package/dist/react/auth/index.js.map +1 -1
  192. package/dist/react/core/index.d.ts +7 -8
  193. package/dist/react/core/index.d.ts.map +1 -1
  194. package/dist/react/core/index.js +6 -3
  195. package/dist/react/core/index.js.map +1 -1
  196. package/dist/react/form/index.d.ts +2 -5
  197. package/dist/react/form/index.d.ts.map +1 -1
  198. package/dist/react/form/index.js +16 -15
  199. package/dist/react/form/index.js.map +1 -1
  200. package/dist/react/head/index.browser.js.map +1 -1
  201. package/dist/react/head/index.d.ts +2 -4
  202. package/dist/react/head/index.d.ts.map +1 -1
  203. package/dist/react/head/index.js.map +1 -1
  204. package/dist/react/i18n/index.d.ts +90 -11
  205. package/dist/react/i18n/index.d.ts.map +1 -1
  206. package/dist/react/i18n/index.js +147 -11
  207. package/dist/react/i18n/index.js.map +1 -1
  208. package/dist/react/intro/index.d.ts +1 -2
  209. package/dist/react/intro/index.d.ts.map +1 -1
  210. package/dist/react/intro/index.js +2 -2
  211. package/dist/react/intro/index.js.map +1 -1
  212. package/dist/react/router/index.browser.js +193 -24
  213. package/dist/react/router/index.browser.js.map +1 -1
  214. package/dist/react/router/index.d.ts +434 -222
  215. package/dist/react/router/index.d.ts.map +1 -1
  216. package/dist/react/router/index.js +249 -35
  217. package/dist/react/router/index.js.map +1 -1
  218. package/dist/react/sitemap/index.browser.js +35 -0
  219. package/dist/react/sitemap/index.browser.js.map +1 -0
  220. package/dist/react/sitemap/index.d.ts +92 -0
  221. package/dist/react/sitemap/index.d.ts.map +1 -0
  222. package/dist/react/sitemap/index.js +131 -0
  223. package/dist/react/sitemap/index.js.map +1 -0
  224. package/dist/react/testing/index.d.ts +1 -2
  225. package/dist/react/testing/index.d.ts.map +1 -1
  226. package/dist/react/testing/index.js +16 -17
  227. package/dist/react/testing/index.js.map +1 -1
  228. package/dist/react/ui/index.d.ts +20 -25
  229. package/dist/react/ui/index.d.ts.map +1 -1
  230. package/dist/react/ui/index.js.map +1 -1
  231. package/dist/redis/index.bun.js.map +1 -1
  232. package/dist/redis/index.d.ts +17 -19
  233. package/dist/redis/index.d.ts.map +1 -1
  234. package/dist/redis/index.js.map +1 -1
  235. package/dist/retry/index.d.ts +2 -4
  236. package/dist/retry/index.d.ts.map +1 -1
  237. package/dist/retry/index.js.map +1 -1
  238. package/dist/router/index.d.ts.map +1 -1
  239. package/dist/router/index.js.map +1 -1
  240. package/dist/scheduler/index.d.ts +10 -13
  241. package/dist/scheduler/index.d.ts.map +1 -1
  242. package/dist/scheduler/index.js.map +1 -1
  243. package/dist/scheduler/index.workerd.js.map +1 -1
  244. package/dist/security/index.browser.js.map +1 -1
  245. package/dist/security/index.d.ts +45 -48
  246. package/dist/security/index.d.ts.map +1 -1
  247. package/dist/security/index.js.map +1 -1
  248. package/dist/server/auth/index.browser.js.map +1 -1
  249. package/dist/server/auth/index.d.ts +272 -173
  250. package/dist/server/auth/index.d.ts.map +1 -1
  251. package/dist/server/auth/index.js +1608 -15
  252. package/dist/server/auth/index.js.map +1 -1
  253. package/dist/server/cookies/index.browser.js.map +1 -1
  254. package/dist/server/cookies/index.d.ts +20 -7
  255. package/dist/server/cookies/index.d.ts.map +1 -1
  256. package/dist/server/cookies/index.js +22 -3
  257. package/dist/server/cookies/index.js.map +1 -1
  258. package/dist/server/core/index.browser.js.map +1 -1
  259. package/dist/server/core/index.d.ts +106 -73
  260. package/dist/server/core/index.d.ts.map +1 -1
  261. package/dist/server/core/index.js +44 -0
  262. package/dist/server/core/index.js.map +1 -1
  263. package/dist/server/cors/index.d.ts +11 -14
  264. package/dist/server/cors/index.d.ts.map +1 -1
  265. package/dist/server/cors/index.js.map +1 -1
  266. package/dist/server/etag/index.d.ts +6 -9
  267. package/dist/server/etag/index.d.ts.map +1 -1
  268. package/dist/server/etag/index.js.map +1 -1
  269. package/dist/server/health/index.d.ts +18 -21
  270. package/dist/server/health/index.d.ts.map +1 -1
  271. package/dist/server/health/index.js.map +1 -1
  272. package/dist/server/links/index.browser.js +2 -0
  273. package/dist/server/links/index.browser.js.map +1 -1
  274. package/dist/server/links/index.d.ts +63 -67
  275. package/dist/server/links/index.d.ts.map +1 -1
  276. package/dist/server/links/index.js +2 -0
  277. package/dist/server/links/index.js.map +1 -1
  278. package/dist/server/metrics/index.d.ts +5 -7
  279. package/dist/server/metrics/index.d.ts.map +1 -1
  280. package/dist/server/metrics/index.js.map +1 -1
  281. package/dist/server/proxy/index.d.ts +3 -5
  282. package/dist/server/proxy/index.d.ts.map +1 -1
  283. package/dist/server/proxy/index.js.map +1 -1
  284. package/dist/server/rate-limit/index.d.ts +10 -13
  285. package/dist/server/rate-limit/index.d.ts.map +1 -1
  286. package/dist/server/rate-limit/index.js.map +1 -1
  287. package/dist/server/static/index.d.ts +3 -5
  288. package/dist/server/static/index.d.ts.map +1 -1
  289. package/dist/server/static/index.js.map +1 -1
  290. package/dist/server/swagger/index.d.ts +5 -8
  291. package/dist/server/swagger/index.d.ts.map +1 -1
  292. package/dist/server/swagger/index.js.map +1 -1
  293. package/dist/sms/index.d.ts +3 -5
  294. package/dist/sms/index.d.ts.map +1 -1
  295. package/dist/sms/index.js.map +1 -1
  296. package/dist/system/index.browser.js.map +1 -1
  297. package/dist/system/index.d.ts +2 -4
  298. package/dist/system/index.d.ts.map +1 -1
  299. package/dist/system/index.js.map +1 -1
  300. package/dist/system/index.workerd.js.map +1 -1
  301. package/dist/topic/core/index.d.ts +4 -6
  302. package/dist/topic/core/index.d.ts.map +1 -1
  303. package/dist/topic/core/index.js.map +1 -1
  304. package/dist/topic/redis/index.d.ts +5 -8
  305. package/dist/topic/redis/index.d.ts.map +1 -1
  306. package/dist/topic/redis/index.js.map +1 -1
  307. package/package.json +59 -23
  308. package/src/api/audits/__tests__/AuditService.spec.ts +18 -110
  309. package/src/api/audits/controllers/AdminAuditController.ts +14 -0
  310. package/src/api/audits/services/AuditService.ts +21 -88
  311. package/src/api/files/__tests__/FileService.spec.ts +207 -2
  312. package/src/api/files/index.ts +3 -0
  313. package/src/api/files/schemas/fileCreatorSummarySchema.ts +22 -0
  314. package/src/api/files/schemas/fileResourceSchema.ts +10 -1
  315. package/src/api/files/services/FileService.ts +170 -72
  316. package/src/api/jobs/__tests__/$job.spec.ts +24 -1
  317. package/src/api/jobs/index.ts +4 -3
  318. package/src/api/jobs/primitives/$job.ts +7 -3
  319. package/src/api/jobs/providers/DirectJobDispatcher.ts +17 -36
  320. package/src/api/jobs/providers/JobProvider.ts +53 -24
  321. package/src/api/jobs/schemas/jobConfigAtom.ts +1 -1
  322. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +4 -1
  323. package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +3 -1
  324. package/src/api/parameters/__tests__/$parameter.spec.ts +19 -2
  325. package/src/api/parameters/audits/ParameterAudits.ts +17 -0
  326. package/src/api/parameters/controllers/AdminParameterController.ts +95 -19
  327. package/src/api/parameters/index.ts +3 -0
  328. package/src/api/parameters/schemas/activateParameterBodySchema.ts +3 -3
  329. package/src/api/parameters/schemas/createParameterVersionBodySchema.ts +3 -2
  330. package/src/api/parameters/schemas/parameterCreatorSummarySchema.ts +25 -0
  331. package/src/api/parameters/schemas/parameterResponseSchema.ts +5 -0
  332. package/src/api/parameters/schemas/rollbackParameterBodySchema.ts +4 -2
  333. package/src/api/parameters/services/ParameterProvider.ts +69 -6
  334. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +1 -1
  335. package/src/api/users/__tests__/AdminSessionController.spec.ts +37 -0
  336. package/src/api/users/audits/SessionAudits.ts +33 -0
  337. package/src/api/users/audits/UserAudits.ts +19 -43
  338. package/src/api/users/controllers/AdminUserController.ts +66 -1
  339. package/src/api/users/controllers/RealmController.ts +1 -0
  340. package/src/api/users/entities/sessions.ts +6 -0
  341. package/src/api/users/entities/users.ts +2 -0
  342. package/src/api/users/index.ts +9 -1
  343. package/src/api/users/primitives/$realm.ts +29 -0
  344. package/src/api/users/providers/RealmProvider.ts +15 -0
  345. package/src/api/users/schemas/realmConfigSchema.ts +14 -0
  346. package/src/api/users/schemas/sessionResourceSchema.ts +16 -0
  347. package/src/api/users/schemas/updateUserSchema.ts +1 -8
  348. package/src/api/users/schemas/userQuerySchema.ts +7 -0
  349. package/src/api/users/services/CredentialService.ts +15 -6
  350. package/src/api/users/services/IdentityService.ts +2 -1
  351. package/src/api/users/services/RegistrationService.ts +2 -1
  352. package/src/api/users/services/SessionCrudService.ts +19 -2
  353. package/src/api/users/services/SessionService.ts +39 -19
  354. package/src/api/users/services/UserService.ts +106 -8
  355. package/src/background/__tests__/BackgroundTaskProvider.spec.ts +96 -0
  356. package/src/background/index.ts +37 -0
  357. package/src/background/index.workerd.ts +28 -0
  358. package/src/background/providers/BackgroundTaskProvider.ts +70 -0
  359. package/src/background/providers/WorkerdBackgroundTaskProvider.ts +43 -0
  360. package/src/bucket/__tests__/$bucket.spec.ts +18 -0
  361. package/src/bucket/__tests__/LocalFileStorageProvider.spec.ts +5 -0
  362. package/src/bucket/__tests__/MemoryFileStorageProvider.spec.ts +5 -0
  363. package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +23 -4
  364. package/src/bucket/__tests__/shared.ts +30 -0
  365. package/src/bucket/index.ts +5 -5
  366. package/src/bucket/index.workerd.ts +11 -4
  367. package/src/bucket/primitives/$bucket.ts +27 -0
  368. package/src/bucket/providers/FileStorageProvider.ts +13 -0
  369. package/src/bucket/providers/LocalFileStorageProvider.ts +17 -1
  370. package/src/bucket/providers/MemoryFileStorageProvider.ts +7 -0
  371. package/src/bucket/providers/{CloudflareR2Provider.ts → R2FileStorageProvider.ts} +10 -1
  372. package/src/bucket/providers/{NodeS3BucketProvider.ts → S3FileStorageProvider.ts} +27 -5
  373. package/src/cli/core/__tests__/BuildDockerTask.spec.ts +25 -1
  374. package/src/cli/core/__tests__/init.spec.ts +0 -219
  375. package/src/cli/core/atoms/buildOptions.ts +0 -12
  376. package/src/cli/core/commands/__tests__/BuildCommand.spec.ts +43 -0
  377. package/src/cli/core/commands/build.ts +105 -37
  378. package/src/cli/core/commands/init.ts +0 -12
  379. package/src/cli/core/commands/pack.ts +133 -0
  380. package/src/cli/core/index.ts +3 -3
  381. package/src/cli/core/providers/ViteDevServerProvider.ts +40 -16
  382. package/src/cli/core/services/PackageManagerUtils.ts +0 -16
  383. package/src/cli/core/services/ProjectScaffolder.ts +29 -291
  384. package/src/cli/core/tasks/BuildCloudflareTask.ts +382 -56
  385. package/src/cli/core/tasks/BuildDockerTask.ts +33 -3
  386. package/src/cli/core/tasks/BuildPrerenderTask.ts +44 -7
  387. package/src/cli/core/tasks/BuildTask.ts +34 -0
  388. package/src/cli/core/templates/apiIndexTs.ts +1 -22
  389. package/src/cli/core/templates/mainCss.ts +0 -1
  390. package/src/cli/core/templates/webAppRouterTs.ts +0 -99
  391. package/src/cli/core/templates/webIndexTs.ts +1 -22
  392. package/src/cli/i18n/__tests__/I18nCheckService.spec.ts +48 -0
  393. package/src/cli/i18n/services/I18nCheckService.ts +65 -11
  394. package/src/cli/platform/__tests__/SecretsCommand.spec.ts +5 -3
  395. package/src/cli/platform/commands/SecretsCommand.ts +8 -6
  396. package/src/cli/platform/commands/platform.ts +192 -46
  397. package/src/cli/platform/index.ts +12 -52
  398. package/src/cli/{platform → platform-lib}/__tests__/CloudflareAdapter.spec.ts +426 -169
  399. package/src/cli/{platform → platform-lib}/__tests__/NamingService.spec.ts +91 -4
  400. package/src/cli/{platform → platform-lib}/__tests__/VercelAdapter.spec.ts +56 -85
  401. package/src/cli/{platform → platform-lib}/adapters/CloudflareAdapter.ts +519 -190
  402. package/src/cli/{platform → platform-lib}/adapters/PlatformAdapter.ts +62 -35
  403. package/src/cli/{platform → platform-lib}/adapters/VercelAdapter.ts +6 -10
  404. package/src/cli/{platform → platform-lib}/atoms/platformOptions.ts +34 -1
  405. package/src/cli/platform-lib/index.ts +67 -0
  406. package/src/cli/platform-lib/services/NamingService.ts +136 -0
  407. package/src/cli/{platform → platform-lib}/services/PlatformInspector.ts +60 -13
  408. package/src/cli/{platform → platform-lib}/services/PlatformOrchestrator.ts +54 -43
  409. package/src/cli/{platform → platform-lib}/services/WranglerApi.ts +4 -2
  410. package/src/command/__tests__/Runner.spec.ts +20 -0
  411. package/src/command/helpers/EnvUtils.ts +19 -3
  412. package/src/command/helpers/Runner.ts +12 -2
  413. package/src/command/providers/CliProvider.ts +34 -1
  414. package/src/{containers → container}/core/__tests__/$container.spec.ts +5 -5
  415. package/src/{containers → container}/core/index.ts +4 -4
  416. package/src/{containers → container}/core/index.workerd.ts +19 -3
  417. package/src/{containers → container}/core/primitives/$container.ts +1 -1
  418. package/src/{containers → container}/core/providers/CloudflareContainerProvider.ts +17 -19
  419. package/src/{containers → container}/core/providers/ContainerProvider.ts +16 -2
  420. package/src/{containers → container}/core/providers/MockContainerProvider.ts +1 -1
  421. package/src/core/Alepha.ts +49 -1
  422. package/src/core/__tests__/$env.spec.ts +42 -0
  423. package/src/core/__tests__/dump.spec.ts +47 -0
  424. package/src/email/cloudflare/__tests__/CloudflareEmailProvider.spec.ts +42 -10
  425. package/src/email/cloudflare/index.ts +14 -5
  426. package/src/email/cloudflare/providers/CloudflareEmailProvider.ts +54 -9
  427. package/src/logger/__tests__/Logger.spec.ts +55 -0
  428. package/src/logger/index.ts +13 -0
  429. package/src/logger/services/Logger.ts +31 -1
  430. package/src/mcp/__tests__/McpServerProvider.spec.ts +71 -0
  431. package/src/mcp/providers/McpServerProvider.ts +55 -0
  432. package/src/orm/__tests__/orm-showcase-tests.ts +27 -0
  433. package/src/orm/__tests__/orm-showcase.spec.ts +12 -0
  434. package/src/orm/core/interfaces/PgQuery.ts +4 -1
  435. package/src/orm/core/services/Repository.ts +27 -11
  436. package/src/react/auth/hooks/useAuth.ts +10 -5
  437. package/src/react/core/__tests__/useQuery.browser.spec.tsx +25 -0
  438. package/src/react/core/hooks/useAction.ts +14 -3
  439. package/src/react/core/hooks/useQuery.ts +24 -4
  440. package/src/react/form/__tests__/FormModel-submit-loading.spec.ts +71 -0
  441. package/src/react/form/__tests__/form-submitting-reactive.browser.spec.tsx +96 -0
  442. package/src/react/form/services/FormModel.ts +57 -39
  443. package/src/react/i18n/__tests__/I18nProvider.spec.ts +89 -0
  444. package/src/react/i18n/__tests__/locale-routing.spec.ts +107 -0
  445. package/src/react/i18n/components/Translate.tsx +47 -0
  446. package/src/react/i18n/index.ts +2 -0
  447. package/src/react/i18n/providers/I18nProvider.ts +171 -12
  448. package/src/react/intro/components/GettingStartedAdminSlide.tsx +2 -2
  449. package/src/react/router/__tests__/$page.spec.tsx +3 -2
  450. package/src/react/router/__tests__/RouterLocaleProvider.spec.ts +127 -0
  451. package/src/react/router/__tests__/page-can.spec.ts +18 -13
  452. package/src/react/router/hooks/useQueryParams.ts +114 -14
  453. package/src/react/router/index.browser.ts +4 -0
  454. package/src/react/router/index.shared.ts +1 -0
  455. package/src/react/router/index.ts +9 -0
  456. package/src/react/router/primitives/$page.ts +85 -4
  457. package/src/react/router/providers/ReactBrowserRouterProvider.ts +18 -8
  458. package/src/react/router/providers/ReactPageProvider.ts +12 -1
  459. package/src/react/router/providers/ReactServerProvider.ts +96 -14
  460. package/src/react/router/providers/RootComponentsProvider.ts +13 -0
  461. package/src/react/router/providers/RouterLocaleProvider.ts +125 -0
  462. package/src/react/router/providers/__tests__/RootComponentsProvider.spec.ts +15 -0
  463. package/src/react/router/providers/__tests__/rootComponents.ssr.browser.spec.tsx +67 -0
  464. package/src/react/sitemap/__tests__/$sitemap.spec.ts +131 -0
  465. package/src/react/sitemap/index.browser.ts +21 -0
  466. package/src/react/sitemap/index.ts +25 -0
  467. package/src/react/sitemap/primitives/$sitemap.browser.ts +26 -0
  468. package/src/react/sitemap/primitives/$sitemap.ts +196 -0
  469. package/src/react/ui/services/SchemaControl.ts +3 -4
  470. package/src/server/auth/__tests__/appleClientSecret.spec.ts +34 -0
  471. package/src/server/auth/__tests__/authFederationClient.spec.ts +40 -0
  472. package/src/server/auth/__tests__/federationAssertion.spec.ts +146 -0
  473. package/src/server/auth/__tests__/federationRedirectReplay.spec.ts +44 -0
  474. package/src/server/auth/helpers/appleClientSecret.ts +24 -0
  475. package/src/server/auth/helpers/federationAssertion.ts +74 -0
  476. package/src/server/auth/helpers/jtiReplayGuard.ts +41 -0
  477. package/src/server/auth/helpers/safeRedirectPath.ts +19 -0
  478. package/src/server/auth/index.ts +4 -0
  479. package/src/server/auth/primitives/$authFederationBroker.ts +273 -0
  480. package/src/server/auth/primitives/$authFederationClient.ts +89 -0
  481. package/src/server/auth/providers/ServerAuthProvider.ts +18 -4
  482. package/src/server/cookies/__tests__/ServerCookiesProvider.spec.ts +70 -0
  483. package/src/server/cookies/providers/ServerCookiesProvider.ts +23 -3
  484. package/src/server/core/interfaces/ServerRequest.ts +8 -0
  485. package/src/server/core/primitives/$route.ts +27 -0
  486. package/src/server/core/providers/ServerMultipartProvider.ts +19 -0
  487. package/src/server/links/providers/LinkProvider.ts +10 -0
  488. package/dist/containers/core/index.d.ts.map +0 -1
  489. package/dist/containers/core/index.js.map +0 -1
  490. package/dist/containers/core/index.workerd.js.map +0 -1
  491. package/src/cli/core/tasks/BuildSitemapTask.ts +0 -130
  492. package/src/cli/core/templates/componentsJsonTs.ts +0 -39
  493. package/src/cli/core/templates/saasAdminLayoutTsx.ts +0 -77
  494. package/src/cli/core/templates/saasAdminPagesTsx.ts +0 -26
  495. package/src/cli/core/templates/saasAuthLayoutTsx.ts +0 -22
  496. package/src/cli/core/templates/saasAuthPagesTsx.ts +0 -62
  497. package/src/cli/core/templates/saasRealmProviderTs.ts +0 -52
  498. package/src/cli/platform/services/NamingService.ts +0 -54
  499. /package/dist/orm/core/{chunk-o8xxKEmq.js → chunk-B4FMCO8f.js} +0 -0
  500. /package/dist/react/testing/{chunk-6Ep1yQYe.js → chunk-BpyX8vjI.js} +0 -0
  501. /package/src/cli/{platform → platform-lib}/__tests__/GitHubSecretStore.spec.ts +0 -0
  502. /package/src/cli/{platform → platform-lib}/__tests__/PlatformCacheProvider.spec.ts +0 -0
  503. /package/src/cli/{platform → platform-lib}/__tests__/PlatformInspector.spec.ts +0 -0
  504. /package/src/cli/{platform → platform-lib}/__tests__/PlatformOrchestrator.spec.ts +0 -0
  505. /package/src/cli/{platform → platform-lib}/__tests__/SecretFilterService.spec.ts +0 -0
  506. /package/src/cli/{platform → platform-lib}/__tests__/detectResources.spec.ts +0 -0
  507. /package/src/cli/{platform → platform-lib}/providers/GitHubSecretStore.ts +0 -0
  508. /package/src/cli/{platform → platform-lib}/providers/MemorySecretStore.ts +0 -0
  509. /package/src/cli/{platform → platform-lib}/providers/PlatformCacheProvider.ts +0 -0
  510. /package/src/cli/{platform → platform-lib}/providers/SecretStoreProvider.ts +0 -0
  511. /package/src/cli/{platform → platform-lib}/schemas/cloudflare.ts +0 -0
  512. /package/src/cli/{platform → platform-lib}/schemas/platform.ts +0 -0
  513. /package/src/cli/{platform → platform-lib}/schemas/vercel.ts +0 -0
  514. /package/src/cli/{platform → platform-lib}/services/CloudflareApi.ts +0 -0
  515. /package/src/cli/{platform → platform-lib}/services/SecretFilterService.ts +0 -0
  516. /package/src/cli/{platform → platform-lib}/services/VercelApi.ts +0 -0
  517. /package/src/cli/{platform → platform-lib}/services/VercelCli.ts +0 -0
  518. /package/src/{containers → container}/core/interfaces/ContainerOptions.ts +0 -0
  519. /package/src/{containers → container}/core/providers/NodeContainerProvider.ts +0 -0
@@ -67,9 +67,12 @@ export type PgRelation<Base extends TObject> = {
67
67
  schema: TObject;
68
68
  name: string;
69
69
  };
70
+ // `readonly` so a literal tuple written with `as const` (the natural
71
+ // shape for `["userId", users.cols.id]`) satisfies the type without an
72
+ // explicit cast. The relation manager only reads the indices.
70
73
  on:
71
74
  | SQLWrapper
72
- | [
75
+ | readonly [
73
76
  keyof Static<Base>,
74
77
  {
75
78
  name: string;
@@ -597,27 +597,43 @@ export abstract class Repository<T extends TObject> {
597
597
 
598
598
  /**
599
599
  * Find an entity by ID. Returns `undefined` if not found.
600
+ *
601
+ * Pass `with` to eager-load relations on the result — same `with` map
602
+ * shape as `findOne` / `paginate`. Without `with`, returns the plain
603
+ * row.
604
+ *
605
+ * @example
606
+ * ```ts
607
+ * const session = await sessions.findById(id, {
608
+ * with: { user: { join: users, on: ["userId", users.cols.id] as const } },
609
+ * });
610
+ * session?.user?.email;
611
+ * ```
600
612
  */
601
- public async findById(
613
+ public async findById<R extends PgRelationMap<T>>(
602
614
  id: string | number,
603
- opts: StatementOptions = {},
604
- ): Promise<Static<T> | undefined> {
605
- return await this.findOne(
615
+ opts: StatementOptions & { with?: R } = {},
616
+ ): Promise<PgStatic<T, R> | undefined> {
617
+ const { with: withRelations, ...rest } = opts;
618
+ return (await this.findOne<R>(
606
619
  {
607
620
  where: this.getWhereId(id),
608
- },
609
- opts,
610
- );
621
+ ...(withRelations ? { with: withRelations } : {}),
622
+ } as Pick<PgQueryRelations<T, R>, "with" | "where">,
623
+ rest,
624
+ )) as PgStatic<T, R> | undefined;
611
625
  }
612
626
 
613
627
  /**
614
628
  * Find an entity by ID. Throws `DbEntityNotFoundError` if not found.
629
+ *
630
+ * Pass `with` to eager-load relations — see {@link findById}.
615
631
  */
616
- public async getById(
632
+ public async getById<R extends PgRelationMap<T>>(
617
633
  id: string | number,
618
- opts: StatementOptions = {},
619
- ): Promise<Static<T>> {
620
- const entity = await this.findById(id, opts);
634
+ opts: StatementOptions & { with?: R } = {},
635
+ ): Promise<PgStatic<T, R>> {
636
+ const entity = await this.findById<R>(id, opts);
621
637
 
622
638
  if (!entity) {
623
639
  throw new DbEntityNotFoundError(this.tableName);
@@ -1,6 +1,6 @@
1
1
  import { useAlepha, useStore } from "alepha/react";
2
2
  import { currentUserAtom } from "alepha/security";
3
- import { type HttpVirtualClient, LinkProvider } from "alepha/server/links";
3
+ import { LinkProvider } from "alepha/server/links";
4
4
  import { ReactAuth } from "../services/ReactAuth.ts";
5
5
 
6
6
  export const useAuth = <T extends object = any>() => {
@@ -24,10 +24,15 @@ export const useAuth = <T extends object = any>() => {
24
24
  ) => {
25
25
  await alepha.inject(ReactAuth).login(provider as string, options);
26
26
  },
27
- can: <Api extends object = any>(
28
- name: keyof HttpVirtualClient<Api>,
29
- ): boolean => {
30
- return alepha.inject(LinkProvider).can(name as string);
27
+ /**
28
+ * UI permission check — does the current user hold this permission?
29
+ * Supports exact and wildcard names (e.g. `"admin:*"`, `"admin:user:read"`).
30
+ *
31
+ * UI affordance only (show/hide/disable). Real access control is enforced
32
+ * server-side via `$secure` on the route/action.
33
+ */
34
+ has: (permission: string): boolean => {
35
+ return alepha.inject(LinkProvider).can(permission);
31
36
  },
32
37
  };
33
38
  };
@@ -41,6 +41,31 @@ describe("useQuery", () => {
41
41
  expect(handler).toHaveBeenCalledTimes(2);
42
42
  });
43
43
 
44
+ test("reports loading on the first render, before the fetch settles", async ({
45
+ expect,
46
+ }) => {
47
+ const alepha = Alepha.create().with(AlephaDateTime);
48
+ await alepha.start();
49
+
50
+ const handler = vi.fn(async () => "value");
51
+
52
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
53
+ <AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
54
+ );
55
+
56
+ const { result } = renderHook(() => useQuery({ handler }, []), { wrapper });
57
+
58
+ // No data yet, but loading must already be true so callers can render a
59
+ // skeleton instead of an empty/not-found flash.
60
+ expect(result.current.data).toBe(undefined);
61
+ expect(result.current.loading).toBe(true);
62
+
63
+ await waitFor(() => {
64
+ expect(result.current.data).toBe("value");
65
+ });
66
+ expect(result.current.loading).toBe(false);
67
+ });
68
+
44
69
  test("does not run when enabled is false", async ({ expect }) => {
45
70
  const alepha = Alepha.create().with(AlephaDateTime);
46
71
  await alepha.start();
@@ -158,6 +158,15 @@ export function useAction<Args extends any[], Result = void>(
158
158
  abortControllerRef.current.abort();
159
159
  abortControllerRef.current = undefined;
160
160
  }
161
+
162
+ // Release the concurrency guard: we've just abandoned the in-flight run
163
+ // (its result is discarded by the aborted-signal check), so the *next*
164
+ // lifecycle must be free to start. Without this, a remount that reuses
165
+ // the same refs — React StrictMode's double-invoke, or a Suspense/router
166
+ // remount on client-side navigation — would hit `if (isExecutingRef
167
+ // .current) return` and never re-run, leaving a `useQuery` stuck on its
168
+ // loading skeleton forever (the original request 200s but is dropped).
169
+ isExecutingRef.current = false;
161
170
  };
162
171
  }, []);
163
172
 
@@ -230,10 +239,12 @@ export function useAction<Args extends any[], Result = void>(
230
239
 
231
240
  if (options.onError) {
232
241
  await options.onError(error);
233
- } else {
234
- // Re-throw if no custom error handler
235
- throw error;
236
242
  }
243
+ // Without a custom `onError`, the error is NOT re-thrown: it is captured
244
+ // in `error` state and emitted as `react:action:error` (a mounted
245
+ // <ActionErrorToaster /> surfaces it as a toast). This keeps
246
+ // fire-and-forget `action.run()` calls from producing unhandled
247
+ // promise rejections.
237
248
  } finally {
238
249
  isExecutingRef.current = false;
239
250
  if (isMountedRef.current) {
@@ -1,6 +1,6 @@
1
1
  import type { Async } from "alepha";
2
2
  import type { DurationLike } from "alepha/datetime";
3
- import { type DependencyList, useCallback, useState } from "react";
3
+ import { type DependencyList, useCallback, useRef, useState } from "react";
4
4
  import type { ActionContext } from "./useAction.ts";
5
5
  import { useAction } from "./useAction.ts";
6
6
 
@@ -47,17 +47,23 @@ export function useQuery<Result>(
47
47
  options: UseQueryOptions<Result>,
48
48
  deps: DependencyList,
49
49
  ): UseQueryReturn<Result> {
50
+ const enabled = options.enabled !== false;
50
51
  const [data, setData] = useState<Result | undefined>(options.initialData);
52
+ // Tracks whether the auto-run has completed at least once, so we can keep
53
+ // `loading` true on the very first render — before `runOnInit`'s effect
54
+ // fires — instead of briefly reporting `loading: false` with no data.
55
+ const settledRef = useRef(false);
51
56
 
52
57
  const action = useAction<[], Result>(
53
58
  {
54
59
  id: options.id,
55
60
  handler: options.handler,
56
- runOnInit: options.enabled !== false,
61
+ runOnInit: enabled,
57
62
  runEvery: options.runEvery,
58
63
  debounce: options.debounce,
59
64
  onError: options.onError,
60
65
  onSuccess: async (result) => {
66
+ settledRef.current = true;
61
67
  setData(result);
62
68
  if (options.onSuccess) {
63
69
  await options.onSuccess(result);
@@ -69,9 +75,21 @@ export function useQuery<Result>(
69
75
 
70
76
  const refetch = useCallback(() => action.run(), [action.run]);
71
77
 
78
+ // `loading` is true while a fetch is in flight, AND during the initial gap
79
+ // between first render and the auto-run effect — so consumers can render a
80
+ // skeleton immediately instead of flashing an empty/not-found state. Only
81
+ // applies to enabled queries with no `initialData` seed; an error settles
82
+ // it (the error path keeps the caller's `onError` re-throw semantics).
83
+ const loading =
84
+ action.loading ||
85
+ (enabled &&
86
+ options.initialData === undefined &&
87
+ !settledRef.current &&
88
+ action.error === undefined);
89
+
72
90
  return {
73
91
  data,
74
- loading: action.loading,
92
+ loading,
75
93
  error: action.error,
76
94
  refetch,
77
95
  cancel: action.cancel,
@@ -132,7 +150,9 @@ export interface UseQueryReturn<Result> {
132
150
  data: Result | undefined;
133
151
 
134
152
  /**
135
- * Loading state — `true` while a fetch is in flight.
153
+ * Loading state — `true` while a fetch is in flight, and also from the
154
+ * first render until the initial auto-run settles (for enabled queries
155
+ * with no `initialData`), so a skeleton can render without a flash.
136
156
  */
137
157
  loading: boolean;
138
158
 
@@ -0,0 +1,71 @@
1
+ import { Alepha, t } from "alepha";
2
+ import { AlephaLogger } from "alepha/logger";
3
+ import { describe, it } from "vitest";
4
+ import { FormModel } from "../services/FormModel.ts";
5
+
6
+ /**
7
+ * Repro for: a form whose submit fails TypeBox validation (missing field)
8
+ * leaves the submit button stuck in its loading state forever.
9
+ *
10
+ * The button's loading is driven by the `form:submit:begin` / `form:submit:end`
11
+ * event pair (see useFormState). So the invariant that matters is: every
12
+ * `form:submit:begin` is followed by a `form:submit:end`, even when validation
13
+ * throws AND even when a downstream error listener (e.g. the action-error
14
+ * toaster) itself throws.
15
+ */
16
+ describe("FormModel.submit loading pairing", () => {
17
+ const makeForm = (alepha: Alepha) =>
18
+ alepha.inject(FormModel as any, {
19
+ lifetime: "transient",
20
+ args: [
21
+ "f1",
22
+ {
23
+ id: "f1",
24
+ schema: t.object({ email: t.text(), password: t.text() }),
25
+ handler: () => {},
26
+ initialValues: {}, // both required fields missing → decode throws
27
+ },
28
+ ],
29
+ }) as FormModel<any>;
30
+
31
+ it("emits form:submit:end on a validation error (intrinsic)", async ({
32
+ expect,
33
+ }) => {
34
+ const alepha = Alepha.create().with(AlephaLogger);
35
+ await alepha.start();
36
+ const form = makeForm(alepha);
37
+ let begun = false;
38
+ let ended = false;
39
+ alepha.events.on("form:submit:begin", () => {
40
+ begun = true;
41
+ });
42
+ alepha.events.on("form:submit:end", () => {
43
+ ended = true;
44
+ });
45
+
46
+ await form.submit().catch(() => {});
47
+
48
+ expect(begun).toBe(true);
49
+ expect(ended).toBe(true); // pairing must hold
50
+ });
51
+
52
+ it("emits form:submit:end even when a react:action:error listener throws", async ({
53
+ expect,
54
+ }) => {
55
+ const alepha = Alepha.create().with(AlephaLogger);
56
+ await alepha.start();
57
+ const form = makeForm(alepha);
58
+ let ended = false;
59
+ alepha.events.on("form:submit:end", () => {
60
+ ended = true;
61
+ });
62
+ // Simulates a globally-mounted listener (e.g. ActionErrorToaster) throwing.
63
+ alepha.events.on("react:action:error", () => {
64
+ throw new Error("toaster boom");
65
+ });
66
+
67
+ await form.submit().catch(() => {});
68
+
69
+ expect(ended).toBe(true); // currently FAILS: end is skipped → button stuck
70
+ });
71
+ });
@@ -0,0 +1,96 @@
1
+ import { fireEvent, render, waitFor } from "@testing-library/react";
2
+ import { Alepha, t } from "alepha";
3
+ import { AlephaLogger } from "alepha/logger";
4
+ import { AlephaContext } from "alepha/react";
5
+ import { describe, it } from "vitest";
6
+ import { useForm, useFormState } from "../index.ts";
7
+
8
+ /**
9
+ * Submit buttons bind their loading state to `useFormState(form, ["loading"])`
10
+ * (the reactive form-state API; the non-reactive `form.submitting` getter was
11
+ * removed). This loading must:
12
+ * - turn ON while an async submit is in flight, and
13
+ * - turn OFF again afterwards — including after a TypeBox validation error.
14
+ *
15
+ * The OFF guarantee relies on `FormModel.submit` always emitting
16
+ * `form:submit:end` (see FormModel-submit-loading.spec.ts).
17
+ */
18
+ describe("useFormState loading drives the submit button", () => {
19
+ const mount = (alepha: Alepha, ui: React.ReactNode) =>
20
+ render(
21
+ <AlephaContext.Provider value={alepha}>{ui}</AlephaContext.Provider>,
22
+ );
23
+
24
+ it("turns loading on during an async submit and off after", async ({
25
+ expect,
26
+ }) => {
27
+ const alepha = Alepha.create().with(AlephaLogger);
28
+ let release!: () => void;
29
+
30
+ const Form = () => {
31
+ const form = useForm({
32
+ id: "async",
33
+ schema: t.object({ name: t.string({ minLength: 1 }) }),
34
+ initialValues: { name: "ok" },
35
+ handler: () => new Promise<void>((r) => (release = r)),
36
+ });
37
+ const { loading } = useFormState(form, ["loading"]);
38
+ return (
39
+ <form {...form.props} data-testid="form">
40
+ <button
41
+ type="submit"
42
+ data-testid="submit"
43
+ data-loading={loading ? "true" : "false"}
44
+ >
45
+ go
46
+ </button>
47
+ </form>
48
+ );
49
+ };
50
+
51
+ const { getByTestId } = mount(alepha, <Form />);
52
+ fireEvent.submit(getByTestId("form"));
53
+
54
+ await waitFor(() =>
55
+ expect(getByTestId("submit").getAttribute("data-loading")).toBe("true"),
56
+ );
57
+ release();
58
+ await waitFor(() =>
59
+ expect(getByTestId("submit").getAttribute("data-loading")).toBe("false"),
60
+ );
61
+ });
62
+
63
+ it("clears loading after a TypeBox validation error", async ({ expect }) => {
64
+ const alepha = Alepha.create().with(AlephaLogger);
65
+
66
+ const Form = () => {
67
+ const form = useForm({
68
+ id: "login",
69
+ schema: t.object({
70
+ identifier: t.string({ minLength: 1 }),
71
+ password: t.string({ minLength: 6 }),
72
+ }),
73
+ handler: async () => {},
74
+ });
75
+ const { loading } = useFormState(form, ["loading"]);
76
+ return (
77
+ <form {...form.props} data-testid="form">
78
+ <button
79
+ type="submit"
80
+ data-testid="submit"
81
+ data-loading={loading ? "true" : "false"}
82
+ >
83
+ Sign in
84
+ </button>
85
+ </form>
86
+ );
87
+ };
88
+
89
+ const { getByTestId } = mount(alepha, <Form />);
90
+ fireEvent.submit(getByTestId("form")); // empty → validation error
91
+
92
+ await waitFor(() =>
93
+ expect(getByTestId("submit").getAttribute("data-loading")).toBe("false"),
94
+ );
95
+ });
96
+ });
@@ -27,10 +27,6 @@ export class FormModel<T extends TObject> {
27
27
 
28
28
  public input: SchemaToInput<T>;
29
29
 
30
- public get submitting(): boolean {
31
- return this.submitInProgress;
32
- }
33
-
34
30
  constructor(
35
31
  public readonly id: string,
36
32
  public readonly options: FormCtrlOptions<T>,
@@ -185,14 +181,22 @@ export class FormModel<T extends TObject> {
185
181
  return;
186
182
  }
187
183
 
188
- // emit both action and form events
189
- await this.alepha.events.emit("react:action:begin", {
190
- type: "form",
191
- id: this.id,
192
- });
193
- await this.alepha.events.emit("form:submit:begin", {
194
- id: this.id,
195
- });
184
+ // Lifecycle events are best-effort NOTIFICATIONS (loading spinners, toasts,
185
+ // analytics). A misbehaving listener must never break form state or reject
186
+ // submit() — hence `{ catch: true }` on every emit below. Without it, a
187
+ // throwing `react:action:error`/`form:submit:error` listener would skip the
188
+ // `form:submit:end` "loading off" signal and leave submit buttons stuck in
189
+ // their loading state forever.
190
+ await this.alepha.events.emit(
191
+ "react:action:begin",
192
+ { type: "form", id: this.id },
193
+ { catch: true },
194
+ );
195
+ await this.alepha.events.emit(
196
+ "form:submit:begin",
197
+ { id: this.id },
198
+ { catch: true },
199
+ );
196
200
 
197
201
  this.submitInProgress = true;
198
202
 
@@ -210,39 +214,53 @@ export class FormModel<T extends TObject> {
210
214
 
211
215
  await options.handler(values as any);
212
216
 
213
- await this.alepha.events.emit("react:action:success", {
214
- type: "form",
215
- id: this.id,
216
- });
217
- await this.alepha.events.emit("form:submit:success", {
218
- id: this.id,
219
- values,
220
- });
217
+ await this.alepha.events.emit(
218
+ "react:action:success",
219
+ { type: "form", id: this.id },
220
+ { catch: true },
221
+ );
222
+ await this.alepha.events.emit(
223
+ "form:submit:success",
224
+ { id: this.id, values },
225
+ { catch: true },
226
+ );
221
227
  } catch (error) {
222
228
  this.log.error("Form submission error:", error);
223
229
 
224
- options.onError?.(error as Error);
225
-
226
- await this.alepha.events.emit("react:action:error", {
227
- type: "form",
228
- id: this.id,
229
- error: error as Error,
230
- });
231
- await this.alepha.events.emit("form:submit:error", {
232
- error: error as Error,
233
- id: this.id,
234
- });
230
+ // A throwing onError callback must not abort the lifecycle either.
231
+ try {
232
+ options.onError?.(error as Error);
233
+ } catch (handlerError) {
234
+ this.log.error("Form onError handler threw:", handlerError);
235
+ }
236
+
237
+ await this.alepha.events.emit(
238
+ "react:action:error",
239
+ { type: "form", id: this.id, error: error as Error },
240
+ { catch: true },
241
+ );
242
+ await this.alepha.events.emit(
243
+ "form:submit:error",
244
+ { error: error as Error, id: this.id },
245
+ { catch: true },
246
+ );
235
247
  } finally {
236
248
  this.submitInProgress = false;
237
- }
238
249
 
239
- await this.alepha.events.emit("react:action:end", {
240
- type: "form",
241
- id: this.id,
242
- });
243
- await this.alepha.events.emit("form:submit:end", {
244
- id: this.id,
245
- });
250
+ // The "loading off" signals live in `finally` so they ALWAYS fire,
251
+ // even if something above threw — guaranteeing the begin/end pairing
252
+ // that drives submit-button loading state.
253
+ await this.alepha.events.emit(
254
+ "react:action:end",
255
+ { type: "form", id: this.id },
256
+ { catch: true },
257
+ );
258
+ await this.alepha.events.emit(
259
+ "form:submit:end",
260
+ { id: this.id },
261
+ { catch: true },
262
+ );
263
+ }
246
264
  };
247
265
 
248
266
  /**
@@ -449,6 +449,95 @@ describe("I18nProvider", () => {
449
449
  });
450
450
  });
451
451
 
452
+ describe("server-side language autodetect (Accept-Language)", () => {
453
+ /**
454
+ * Injects a real, dictionary-populated `I18nProvider` and exposes the
455
+ * protected resolution used by the `server:onRequest` hook, so the priority
456
+ * (cookie → header → fallback) can be unit-tested against a true registry.
457
+ */
458
+ const setup = (AppClass: new () => unknown) => {
459
+ const alepha = Alepha.create().with(AlephaReactI18n);
460
+ alepha.inject(AppClass as any);
461
+ const i18n = alepha.inject(I18nProvider);
462
+ return {
463
+ i18n,
464
+ resolve: (cookieLang?: string, headerLang?: string): string =>
465
+ (i18n as any).resolveRequestLang(cookieLang, headerLang),
466
+ };
467
+ };
468
+
469
+ class App {
470
+ en = $dictionary({
471
+ lazy: async () => ({ default: { hello: "Hello" } }),
472
+ });
473
+
474
+ fr = $dictionary({
475
+ lazy: async () => ({ default: { hello: "Bonjour" } }),
476
+ });
477
+ }
478
+
479
+ test("uses the Accept-Language header when no cookie is set", ({
480
+ expect,
481
+ }) => {
482
+ expect(setup(App).resolve(undefined, "fr")).toBe("fr");
483
+ });
484
+
485
+ test("lets a manually-selected cookie win over the header", ({
486
+ expect,
487
+ }) => {
488
+ expect(setup(App).resolve("en", "fr")).toBe("en");
489
+ });
490
+
491
+ test("falls back when the header language is not registered", ({
492
+ expect,
493
+ }) => {
494
+ expect(setup(App).resolve(undefined, "de")).toBe("en");
495
+ });
496
+
497
+ test("falls back when neither cookie nor header is present", ({
498
+ expect,
499
+ }) => {
500
+ expect(setup(App).resolve(undefined, undefined)).toBe("en");
501
+ });
502
+
503
+ test("normalizes a region-qualified header to a registered base language", ({
504
+ expect,
505
+ }) => {
506
+ expect(setup(App).resolve(undefined, "fr-FR")).toBe("fr");
507
+ });
508
+
509
+ test("prefers an exact region match when one is registered", ({
510
+ expect,
511
+ }) => {
512
+ class RegionApp {
513
+ enUS = $dictionary({
514
+ lang: "en-US",
515
+ lazy: async () => ({ default: { color: "color" } }),
516
+ });
517
+
518
+ fr = $dictionary({
519
+ lazy: async () => ({ default: { color: "couleur" } }),
520
+ });
521
+ }
522
+
523
+ expect(setup(RegionApp).resolve(undefined, "en-US")).toBe("en-US");
524
+ });
525
+
526
+ test("ignores the header when autoDetect is disabled", ({ expect }) => {
527
+ const { i18n, resolve } = setup(App);
528
+ i18n.options.autoDetect = false;
529
+ expect(resolve(undefined, "fr")).toBe("en");
530
+ });
531
+
532
+ test("still honours a manual cookie when autoDetect is disabled", ({
533
+ expect,
534
+ }) => {
535
+ const { i18n, resolve } = setup(App);
536
+ i18n.options.autoDetect = false;
537
+ expect(resolve("fr", "en")).toBe("fr");
538
+ });
539
+ });
540
+
452
541
  test("should handle partial interpolation args", async ({ expect }) => {
453
542
  class App {
454
543
  en = $dictionary({