alepha 0.19.3 → 0.19.5

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 (289) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/invitations/index.d.ts +790 -0
  3. package/dist/api/invitations/index.d.ts.map +1 -0
  4. package/dist/api/invitations/index.js +665 -0
  5. package/dist/api/invitations/index.js.map +1 -0
  6. package/dist/api/issues/index.d.ts +810 -0
  7. package/dist/api/issues/index.d.ts.map +1 -0
  8. package/dist/api/issues/index.js +447 -0
  9. package/dist/api/issues/index.js.map +1 -0
  10. package/dist/api/jobs/index.browser.js +8 -9
  11. package/dist/api/jobs/index.browser.js.map +1 -1
  12. package/dist/api/jobs/index.d.ts +99 -43
  13. package/dist/api/jobs/index.d.ts.map +1 -1
  14. package/dist/api/jobs/index.js +257 -40
  15. package/dist/api/jobs/index.js.map +1 -1
  16. package/dist/api/notifications/index.browser.js +0 -1
  17. package/dist/api/notifications/index.browser.js.map +1 -1
  18. package/dist/api/notifications/index.d.ts +3 -3
  19. package/dist/api/notifications/index.d.ts.map +1 -1
  20. package/dist/api/notifications/index.js +0 -1
  21. package/dist/api/notifications/index.js.map +1 -1
  22. package/dist/api/parameters/index.browser.js +112 -1
  23. package/dist/api/parameters/index.browser.js.map +1 -1
  24. package/dist/api/parameters/index.d.ts +90 -3
  25. package/dist/api/parameters/index.d.ts.map +1 -1
  26. package/dist/api/parameters/index.js +79 -12
  27. package/dist/api/parameters/index.js.map +1 -1
  28. package/dist/{billing → api/payments}/index.d.ts +67 -49
  29. package/dist/api/payments/index.d.ts.map +1 -0
  30. package/dist/{billing → api/payments}/index.js +108 -74
  31. package/dist/api/payments/index.js.map +1 -0
  32. package/dist/api/subscriptions/index.d.ts +1692 -0
  33. package/dist/api/subscriptions/index.d.ts.map +1 -0
  34. package/dist/api/subscriptions/index.js +1870 -0
  35. package/dist/api/subscriptions/index.js.map +1 -0
  36. package/dist/api/users/index.d.ts +24 -2
  37. package/dist/api/users/index.d.ts.map +1 -1
  38. package/dist/api/users/index.js +176 -36
  39. package/dist/api/users/index.js.map +1 -1
  40. package/dist/api/verifications/index.d.ts +13 -13
  41. package/dist/api/workflows/index.browser.js +246 -0
  42. package/dist/api/workflows/index.browser.js.map +1 -0
  43. package/dist/api/workflows/index.d.ts +1618 -0
  44. package/dist/api/workflows/index.d.ts.map +1 -0
  45. package/dist/api/workflows/index.js +1504 -0
  46. package/dist/api/workflows/index.js.map +1 -0
  47. package/dist/captcha/index.d.ts +142 -0
  48. package/dist/captcha/index.d.ts.map +1 -0
  49. package/dist/captcha/index.js +177 -0
  50. package/dist/captcha/index.js.map +1 -0
  51. package/dist/cli/core/index.d.ts +126 -30
  52. package/dist/cli/core/index.d.ts.map +1 -1
  53. package/dist/cli/core/index.js +106 -67
  54. package/dist/cli/core/index.js.map +1 -1
  55. package/dist/cli/platform/index.d.ts +84 -10
  56. package/dist/cli/platform/index.d.ts.map +1 -1
  57. package/dist/cli/platform/index.js +92 -4
  58. package/dist/cli/platform/index.js.map +1 -1
  59. package/dist/cli/vendor/index.d.ts +60 -10
  60. package/dist/cli/vendor/index.d.ts.map +1 -1
  61. package/dist/cli/vendor/index.js +177 -45
  62. package/dist/cli/vendor/index.js.map +1 -1
  63. package/dist/command/index.d.ts.map +1 -1
  64. package/dist/command/index.js +2 -3
  65. package/dist/command/index.js.map +1 -1
  66. package/dist/core/index.browser.js +21 -2
  67. package/dist/core/index.browser.js.map +1 -1
  68. package/dist/core/index.d.ts +33 -2
  69. package/dist/core/index.d.ts.map +1 -1
  70. package/dist/core/index.js +21 -2
  71. package/dist/core/index.js.map +1 -1
  72. package/dist/core/index.native.js +21 -2
  73. package/dist/core/index.native.js.map +1 -1
  74. package/dist/core/index.workerd.js +21 -2
  75. package/dist/core/index.workerd.js.map +1 -1
  76. package/dist/email/smtp/index.js +24 -8
  77. package/dist/email/smtp/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +0 -18
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +6 -23
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +1 -13
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +6 -23
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.bun.js +3 -3
  87. package/dist/orm/postgres/index.bun.js.map +1 -1
  88. package/dist/orm/postgres/index.d.ts.map +1 -1
  89. package/dist/orm/postgres/index.js +3 -3
  90. package/dist/orm/postgres/index.js.map +1 -1
  91. package/dist/react/i18n/index.d.ts +1 -0
  92. package/dist/react/i18n/index.d.ts.map +1 -1
  93. package/dist/react/i18n/index.js +8 -4
  94. package/dist/react/i18n/index.js.map +1 -1
  95. package/dist/react/router/index.browser.js +25 -3
  96. package/dist/react/router/index.browser.js.map +1 -1
  97. package/dist/react/router/index.d.ts +16 -1
  98. package/dist/react/router/index.d.ts.map +1 -1
  99. package/dist/react/router/index.js +25 -3
  100. package/dist/react/router/index.js.map +1 -1
  101. package/dist/security/index.d.ts +28 -0
  102. package/dist/security/index.d.ts.map +1 -1
  103. package/dist/security/index.js +28 -0
  104. package/dist/security/index.js.map +1 -1
  105. package/dist/server/auth/index.d.ts +145 -2
  106. package/dist/server/auth/index.d.ts.map +1 -1
  107. package/dist/server/auth/index.js +364 -63
  108. package/dist/server/auth/index.js.map +1 -1
  109. package/dist/server/cookies/index.d.ts.map +1 -1
  110. package/dist/server/cookies/index.js.map +1 -1
  111. package/dist/websocket/index.d.ts.map +1 -1
  112. package/dist/websocket/index.js.map +1 -1
  113. package/package.json +47 -20
  114. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  115. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  116. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  117. package/src/api/invitations/entities/invitations.ts +33 -0
  118. package/src/api/invitations/index.ts +65 -0
  119. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  120. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  121. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  122. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  123. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  124. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  125. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  126. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  127. package/src/api/invitations/services/InvitationService.ts +556 -0
  128. package/src/api/issues/__tests__/IssueService.spec.ts +263 -0
  129. package/src/api/issues/controllers/AdminIssueController.ts +149 -0
  130. package/src/api/issues/controllers/IssueController.ts +44 -0
  131. package/src/api/issues/entities/issues.ts +49 -0
  132. package/src/api/issues/index.ts +53 -0
  133. package/src/api/issues/schemas/createIssueSchema.ts +13 -0
  134. package/src/api/issues/schemas/issueConfigAtom.ts +13 -0
  135. package/src/api/issues/schemas/issueQuerySchema.ts +18 -0
  136. package/src/api/issues/schemas/issueResourceSchema.ts +6 -0
  137. package/src/api/issues/schemas/myIssueQuerySchema.ts +10 -0
  138. package/src/api/issues/schemas/updateIssueSchema.ts +13 -0
  139. package/src/api/issues/services/IssueService.ts +264 -0
  140. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  141. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  142. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  143. package/src/api/jobs/index.ts +0 -3
  144. package/src/api/jobs/primitives/$job.ts +22 -11
  145. package/src/api/jobs/providers/JobProvider.ts +229 -19
  146. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  147. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  148. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  149. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  150. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  151. package/src/api/jobs/services/JobService.ts +51 -12
  152. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  153. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  154. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  155. package/src/api/parameters/index.browser.ts +12 -0
  156. package/src/api/parameters/primitives/$parameter.ts +20 -3
  157. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  158. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  159. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  160. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  161. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  162. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  163. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  164. package/src/{billing → api/payments}/index.ts +31 -25
  165. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  166. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  167. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  168. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  169. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  170. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  171. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  172. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  173. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  174. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  175. package/src/api/subscriptions/index.ts +144 -0
  176. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  177. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  178. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  179. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  180. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  181. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  182. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  183. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  184. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  185. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  186. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  187. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  188. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  189. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  190. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  191. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  192. package/src/api/subscriptions/services/BillingService.ts +437 -0
  193. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  194. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  195. package/src/api/subscriptions/services/UsageService.ts +118 -0
  196. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  197. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  198. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  199. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  200. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  201. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  202. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  203. package/src/api/users/controllers/UserController.ts +3 -8
  204. package/src/api/users/notifications/UserNotifications.ts +23 -0
  205. package/src/api/users/primitives/$realm.ts +24 -0
  206. package/src/api/users/schemas/loginSchema.ts +1 -1
  207. package/src/api/users/services/CredentialService.ts +57 -7
  208. package/src/api/users/services/RegistrationService.ts +50 -11
  209. package/src/api/users/services/SessionService.ts +64 -9
  210. package/src/api/users/services/UserService.ts +21 -12
  211. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  212. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  213. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  214. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  215. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  216. package/src/api/workflows/index.browser.ts +22 -0
  217. package/src/api/workflows/index.ts +124 -0
  218. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  219. package/src/api/workflows/primitives/$workflow.ts +202 -0
  220. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  221. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  222. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  223. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  224. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  225. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  226. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  227. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  228. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  229. package/src/api/workflows/services/WorkflowService.ts +382 -0
  230. package/src/captcha/__tests__/MemoryCaptchaProvider.spec.ts +74 -0
  231. package/src/captcha/index.ts +33 -0
  232. package/src/captcha/providers/CaptchaProvider.ts +17 -0
  233. package/src/captcha/providers/MemoryCaptchaProvider.ts +65 -0
  234. package/src/captcha/providers/TurnstileCaptchaProvider.ts +125 -0
  235. package/src/cli/core/atoms/buildOptions.ts +57 -0
  236. package/src/cli/core/commands/build.ts +2 -0
  237. package/src/cli/core/providers/ViteDevServerProvider.ts +1 -1
  238. package/src/cli/core/services/ViteUtils.ts +5 -2
  239. package/src/cli/core/tasks/BuildClientTask.ts +3 -1
  240. package/src/cli/core/tasks/BuildCloudflareTask.ts +4 -0
  241. package/src/cli/core/tasks/BuildPwaTask.ts +81 -0
  242. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  243. package/src/cli/platform/adapters/CloudflareAdapter.ts +24 -0
  244. package/src/cli/platform/atoms/platformOptions.ts +19 -3
  245. package/src/cli/platform/hooks/PlatformHook.ts +51 -0
  246. package/src/cli/platform/index.ts +1 -0
  247. package/src/cli/platform/services/CloudflareApi.ts +22 -1
  248. package/src/cli/platform/services/PlatformOrchestrator.ts +67 -2
  249. package/src/cli/vendor/__tests__/VendorService.spec.ts +322 -178
  250. package/src/cli/vendor/commands/VendorCommand.ts +41 -38
  251. package/src/cli/vendor/services/VendorService.ts +234 -31
  252. package/src/command/__tests__/CliProvider.spec.ts +45 -0
  253. package/src/command/providers/CliProvider.ts +3 -4
  254. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  255. package/src/core/providers/SchemaValidator.ts +1 -1
  256. package/src/core/providers/TypeProvider.ts +46 -3
  257. package/src/orm/__tests__/enums.spec.ts +22 -29
  258. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  259. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  260. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  261. package/src/orm/core/services/Repository.ts +20 -6
  262. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  263. package/src/react/i18n/__tests__/I18nProvider.spec.ts +83 -0
  264. package/src/react/i18n/providers/I18nProvider.ts +12 -10
  265. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  266. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  267. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  268. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  269. package/src/security/primitives/$issuer.ts +3 -1
  270. package/src/security/primitives/$secure.ts +28 -0
  271. package/src/server/auth/index.ts +7 -0
  272. package/src/server/auth/primitives/$auth.ts +37 -3
  273. package/src/server/auth/primitives/$authApple.ts +114 -4
  274. package/src/server/auth/primitives/$authFacebook.ts +98 -0
  275. package/src/server/auth/primitives/$authFranceConnect.ts +105 -0
  276. package/src/server/auth/primitives/$authGithub.ts +22 -16
  277. package/src/server/auth/primitives/$authMicrosoft.ts +88 -0
  278. package/src/server/auth/providers/ServerAuthProvider.ts +197 -72
  279. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -0
  280. package/src/server/core/__tests__/ServerRouterProvider-errorHandler.spec.ts +1 -1
  281. package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -1
  282. package/dist/billing/index.d.ts.map +0 -1
  283. package/dist/billing/index.js.map +0 -1
  284. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  285. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  286. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  287. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  288. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  289. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Captcha verification provider interface.
