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
@@ -25,6 +25,7 @@ export class AdminAuditController {
25
25
  public readonly findAudits = $action({
26
26
  path: this.url,
27
27
  group: this.group,
28
+ secure: true,
28
29
  description: "Find audit entries with filtering and pagination",
29
30
  schema: {
30
31
  query: auditQuerySchema,
@@ -39,6 +40,7 @@ export class AdminAuditController {
39
40
  public readonly getAudit = $action({
40
41
  path: `${this.url}/:id`,
41
42
  group: this.group,
43
+ secure: true,
42
44
  description: "Get a single audit entry by ID",
43
45
  schema: {
44
46
  params: t.object({
@@ -56,6 +58,7 @@ export class AdminAuditController {
56
58
  method: "POST",
57
59
  path: this.url,
58
60
  group: this.group,
61
+ secure: true,
59
62
  description: "Create a new audit entry",
60
63
  schema: {
61
64
  body: createAuditSchema,
@@ -70,6 +73,7 @@ export class AdminAuditController {
70
73
  public readonly findByUser = $action({
71
74
  path: `${this.url}/user/:userId`,
72
75
  group: this.group,
76
+ secure: true,
73
77
  description: "Get audit entries for a specific user",
74
78
  schema: {
75
79
  params: t.object({
@@ -88,6 +92,7 @@ export class AdminAuditController {
88
92
  public readonly findByResource = $action({
89
93
  path: `${this.url}/resource/:resourceType/:resourceId`,
90
94
  group: this.group,
95
+ secure: true,
91
96
  description: "Get audit entries for a specific resource",
92
97
  schema: {
93
98
  params: t.object({
@@ -111,6 +116,7 @@ export class AdminAuditController {
111
116
  public readonly getStats = $action({
112
117
  path: `${this.url}/stats`,
113
118
  group: this.group,
119
+ secure: true,
114
120
  description: "Get audit statistics for a time period",
115
121
  schema: {
116
122
  query: t.object({
@@ -144,6 +150,7 @@ export class AdminAuditController {
144
150
  public readonly getTypes = $action({
145
151
  path: `${this.url}/types`,
146
152
  group: this.group,
153
+ secure: true,
147
154
  description: "Get all registered audit types",
148
155
  schema: {
149
156
  response: t.array(
@@ -163,6 +170,7 @@ export class AdminAuditController {
163
170
  public readonly getFilterOptions = $action({
164
171
  path: `${this.url}/filters`,
165
172
  group: this.group,
173
+ secure: true,
166
174
  description: "Get distinct values for audit filters",
167
175
  schema: {
168
176
  response: t.object({
@@ -20,6 +20,7 @@ export class AdminFileStatsController {
20
20
  public readonly getStats = $action({
21
21
  path: this.url,
22
22
  group: this.group,
23
+ secure: true,
23
24
  description: "Get storage statistics",
24
25
  schema: {
25
26
  response: storageStatsSchema,
@@ -13,6 +13,7 @@ export class AdminJobController {
13
13
  public readonly getJobs = $action({
14
14
  path: this.url,
15
15
  group: this.group,
16
+ secure: true,
16
17
  schema: {
17
18
  response: t.array(t.string()),
18
19
  },
@@ -22,6 +23,7 @@ export class AdminJobController {
22
23
  public readonly getJobExecutions = $action({
23
24
  path: `${this.url}/executions`,
24
25
  group: this.group,
26
+ secure: true,
25
27
  schema: {
26
28
  query: jobExecutionQuerySchema,
27
29
  response: t.page(jobExecutionResourceSchema),
@@ -33,6 +35,7 @@ export class AdminJobController {
33
35
  method: "POST",
34
36
  path: `${this.url}/trigger`,
35
37
  group: this.group,
38
+ secure: true,
36
39
  schema: {
37
40
  body: triggerJobSchema,
38
41
  response: okSchema,
@@ -15,6 +15,7 @@ export class AdminNotificationController {
15
15
  public readonly findNotifications = $action({
16
16
  path: this.url,
17
17
  group: this.group,
18
+ secure: true,
18
19
  description: "Find notifications with pagination and filtering",
19
20
  schema: {
20
21
  query: notificationQuerySchema,
@@ -41,6 +41,7 @@ export class AdminConfigController {
41
41
  */
42
42
  getConfigTree = $action({
43
43
  group: this.group,
44
+ secure: true,
44
45
  description:
45
46
  "Get tree structure of all configuration names for navigation.",
46
47
  path: "/configs/tree",
@@ -58,6 +59,7 @@ export class AdminConfigController {
58
59
  */
59
60
  listConfigNames = $action({
60
61
  group: this.group,
62
+ secure: true,
61
63
  description: "List all unique configuration names.",
62
64
  path: "/configs",
63
65
  method: "GET",
@@ -75,6 +77,7 @@ export class AdminConfigController {
75
77
  */
76
78
  getByStatus = $action({
77
79
  group: this.group,
80
+ secure: true,
78
81
  description: "Get all configurations with a specific status.",
79
82
  path: "/configs/status/:status",
80
83
  method: "GET",
@@ -95,6 +98,7 @@ export class AdminConfigController {
95
98
  */
96
99
  getHistory = $action({
97
100
  group: this.group,
101
+ secure: true,
98
102
  description: "Get all versions of a specific configuration.",
99
103
  path: "/configs/:name/history",
100
104
  method: "GET",
@@ -115,6 +119,7 @@ export class AdminConfigController {
115
119
  */
116
120
  getCurrent = $action({
117
121
  group: this.group,
122
+ secure: true,
118
123
  description: "Get current and next scheduled values for a configuration.",
119
124
  path: "/configs/:name",
120
125
  method: "GET",
@@ -139,6 +144,7 @@ export class AdminConfigController {
139
144
  */
140
145
  getVersion = $action({
141
146
  group: this.group,
147
+ secure: true,
142
148
  description: "Get a specific version of a configuration.",
143
149
  path: "/configs/:name/versions/:version",
144
150
  method: "GET",
@@ -157,6 +163,7 @@ export class AdminConfigController {
157
163
  */
158
164
  createVersion = $action({
159
165
  group: this.group,
166
+ secure: true,
160
167
  description:
161
168
  "Create a new version of a configuration (immediate or scheduled).",
162
169
  path: "/configs/:name",
@@ -184,6 +191,7 @@ export class AdminConfigController {
184
191
  */
185
192
  rollback = $action({
186
193
  group: this.group,
194
+ secure: true,
187
195
  description:
188
196
  "Rollback a configuration to a previous version (creates new version with old content).",
189
197
  path: "/configs/:name/rollback",
@@ -207,6 +215,7 @@ export class AdminConfigController {
207
215
  */
208
216
  activateNow = $action({
209
217
  group: this.group,
218
+ secure: true,
210
219
  description: "Activate a future/next configuration version immediately.",
211
220
  path: "/configs/:name/activate",
212
221
  method: "POST",
@@ -248,6 +257,7 @@ export class AdminConfigController {
248
257
  */
249
258
  checkScheduled = $action({
250
259
  group: this.group,
260
+ secure: true,
251
261
  description:
252
262
  "Manually trigger activation check for all scheduled configurations.",
253
263
  path: "/configs/activate-scheduled",
@@ -15,6 +15,7 @@ export class AdminIdentityController {
15
15
  public readonly findIdentities = $action({
16
16
  path: this.url,
17
17
  group: this.group,
18
+ secure: true,
18
19
  description: "Find identities with pagination and filtering",
19
20
  schema: {
20
21
  query: t.extend(identityQuerySchema, {
@@ -34,6 +35,7 @@ export class AdminIdentityController {
34
35
  public readonly getIdentity = $action({
35
36
  path: `${this.url}/:id`,
36
37
  group: this.group,
38
+ secure: true,
37
39
  description: "Get an identity by ID",
38
40
  schema: {
39
41
  params: t.object({
@@ -55,6 +57,7 @@ export class AdminIdentityController {
55
57
  method: "DELETE",
56
58
  path: `${this.url}/:id`,
57
59
  group: this.group,
60
+ secure: true,
58
61
  description: "Delete an identity",
59
62
  schema: {
60
63
  params: t.object({
@@ -15,6 +15,7 @@ export class AdminSessionController {
15
15
  public readonly findSessions = $action({
16
16
  path: this.url,
17
17
  group: this.group,
18
+ secure: true,
18
19
  description: "Find sessions with pagination and filtering",
19
20
  schema: {
20
21
  query: t.extend(sessionQuerySchema, {
@@ -34,6 +35,7 @@ export class AdminSessionController {
34
35
  public readonly getSession = $action({
35
36
  path: `${this.url}/:id`,
36
37
  group: this.group,
38
+ secure: true,
37
39
  description: "Get a session by ID",
38
40
  schema: {
39
41
  params: t.object({
@@ -55,6 +57,7 @@ export class AdminSessionController {
55
57
  method: "DELETE",
56
58
  path: `${this.url}/:id`,
57
59
  group: this.group,
60
+ secure: true,
58
61
  description: "Delete a session",
59
62
  schema: {
60
63
  params: t.object({
@@ -17,6 +17,7 @@ export class AdminUserController {
17
17
  public readonly findUsers = $action({
18
18
  path: this.url,
19
19
  group: this.group,
20
+ secure: true,
20
21
  description: "Find users with pagination and filtering",
21
22
  schema: {
22
23
  query: t.extend(userQuerySchema, {
@@ -36,6 +37,7 @@ export class AdminUserController {
36
37
  public readonly getUser = $action({
37
38
  path: `${this.url}/:id`,
38
39
  group: this.group,
40
+ secure: true,
39
41
  description: "Get a user by ID",
40
42
  schema: {
41
43
  params: t.object({
@@ -57,6 +59,7 @@ export class AdminUserController {
57
59
  method: "POST",
58
60
  path: this.url,
59
61
  group: this.group,
62
+ secure: true,
60
63
  description: "Create a new user",
61
64
  schema: {
62
65
  query: t.object({
@@ -76,6 +79,7 @@ export class AdminUserController {
76
79
  method: "PATCH",
77
80
  path: `${this.url}/:id`,
78
81
  group: this.group,
82
+ secure: true,
79
83
  description: "Update a user",
80
84
  schema: {
81
85
  params: t.object({
@@ -98,6 +102,7 @@ export class AdminUserController {
98
102
  method: "DELETE",
99
103
  path: `${this.url}/:id`,
100
104
  group: this.group,
105
+ secure: true,
101
106
  description: "Delete a user",
102
107
  schema: {
103
108
  params: t.object({
@@ -269,6 +269,7 @@ export class BuildCommand {
269
269
  generateCloudflare({
270
270
  distDir,
271
271
  config: options.cloudflare?.config,
272
+ alepha: alepha!,
272
273
  }),
273
274
  });
274
275
  }
@@ -245,6 +245,11 @@ export class ViteDevServerProvider {
245
245
  return;
246
246
  }
247
247
 
248
+ // Generate dev head content using Vite's transformIndexHtml
249
+ // This lets Vite and all plugins (React, etc.) inject their scripts
250
+ const devHead = await this.generateDevHead();
251
+ this.alepha.store.set("alepha.react.ssr.manifest" as any, { devHead });
252
+
248
253
  this.alepha.events.on("server:onRequest", {
249
254
  priority: "first",
250
255
  callback: async ({ request }) => {
@@ -264,6 +269,32 @@ export class ViteDevServerProvider {
264
269
  });
265
270
  }
266
271
 
272
+ /**
273
+ * Generate dev head content by transforming a minimal HTML through Vite.
274
+ * This lets Vite and all plugins inject their scripts (HMR client, React Fast Refresh, etc.).
275
+ */
276
+ protected async generateDevHead(): Promise<string> {
277
+ const { browser, style } = this.options.entry;
278
+
279
+ // Build minimal HTML with entry points
280
+ const scripts: string[] = [];
281
+ if (style) {
282
+ scripts.push(`<link rel="stylesheet" href="/${style}">`);
283
+ }
284
+ if (browser) {
285
+ scripts.push(`<script type="module" src="/${browser}"></script>`);
286
+ }
287
+
288
+ const minimalHtml = `<!DOCTYPE html><html><head>${scripts.join("\n")}</head><body></body></html>`;
289
+
290
+ // Transform through Vite to inject all plugin scripts
291
+ const transformed = await this.server.transformIndexHtml("/", minimalHtml);
292
+
293
+ // Extract head content
294
+ const headMatch = transformed.match(/<head>([\s\S]*?)<\/head>/i);
295
+ return headMatch?.[1]?.trim() ?? "";
296
+ }
297
+
267
298
  /**
268
299
  * Check if request is for an HTML page (not an asset).
269
300
  */
@@ -0,0 +1,36 @@
1
+ import { $module } from "alepha";
2
+ import { $email } from "./primitives/$email.ts";
3
+ import { EmailProvider } from "./providers/EmailProvider.ts";
4
+ import { MemoryEmailProvider } from "./providers/MemoryEmailProvider.ts";
5
+ import { WorkermailerEmailProvider } from "./providers/WorkermailerEmailProvider.ts";
6
+
7
+ // ---------------------------------------------------------------------------------------------------------------------
8
+
9
+ export * from "./errors/EmailError.ts";
10
+ export * from "./primitives/$email.ts";
11
+ export * from "./providers/EmailProvider.ts";
12
+ export * from "./providers/MemoryEmailProvider.ts";
13
+ export * from "./providers/WorkermailerEmailProvider.ts";
14
+
15
+ // ---------------------------------------------------------------------------------------------------------------------
16
+
17
+ export const AlephaEmail = $module({
18
+ name: "alepha.email",
19
+ primitives: [$email],
20
+ services: [EmailProvider, MemoryEmailProvider, WorkermailerEmailProvider],
21
+ register: (alepha) => {
22
+ if (alepha.env.EMAIL_HOST) {
23
+ alepha.with({
24
+ optional: true,
25
+ provide: EmailProvider,
26
+ use: WorkermailerEmailProvider,
27
+ });
28
+ } else {
29
+ alepha.with({
30
+ optional: true,
31
+ provide: EmailProvider,
32
+ use: MemoryEmailProvider,
33
+ });
34
+ }
35
+ },
36
+ });
@@ -0,0 +1,221 @@
1
+ import { $atom, $env, $use, type Static, t } from "alepha";
2
+ import { $logger } from "alepha/logger";
3
+ import { WorkerMailer } from "worker-mailer";
4
+ import { EmailError } from "../errors/EmailError.ts";
5
+ import type { EmailProvider, EmailSendOptions } from "./EmailProvider.ts";
6
+
7
+ // ---------------------------------------------------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Environment variables for worker-mailer configuration
11
+ */
12
+ const envSchema = t.object({
13
+ EMAIL_HOST: t.optional(
14
+ t.text({
15
+ description: "SMTP server host",
16
+ }),
17
+ ),
18
+ EMAIL_PORT: t.number({
19
+ default: 587,
20
+ description: "SMTP server port (465 or 587, not 25)",
21
+ }),
22
+ EMAIL_USER: t.optional(
23
+ t.text({
24
+ description: "SMTP authentication username",
25
+ }),
26
+ ),
27
+ EMAIL_PASS: t.optional(
28
+ t.text({
29
+ description: "SMTP authentication password",
30
+ }),
31
+ ),
32
+ EMAIL_FROM: t.optional(
33
+ t.text({
34
+ description: "Default from email address",
35
+ }),
36
+ ),
37
+ EMAIL_FROM_NAME: t.optional(
38
+ t.text({
39
+ description: "Default from name",
40
+ }),
41
+ ),
42
+ EMAIL_SECURE: t.boolean({
43
+ default: true,
44
+ description: "Use secure connection (TLS/STARTTLS)",
45
+ }),
46
+ EMAIL_AUTH_TYPE: t.optional(
47
+ t.enum(["plain", "login", "cram-md5"], {
48
+ default: "plain",
49
+ description: "SMTP authentication type",
50
+ }),
51
+ ),
52
+ });
53
+
54
+ // ---------------------------------------------------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Worker-mailer specific options
58
+ */
59
+ export const workermailerEmailOptions = $atom({
60
+ name: "alepha.email.workermailer.options",
61
+ schema: t.object({
62
+ authType: t.optional(
63
+ t.enum(["plain", "login", "cram-md5"], {
64
+ description: "SMTP authentication type (default: plain)",
65
+ }),
66
+ ),
67
+ }),
68
+ default: {},
69
+ });
70
+
71
+ export type WorkermailerEmailProviderOptions = Static<
72
+ typeof workermailerEmailOptions.schema
73
+ >;
74
+
75
+ declare module "alepha" {
76
+ interface State {
77
+ [workermailerEmailOptions.key]: WorkermailerEmailProviderOptions;
78
+ }
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Email provider using worker-mailer for Cloudflare Workers.
85
+ *
86
+ * This provider uses Cloudflare's TCP Sockets API to send emails via SMTP,
87
+ * making it suitable for edge runtime environments.
88
+ *
89
+ * Configuration is provided via environment variables:
90
+ * - EMAIL_HOST: SMTP server host
91
+ * - EMAIL_PORT: SMTP server port (default: 587, note: port 25 is blocked)
92
+ * - EMAIL_USER: SMTP authentication username
93
+ * - EMAIL_PASS: SMTP authentication password
94
+ * - EMAIL_FROM: Default from email address
95
+ * - EMAIL_FROM_NAME: Default from name (optional)
96
+ * - EMAIL_SECURE: Use secure connection (default: true)
97
+ * - EMAIL_AUTH_TYPE: Authentication type - plain, login, or cram-md5 (default: plain)
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * // Configure via environment variables
102
+ * // EMAIL_HOST=smtp.example.com
103
+ * // EMAIL_PORT=587
104
+ * // EMAIL_USER=user@example.com
105
+ * // EMAIL_PASS=secret
106
+ * // EMAIL_FROM=noreply@example.com
107
+ * // EMAIL_FROM_NAME=My App
108
+ * // EMAIL_SECURE=true
109
+ * // EMAIL_AUTH_TYPE=plain
110
+ * ```
111
+ *
112
+ * @see https://github.com/zou-yu/worker-mailer
113
+ */
114
+ export class WorkermailerEmailProvider implements EmailProvider {
115
+ protected readonly env = $env(envSchema);
116
+ protected readonly log = $logger();
117
+ protected readonly options = $use(workermailerEmailOptions);
118
+
119
+ protected get host(): string {
120
+ const host = this.env.EMAIL_HOST;
121
+ if (!host) {
122
+ throw new EmailError(
123
+ "Email host not configured. Set EMAIL_HOST env var.",
124
+ );
125
+ }
126
+ return host;
127
+ }
128
+
129
+ protected get port(): number {
130
+ return this.env.EMAIL_PORT;
131
+ }
132
+
133
+ protected get secure(): boolean {
134
+ return this.env.EMAIL_SECURE;
135
+ }
136
+
137
+ protected get user(): string | undefined {
138
+ return this.env.EMAIL_USER;
139
+ }
140
+
141
+ protected get pass(): string | undefined {
142
+ return this.env.EMAIL_PASS;
143
+ }
144
+
145
+ protected get fromAddress(): string {
146
+ const from = this.env.EMAIL_FROM;
147
+ if (!from) {
148
+ throw new EmailError(
149
+ "Email from address not configured. Set EMAIL_FROM env var.",
150
+ );
151
+ }
152
+ return from;
153
+ }
154
+
155
+ protected get fromName(): string | undefined {
156
+ return this.env.EMAIL_FROM_NAME;
157
+ }
158
+
159
+ protected get authType(): "plain" | "login" | "cram-md5" {
160
+ return this.options.authType ?? this.env.EMAIL_AUTH_TYPE ?? "plain";
161
+ }
162
+
163
+ public async send(options: EmailSendOptions): Promise<void> {
164
+ const { to, subject, body } = options;
165
+ this.log.debug("Sending email via worker-mailer", { to, subject });
166
+
167
+ const user = this.user;
168
+ const pass = this.pass;
169
+
170
+ if (!user || !pass) {
171
+ throw new EmailError(
172
+ "Email credentials not configured. Set EMAIL_USER and EMAIL_PASS env vars.",
173
+ );
174
+ }
175
+
176
+ let mailer: WorkerMailer | undefined;
177
+
178
+ try {
179
+ mailer = await WorkerMailer.connect({
180
+ credentials: {
181
+ username: user,
182
+ password: pass,
183
+ },
184
+ authType: this.authType,
185
+ host: this.host,
186
+ port: this.port,
187
+ secure: this.secure,
188
+ });
189
+
190
+ const recipients = Array.isArray(to) ? to : [to];
191
+
192
+ for (const recipient of recipients) {
193
+ await mailer.send({
194
+ from: this.fromName
195
+ ? { name: this.fromName, email: this.fromAddress }
196
+ : { email: this.fromAddress },
197
+ to: { email: recipient },
198
+ subject,
199
+ html: body,
200
+ });
201
+
202
+ this.log.info("Email sent successfully", {
203
+ to: recipient,
204
+ subject,
205
+ });
206
+ }
207
+ } catch (error) {
208
+ const message = `Failed to send email via worker-mailer: ${error instanceof Error ? error.message : String(error)}`;
209
+ this.log.error(message, { to, subject });
210
+ throw new EmailError(message, error instanceof Error ? error : undefined);
211
+ } finally {
212
+ if (mailer) {
213
+ try {
214
+ await mailer.close();
215
+ } catch {
216
+ // Ignore close errors
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
@@ -302,7 +302,19 @@ export class LockPrimitive<TFunc extends AsyncFn> extends Primitive<
302
302
  protected readonly provider = $inject(LockProvider);
303
303
  protected readonly env = $env(envSchema);
304
304
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
305
- protected readonly id = crypto.randomUUID();
305
+
306
+ /**
307
+ * Lazy-initialized UUID to avoid calling crypto.randomUUID() in global scope.
308
+ * Cloudflare Workers doesn't allow random value generation during initialization.
309
+ */
310
+ protected _id?: string;
311
+ protected get id(): string {
312
+ if (!this._id) {
313
+ this._id = crypto.randomUUID();
314
+ }
315
+ return this._id;
316
+ }
317
+
306
318
  public readonly maxDuration = this.dateTimeProvider.duration(
307
319
  this.options.maxDuration ?? [5, "minutes"],
308
320
  );
@@ -130,6 +130,8 @@ export class ReactAuth {
130
130
  }
131
131
 
132
132
  public logout() {
133
- window.location.href = `${alephaServerAuthRoutes.logout}?post_logout_redirect_uri=${encodeURIComponent(window.location.origin)}`;
133
+ // Add cache-busting parameter to prevent browser from using cached redirect
134
+ const cacheBuster = Date.now();
135
+ window.location.href = `${alephaServerAuthRoutes.logout}?post_logout_redirect_uri=${encodeURIComponent(window.location.origin)}&_=${cacheBuster}`;
134
136
  }
135
137
  }
@@ -32,6 +32,13 @@ export const ssrManifestAtomSchema = t.object({
32
32
  }),
33
33
  ),
34
34
  ),
35
+
36
+ /**
37
+ * Dev mode head content.
38
+ * Contains pre-transformed scripts injected by Vite and plugins (React, etc.).
39
+ * Only set in dev mode via ViteDevServerProvider.
40
+ */
41
+ devHead: t.optional(t.string()),
35
42
  });
36
43
 
37
44
  /**
@@ -142,11 +142,22 @@ export class ReactServerProvider {
142
142
  * allowing the browser to start downloading entry.js and CSS files early.
143
143
  */
144
144
  protected setupEarlyHeadContent(): void {
145
- const assets = this.ssrManifestProvider.getEntryAssets();
146
145
  const globalHead = this.serverHeadProvider.resolveGlobalHead();
146
+ const manifest = this.ssrManifestProvider.getManifest();
147
147
 
148
- const parts: string[] = [];
148
+ // Dev mode: use pre-transformed head content from Vite
149
+ if (manifest.devHead) {
150
+ this.templateProvider.setEarlyHeadContent(
151
+ `${manifest.devHead}\n`,
152
+ globalHead,
153
+ );
154
+ this.log.debug("Early head content set (dev mode)");
155
+ return;
156
+ }
149
157
 
158
+ // Production: build from SSR manifest entry assets
159
+ const parts: string[] = [];
160
+ const assets = this.ssrManifestProvider.getEntryAssets();
150
161
  if (assets) {
151
162
  for (const css of assets.css) {
152
163
  parts.push(`<link rel="stylesheet" href="${css}" crossorigin="">`);
@@ -164,8 +175,7 @@ export class ReactServerProvider {
164
175
  );
165
176
 
166
177
  this.log.debug("Early head content set", {
167
- css: assets?.css.length ?? 0,
168
- js: assets?.js ? 1 : 0,
178
+ parts: parts.length,
169
179
  });
170
180
  }
171
181
 
@@ -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.