alepha 0.13.1 → 0.13.2
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 +1 -1
- package/dist/api-files/index.d.ts +28 -91
- package/dist/api-files/index.js +10 -755
- package/dist/api-files/index.js.map +1 -1
- package/dist/api-jobs/index.d.ts +46 -46
- package/dist/api-jobs/index.js +13 -13
- package/dist/api-jobs/index.js.map +1 -1
- package/dist/api-notifications/index.d.ts +129 -146
- package/dist/api-notifications/index.js +17 -39
- package/dist/api-notifications/index.js.map +1 -1
- package/dist/api-parameters/index.d.ts +21 -22
- package/dist/api-parameters/index.js +22 -22
- package/dist/api-parameters/index.js.map +1 -1
- package/dist/api-users/index.d.ts +223 -2000
- package/dist/api-users/index.js +914 -4787
- package/dist/api-users/index.js.map +1 -1
- package/dist/api-verifications/index.d.ts +96 -96
- package/dist/batch/index.d.ts +13 -13
- package/dist/batch/index.js +8 -8
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +14 -14
- package/dist/bucket/index.js +12 -12
- package/dist/bucket/index.js.map +1 -1
- package/dist/cache/index.d.ts +11 -11
- package/dist/cache/index.js +9 -9
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.d.ts +28 -26
- package/dist/cli/index.js +50 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +19 -19
- package/dist/command/index.js +25 -25
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +218 -218
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +232 -232
- package/dist/core/index.js +218 -218
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +2113 -0
- package/dist/core/index.native.js.map +1 -0
- package/dist/datetime/index.d.ts +9 -9
- package/dist/datetime/index.js +7 -7
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +16 -16
- package/dist/email/index.js +9 -9
- package/dist/email/index.js.map +1 -1
- package/dist/file/index.js +1 -1
- package/dist/file/index.js.map +1 -1
- package/dist/lock/index.d.ts +9 -9
- package/dist/lock/index.js +8 -8
- package/dist/lock/index.js.map +1 -1
- package/dist/lock-redis/index.js +3 -66
- package/dist/lock-redis/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -5
- package/dist/logger/index.js +8 -8
- package/dist/logger/index.js.map +1 -1
- package/dist/orm/index.browser.js +114 -114
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.d.ts +218 -218
- package/dist/orm/index.js +46 -46
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/index.d.ts +29 -29
- package/dist/queue/index.js +20 -20
- package/dist/queue/index.js.map +1 -1
- package/dist/queue-redis/index.d.ts +2 -2
- package/dist/redis/index.d.ts +10 -10
- package/dist/retry/index.d.ts +19 -19
- package/dist/retry/index.js +7 -7
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +16 -16
- package/dist/scheduler/index.js +9 -9
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +80 -80
- package/dist/security/index.js +32 -32
- package/dist/security/index.js.map +1 -1
- package/dist/server/index.browser.js +1 -1
- package/dist/server/index.browser.js.map +1 -1
- package/dist/server/index.d.ts +101 -101
- package/dist/server/index.js +16 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server-auth/index.browser.js +4 -982
- package/dist/server-auth/index.browser.js.map +1 -1
- package/dist/server-auth/index.d.ts +204 -785
- package/dist/server-auth/index.js +47 -1239
- package/dist/server-auth/index.js.map +1 -1
- package/dist/server-cache/index.d.ts +10 -10
- package/dist/server-cache/index.js +2 -2
- package/dist/server-cache/index.js.map +1 -1
- package/dist/server-compress/index.d.ts +4 -4
- package/dist/server-compress/index.js +1 -1
- package/dist/server-compress/index.js.map +1 -1
- package/dist/server-cookies/index.browser.js +8 -8
- package/dist/server-cookies/index.browser.js.map +1 -1
- package/dist/server-cookies/index.d.ts +17 -17
- package/dist/server-cookies/index.js +10 -10
- package/dist/server-cookies/index.js.map +1 -1
- package/dist/server-cors/index.d.ts +17 -17
- package/dist/server-cors/index.js +9 -9
- package/dist/server-cors/index.js.map +1 -1
- package/dist/server-health/index.d.ts +19 -19
- package/dist/server-helmet/index.d.ts +1 -1
- package/dist/server-links/index.browser.js +12 -12
- package/dist/server-links/index.browser.js.map +1 -1
- package/dist/server-links/index.d.ts +59 -251
- package/dist/server-links/index.js +23 -502
- package/dist/server-links/index.js.map +1 -1
- package/dist/server-metrics/index.d.ts +4 -4
- package/dist/server-multipart/index.d.ts +2 -2
- package/dist/server-proxy/index.d.ts +12 -12
- package/dist/server-proxy/index.js +10 -10
- package/dist/server-proxy/index.js.map +1 -1
- package/dist/server-rate-limit/index.d.ts +22 -22
- package/dist/server-rate-limit/index.js +12 -12
- package/dist/server-rate-limit/index.js.map +1 -1
- package/dist/server-security/index.d.ts +22 -22
- package/dist/server-security/index.js +15 -15
- package/dist/server-security/index.js.map +1 -1
- package/dist/server-static/index.d.ts +14 -14
- package/dist/server-static/index.js +8 -8
- package/dist/server-static/index.js.map +1 -1
- package/dist/server-swagger/index.d.ts +25 -184
- package/dist/server-swagger/index.js +21 -724
- package/dist/server-swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +14 -14
- package/dist/sms/index.js +9 -9
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +11 -11
- package/dist/thread/index.js +17 -17
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/index.d.ts +26 -26
- package/dist/topic/index.js +16 -16
- package/dist/topic/index.js.map +1 -1
- package/dist/topic-redis/index.d.ts +1 -1
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +8 -8
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +11 -11
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +58 -58
- package/dist/websocket/index.js +13 -13
- package/dist/websocket/index.js.map +1 -1
- package/package.json +113 -52
- package/src/api-files/services/FileService.ts +5 -7
- package/src/api-jobs/index.ts +1 -1
- package/src/api-jobs/{descriptors → primitives}/$job.ts +8 -8
- package/src/api-jobs/providers/JobProvider.ts +9 -9
- package/src/api-jobs/services/JobService.ts +5 -5
- package/src/api-notifications/index.ts +5 -15
- package/src/api-notifications/{descriptors → primitives}/$notification.ts +10 -10
- package/src/api-notifications/services/NotificationSenderService.ts +3 -3
- package/src/api-parameters/index.ts +1 -1
- package/src/api-parameters/{descriptors → primitives}/$config.ts +7 -12
- package/src/api-users/index.ts +1 -1
- package/src/api-users/{descriptors → primitives}/$userRealm.ts +8 -8
- package/src/api-users/providers/UserRealmProvider.ts +1 -1
- package/src/batch/index.ts +3 -3
- package/src/batch/{descriptors → primitives}/$batch.ts +13 -16
- package/src/bucket/index.ts +8 -8
- package/src/bucket/{descriptors → primitives}/$bucket.ts +8 -8
- package/src/bucket/providers/LocalFileStorageProvider.ts +3 -3
- package/src/cache/index.ts +4 -4
- package/src/cache/{descriptors → primitives}/$cache.ts +15 -15
- package/src/cli/apps/AlephaPackageBuilderCli.ts +24 -2
- package/src/cli/commands/DrizzleCommands.ts +6 -6
- package/src/cli/commands/VerifyCommands.ts +1 -1
- package/src/cli/commands/ViteCommands.ts +6 -1
- package/src/cli/services/ProjectUtils.ts +34 -3
- package/src/command/index.ts +5 -5
- package/src/command/{descriptors → primitives}/$command.ts +9 -12
- package/src/command/providers/CliProvider.ts +10 -10
- package/src/core/Alepha.ts +30 -33
- package/src/core/constants/KIND.ts +1 -1
- package/src/core/constants/OPTIONS.ts +1 -1
- package/src/core/helpers/{descriptor.ts → primitive.ts} +18 -18
- package/src/core/helpers/ref.ts +1 -1
- package/src/core/index.shared.ts +8 -8
- package/src/core/{descriptors → primitives}/$context.ts +5 -5
- package/src/core/{descriptors → primitives}/$hook.ts +4 -4
- package/src/core/{descriptors → primitives}/$inject.ts +2 -2
- package/src/core/{descriptors → primitives}/$module.ts +9 -9
- package/src/core/{descriptors → primitives}/$use.ts +2 -2
- package/src/core/providers/CodecManager.ts +1 -1
- package/src/core/providers/JsonSchemaCodec.ts +1 -1
- package/src/core/providers/StateManager.ts +2 -2
- package/src/datetime/index.ts +3 -3
- package/src/datetime/{descriptors → primitives}/$interval.ts +6 -6
- package/src/email/index.ts +4 -4
- package/src/email/{descriptors → primitives}/$email.ts +8 -8
- package/src/file/index.ts +1 -1
- package/src/lock/index.ts +3 -3
- package/src/lock/{descriptors → primitives}/$lock.ts +10 -10
- package/src/logger/index.ts +8 -8
- package/src/logger/{descriptors → primitives}/$logger.ts +2 -2
- package/src/logger/services/Logger.ts +1 -1
- package/src/orm/constants/PG_SYMBOLS.ts +2 -2
- package/src/orm/index.browser.ts +2 -2
- package/src/orm/index.ts +8 -8
- package/src/orm/{descriptors → primitives}/$entity.ts +11 -11
- package/src/orm/{descriptors → primitives}/$repository.ts +2 -2
- package/src/orm/{descriptors → primitives}/$sequence.ts +8 -8
- package/src/orm/{descriptors → primitives}/$transaction.ts +4 -4
- package/src/orm/providers/PostgresTypeProvider.ts +3 -3
- package/src/orm/providers/RepositoryProvider.ts +4 -4
- package/src/orm/providers/drivers/DatabaseProvider.ts +7 -7
- package/src/orm/services/ModelBuilder.ts +9 -9
- package/src/orm/services/PgRelationManager.ts +2 -2
- package/src/orm/services/PostgresModelBuilder.ts +5 -5
- package/src/orm/services/Repository.ts +7 -7
- package/src/orm/services/SqliteModelBuilder.ts +5 -5
- package/src/queue/index.ts +7 -7
- package/src/queue/{descriptors → primitives}/$consumer.ts +15 -15
- package/src/queue/{descriptors → primitives}/$queue.ts +12 -12
- package/src/queue/providers/WorkerProvider.ts +7 -7
- package/src/retry/index.ts +3 -3
- package/src/retry/{descriptors → primitives}/$retry.ts +14 -14
- package/src/scheduler/index.ts +3 -3
- package/src/scheduler/{descriptors → primitives}/$scheduler.ts +9 -9
- package/src/scheduler/providers/CronProvider.ts +1 -1
- package/src/security/index.ts +9 -9
- package/src/security/{descriptors → primitives}/$permission.ts +7 -7
- package/src/security/{descriptors → primitives}/$realm.ts +6 -12
- package/src/security/{descriptors → primitives}/$role.ts +12 -12
- package/src/security/{descriptors → primitives}/$serviceAccount.ts +8 -8
- package/src/server/index.browser.ts +1 -1
- package/src/server/index.ts +14 -14
- package/src/server/{descriptors → primitives}/$action.ts +13 -13
- package/src/server/{descriptors → primitives}/$route.ts +9 -9
- package/src/server/providers/NodeHttpServerProvider.ts +1 -1
- package/src/server/services/HttpClient.ts +1 -1
- package/src/server-auth/index.browser.ts +1 -1
- package/src/server-auth/index.ts +6 -6
- package/src/server-auth/{descriptors → primitives}/$auth.ts +10 -10
- package/src/server-auth/{descriptors → primitives}/$authCredentials.ts +4 -4
- package/src/server-auth/{descriptors → primitives}/$authGithub.ts +4 -4
- package/src/server-auth/{descriptors → primitives}/$authGoogle.ts +4 -4
- package/src/server-auth/providers/ServerAuthProvider.ts +4 -4
- package/src/server-cache/providers/ServerCacheProvider.ts +7 -7
- package/src/server-compress/providers/ServerCompressProvider.ts +3 -3
- package/src/server-cookies/index.browser.ts +2 -2
- package/src/server-cookies/index.ts +5 -5
- package/src/server-cookies/{descriptors → primitives}/$cookie.browser.ts +12 -12
- package/src/server-cookies/{descriptors → primitives}/$cookie.ts +13 -13
- package/src/server-cookies/providers/ServerCookiesProvider.ts +4 -4
- package/src/server-cookies/services/CookieParser.ts +1 -1
- package/src/server-cors/index.ts +3 -3
- package/src/server-cors/{descriptors → primitives}/$cors.ts +11 -13
- package/src/server-cors/providers/ServerCorsProvider.ts +5 -5
- package/src/server-links/index.browser.ts +5 -5
- package/src/server-links/index.ts +9 -9
- package/src/server-links/{descriptors → primitives}/$remote.ts +11 -11
- package/src/server-links/providers/LinkProvider.ts +7 -7
- package/src/server-links/providers/{RemoteDescriptorProvider.ts → RemotePrimitiveProvider.ts} +6 -6
- package/src/server-links/providers/ServerLinksProvider.ts +3 -3
- package/src/server-proxy/index.ts +3 -3
- package/src/server-proxy/{descriptors → primitives}/$proxy.ts +8 -8
- package/src/server-proxy/providers/ServerProxyProvider.ts +4 -4
- package/src/server-rate-limit/index.ts +6 -6
- package/src/server-rate-limit/{descriptors → primitives}/$rateLimit.ts +13 -13
- package/src/server-rate-limit/providers/ServerRateLimitProvider.ts +5 -5
- package/src/server-security/index.ts +3 -3
- package/src/server-security/{descriptors → primitives}/$basicAuth.ts +13 -13
- package/src/server-security/providers/ServerBasicAuthProvider.ts +5 -5
- package/src/server-security/providers/ServerSecurityProvider.ts +4 -4
- package/src/server-static/index.ts +3 -3
- package/src/server-static/{descriptors → primitives}/$serve.ts +8 -10
- package/src/server-static/providers/ServerStaticProvider.ts +6 -6
- package/src/server-swagger/index.ts +5 -5
- package/src/server-swagger/{descriptors → primitives}/$swagger.ts +9 -9
- package/src/server-swagger/providers/ServerSwaggerProvider.ts +11 -10
- package/src/sms/index.ts +4 -4
- package/src/sms/{descriptors → primitives}/$sms.ts +8 -8
- package/src/thread/index.ts +3 -3
- package/src/thread/{descriptors → primitives}/$thread.ts +13 -13
- package/src/thread/providers/ThreadProvider.ts +7 -9
- package/src/topic/index.ts +5 -5
- package/src/topic/{descriptors → primitives}/$subscriber.ts +14 -14
- package/src/topic/{descriptors → primitives}/$topic.ts +10 -10
- package/src/topic/providers/TopicProvider.ts +4 -4
- package/src/vite/tasks/copyAssets.ts +1 -1
- package/src/vite/tasks/generateSitemap.ts +3 -3
- package/src/vite/tasks/prerenderPages.ts +2 -2
- package/src/vite/tasks/runAlepha.ts +2 -2
- package/src/websocket/index.browser.ts +3 -3
- package/src/websocket/index.shared.ts +2 -2
- package/src/websocket/index.ts +4 -4
- package/src/websocket/interfaces/WebSocketInterfaces.ts +3 -3
- package/src/websocket/{descriptors → primitives}/$channel.ts +10 -10
- package/src/websocket/{descriptors → primitives}/$websocket.ts +8 -8
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +7 -7
- package/src/websocket/providers/WebSocketServerProvider.ts +3 -3
- package/src/websocket/services/WebSocketClient.ts +5 -5
- package/src/api-notifications/providers/MemorySmsProvider.ts +0 -20
- package/src/api-notifications/providers/SmsProvider.ts +0 -8
- /package/src/core/{descriptors → primitives}/$atom.ts +0 -0
- /package/src/core/{descriptors → primitives}/$env.ts +0 -0
- /package/src/server-auth/{descriptors → primitives}/$authApple.ts +0 -0
- /package/src/server-links/{descriptors → primitives}/$client.ts +0 -0
|
@@ -1,722 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { $
|
|
3
|
-
import { $action,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { DateTimeProvider } from "alepha/datetime";
|
|
8
|
-
import { createReadStream } from "node:fs";
|
|
9
|
-
import { access, readdir, stat } from "node:fs/promises";
|
|
10
|
-
import { basename, isAbsolute, join } from "node:path";
|
|
11
|
-
import { FileDetector, FileSystemProvider } from "alepha/file";
|
|
1
|
+
import "alepha/server/security";
|
|
2
|
+
import { $atom, $hook, $inject, $module, $use, Alepha, KIND, Primitive, createPrimitive, isTypeFile, t } from "alepha";
|
|
3
|
+
import { $action, AlephaServer, ServerProvider, ServerRouterProvider } from "alepha/server";
|
|
4
|
+
import { AlephaServerCache } from "alepha/server/cache";
|
|
5
|
+
import { AlephaServerStatic, ServerStaticProvider } from "alepha/server/static";
|
|
6
|
+
import { join } from "node:path";
|
|
12
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { FileSystemProvider } from "alepha/file";
|
|
9
|
+
import { $logger } from "alepha/logger";
|
|
10
|
+
import { AlephaSecurity } from "alepha/security";
|
|
13
11
|
|
|
14
|
-
//#region src/server-
|
|
15
|
-
var ServerBasicAuthProvider = class {
|
|
16
|
-
alepha = $inject(Alepha);
|
|
17
|
-
log = $logger();
|
|
18
|
-
routerProvider = $inject(ServerRouterProvider);
|
|
19
|
-
realm = "Secure Area";
|
|
20
|
-
/**
|
|
21
|
-
* Registered basic auth descriptors with their configurations
|
|
22
|
-
*/
|
|
23
|
-
registeredAuths = [];
|
|
24
|
-
/**
|
|
25
|
-
* Register a basic auth configuration (called by descriptors)
|
|
26
|
-
*/
|
|
27
|
-
registerAuth(config) {
|
|
28
|
-
this.registeredAuths.push(config);
|
|
29
|
-
}
|
|
30
|
-
onStart = $hook({
|
|
31
|
-
on: "start",
|
|
32
|
-
handler: async () => {
|
|
33
|
-
for (const auth of this.registeredAuths) if (auth.paths) for (const pattern of auth.paths) {
|
|
34
|
-
const matchedRoutes = this.routerProvider.getRoutes(pattern);
|
|
35
|
-
for (const route of matchedRoutes) route.secure = { basic: {
|
|
36
|
-
username: auth.username,
|
|
37
|
-
password: auth.password
|
|
38
|
-
} };
|
|
39
|
-
}
|
|
40
|
-
if (this.registeredAuths.length > 0) this.log.info(`Initialized with ${this.registeredAuths.length} registered basic-auth configurations.`);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
/**
|
|
44
|
-
* Hook into server:onRequest to check basic auth
|
|
45
|
-
*/
|
|
46
|
-
onRequest = $hook({
|
|
47
|
-
on: "server:onRequest",
|
|
48
|
-
handler: async ({ route, request }) => {
|
|
49
|
-
const routeAuth = route.secure;
|
|
50
|
-
if (typeof routeAuth === "object" && "basic" in routeAuth && routeAuth.basic) this.checkAuth(request, routeAuth.basic);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
/**
|
|
54
|
-
* Hook into action:onRequest to check basic auth for actions
|
|
55
|
-
*/
|
|
56
|
-
onActionRequest = $hook({
|
|
57
|
-
on: "action:onRequest",
|
|
58
|
-
handler: async ({ action, request }) => {
|
|
59
|
-
const routeAuth = action.route.secure;
|
|
60
|
-
if (isBasicAuth(routeAuth)) this.checkAuth(request, routeAuth.basic);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
/**
|
|
64
|
-
* Check basic authentication
|
|
65
|
-
*/
|
|
66
|
-
checkAuth(request, options) {
|
|
67
|
-
const authHeader = request.headers?.authorization;
|
|
68
|
-
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
69
|
-
this.sendAuthRequired(request);
|
|
70
|
-
throw new HttpError({
|
|
71
|
-
status: 401,
|
|
72
|
-
message: "Authentication required"
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
const base64Credentials = authHeader.slice(6);
|
|
76
|
-
const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8");
|
|
77
|
-
const colonIndex = credentials.indexOf(":");
|
|
78
|
-
const username = colonIndex !== -1 ? credentials.slice(0, colonIndex) : credentials;
|
|
79
|
-
const password = colonIndex !== -1 ? credentials.slice(colonIndex + 1) : "";
|
|
80
|
-
if (!this.timingSafeCredentialCheck(username, password, options.username, options.password)) {
|
|
81
|
-
this.sendAuthRequired(request);
|
|
82
|
-
this.log.warn(`Failed basic auth attempt for user`, { username });
|
|
83
|
-
throw new HttpError({
|
|
84
|
-
status: 401,
|
|
85
|
-
message: "Invalid credentials"
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Performs a timing-safe comparison of credentials to prevent timing attacks.
|
|
91
|
-
* Always compares both username and password to avoid leaking which one is wrong.
|
|
92
|
-
*/
|
|
93
|
-
timingSafeCredentialCheck(inputUsername, inputPassword, expectedUsername, expectedPassword) {
|
|
94
|
-
const inputUserBuf = Buffer.from(inputUsername, "utf-8");
|
|
95
|
-
const expectedUserBuf = Buffer.from(expectedUsername, "utf-8");
|
|
96
|
-
const inputPassBuf = Buffer.from(inputPassword, "utf-8");
|
|
97
|
-
const expectedPassBuf = Buffer.from(expectedPassword, "utf-8");
|
|
98
|
-
return (this.safeCompare(inputUserBuf, expectedUserBuf) & this.safeCompare(inputPassBuf, expectedPassBuf)) === 1;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Compares two buffers in constant time, handling different lengths safely.
|
|
102
|
-
* Returns 1 if equal, 0 if not equal.
|
|
103
|
-
*/
|
|
104
|
-
safeCompare(input, expected) {
|
|
105
|
-
if (input.length !== expected.length) {
|
|
106
|
-
timingSafeEqual(input, input);
|
|
107
|
-
return 0;
|
|
108
|
-
}
|
|
109
|
-
return timingSafeEqual(input, expected) ? 1 : 0;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Send WWW-Authenticate header
|
|
113
|
-
*/
|
|
114
|
-
sendAuthRequired(request) {
|
|
115
|
-
request.reply.setHeader("WWW-Authenticate", `Basic realm="${this.realm}"`);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const isBasicAuth = (value) => {
|
|
119
|
-
return typeof value === "object" && !!value && "basic" in value && !!value.basic;
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
//#endregion
|
|
123
|
-
//#region src/server-security/descriptors/$basicAuth.ts
|
|
124
|
-
/**
|
|
125
|
-
* Declares HTTP Basic Authentication for server routes.
|
|
126
|
-
* This descriptor provides methods to protect routes with username/password authentication.
|
|
127
|
-
*/
|
|
128
|
-
const $basicAuth = (options) => {
|
|
129
|
-
return createDescriptor(BasicAuthDescriptor, options);
|
|
130
|
-
};
|
|
131
|
-
var BasicAuthDescriptor = class extends Descriptor {
|
|
132
|
-
serverBasicAuthProvider = $inject(ServerBasicAuthProvider);
|
|
133
|
-
get name() {
|
|
134
|
-
return this.options.name ?? `${this.config.propertyKey}`;
|
|
135
|
-
}
|
|
136
|
-
onInit() {
|
|
137
|
-
this.serverBasicAuthProvider.registerAuth(this.options);
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Checks basic auth for the given request using this descriptor's configuration.
|
|
141
|
-
*/
|
|
142
|
-
check(request, options) {
|
|
143
|
-
const mergedOptions = {
|
|
144
|
-
...this.options,
|
|
145
|
-
...options
|
|
146
|
-
};
|
|
147
|
-
this.serverBasicAuthProvider.checkAuth(request, mergedOptions);
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
$basicAuth[KIND] = BasicAuthDescriptor;
|
|
151
|
-
|
|
152
|
-
//#endregion
|
|
153
|
-
//#region src/server-security/providers/ServerSecurityProvider.ts
|
|
154
|
-
var ServerSecurityProvider = class {
|
|
155
|
-
log = $logger();
|
|
156
|
-
securityProvider = $inject(SecurityProvider);
|
|
157
|
-
jwtProvider = $inject(JwtProvider);
|
|
158
|
-
alepha = $inject(Alepha);
|
|
159
|
-
onConfigure = $hook({
|
|
160
|
-
on: "configure",
|
|
161
|
-
handler: async () => {
|
|
162
|
-
for (const action of this.alepha.descriptors($action)) {
|
|
163
|
-
if (action.options.disabled || action.options.secure === false || this.securityProvider.getRealms().length === 0) continue;
|
|
164
|
-
if (typeof action.options.secure !== "object") this.securityProvider.createPermission({
|
|
165
|
-
name: action.name,
|
|
166
|
-
group: action.group,
|
|
167
|
-
method: action.route.method,
|
|
168
|
-
path: action.route.path
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
onActionRequest = $hook({
|
|
174
|
-
on: "action:onRequest",
|
|
175
|
-
handler: async ({ action, request, options }) => {
|
|
176
|
-
if (action.options.secure === false && !options.user) {
|
|
177
|
-
this.log.trace("Skipping security check for route");
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (isBasicAuth(action.route.secure)) return;
|
|
181
|
-
const permission = this.securityProvider.getPermissions().find((it) => it.path === action.route.path && it.method === action.route.method);
|
|
182
|
-
try {
|
|
183
|
-
request.user = this.createUserFromLocalFunctionContext(options, permission);
|
|
184
|
-
const route = action.route;
|
|
185
|
-
if (typeof route.secure === "object") this.check(request.user, route.secure);
|
|
186
|
-
this.alepha.state.set("alepha.server.request.user", this.alepha.codec.decode(userAccountInfoSchema, request.user));
|
|
187
|
-
} catch (error) {
|
|
188
|
-
if (action.options.secure || permission) throw error;
|
|
189
|
-
this.log.trace("Skipping security check for action");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
onRequest = $hook({
|
|
194
|
-
on: "server:onRequest",
|
|
195
|
-
priority: "last",
|
|
196
|
-
handler: async ({ request, route }) => {
|
|
197
|
-
if (route.secure === false) {
|
|
198
|
-
this.log.trace("Skipping security check for route - explicitly disabled");
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (isBasicAuth(route.secure)) return;
|
|
202
|
-
const permission = this.securityProvider.getPermissions().find((it) => it.path === route.path && it.method === route.method);
|
|
203
|
-
if (!request.headers.authorization && !route.secure && !permission) {
|
|
204
|
-
this.log.trace("Skipping security check for route - no authorization header and not secure");
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
request.user = await this.securityProvider.createUserFromToken(request.headers.authorization, { permission });
|
|
209
|
-
if (typeof route.secure === "object") this.check(request.user, route.secure);
|
|
210
|
-
this.alepha.state.set("alepha.server.request.user", this.alepha.codec.decode(userAccountInfoSchema, request.user));
|
|
211
|
-
this.log.trace("User set from request token", {
|
|
212
|
-
user: request.user,
|
|
213
|
-
permission
|
|
214
|
-
});
|
|
215
|
-
} catch (error) {
|
|
216
|
-
if (route.secure || permission) throw error;
|
|
217
|
-
this.log.trace("Skipping security check for route - error occurred", error);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
check(user, secure) {
|
|
222
|
-
if (secure.realm) {
|
|
223
|
-
if (user.realm !== secure.realm) throw new ForbiddenError(`User must belong to realm '${secure.realm}' to access this route`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Get the user account token for a local action call.
|
|
228
|
-
* There are three possible sources for the user:
|
|
229
|
-
* - `options.user`: the user passed in the options
|
|
230
|
-
* - `"system"`: the system user from the state (you MUST set state `server.security.system.user`)
|
|
231
|
-
* - `"context"`: the user from the request context (you MUST be in an HTTP request context)
|
|
232
|
-
*
|
|
233
|
-
* Priority order: `options.user` > `"system"` > `"context"`.
|
|
234
|
-
*
|
|
235
|
-
* In testing environment, if no user is provided, a test user is created based on the SecurityProvider's roles.
|
|
236
|
-
*/
|
|
237
|
-
createUserFromLocalFunctionContext(options, permission) {
|
|
238
|
-
const fromOptions = typeof options.user === "object" ? options.user : void 0;
|
|
239
|
-
const type = typeof options.user === "string" ? options.user : void 0;
|
|
240
|
-
let user;
|
|
241
|
-
const fromContext = this.alepha.context.get("request")?.user;
|
|
242
|
-
const fromSystem = this.alepha.state.get("alepha.server.security.system.user");
|
|
243
|
-
if (type === "system") user = fromSystem;
|
|
244
|
-
else if (type === "context") user = fromContext;
|
|
245
|
-
else user = fromOptions ?? fromContext ?? fromSystem;
|
|
246
|
-
if (!user) {
|
|
247
|
-
if (this.alepha.isTest() && !("user" in options)) return this.createTestUser();
|
|
248
|
-
throw new UnauthorizedError("User is required for calling this action");
|
|
249
|
-
}
|
|
250
|
-
const roles = user.roles ?? (this.alepha.isTest() ? this.securityProvider.getRoles().map((role) => role.name) : []);
|
|
251
|
-
let ownership;
|
|
252
|
-
if (permission) {
|
|
253
|
-
const result = this.securityProvider.checkPermission(permission, ...roles);
|
|
254
|
-
if (!result.isAuthorized) throw new ForbiddenError(`Permission '${this.securityProvider.permissionToString(permission)}' is required for this route`);
|
|
255
|
-
ownership = result.ownership;
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
...user,
|
|
259
|
-
ownership
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
createTestUser() {
|
|
263
|
-
return {
|
|
264
|
-
id: randomUUID(),
|
|
265
|
-
name: "Test",
|
|
266
|
-
roles: this.securityProvider.getRoles().map((role) => role.name)
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
onClientRequest = $hook({
|
|
270
|
-
on: "client:onRequest",
|
|
271
|
-
handler: async ({ request, options }) => {
|
|
272
|
-
if (!this.alepha.isTest()) return;
|
|
273
|
-
if ("user" in options && options.user === void 0) return;
|
|
274
|
-
request.headers = new Headers(request.headers);
|
|
275
|
-
if (!request.headers.has("authorization")) {
|
|
276
|
-
const test = this.createTestUser();
|
|
277
|
-
const user = typeof options?.user === "object" ? options.user : void 0;
|
|
278
|
-
const sub = user?.id ?? test.id;
|
|
279
|
-
const roles = user?.roles ?? test.roles;
|
|
280
|
-
const token = await this.jwtProvider.create({
|
|
281
|
-
sub,
|
|
282
|
-
roles
|
|
283
|
-
}, user?.realm ?? this.securityProvider.getRealms()[0]?.name);
|
|
284
|
-
request.headers.set("authorization", `Bearer ${token}`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
//#endregion
|
|
291
|
-
//#region src/server-security/index.ts
|
|
292
|
-
/**
|
|
293
|
-
* Plugin for Alepha Server that provides security features. Based on the Alepha Security module.
|
|
294
|
-
*
|
|
295
|
-
* By default, all $action will be guarded by a permission check.
|
|
296
|
-
*
|
|
297
|
-
* @see {@link ServerSecurityProvider}
|
|
298
|
-
* @module alepha.server.security
|
|
299
|
-
*/
|
|
300
|
-
const AlephaServerSecurity = $module({
|
|
301
|
-
name: "alepha.server.security",
|
|
302
|
-
descriptors: [
|
|
303
|
-
$realm,
|
|
304
|
-
$role,
|
|
305
|
-
$permission,
|
|
306
|
-
$basicAuth
|
|
307
|
-
],
|
|
308
|
-
services: [
|
|
309
|
-
AlephaServer,
|
|
310
|
-
AlephaSecurity,
|
|
311
|
-
ServerSecurityProvider,
|
|
312
|
-
ServerBasicAuthProvider
|
|
313
|
-
]
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
//#endregion
|
|
317
|
-
//#region src/server-cache/providers/ServerCacheProvider.ts
|
|
318
|
-
ActionDescriptor.prototype.invalidate = async function() {
|
|
319
|
-
await this.alepha.inject(ServerCacheProvider).invalidate(this.route);
|
|
320
|
-
};
|
|
321
|
-
var ServerCacheProvider = class {
|
|
322
|
-
log = $logger();
|
|
323
|
-
alepha = $inject(Alepha);
|
|
324
|
-
time = $inject(DateTimeProvider);
|
|
325
|
-
cache = $cache({ provider: "memory" });
|
|
326
|
-
generateETag(content) {
|
|
327
|
-
return `"${createHash("md5").update(content).digest("hex")}"`;
|
|
328
|
-
}
|
|
329
|
-
async invalidate(route) {
|
|
330
|
-
if (!route.cache) return;
|
|
331
|
-
await this.cache.invalidate(this.createCacheKey(route));
|
|
332
|
-
}
|
|
333
|
-
onActionRequest = $hook({
|
|
334
|
-
on: "action:onRequest",
|
|
335
|
-
handler: async ({ action, request }) => {
|
|
336
|
-
const cache = action.route.cache;
|
|
337
|
-
if (this.shouldStore(cache)) {
|
|
338
|
-
const key = this.createCacheKey(action.route, request);
|
|
339
|
-
const cached = await this.cache.get(key);
|
|
340
|
-
if (cached) {
|
|
341
|
-
const body = cached.contentType === "application/json" ? JSON.parse(cached.body) : cached.body;
|
|
342
|
-
this.log.trace("Cache hit for action", {
|
|
343
|
-
key,
|
|
344
|
-
action: action.name
|
|
345
|
-
});
|
|
346
|
-
request.reply.body = body;
|
|
347
|
-
} else this.log.trace("Cache miss for action", {
|
|
348
|
-
key,
|
|
349
|
-
action: action.name
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
onActionResponse = $hook({
|
|
355
|
-
on: "action:onResponse",
|
|
356
|
-
handler: async ({ action, request, response }) => {
|
|
357
|
-
const cache = action.route.cache;
|
|
358
|
-
if (!this.shouldStore(cache) || !response) return;
|
|
359
|
-
if (request.reply.status && request.reply.status >= 400) return;
|
|
360
|
-
const contentType = typeof response === "string" ? "text/plain" : "application/json";
|
|
361
|
-
const body = contentType === "text/plain" ? response : JSON.stringify(response);
|
|
362
|
-
const generatedEtag = this.generateETag(body);
|
|
363
|
-
const lastModified = this.time.toISOString();
|
|
364
|
-
const key = this.createCacheKey(action.route, request);
|
|
365
|
-
this.log.trace("Storing response", {
|
|
366
|
-
key,
|
|
367
|
-
action: action.name
|
|
368
|
-
});
|
|
369
|
-
await this.cache.set(key, {
|
|
370
|
-
body,
|
|
371
|
-
lastModified,
|
|
372
|
-
contentType,
|
|
373
|
-
hash: generatedEtag
|
|
374
|
-
});
|
|
375
|
-
const cacheControl = this.buildCacheControlHeader(cache);
|
|
376
|
-
if (cacheControl) request.reply.setHeader("cache-control", cacheControl);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
onRequest = $hook({
|
|
380
|
-
on: "server:onRequest",
|
|
381
|
-
handler: async ({ route, request }) => {
|
|
382
|
-
const cache = route.cache;
|
|
383
|
-
const shouldStore = this.shouldStore(cache);
|
|
384
|
-
const shouldUseEtag = this.shouldUseEtag(cache);
|
|
385
|
-
if (!shouldStore && !shouldUseEtag) return;
|
|
386
|
-
const key = this.createCacheKey(route, request);
|
|
387
|
-
const cached = await this.cache.get(key);
|
|
388
|
-
if (cached) {
|
|
389
|
-
if (request.headers["if-none-match"] === cached.hash || request.headers["if-modified-since"] === cached.lastModified) {
|
|
390
|
-
request.reply.status = 304;
|
|
391
|
-
request.reply.setHeader("etag", cached.hash);
|
|
392
|
-
request.reply.setHeader("last-modified", cached.lastModified);
|
|
393
|
-
this.log.trace("ETag match, returning 304", {
|
|
394
|
-
route: route.path,
|
|
395
|
-
etag: cached.hash
|
|
396
|
-
});
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
if (shouldStore) {
|
|
400
|
-
this.log.trace("Cache hit for route", {
|
|
401
|
-
key,
|
|
402
|
-
route: route.path
|
|
403
|
-
});
|
|
404
|
-
request.reply.body = cached.body;
|
|
405
|
-
request.reply.status = cached.status ?? 200;
|
|
406
|
-
if (cached.contentType) request.reply.setHeader("Content-Type", cached.contentType);
|
|
407
|
-
request.reply.setHeader("etag", cached.hash);
|
|
408
|
-
request.reply.setHeader("last-modified", cached.lastModified);
|
|
409
|
-
}
|
|
410
|
-
} else if (shouldStore) this.log.trace("Cache miss for route", {
|
|
411
|
-
key,
|
|
412
|
-
route: route.path
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
onSend = $hook({
|
|
417
|
-
on: "server:onSend",
|
|
418
|
-
handler: async ({ route, request }) => {
|
|
419
|
-
const cache = route.cache;
|
|
420
|
-
const shouldStore = this.shouldStore(cache);
|
|
421
|
-
const shouldUseEtag = this.shouldUseEtag(cache);
|
|
422
|
-
if (request.reply.headers.etag) return;
|
|
423
|
-
if (!shouldStore && shouldUseEtag && request.reply.body != null && (typeof request.reply.body === "string" || Buffer.isBuffer(request.reply.body))) {
|
|
424
|
-
const generatedEtag = this.generateETag(request.reply.body);
|
|
425
|
-
if (request.headers["if-none-match"] === generatedEtag) {
|
|
426
|
-
request.reply.status = 304;
|
|
427
|
-
request.reply.body = void 0;
|
|
428
|
-
request.reply.setHeader("etag", generatedEtag);
|
|
429
|
-
this.log.trace("ETag match on send, returning 304", {
|
|
430
|
-
route: route.path,
|
|
431
|
-
etag: generatedEtag
|
|
432
|
-
});
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
onResponse = $hook({
|
|
439
|
-
on: "server:onResponse",
|
|
440
|
-
priority: "first",
|
|
441
|
-
handler: async ({ route, request, response }) => {
|
|
442
|
-
const cache = route.cache;
|
|
443
|
-
const cacheControl = this.buildCacheControlHeader(cache);
|
|
444
|
-
if (cacheControl) response.headers["cache-control"] = cacheControl;
|
|
445
|
-
const shouldStore = this.shouldStore(cache);
|
|
446
|
-
const shouldUseEtag = this.shouldUseEtag(cache);
|
|
447
|
-
if (!shouldStore && !shouldUseEtag) return;
|
|
448
|
-
if (typeof response.body !== "string") return;
|
|
449
|
-
if (response.status && response.status >= 400) return;
|
|
450
|
-
const key = this.createCacheKey(route, request);
|
|
451
|
-
const generatedEtag = this.generateETag(response.body);
|
|
452
|
-
const lastModified = this.time.toISOString();
|
|
453
|
-
response.headers ??= {};
|
|
454
|
-
if (shouldStore) {
|
|
455
|
-
this.log.trace("Storing response", {
|
|
456
|
-
key,
|
|
457
|
-
route: route.path,
|
|
458
|
-
cache: !!cache,
|
|
459
|
-
etag: shouldUseEtag
|
|
460
|
-
});
|
|
461
|
-
await this.cache.set(key, {
|
|
462
|
-
body: response.body,
|
|
463
|
-
status: response.status,
|
|
464
|
-
contentType: response.headers?.["content-type"],
|
|
465
|
-
lastModified,
|
|
466
|
-
hash: generatedEtag
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
if (shouldUseEtag) {
|
|
470
|
-
response.headers.etag = generatedEtag;
|
|
471
|
-
response.headers["last-modified"] = lastModified;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
buildCacheControlHeader(cache) {
|
|
476
|
-
if (!cache) return;
|
|
477
|
-
if (cache === true || typeof cache === "string" || typeof cache === "number") return;
|
|
478
|
-
const control = cache.control;
|
|
479
|
-
if (!control) return;
|
|
480
|
-
if (typeof control === "string") return control;
|
|
481
|
-
if (control === true) return "public, max-age=300";
|
|
482
|
-
const directives = [];
|
|
483
|
-
if (control.public) directives.push("public");
|
|
484
|
-
if (control.private) directives.push("private");
|
|
485
|
-
if (control.noCache) directives.push("no-cache");
|
|
486
|
-
if (control.noStore) directives.push("no-store");
|
|
487
|
-
if (control.maxAge !== void 0) {
|
|
488
|
-
const maxAgeSeconds = this.durationToSeconds(control.maxAge);
|
|
489
|
-
directives.push(`max-age=${maxAgeSeconds}`);
|
|
490
|
-
}
|
|
491
|
-
if (control.sMaxAge !== void 0) {
|
|
492
|
-
const sMaxAgeSeconds = this.durationToSeconds(control.sMaxAge);
|
|
493
|
-
directives.push(`s-maxage=${sMaxAgeSeconds}`);
|
|
494
|
-
}
|
|
495
|
-
if (control.mustRevalidate) directives.push("must-revalidate");
|
|
496
|
-
if (control.proxyRevalidate) directives.push("proxy-revalidate");
|
|
497
|
-
if (control.immutable) directives.push("immutable");
|
|
498
|
-
return directives.length > 0 ? directives.join(", ") : void 0;
|
|
499
|
-
}
|
|
500
|
-
durationToSeconds(duration) {
|
|
501
|
-
if (typeof duration === "number") return duration;
|
|
502
|
-
return this.time.duration(duration).asSeconds();
|
|
503
|
-
}
|
|
504
|
-
shouldStore(cache) {
|
|
505
|
-
if (!cache) return false;
|
|
506
|
-
if (cache === true) return true;
|
|
507
|
-
if (typeof cache === "object" && cache.store) return true;
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
shouldUseEtag(cache) {
|
|
511
|
-
if (cache === true) return true;
|
|
512
|
-
if (typeof cache === "object" && cache.etag) return true;
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
createCacheKey(route, config) {
|
|
516
|
-
const params = [];
|
|
517
|
-
for (const [key, value] of Object.entries(config?.params ?? {})) params.push(`${key}=${value}`);
|
|
518
|
-
for (const [key, value] of Object.entries(config?.query ?? {})) params.push(`${key}=${value}`);
|
|
519
|
-
return `${route.method}:${route.path.replaceAll(":", "")}:${params.join(",").replaceAll(":", "")}`;
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
//#endregion
|
|
524
|
-
//#region src/server-cache/index.ts
|
|
525
|
-
/**
|
|
526
|
-
* Plugin for Alepha Server that provides server-side caching capabilities.
|
|
527
|
-
* It uses the Alepha Cache module to cache responses from server actions ($action).
|
|
528
|
-
* It also provides a ETag-based cache invalidation mechanism.
|
|
529
|
-
*
|
|
530
|
-
* @example
|
|
531
|
-
* ```ts
|
|
532
|
-
* import { Alepha } from "alepha";
|
|
533
|
-
* import { $action } from "alepha/server";
|
|
534
|
-
* import { AlephaServerCache } from "alepha/server/cache";
|
|
535
|
-
*
|
|
536
|
-
* class ApiServer {
|
|
537
|
-
* hello = $action({
|
|
538
|
-
* cache: true,
|
|
539
|
-
* handler: () => "Hello, World!",
|
|
540
|
-
* });
|
|
541
|
-
* }
|
|
542
|
-
*
|
|
543
|
-
* const alepha = Alepha.create()
|
|
544
|
-
* .with(AlephaServerCache)
|
|
545
|
-
* .with(ApiServer);
|
|
546
|
-
*
|
|
547
|
-
* run(alepha);
|
|
548
|
-
* ```
|
|
549
|
-
*
|
|
550
|
-
* @see {@link ServerCacheProvider}
|
|
551
|
-
* @module alepha.server.cache
|
|
552
|
-
*/
|
|
553
|
-
const AlephaServerCache = $module({
|
|
554
|
-
name: "alepha.server.cache",
|
|
555
|
-
services: [AlephaCache, ServerCacheProvider]
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
//#endregion
|
|
559
|
-
//#region src/server-static/descriptors/$serve.ts
|
|
560
|
-
/**
|
|
561
|
-
* Create a new static file handler.
|
|
562
|
-
*/
|
|
563
|
-
const $serve = (options = {}) => {
|
|
564
|
-
return createDescriptor(ServeDescriptor, options);
|
|
565
|
-
};
|
|
566
|
-
var ServeDescriptor = class extends Descriptor {};
|
|
567
|
-
$serve[KIND] = ServeDescriptor;
|
|
568
|
-
|
|
569
|
-
//#endregion
|
|
570
|
-
//#region src/server-static/providers/ServerStaticProvider.ts
|
|
571
|
-
var ServerStaticProvider = class {
|
|
572
|
-
alepha = $inject(Alepha);
|
|
573
|
-
routerProvider = $inject(ServerRouterProvider);
|
|
574
|
-
dateTimeProvider = $inject(DateTimeProvider);
|
|
575
|
-
fileDetector = $inject(FileDetector);
|
|
576
|
-
log = $logger();
|
|
577
|
-
directories = [];
|
|
578
|
-
configure = $hook({
|
|
579
|
-
on: "configure",
|
|
580
|
-
handler: async () => {
|
|
581
|
-
await Promise.all(this.alepha.descriptors($serve).map((it) => this.createStaticServer(it.options)));
|
|
582
|
-
}
|
|
583
|
-
});
|
|
584
|
-
async createStaticServer(options) {
|
|
585
|
-
const prefix = options.path ?? "/";
|
|
586
|
-
let root = options.root ?? process.cwd();
|
|
587
|
-
if (!isAbsolute(root)) root = join(process.cwd(), root);
|
|
588
|
-
this.log.debug("Serve static files", {
|
|
589
|
-
prefix,
|
|
590
|
-
root
|
|
591
|
-
});
|
|
592
|
-
await stat(root);
|
|
593
|
-
const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);
|
|
594
|
-
const routes = await Promise.all(files.map(async (file) => {
|
|
595
|
-
const path = file.replace(root, "").replace(/\\/g, "/");
|
|
596
|
-
this.log.trace(`Mount ${join(prefix, path)} -> ${join(root, path)}`);
|
|
597
|
-
return {
|
|
598
|
-
path: join(prefix, encodeURI(path)),
|
|
599
|
-
handler: await this.createFileHandler(join(root, path), options)
|
|
600
|
-
};
|
|
601
|
-
}));
|
|
602
|
-
for (const route of routes) {
|
|
603
|
-
this.routerProvider.createRoute(route);
|
|
604
|
-
if (options.indexFallback !== false && route.path.endsWith("index.html")) this.routerProvider.createRoute({
|
|
605
|
-
path: route.path.replace(/index\.html$/, ""),
|
|
606
|
-
handler: route.handler
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
this.directories.push({
|
|
610
|
-
options,
|
|
611
|
-
files: files.map((file) => file.replace(root, "").replace(/\\/g, "/"))
|
|
612
|
-
});
|
|
613
|
-
if (options.historyApiFallback) this.routerProvider.createRoute({
|
|
614
|
-
path: join(prefix, "*").replace(/\\/g, "/"),
|
|
615
|
-
handler: async (request) => {
|
|
616
|
-
const { reply } = request;
|
|
617
|
-
if (request.url.pathname.includes(".")) {
|
|
618
|
-
reply.headers["content-type"] = "text/plain";
|
|
619
|
-
reply.body = "Not Found";
|
|
620
|
-
reply.status = 404;
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
reply.headers["content-type"] = "text/html";
|
|
624
|
-
reply.status = 200;
|
|
625
|
-
return createReadStream(join(root, "index.html"));
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
async createFileHandler(filepath, options) {
|
|
630
|
-
const filename = basename(filepath);
|
|
631
|
-
const hasGzip = await access(`${filepath}.gz`).then(() => true).catch(() => false);
|
|
632
|
-
const hasBr = await access(`${filepath}.br`).then(() => true).catch(() => false);
|
|
633
|
-
const fileStat = await stat(filepath);
|
|
634
|
-
const lastModified = fileStat.mtime.toUTCString();
|
|
635
|
-
const etag = `"${fileStat.size}-${fileStat.mtime.getTime()}"`;
|
|
636
|
-
const contentType = this.fileDetector.getContentType(filename);
|
|
637
|
-
const cacheControl = this.getCacheControl(filename, options);
|
|
638
|
-
return async (request) => {
|
|
639
|
-
const { headers, reply } = request;
|
|
640
|
-
let path = filepath;
|
|
641
|
-
const encoding = headers["accept-encoding"];
|
|
642
|
-
if (encoding) {
|
|
643
|
-
if (hasBr && encoding.includes("br")) {
|
|
644
|
-
reply.headers["content-encoding"] = "br";
|
|
645
|
-
path += ".br";
|
|
646
|
-
} else if (hasGzip && encoding.includes("gzip")) {
|
|
647
|
-
reply.headers["content-encoding"] = "gzip";
|
|
648
|
-
path += ".gz";
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
reply.headers["content-type"] = contentType;
|
|
652
|
-
reply.headers["accept-ranges"] = "bytes";
|
|
653
|
-
reply.headers["last-modified"] = lastModified;
|
|
654
|
-
if (cacheControl) {
|
|
655
|
-
reply.headers["cache-control"] = `public, max-age=${cacheControl.maxAge}`;
|
|
656
|
-
if (cacheControl.immutable) reply.headers["cache-control"] += ", immutable";
|
|
657
|
-
}
|
|
658
|
-
reply.headers.etag = etag;
|
|
659
|
-
if (headers["if-none-match"] === etag || headers["if-modified-since"] === lastModified) {
|
|
660
|
-
reply.status = 304;
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
return createReadStream(path);
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
getCacheFileTypes() {
|
|
667
|
-
return [
|
|
668
|
-
".js",
|
|
669
|
-
".css",
|
|
670
|
-
".woff",
|
|
671
|
-
".woff2",
|
|
672
|
-
".ttf",
|
|
673
|
-
".eot",
|
|
674
|
-
".otf",
|
|
675
|
-
".jpg",
|
|
676
|
-
".jpeg",
|
|
677
|
-
".png",
|
|
678
|
-
".svg",
|
|
679
|
-
".gif"
|
|
680
|
-
];
|
|
681
|
-
}
|
|
682
|
-
getCacheControl(filename, options) {
|
|
683
|
-
if (!options.cacheControl) return;
|
|
684
|
-
const fileTypes = options.cacheControl.fileTypes ?? this.getCacheFileTypes();
|
|
685
|
-
for (const type of fileTypes) if (filename.endsWith(type)) return {
|
|
686
|
-
immutable: options.cacheControl.immutable ?? true,
|
|
687
|
-
maxAge: this.dateTimeProvider.duration(options.cacheControl.maxAge ?? [30, "days"]).as("seconds")
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
async getAllFiles(dir, ignoreDotEnvFiles = true) {
|
|
691
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
692
|
-
return (await Promise.all(entries.map((dirent) => {
|
|
693
|
-
if (ignoreDotEnvFiles && dirent.name.startsWith(".")) return [];
|
|
694
|
-
const fullPath = join(dir, dirent.name);
|
|
695
|
-
return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;
|
|
696
|
-
}))).flat();
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
//#endregion
|
|
701
|
-
//#region src/server-static/index.ts
|
|
702
|
-
/**
|
|
703
|
-
* Create static file server with `$static()`.
|
|
704
|
-
*
|
|
705
|
-
* @see {@link ServerStaticProvider}
|
|
706
|
-
* @module alepha.server.static
|
|
707
|
-
*/
|
|
708
|
-
const AlephaServerStatic = $module({
|
|
709
|
-
name: "alepha.server.static",
|
|
710
|
-
descriptors: [$serve],
|
|
711
|
-
services: [AlephaServer, ServerStaticProvider]
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
//#endregion
|
|
715
|
-
//#region src/server-swagger/descriptors/$swagger.ts
|
|
12
|
+
//#region src/server-swagger/primitives/$swagger.ts
|
|
716
13
|
/**
|
|
717
|
-
* Creates an OpenAPI/Swagger documentation
|
|
14
|
+
* Creates an OpenAPI/Swagger documentation primitive with interactive UI.
|
|
718
15
|
*
|
|
719
|
-
* Automatically generates API documentation from your $action
|
|
16
|
+
* Automatically generates API documentation from your $action primitives and serves
|
|
720
17
|
* an interactive Swagger UI for testing endpoints. Supports customization, tag filtering,
|
|
721
18
|
* and OAuth configuration.
|
|
722
19
|
*
|
|
@@ -737,10 +34,10 @@ const AlephaServerStatic = $module({
|
|
|
737
34
|
* ```
|
|
738
35
|
*/
|
|
739
36
|
const $swagger = (options = {}) => {
|
|
740
|
-
return
|
|
37
|
+
return createPrimitive(SwaggerPrimitive, options);
|
|
741
38
|
};
|
|
742
|
-
var
|
|
743
|
-
$swagger[KIND] =
|
|
39
|
+
var SwaggerPrimitive = class extends Primitive {};
|
|
40
|
+
$swagger[KIND] = SwaggerPrimitive;
|
|
744
41
|
|
|
745
42
|
//#endregion
|
|
746
43
|
//#region src/server-swagger/providers/ServerSwaggerProvider.ts
|
|
@@ -765,14 +62,14 @@ var ServerSwaggerProvider = class {
|
|
|
765
62
|
on: "configure",
|
|
766
63
|
priority: "last",
|
|
767
64
|
handler: async (alepha) => {
|
|
768
|
-
const options = alepha.
|
|
65
|
+
const options = alepha.primitives($swagger)?.[0]?.options;
|
|
769
66
|
if (!options) return;
|
|
770
67
|
this.json = await this.createSwagger(options);
|
|
771
68
|
}
|
|
772
69
|
});
|
|
773
70
|
async createSwagger(options) {
|
|
774
71
|
if (options.disabled) return;
|
|
775
|
-
const json = this.configureOpenApi(this.alepha.
|
|
72
|
+
const json = this.configureOpenApi(this.alepha.primitives($action), options);
|
|
776
73
|
if (options.rewrite) options.rewrite(json);
|
|
777
74
|
const prefix = options.prefix ?? "/docs";
|
|
778
75
|
this.configureSwaggerApi(prefix, json);
|
|
@@ -940,7 +237,7 @@ window.onload = function() {
|
|
|
940
237
|
};
|
|
941
238
|
`.trim();
|
|
942
239
|
const dirname = fileURLToPath(import.meta.url);
|
|
943
|
-
const root = await this.getAssetPath(ui.root, join(dirname, "../../assets/swagger-ui"), join(dirname, "../../../../assets/swagger-ui"));
|
|
240
|
+
const root = await this.getAssetPath(ui.root, join(dirname, "../../assets/swagger-ui"), join(dirname, "../../../assets/swagger-ui"), join(dirname, "../../../../assets/swagger-ui"));
|
|
944
241
|
if (!root) {
|
|
945
242
|
this.log.warn(`Failed to locate Swagger UI assets for path ${prefix}`);
|
|
946
243
|
return;
|
|
@@ -1001,17 +298,17 @@ window.onload = function() {
|
|
|
1001
298
|
*/
|
|
1002
299
|
const AlephaServerSwagger = $module({
|
|
1003
300
|
name: "alepha.server.swagger",
|
|
1004
|
-
|
|
301
|
+
primitives: [$swagger],
|
|
1005
302
|
services: [ServerSwaggerProvider],
|
|
1006
303
|
register: (alepha) => {
|
|
1007
304
|
alepha.with(AlephaServer);
|
|
1008
305
|
alepha.with(AlephaServerCache);
|
|
1009
306
|
alepha.with(AlephaServerStatic);
|
|
1010
307
|
alepha.with(ServerSwaggerProvider);
|
|
1011
|
-
alepha.
|
|
308
|
+
alepha.store.push("alepha.build.assets", "alepha");
|
|
1012
309
|
}
|
|
1013
310
|
});
|
|
1014
311
|
|
|
1015
312
|
//#endregion
|
|
1016
|
-
export { $swagger, AlephaServerSwagger, ServerSwaggerProvider,
|
|
313
|
+
export { $swagger, AlephaServerSwagger, ServerSwaggerProvider, SwaggerPrimitive, swaggerOptions };
|
|
1017
314
|
//# sourceMappingURL=index.js.map
|