3
+ *
4
+ * Verifies that a user-submitted captcha token is valid. Implementations
5
+ * call the relevant captcha service (Turnstile, reCAPTCHA, hCaptcha, etc.)
6
+ * to validate the token server-side.
7
+ */
8
+ export abstract class CaptchaProvider {
9
+ /**
10
+ * Verify a captcha token.
11
+ *
12
+ * @param token - The captcha response token submitted by the client.
13
+ * @param ip - Optional client IP address for additional validation.
14
+ * @returns Whether the token is valid.
15
+ */
16
+ public abstract verify(token: string, ip?: string): Promise<boolean>;
17
+ }
@@ -0,0 +1,65 @@
1
+ import { $logger } from "alepha/logger";
2
+ import type { CaptchaProvider } from "./CaptchaProvider.ts";
3
+
4
+ export interface CaptchaRecord {
5
+ token: string;
6
+ ip?: string;
7
+ verifiedAt: Date;
8
+ }
9
+
10
+ /**
11
+ * In-memory captcha provider for testing.
12
+ *
13
+ * Accepts all tokens by default. Use `reject()` to make verification fail,
14
+ * and `accept()` to restore. All verification attempts are recorded for assertions.
15
+ */
16
+ export class MemoryCaptchaProvider implements CaptchaProvider {
17
+ protected readonly log = $logger();
18
+
19
+ /**
20
+ * All verification attempts.
21
+ */
22
+ public records: CaptchaRecord[] = [];
23
+
24
+ protected shouldAccept = true;
25
+
26
+ public async verify(token: string, ip?: string): Promise<boolean> {
27
+ this.log.debug("Verifying captcha in memory store", { token, ip });
28
+
29
+ this.records.push({
30
+ token,
31
+ ip,
32
+ verifiedAt: new Date(),
33
+ });
34
+
35
+ return this.shouldAccept;
36
+ }
37
+
38
+ /**
39
+ * Make all subsequent verifications fail.
40
+ */
41
+ public reject(): void {
42
+ this.shouldAccept = false;
43
+ }
44
+
45
+ /**
46
+ * Make all subsequent verifications pass (default behavior).
47
+ */
48
+ public accept(): void {
49
+ this.shouldAccept = true;
50
+ }
51
+
52
+ /**
53
+ * Whether a token was verified.
54
+ */
55
+ public wasVerified(token: string): boolean {
56
+ return this.records.some((r) => r.token === token);
57
+ }
58
+
59
+ /**
60
+ * Get the last verification attempt.
61
+ */
62
+ public get last(): CaptchaRecord | undefined {
63
+ return this.records[this.records.length - 1];
64
+ }
65
+ }
@@ -0,0 +1,125 @@
1
+ import { $context, AlephaError, t } from "alepha";
2
+ import { $logger } from "alepha/logger";
3
+ import type { CaptchaProvider } from "./CaptchaProvider.ts";
4
+
5
+ /**
6
+ * Cloudflare Turnstile captcha verification provider.
7
+ *
8
+ * Validates captcha tokens against the Cloudflare Turnstile siteverify API.
9
+ * Free, privacy-friendly, and supports invisible mode.
10
+ *
11
+ * ## Setup
12
+ *
13
+ * 1. Create a Turnstile widget at https://dash.cloudflare.com/?to=/:account/turnstile
14
+ * 2. Copy the **Site Key** (public, for the client) and **Secret Key** (private, for the server)
15
+ * 3. Set `TURNSTILE_SECRET_KEY` in your environment
16
+ *
17
+ * ## Client-side integration
18
+ *
19
+ * Add the Turnstile script and widget to your form:
20
+ *
21
+ * ```html
22
+ * <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
23
+ * <form>
24
+ * <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
25
+ * <button type="submit">Submit</button>
26
+ * </form>
27
+ * ```
28
+ *
29
+ * The widget injects a hidden `cf-turnstile-response` input into the form.
30
+ * Send this value as the `captchaToken` in your registration request.
31
+ *
32
+ * For explicit rendering (React, SPA):
33
+ *
34
+ * ```ts
35
+ * turnstile.render("#container", {
36
+ * sitekey: "YOUR_SITE_KEY",
37
+ * callback: (token) => setCaptchaToken(token),
38
+ * });
39
+ * ```
40
+ *
41
+ * ## Server-side usage
42
+ *
43
+ * Register the provider in your app:
44
+ *
45
+ * ```ts
46
+ * import { CaptchaProvider } from "alepha/captcha";
47
+ * import { TurnstileCaptchaProvider } from "alepha/captcha";
48
+ *
49
+ * alepha.with({ provide: CaptchaProvider, use: TurnstileCaptchaProvider });
50
+ * ```
51
+ *
52
+ * ## Test keys (for development)
53
+ *
54
+ * - Always passes: site `1x00000000000000000000AA`, secret `1x0000000000000000000000000000000AA`
55
+ * - Always blocks: site `2x00000000000000000000AB`, secret `2x0000000000000000000000000000000AB`
56
+ * - Forces interactive: site `3x00000000000000000000FF`
57
+ *
58
+ * ## Environment Variables
59
+ *
60
+ * - `TURNSTILE_SECRET_KEY`: The secret key from the Cloudflare Turnstile dashboard.
61
+ *
62
+ * @see https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
63
+ */
64
+ export class TurnstileCaptchaProvider implements CaptchaProvider {
65
+ protected readonly log = $logger();
66
+ protected readonly secretKey: string;
67
+
68
+ constructor() {
69
+ const { alepha } = $context();
70
+
71
+ const env = alepha.parseEnv(
72
+ t.object({
73
+ TURNSTILE_SECRET_KEY: t.text({
74
+ description:
75
+ "The secret key from the Cloudflare Turnstile dashboard.",
76
+ }),
77
+ }),
78
+ );
79
+
80
+ this.secretKey = env.TURNSTILE_SECRET_KEY;
81
+ }
82
+
83
+ public async verify(token: string, ip?: string): Promise<boolean> {
84
+ const body = new URLSearchParams();
85
+ body.set("secret", this.secretKey);
86
+ body.set("response", token);
87
+
88
+ if (ip) {
89
+ body.set("remoteip", ip);
90
+ }
91
+
92
+ try {
93
+ const res = await fetch(
94
+ "https://challenges.cloudflare.com/turnstile/v0/siteverify",
95
+ {
96
+ method: "POST",
97
+ body,
98
+ },
99
+ );
100
+
101
+ const data = (await res.json()) as TurnstileResponse;
102
+
103
+ if (!data.success) {
104
+ this.log.debug("Turnstile verification failed", {
105
+ errorCodes: data["error-codes"],
106
+ });
107
+ }
108
+
109
+ return data.success;
110
+ } catch (error) {
111
+ throw new AlephaError("Failed to verify Turnstile captcha token", {
112
+ cause: error,
113
+ });
114
+ }
115
+ }
116
+ }
117
+
118
+ interface TurnstileResponse {
119
+ success: boolean;
120
+ "error-codes"?: string[];
121
+ challenge_ts?: string;
122
+ hostname?: string;
123
+ action?: string;
124
+ cdata?: string;
125
+ }
@@ -207,6 +207,63 @@ export const buildOptions = $atom({
207
207
  }),
