alepha 0.20.6 → 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 (243) 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 +370 -355
  7. package/dist/api/audits/index.d.ts.map +1 -1
  8. package/dist/api/audits/index.js +1 -0
  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 +179 -170
  13. package/dist/api/files/index.d.ts.map +1 -1
  14. package/dist/api/files/index.js +1 -0
  15. package/dist/api/files/index.js.map +1 -1
  16. package/dist/api/jobs/index.browser.js +7 -0
  17. package/dist/api/jobs/index.browser.js.map +1 -1
  18. package/dist/api/jobs/index.d.ts +271 -262
  19. package/dist/api/jobs/index.d.ts.map +1 -1
  20. package/dist/api/jobs/index.js +21 -3
  21. package/dist/api/jobs/index.js.map +1 -1
  22. package/dist/api/keys/index.d.ts +198 -192
  23. package/dist/api/keys/index.d.ts.map +1 -1
  24. package/dist/api/keys/index.js +1 -0
  25. package/dist/api/keys/index.js.map +1 -1
  26. package/dist/api/notifications/index.d.ts +246 -245
  27. package/dist/api/notifications/index.d.ts.map +1 -1
  28. package/dist/api/organizations/index.d.ts +100 -97
  29. package/dist/api/organizations/index.d.ts.map +1 -1
  30. package/dist/api/parameters/index.d.ts +323 -320
  31. package/dist/api/parameters/index.d.ts.map +1 -1
  32. package/dist/api/payments/index.d.ts +431 -376
  33. package/dist/api/payments/index.d.ts.map +1 -1
  34. package/dist/api/payments/index.js +202 -87
  35. package/dist/api/payments/index.js.map +1 -1
  36. package/dist/api/subscriptions/index.d.ts +1695 -0
  37. package/dist/api/subscriptions/index.d.ts.map +1 -0
  38. package/dist/api/subscriptions/index.js +1919 -0
  39. package/dist/api/subscriptions/index.js.map +1 -0
  40. package/dist/api/users/index.d.ts +863 -847
  41. package/dist/api/users/index.d.ts.map +1 -1
  42. package/dist/api/verifications/index.d.ts +126 -125
  43. package/dist/api/verifications/index.d.ts.map +1 -1
  44. package/dist/bucket/index.d.ts +3 -2
  45. package/dist/bucket/index.d.ts.map +1 -1
  46. package/dist/cache/core/index.d.ts +114 -4
  47. package/dist/cache/core/index.d.ts.map +1 -1
  48. package/dist/cache/core/index.js +181 -15
  49. package/dist/cache/core/index.js.map +1 -1
  50. package/dist/cache/core/index.workerd.js +181 -15
  51. package/dist/cache/core/index.workerd.js.map +1 -1
  52. package/dist/cache/database/index.d.ts +20 -19
  53. package/dist/cache/database/index.d.ts.map +1 -1
  54. package/dist/cache/redis/index.d.ts +3 -2
  55. package/dist/cache/redis/index.d.ts.map +1 -1
  56. package/dist/cli/core/index.d.ts +113 -129
  57. package/dist/cli/core/index.d.ts.map +1 -1
  58. package/dist/cli/core/index.js +75 -7
  59. package/dist/cli/core/index.js.map +1 -1
  60. package/dist/cli/devtools/index.d.ts +3 -2
  61. package/dist/cli/devtools/index.d.ts.map +1 -1
  62. package/dist/cli/platform/index.d.ts +346 -290
  63. package/dist/cli/platform/index.d.ts.map +1 -1
  64. package/dist/cli/platform/index.js +105 -6
  65. package/dist/cli/platform/index.js.map +1 -1
  66. package/dist/cli/vendor/index.d.ts +12 -11
  67. package/dist/cli/vendor/index.d.ts.map +1 -1
  68. package/dist/command/index.d.ts +5 -4
  69. package/dist/command/index.d.ts.map +1 -1
  70. package/dist/core/index.browser.js +1 -1
  71. package/dist/core/index.browser.js.map +1 -1
  72. package/dist/core/index.d.ts +119 -118
  73. package/dist/core/index.d.ts.map +1 -1
  74. package/dist/core/index.js +1 -1
  75. package/dist/core/index.js.map +1 -1
  76. package/dist/core/index.native.js +1 -1
  77. package/dist/core/index.native.js.map +1 -1
  78. package/dist/core/index.workerd.js +1 -1
  79. package/dist/core/index.workerd.js.map +1 -1
  80. package/dist/crypto/index.d.ts +3 -2
  81. package/dist/crypto/index.d.ts.map +1 -1
  82. package/dist/email/core/index.d.ts +3 -2
  83. package/dist/email/core/index.d.ts.map +1 -1
  84. package/dist/email/smtp/index.d.ts +7 -6
  85. package/dist/email/smtp/index.d.ts.map +1 -1
  86. package/dist/lock/core/index.d.ts +5 -4
  87. package/dist/lock/core/index.d.ts.map +1 -1
  88. package/dist/logger/index.d.ts +10 -9
  89. package/dist/logger/index.d.ts.map +1 -1
  90. package/dist/mcp/index.d.ts +9 -8
  91. package/dist/mcp/index.d.ts.map +1 -1
  92. package/dist/mcp/index.js +1 -1
  93. package/dist/mcp/index.js.map +1 -1
  94. package/dist/orm/core/index.browser.js +9 -3
  95. package/dist/orm/core/index.browser.js.map +1 -1
  96. package/dist/orm/core/index.bun.js +31 -10
  97. package/dist/orm/core/index.bun.js.map +1 -1
  98. package/dist/orm/core/index.d.ts +33 -14
  99. package/dist/orm/core/index.d.ts.map +1 -1
  100. package/dist/orm/core/index.js +31 -10
  101. package/dist/orm/core/index.js.map +1 -1
  102. package/dist/orm/postgres/index.d.ts +6 -5
  103. package/dist/orm/postgres/index.d.ts.map +1 -1
  104. package/dist/queue/core/index.d.ts +5 -4
  105. package/dist/queue/core/index.d.ts.map +1 -1
  106. package/dist/queue/redis/index.d.ts +3 -2
  107. package/dist/queue/redis/index.d.ts.map +1 -1
  108. package/dist/react/form/index.d.ts +5 -0
  109. package/dist/react/form/index.d.ts.map +1 -1
  110. package/dist/react/form/index.js +6 -4
  111. package/dist/react/form/index.js.map +1 -1
  112. package/dist/react/i18n/index.d.ts +2 -1
  113. package/dist/react/i18n/index.d.ts.map +1 -1
  114. package/dist/react/router/index.d.ts +206 -205
  115. package/dist/react/router/index.d.ts.map +1 -1
  116. package/dist/react/ui/index.d.ts +11 -11
  117. package/dist/react/ui/index.d.ts.map +1 -1
  118. package/dist/scheduler/index.d.ts +3 -2
  119. package/dist/scheduler/index.d.ts.map +1 -1
  120. package/dist/security/index.browser.js +29 -1
  121. package/dist/security/index.browser.js.map +1 -1
  122. package/dist/security/index.d.ts +82 -35
  123. package/dist/security/index.d.ts.map +1 -1
  124. package/dist/security/index.js +56 -3
  125. package/dist/security/index.js.map +1 -1
  126. package/dist/server/auth/index.d.ts +163 -158
  127. package/dist/server/auth/index.d.ts.map +1 -1
  128. package/dist/server/auth/index.js +16 -4
  129. package/dist/server/auth/index.js.map +1 -1
  130. package/dist/server/core/index.d.ts +35 -34
  131. package/dist/server/core/index.d.ts.map +1 -1
  132. package/dist/server/cors/index.d.ts +7 -6
  133. package/dist/server/cors/index.d.ts.map +1 -1
  134. package/dist/server/health/index.d.ts +16 -15
  135. package/dist/server/health/index.d.ts.map +1 -1
  136. package/dist/server/links/index.d.ts +51 -50
  137. package/dist/server/links/index.d.ts.map +1 -1
  138. package/dist/server/rate-limit/index.d.ts +6 -5
  139. package/dist/server/rate-limit/index.d.ts.map +1 -1
  140. package/dist/server/swagger/index.d.ts +2 -1
  141. package/dist/server/swagger/index.d.ts.map +1 -1
  142. package/dist/topic/redis/index.d.ts +3 -2
  143. package/dist/topic/redis/index.d.ts.map +1 -1
  144. package/package.json +16 -32
  145. package/src/api/audits/entities/audits.ts +1 -0
  146. package/src/api/files/entities/files.ts +1 -0
  147. package/src/api/jobs/__tests__/$job.spec.ts +92 -40
  148. package/src/api/jobs/entities/jobExecutionEntity.ts +1 -0
  149. package/src/api/jobs/providers/JobProvider.ts +20 -5
  150. package/src/api/jobs/schemas/jobConfigAtom.ts +5 -0
  151. package/src/api/keys/entities/apiKeyEntity.ts +1 -0
  152. package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
  153. package/src/api/payments/index.ts +3 -0
  154. package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
  155. package/src/api/payments/providers/PaymentProvider.ts +25 -9
  156. package/src/api/payments/services/PaymentService.ts +3 -0
  157. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  158. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  159. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  160. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  161. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  162. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  163. package/src/api/subscriptions/index.ts +133 -0
  164. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  165. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  166. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  167. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  168. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  169. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  170. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  171. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  172. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  173. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  174. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  175. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  176. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  177. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  178. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  179. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  180. package/src/api/subscriptions/services/BillingService.ts +437 -0
  181. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  182. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  183. package/src/api/subscriptions/services/UsageService.ts +118 -0
  184. package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
  185. package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
  186. package/src/cache/core/index.ts +16 -0
  187. package/src/cache/core/primitives/$cache.ts +347 -21
  188. package/src/cli/core/tasks/BuildCloudflareTask.ts +16 -0
  189. package/src/cli/core/templates/agentMd.ts +39 -4
  190. package/src/cli/core/templates/biomeJson.ts +25 -1
  191. package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
  192. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
  193. package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
  194. package/src/cli/platform/atoms/platformOptions.ts +13 -0
  195. package/src/cli/platform/schemas/platform.ts +1 -0
  196. package/src/cli/platform/services/CloudflareApi.ts +61 -0
  197. package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
  198. package/src/core/__tests__/$module.spec.ts +2 -2
  199. package/src/core/primitives/$module.ts +4 -4
  200. package/src/mcp/providers/McpServerProvider.ts +1 -1
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
  202. package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
  203. package/src/orm/core/schemas/insertSchema.ts +10 -2
  204. package/src/orm/core/services/Repository.ts +27 -7
  205. package/src/react/form/hooks/useFormState.ts +8 -1
  206. package/src/react/form/index.ts +10 -1
  207. package/src/react/form/services/FormModel.ts +9 -3
  208. package/src/security/atoms/currentTenantAtom.ts +34 -0
  209. package/src/security/index.browser.ts +1 -0
  210. package/src/security/index.ts +12 -1
  211. package/src/security/primitives/$issuer.ts +17 -1
  212. package/src/security/providers/SecurityProvider.ts +37 -0
  213. package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
  214. package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
  215. package/tsconfig.base.json +2 -1
  216. package/dist/react/websocket/index.d.ts +0 -117
  217. package/dist/react/websocket/index.d.ts.map +0 -1
  218. package/dist/react/websocket/index.js +0 -108
  219. package/dist/react/websocket/index.js.map +0 -1
  220. package/dist/websocket/index.browser.js +0 -848
  221. package/dist/websocket/index.browser.js.map +0 -1
  222. package/dist/websocket/index.d.ts +0 -876
  223. package/dist/websocket/index.d.ts.map +0 -1
  224. package/dist/websocket/index.js +0 -1185
  225. package/dist/websocket/index.js.map +0 -1
  226. package/src/react/websocket/hooks/useRoom.tsx +0 -251
  227. package/src/react/websocket/index.ts +0 -7
  228. package/src/websocket/__tests__/$channel.spec.ts +0 -30
  229. package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
  230. package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
  231. package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
  232. package/src/websocket/errors/WebSocketError.ts +0 -34
  233. package/src/websocket/index.browser.ts +0 -25
  234. package/src/websocket/index.shared.ts +0 -8
  235. package/src/websocket/index.ts +0 -85
  236. package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
  237. package/src/websocket/primitives/$channel.ts +0 -131
  238. package/src/websocket/primitives/$websocket.ts +0 -107
  239. package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
  240. package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
  241. package/src/websocket/services/RoomManager.ts +0 -160
  242. package/src/websocket/services/WebSocketClient.ts +0 -642
  243. package/src/websocket/services/WebSocketTopicService.ts +0 -108
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "alepha",
3
3
  "description": "Easy-to-use modern TypeScript framework for building many kind of applications.",
