alepha 0.15.2 → 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 (180) hide show
  1. package/README.md +68 -80
  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.d.ts +170 -170
  6. package/dist/api/files/index.d.ts.map +1 -1
  7. package/dist/api/files/index.js +1 -0
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js +3 -0
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/notifications/index.browser.js +1 -0
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.js +1 -0
  15. package/dist/api/notifications/index.js.map +1 -1
  16. package/dist/api/parameters/index.d.ts +260 -260
  17. package/dist/api/parameters/index.d.ts.map +1 -1
  18. package/dist/api/parameters/index.js +10 -0
  19. package/dist/api/parameters/index.js.map +1 -1
  20. package/dist/api/users/index.d.ts +12 -1
  21. package/dist/api/users/index.d.ts.map +1 -1
  22. package/dist/api/users/index.js +18 -2
  23. package/dist/api/users/index.js.map +1 -1
  24. package/dist/batch/index.d.ts +4 -4
  25. package/dist/bucket/index.d.ts +8 -0
  26. package/dist/bucket/index.d.ts.map +1 -1
  27. package/dist/bucket/index.js +7 -2
  28. package/dist/bucket/index.js.map +1 -1
  29. package/dist/cli/index.d.ts +196 -74
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +234 -50
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/command/index.d.ts +10 -0
  34. package/dist/command/index.d.ts.map +1 -1
  35. package/dist/command/index.js +67 -13
  36. package/dist/command/index.js.map +1 -1
  37. package/dist/core/index.browser.js +28 -21
  38. package/dist/core/index.browser.js.map +1 -1
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +28 -21
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/index.native.js +28 -21
  43. package/dist/core/index.native.js.map +1 -1
  44. package/dist/email/index.d.ts +21 -13
  45. package/dist/email/index.d.ts.map +1 -1
  46. package/dist/email/index.js +10561 -4
  47. package/dist/email/index.js.map +1 -1
  48. package/dist/lock/core/index.d.ts +6 -1
  49. package/dist/lock/core/index.d.ts.map +1 -1
  50. package/dist/lock/core/index.js +9 -1
  51. package/dist/lock/core/index.js.map +1 -1
  52. package/dist/mcp/index.d.ts +5 -5
  53. package/dist/orm/index.bun.js +32 -16
  54. package/dist/orm/index.bun.js.map +1 -1
  55. package/dist/orm/index.d.ts +4 -1
  56. package/dist/orm/index.d.ts.map +1 -1
  57. package/dist/orm/index.js +34 -22
  58. package/dist/orm/index.js.map +1 -1
  59. package/dist/react/auth/index.browser.js +2 -1
  60. package/dist/react/auth/index.browser.js.map +1 -1
  61. package/dist/react/auth/index.js +2 -1
  62. package/dist/react/auth/index.js.map +1 -1
  63. package/dist/react/core/index.d.ts +3 -3
  64. package/dist/react/router/index.browser.js +9 -15
  65. package/dist/react/router/index.browser.js.map +1 -1
  66. package/dist/react/router/index.d.ts +305 -407
  67. package/dist/react/router/index.d.ts.map +1 -1
  68. package/dist/react/router/index.js +581 -781
  69. package/dist/react/router/index.js.map +1 -1
  70. package/dist/scheduler/index.d.ts +13 -1
  71. package/dist/scheduler/index.d.ts.map +1 -1
  72. package/dist/scheduler/index.js +42 -4
  73. package/dist/scheduler/index.js.map +1 -1
  74. package/dist/security/index.d.ts +42 -42
  75. package/dist/security/index.d.ts.map +1 -1
  76. package/dist/security/index.js +8 -7
  77. package/dist/security/index.js.map +1 -1
  78. package/dist/server/auth/index.d.ts +167 -167
  79. package/dist/server/compress/index.d.ts.map +1 -1
  80. package/dist/server/compress/index.js +1 -0
  81. package/dist/server/compress/index.js.map +1 -1
  82. package/dist/server/health/index.d.ts +17 -17
  83. package/dist/server/links/index.d.ts +39 -39
  84. package/dist/server/links/index.js +1 -1
  85. package/dist/server/links/index.js.map +1 -1
  86. package/dist/server/static/index.js +7 -2
  87. package/dist/server/static/index.js.map +1 -1
  88. package/dist/server/swagger/index.d.ts +8 -0
  89. package/dist/server/swagger/index.d.ts.map +1 -1
  90. package/dist/server/swagger/index.js +7 -2
  91. package/dist/server/swagger/index.js.map +1 -1
  92. package/dist/sms/index.d.ts +8 -0
  93. package/dist/sms/index.d.ts.map +1 -1
  94. package/dist/sms/index.js +7 -2
  95. package/dist/sms/index.js.map +1 -1
  96. package/dist/system/index.browser.js +734 -12
  97. package/dist/system/index.browser.js.map +1 -1
  98. package/dist/system/index.d.ts +8 -0
  99. package/dist/system/index.d.ts.map +1 -1
  100. package/dist/system/index.js +7 -2
  101. package/dist/system/index.js.map +1 -1
  102. package/dist/vite/index.d.ts +3 -2
  103. package/dist/vite/index.d.ts.map +1 -1
  104. package/dist/vite/index.js +42 -8
  105. package/dist/vite/index.js.map +1 -1
  106. package/dist/websocket/index.d.ts +34 -34
  107. package/dist/websocket/index.d.ts.map +1 -1
  108. package/package.json +9 -4
  109. package/src/api/audits/controllers/AdminAuditController.ts +8 -0
  110. package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
  111. package/src/api/jobs/controllers/AdminJobController.ts +3 -0
  112. package/src/api/logs/TODO.md +13 -10
  113. package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
  114. package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
  115. package/src/api/users/controllers/AdminIdentityController.ts +3 -0
  116. package/src/api/users/controllers/AdminSessionController.ts +3 -0
  117. package/src/api/users/controllers/AdminUserController.ts +5 -0
  118. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
  119. package/src/cli/atoms/buildOptions.ts +99 -9
  120. package/src/cli/commands/build.ts +150 -32
  121. package/src/cli/commands/db.ts +5 -7
  122. package/src/cli/commands/init.spec.ts +50 -6
  123. package/src/cli/commands/init.ts +28 -5
  124. package/src/cli/providers/ViteDevServerProvider.ts +31 -9
  125. package/src/cli/services/AlephaCliUtils.ts +16 -0
  126. package/src/cli/services/PackageManagerUtils.ts +2 -0
  127. package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
  128. package/src/cli/services/ProjectScaffolder.ts +28 -6
  129. package/src/cli/templates/agentMd.ts +6 -1
  130. package/src/cli/templates/apiAppSecurityTs.ts +11 -0
  131. package/src/cli/templates/apiIndexTs.ts +18 -4
  132. package/src/cli/templates/webAppRouterTs.ts +25 -1
  133. package/src/cli/templates/webHelloComponentTsx.ts +15 -5
  134. package/src/command/helpers/Runner.spec.ts +135 -0
  135. package/src/command/helpers/Runner.ts +4 -1
  136. package/src/command/providers/CliProvider.spec.ts +325 -0
  137. package/src/command/providers/CliProvider.ts +117 -7
  138. package/src/core/Alepha.ts +32 -25
  139. package/src/email/index.workerd.ts +36 -0
  140. package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
  141. package/src/lock/core/primitives/$lock.ts +13 -1
  142. package/src/orm/index.bun.ts +1 -1
  143. package/src/orm/index.ts +2 -6
  144. package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
  145. package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
  146. package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
  147. package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
  148. package/src/react/auth/services/ReactAuth.ts +3 -1
  149. package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
  150. package/src/react/router/hooks/useActive.ts +1 -1
  151. package/src/react/router/hooks/useRouter.ts +1 -1
  152. package/src/react/router/index.ts +4 -0
  153. package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
  154. package/src/react/router/primitives/$page.spec.tsx +0 -32
  155. package/src/react/router/primitives/$page.ts +6 -14
  156. package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
  157. package/src/react/router/providers/ReactPageProvider.ts +1 -1
  158. package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
  159. package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
  160. package/src/react/router/providers/ReactServerProvider.ts +21 -82
  161. package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
  162. package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
  163. package/src/react/router/providers/SSRManifestProvider.ts +7 -0
  164. package/src/react/router/services/ReactRouter.ts +13 -13
  165. package/src/scheduler/index.workerd.ts +43 -0
  166. package/src/scheduler/providers/CronProvider.ts +53 -6
  167. package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
  168. package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
  169. package/src/security/providers/ServerSecurityProvider.ts +30 -22
  170. package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
  171. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
  172. package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
  173. package/src/server/links/providers/ServerLinksProvider.ts +1 -1
  174. package/src/system/index.browser.ts +25 -0
  175. package/src/system/index.workerd.ts +1 -0
  176. package/src/system/providers/FileSystemProvider.ts +8 -0
  177. package/src/system/providers/NodeFileSystemProvider.ts +11 -2
  178. package/src/vite/tasks/buildServer.ts +2 -12
  179. package/src/vite/tasks/generateCloudflare.ts +47 -8
  180. package/src/vite/tasks/generateDocker.ts +4 -0
