alepha 0.15.3 → 0.15.4

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 (90) hide show
  1. package/dist/api/audits/index.d.ts +332 -332
  2. package/dist/api/audits/index.d.ts.map +1 -1
  3. package/dist/api/audits/index.js +8 -0
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.js +1 -0
  6. package/dist/api/files/index.js.map +1 -1
  7. package/dist/api/jobs/index.d.ts +151 -151
  8. package/dist/api/jobs/index.d.ts.map +1 -1
  9. package/dist/api/jobs/index.js +3 -0
  10. package/dist/api/jobs/index.js.map +1 -1
  11. package/dist/api/keys/index.d.ts +195 -195
  12. package/dist/api/keys/index.d.ts.map +1 -1
  13. package/dist/api/notifications/index.browser.js +1 -0
  14. package/dist/api/notifications/index.browser.js.map +1 -1
  15. package/dist/api/notifications/index.js +1 -0
  16. package/dist/api/notifications/index.js.map +1 -1
  17. package/dist/api/parameters/index.d.ts +260 -260
  18. package/dist/api/parameters/index.d.ts.map +1 -1
  19. package/dist/api/parameters/index.js +10 -0
  20. package/dist/api/parameters/index.js.map +1 -1
  21. package/dist/api/users/index.d.ts +10 -10
  22. package/dist/api/users/index.d.ts.map +1 -1
  23. package/dist/api/users/index.js +11 -0
  24. package/dist/api/users/index.js.map +1 -1
  25. package/dist/api/verifications/index.d.ts +128 -128
  26. package/dist/api/verifications/index.d.ts.map +1 -1
  27. package/dist/batch/index.d.ts +4 -4
  28. package/dist/cli/index.d.ts +5 -0
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +19 -2
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/email/index.d.ts +13 -13
  33. package/dist/email/index.d.ts.map +1 -1
  34. package/dist/email/index.js +10554 -2
  35. package/dist/email/index.js.map +1 -1
  36. package/dist/lock/core/index.d.ts +6 -1
  37. package/dist/lock/core/index.d.ts.map +1 -1
  38. package/dist/lock/core/index.js +9 -1
  39. package/dist/lock/core/index.js.map +1 -1
  40. package/dist/react/auth/index.browser.js +2 -1
  41. package/dist/react/auth/index.browser.js.map +1 -1
  42. package/dist/react/auth/index.js +2 -1
  43. package/dist/react/auth/index.js.map +1 -1
  44. package/dist/react/core/index.d.ts +3 -3
  45. package/dist/react/router/index.d.ts +10 -0
  46. package/dist/react/router/index.d.ts.map +1 -1
  47. package/dist/react/router/index.js +16 -6
  48. package/dist/react/router/index.js.map +1 -1
  49. package/dist/redis/index.d.ts +19 -19
  50. package/dist/scheduler/index.d.ts +13 -1
  51. package/dist/scheduler/index.d.ts.map +1 -1
  52. package/dist/scheduler/index.js +42 -4
  53. package/dist/scheduler/index.js.map +1 -1
  54. package/dist/server/compress/index.d.ts.map +1 -1
  55. package/dist/server/compress/index.js +1 -0
  56. package/dist/server/compress/index.js.map +1 -1
  57. package/dist/server/core/index.d.ts +9 -9
  58. package/dist/server/links/index.js +1 -1
  59. package/dist/server/links/index.js.map +1 -1
  60. package/dist/vite/index.d.ts +2 -1
  61. package/dist/vite/index.d.ts.map +1 -1
  62. package/dist/vite/index.js +28 -2
  63. package/dist/vite/index.js.map +1 -1
  64. package/dist/websocket/index.d.ts +34 -34
  65. package/dist/websocket/index.d.ts.map +1 -1
  66. package/package.json +6 -3
  67. package/src/api/audits/controllers/AdminAuditController.ts +8 -0
  68. package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
  69. package/src/api/jobs/controllers/AdminJobController.ts +3 -0
  70. package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
  71. package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
  72. package/src/api/users/controllers/AdminIdentityController.ts +3 -0
  73. package/src/api/users/controllers/AdminSessionController.ts +3 -0
  74. package/src/api/users/controllers/AdminUserController.ts +5 -0
  75. package/src/cli/commands/build.ts +1 -0
  76. package/src/cli/providers/ViteDevServerProvider.ts +31 -0
  77. package/src/email/index.workerd.ts +36 -0
  78. package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
  79. package/src/lock/core/primitives/$lock.ts +13 -1
  80. package/src/react/auth/services/ReactAuth.ts +3 -1
  81. package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
  82. package/src/react/router/providers/ReactServerProvider.ts +14 -4
  83. package/src/react/router/providers/SSRManifestProvider.ts +7 -0
  84. package/src/scheduler/index.workerd.ts +43 -0
  85. package/src/scheduler/providers/CronProvider.ts +53 -6
  86. package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
  87. package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
  88. package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
  89. package/src/server/links/providers/ServerLinksProvider.ts +1 -1
  90. package/src/vite/tasks/generateCloudflare.ts +38 -2