4
4
  "author": "Feunard",
5
- "version": "0.20.6",
5
+ "version": "0.20.7",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.0.0"
@@ -29,15 +29,14 @@
29
29
  "tsx": "^4.21.0",
30
30
  "typebox": "^1.1.38",
31
31
  "typescript": "^6.0.3",
32
- "vite-bundle-analyzer": "^1.3.8",
33
- "ws": "^8.20.0"
32
+ "vite-bundle-analyzer": "^1.3.8"
34
33
  },
35
34
  "devDependencies": {
36
- "@biomejs/biome": "^2.4.14",
37
- "@cloudflare/workers-types": "^4.20260508.1",
35
+ "@biomejs/biome": "^2.4.15",
36
+ "@cloudflare/workers-types": "^4.20260510.1",
38
37
  "@electric-sql/pglite": "^0.4.5",
39
38
  "@faker-js/faker": "^10.4.0",
40
- "@tailwindcss/vite": "^4.2.4",
39
+ "@tailwindcss/vite": "^4.3.0",
41
40
  "@testing-library/dom": "^10.4.1",
42
41
  "@testing-library/react": "^16.3.2",
43
42
  "@types/bun": "^1.3.13",
@@ -45,7 +44,6 @@
45
44
  "@types/nodemailer": "^8.0.0",
46
45
  "@types/react": "^19.2.14",
47
46
  "@types/react-dom": "^19.2.3",
48
- "@types/ws": "^8.18.1",
49
47
  "cron-schedule": "^6.0.0",
50
48
  "drizzle-kit": "^0.31.10",
51
49
  "jose": "^6.2.3",
@@ -57,7 +55,7 @@
57
55
  "react-dom": "^19.2.6",
58
56
  "shadcn": "^4.7.0",
59
57
  "swagger-ui-dist": "^5.32.5",
60
- "tailwindcss": "^4.2.4",
58
+ "tailwindcss": "^4.3.0",
61
59
  "tsdown": "^0.22.0",
62
60
  "vite": "^8.0.11",
63
61
  "vitest": "^4.1.5"
@@ -151,6 +149,11 @@
151
149
  "import": "./dist/api/payments/index.js",
152
150
  "default": "./dist/api/payments/index.js"
153
151
  },
