alepha 0.20.5 → 0.20.7

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 (367) hide show
  1. package/AGENTS.md +0 -1
  2. package/CLAUDE.md +0 -1
  3. package/assets/agents-template.md +0 -1
  4. package/dist/api/audits/index.browser.js +1 -0
  5. package/dist/api/audits/index.browser.js.map +1 -1
  6. package/dist/api/audits/index.d.ts +701 -654
  7. package/dist/api/audits/index.d.ts.map +1 -1
  8. package/dist/api/audits/index.js +24 -1
  9. package/dist/api/audits/index.js.map +1 -1
  10. package/dist/api/files/index.browser.js +1 -0
  11. package/dist/api/files/index.browser.js.map +1 -1
  12. package/dist/api/files/index.d.ts +193 -166
  13. package/dist/api/files/index.d.ts.map +1 -1
  14. package/dist/api/files/index.js +52 -0
  15. package/dist/api/files/index.js.map +1 -1
  16. package/dist/api/jobs/index.browser.js +40 -14
  17. package/dist/api/jobs/index.browser.js.map +1 -1
  18. package/dist/api/jobs/index.d.ts +639 -333
  19. package/dist/api/jobs/index.d.ts.map +1 -1
  20. package/dist/api/jobs/index.js +495 -162
  21. package/dist/api/jobs/index.js.map +1 -1
  22. package/dist/api/keys/index.d.ts +222 -188
  23. package/dist/api/keys/index.d.ts.map +1 -1
  24. package/dist/api/keys/index.js +54 -0
  25. package/dist/api/keys/index.js.map +1 -1
  26. package/dist/api/notifications/index.d.ts +265 -236
  27. package/dist/api/notifications/index.d.ts.map +1 -1
  28. package/dist/api/notifications/index.js +55 -13
  29. package/dist/api/notifications/index.js.map +1 -1
  30. package/dist/api/organizations/index.d.ts +100 -97
  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.d.ts +332 -314
  34. package/dist/api/parameters/index.d.ts.map +1 -1
  35. package/dist/api/parameters/index.js +37 -0
  36. package/dist/api/parameters/index.js.map +1 -1
  37. package/dist/api/payments/index.d.ts +431 -376
  38. package/dist/api/payments/index.d.ts.map +1 -1
  39. package/dist/api/payments/index.js +202 -87
  40. package/dist/api/payments/index.js.map +1 -1
  41. package/dist/api/subscriptions/index.d.ts +1695 -0
  42. package/dist/api/subscriptions/index.d.ts.map +1 -0
  43. package/dist/api/subscriptions/index.js +1919 -0
  44. package/dist/api/subscriptions/index.js.map +1 -0
  45. package/dist/api/users/index.d.ts +1001 -844
  46. package/dist/api/users/index.d.ts.map +1 -1
  47. package/dist/api/users/index.js +237 -28
  48. package/dist/api/users/index.js.map +1 -1
  49. package/dist/api/verifications/index.d.ts +123 -122
  50. package/dist/api/verifications/index.d.ts.map +1 -1
  51. package/dist/api/verifications/index.js.map +1 -1
  52. package/dist/batch/index.js.map +1 -1
  53. package/dist/bucket/index.d.ts +21 -2
  54. package/dist/bucket/index.d.ts.map +1 -1
  55. package/dist/bucket/index.js +47 -0
  56. package/dist/bucket/index.js.map +1 -1
  57. package/dist/bucket/index.workerd.js +24 -0
  58. package/dist/bucket/index.workerd.js.map +1 -1
  59. package/dist/cache/core/index.d.ts +134 -7
  60. package/dist/cache/core/index.d.ts.map +1 -1
  61. package/dist/cache/core/index.js +181 -15
  62. package/dist/cache/core/index.js.map +1 -1
  63. package/dist/cache/core/index.workerd.js +181 -15
  64. package/dist/cache/core/index.workerd.js.map +1 -1
  65. package/dist/cache/database/index.d.ts +156 -0
  66. package/dist/cache/database/index.d.ts.map +1 -0
  67. package/dist/cache/database/index.js +266 -0
  68. package/dist/cache/database/index.js.map +1 -0
  69. package/dist/cache/redis/index.d.ts +3 -2
  70. package/dist/cache/redis/index.d.ts.map +1 -1
  71. package/dist/cache/redis/index.js.map +1 -1
  72. package/dist/captcha/index.js.map +1 -1
  73. package/dist/cli/config/index.js.map +1 -1
  74. package/dist/cli/core/index.d.ts +142 -128
  75. package/dist/cli/core/index.d.ts.map +1 -1
  76. package/dist/cli/core/index.js +160 -13
  77. package/dist/cli/core/index.js.map +1 -1
  78. package/dist/cli/devtools/index.d.ts +3 -2
  79. package/dist/cli/devtools/index.d.ts.map +1 -1
  80. package/dist/cli/devtools/index.js.map +1 -1
  81. package/dist/cli/platform/index.d.ts +346 -290
  82. package/dist/cli/platform/index.d.ts.map +1 -1
  83. package/dist/cli/platform/index.js +106 -7
  84. package/dist/cli/platform/index.js.map +1 -1
  85. package/dist/cli/vendor/index.d.ts +12 -11
  86. package/dist/cli/vendor/index.d.ts.map +1 -1
  87. package/dist/cli/vendor/index.js.map +1 -1
  88. package/dist/command/index.d.ts +6 -5
  89. package/dist/command/index.d.ts.map +1 -1
  90. package/dist/command/index.js.map +1 -1
  91. package/dist/core/index.browser.js +1 -1
  92. package/dist/core/index.browser.js.map +1 -1
  93. package/dist/core/index.d.ts +119 -118
  94. package/dist/core/index.d.ts.map +1 -1
  95. package/dist/core/index.js +1 -1
  96. package/dist/core/index.js.map +1 -1
  97. package/dist/core/index.native.js +1 -1
  98. package/dist/core/index.native.js.map +1 -1
  99. package/dist/core/index.workerd.js +1 -1
  100. package/dist/core/index.workerd.js.map +1 -1
  101. package/dist/crypto/index.browser.js.map +1 -1
  102. package/dist/crypto/index.d.ts +3 -2
  103. package/dist/crypto/index.d.ts.map +1 -1
  104. package/dist/crypto/index.js.map +1 -1
  105. package/dist/datetime/index.js.map +1 -1
  106. package/dist/email/brevo/index.js.map +1 -1
  107. package/dist/email/core/index.d.ts +3 -2
  108. package/dist/email/core/index.d.ts.map +1 -1
  109. package/dist/email/core/index.js.map +1 -1
  110. package/dist/email/core/index.workerd.js.map +1 -1
  111. package/dist/email/smtp/index.d.ts +7 -6
  112. package/dist/email/smtp/index.d.ts.map +1 -1
  113. package/dist/email/smtp/index.js.map +1 -1
  114. package/dist/fake/index.js.map +1 -1
  115. package/dist/lock/core/index.d.ts +5 -4
  116. package/dist/lock/core/index.d.ts.map +1 -1
  117. package/dist/lock/core/index.js.map +1 -1
  118. package/dist/lock/redis/index.js.map +1 -1
  119. package/dist/logger/index.d.ts +10 -9
  120. package/dist/logger/index.d.ts.map +1 -1
  121. package/dist/logger/index.js.map +1 -1
  122. package/dist/mcp/index.d.ts +9 -8
  123. package/dist/mcp/index.d.ts.map +1 -1
  124. package/dist/mcp/index.js +1 -1
  125. package/dist/mcp/index.js.map +1 -1
  126. package/dist/orm/core/index.browser.js +9 -3
  127. package/dist/orm/core/index.browser.js.map +1 -1
  128. package/dist/orm/core/index.bun.js +31 -10
  129. package/dist/orm/core/index.bun.js.map +1 -1
  130. package/dist/orm/core/index.d.ts +33 -14
  131. package/dist/orm/core/index.d.ts.map +1 -1
  132. package/dist/orm/core/index.js +31 -10
  133. package/dist/orm/core/index.js.map +1 -1
  134. package/dist/orm/postgres/index.bun.js.map +1 -1
  135. package/dist/orm/postgres/index.d.ts +6 -5
  136. package/dist/orm/postgres/index.d.ts.map +1 -1
  137. package/dist/orm/postgres/index.js.map +1 -1
  138. package/dist/queue/core/index.d.ts +5 -4
  139. package/dist/queue/core/index.d.ts.map +1 -1
  140. package/dist/queue/core/index.js.map +1 -1
  141. package/dist/queue/core/index.workerd.js.map +1 -1
  142. package/dist/queue/redis/index.d.ts +3 -2
  143. package/dist/queue/redis/index.d.ts.map +1 -1
  144. package/dist/queue/redis/index.js.map +1 -1
  145. package/dist/react/auth/index.browser.js.map +1 -1
  146. package/dist/react/auth/index.js.map +1 -1
  147. package/dist/react/core/index.js.map +1 -1
  148. package/dist/react/form/index.d.ts +5 -0
  149. package/dist/react/form/index.d.ts.map +1 -1
  150. package/dist/react/form/index.js +8 -4
  151. package/dist/react/form/index.js.map +1 -1
  152. package/dist/react/head/index.browser.js.map +1 -1
  153. package/dist/react/head/index.js.map +1 -1
  154. package/dist/react/i18n/index.d.ts +2 -1
  155. package/dist/react/i18n/index.d.ts.map +1 -1
  156. package/dist/react/i18n/index.js.map +1 -1
  157. package/dist/react/intro/index.js.map +1 -1
  158. package/dist/react/router/index.browser.js.map +1 -1
  159. package/dist/react/router/index.d.ts +206 -205
  160. package/dist/react/router/index.d.ts.map +1 -1
  161. package/dist/react/router/index.js.map +1 -1
  162. package/dist/react/testing/index.js.map +1 -1
  163. package/dist/react/ui/index.d.ts +11 -11
  164. package/dist/react/ui/index.d.ts.map +1 -1
  165. package/dist/react/ui/index.js.map +1 -1
  166. package/dist/redis/index.bun.js.map +1 -1
  167. package/dist/redis/index.js.map +1 -1
  168. package/dist/retry/index.js.map +1 -1
  169. package/dist/router/index.js.map +1 -1
  170. package/dist/scheduler/index.d.ts +25 -2
  171. package/dist/scheduler/index.d.ts.map +1 -1
  172. package/dist/scheduler/index.js +12 -0
  173. package/dist/scheduler/index.js.map +1 -1
  174. package/dist/scheduler/index.workerd.js +12 -0
  175. package/dist/scheduler/index.workerd.js.map +1 -1
  176. package/dist/security/index.browser.js +29 -1
  177. package/dist/security/index.browser.js.map +1 -1
  178. package/dist/security/index.d.ts +82 -35
  179. package/dist/security/index.d.ts.map +1 -1
  180. package/dist/security/index.js +56 -3
  181. package/dist/security/index.js.map +1 -1
  182. package/dist/server/auth/index.d.ts +163 -158
  183. package/dist/server/auth/index.d.ts.map +1 -1
  184. package/dist/server/auth/index.js +16 -4
  185. package/dist/server/auth/index.js.map +1 -1
  186. package/dist/server/cookies/index.browser.js.map +1 -1
  187. package/dist/server/cookies/index.js.map +1 -1
  188. package/dist/server/core/index.browser.js.map +1 -1
  189. package/dist/server/core/index.d.ts +35 -34
  190. package/dist/server/core/index.d.ts.map +1 -1
  191. package/dist/server/core/index.js.map +1 -1
  192. package/dist/server/cors/index.d.ts +7 -6
  193. package/dist/server/cors/index.d.ts.map +1 -1
  194. package/dist/server/cors/index.js.map +1 -1
  195. package/dist/server/etag/index.js.map +1 -1
  196. package/dist/server/health/index.d.ts +16 -15
  197. package/dist/server/health/index.d.ts.map +1 -1
  198. package/dist/server/health/index.js.map +1 -1
  199. package/dist/server/links/index.browser.js.map +1 -1
  200. package/dist/server/links/index.d.ts +51 -50
  201. package/dist/server/links/index.d.ts.map +1 -1
  202. package/dist/server/links/index.js.map +1 -1
  203. package/dist/server/metrics/index.js.map +1 -1
  204. package/dist/server/proxy/index.js.map +1 -1
  205. package/dist/server/rate-limit/index.d.ts +6 -5
  206. package/dist/server/rate-limit/index.d.ts.map +1 -1
  207. package/dist/server/rate-limit/index.js.map +1 -1
  208. package/dist/server/static/index.js.map +1 -1
  209. package/dist/server/swagger/index.d.ts +2 -1
  210. package/dist/server/swagger/index.d.ts.map +1 -1
  211. package/dist/server/swagger/index.js.map +1 -1
  212. package/dist/sms/index.js.map +1 -1
  213. package/dist/system/index.browser.js.map +1 -1
  214. package/dist/system/index.js.map +1 -1
  215. package/dist/system/index.workerd.js.map +1 -1
  216. package/dist/topic/core/index.js.map +1 -1
  217. package/dist/topic/redis/index.d.ts +3 -2
  218. package/dist/topic/redis/index.d.ts.map +1 -1
  219. package/dist/topic/redis/index.js.map +1 -1
  220. package/package.json +33 -39
  221. package/src/api/audits/controllers/AdminAuditController.ts +29 -0
  222. package/src/api/audits/entities/audits.ts +1 -0
  223. package/src/api/files/controllers/FileController.ts +24 -0
  224. package/src/api/files/entities/files.ts +1 -0
  225. package/src/api/files/services/FileService.ts +41 -0
  226. package/src/api/jobs/__tests__/$job.spec.ts +501 -24
  227. package/src/api/jobs/entities/jobExecutionEntity.ts +4 -3
  228. package/src/api/jobs/index.ts +47 -10
  229. package/src/api/jobs/primitives/$job.ts +22 -9
  230. package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
  231. package/src/api/jobs/providers/JobDispatcher.ts +49 -0
  232. package/src/api/jobs/providers/JobProvider.ts +385 -147
  233. package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
  234. package/src/api/jobs/schemas/jobConfigAtom.ts +9 -3
  235. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
  236. package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
  237. package/src/api/jobs/services/JobService.ts +21 -11
  238. package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
  239. package/src/api/keys/entities/apiKeyEntity.ts +1 -0
  240. package/src/api/keys/services/ApiKeyService.ts +42 -0
  241. package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
  242. package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
  243. package/src/api/notifications/index.ts +13 -3
  244. package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
  245. package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
  246. package/src/api/parameters/services/ParameterProvider.ts +18 -0
  247. package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
  248. package/src/api/payments/index.ts +3 -0
  249. package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
  250. package/src/api/payments/providers/PaymentProvider.ts +25 -9
  251. package/src/api/payments/services/PaymentService.ts +3 -0
  252. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  253. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  254. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  255. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  256. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  257. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  258. package/src/api/subscriptions/index.ts +133 -0
  259. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  260. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  261. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  262. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  263. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  264. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  265. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  266. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  267. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  268. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  269. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  270. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  271. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  272. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  273. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  274. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  275. package/src/api/subscriptions/services/BillingService.ts +437 -0
  276. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  277. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  278. package/src/api/subscriptions/services/UsageService.ts +118 -0
  279. package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
  280. package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
  281. package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
  282. package/src/api/users/controllers/AdminSessionController.ts +29 -0
  283. package/src/api/users/controllers/AdminUserController.ts +32 -0
  284. package/src/api/users/index.ts +3 -0
  285. package/src/api/users/services/CredentialService.ts +5 -0
  286. package/src/api/users/services/RegistrationService.ts +49 -1
  287. package/src/api/users/services/SessionCrudService.ts +16 -0
  288. package/src/api/users/services/SessionService.ts +17 -59
  289. package/src/api/users/services/UsernameSlugger.ts +195 -0
  290. package/src/bucket/primitives/$bucket.ts +21 -0
  291. package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
  292. package/src/bucket/providers/FileStorageProvider.ts +9 -0
  293. package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
  294. package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
  295. package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
  296. package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
  297. package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
  298. package/src/cache/core/index.ts +16 -0
  299. package/src/cache/core/primitives/$cache.ts +367 -24
  300. package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
  301. package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
  302. package/src/cache/database/entities/cacheEntries.ts +55 -0
  303. package/src/cache/database/index.ts +36 -0
  304. package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
  305. package/src/cli/core/services/ProjectScaffolder.ts +0 -2
  306. package/src/cli/core/tasks/BuildCloudflareTask.ts +33 -3
  307. package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
  308. package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
  309. package/src/cli/core/templates/agentMd.ts +39 -4
  310. package/src/cli/core/templates/biomeJson.ts +25 -1
  311. package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
  312. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
  313. package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
  314. package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
  315. package/src/cli/platform/atoms/platformOptions.ts +13 -0
  316. package/src/cli/platform/commands/platform.ts +7 -1
  317. package/src/cli/platform/schemas/platform.ts +1 -0
  318. package/src/cli/platform/services/CloudflareApi.ts +61 -0
  319. package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
  320. package/src/core/__tests__/$module.spec.ts +2 -2
  321. package/src/core/primitives/$module.ts +4 -4
  322. package/src/mcp/providers/McpServerProvider.ts +1 -1
  323. package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
  324. package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
  325. package/src/orm/core/schemas/insertSchema.ts +10 -2
  326. package/src/orm/core/services/Repository.ts +27 -7
  327. package/src/react/form/hooks/useFormState.ts +8 -1
  328. package/src/react/form/index.ts +10 -1
  329. package/src/react/form/services/FormModel.ts +9 -3
  330. package/src/scheduler/index.ts +14 -0
  331. package/src/scheduler/providers/CronProvider.ts +13 -0
  332. package/src/security/atoms/currentTenantAtom.ts +34 -0
  333. package/src/security/index.browser.ts +1 -0
  334. package/src/security/index.ts +12 -1
  335. package/src/security/primitives/$issuer.ts +17 -1
  336. package/src/security/providers/SecurityProvider.ts +37 -0
  337. package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
  338. package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
  339. package/tsconfig.base.json +2 -1
  340. package/dist/react/websocket/index.d.ts +0 -117
  341. package/dist/react/websocket/index.d.ts.map +0 -1
  342. package/dist/react/websocket/index.js +0 -108
  343. package/dist/react/websocket/index.js.map +0 -1
  344. package/dist/websocket/index.browser.js +0 -844
  345. package/dist/websocket/index.browser.js.map +0 -1
  346. package/dist/websocket/index.d.ts +0 -876
  347. package/dist/websocket/index.d.ts.map +0 -1
  348. package/dist/websocket/index.js +0 -1175
  349. package/dist/websocket/index.js.map +0 -1
  350. package/src/react/websocket/hooks/useRoom.tsx +0 -251
  351. package/src/react/websocket/index.ts +0 -7
  352. package/src/websocket/__tests__/$channel.spec.ts +0 -30
  353. package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
  354. package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
  355. package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
  356. package/src/websocket/errors/WebSocketError.ts +0 -34
  357. package/src/websocket/index.browser.ts +0 -25
  358. package/src/websocket/index.shared.ts +0 -8
  359. package/src/websocket/index.ts +0 -85
  360. package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
  361. package/src/websocket/primitives/$channel.ts +0 -131
  362. package/src/websocket/primitives/$websocket.ts +0 -107
  363. package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
  364. package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
  365. package/src/websocket/services/RoomManager.ts +0 -160
  366. package/src/websocket/services/WebSocketClient.ts +0 -642
  367. package/src/websocket/services/WebSocketTopicService.ts +0 -108