@@ -32,6 +32,13 @@ export class SSRManifestProvider {
32
32
  );
33
33
  }
34
34
 
35
+ /**
36
+ * Get the full manifest object.
37
+ */
38
+ public getManifest(): Static<SsrManifestAtomSchema> {
39
+ return this.manifest;
40
+ }
41
+
35
42
  /**
36
43
  * Get the base path for assets (from Vite's base config).
37
44
  * Returns empty string if base is "/" (default), otherwise returns the base path.
@@ -7,7 +7,7 @@ import {
7
7
  type ReactRouterState,
8
8
  } from "../providers/ReactPageProvider.ts";
9
9
 
10
- export interface RouterGoOptions {
10
+ export interface RouterPushOptions {
11
11
  replace?: boolean;
12
12
  params?: Record<string, string>;
13
13
  query?: Record<string, string>;
@@ -114,7 +114,7 @@ export class ReactRouter<T extends object> {
114
114
  return;
115
115
  }
116
116
 
117
- await this.go(this.location.pathname + this.location.search, {
117
+ await this.push(this.location.pathname + this.location.search, {
118
118
  replace: true,
119
119
  force: true,
120
120
  });
@@ -168,18 +168,18 @@ export class ReactRouter<T extends object> {
168
168
  await this.browser?.invalidate(props);
169
169
  }
170
170
 
171
- public async go(path: string, options?: RouterGoOptions): Promise<void>;
172
- public async go(
171
+ public async push(path: string, options?: RouterPushOptions): Promise<void>;
172
+ public async push(
173
173
  path: keyof VirtualRouter<T>,
174
- options?: RouterGoOptions,
174
+ options?: RouterPushOptions,
175
175
  ): Promise<void>;
176
- public async go(
176
+ public async push(
177
177
  path: string | keyof VirtualRouter<T>,
178
- options?: RouterGoOptions,
178
+ options?: RouterPushOptions,
179
179
  ): Promise<void> {
180
180
  for (const page of this.pages) {
181
181
  if (page.name === path) {
182
- await this.browser?.go(
182
+ await this.browser?.push(
183
183
  this.path(path as keyof VirtualRouter<T>, options),
184
184
  options,
185
185
  );
@@ -187,17 +187,17 @@ export class ReactRouter<T extends object> {
187
187
  }
188
188
  }
189
189
 
190
- await this.browser?.go(path as string, options);
190
+ await this.browser?.push(path as string, options);
191
191
  }
192
192
 
193
- public anchor(path: string, options?: RouterGoOptions): AnchorProps;
193
+ public anchor(path: string, options?: RouterPushOptions): AnchorProps;
194
194
  public anchor(
195
195
  path: keyof VirtualRouter<T>,
196
- options?: RouterGoOptions,
196
+ options?: RouterPushOptions,
197
197
  ): AnchorProps;
198
198
  public anchor(
199
199
  path: string | keyof VirtualRouter<T>,
200
- options: RouterGoOptions = {},
200
+ options: RouterPushOptions = {},
201
201
  ): AnchorProps {
202
202
  let href = path as string;
203
203
 
@@ -214,7 +214,7 @@ export class ReactRouter<T extends object> {
214
214
  ev.stopPropagation();
215
215
  ev.preventDefault();
216
216
 
217
- this.go(href, options).catch(console.error);
217
+ this.push(href, options).catch(console.error);
218
218
  },
219
219
  };
220
220
  }
@@ -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
+ }
@@ -15,6 +15,7 @@ describe("ServerSecurityProvider", () => {
15
15
  it("should protect action from unauthorized users", async () => {
16
16
  class TestApp {
17
17
  ok = $action({
18
+ secure: true,
18
19
  handler: () => "OK",
19
20
  });
20
21
  }
@@ -63,10 +64,12 @@ describe("ServerSecurityProvider", () => {
63
64
  it("should guard by permission", async () => {
64
65
  class TestApp {
65
66
  admin = $action({
67
+ secure: true,
66
68
  group: "read",
67
69
  handler: () => "ADMIN",
68
70
  });
69
71
  user = $action({
72
+ secure: true,
70
73
  group: "read",
71
74
  handler: () => "USER",
72
75
  });
@@ -128,4 +131,78 @@ describe("ServerSecurityProvider", () => {
128
131
  await app.admin.fetch({}, { user: admin }).then((it) => it.data),
129
132
  ).toBe("ADMIN");
130
133
  });
134
+
135
+ it("should allow public actions by default (no secure option)", async () => {
136
+ class TestApp {
137
+ public = $action({
138
+ handler: () => "PUBLIC",
139
+ });
140
+ issuer = $issuer({
141
+ secret: "test",
142
+ roles: [{ name: "user", permissions: [{ name: "*" }] }],
143
+ });
144
+ }
145
+
146
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity);
147
+ const app = alepha.inject(TestApp);
148
+ await alepha.start();
149
+
150
+ // Should work without authentication via .run()
151
+ expect(await app.public.run({})).toBe("PUBLIC");
152
+
153
+ // Should work without authentication via .fetch()
154
+ expect(await app.public.fetch({}).then((it) => it.data)).toBe("PUBLIC");
155
+
156
+ // Should work via HTTP without token
157
+ const response = await fetch(
158
+ `${alepha.inject(ServerProvider).hostname}${app.public.route.path}`,
159
+ );
160
+ expect(response.status).toBe(200);
161
+ expect(await response.text()).toBe("PUBLIC");
162
+ });
163
+
164
+ it("should allow explicit secure: false", async () => {
165
+ class TestApp {
166
+ public = $action({
167
+ secure: false,
168
+ handler: () => "PUBLIC",
169
+ });
170
+ issuer = $issuer({
171
+ secret: "test",
172
+ roles: [{ name: "user", permissions: [{ name: "*" }] }],
173
+ });
174
+ }
175
+
176
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity);
177
+ const app = alepha.inject(TestApp);
178
+ await alepha.start();
179
+
180
+ // Should work without authentication
181
+ expect(await app.public.run({})).toBe("PUBLIC");
182
+ expect(await app.public.fetch({}).then((it) => it.data)).toBe("PUBLIC");
183
+ });
184
+
185
+ it("should require auth when secure: true is explicit", async () => {
186
+ class TestApp {
187
+ protected = $action({
188
+ secure: true,
189
+ handler: () => "PROTECTED",
190
+ });
191
+ issuer = $issuer({
192
+ secret: "test",
193
+ roles: [{ name: "user", permissions: [{ name: "*" }] }],
194
+ });
195
+ }
196
+
197
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity);
198
+ const app = alepha.inject(TestApp);
199
+ await alepha.start();
200
+
201
+ // Should fail without user
202
+ await expect(app.protected.run({})).rejects.toThrowError(UnauthorizedError);
203
+
204
+ // Should succeed with user
205
+ const user = { id: randomUUID(), roles: ["user"] };
206
+ expect(await app.protected.run({}, { user })).toBe("PROTECTED");
207
+ });
131
208
  });
@@ -31,25 +31,23 @@ export class ServerSecurityProvider {
31
31
  handler: async () => {
32
32
  for (const action of this.alepha.primitives($action)) {
33
33
  // -------------------------------------------------------------------------------------------------------------
34
- // if the action is disabled or not secure, we do NOT create a permission for it
34
+ // Only create permission when secure is explicitly set to true
35
+ // Actions are public by default (like $route)
35
36
  // -------------------------------------------------------------------------------------------------------------
36
37
  if (
37
38
  action.options.disabled ||
38
- action.options.secure === false ||
39
+ action.options.secure !== true ||
39
40
  this.securityProvider.getRealms().length === 0
40
41
  ) {
41
42
  continue;
42
43
  }
43
44
 
44
- const secure = action.options.secure;
45
- if (typeof secure !== "object") {
46
- this.securityProvider.createPermission({
47
- name: action.name,
48
- group: action.group,
49
- method: action.route.method,
50
- path: action.route.path,
51
- });
52
- }
45
+ this.securityProvider.createPermission({
46
+ name: action.name,
47
+ group: action.group,
48
+ method: action.route.method,
49
+ path: action.route.path,
50
+ });
53
51
  }
54
52
  },
55
53
  });
@@ -59,10 +57,12 @@ export class ServerSecurityProvider {
59
57
  protected readonly onActionRequest = $hook({
60
58
  on: "action:onRequest",
61
59
  handler: async ({ action, request, options }) => {
62
- // if you set explicitly secure: false, we assume you don't want any security check
63
- // but only if no user is provided in options
64
- if (action.options.secure === false && !options.user) {
65
- this.log.trace("Skipping security check for route");
60
+ const secure = action.options.secure;
61
+
62
+ // Skip security if not explicitly enabled (secure: true or secure: { realm: ... })
63
+ // Actions are public by default (like $route)
64
+ if (secure !== true && typeof secure !== "object" && !options.user) {
65
+ this.log.trace("Skipping security check for action - not secured");
66
66
  return;
67
67
  }
68
68
 
@@ -93,7 +93,7 @@ export class ServerSecurityProvider {
93
93
  this.alepha.codec.decode(userAccountInfoSchema, request.user),
94
94
  );
95
95
  } catch (error) {
96
- if (action.options.secure || permission) {
96
+ if (secure === true || typeof secure === "object" || permission) {
97
97
  throw error;
98
98
  }
99
99
  // else, we skip the security check
@@ -106,7 +106,7 @@ export class ServerSecurityProvider {
106
106
  on: "server:onRequest",
107
107
  priority: "last",
108
108
  handler: async ({ request, route }) => {
109
- // if you set explicitly secure: false, we assume you don't want any security check
109
+ // Skip entirely only if explicitly disabled
110
110
  if (route.secure === false) {
111
111
  this.log.trace(
112
112
  "Skipping security check for route - explicitly disabled",
@@ -126,7 +126,7 @@ export class ServerSecurityProvider {
126
126
  typeof route.secure === "object" ? route.secure.realm : undefined;
127
127
 
128
128
  try {
129
- // Try to resolve user (JWT, API key, etc.)
129
+ // Try to resolve user (JWT, API key, etc.) - even for public routes (optional auth)
130
130
  request.user = await this.securityProvider.resolveUserFromServerRequest(
131
131
  request,
132
132
  { permission, realm },
@@ -135,7 +135,11 @@ export class ServerSecurityProvider {
135
135
  // No user resolved?
136
136
  if (!request.user) {
137
137
  // Route requires auth → throw
138
- if (route.secure || permission) {
138
+ if (
139
+ route.secure === true ||
140
+ typeof route.secure === "object" ||
141
+ permission
142
+ ) {
139
143
  // Provide a more specific error message when no auth header was provided
140
144
  if (!request.headers.authorization) {
141
145
  throw new InvalidTokenError(
@@ -144,7 +148,7 @@ export class ServerSecurityProvider {
144
148
  }
145
149
  throw new UnauthorizedError("Authentication required");
146
150
  }
147
- // Route is public → skip
151
+ // Route is public → skip (but we tried to resolve user for optional auth)
148
152
  this.log.trace(
149
153
  "Skipping security check for route - no auth provided and not required",
150
154
  );
@@ -166,11 +170,15 @@ export class ServerSecurityProvider {
166
170
  permission,
167
171
  });
168
172
  } catch (error) {
169
- if (route.secure || permission) {
173
+ if (
174
+ route.secure === true ||
175
+ typeof route.secure === "object" ||
176
+ permission
177
+ ) {
170
178
  throw error;
171
179
  }
172
180
 
173
- // else, we skip the security check
181
+ // else, we skip the security check (route is public)
174
182
  this.log.trace(
175
183
  "Skipping security check for route - error occurred",
176
184
  error,
@@ -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;
@@ -31,7 +31,9 @@ describe("NodeHttpServerProvider", () => {
31
31
  });
32
32
 
33
33
  test("production mode: waits for connections then closes", async () => {
34
- alepha = Alepha.create({ env: { NODE_ENV: "production" } });
34
+ alepha = Alepha.create({
35
+ env: { NODE_ENV: "production", SERVER_PORT: 0 },
36
+ });
35
37
  alepha.with(NodeHttpServerProvider);
36
38
 
37
39
  await alepha.start();
@@ -52,7 +54,9 @@ describe("NodeHttpServerProvider", () => {
52
54
  });
53
55
 
54
56
  test("production mode: forces close after timeout", async () => {
55
- alepha = Alepha.create({ env: { NODE_ENV: "production" } });
57
+ alepha = Alepha.create({
58
+ env: { NODE_ENV: "production", SERVER_PORT: 0 },
59
+ });
56
60
  alepha.with(NodeHttpServerProvider);
57
61
 
58
62
  await alepha.start();
@@ -89,7 +93,9 @@ describe("NodeHttpServerProvider", () => {
89
93
  });
90
94
 
91
95
  test("rejects new requests during shutdown", async () => {
92
- alepha = Alepha.create({ env: { NODE_ENV: "production" } });
96
+ alepha = Alepha.create({
97
+ env: { NODE_ENV: "production", SERVER_PORT: 0 },
98
+ });
93
99
  alepha.with(NodeHttpServerProvider);
94
100
 
95
101
  await alepha.start();