152
+ "./api/subscriptions": {
153
+ "types": "./dist/api/subscriptions/index.d.ts",
154
+ "import": "./dist/api/subscriptions/index.js",
155
+ "default": "./dist/api/subscriptions/index.js"
156
+ },
154
157
  "./api/users": {
155
158
  "types": "./dist/api/users/index.d.ts",
156
159
  "react-native": "./dist/api/users/index.browser.js",
@@ -369,11 +372,6 @@
369
372
  "import": "./dist/react/ui/index.js",
370
373
  "default": "./dist/react/ui/index.js"
371
374
  },
372
- "./react/websocket": {
373
- "types": "./dist/react/websocket/index.d.ts",
374
- "import": "./dist/react/websocket/index.js",
375
- "default": "./dist/react/websocket/index.js"
376
- },
377
375
  "./redis": {
378
376
  "types": "./dist/redis/index.d.ts",
379
377
  "bun": "./dist/redis/index.bun.js",
@@ -494,13 +492,6 @@
494
492
  "import": "./dist/topic/redis/index.js",
495
493
  "default": "./dist/topic/redis/index.js"
496
494
  },
497
- "./websocket": {
498
- "types": "./dist/websocket/index.d.ts",
499
- "react-native": "./dist/websocket/index.browser.js",
500
- "browser": "./dist/websocket/index.browser.js",
501
- "import": "./dist/websocket/index.js",
502
- "default": "./dist/websocket/index.js"
503
- },
504
495
  "./tsconfig.base": "./tsconfig.base.json",