@@ -0,0 +1,118 @@
1
+ import { $inject } from "alepha";
2
+ import { CacheProvider } from "alepha/cache";
3
+ import { DateTimeProvider } from "alepha/datetime";
4
+ import { SubscriptionService } from "./SubscriptionService.ts";
5
+
6
+ // -----------------------------------------------------------------------------------------------------------------
7
+
8
+ /**
9
+ * The result of a usage check or increment operation.
10
+ */
11
+ export interface UsageResult {
12
+ /**
13
+ * Whether the operation is allowed given the current usage and limit.
14
+ */
15
+ allowed: boolean;
16
+
17
+ /**
18
+ * Current usage count for the period.
19
+ */
20
+ current: number;
21
+
22
+ /**
23
+ * The plan limit for the resource. -1 means unlimited.
24
+ */
25
+ limit: number;
26
+
27
+ /**
28
+ * Remaining capacity. -1 means unlimited.
29
+ */
30
+ remaining: number;
31
+ }
32
+
33
+ // -----------------------------------------------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Tracks and enforces per-organization resource usage limits.
37
+ *
38
+ * Usage counters are keyed by `organizationId:resource:YYYY-MM` and stored in the cache.
39
+ * Limits are resolved from the organization's current subscription plan.
40
+ */
41
+ export class UsageService {
42
+ protected readonly cache = $inject(CacheProvider);
43
+ protected readonly dateTime = $inject(DateTimeProvider);
44
+ protected readonly subscriptionService = $inject(SubscriptionService);
45
+
46
+ /**
47
+ * Increment a resource counter for the current period and return the usage result.
48
+ *
49
+ * @param organizationId The organization to track usage for.
50
+ * @param resource The resource identifier (e.g., "api_calls", "seats").
51
+ * @param amount Amount to increment by (default: 1).
52
+ */
53
+ public async increment(
54
+ organizationId: string,
55
+ resource: string,
56
+ amount = 1,
57
+ ): Promise<UsageResult> {
58
+ const limit = await this.subscriptionService.limit(
59
+ organizationId,
60
+ resource,
61
+ );
62
+ const key = this.buildKey(organizationId, resource);
63
+ const current = await this.cache.incr("subscriptions:usage", key, amount);
64
+ const allowed = limit === -1 || current <= limit;
65
+ return {
66
+ allowed,
67
+ current,
68
+ limit,
69
+ remaining: limit === -1 ? -1 : Math.max(0, limit - current),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Get the current usage for a resource without incrementing.
75
+ *
76
+ * @param organizationId The organization to query usage for.
77
+ * @param resource The resource identifier.
78
+ */
79
+ public async getUsage(
80
+ organizationId: string,
81
+ resource: string,
82
+ ): Promise<UsageResult> {
83
+ const limit = await this.subscriptionService.limit(
84
+ organizationId,
85
+ resource,
86
+ );
87
+ const key = this.buildKey(organizationId, resource);
88
+ const current = await this.cache.incr("subscriptions:usage", key, 0);
89
+ return {
90
+ allowed: limit === -1 || current <= limit,
91
+ current,
92
+ limit,
93
+ remaining: limit === -1 ? -1 : Math.max(0, limit - current),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Reset all usage counters for an organization.
99
+ *
100
+ * Used at the start of a new billing period.
101
+ *
102
+ * @param organizationId The organization whose counters to reset.
103
+ */
104
+ public async resetForPeriod(organizationId: string): Promise<void> {
105
+ const pattern = this.buildKey(organizationId, "*");
106
+ await this.cache.invalidateKeys("subscriptions:usage", [pattern]);
107
+ }
108
+
109
+ /**
110
+ * Build the cache key for a usage counter.
111
+ *
112
+ * Format: `organizationId:resource:YYYY-MM`
113
+ */
114
+ protected buildKey(organizationId: string, resource: string): string {
115
+ const period = this.dateTime.now().format("YYYY-MM");
116
+ return `${organizationId}:${resource}:${period}`;
117
+ }
118
+ }
@@ -0,0 +1,203 @@
1
+ import { Alepha } from "alepha";
2
+ import { AlephaApiVerification } from "alepha/api/verifications";
3
+ import { AlephaEmail, MemoryEmailProvider } from "alepha/email";
4
+ import { AlephaOrmPostgres } from "alepha/orm/postgres";
5
+ import { AlephaSecurity } from "alepha/security";
6
+ import { BadRequestError } from "alepha/server";
7
+ import { describe, expect, it } from "vitest";
8
+ import {
9
+ AlephaApiUsers,
10
+ RealmProvider,
11
+ RegistrationService,
12
+ UserNotifications,
13
+ UserService,
14
+ } from "../index.ts";
15
+
16
+ const setup = async (
17
+ realmName: string,
18
+ realmSettings?: Record<string, unknown>,
19
+ ) => {
20
+ const alepha = Alepha.create();
21
+ alepha.with(AlephaOrmPostgres);
22
+ alepha.with(AlephaSecurity);
23
+ alepha.with(AlephaEmail);
24
+ alepha.with(AlephaApiVerification);
25
+ alepha.with(AlephaApiUsers);
26
+ alepha.with(UserNotifications);
27
+ await alepha.start();
28
+
29
+ const realmProvider = alepha.inject(RealmProvider);
30
+ if (realmSettings) {
31
+ realmProvider.register(realmName, {
32
+ settings: realmSettings as never,
33
+ });
34
+ }
35
+
36
+ // Wipe between cases.
37
+ await realmProvider.userRepository(realmName).deleteMany({});
38
+ alepha.inject(MemoryEmailProvider).records = [];
39
+
40
+ return {
41
+ alepha,
42
+ registrationService: alepha.inject(RegistrationService),
43
+ userService: alepha.inject(UserService),
44
+ realmProvider,
45
+ };
46
+ };
47
+
48
+ // ---------------------------------------------------------------------------------------------------------------------
49
+
50
+ describe("RegistrationService — username: 'email' mode", () => {
51
+ it("derives the username from the email and ignores any client-sent value", async () => {
52
+ const { registrationService, userService } = await setup("email-mode-1", {
53
+ username: "email",
54
+ });
55
+
56
+ const intent = await registrationService.createRegistrationIntent(
57
+ {
58
+ email: "ni.foures+testkv@gmail.com",
59
+ password: "SecurePassword123!",
60
+ // The client sneaks a custom username into the request — server
61
+ // must drop it on the floor and use the slugger.
62
+ username: "i-am-the-admin" as never,
63
+ },
64
+ "email-mode-1",
65
+ );
66
+
67
+ const user = await registrationService.completeRegistration({
68
+ intentId: intent.intentId,
69
+ });
70
+
71
+ // Slug rule: gmail "+suffix" preserved, dots → "-".
72
+ expect(user.username).toBe("ni-foures-testkv");
73
+
74
+ const reloaded = await userService
75
+ .users("email-mode-1")
76
+ .findOne({ where: { email: { eq: "ni.foures+testkv@gmail.com" } } });
77
+ expect(reloaded?.username).toBe("ni-foures-testkv");
78
+ });
79
+
80
+ it("appends a 4-char random suffix on collision instead of failing", async () => {
81
+ const { registrationService, userService } = await setup("email-mode-2", {
82
+ username: "email",
83
+ });
84
+
85
+ // First user takes the slug.
86
+ await userService.users("email-mode-2").create({
87
+ realm: "email-mode-2",
88
+ username: "alice",
89
+ email: "preexisting@example.com",
90
+ roles: ["user"],
91
+ });
92
+
93
+ const intent = await registrationService.createRegistrationIntent(
94
+ {
95
+ email: "alice@example.com",
96
+ password: "SecurePassword123!",
97
+ },
98
+ "email-mode-2",
99
+ );
100
+ const created = await registrationService.completeRegistration({
101
+ intentId: intent.intentId,
102
+ });
103
+
104
+ expect(created.username).toMatch(/^alice-[a-z0-9]{4}$/);
105
+ });
106
+
107
+ it("rejects email-mode when no email is provided", async () => {
108
+ const { registrationService } = await setup("email-mode-3", {
109
+ username: "email",
110
+ email: "optional",
111
+ });
112
+
113
+ await expect(
114
+ registrationService.createRegistrationIntent(
115
+ {
116
+ password: "SecurePassword123!",
117
+ },
118
+ "email-mode-3",
119
+ ),
120
+ ).rejects.toThrowError(BadRequestError);
121
+ });
122
+
123
+ it("blocklist applies even though the client never sees the field", async () => {
124
+ const { registrationService } = await setup("email-mode-blocklist", {
125
+ username: "email",
126
+ usernameBlocklist: ["admin"],
127
+ });
128
+
129
+ const intent = await registrationService.createRegistrationIntent(
130
+ {
131
+ email: "admin@example.com",
132
+ password: "SecurePassword123!",
133
+ },
134
+ "email-mode-blocklist",
135
+ );
136
+ const user = await registrationService.completeRegistration({
137
+ intentId: intent.intentId,
138
+ });
139
+
140
+ // Slug "admin" is blocked → falls through to the suffix path.
141
+ expect(user.username).not.toBe("admin");
142
+ expect(user.username).toMatch(/^admin-[a-z0-9]{4}$/);
143
+ });
144
+ });
145
+
146
+ // ---------------------------------------------------------------------------------------------------------------------
147
+
148
+ describe("RegistrationService — blocklist applies in 'required' mode too", () => {
149
+ it("rejects a manual username that hits the blocklist", async () => {
150
+ const { registrationService } = await setup("manual-mode", {
151
+ username: "required",
152
+ usernameBlocklist: ["admin"],
153
+ });
154
+
155
+ await expect(
156
+ registrationService.createRegistrationIntent(
157
+ {
158
+ username: "admin",
159
+ email: "someone@example.com",
160
+ password: "SecurePassword123!",
161
+ },
162
+ "manual-mode",
163
+ ),
164
+ ).rejects.toThrowError(BadRequestError);
165
+ });
166
+
167
+ it("blocklist match is case-insensitive in manual mode", async () => {
168
+ const { registrationService } = await setup("manual-case", {
169
+ username: "required",
170
+ usernameBlocklist: ["Admin"],
171
+ });
172
+
173
+ await expect(
174
+ registrationService.createRegistrationIntent(
175
+ {
176
+ username: "ADMIN",
177
+ email: "someone@example.com",
178
+ password: "SecurePassword123!",
179
+ },
180
+ "manual-case",
181
+ ),
182
+ ).rejects.toThrowError(BadRequestError);
183
+ });
184
+
185
+ it("default (empty) blocklist does not reject special names", async () => {
186
+ const { registrationService } = await setup("manual-empty", {
187
+ username: "required",
188
+ });
189
+
190
+ const intent = await registrationService.createRegistrationIntent(
191
+ {
192
+ username: "admin",
193
+ email: "admin-empty@example.com",
194
+ password: "SecurePassword123!",
195
+ },
196
+ "manual-empty",
197
+ );
198
+ const created = await registrationService.completeRegistration({
199
+ intentId: intent.intentId,
200
+ });
201
+ expect(created.username).toBe("admin");
202
+ });
203
+ });
@@ -0,0 +1,138 @@
1
+ import { Alepha } from "alepha";
2
+ import { AlephaOrmPostgres } from "alepha/orm/postgres";
3
+ import { AlephaSecurity } from "alepha/security";
4
+ import { describe, expect, it } from "vitest";
5
+ import { realmAuthSettingsAtom } from "../atoms/realmAuthSettingsAtom.ts";
6
+ import { AlephaApiUsers, RealmProvider, UsernameSlugger } from "../index.ts";
7
+
8
+ const setup = async (realmSettings?: Record<string, unknown>) => {
9
+ const alepha = Alepha.create();
10
+ alepha.with(AlephaOrmPostgres);
11
+ alepha.with(AlephaSecurity);
12
+ alepha.with(AlephaApiUsers);
13
+
14
+ await alepha.start();
15
+
16
+ if (realmSettings) {
17
+ alepha.inject(RealmProvider).register("default", {
18
+ settings: realmSettings as never,
19
+ });
20
+ }
21
+
22
+ // Wipe users table between cases so collision tests don't interfere.
23
+ const users = alepha.inject(RealmProvider).userRepository();
24
+ await users.deleteMany({});
25
+
26
+ return {
27
+ alepha,
28
+ slugger: alepha.inject(UsernameSlugger),
29
+ users,
30
+ };
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------------------------------------------------
34
+
35
+ describe("UsernameSlugger.slug — pure rule", () => {
36
+ it("strips the domain and the gmail '+suffix' is kept verbatim", async () => {
37
+ const { slugger } = await setup();
38
+ expect(slugger.slug("ni.foures+testkv@gmail.com")).toBe("ni-foures-testkv");
39
+ });
40
+
41
+ it("collapses runs of non-alphanumeric chars and trims edges", async () => {
42
+ const { slugger } = await setup();
43
+ expect(slugger.slug("john...doe@example.com")).toBe("john-doe");
44
+ expect(slugger.slug("---weird---name---@x.io")).toBe("weird-name");
45
+ });
46
+
47
+ it("lowercases the result", async () => {
48
+ const { slugger } = await setup();
49
+ expect(slugger.slug("Alice.SMITH@Example.com")).toBe("alice-smith");
50
+ });
51
+
52
+ it("drops every non-[a-z0-9] char (no NFD normalize)", async () => {
53
+ // `é` is dropped, so `josé` becomes `jos` — and then padded to MIN_LENGTH.
54
+ const { slugger } = await setup();
55
+ const result = slugger.slug("josé@example.com");
56
+ expect(result.startsWith("jos")).toBe(true);
57
+ expect(result.length).toBeGreaterThanOrEqual(UsernameSlugger.MIN_LENGTH);
58
+ });
59
+
60
+ it("pads short slugs with random alphanumerics", async () => {
61
+ const { slugger } = await setup();
62
+ const result = slugger.slug("a@example.com");
63
+ expect(result.startsWith("a")).toBe(true);
64
+ expect(result.length).toBeGreaterThanOrEqual(UsernameSlugger.MIN_LENGTH);
65
+ expect(result).toMatch(/^[a-z0-9-]+$/);
66
+ });
67
+
68
+ it("falls back to the EMPTY_LOCAL_FALLBACK when the local part has no [a-z0-9]", async () => {
69
+ const { slugger } = await setup();
70
+ expect(slugger.slug("é@example.com")).toBe(
71
+ UsernameSlugger.EMPTY_LOCAL_FALLBACK,
72
+ );
73
+ });
74
+
75
+ it("clamps to MAX_LENGTH", async () => {
76
+ const { slugger } = await setup();
77
+ const long = `${"a".repeat(60)}@example.com`;
78
+ expect(slugger.slug(long).length).toBe(UsernameSlugger.MAX_LENGTH);
79
+ });
80
+
81
+ it("treats null/empty input by returning the fallback", async () => {
82
+ const { slugger } = await setup();
83
+ expect(slugger.slug(null)).toBe(UsernameSlugger.EMPTY_LOCAL_FALLBACK);
84
+ expect(slugger.slug("")).toBe(UsernameSlugger.EMPTY_LOCAL_FALLBACK);
85
+ expect(slugger.slug(undefined)).toBe(UsernameSlugger.EMPTY_LOCAL_FALLBACK);
86
+ });
87
+ });
88
+
89
+ // ---------------------------------------------------------------------------------------------------------------------
90
+
91
+ describe("UsernameSlugger.pickAvailable — DB-backed availability + retry", () => {
92
+ it("returns the base when nothing collides", async () => {
93
+ const { slugger } = await setup();
94
+ const picked = await slugger.pickAvailable("default", "alice");
95
+ expect(picked).toBe("alice");
96
+ });
97
+
98
+ it("appends a 4-char random suffix when the base is taken", async () => {
99
+ const { slugger, users } = await setup();
100
+ await users.create({ realm: "default", username: "alice", email: "a@x" });
101
+
102
+ const picked = await slugger.pickAvailable("default", "alice");
103
+ expect(picked).toMatch(/^alice-[a-z0-9]{4}$/);
104
+ });
105
+
106
+ it("treats blocklisted candidates as collisions and falls through to the suffix path", async () => {
107
+ const { slugger } = await setup({ usernameBlocklist: ["admin", "root"] });
108
+
109
+ const picked = await slugger.pickAvailable("default", "admin");
110
+ expect(picked).toMatch(/^admin-[a-z0-9]{4}$/);
111
+ });
112
+
113
+ it("trims the base before adding the suffix when MAX_LENGTH would be exceeded", async () => {
114
+ const { slugger, users } = await setup();
115
+ const long = "a".repeat(UsernameSlugger.MAX_LENGTH);
116
+ await users.create({ realm: "default", username: long, email: "a@x" });
117
+
118
+ const picked = await slugger.pickAvailable("default", long);
119
+ expect(picked.length).toBeLessThanOrEqual(UsernameSlugger.MAX_LENGTH);
120
+ expect(picked).toMatch(/-[a-z0-9]{4}$/);
121
+ });
122
+
123
+ it("blocklist match is case-insensitive", async () => {
124
+ const { slugger } = await setup({ usernameBlocklist: ["Admin"] });
125
+ expect(await slugger.isBlocked("default", "admin")).toBe(true);
126
+ expect(await slugger.isBlocked("default", "ADMIN")).toBe(true);
127
+ expect(await slugger.isBlocked("default", "user")).toBe(false);
128
+ });
129
+
130
+ it("default blocklist is empty — no name is implicitly reserved", async () => {
131
+ const { alepha, slugger } = await setup();
132
+ const settings = alepha.get(realmAuthSettingsAtom);
133
+ expect(settings.usernameBlocklist).toEqual([]);
134
+
135
+ const picked = await slugger.pickAvailable("default", "admin");
136
+ expect(picked).toBe("admin");
137
+ });
138
+ });
@@ -14,6 +14,31 @@ const fieldRequirement = (description: string) =>
14
14
  description,
15
15
  });
16
16
 
17
+ /**
18
+ * Username-specific field requirement, extending {@link FieldRequirement}
19
+ * with an additional auto-derivation mode.
20
+ *
21
+ * - `"none"` / `"optional"` / `"required"`: same as {@link FieldRequirement}.
22
+ * - `"email"`: Field is hidden in the registration UI and the value is
23
+ * auto-derived from the user's email at signup. Same handling on
24
+ * credentials and OAuth flows. See `UsernameSlugger` for the rule and
25
+ * collision behavior.
26
+ */
27
+ export type UsernameFieldRequirement = FieldRequirement | "email";
28
+
29
+ const usernameFieldRequirement = (description: string) =>
30
+ t.union(
31
+ [
32
+ t.const("none"),
33
+ t.const("optional"),
34
+ t.const("required"),
35
+ t.const("email"),
36
+ ],
37
+ {
38
+ description,
39
+ },
40
+ );
41
+
17
42
  export const realmAuthSettingsAtom = $atom({
18
43
  name: "alepha.api.users.realmAuthSettings",
19
44
  schema: t.object({
@@ -42,11 +67,20 @@ export const realmAuthSettingsAtom = $atom({
42
67
  email: fieldRequirement(
43
68
  "Email address field requirement for user accounts",
44
69
  ),
45
- username: fieldRequirement("Username field requirement for user accounts"),
70
+ username: usernameFieldRequirement(
71
+ "Username field requirement for user accounts",
72
+ ),
46
73
  usernameRegExp: t.string({
47
74
  description:
48
75
  "Regular expression that usernames must match (if username is enabled)",
49
76
  }),
77
+ usernameBlocklist: t.array(t.text(), {
78
+ description:
79
+ "Usernames that the slugger / manual registration must reject. " +
80
+ "Default empty so apps can register `admin`/`root`/`me`/etc. if " +
81
+ "they want; populate it explicitly for handles you want to keep " +
82
+ "off-limits.",
83
+ }),
50
84
  phoneNumber: fieldRequirement(
51
85
  "Phone number field requirement for user accounts",
52
86
  ),
@@ -138,8 +172,12 @@ export const realmAuthSettingsAtom = $atom({
138
172
  // for a fresh hello world setup, we accept registration and email login
139
173
  registrationAllowed: true,
140
174
  email: "required" as FieldRequirement,
141
- username: "none" as FieldRequirement,
142
- usernameRegExp: "^[a-zA-Z0-9_]{3,30}$",
175
+ username: "none" as UsernameFieldRequirement,
176
+ // Allow hyphens by default so the UsernameSlugger output (`ni-foures-testkv`)
177
+ // matches without app overrides. Existing `[a-zA-Z0-9_]` usernames remain
178
+ // valid because the new pattern is a strict superset.
179
+ usernameRegExp: "^[a-zA-Z0-9_-]{3,30}$",
180
+ usernameBlocklist: [] as string[],
143
181
  phoneNumber: "none" as FieldRequirement,
144
182
  verifyEmailRequired: false,
145
183
  verifyPhoneRequired: false,
@@ -74,4 +74,33 @@ export class AdminSessionController {
74
74
  return { ok: true, id: params.id };
75
75
  },
76
76
  });
77
+
78
+ /**
79
+ * Delete many sessions in one repository call.
80
+ */
81
+ public readonly deleteSessions = $action({
82
+ method: "POST",
83
+ path: `${this.url}/delete`,
84
+ group: this.group,
85
+ use: [$secure({ permissions: ["admin:session:delete"] })],
86
+ description: "Delete many sessions",
87
+ schema: {
88
+ query: t.object({
89
+ userRealmName: t.optional(t.string()),
90
+ }),
91
+ body: t.object({
92
+ ids: t.array(t.uuid(), { minItems: 1, maxItems: 1000 }),
93
+ }),
94
+ response: t.object({
95
+ deleted: t.array(t.string()),
96
+ }),
97
+ },
98
+ handler: async ({ body, query }) => {
99
+ const deleted = await this.sessionService.deleteSessions(
100
+ body.ids,
101
+ query.userRealmName,
102
+ );
103
+ return { deleted };
104
+ },
105
+ });
77
106
  }
@@ -119,4 +119,36 @@ export class AdminUserController {
119
119
  return { ok: true, id: params.id };
120
120
  },
121
121
  });
122
+
123
+ /**
124
+ * Delete many users in one request. Each id is processed sequentially so
125
+ * cascades and side-effects run as if called one-by-one. Errors on a single
126
+ * id surface with that id in the response.
127
+ */
128
+ public readonly deleteUsers = $action({
129
+ method: "POST",
130
+ path: `${this.url}/delete`,
131
+ group: this.group,
132
+ use: [$secure({ permissions: ["admin:user:delete"] })],
133
+ description: "Delete many users",
134
+ schema: {
135
+ query: t.object({
136
+ userRealmName: t.optional(t.string()),
137
+ }),
138
+ body: t.object({
139
+ ids: t.array(t.uuid(), { minItems: 1, maxItems: 1000 }),
140
+ }),
141
+ response: t.object({
142
+ deleted: t.array(t.uuid()),
143
+ }),
144
+ },
145
+ handler: async ({ body, query }) => {
146
+ const deleted: string[] = [];
147
+ for (const id of body.ids) {
148
+ await this.userService.deleteUser(id, query.userRealmName);
149
+ deleted.push(id);
150
+ }
151
+ return { deleted };
152
+ },
153
+ });
122
154
  }
@@ -14,6 +14,7 @@ import { IdentityService } from "./services/IdentityService.ts";
14
14
  import { RegistrationService } from "./services/RegistrationService.ts";
15
15
  import { SessionCrudService } from "./services/SessionCrudService.ts";
16
16
  import { SessionService } from "./services/SessionService.ts";
17
+ import { UsernameSlugger } from "./services/UsernameSlugger.ts";
17
18
  import { UserService } from "./services/UserService.ts";
18
19
 
19
20
  // ---------------------------------------------------------------------------------------------------------------------
@@ -54,6 +55,7 @@ export * from "./services/IdentityService.ts";
54
55
  export * from "./services/RegistrationService.ts";
55
56
  export * from "./services/SessionCrudService.ts";
56
57
  export * from "./services/SessionService.ts";
58
+ export * from "./services/UsernameSlugger.ts";
57
59
  export * from "./services/UserService.ts";
58
60
 
59
61
  // ---------------------------------------------------------------------------------------------------------------------
@@ -82,6 +84,7 @@ export const AlephaApiUsers = $module({
82
84
  CredentialService,
83
85
  RegistrationService,
84
86
  UserService,
87
+ UsernameSlugger,
85
88
  IdentityService,
86
89
  UserController,
87
90
  AdminUserController,
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { $inject, Alepha } from "alepha";
3
3
  import type { VerificationController } from "alepha/api/verifications";
4
4
  import { $cache } from "alepha/cache";
5
+ import { DatabaseCacheProvider } from "alepha/cache/database";
5
6
  import { DateTimeProvider } from "alepha/datetime";
6
7
  import { $logger } from "alepha/logger";
7
8
  import { CryptoProvider } from "alepha/security";
@@ -52,6 +53,10 @@ export class CredentialService {
52
53
  }
53
54
 
54
55
  protected readonly intentCache = $cache<PasswordResetIntent>({
56
+ // Use the SQL-backed cache so phase-2 reads what phase-1 wrote with
57
+ // strong consistency, and so bare deployments don't need a distributed
58
+ // KV resource just to support password reset.
59
+ provider: DatabaseCacheProvider,
55
60
  name: "api:users:password-reset-intents",
56
61
  ttl: [INTENT_TTL_MINUTES, "minutes"],
57
62
  });