@@ -0,0 +1,43 @@
1
+ import { $module } from "alepha";
2
+ import { AlephaLock } from "alepha/lock";
3
+ import { $scheduler } from "./primitives/$scheduler.ts";
4
+ import { CronProvider } from "./providers/CronProvider.ts";
5
+ import { WorkerdCronProvider } from "./providers/WorkerdCronProvider.ts";
6
+
7
+ // ---------------------------------------------------------------------------------------------------------------------
8
+
9
+ export * from "./constants/CRON.ts";
10
+ export * from "./primitives/$scheduler.ts";
11
+ export * from "./providers/CronProvider.ts";
12
+ export * from "./providers/WorkerdCronProvider.ts";
13
+
14
+ // ---------------------------------------------------------------------------------------------------------------------
15
+
16
+ declare module "alepha" {
17
+ interface Hooks {
18
+ /**
19
+ * Cloudflare Workers scheduled event.
20
+ *
21
+ * Emitted when a cron trigger fires in Cloudflare Workers.
22
+ */
23
+ "cloudflare:scheduled": {
24
+ cron: string;
25
+ scheduledTime: number;
26
+ };
27
+ }
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------------------------------------------------
31
+
32
+ export const AlephaScheduler = $module({
33
+ name: "alepha.scheduler",
34
+ primitives: [$scheduler],
35
+ services: [AlephaLock, CronProvider, WorkerdCronProvider],
36
+ register: (alepha) => {
37
+ // Replace CronProvider with WorkerdCronProvider for Cloudflare Workers
38
+ alepha.with({
39
+ provide: CronProvider,
40
+ use: WorkerdCronProvider,
41
+ });
42
+ },
43
+ });
@@ -16,6 +16,11 @@ export class CronProvider {
16
16
  protected readonly start = $hook({
17
17
  on: "start",
18
18
  handler: () => {
19
+ if (this.alepha.isServerless()) {
20
+ this.log.info("Ignoring cron jobs in serverless environment");
21
+ return;
22
+ }
23
+
19
24
  for (const cron of this.cronJobs) {
20
25
  if (!cron.running) {
21
26
  cron.running = true;
@@ -62,12 +67,12 @@ export class CronProvider {
62
67
  ? this.cronJobs.find((c) => c.name === name)
63
68
  : name;
64
69
 
65
- if (!cron) {
70
+ if (!cron || !cron.running) {
66
71
  return;
67
72
  }
68
73
 
69
74
  cron.running = false;
70
- cron.abort.abort();
75
+ cron.abort?.abort();
71
76
  this.log.debug(`Cron task '${cron.name}' stopped`);
72
77
  }
73
78
 
@@ -109,13 +114,13 @@ export class CronProvider {
109
114
  }
110
115
 
111
116
  const duration = next.getTime() - now.toDate().getTime();
112
-
113
- task.abort = new AbortController();
117
+ const abort = new AbortController();
118
+ task.abort = abort;
114
119
 
115
120
  this.dt
116
121
  .wait(duration, {
117
122
  now: now.valueOf(),
118
- signal: task.abort.signal,
123
+ signal: abort.signal,
119
124
  })
120
125
  .then(() => {
121
126
  if (!task.running) {
@@ -141,6 +146,48 @@ export class CronProvider {
141
146
  this.log.warn("Issue during cron waiting timer", err as Error);
142
147
  });
143
148
  }
149
+
150
+ /**
151
+ * Trigger a specific cron job by name.
152
+ */
153
+ public async trigger(name: string): Promise<void> {
154
+ const job = this.cronJobs.find((j) => j.name === name);
155
+ if (!job) {
156
+ this.log.warn(`Cron job '${name}' not found`);
157
+ return;
158
+ }
159
+ await this.runJobs([job], this.dt.now());
160
+ }
161
+
162
+ /**
163
+ * Trigger all registered cron jobs.
164
+ */
165
+ public async triggerAll(): Promise<void> {
166
+ await this.runJobs(this.cronJobs, this.dt.now());
167
+ }
168
+
169
+ /**
170
+ * Run multiple cron jobs in parallel.
171
+ */
172
+ protected async runJobs(jobs: CronJob[], now: DateTime): Promise<void> {
173
+ const results = await Promise.allSettled(
174
+ jobs.map(async (job) => {
175
+ this.log.debug(`Running cron job '${job.name}'`);
176
+ try {
177
+ await job.handler({ now });
178
+ this.log.debug(`Cron job '${job.name}' completed`);
179
+ } catch (error) {
180
+ this.log.error(`Cron job '${job.name}' failed`, error);
181
+ throw error;
182
+ }
183
+ }),
184
+ );
185
+
186
+ const failures = results.filter((r) => r.status === "rejected");
187
+ if (failures.length > 0) {
188
+ this.log.error(`${failures.length}/${jobs.length} cron jobs failed`);
189
+ }
190
+ }
144
191
  }
145
192
 
146
193
  export interface CronJob {
@@ -151,5 +198,5 @@ export interface CronJob {
151
198
  loop: boolean;
152
199
  running?: boolean;
153
200
  onError?: (error: Error) => void;
154
- abort: AbortController;
201
+ abort?: AbortController;
155
202
  }
@@ -0,0 +1,102 @@
1
+ import { $hook } from "alepha";
2
+ import type { DateTime } from "alepha/datetime";
3
+ import { parseCronExpression } from "cron-schedule";
4
+ import { CronProvider } from "./CronProvider.ts";
5
+
6
+ // ---------------------------------------------------------------------------------------------------------------------
7
+
8
+ declare module "alepha" {
9
+ interface Hooks {
10
+ /**
11
+ * Cloudflare Workers scheduled event.
12
+ *
13
+ * Emitted when a cron trigger fires in Cloudflare Workers.
14
+ */
15
+ "cloudflare:scheduled": {
16
+ cron: string;
17
+ scheduledTime: number;
18
+ };
19
+ }
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Cloudflare Workers cron provider.
26
+ *
27
+ * This provider handles scheduled events from Cloudflare Workers Cron Triggers.
28
+ * Unlike the Node.js CronProvider, this doesn't use intervals/timeouts - instead,
29
+ * it reacts to scheduled events triggered by Cloudflare.
30
+ *
31
+ * **Usage:**
32
+ * 1. Define schedulers with `$scheduler({ cron: "0 * * * *", handler: ... })`
33
+ * 2. Build your app with `alepha build` - cron triggers are automatically added to `wrangler.jsonc`
34
+ * 3. Deploy to Cloudflare Workers
35
+ *
36
+ * **How it works:**
37
+ * - During build, all registered `$scheduler` cron expressions are collected
38
+ * - The build generates `wrangler.jsonc` with `triggers.crons` automatically filled
39
+ * - When Cloudflare fires a cron trigger, the `scheduled` handler emits `cloudflare:scheduled`
40
+ * - This provider listens to that event and runs matching schedulers
41
+ *
42
+ * @see https://developers.cloudflare.com/workers/configuration/cron-triggers/
43
+ */
44
+ export class WorkerdCronProvider extends CronProvider {
45
+ /**
46
+ * Override to avoid creating AbortController in global scope.
47
+ * Cloudflare Workers doesn't allow this during initialization.
48
+ */
49
+ public override createCronJob(
50
+ name: string,
51
+ expression: string,
52
+ handler: (context: { now: DateTime }) => Promise<void>,
53
+ ): void {
54
+ this.cronJobs.push({
55
+ name,
56
+ cron: parseCronExpression(expression),
57
+ expression,
58
+ handler,
59
+ loop: false,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Handle a scheduled event from Cloudflare Workers.
65
+ */
66
+ protected readonly onScheduledEvent = $hook({
67
+ on: "cloudflare:scheduled",
68
+ handler: async (event) => {
69
+ const now = this.dt.of(event.scheduledTime);
70
+
71
+ this.log.info("Received scheduled event", {
72
+ cron: event.cron,
73
+ scheduledTime: now.format(),
74
+ });
75
+
76
+ // Find jobs that match this cron expression
77
+ const matchingJobs = this.cronJobs.filter(
78
+ (job) => job.expression === event.cron,
79
+ );
80
+
81
+ if (matchingJobs.length === 0) {
82
+ // No exact match - try to find jobs that would fire at this time
83
+ const matchingByTime = this.cronJobs.filter((job) =>
84
+ job.cron.matchDate(now.toDate()),
85
+ );
86
+
87
+ if (matchingByTime.length > 0) {
88
+ this.log.debug(
89
+ `No exact cron match for '${event.cron}', found ${matchingByTime.length} jobs matching by time`,
90
+ );
91
+ await this.runJobs(matchingByTime, now);
92
+ return;
93
+ }
94
+
95
+ this.log.warn(`No cron jobs found for expression '${event.cron}'`);
96
+ return;
97
+ }
98
+
99
+ await this.runJobs(matchingJobs, now);
100
+ },
101
+ });
102
+ }
@@ -62,6 +62,12 @@ export class ServerCompressProvider {
62
62
  public readonly onResponse = $hook({
63
63
  on: "server:onResponse",
64
64
  handler: async ({ request, response }) => {
65
+ // In serverless (Cloudflare Workers), skip compression entirely:
66
+ // Cloudflare's edge network automatically compresses responses
67
+ if (this.alepha.isServerless()) {
68
+ return;
69
+ }
70
+
65
71
  // skip if already compressed
66
72
  if (response.headers["content-encoding"]) {
67
73
  return;
@@ -0,0 +1,332 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Alepha, t } from "alepha";
3
+ import { $issuer, AlephaSecurity } from "alepha/security";
4
+ import { $action, ServerProvider } from "alepha/server";
5
+ import { describe, it } from "vitest";
6
+ import { LinkProvider, ServerLinksProvider } from "../index.ts";
7
+
8
+ describe("ServerLinksProvider", () => {
9
+ describe("secured field in links", () => {
10
+ it("should set secured=undefined for public actions (no secure option)", async ({
11
+ expect,
12
+ }) => {
13
+ class App {
14
+ publicAction = $action({
15
+ handler: () => "PUBLIC",
16
+ });
17
+ }
18
+
19
+ const alepha = Alepha.create().with(App).with(ServerLinksProvider);
20
+ await alepha.start();
21
+
22
+ const links = alepha.inject(LinkProvider).getServerLinks();
23
+ const link = links.find((l) => l.name === "publicAction");
24
+
25
+ expect(link).toBeDefined();
26
+ expect(link?.secured).toBeUndefined();
27
+ });
28
+
29
+ it("should set secured=false for explicitly public actions", async ({
30
+ expect,
31
+ }) => {
32
+ class App {
33
+ publicAction = $action({
34
+ secure: false,
35
+ handler: () => "PUBLIC",
36
+ });
37
+ }
38
+
39
+ const alepha = Alepha.create().with(App).with(ServerLinksProvider);
40
+ await alepha.start();
41
+
42
+ const links = alepha.inject(LinkProvider).getServerLinks();
43
+ const link = links.find((l) => l.name === "publicAction");
44
+
45
+ expect(link).toBeDefined();
46
+ expect(link?.secured).toBe(false);
47
+ });
48
+
49
+ it("should set secured=true for secured actions", async ({ expect }) => {
50
+ class App {
51
+ securedAction = $action({
52
+ secure: true,
53
+ handler: () => "SECURED",
54
+ });
55
+ }
56
+
57
+ const alepha = Alepha.create().with(App).with(ServerLinksProvider);
58
+ await alepha.start();
59
+
60
+ const links = alepha.inject(LinkProvider).getServerLinks();
61
+ const link = links.find((l) => l.name === "securedAction");
62
+
63
+ expect(link).toBeDefined();
64
+ expect(link?.secured).toBe(true);
65
+ });
66
+
67
+ it("should set secured to object for realm-secured actions", async ({
68
+ expect,
69
+ }) => {
70
+ class App {
71
+ realmAction = $action({
72
+ secure: { realm: "admin" },
73
+ handler: () => "REALM",
74
+ });
75
+ }
76
+
77
+ const alepha = Alepha.create().with(App).with(ServerLinksProvider);
78
+ await alepha.start();
79
+
80
+ const links = alepha.inject(LinkProvider).getServerLinks();
81
+ const link = links.find((l) => l.name === "realmAction");
82
+
83
+ expect(link).toBeDefined();
84
+ expect(link?.secured).toEqual({ realm: "admin" });
85
+ });
86
+ });
87
+
88
+ describe("/_links endpoint with security", () => {
89
+ it("should return public actions to unauthenticated users", async ({
90
+ expect,
91
+ }) => {
92
+ class App {
93
+ publicAction = $action({
94
+ schema: { response: t.text() },
95
+ handler: () => "PUBLIC",
96
+ });
97
+ issuer = $issuer({
98
+ secret: "test",
99
+ roles: [{ name: "user", permissions: [{ name: "*" }] }],
100
+ });
101
+ }
102
+
103
+ const alepha = Alepha.create()
104
+ .with(App)
105
+ .with(ServerLinksProvider)
106
+ .with(AlephaSecurity);
107
+ await alepha.start();
108
+
109
+ const res = await fetch(
110
+ `${alepha.inject(ServerProvider).hostname}/api/_links`,
111
+ );
112
+ const data = await res.json();
113
+
114
+ expect(data.links).toContainEqual(
115
+ expect.objectContaining({
116
+ name: "publicAction",
117
+ path: "/publicAction",
118
+ }),
119
+ );
120
+ });
121
+
122
+ it("should NOT return secured actions to unauthenticated users", async ({
123
+ expect,
124
+ }) => {
125
+ class App {
126
+ securedAction = $action({
127
+ secure: true,
128
+ schema: { response: t.text() },
129
+ handler: () => "SECURED",
130
+ });
131
+ issuer = $issuer({
132
+ secret: "test",
133
+ roles: [{ name: "user", permissions: [{ name: "*" }] }],
134
+ });
135
+ }
136
+
137
+ const alepha = Alepha.create()
138
+ .with(App)
139
+ .with(ServerLinksProvider)
140
+ .with(AlephaSecurity);
141
+ await alepha.start();
142
+
143
+ const res = await fetch(
144
+ `${alepha.inject(ServerProvider).hostname}/api/_links`,
145
+ );
146
+ const data = await res.json();
147
+
148
+ expect(data.links).not.toContainEqual(
149
+ expect.objectContaining({
150
+ name: "securedAction",
151
+ }),
152
+ );
153
+ });
154
+
155
+ it("should return secured actions to authenticated users with permissions", async ({
156
+ expect,
157
+ }) => {
158
+ class App {
159
+ securedAction = $action({
160
+ secure: true,
161
+ schema: { response: t.text() },
162
+ handler: () => "SECURED",
163
+ });
164
+ issuer = $issuer({
165
+ secret: "test",
166
+ roles: [{ name: "user", permissions: [{ name: "*" }] }],
167
+ });
168
+ }
169
+
170
+ const alepha = Alepha.create()
171
+ .with(App)
172
+ .with(ServerLinksProvider)
173
+ .with(AlephaSecurity);
174
+ await alepha.start();
175
+
176
+ // Use HttpClient to get a token automatically in test mode
177
+ const app = alepha.inject(App);
178
+ const { data } = await app.securedAction.fetch(
179
+ {},
180
+ {
181
+ user: { id: randomUUID(), roles: ["user"] },
182
+ },
183
+ );
184
+ expect(data).toBe("SECURED");
185
+ });
186
+
187
+ it("should return both public and secured actions when user is authenticated", async ({
188
+ expect,
189
+ }) => {
190
+ class App {
191
+ publicAction = $action({
192
+ schema: { response: t.text() },
193
+ handler: () => "PUBLIC",
194
+ });
195
+ securedAction = $action({
196
+ secure: true,
197
+ schema: { response: t.text() },
198
+ handler: () => "SECURED",
199
+ });
200
+ issuer = $issuer({
201
+ secret: "test",
202
+ roles: [{ name: "user", permissions: [{ name: "*" }] }],
203
+ });
204
+ }
205
+
206
+ const alepha = Alepha.create()
207
+ .with(App)
208
+ .with(ServerLinksProvider)
209
+ .with(AlephaSecurity);
210
+ await alepha.start();
211
+
212
+ const linksProvider = alepha.inject(ServerLinksProvider);
213
+ const user = { id: randomUUID(), roles: ["user"] };
214
+
215
+ const { links } = await linksProvider.getUserApiLinks({ user });
216
+
217
+ expect(links).toContainEqual(
218
+ expect.objectContaining({ name: "publicAction" }),
219
+ );
220
+ expect(links).toContainEqual(
221
+ expect.objectContaining({ name: "securedAction" }),
222
+ );
223
+ });
224
+
225
+ it("should filter secured actions based on user permissions", async ({
226
+ expect,
227
+ }) => {
228
+ class App {
229
+ adminOnly = $action({
230
+ secure: true,
231
+ group: "admin",
232
+ schema: { response: t.text() },
233
+ handler: () => "ADMIN",
234
+ });
235
+ userAction = $action({
236
+ secure: true,
237
+ group: "user",
238
+ schema: { response: t.text() },
239
+ handler: () => "USER",
240
+ });
241
+ issuer = $issuer({
242
+ secret: "test",
243
+ roles: [
244
+ { name: "admin", permissions: [{ name: "*" }] },
245
+ { name: "user", permissions: [{ name: "user:*" }] },
246
+ ],
247
+ });
248
+ }
249
+
250
+ const alepha = Alepha.create()
251
+ .with(App)
252
+ .with(ServerLinksProvider)
253
+ .with(AlephaSecurity);
254
+ await alepha.start();
255
+
256
+ const linksProvider = alepha.inject(ServerLinksProvider);
257
+
258
+ // User with "user" role should only see userAction
259
+ const userLinks = await linksProvider.getUserApiLinks({
260
+ user: { id: randomUUID(), roles: ["user"] },
261
+ });
262
+
263
+ expect(userLinks.links).toContainEqual(
264
+ expect.objectContaining({ name: "userAction" }),
265
+ );
266
+ expect(userLinks.links).not.toContainEqual(
267
+ expect.objectContaining({ name: "adminOnly" }),
268
+ );
269
+
270
+ // User with "admin" role should see both
271
+ const adminLinks = await linksProvider.getUserApiLinks({
272
+ user: { id: randomUUID(), roles: ["admin"] },
273
+ });
274
+
275
+ expect(adminLinks.links).toContainEqual(
276
+ expect.objectContaining({ name: "userAction" }),
277
+ );
278
+ expect(adminLinks.links).toContainEqual(
279
+ expect.objectContaining({ name: "adminOnly" }),
280
+ );
281
+ });
282
+ });
283
+
284
+ describe("mixed public and secured actions", () => {
285
+ it("should correctly differentiate public from secured in server links", async ({
286
+ expect,
287
+ }) => {
288
+ class App {
289
+ getUsers = $action({
290
+ path: "/users",
291
+ schema: { response: t.array(t.text()) },
292
+ handler: () => ["user1", "user2"],
293
+ });
294
+ createUser = $action({
295
+ path: "/users",
296
+ secure: true,
297
+ schema: {
298
+ body: t.object({ name: t.text() }),
299
+ response: t.text(),
300
+ },
301
+ handler: ({ body }) => body.name,
302
+ });
303
+ deleteUser = $action({
304
+ method: "DELETE",
305
+ path: "/users/:id",
306
+ secure: true,
307
+ schema: {
308
+ params: t.object({ id: t.text() }),
309
+ response: t.void(),
310
+ },
311
+ handler: () => {},
312
+ });
313
+ }
314
+
315
+ const alepha = Alepha.create().with(App).with(ServerLinksProvider);
316
+ await alepha.start();
317
+
318
+ const links = alepha.inject(LinkProvider).getServerLinks();
319
+
320
+ const getUsers = links.find((l) => l.name === "getUsers");
321
+ const createUser = links.find((l) => l.name === "createUser");
322
+ const deleteUser = links.find((l) => l.name === "deleteUser");
323
+
324
+ // getUsers is public (no secure option)
325
+ expect(getUsers?.secured).toBeUndefined();
326
+
327
+ // createUser and deleteUser are secured
328
+ expect(createUser?.secured).toBe(true);
329
+ expect(deleteUser?.secured).toBe(true);
330
+ });
331
+ });
332
+ });
@@ -48,7 +48,7 @@ export class ServerLinksProvider {
48
48
  group: action.group,
49
49
  schema: action.options.schema,
50
50
  requestBodyType: action.getBodyContentType(),
51
- secured: action.options.secure ?? true,
51
+ secured: action.options.secure,
52
52
  method: action.method === "GET" ? undefined : action.method,
53
53
  prefix: action.prefix,
54
54
  path: action.path,
@@ -1,5 +1,8 @@
1
1
  import { access, writeFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
+ import type { Alepha } from "alepha";
4
+ import type { CronProvider } from "alepha/scheduler";
5
+ import type { WorkerdCronProvider } from "../../scheduler/providers/WorkerdCronProvider.ts";
3
6
 
4
7
  export interface GenerateCloudflareOptions {
5
8
  /**
@@ -13,6 +16,8 @@ export interface GenerateCloudflareOptions {
13
16
  * Additional Wrangler configuration options to merge into wrangler.jsonc.
14
17
  */
15
18
  config?: WranglerConfig;
19
+
20
+ alepha: Alepha;
16
21
  }
17
22
 
18
23
  export interface WranglerConfig {
@@ -31,7 +36,7 @@ const WARNING_COMMENT =
31
36
  * - worker.js entry point for Cloudflare Workers
32
37
  */
33
38
  export async function generateCloudflare(
34
- opts: GenerateCloudflareOptions = {},
39
+ opts: GenerateCloudflareOptions,
35
40
  ): Promise<void> {
36
41
  const distDir = opts.distDir ?? "dist";
37
42
  const root = process.cwd();
@@ -40,6 +45,15 @@ export async function generateCloudflare(
40
45
  .then(() => true)
41
46
  .catch(() => false);
42
47
 
48
+ let workerdCronProvider: CronProvider | undefined;
49
+ try {
50
+ workerdCronProvider = opts.alepha.inject(
51
+ "CronProvider",
52
+ ) as WorkerdCronProvider;
53
+ } catch {}
54
+
55
+ const crons = workerdCronProvider?.getCronJobs();
56
+
43
57
  const wrangler: WranglerConfig = {
44
58
  name,
45
59
  main: "./main.cloudflare.js",
@@ -62,6 +76,12 @@ export async function generateCloudflare(
62
76
  };
63
77
  }
64
78
 
79
+ if (crons && crons.length > 0) {
80
+ const cronExpressions = [...new Set(crons.map((c) => c.expression))];
81
+ wrangler.triggers ??= {};
82
+ wrangler.triggers.crons = cronExpressions;
83
+ }
84
+
65
85
  const url = process.env.DATABASE_URL;
66
86
  if (url?.startsWith("d1:")) {
67
87
  const [name, id] = url.replace("d1://", "").replace("d1:", "").split(":");
@@ -102,7 +122,7 @@ export default {
102
122
  try {
103
123
  await __alepha.start();
104
124
  } catch (err) {
105
- console.error(err);
125
+ console.error("Failed to start Alepha for fetch event", err);
106
126
  return new Response("Internal Server Error", { status: 500 });
107
127
  }
108
128
 
@@ -110,6 +130,22 @@ export default {
110
130
 
111
131
  return ctx.res;
112
132
  },
133
+
134
+ scheduled: async (event, env, ctx) => {
135
+ __alepha.set("cloudflare.env", env);
136
+
137
+ try {
138
+ await __alepha.start();
139
+ } catch (err) {
140
+ console.error("Failed to start Alepha for scheduled event", err);
141
+ throw err;
142
+ }
143
+
144
+ await __alepha.events.emit("cloudflare:scheduled", {
145
+ cron: event.cron,
146
+ scheduledTime: event.scheduledTime,
147
+ });
148
+ },
113
149
  };
114
150
  `.trim();
115
151