505
496
  "./package.json": "./package.json"
506
497
  },
@@ -561,6 +552,11 @@
561
552
  "import": "./dist/api/payments/index.js",
562
553
  "default": "./dist/api/payments/index.js"
563
554
  },
555
+ "./api/subscriptions": {
556
+ "types": "./dist/api/subscriptions/index.d.ts",
557
+ "import": "./dist/api/subscriptions/index.js",
558
+ "default": "./dist/api/subscriptions/index.js"
559
+ },
564
560
  "./api/users": {
565
561
  "types": "./dist/api/users/index.d.ts",
566
562
  "react-native": "./dist/api/users/index.browser.js",
@@ -779,11 +775,6 @@
779
775
  "import": "./dist/react/ui/index.js",
780
776
  "default": "./dist/react/ui/index.js"
781
777
  },
782
- "./react/websocket": {
783
- "types": "./dist/react/websocket/index.d.ts",
784
- "import": "./dist/react/websocket/index.js",
785
- "default": "./dist/react/websocket/index.js"
786
- },
787
778
  "./redis": {
788
779
  "types": "./dist/redis/index.d.ts",
789
780
  "bun": "./dist/redis/index.bun.js",
@@ -904,13 +895,6 @@
904
895
  "import": "./dist/topic/redis/index.js",
905
896
  "default": "./dist/topic/redis/index.js"
906
897
  },
907
- "./websocket": {
908
- "types": "./dist/websocket/index.d.ts",
909
- "react-native": "./dist/websocket/index.browser.js",
910
- "browser": "./dist/websocket/index.browser.js",
911
- "import": "./dist/websocket/index.js",
912
- "default": "./dist/websocket/index.js"
913
- },
914
898
  "./tsconfig.base": "./tsconfig.base.json",
915
899
  "./package.json": "./package.json"
916
900
  }