208
208
  ),
209
209
 
210
+ /**
211
+ * PWA (Progressive Web App) configuration.
212
+ *
213
+ * Generates a web app manifest and enables installability.
214
+ * Requires a client-side bundle (React).
215
+ */
216
+ pwa: t.optional(
217
+ t.object({
218
+ /**
219
+ * Full application name displayed on the splash screen
220
+ * and in the OS app switcher.
221
+ */
222
+ name: t.string(),
223
+
224
+ /**
225
+ * Short name displayed on the home screen icon.
226
+ * Falls back to `name` if omitted.
227
+ */
228
+ shortName: t.optional(t.string()),
229
+
230
+ /**
231
+ * Theme color used for the browser toolbar and OS chrome.
232
+ *
233
+ * @default "#ffffff"
234
+ */
235
+ themeColor: t.optional(t.string()),
236
+
237
+ /**
238
+ * Background color for the splash screen.
239
+ *
240
+ * @default "#ffffff"
241
+ */
242
+ backgroundColor: t.optional(t.string()),
243
+
244
+ /**
245
+ * Display mode for the installed PWA.
246
+ *
247
+ * - `standalone` - Looks like a native app (default)
248
+ * - `fullscreen` - Uses entire screen (games, immersive)
249
+ * - `minimal-ui` - Like standalone with minimal browser UI
250
+ * - `browser` - Standard browser tab
251
+ *
252
+ * @default "standalone"
253
+ */
254
+ display: t.optional(
255
+ t.enum(["standalone", "fullscreen", "minimal-ui", "browser"]),
256
+ ),
257
+
258
+ /**
259
+ * Enable offline support via service worker.
260
+ *
261
+ * TODO: Not yet implemented.
262
+ */
263
+ offline: t.optional(t.boolean()),
264
+ }),
265
+ ),
266
+
210
267
  /**
211
268
  * Sitemap generation configuration.
212
269
  */
