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.
- package/dist/api/audits/index.d.ts +332 -332
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +8 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.js +1 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +151 -151
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +3 -0
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +195 -195
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/notifications/index.browser.js +1 -0
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.js +1 -0
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +260 -260
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +10 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.d.ts +10 -10
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +11 -0
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +128 -128
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts +4 -4
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +19 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/email/index.d.ts +13 -13
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +10554 -2
- package/dist/email/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +6 -1
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +9 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/react/auth/index.browser.js +2 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js +2 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.d.ts +3 -3
- package/dist/react/router/index.d.ts +10 -0
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +16 -6
- package/dist/react/router/index.js.map +1 -1
- package/dist/redis/index.d.ts +19 -19
- package/dist/scheduler/index.d.ts +13 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +42 -4
- package/dist/scheduler/index.js.map +1 -1
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +1 -0
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/core/index.d.ts +9 -9
- package/dist/server/links/index.js +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +28 -2
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +34 -34
- package/dist/websocket/index.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/api/audits/controllers/AdminAuditController.ts +8 -0
- package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
- package/src/api/jobs/controllers/AdminJobController.ts +3 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
- package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
- package/src/api/users/controllers/AdminIdentityController.ts +3 -0
- package/src/api/users/controllers/AdminSessionController.ts +3 -0
- package/src/api/users/controllers/AdminUserController.ts +5 -0
- package/src/cli/commands/build.ts +1 -0
- package/src/cli/providers/ViteDevServerProvider.ts +31 -0
- package/src/email/index.workerd.ts +36 -0
- package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
- package/src/lock/core/primitives/$lock.ts +13 -1
- package/src/react/auth/services/ReactAuth.ts +3 -1
- package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
- package/src/react/router/providers/ReactServerProvider.ts +14 -4
- package/src/react/router/providers/SSRManifestProvider.ts +7 -0
- package/src/scheduler/index.workerd.ts +43 -0
- package/src/scheduler/providers/CronProvider.ts +53 -6
- package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
- package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
- package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
- package/src/server/links/providers/ServerLinksProvider.ts +1 -1
- 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({
|
|
@@ -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({
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|