@@ -26,6 +26,7 @@ export const audits = $entity({
26
26
  schema: t.object({
27
27
  id: db.primaryKey(t.bigint()),
28
28
  createdAt: db.createdAt(),
29
+ organizationId: db.organization(),
29
30
 
30
31
  /**
31
32
  * Audit event type (e.g., "auth", "user", "payment", "system").
@@ -8,6 +8,7 @@ export const files = $entity({
8
8
  version: db.version(),
9
9
  createdAt: db.createdAt(),
10
10
  updatedAt: db.updatedAt(),
11
+ organizationId: db.organization(),
11
12
  blobId: t.text(),
12
13
  creator: t.optional(t.uuid()),
13
14
  creatorRealm: t.optional(t.string()),
@@ -21,6 +21,29 @@ const makeApp = () =>
21
21
  const makeAppDirect = () =>
22
22
  Alepha.create().with(AlephaOrmPostgres).with(AlephaApiJobs);
23
23
 
24
+ /**
25
+ * Poll `fn` until `predicate` returns true, or throw on timeout.
26
+ * Use this instead of `setTimeout(r, fixedMs)` — fixed sleeps race the
27
+ * in-memory queue under CI load and produce flaky failures.
28
+ */
29
+ async function waitFor<T>(
30
+ fn: () => Promise<T> | T,
31
+ predicate: (v: T) => boolean,
32
+ { timeout = 2000, interval = 10, label = "condition" } = {},
33
+ ): Promise<T> {
34
+ const deadline = Date.now() + timeout;
35
+ let last: T = await fn();
36
+ while (Date.now() < deadline) {
37
+ if (predicate(last)) return last;
38
+ await new Promise((r) => setTimeout(r, interval));
39
+ last = await fn();
40
+ }
41
+ if (predicate(last)) return last;
42
+ throw new Error(
43
+ `waitFor: ${label} not met within ${timeout}ms; last value: ${JSON.stringify(last)}`,
44
+ );
45
+ }
46
+
24
47
  describe("$job — registration validation", () => {
25
48
  it("rejects jobs declaring both cron and schema", async ({ expect }) => {
26
49
  const alepha = makeApp();
@@ -136,13 +159,12 @@ describe("$job — queue mode (outbox)", () => {
136
159
  await alepha.start();
137
160
  await app.work.push({ n: 42 });
138
161
 
139
- // Memory queue is synchronous-ish; processing completes promptly.
140
- await new Promise((r) => setTimeout(r, 50));
141
-
162
+ const rows = await waitFor(
163
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
164
+ (r) => r.length === 0 && received !== undefined,
165
+ { label: "row deleted on success" },
166
+ );
142
167
  expect(received).toEqual({ n: 42 });
143
- const rows = await app.executions.findMany({
144
- where: { jobName: { eq: "App.work" } },
145
- });
146
168
  expect(rows).toHaveLength(0);
147
169
  });
148
170
 
@@ -159,10 +181,11 @@ describe("$job — queue mode (outbox)", () => {
159
181
  const app = alepha.inject(App);
160
182
  await alepha.start();
161
183
  await app.work.push({ n: 1 });
162
- await new Promise((r) => setTimeout(r, 50));
163
- const rows = await app.executions.findMany({
164
- where: { jobName: { eq: "App.work" } },
165
- });
184
+ const rows = await waitFor(
185
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
186
+ (r) => r.length === 1 && r[0].status === "ok",
187
+ { label: "row reaches status=ok" },
188
+ );
166
189
  expect(rows).toHaveLength(1);
167
190
  expect(rows[0].status).toBe("ok");
168
191
  });
@@ -209,11 +232,12 @@ describe("$job — queue mode (outbox)", () => {
209
232
  const app = alepha.inject(App);
210
233
  await alepha.start();
211
234
  await app.work.push({ v: 1 }, { delay: [1, "hour"] });
212
- await new Promise((r) => setTimeout(r, 50));
235
+ const rows = await waitFor(
236
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
237
+ (r) => r.length === 1 && r[0].status === "scheduled",
238
+ { label: "row reaches status=scheduled" },
239
+ );
213
240
  expect(calls).toBe(0);
214
- const rows = await app.executions.findMany({
215
- where: { jobName: { eq: "App.work" } },
216
- });
217
241
  expect(rows).toHaveLength(1);
218
242
  expect(rows[0].status).toBe("scheduled");
219
243
  expect(rows[0].scheduledAt).toBeTruthy();
@@ -265,10 +289,12 @@ describe("$job — queue mode (outbox)", () => {
265
289
  const app = alepha.inject(App);
266
290
  await alepha.start();
267
291
  await app.work.push({ v: 1 });
268
- await new Promise((r) => setTimeout(r, 50));
269
- const rows = await app.executions.findMany({
270
- where: { jobName: { eq: "App.work" } },
271
- });
292
+ const rows = await waitFor(
293
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
294
+ (r) =>
295
+ r.length === 1 && r[0].status === "scheduled" && r[0].attempt === 1,
296
+ { label: "row rescheduled after first failure" },
297
+ );
272
298
  expect(rows).toHaveLength(1);
273
299
  expect(rows[0].status).toBe("scheduled");
274
300
  expect(rows[0].attempt).toBe(1);
@@ -297,10 +323,11 @@ describe("$job — queue mode (outbox)", () => {
297
323
  const app = alepha.inject(App);
298
324
  await alepha.start();
299
325
  await app.work.push({ v: 1 });
300
- await new Promise((r) => setTimeout(r, 100));
301
- const rows = await app.executions.findMany({
302
- where: { jobName: { eq: "App.work" } },
303
- });
326
+ const rows = await waitFor(
327
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
328
+ (r) => r.length === 1 && r[0].status === "error",
329
+ { label: "row reaches terminal status=error" },
330
+ );
304
331
  expect(rows).toHaveLength(1);
305
332
  expect(rows[0].status).toBe("error");
306
333
  expect(rows[0].error).toBe("dead");
@@ -579,18 +606,31 @@ describe("$job — retry semantics", () => {
579
606
 
580
607
  await app.work.push({ v: 1 });
581
608
 
582
- // First attempt runs immediately. Subsequent retries are sweep-driven.
583
- await new Promise((r) => setTimeout(r, 100));
609
+ // First attempt runs immediately, then the row is rescheduled with attempt=1.
610
+ // Wait for that stable state before sweeping (sweep needs status=scheduled).
611
+ await waitFor(
612
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
613
+ (r) => r[0]?.status === "scheduled" && r[0]?.attempt === 1,
614
+ { label: "row rescheduled after attempt 1" },
615
+ );
584
616
  expect(attempts).toBe(1);
585
617
 
586
618
  const provider = alepha.inject(JobProvider);
587
619
  // Trigger sweeps manually — each one should claim and run the next attempt.
588
620
  await (provider as any).sweep();
589
- await new Promise((r) => setTimeout(r, 50));
621
+ await waitFor(
622
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
623
+ (r) => r[0]?.status === "scheduled" && r[0]?.attempt === 2,
624
+ { label: "row rescheduled after attempt 2" },
625
+ );
590
626
  expect(attempts).toBe(2);
591
627
 
592
628
  await (provider as any).sweep();
593
- await new Promise((r) => setTimeout(r, 50));
629
+ await waitFor(
630
+ () => app.executions.findMany({ where: { jobName: { eq: "App.work" } } }),
631
+ (r) => r[0]?.status === "error" && r[0]?.attempt === 3,
632
+ { label: "row terminal after attempt 3" },
633
+ );
594
634
  expect(attempts).toBe(3);
595
635
 
596
636
  // After 3 attempts the row is terminal — no more retries.
@@ -640,11 +680,16 @@ describe("$job — cancel race", () => {
640
680
 
641
681
  await app.slow.cancel(id);
642
682
 
643
- // Give the handler's abort path a moment to execute and (without the
644
- // guard) try to overwrite the row.
645
- await new Promise((r) => setTimeout(r, 100));
646
-
647
- const final = await app.executions.findById(id);
683
+ // Confirm the row is `cancelled` and stays that way without the guard,
684
+ // the handler's abort path would race to overwrite to `error`. Poll for a
685
+ // stable window so the (buggy) overwrite would be caught.
686
+ const settleDeadline = Date.now() + 300;
687
+ let final: any;
688
+ while (Date.now() < settleDeadline) {
689
+ final = await app.executions.findById(id);
690
+ if (final?.status !== "cancelled") break;
691
+ await new Promise((r) => setTimeout(r, 20));
692
+ }
648
693
  expect(final?.status).toBe("cancelled");
649
694
  expect(final?.error).toBeFalsy();
650
695
  });
@@ -695,11 +740,16 @@ describe("$job — cron + retry (outbox path)", () => {
695
740
 
696
741
  // Sweep picks it up and runs attempt 2 → terminal.
697
742
  await (provider as any).sweep();
698
- await new Promise((r) => setTimeout(r, 100));
699
- expect(attempts).toBe(2);
700
- const rows2 = await app.executions.findMany({
701
- where: { jobName: { eq: "App.tick" } },
702
- });
743
+ await waitFor(
744
+ () => attempts,
745
+ (n) => n === 2,
746
+ { label: "second attempt runs after cron sweep" },
747
+ );
748
+ const rows2 = await waitFor(
749
+ () => app.executions.findMany({ where: { jobName: { eq: "App.tick" } } }),
750
+ (r) => r[0]?.status === "error",
751
+ { label: "row reaches terminal error" },
752
+ );
703
753
  expect(rows2[0].status).toBe("error");
704
754
  });
705
755
  });
@@ -765,12 +815,14 @@ describe("$job — admin resource shape", () => {
765
815
  await alepha.start();
766
816
 
767
817
  const id = await app.work.push({ v: 1 });
768
- // Wait for the handler to finish so the row is `ok` (record: all keeps it).
769
- await new Promise((r) => setTimeout(r, 100));
770
-
771
818
  const { JobService } = await import("../services/JobService.ts");
772
819
  const svc = alepha.inject(JobService);
773
- const resource = await svc.getExecution(id);
820
+ // Wait for the handler to finish so the row is `ok` (record: all keeps it).
821
+ const resource = await waitFor(
822
+ () => svc.getExecution(id),
823
+ (r) => r?.status === "ok",
824
+ { label: "execution reaches status=ok" },
825
+ );
774
826
  expect(resource.priority).toBe("high");
775
827
  expect(typeof resource.priority).toBe("string");
776
828
  });
@@ -54,6 +54,7 @@ export const jobExecutionEntity = $entity({
54
54
  }),
55
55
  indexes: [
56
56
  { columns: ["jobName", "status", "scheduledAt"] },
57
+ { columns: ["jobName", "status", "createdAt"] },
57
58
  { columns: ["jobName", "startedAt"] },
58
59
  { columns: ["jobName", "key"], unique: true },
59
60
  ],
@@ -112,7 +112,11 @@ export type JobEffectiveMode = "cron" | "queue" | "direct";
112
112
  * - re-enqueue pending rows older than `staleThreshold`
113
113
  * - mark crashed running rows as failed and apply retry policy
114
114
  * - move `scheduled` rows with `scheduledAt <= now` to pending + dispatch
115
- * - trim per-job history beyond `keepLastSuccess` / `keepLastError`
115
+ *
116
+ * Trim runs on its own cron (`trimCron`, default hourly):
117
+ * - per-job history trimmed beyond `keepLastSuccess` / `keepLastError`
118
+ * - decoupled from sweep because trim cost scales with job count, not
119
+ * retry latency — running it every sweep is wasted work for most apps.
116
120
  */
117
121
  export class JobProvider {
118
122
  protected readonly alepha = $inject(Alepha);
@@ -1127,9 +1131,6 @@ export class JobProvider {
1127
1131
  await this.handleFailure(exec.id, reg, exec.attempt, err, "");
1128
1132
  }
1129
1133
  }
1130
-
1131
- // 4. Trim ring buffer per job
1132
- await this.trimRingBuffers();
1133
1134
  } catch (e) {
1134
1135
  this.log.error("Sweep failed", { error: e });
1135
1136
  }
@@ -1188,7 +1189,7 @@ export class JobProvider {
1188
1189
  try {
1189
1190
  const rows = await this.executions.findMany({
1190
1191
  where: { jobName: { eq: jobName }, status: { eq: status } },
1191
- orderBy: { column: "startedAt", direction: "desc" },
1192
+ orderBy: { column: "createdAt", direction: "desc" },
1192
1193
  limit: keep + 50,
1193
1194
  });
1194
1195
  if (rows.length <= keep) return;
@@ -1251,6 +1252,20 @@ export class JobProvider {
1251
1252
  },
1252
1253
  true,
1253
1254
  );
1255
+
1256
+ this.cronProvider.createCronJob(
1257
+ "api:jobs:trim",
1258
+ this.config.trimCron,
1259
+ async () => {
1260
+ if (this.stopping) return;
1261
+ try {
1262
+ await this.trimRingBuffers();
1263
+ } catch (e) {
1264
+ this.log.error("Trim failed", { error: e });
1265
+ }
1266
+ },
1267
+ true,
1268
+ );
1254
1269
  },
1255
1270
  });
1256
1271
 
@@ -8,6 +8,10 @@ export const jobConfig = $atom({
8
8
  description:
9
9
  "Cron expression for the sweep tick. Must be minute-granular at minimum (cron resolution). On Cloudflare Workers this expression is emitted into wrangler.jsonc by the build.",
10
10
  }),
11
+ trimCron: t.text({
12
+ description:
13
+ "Cron expression for the ring-buffer trim tick (per-job keepLastSuccess/keepLastError enforcement). Decoupled from `sweepCron` because trim is bounded by job execution rate, not retry latency — running it every sweep is wasted work for most apps.",
14
+ }),
11
15
  staleThreshold: t.integer({
12
16
  description: "Pending age (ms) before the sweep re-dispatches it.",
13
17
  }),
@@ -31,6 +35,7 @@ export const jobConfig = $atom({
31
35
  }),
32
36
  default: {
33
37
  sweepCron: "*/5 * * * *",
38
+ trimCron: "0 * * * *",
34
39
  staleThreshold: 300_000,
35
40
  runTimeout: 1_800_000,
36
41
  keepLastSuccess: 10,
@@ -7,6 +7,7 @@ export const apiKeyEntity = $entity({
7
7
  id: db.primaryKey(t.uuid()),
8
8
  createdAt: db.createdAt(),
9
9
  updatedAt: db.updatedAt(),
10
+ organizationId: db.organization(),
10
11
 
11
12
  // Owner
12
13
  userId: t.uuid(),
@@ -0,0 +1,146 @@
1
+ import { $inject, t } from "alepha";
2
+ import { $route, type ServerReply } from "alepha/server";
3
+ import { MemoryPaymentProvider } from "../providers/MemoryPaymentProvider.ts";
4
+ import { PaymentProvider } from "../providers/PaymentProvider.ts";
5
+ import { PaymentService } from "../services/PaymentService.ts";
6
+
7
+ const FORBIDDEN_HTML =
8
+ "<!doctype html><meta charset=utf-8><title>Mock checkout</title>" +
9
+ "<p>Mock checkout is only available with the in-memory payment provider.</p>";
10
+
11
+ const escapeHtml = (s: string) =>
12
+ s.replace(/[&<>"']/g, (c) =>
13
+ c === "&"
14
+ ? "&amp;"
15
+ : c === "<"
16
+ ? "&lt;"
17
+ : c === ">"
18
+ ? "&gt;"
19
+ : c === '"'
20
+ ? "&quot;"
21
+ : "&#39;",
22
+ );
23
+
24
+ const formatAmount = (cents: number, currency: string) => {
25
+ const value = (cents / 100).toFixed(2);
26
+ return `${value} ${currency.toUpperCase()}`;
27
+ };
28
+
29
+ const renderPage = (opts: {
30
+ intentId: string;
31
+ amount: string;
32
+ returnUrl: string;
33
+ }) => `<!doctype html>
34
+ <html lang="en">
35
+ <head>
36
+ <meta charset="utf-8" />
37
+ <title>Mock checkout</title>
38
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
39
+ <style>
40
+ :root { color-scheme: light dark; font-family: system-ui, sans-serif; }
41
+ body { display:flex; min-height:100vh; align-items:center; justify-content:center; margin:0; background:#f6f7f9; }
42
+ .card { background:#fff; padding:2rem; border-radius:12px; box-shadow:0 8px 24px rgba(0,0,0,.08); max-width:380px; width:100%; }
43
+ h1 { margin:0 0 .25rem; font-size:1.1rem; }
44
+ .badge { display:inline-block; background:#fef3c7; color:#92400e; font-size:.7rem; padding:2px 8px; border-radius:999px; margin-bottom:.75rem; }
45
+ .amount { font-size:2rem; font-weight:600; margin:.75rem 0 1.25rem; }
46
+ .row { display:flex; gap:.5rem; }
47
+ button { flex:1; padding:.75rem 1rem; border-radius:8px; border:0; font-size:.95rem; cursor:pointer; }
48
+ .pay { background:#16a34a; color:#fff; }
49
+ .cancel { background:#e5e7eb; color:#111; }
50
+ .meta { font-size:.75rem; color:#6b7280; margin-top:1rem; word-break:break-all; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <div class="card">
55
+ <span class="badge">DEV — mock checkout</span>
56
+ <h1>Confirm payment</h1>
57
+ <div class="amount">${escapeHtml(opts.amount)}</div>
58
+ <div class="row">
59
+ <form method="post" action="/payments/mock-checkout/${opts.intentId}/cancel">
60
+ <input type="hidden" name="returnUrl" value="${escapeHtml(opts.returnUrl)}" />
61
+ <button type="submit" class="cancel">Cancel</button>
62
+ </form>
63
+ <form method="post" action="/payments/mock-checkout/${opts.intentId}/confirm">
64
+ <input type="hidden" name="returnUrl" value="${escapeHtml(opts.returnUrl)}" />
65
+ <button type="submit" class="pay">Pay</button>
66
+ </form>
67
+ </div>
68
+ <div class="meta">intent: ${opts.intentId}</div>
69
+ </div>
70
+ </body>
71
+ </html>`;
72
+
73
+ const appendStatusParam = (returnUrl: string, status: "success" | "cancel") => {
74
+ try {
75
+ const url = new URL(returnUrl, "http://placeholder.local");
76
+ url.searchParams.set("booking", status);
77
+ return returnUrl.startsWith("http")
78
+ ? url.toString()
79
+ : url.pathname + url.search + url.hash;
80
+ } catch {
81
+ return returnUrl;
82
+ }
83
+ };
84
+
85
+ export class MockCheckoutController {
86
+ protected readonly url = "/payments/mock-checkout";
87
+ protected readonly payments = $inject(PaymentService);
88
+ protected readonly provider = $inject(PaymentProvider);
89
+
90
+ protected isMemoryProvider() {
91
+ return this.provider instanceof MemoryPaymentProvider;
92
+ }
93
+
94
+ protected forbidden(reply: ServerReply) {
95
+ reply.headers["content-type"] = "text/html; charset=utf-8";
96
+ reply.status = 403;
97
+ return FORBIDDEN_HTML;
98
+ }
99
+
100
+ public readonly mockCheckoutPage = $route({
101
+ method: "GET",
102
+ path: `${this.url}/:id`,
103
+ schema: {
104
+ params: t.object({ id: t.uuid() }),
105
+ query: t.object({ returnUrl: t.optional(t.text({ size: "rich" })) }),
106
+ },
107
+ handler: async ({ params, query, reply }) => {
108
+ if (!this.isMemoryProvider()) return this.forbidden(reply);
109
+ const intent = await this.payments.getIntent(params.id);
110
+ reply.headers["content-type"] = "text/html; charset=utf-8";
111
+ return renderPage({
112
+ intentId: intent.id,
113
+ amount: formatAmount(intent.amount, intent.currency),
114
+ returnUrl: query.returnUrl ?? "/",
115
+ });
116
+ },
117
+ });
118
+
119
+ public readonly mockCheckoutConfirm = $route({
120
+ method: "POST",
121
+ path: `${this.url}/:id/confirm`,
122
+ schema: {
123
+ params: t.object({ id: t.uuid() }),
124
+ body: t.object({ returnUrl: t.optional(t.text({ size: "rich" })) }),
125
+ },
126
+ handler: async ({ params, body, reply }) => {
127
+ if (!this.isMemoryProvider()) return this.forbidden(reply);
128
+ await this.payments.handleWebhookEvent(params.id, "captured");
129
+ reply.redirect(appendStatusParam(body.returnUrl ?? "/", "success"), 302);
130
+ },
131
+ });
132
+
133
+ public readonly mockCheckoutCancel = $route({
134
+ method: "POST",
135
+ path: `${this.url}/:id/cancel`,
136
+ schema: {
137
+ params: t.object({ id: t.uuid() }),
138
+ body: t.object({ returnUrl: t.optional(t.text({ size: "rich" })) }),
139
+ },
140
+ handler: async ({ params, body, reply }) => {
141
+ if (!this.isMemoryProvider()) return this.forbidden(reply);
142
+ await this.payments.handleWebhookEvent(params.id, "failed");
143
+ reply.redirect(appendStatusParam(body.returnUrl ?? "/", "cancel"), 302);
144
+ },
145
+ });
146
+ }
@@ -1,5 +1,6 @@
1
1
  import { $module } from "alepha";
2
2
  import { AdminPaymentController } from "./controllers/AdminPaymentController.ts";
3
+ import { MockCheckoutController } from "./controllers/MockCheckoutController.ts";
3
4
  import { PaymentController } from "./controllers/PaymentController.ts";
4
5
  import { MemoryPaymentProvider } from "./providers/MemoryPaymentProvider.ts";
5
6
  import { PaymentProvider } from "./providers/PaymentProvider.ts";
@@ -7,6 +8,7 @@ import { PaymentMethodService } from "./services/PaymentMethodService.ts";
7
8
  import { PaymentService } from "./services/PaymentService.ts";
8
9
 
9
10
  export * from "./controllers/AdminPaymentController.ts";
11
+ export * from "./controllers/MockCheckoutController.ts";
10
12
  export * from "./controllers/PaymentController.ts";
11
13
  export * from "./entities/paymentIntents.ts";
12
14
  export * from "./entities/paymentMethods.ts";
@@ -67,6 +69,7 @@ export const AlephaApiPayments = $module({
67
69
  services: [
68
70
  AdminPaymentController,
69
71
  PaymentController,
72
+ MockCheckoutController,
70
73
  PaymentProvider,
71
74
  PaymentService,
72
75
  PaymentMethodService,
@@ -29,7 +29,12 @@ export class MemoryPaymentProvider implements PaymentProvider {
29
29
 
30
30
  public async createSession(
31
31
  intent: PaymentIntentEntity,
32
- options: { returnUrl: string; authorize?: boolean },
32
+ options: {
33
+ returnUrl: string;
34
+ authorize?: boolean;
35
+ stripeAccount?: string;
36
+ applicationFeeAmount?: number;
37
+ },
33
38
  ): Promise<CreateSessionResult> {
34
39
  const providerRef = `mem_session_${randomUUID()}`;
35
40
  const status = options.authorize ? "authorized" : "captured";
@@ -39,7 +44,7 @@ export class MemoryPaymentProvider implements PaymentProvider {
39
44
  status,
40
45
  });
41
46
  return {
42
- url: `/payments/mock-checkout/${intent.id}`,
47
+ url: `/payments/mock-checkout/${intent.id}?returnUrl=${encodeURIComponent(options.returnUrl)}`,
43
48
  providerRef,
44
49
  };
45
50
  }
@@ -88,8 +93,8 @@ export class MemoryPaymentProvider implements PaymentProvider {
88
93
  }
89
94
 
90
95
  public async createPaymentMethod(
91
- userId: string,
92
- token: string,
96
+ _userId: string,
97
+ _token: string,
93
98
  ): Promise<CreatePaymentMethodResult> {
94
99
  const providerRef = `mem_pm_${randomUUID()}`;
95
100
  const result: CreatePaymentMethodResult = {