@@ -17,6 +17,7 @@ import { BuildCloudflareTask } from "../tasks/BuildCloudflareTask.ts";
17
17
  import { BuildCompressTask } from "../tasks/BuildCompressTask.ts";
18
18
  import { BuildDockerTask } from "../tasks/BuildDockerTask.ts";
19
19
  import { BuildPrerenderTask } from "../tasks/BuildPrerenderTask.ts";
20
+ import { BuildPwaTask } from "../tasks/BuildPwaTask.ts";
20
21
  import { BuildServerTask } from "../tasks/BuildServerTask.ts";
21
22
  import { BuildSitemapTask } from "../tasks/BuildSitemapTask.ts";
22
23
  import { BuildStaticTask } from "../tasks/BuildStaticTask.ts";
@@ -43,6 +44,7 @@ export class BuildCommand {
43
44
  $inject(BuildServerTask),
44
45
  $inject(BuildAssetsTask),
45
46
  $inject(BuildSitemapTask),
47
+ $inject(BuildPwaTask),
46
48
  $inject(BuildPrerenderTask),
47
49
  $inject(BuildVercelTask),
48
50
  $inject(BuildCloudflareTask),
@@ -594,7 +594,7 @@ if (import.meta.hot) {
594
594
  </script>`);
595
595
 
596
596
  if (style) {
597
- tags.push(`<link rel="stylesheet" href="/${style}">`);
597
+ tags.push(`<script type="module">import "/${style}";</script>`);
598
598
  }
599
599
  if (browser) {
600
600
  tags.push(`<script type="module" src="/${browser}"></script>`);
@@ -359,16 +359,19 @@ export class ViteUtils {
359
359
  // HTML template
360
360
  // ---------------------------------------------------------------------------------------------------------------
361
361
 
362
- public generateIndexHtml(entry: AppEntry): string {
362
+ public generateIndexHtml(entry: AppEntry, opts?: { pwa?: boolean }): string {
363
363
  const style = entry.style;
364
364
  const browser = entry.browser ?? entry.server;
365
+ const manifestLink = opts?.pwa
366
+ ? '\n<link rel="manifest" href="/manifest.webmanifest" />'
367
+ : "";
365
368
  return `
366
369
  <!DOCTYPE html>
367
370
  <html lang="en">
368
371
  <head>
369
372
  <meta charset="UTF-8" />
370
373
  <title>App</title>
371
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
374
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>${manifestLink}
372
375
  ${style ? `<link rel="stylesheet" href="/${style}" />` : ""}
373
376
  </head>
374
377
  <body>
@@ -29,7 +29,9 @@ export class BuildClientTask extends BuildTask {
29
29
  const isCI = this.alepha.isCI();
30
30
 
31
31
  // Write index.html template for Vite to consume
32
- const template = this.viteUtils.generateIndexHtml(ctx.entry);
32
+ const template = this.viteUtils.generateIndexHtml(ctx.entry, {
33
+ pwa: !!ctx.options.pwa,
34
+ });
33
35
  await this.fs.mkdir(this.fs.join(ctx.root, "node_modules/.alepha"));
34
36
  const indexHtmlPath = this.fs.join(
35
37
  ctx.root,
@@ -141,11 +141,13 @@ export class BuildCloudflareTask extends BuildTask {
141
141
 
142
142
  const [dbName, id] = url.replace("d1://", "").replace("d1:", "").split(":");
143
143
  const binding = BuildCloudflareTask.D1_BINDING;
144
+ const jurisdiction = process.env.CLOUDFLARE_JURISDICTION;
144
145
  wrangler.d1_databases = wrangler.d1_databases || [];
145
146
  wrangler.d1_databases.push({
146
147
  binding,
147
148
  database_name: dbName,
148
149
  database_id: id,
150
+ ...(jurisdiction ? { jurisdiction } : {}),
149
151
  });
150
152
  wrangler.vars ??= {};
151
153
  wrangler.vars.DATABASE_URL = `d1://${binding}`;
@@ -177,10 +179,12 @@ export class BuildCloudflareTask extends BuildTask {
177
179
  return;
178
180
  }
179
181
 
182
+ const jurisdiction = process.env.CLOUDFLARE_JURISDICTION;
180
183
  wrangler.r2_buckets = wrangler.r2_buckets || [];
181
184
  wrangler.r2_buckets.push({
182
185
  binding: bucketName,
183
186
  bucket_name: bucketName,
187
+ ...(jurisdiction ? { jurisdiction } : {}),
184
188
  });
185
189
  wrangler.vars ??= {};
186
190
  wrangler.vars.R2_BUCKET_NAME = bucketName;
@@ -0,0 +1,81 @@
1
+ import { $inject } from "alepha";
2
+ import { FileSystemProvider } from "alepha/system";
3
+ import { BuildTask, type BuildTaskContext } from "./BuildTask.ts";
4
+
5
+ /**
6
+ * Generate PWA web app manifest.
7
+ *
8
+ * Produces a `manifest.webmanifest` in the public output directory
9
+ * from the `pwa` section of build options. Detects icons from `public/`.
10
+ */
11
+ export class BuildPwaTask extends BuildTask {
12
+ protected readonly fs = $inject(FileSystemProvider);
13
+
14
+ async run(ctx: BuildTaskContext): Promise<void> {
15
+ const pwa = ctx.options.pwa;
16
+ if (!pwa || !ctx.hasClient) {
17
+ return;
18
+ }
19
+
20
+ const distDir = ctx.options.output?.dist ?? "dist";
21
+ const publicDir = ctx.options.output?.public ?? "public";
22
+ const outputDir = this.fs.join(ctx.root, distDir, publicDir);
23
+
24
+ await ctx.run({
25
+ name: "generate pwa manifest",
26
+ handler: async () => {
27
+ const icons = await this.detectIcons(outputDir);
28
+
29
+ const manifest: Record<string, unknown> = {
30
+ name: pwa.name,
31
+ short_name: pwa.shortName ?? pwa.name,
32
+ start_url: "/",
33
+ display: pwa.display ?? "standalone",
34
+ theme_color: pwa.themeColor ?? "#ffffff",
35
+ background_color: pwa.backgroundColor ?? "#ffffff",
36
+ };
37
+
38
+ if (icons.length > 0) {
39
+ manifest.icons = icons;
40
+ }
41
+
42
+ const output = this.fs.join(outputDir, "manifest.webmanifest");
43
+ await this.fs.writeFile(output, JSON.stringify(manifest, null, 2));
44
+ },
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Detect icon files in the public output directory.
50
+ *
51
+ * Looks for common icon filenames and generates
52
+ * manifest icon entries with appropriate sizes and types.
53
+ */
54
+ protected async detectIcons(
55
+ publicDir: string,
56
+ ): Promise<Array<{ src: string; sizes: string; type: string }>> {
57
+ const icons: Array<{ src: string; sizes: string; type: string }> = [];
58
+
59
+ const candidates: Array<{
60
+ file: string;
61
+ sizes: string;
62
+ type: string;
63
+ }> = [
64
+ { file: "icon-192.png", sizes: "192x192", type: "image/png" },
65
+ { file: "icon-512.png", sizes: "512x512", type: "image/png" },
66
+ { file: "icon.svg", sizes: "any", type: "image/svg+xml" },
67
+ ];
68
+
69
+ for (const candidate of candidates) {
70
+ if (await this.fs.exists(this.fs.join(publicDir, candidate.file))) {
71
+ icons.push({
72
+ src: `/${candidate.file}`,
73
+ sizes: candidate.sizes,
74
+ type: candidate.type,
75
+ });
76
+ }
77
+ }
78
+
79
+ return icons;
80
+ }
81
+ }
@@ -24,23 +24,8 @@ export const webAppRouterTs = (options: {
24
24
  imports.push(
25
25
  'import { AdminSessionRouter } from "@alepha/ui/admin-sessions";',
26
26
  );
27
- imports.push('import { AdminAuditRouter } from "@alepha/ui/admin-audits";');
28
- imports.push('import { AdminFileRouter } from "@alepha/ui/admin-files";');
29
- imports.push(
30
- 'import { AdminParameterRouter } from "@alepha/ui/admin-parameters";',
31
- );
32
- imports.push('import { AdminJobRouter } from "@alepha/ui/admin-jobs";');
33
- imports.push('import { AdminApiKeyRouter } from "@alepha/ui/admin-keys";');
34
- imports.push(
35
- 'import { AdminNotificationRouter } from "@alepha/ui/admin-notifications";',
36
- );
37
- imports.push(
38
- 'import { AdminBillingRouter } from "@alepha/ui/admin-billing";',
39
- );
40
27
  imports.push('import { $inject } from "alepha";');
41
- imports.push(
42
- 'import { IconLayoutDashboard, IconLockPassword, IconCreditCard } from "@tabler/icons-react";',
43
- );
28
+ imports.push('import { IconLayoutDashboard } from "@tabler/icons-react";');
44
29
  }
45
30
 
46
31
  // Page import
@@ -67,61 +52,23 @@ export const webAppRouterTs = (options: {
67
52
  classMembers.push(` // ── Admin Domain Routers ──────────────────────────
68
53
  protected users = $inject(AdminUserRouter);
69
54
  protected sessions = $inject(AdminSessionRouter);
70
- protected audits = $inject(AdminAuditRouter);
71
- protected files = $inject(AdminFileRouter);
72
- protected parameters = $inject(AdminParameterRouter);
73
- protected jobs = $inject(AdminJobRouter);
74
- protected apiKeys = $inject(AdminApiKeyRouter);
75
- protected notifications = $inject(AdminNotificationRouter);
76
- protected billing = $inject(AdminBillingRouter);
77
55
 
78
56
  // ── Admin Panel ─────────────────────────────────
79
57
  admin = $uiAdmin({
80
58
  pages: [
81
59
  this.users.adminUsers,
60
+ this.users.adminUserLayout,
82
61
  this.sessions.adminSessions,
83
- this.audits.adminAudits,
84
- this.files.adminFiles,
85
- this.parameters.adminParameters,
86
- this.jobs.adminJobs,
87
- this.apiKeys.adminApiKeys,
88
- this.notifications.adminNotifications,
89
- this.billing.adminBilling,
90
62
  ],
91
63
  sidebarItems: [
92
- {
93
- label: "Security",
94
- children: [
95
- { label: "Identity", icon: IconLockPassword, children: [
96
- this.users.adminUsers,
97
- this.sessions.adminSessions,
98
- this.apiKeys.adminApiKeys,
99
- ]},
100
- this.audits.adminAudits,
101
- ],
102
- },
103
- {
104
- label: "System",
105
- children: [
106
- this.files.adminFiles,
107
- this.jobs.adminJobs,
108
- this.notifications.adminNotifications,
109
- this.parameters.adminParameters,
110
- ],
111
- },
112
- {
113
- label: "Commerce",
114
- icon: IconCreditCard,
115
- children: [
116
- this.billing.adminBilling,
117
- ],
118
- },
64
+ this.users.adminUsers,
65
+ this.sessions.adminSessions,
119
66
  ],
120
67
  });
121
68
 
122
69
  // ── Admin Dashboard ─────────────────────────────
123
70
  adminDashboard = $page({
124
- parent: this.admin.adminLayout,
71
+ parent: this.admin,
125
72
  path: "/",
126
73
  label: "Dashboard",
127
74
  icon: IconLayoutDashboard,
@@ -48,6 +48,17 @@ export class CloudflareAdapter extends PlatformAdapter {
48
48
  return !!dbUrl?.startsWith("postgres:");
49
49
  }
50
50
 
51
+ /**
52
+ * Propagate the environment's data-jurisdiction setting to the API client.
53
+ *
54
+ * Must be invoked at the top of every entry point (authenticate, build,
55
+ * deploy, secrets, provision, migrate, inspect, teardown) because
56
+ * CloudflareApi is a singleton reused across env invocations.
57
+ */
58
+ protected configureApi(ctx: PlatformContext): void {
59
+ this.api.setJurisdiction(ctx.envConfig.jurisdiction);
60
+ }
61
+
51
62
  protected async runShell(
52
63
  command: string,
53
64
  options: Parameters<ShellProvider["run"]>[1] = {},
@@ -70,6 +81,7 @@ export class CloudflareAdapter extends PlatformAdapter {
70
81
  // -------------------------------------------------------------------------
71
82
 
72
83
  async authenticate(ctx: PlatformContext, run: RunnerMethod): Promise<void> {
84
+ this.configureApi(ctx);
73
85
  await run({
74
86
  name: "authenticate",
75
87
  handler: async () => {
@@ -112,6 +124,7 @@ export class CloudflareAdapter extends PlatformAdapter {
112
124
  // -------------------------------------------------------------------------
113
125
 
114
126
  async build(ctx: AppContext, run: RunnerMethod): Promise<void> {
127
+ this.configureApi(ctx);
115
128
  const appDir = ctx.app.path
116
129
  ? this.fs.join(ctx.root, ctx.app.path)
117
130
  : ctx.root;
@@ -159,6 +172,10 @@ export class CloudflareAdapter extends PlatformAdapter {
159
172
  env.CLOUDFLARE_DOMAIN = ctx.envConfig.domain;
160
173
  }
161
174
 
175
+ if (ctx.envConfig.jurisdiction) {
176
+ env.CLOUDFLARE_JURISDICTION = ctx.envConfig.jurisdiction;
177
+ }
178
+
162
179
  await run({
163
180
  name: "alepha build -t cloudflare",
164
181
  handler: async () => {
@@ -178,6 +195,7 @@ export class CloudflareAdapter extends PlatformAdapter {
178
195
  ctx: AppContext,
179
196
  run: RunnerMethod,
180
197
  ): Promise<string | undefined> {
198
+ this.configureApi(ctx);
181
199
  const workerName = ctx.naming.worker(
182
200
  ctx.apps.length > 1 ? ctx.app.name : undefined,
183
201
  );
@@ -212,6 +230,7 @@ export class CloudflareAdapter extends PlatformAdapter {
212
230
  "DATABASE_URL",
213
231
  "R2_BUCKET_NAME",
214
232
  "CLOUDFLARE_DOMAIN",
233
+ "CLOUDFLARE_JURISDICTION",
215
234
  "HYPERDRIVE_ID",
216
235
  "POSTGRES_SCHEMA",
217
236
  "NODE_ENV",
@@ -221,6 +240,7 @@ export class CloudflareAdapter extends PlatformAdapter {
221
240
  ctx: PlatformContext,
222
241
  run: RunnerMethod,
223
242
  ): Promise<void> {
243
+ this.configureApi(ctx);
224
244
  const envVars = await this.envUtils.parseEnv(ctx.root, [`.env.${ctx.env}`]);
225
245
 
226
246
  // Filter out binding/build vars, VITE_* vars, and empty values
@@ -265,6 +285,7 @@ export class CloudflareAdapter extends PlatformAdapter {
265
285
  ctx: PlatformContext,
266
286
  run: RunnerMethod,
267
287
  ): Promise<void> {
288
+ this.configureApi(ctx);
268
289
  const needsDB = ctx.apps.some((a) => a.resources.hasDatabase);
269
290
  const needsBucket = ctx.apps.some((a) => a.resources.hasBucket);
270
291
  const postgres = needsDB && (await this.isPostgres(ctx));
@@ -345,6 +366,7 @@ export class CloudflareAdapter extends PlatformAdapter {
345
366
  ctx: PlatformContext,
346
367
  run: RunnerMethod,
347
368
  ): Promise<void> {
369
+ this.configureApi(ctx);
348
370
  const needsDB = ctx.apps.some((a) => a.resources.hasDatabase);
349
371
  if (!needsDB) {
350
372
  return;
@@ -431,6 +453,7 @@ export class CloudflareAdapter extends PlatformAdapter {
431
453
  ctx: PlatformContext,
432
454
  run: RunnerMethod,
433
455
  ): Promise<PlatformState> {
456
+ this.configureApi(ctx);
434
457
  const state: PlatformState = {
435
458
  workers: [],
436
459
  databases: [],
@@ -610,6 +633,7 @@ export class CloudflareAdapter extends PlatformAdapter {
610
633
  // -------------------------------------------------------------------------
611
634
 
612
635
  async teardown(ctx: PlatformContext, run: RunnerMethod): Promise<void> {
636
+ this.configureApi(ctx);
613
637
  // 1. Remove queue consumers (must happen before worker or queue deletion)
614
638
  for (const app of ctx.apps) {
615
639
  if (app.resources.hasQueue) {
@@ -53,11 +53,27 @@ export const platformOptions = $atom({
53
53
  * Named environments with their adapter and configuration.
54
54
  */
55
55
  environments: t.record(
56
- t.text(),
56
+ t.text({
57
+ description:
58
+ "Environment name (e.g. 'production', 'staging', 'preview'). Used in resource naming and selected via --env.",
59
+ }),
57
60
  t.object({
58
61
  adapter: t.enum(["cloudflare", "vercel"]),
62
+ /**
63
+ * Custom domain for the deployed worker (e.g. "api.example.com").
64
+ *
65
+ * On Cloudflare this is attached as a custom-domain route.
66
+ * Omit to use the adapter's default `*.workers.dev` / preview URL.
67
+ */
59
68
  domain: t.optional(t.text()),
60
- domains: t.optional(t.record(t.text(), t.text())),
69
+ /**
70
+ * Cloudflare data jurisdiction for R2 buckets and D1 databases.
71
+ * - "eu": data stays within the EU
72
+ * - "fedramp": FedRAMP-authorized regions
73
+ *
74
+ * Omit for the default (global) jurisdiction.
75
+ */
76
+ jurisdiction: t.optional(t.enum(["eu", "fedramp"])),
61
77
  }),
62
78
  ),
63
79
  }),
@@ -75,6 +91,6 @@ export type PlatformOptions = Static<typeof platformOptions.schema>;
75
91
  export interface EnvironmentConfig {
76
92
  adapter: "cloudflare" | "vercel";
77
93
  domain?: string;
78
- domains?: Record<string, string>;
79
94
  vars?: Record<string, string>;
95
+ jurisdiction?: "eu" | "fedramp";
80
96
  }