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.
- package/README.md +68 -80
- 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.d.ts +170 -170
- package/dist/api/files/index.d.ts.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.map +1 -1
- package/dist/api/jobs/index.js +3 -0
- package/dist/api/jobs/index.js.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 +12 -1
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +18 -2
- package/dist/api/users/index.js.map +1 -1
- package/dist/batch/index.d.ts +4 -4
- package/dist/bucket/index.d.ts +8 -0
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +7 -2
- package/dist/bucket/index.js.map +1 -1
- package/dist/cli/index.d.ts +196 -74
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +234 -50
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +10 -0
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +67 -13
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +28 -21
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +28 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +28 -21
- package/dist/core/index.native.js.map +1 -1
- package/dist/email/index.d.ts +21 -13
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +10561 -4
- 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/mcp/index.d.ts +5 -5
- package/dist/orm/index.bun.js +32 -16
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +4 -1
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +34 -22
- package/dist/orm/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.browser.js +9 -15
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +305 -407
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +581 -781
- package/dist/react/router/index.js.map +1 -1
- 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/security/index.d.ts +42 -42
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +8 -7
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +167 -167
- 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/health/index.d.ts +17 -17
- package/dist/server/links/index.d.ts +39 -39
- package/dist/server/links/index.js +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/static/index.js +7 -2
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +8 -0
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +7 -2
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +8 -0
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +7 -2
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js +734 -12
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.d.ts +8 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +7 -2
- package/dist/system/index.js.map +1 -1
- package/dist/vite/index.d.ts +3 -2
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +42 -8
- 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 +9 -4
- 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/logs/TODO.md +13 -10
- 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/apps/AlephaPackageBuilderCli.ts +9 -0
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +150 -32
- package/src/cli/commands/db.ts +5 -7
- package/src/cli/commands/init.spec.ts +50 -6
- package/src/cli/commands/init.ts +28 -5
- package/src/cli/providers/ViteDevServerProvider.ts +31 -9
- package/src/cli/services/AlephaCliUtils.ts +16 -0
- package/src/cli/services/PackageManagerUtils.ts +2 -0
- package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
- package/src/cli/services/ProjectScaffolder.ts +28 -6
- package/src/cli/templates/agentMd.ts +6 -1
- package/src/cli/templates/apiAppSecurityTs.ts +11 -0
- package/src/cli/templates/apiIndexTs.ts +18 -4
- package/src/cli/templates/webAppRouterTs.ts +25 -1
- package/src/cli/templates/webHelloComponentTsx.ts +15 -5
- package/src/command/helpers/Runner.spec.ts +135 -0
- package/src/command/helpers/Runner.ts +4 -1
- package/src/command/providers/CliProvider.spec.ts +325 -0
- package/src/command/providers/CliProvider.ts +117 -7
- package/src/core/Alepha.ts +32 -25
- 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/orm/index.bun.ts +1 -1
- package/src/orm/index.ts +2 -6
- package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
- package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
- package/src/react/auth/services/ReactAuth.ts +3 -1
- package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
- package/src/react/router/hooks/useActive.ts +1 -1
- package/src/react/router/hooks/useRouter.ts +1 -1
- package/src/react/router/index.ts +4 -0
- package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
- package/src/react/router/primitives/$page.spec.tsx +0 -32
- package/src/react/router/primitives/$page.ts +6 -14
- package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
- package/src/react/router/providers/ReactPageProvider.ts +1 -1
- package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
- package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
- package/src/react/router/providers/ReactServerProvider.ts +21 -82
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
- package/src/react/router/providers/SSRManifestProvider.ts +7 -0
- package/src/react/router/services/ReactRouter.ts +13 -13
- 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/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
- package/src/security/providers/ServerSecurityProvider.ts +30 -22
- package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
- package/src/server/links/providers/ServerLinksProvider.ts +1 -1
- package/src/system/index.browser.ts +25 -0
- package/src/system/index.workerd.ts +1 -0
- package/src/system/providers/FileSystemProvider.ts +8 -0
- package/src/system/providers/NodeFileSystemProvider.ts +11 -2
- package/src/vite/tasks/buildServer.ts +2 -12
- package/src/vite/tasks/generateCloudflare.ts +47 -8
- 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
|
|
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.
|
|
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
|
|
172
|
-
public async
|
|
171
|
+
public async push(path: string, options?: RouterPushOptions): Promise<void>;
|
|
172
|
+
public async push(
|
|
173
173
|
path: keyof VirtualRouter<T>,
|
|
174
|
-
options?:
|
|
174
|
+
options?: RouterPushOptions,
|
|
175
175
|
): Promise<void>;
|
|
176
|
-
public async
|
|
176
|
+
public async push(
|
|
177
177
|
path: string | keyof VirtualRouter<T>,
|
|
178
|
-
options?:
|
|
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?.
|
|
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?.
|
|
190
|
+
await this.browser?.push(path as string, options);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
public anchor(path: string, options?:
|
|
193
|
+
public anchor(path: string, options?: RouterPushOptions): AnchorProps;
|
|
194
194
|
public anchor(
|
|
195
195
|
path: keyof VirtualRouter<T>,
|
|
196
|
-
options?:
|
|
196
|
+
options?: RouterPushOptions,
|
|
197
197
|
): AnchorProps;
|
|
198
198
|
public anchor(
|
|
199
199
|
path: string | keyof VirtualRouter<T>,
|
|
200
|
-
options:
|
|
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.
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
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
|
|
39
|
+
action.options.secure !== true ||
|
|
39
40
|
this.securityProvider.getRealms().length === 0
|
|
40
41
|
) {
|
|
41
42
|
continue;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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 (
|
|
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 (
|
|
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({
|
|
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({
|
|
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({
|
|
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();
|