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,10 +1,6 @@
|
|
|
1
|
-
import { $
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
|
-
import { $logger } from "alepha/logger";
|
|
6
|
-
import { $retry } from "alepha/retry";
|
|
7
|
-
import { ReadableStream } from "node:stream/web";
|
|
1
|
+
import { $module, t } from "alepha";
|
|
2
|
+
import { userAccountInfoSchema } from "alepha/security";
|
|
3
|
+
import { apiLinksResponseSchema } from "alepha/server/links";
|
|
8
4
|
|
|
9
5
|
//#region src/server-auth/constants/routes.ts
|
|
10
6
|
const alephaServerAuthRoutes = {
|
|
@@ -27,980 +23,6 @@ const authenticationProviderSchema = t.object({
|
|
|
27
23
|
], { description: "Type of the authentication provider." })
|
|
28
24
|
}, { title: "AuthenticationProvider" });
|
|
29
25
|
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/server-security/providers/ServerBasicAuthProvider.ts
|
|
32
|
-
var ServerBasicAuthProvider = class {
|
|
33
|
-
alepha = $inject(Alepha);
|
|
34
|
-
log = $logger();
|
|
35
|
-
routerProvider = $inject(ServerRouterProvider);
|
|
36
|
-
realm = "Secure Area";
|
|
37
|
-
/**
|
|
38
|
-
* Registered basic auth descriptors with their configurations
|
|
39
|
-
*/
|
|
40
|
-
registeredAuths = [];
|
|
41
|
-
/**
|
|
42
|
-
* Register a basic auth configuration (called by descriptors)
|
|
43
|
-
*/
|
|
44
|
-
registerAuth(config) {
|
|
45
|
-
this.registeredAuths.push(config);
|
|
46
|
-
}
|
|
47
|
-
onStart = $hook({
|
|
48
|
-
on: "start",
|
|
49
|
-
handler: async () => {
|
|
50
|
-
for (const auth of this.registeredAuths) if (auth.paths) for (const pattern of auth.paths) {
|
|
51
|
-
const matchedRoutes = this.routerProvider.getRoutes(pattern);
|
|
52
|
-
for (const route of matchedRoutes) route.secure = { basic: {
|
|
53
|
-
username: auth.username,
|
|
54
|
-
password: auth.password
|
|
55
|
-
} };
|
|
56
|
-
}
|
|
57
|
-
if (this.registeredAuths.length > 0) this.log.info(`Initialized with ${this.registeredAuths.length} registered basic-auth configurations.`);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
/**
|
|
61
|
-
* Hook into server:onRequest to check basic auth
|
|
62
|
-
*/
|
|
63
|
-
onRequest = $hook({
|
|
64
|
-
on: "server:onRequest",
|
|
65
|
-
handler: async ({ route, request }) => {
|
|
66
|
-
const routeAuth = route.secure;
|
|
67
|
-
if (typeof routeAuth === "object" && "basic" in routeAuth && routeAuth.basic) this.checkAuth(request, routeAuth.basic);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
/**
|
|
71
|
-
* Hook into action:onRequest to check basic auth for actions
|
|
72
|
-
*/
|
|
73
|
-
onActionRequest = $hook({
|
|
74
|
-
on: "action:onRequest",
|
|
75
|
-
handler: async ({ action, request }) => {
|
|
76
|
-
const routeAuth = action.route.secure;
|
|
77
|
-
if (isBasicAuth(routeAuth)) this.checkAuth(request, routeAuth.basic);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
/**
|
|
81
|
-
* Check basic authentication
|
|
82
|
-
*/
|
|
83
|
-
checkAuth(request, options) {
|
|
84
|
-
const authHeader = request.headers?.authorization;
|
|
85
|
-
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
86
|
-
this.sendAuthRequired(request);
|
|
87
|
-
throw new HttpError({
|
|
88
|
-
status: 401,
|
|
89
|
-
message: "Authentication required"
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
const base64Credentials = authHeader.slice(6);
|
|
93
|
-
const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8");
|
|
94
|
-
const colonIndex = credentials.indexOf(":");
|
|
95
|
-
const username = colonIndex !== -1 ? credentials.slice(0, colonIndex) : credentials;
|
|
96
|
-
const password = colonIndex !== -1 ? credentials.slice(colonIndex + 1) : "";
|
|
97
|
-
if (!this.timingSafeCredentialCheck(username, password, options.username, options.password)) {
|
|
98
|
-
this.sendAuthRequired(request);
|
|
99
|
-
this.log.warn(`Failed basic auth attempt for user`, { username });
|
|
100
|
-
throw new HttpError({
|
|
101
|
-
status: 401,
|
|
102
|
-
message: "Invalid credentials"
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Performs a timing-safe comparison of credentials to prevent timing attacks.
|
|
108
|
-
* Always compares both username and password to avoid leaking which one is wrong.
|
|
109
|
-
*/
|
|
110
|
-
timingSafeCredentialCheck(inputUsername, inputPassword, expectedUsername, expectedPassword) {
|
|
111
|
-
const inputUserBuf = Buffer.from(inputUsername, "utf-8");
|
|
112
|
-
const expectedUserBuf = Buffer.from(expectedUsername, "utf-8");
|
|
113
|
-
const inputPassBuf = Buffer.from(inputPassword, "utf-8");
|
|
114
|
-
const expectedPassBuf = Buffer.from(expectedPassword, "utf-8");
|
|
115
|
-
return (this.safeCompare(inputUserBuf, expectedUserBuf) & this.safeCompare(inputPassBuf, expectedPassBuf)) === 1;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Compares two buffers in constant time, handling different lengths safely.
|
|
119
|
-
* Returns 1 if equal, 0 if not equal.
|
|
120
|
-
*/
|
|
121
|
-
safeCompare(input, expected) {
|
|
122
|
-
if (input.length !== expected.length) {
|
|
123
|
-
timingSafeEqual(input, input);
|
|
124
|
-
return 0;
|
|
125
|
-
}
|
|
126
|
-
return timingSafeEqual(input, expected) ? 1 : 0;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Send WWW-Authenticate header
|
|
130
|
-
*/
|
|
131
|
-
sendAuthRequired(request) {
|
|
132
|
-
request.reply.setHeader("WWW-Authenticate", `Basic realm="${this.realm}"`);
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
const isBasicAuth = (value) => {
|
|
136
|
-
return typeof value === "object" && !!value && "basic" in value && !!value.basic;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
//#endregion
|
|
140
|
-
//#region src/server-security/descriptors/$basicAuth.ts
|
|
141
|
-
/**
|
|
142
|
-
* Declares HTTP Basic Authentication for server routes.
|
|
143
|
-
* This descriptor provides methods to protect routes with username/password authentication.
|
|
144
|
-
*/
|
|
145
|
-
const $basicAuth = (options) => {
|
|
146
|
-
return createDescriptor(BasicAuthDescriptor, options);
|
|
147
|
-
};
|
|
148
|
-
var BasicAuthDescriptor = class extends Descriptor {
|
|
149
|
-
serverBasicAuthProvider = $inject(ServerBasicAuthProvider);
|
|
150
|
-
get name() {
|
|
151
|
-
return this.options.name ?? `${this.config.propertyKey}`;
|
|
152
|
-
}
|
|
153
|
-
onInit() {
|
|
154
|
-
this.serverBasicAuthProvider.registerAuth(this.options);
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Checks basic auth for the given request using this descriptor's configuration.
|
|
158
|
-
*/
|
|
159
|
-
check(request, options) {
|
|
160
|
-
const mergedOptions = {
|
|
161
|
-
...this.options,
|
|
162
|
-
...options
|
|
163
|
-
};
|
|
164
|
-
this.serverBasicAuthProvider.checkAuth(request, mergedOptions);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
$basicAuth[KIND] = BasicAuthDescriptor;
|
|
168
|
-
|
|
169
|
-
//#endregion
|
|
170
|
-
//#region src/server-security/providers/ServerSecurityProvider.ts
|
|
171
|
-
var ServerSecurityProvider = class {
|
|
172
|
-
log = $logger();
|
|
173
|
-
securityProvider = $inject(SecurityProvider);
|
|
174
|
-
jwtProvider = $inject(JwtProvider);
|
|
175
|
-
alepha = $inject(Alepha);
|
|
176
|
-
onConfigure = $hook({
|
|
177
|
-
on: "configure",
|
|
178
|
-
handler: async () => {
|
|
179
|
-
for (const action of this.alepha.descriptors($action)) {
|
|
180
|
-
if (action.options.disabled || action.options.secure === false || this.securityProvider.getRealms().length === 0) continue;
|
|
181
|
-
if (typeof action.options.secure !== "object") this.securityProvider.createPermission({
|
|
182
|
-
name: action.name,
|
|
183
|
-
group: action.group,
|
|
184
|
-
method: action.route.method,
|
|
185
|
-
path: action.route.path
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
onActionRequest = $hook({
|
|
191
|
-
on: "action:onRequest",
|
|
192
|
-
handler: async ({ action, request, options }) => {
|
|
193
|
-
if (action.options.secure === false && !options.user) {
|
|
194
|
-
this.log.trace("Skipping security check for route");
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (isBasicAuth(action.route.secure)) return;
|
|
198
|
-
const permission = this.securityProvider.getPermissions().find((it) => it.path === action.route.path && it.method === action.route.method);
|
|
199
|
-
try {
|
|
200
|
-
request.user = this.createUserFromLocalFunctionContext(options, permission);
|
|
201
|
-
const route = action.route;
|
|
202
|
-
if (typeof route.secure === "object") this.check(request.user, route.secure);
|
|
203
|
-
this.alepha.state.set("alepha.server.request.user", this.alepha.codec.decode(userAccountInfoSchema, request.user));
|
|
204
|
-
} catch (error) {
|
|
205
|
-
if (action.options.secure || permission) throw error;
|
|
206
|
-
this.log.trace("Skipping security check for action");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
onRequest = $hook({
|
|
211
|
-
on: "server:onRequest",
|
|
212
|
-
priority: "last",
|
|
213
|
-
handler: async ({ request, route }) => {
|
|
214
|
-
if (route.secure === false) {
|
|
215
|
-
this.log.trace("Skipping security check for route - explicitly disabled");
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (isBasicAuth(route.secure)) return;
|
|
219
|
-
const permission = this.securityProvider.getPermissions().find((it) => it.path === route.path && it.method === route.method);
|
|
220
|
-
if (!request.headers.authorization && !route.secure && !permission) {
|
|
221
|
-
this.log.trace("Skipping security check for route - no authorization header and not secure");
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
request.user = await this.securityProvider.createUserFromToken(request.headers.authorization, { permission });
|
|
226
|
-
if (typeof route.secure === "object") this.check(request.user, route.secure);
|
|
227
|
-
this.alepha.state.set("alepha.server.request.user", this.alepha.codec.decode(userAccountInfoSchema, request.user));
|
|
228
|
-
this.log.trace("User set from request token", {
|
|
229
|
-
user: request.user,
|
|
230
|
-
permission
|
|
231
|
-
});
|
|
232
|
-
} catch (error) {
|
|
233
|
-
if (route.secure || permission) throw error;
|
|
234
|
-
this.log.trace("Skipping security check for route - error occurred", error);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
check(user, secure) {
|
|
239
|
-
if (secure.realm) {
|
|
240
|
-
if (user.realm !== secure.realm) throw new ForbiddenError(`User must belong to realm '${secure.realm}' to access this route`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Get the user account token for a local action call.
|
|
245
|
-
* There are three possible sources for the user:
|
|
246
|
-
* - `options.user`: the user passed in the options
|
|
247
|
-
* - `"system"`: the system user from the state (you MUST set state `server.security.system.user`)
|
|
248
|
-
* - `"context"`: the user from the request context (you MUST be in an HTTP request context)
|
|
249
|
-
*
|
|
250
|
-
* Priority order: `options.user` > `"system"` > `"context"`.
|
|
251
|
-
*
|
|
252
|
-
* In testing environment, if no user is provided, a test user is created based on the SecurityProvider's roles.
|
|
253
|
-
*/
|
|
254
|
-
createUserFromLocalFunctionContext(options, permission) {
|
|
255
|
-
const fromOptions = typeof options.user === "object" ? options.user : void 0;
|
|
256
|
-
const type = typeof options.user === "string" ? options.user : void 0;
|
|
257
|
-
let user;
|
|
258
|
-
const fromContext = this.alepha.context.get("request")?.user;
|
|
259
|
-
const fromSystem = this.alepha.state.get("alepha.server.security.system.user");
|
|
260
|
-
if (type === "system") user = fromSystem;
|
|
261
|
-
else if (type === "context") user = fromContext;
|
|
262
|
-
else user = fromOptions ?? fromContext ?? fromSystem;
|
|
263
|
-
if (!user) {
|
|
264
|
-
if (this.alepha.isTest() && !("user" in options)) return this.createTestUser();
|
|
265
|
-
throw new UnauthorizedError("User is required for calling this action");
|
|
266
|
-
}
|
|
267
|
-
const roles = user.roles ?? (this.alepha.isTest() ? this.securityProvider.getRoles().map((role) => role.name) : []);
|
|
268
|
-
let ownership;
|
|
269
|
-
if (permission) {
|
|
270
|
-
const result = this.securityProvider.checkPermission(permission, ...roles);
|
|
271
|
-
if (!result.isAuthorized) throw new ForbiddenError(`Permission '${this.securityProvider.permissionToString(permission)}' is required for this route`);
|
|
272
|
-
ownership = result.ownership;
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
...user,
|
|
276
|
-
ownership
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
createTestUser() {
|
|
280
|
-
return {
|
|
281
|
-
id: randomUUID(),
|
|
282
|
-
name: "Test",
|
|
283
|
-
roles: this.securityProvider.getRoles().map((role) => role.name)
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
onClientRequest = $hook({
|
|
287
|
-
on: "client:onRequest",
|
|
288
|
-
handler: async ({ request, options }) => {
|
|
289
|
-
if (!this.alepha.isTest()) return;
|
|
290
|
-
if ("user" in options && options.user === void 0) return;
|
|
291
|
-
request.headers = new Headers(request.headers);
|
|
292
|
-
if (!request.headers.has("authorization")) {
|
|
293
|
-
const test = this.createTestUser();
|
|
294
|
-
const user = typeof options?.user === "object" ? options.user : void 0;
|
|
295
|
-
const sub = user?.id ?? test.id;
|
|
296
|
-
const roles = user?.roles ?? test.roles;
|
|
297
|
-
const token = await this.jwtProvider.create({
|
|
298
|
-
sub,
|
|
299
|
-
roles
|
|
300
|
-
}, user?.realm ?? this.securityProvider.getRealms()[0]?.name);
|
|
301
|
-
request.headers.set("authorization", `Bearer ${token}`);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
//#endregion
|
|
308
|
-
//#region src/server-security/index.ts
|
|
309
|
-
/**
|
|
310
|
-
* Plugin for Alepha Server that provides security features. Based on the Alepha Security module.
|
|
311
|
-
*
|
|
312
|
-
* By default, all $action will be guarded by a permission check.
|
|
313
|
-
*
|
|
314
|
-
* @see {@link ServerSecurityProvider}
|
|
315
|
-
* @module alepha.server.security
|
|
316
|
-
*/
|
|
317
|
-
const AlephaServerSecurity = $module({
|
|
318
|
-
name: "alepha.server.security",
|
|
319
|
-
descriptors: [
|
|
320
|
-
$realm,
|
|
321
|
-
$role,
|
|
322
|
-
$permission,
|
|
323
|
-
$basicAuth
|
|
324
|
-
],
|
|
325
|
-
services: [
|
|
326
|
-
AlephaServer,
|
|
327
|
-
AlephaSecurity,
|
|
328
|
-
ServerSecurityProvider,
|
|
329
|
-
ServerBasicAuthProvider
|
|
330
|
-
]
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
//#endregion
|
|
334
|
-
//#region src/server-links/schemas/apiLinksResponseSchema.ts
|
|
335
|
-
const apiLinkSchema = t.object({
|
|
336
|
-
name: t.text({ description: "Name of the API link, used for identification." }),
|
|
337
|
-
group: t.optional(t.text({ description: "Group to which the API link belongs, used for categorization." })),
|
|
338
|
-
path: t.text({ description: "Pathname used to access the API link." }),
|
|
339
|
-
method: t.optional(t.text({ description: "HTTP method used for the API link, e.g., GET, POST, etc. If not specified, defaults to GET." })),
|
|
340
|
-
requestBodyType: t.optional(t.text({ description: "Type of the request body for the API link. Default is application/json for POST/PUT/PATCH, null for others." })),
|
|
341
|
-
service: t.optional(t.text({ description: "Service name associated with the API link, used for service discovery." }))
|
|
342
|
-
});
|
|
343
|
-
const apiLinksResponseSchema = t.object({
|
|
344
|
-
prefix: t.optional(t.text()),
|
|
345
|
-
links: t.array(apiLinkSchema)
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
//#endregion
|
|
349
|
-
//#region src/server-links/providers/LinkProvider.ts
|
|
350
|
-
/**
|
|
351
|
-
* Browser, SSR friendly, service to handle links.
|
|
352
|
-
*/
|
|
353
|
-
var LinkProvider = class LinkProvider {
|
|
354
|
-
static path = {
|
|
355
|
-
apiLinks: "/api/_links",
|
|
356
|
-
apiSchema: "/api/_links/:name/schema"
|
|
357
|
-
};
|
|
358
|
-
log = $logger();
|
|
359
|
-
alepha = $inject(Alepha);
|
|
360
|
-
httpClient = $inject(HttpClient);
|
|
361
|
-
serverLinks = [];
|
|
362
|
-
/**
|
|
363
|
-
* Get applicative links registered on the server.
|
|
364
|
-
* This does not include lazy-loaded remote links.
|
|
365
|
-
*/
|
|
366
|
-
getServerLinks() {
|
|
367
|
-
if (this.alepha.isBrowser()) {
|
|
368
|
-
this.log.warn("Getting server links in the browser is not supported. Use `fetchLinks` to get links from the server.");
|
|
369
|
-
return [];
|
|
370
|
-
}
|
|
371
|
-
return this.serverLinks;
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Register a new link for the application.
|
|
375
|
-
*/
|
|
376
|
-
registerLink(link) {
|
|
377
|
-
if (this.alepha.isBrowser()) {
|
|
378
|
-
this.log.warn("Registering links in the browser is not supported. Use `fetchLinks` to get links from the server.");
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (!link.handler && !link.host) throw new AlephaError("Can't create link - 'handler' or 'host' is required");
|
|
382
|
-
if (this.serverLinks.some((l) => l.name === link.name)) this.serverLinks = this.serverLinks.filter((l) => l.name !== link.name);
|
|
383
|
-
this.serverLinks.push(link);
|
|
384
|
-
}
|
|
385
|
-
get links() {
|
|
386
|
-
const apiLinks = this.alepha.state.get("alepha.server.request.apiLinks")?.links;
|
|
387
|
-
if (apiLinks) {
|
|
388
|
-
if (this.alepha.isBrowser()) return apiLinks;
|
|
389
|
-
const links = [];
|
|
390
|
-
for (const link of apiLinks) {
|
|
391
|
-
const originalLink = this.serverLinks.find((l) => l.name === link.name);
|
|
392
|
-
if (originalLink) links.push(originalLink);
|
|
393
|
-
}
|
|
394
|
-
return links;
|
|
395
|
-
}
|
|
396
|
-
return this.serverLinks ?? [];
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Force browser to refresh links from the server.
|
|
400
|
-
*/
|
|
401
|
-
async fetchLinks() {
|
|
402
|
-
const { data } = await this.httpClient.fetch(`${LinkProvider.path.apiLinks}`, {
|
|
403
|
-
method: "GET",
|
|
404
|
-
schema: { response: apiLinksResponseSchema }
|
|
405
|
-
});
|
|
406
|
-
this.alepha.state.set("alepha.server.request.apiLinks", data);
|
|
407
|
-
return data.links;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Create a virtual client that can be used to call actions.
|
|
411
|
-
*
|
|
412
|
-
* Use js Proxy under the hood.
|
|
413
|
-
*/
|
|
414
|
-
client(scope = {}) {
|
|
415
|
-
return new Proxy({}, { get: (_, prop) => {
|
|
416
|
-
if (typeof prop !== "string") return;
|
|
417
|
-
return this.createVirtualAction(prop, scope);
|
|
418
|
-
} });
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Check if a link with the given name exists.
|
|
422
|
-
* @param name
|
|
423
|
-
*/
|
|
424
|
-
can(name) {
|
|
425
|
-
return this.links.some((link) => link.name === name);
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Resolve a link by its name and call it.
|
|
429
|
-
* - If link is local, it will call the local handler.
|
|
430
|
-
* - If link is remote, it will make a fetch request to the remote server.
|
|
431
|
-
*/
|
|
432
|
-
async follow(name, config = {}, options = {}) {
|
|
433
|
-
this.log.trace("Following link", {
|
|
434
|
-
name,
|
|
435
|
-
config,
|
|
436
|
-
options
|
|
437
|
-
});
|
|
438
|
-
const link = await this.getLinkByName(name, options);
|
|
439
|
-
if (link.handler && !options.request) {
|
|
440
|
-
this.log.trace("Local link found", { name });
|
|
441
|
-
return link.handler({
|
|
442
|
-
method: link.method,
|
|
443
|
-
url: new URL(`http://localhost${link.path}`),
|
|
444
|
-
query: config.query ?? {},
|
|
445
|
-
body: config.body ?? {},
|
|
446
|
-
params: config.params ?? {},
|
|
447
|
-
headers: config.headers ?? {},
|
|
448
|
-
metadata: {},
|
|
449
|
-
reply: new ServerReply()
|
|
450
|
-
}, options);
|
|
451
|
-
}
|
|
452
|
-
this.log.trace("Remote link found", {
|
|
453
|
-
name,
|
|
454
|
-
host: link.host,
|
|
455
|
-
service: link.service
|
|
456
|
-
});
|
|
457
|
-
return this.followRemote(link, config, options).then((response) => response.data);
|
|
458
|
-
}
|
|
459
|
-
createVirtualAction(name, scope = {}) {
|
|
460
|
-
const $ = async (config = {}, options = {}) => {
|
|
461
|
-
return this.follow(name, config, {
|
|
462
|
-
...scope,
|
|
463
|
-
...options
|
|
464
|
-
});
|
|
465
|
-
};
|
|
466
|
-
Object.defineProperty($, "name", {
|
|
467
|
-
value: name,
|
|
468
|
-
writable: false
|
|
469
|
-
});
|
|
470
|
-
$.run = async (config = {}, options = {}) => {
|
|
471
|
-
return this.follow(name, config, {
|
|
472
|
-
...scope,
|
|
473
|
-
...options
|
|
474
|
-
});
|
|
475
|
-
};
|
|
476
|
-
$.fetch = async (config = {}, options = {}) => {
|
|
477
|
-
const link = await this.getLinkByName(name, scope);
|
|
478
|
-
return this.followRemote(link, config, options);
|
|
479
|
-
};
|
|
480
|
-
$.can = () => {
|
|
481
|
-
return this.can(name);
|
|
482
|
-
};
|
|
483
|
-
return $;
|
|
484
|
-
}
|
|
485
|
-
async followRemote(link, config = {}, options = {}) {
|
|
486
|
-
options.request ??= {};
|
|
487
|
-
options.request.headers = new Headers(options.request.headers);
|
|
488
|
-
const als = this.alepha.context.get("request");
|
|
489
|
-
if (als?.headers.authorization) options.request.headers.set("authorization", als.headers.authorization);
|
|
490
|
-
const context = this.alepha.context.get("context");
|
|
491
|
-
if (typeof context === "string") options.request.headers.set("x-request-id", context);
|
|
492
|
-
const action = {
|
|
493
|
-
...link,
|
|
494
|
-
schema: {
|
|
495
|
-
body: t.any(),
|
|
496
|
-
response: t.any()
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
if (!link.host && link.service) action.path = `/${link.service}${action.path}`;
|
|
500
|
-
action.path = `${action.prefix ?? "/api"}${action.path}`;
|
|
501
|
-
action.prefix = void 0;
|
|
502
|
-
return this.httpClient.fetchAction({
|
|
503
|
-
host: link.host,
|
|
504
|
-
config,
|
|
505
|
-
options,
|
|
506
|
-
action
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
async getLinkByName(name, options = {}) {
|
|
510
|
-
if (this.alepha.isBrowser() && !this.alepha.state.get("alepha.server.request.apiLinks")) await this.fetchLinks();
|
|
511
|
-
const link = this.links.find((a) => a.name === name && (!options.group || a.group === options.group) && (!options.service || options.service === a.service));
|
|
512
|
-
if (!link) {
|
|
513
|
-
const error = new UnauthorizedError(`Action ${name} not found.`);
|
|
514
|
-
await this.alepha.events.emit("client:onError", {
|
|
515
|
-
route: link,
|
|
516
|
-
error
|
|
517
|
-
});
|
|
518
|
-
throw error;
|
|
519
|
-
}
|
|
520
|
-
if (options.hostname) return {
|
|
521
|
-
...link,
|
|
522
|
-
host: options.hostname
|
|
523
|
-
};
|
|
524
|
-
return link;
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
//#endregion
|
|
529
|
-
//#region src/server-links/descriptors/$client.ts
|
|
530
|
-
/**
|
|
531
|
-
* Create a new client.
|
|
532
|
-
*/
|
|
533
|
-
const $client = (scope) => {
|
|
534
|
-
return $inject(LinkProvider).client(scope);
|
|
535
|
-
};
|
|
536
|
-
$client[KIND] = "$client";
|
|
537
|
-
|
|
538
|
-
//#endregion
|
|
539
|
-
//#region src/server-links/descriptors/$remote.ts
|
|
540
|
-
/**
|
|
541
|
-
* $remote is a descriptor that allows you to define remote service access.
|
|
542
|
-
*
|
|
543
|
-
* Use it only when you have 2 or more services that need to communicate with each other.
|
|
544
|
-
*
|
|
545
|
-
* All remote services can be exposed as actions, ... or not.
|
|
546
|
-
*
|
|
547
|
-
* You can add a service account if you want to use a security layer.
|
|
548
|
-
*/
|
|
549
|
-
const $remote = (options) => {
|
|
550
|
-
return createDescriptor(RemoteDescriptor, options);
|
|
551
|
-
};
|
|
552
|
-
var RemoteDescriptor = class extends Descriptor {
|
|
553
|
-
get name() {
|
|
554
|
-
return this.options.name ?? this.config.propertyKey;
|
|
555
|
-
}
|
|
556
|
-
};
|
|
557
|
-
$remote[KIND] = RemoteDescriptor;
|
|
558
|
-
|
|
559
|
-
//#endregion
|
|
560
|
-
//#region src/server-proxy/descriptors/$proxy.ts
|
|
561
|
-
/**
|
|
562
|
-
* Creates a proxy descriptor to forward requests to another server.
|
|
563
|
-
*
|
|
564
|
-
* This descriptor enables you to create reverse proxy functionality, allowing your Alepha server
|
|
565
|
-
* to forward requests to other services while maintaining a unified API surface. It's particularly
|
|
566
|
-
* useful for microservice architectures, API gateways, or when you need to aggregate multiple
|
|
567
|
-
* services behind a single endpoint.
|
|
568
|
-
*
|
|
569
|
-
* **Key Features**
|
|
570
|
-
*
|
|
571
|
-
* - **Path-based routing**: Match specific paths or patterns to proxy
|
|
572
|
-
* - **Dynamic targets**: Support both static and dynamic target resolution
|
|
573
|
-
* - **Request/Response hooks**: Modify requests before forwarding and responses after receiving
|
|
574
|
-
* - **URL rewriting**: Transform URLs before forwarding to the target
|
|
575
|
-
* - **Conditional proxying**: Enable/disable proxies based on environment or conditions
|
|
576
|
-
*
|
|
577
|
-
* @example
|
|
578
|
-
* **Basic proxy setup:**
|
|
579
|
-
* ```ts
|
|
580
|
-
* import { $proxy } from "alepha/server/proxy";
|
|
581
|
-
*
|
|
582
|
-
* class ApiGateway {
|
|
583
|
-
* // Forward all /api/* requests to external service
|
|
584
|
-
* api = $proxy({
|
|
585
|
-
* path: "/api/*",
|
|
586
|
-
* target: "https://api.example.com"
|
|
587
|
-
* });
|
|
588
|
-
* }
|
|
589
|
-
* ```
|
|
590
|
-
*
|
|
591
|
-
* @example
|
|
592
|
-
* **Dynamic target with environment-based routing:**
|
|
593
|
-
* ```ts
|
|
594
|
-
* class ApiGateway {
|
|
595
|
-
* // Route to different environments based on configuration
|
|
596
|
-
* api = $proxy({
|
|
597
|
-
* path: "/api/*",
|
|
598
|
-
* target: () => process.env.NODE_ENV === "production"
|
|
599
|
-
* ? "https://api.prod.example.com"
|
|
600
|
-
* : "https://api.dev.example.com"
|
|
601
|
-
* });
|
|
602
|
-
* }
|
|
603
|
-
* ```
|
|
604
|
-
*
|
|
605
|
-
* @example
|
|
606
|
-
* **Advanced proxy with request/response modification:**
|
|
607
|
-
* ```ts
|
|
608
|
-
* class SecureProxy {
|
|
609
|
-
* secure = $proxy({
|
|
610
|
-
* path: "/secure/*",
|
|
611
|
-
* target: "https://secure-api.example.com",
|
|
612
|
-
* beforeRequest: async (request, proxyRequest) => {
|
|
613
|
-
* // Add authentication headers
|
|
614
|
-
* proxyRequest.headers = {
|
|
615
|
-
* ...proxyRequest.headers,
|
|
616
|
-
* 'Authorization': `Bearer ${await getServiceToken()}`,
|
|
617
|
-
* 'X-Forwarded-For': request.headers['x-forwarded-for'] || request.ip
|
|
618
|
-
* };
|
|
619
|
-
* },
|
|
620
|
-
* afterResponse: async (request, proxyResponse) => {
|
|
621
|
-
* // Log response for monitoring
|
|
622
|
-
* console.log(`Proxied ${request.url} -> ${proxyResponse.status}`);
|
|
623
|
-
* },
|
|
624
|
-
* rewrite: (url) => {
|
|
625
|
-
* // Remove /secure prefix when forwarding
|
|
626
|
-
* url.pathname = url.pathname.replace('/secure', '');
|
|
627
|
-
* }
|
|
628
|
-
* });
|
|
629
|
-
* }
|
|
630
|
-
* ```
|
|
631
|
-
*
|
|
632
|
-
* @example
|
|
633
|
-
* **Conditional proxy based on feature flags:**
|
|
634
|
-
* ```ts
|
|
635
|
-
* class FeatureProxy {
|
|
636
|
-
* newApi = $proxy({
|
|
637
|
-
* path: "/v2/*",
|
|
638
|
-
* target: "https://new-api.example.com",
|
|
639
|
-
* disabled: !process.env.ENABLE_V2_API // Disable if feature flag is off
|
|
640
|
-
* });
|
|
641
|
-
* }
|
|
642
|
-
* ```
|
|
643
|
-
*/
|
|
644
|
-
const $proxy = (options) => {
|
|
645
|
-
return createDescriptor(ProxyDescriptor, options);
|
|
646
|
-
};
|
|
647
|
-
var ProxyDescriptor = class extends Descriptor {};
|
|
648
|
-
$proxy[KIND] = ProxyDescriptor;
|
|
649
|
-
|
|
650
|
-
//#endregion
|
|
651
|
-
//#region src/server-proxy/providers/ServerProxyProvider.ts
|
|
652
|
-
var ServerProxyProvider = class {
|
|
653
|
-
log = $logger();
|
|
654
|
-
routerProvider = $inject(ServerRouterProvider);
|
|
655
|
-
alepha = $inject(Alepha);
|
|
656
|
-
configure = $hook({
|
|
657
|
-
on: "configure",
|
|
658
|
-
handler: () => {
|
|
659
|
-
for (const proxy of this.alepha.descriptors($proxy)) this.createProxy(proxy.options);
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
createProxy(options) {
|
|
663
|
-
if (options.disabled) return;
|
|
664
|
-
const path = options.path;
|
|
665
|
-
const target = typeof options.target === "function" ? options.target() : options.target;
|
|
666
|
-
if (!path.endsWith("/*")) throw new AlephaError("Proxy path should end with '/*'");
|
|
667
|
-
const handler = this.createProxyHandler(target, options);
|
|
668
|
-
for (const method of routeMethods) this.routerProvider.createRoute({
|
|
669
|
-
method,
|
|
670
|
-
path,
|
|
671
|
-
handler
|
|
672
|
-
});
|
|
673
|
-
this.log.info("Proxying", {
|
|
674
|
-
path,
|
|
675
|
-
target
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
createProxyHandler(target, options) {
|
|
679
|
-
return async (request) => {
|
|
680
|
-
const url = new URL(target + request.url.pathname);
|
|
681
|
-
if (request.url.search) url.search = request.url.search;
|
|
682
|
-
options.rewrite?.(url);
|
|
683
|
-
const requestInit = {
|
|
684
|
-
url: url.toString(),
|
|
685
|
-
method: request.method,
|
|
686
|
-
headers: {
|
|
687
|
-
...request.headers,
|
|
688
|
-
"accept-encoding": "identity"
|
|
689
|
-
},
|
|
690
|
-
body: this.getRawRequestBody(request)
|
|
691
|
-
};
|
|
692
|
-
if (requestInit.body) requestInit.duplex = "half";
|
|
693
|
-
if (options.beforeRequest) await options.beforeRequest(request, requestInit);
|
|
694
|
-
this.log.debug("Proxying request", {
|
|
695
|
-
url: url.toString(),
|
|
696
|
-
method: request.method,
|
|
697
|
-
headers: request.headers
|
|
698
|
-
});
|
|
699
|
-
const response = await fetch(requestInit.url, requestInit);
|
|
700
|
-
request.reply.status = response.status;
|
|
701
|
-
request.reply.headers = Object.fromEntries(response.headers.entries());
|
|
702
|
-
request.reply.body = response.body;
|
|
703
|
-
this.log.debug("Received response", {
|
|
704
|
-
status: request.reply.status,
|
|
705
|
-
headers: request.reply.headers
|
|
706
|
-
});
|
|
707
|
-
if (options.afterResponse) await options.afterResponse(request, response);
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
getRawRequestBody(req) {
|
|
711
|
-
const { method } = req;
|
|
712
|
-
if (method === "GET" || method === "HEAD" || method === "OPTIONS") return;
|
|
713
|
-
if (req.raw?.web?.req) return req.raw.web.req.body;
|
|
714
|
-
if (req.raw?.node?.req) {
|
|
715
|
-
const nodeReq = req.raw.node.req;
|
|
716
|
-
return ReadableStream.from(nodeReq);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
//#endregion
|
|
722
|
-
//#region src/server-proxy/index.ts
|
|
723
|
-
/**
|
|
724
|
-
* Plugin for Alepha that provides a proxy server functionality.
|
|
725
|
-
*
|
|
726
|
-
* @see {@link $proxy}
|
|
727
|
-
* @module alepha.server.proxy
|
|
728
|
-
*/
|
|
729
|
-
const AlephaServerProxy = $module({
|
|
730
|
-
name: "alepha.server.proxy",
|
|
731
|
-
descriptors: [$proxy],
|
|
732
|
-
services: [AlephaServer, ServerProxyProvider]
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
//#endregion
|
|
736
|
-
//#region src/server-links/providers/RemoteDescriptorProvider.ts
|
|
737
|
-
const envSchema$1 = t.object({ SERVER_API_PREFIX: t.text({
|
|
738
|
-
description: "Prefix for all API routes (e.g. $action).",
|
|
739
|
-
default: "/api"
|
|
740
|
-
}) });
|
|
741
|
-
var RemoteDescriptorProvider = class {
|
|
742
|
-
env = $env(envSchema$1);
|
|
743
|
-
alepha = $inject(Alepha);
|
|
744
|
-
proxyProvider = $inject(ServerProxyProvider);
|
|
745
|
-
linkProvider = $inject(LinkProvider);
|
|
746
|
-
remotes = [];
|
|
747
|
-
log = $logger();
|
|
748
|
-
getRemotes() {
|
|
749
|
-
return this.remotes;
|
|
750
|
-
}
|
|
751
|
-
configure = $hook({
|
|
752
|
-
on: "configure",
|
|
753
|
-
handler: async () => {
|
|
754
|
-
const remotes = this.alepha.descriptors($remote);
|
|
755
|
-
for (const remote of remotes) await this.registerRemote(remote);
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
start = $hook({
|
|
759
|
-
on: "start",
|
|
760
|
-
handler: async () => {
|
|
761
|
-
for (const remote of this.remotes) {
|
|
762
|
-
const token = typeof remote.serviceAccount?.token === "function" ? await remote.serviceAccount.token() : void 0;
|
|
763
|
-
if (!remote.internal) continue;
|
|
764
|
-
const { links } = await remote.links({ authorization: token });
|
|
765
|
-
for (const link of links) {
|
|
766
|
-
let path = link.path.replace(remote.prefix, "");
|
|
767
|
-
if (link.service) path = `/${link.service}${path}`;
|
|
768
|
-
this.linkProvider.registerLink({
|
|
769
|
-
...link,
|
|
770
|
-
prefix: remote.prefix,
|
|
771
|
-
path,
|
|
772
|
-
method: link.method ?? "GET",
|
|
773
|
-
host: remote.url,
|
|
774
|
-
service: remote.name
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
this.log.info(`Remote '${remote.name}' OK`, {
|
|
778
|
-
links: remote.links.length,
|
|
779
|
-
prefix: remote.prefix
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
async registerRemote(value) {
|
|
785
|
-
const options = value.options;
|
|
786
|
-
const url = typeof options.url === "string" ? options.url : options.url();
|
|
787
|
-
const linkPath = LinkProvider.path.apiLinks;
|
|
788
|
-
const name = value.name;
|
|
789
|
-
const proxy = typeof options.proxy === "object" ? options.proxy : {};
|
|
790
|
-
const remote = {
|
|
791
|
-
url,
|
|
792
|
-
name,
|
|
793
|
-
prefix: "/api",
|
|
794
|
-
serviceAccount: options.serviceAccount,
|
|
795
|
-
proxy: !!options.proxy,
|
|
796
|
-
internal: !proxy.noInternal,
|
|
797
|
-
schema: async (opts) => {
|
|
798
|
-
const { authorization, name: name$1 } = opts;
|
|
799
|
-
return await fetch(`${url}${linkPath}/${name$1}/schema`, { headers: new Headers(authorization ? { authorization } : {}) }).then((it) => it.json());
|
|
800
|
-
},
|
|
801
|
-
links: async (opts) => {
|
|
802
|
-
const { authorization } = opts;
|
|
803
|
-
const remoteApi = await this.fetchLinks.run({
|
|
804
|
-
service: name,
|
|
805
|
-
url: `${url}${linkPath}`,
|
|
806
|
-
authorization
|
|
807
|
-
});
|
|
808
|
-
if (remoteApi.prefix != null) remote.prefix = remoteApi.prefix;
|
|
809
|
-
return remoteApi;
|
|
810
|
-
}
|
|
811
|
-
};
|
|
812
|
-
this.remotes.push(remote);
|
|
813
|
-
if (options.proxy) this.proxyProvider.createProxy({
|
|
814
|
-
path: `${this.env.SERVER_API_PREFIX}/${name}/*`,
|
|
815
|
-
target: url,
|
|
816
|
-
rewrite: (url$1) => {
|
|
817
|
-
url$1.pathname = url$1.pathname.replace(`${this.env.SERVER_API_PREFIX}/${name}`, remote.prefix);
|
|
818
|
-
},
|
|
819
|
-
...proxy
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
fetchLinks = $retry({
|
|
823
|
-
max: 10,
|
|
824
|
-
backoff: { initial: 1e3 },
|
|
825
|
-
onError: (_, attempt, { service, url }) => {
|
|
826
|
-
this.log.warn(`Failed to fetch links, retry (${attempt})...`, {
|
|
827
|
-
service,
|
|
828
|
-
url
|
|
829
|
-
});
|
|
830
|
-
},
|
|
831
|
-
handler: async (opts) => {
|
|
832
|
-
const { url, authorization } = opts;
|
|
833
|
-
const response = await fetch(url, { headers: new Headers(authorization ? { authorization } : {}) });
|
|
834
|
-
if (!response.ok) throw new Error(`Failed to fetch links from ${url}`);
|
|
835
|
-
return this.alepha.codec.decode(apiLinksResponseSchema, await response.json());
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
//#endregion
|
|
841
|
-
//#region src/server-links/providers/ServerLinksProvider.ts
|
|
842
|
-
const envSchema = t.object({ SERVER_API_PREFIX: t.text({
|
|
843
|
-
description: "Prefix for all API routes (e.g. $action).",
|
|
844
|
-
default: "/api"
|
|
845
|
-
}) });
|
|
846
|
-
var ServerLinksProvider = class {
|
|
847
|
-
env = $env(envSchema);
|
|
848
|
-
alepha = $inject(Alepha);
|
|
849
|
-
linkProvider = $inject(LinkProvider);
|
|
850
|
-
remoteProvider = $inject(RemoteDescriptorProvider);
|
|
851
|
-
serverTimingProvider = $inject(ServerTimingProvider);
|
|
852
|
-
get prefix() {
|
|
853
|
-
return this.env.SERVER_API_PREFIX;
|
|
854
|
-
}
|
|
855
|
-
onRoute = $hook({
|
|
856
|
-
on: "configure",
|
|
857
|
-
handler: () => {
|
|
858
|
-
for (const action of this.alepha.descriptors($action)) this.linkProvider.registerLink({
|
|
859
|
-
name: action.name,
|
|
860
|
-
group: action.group,
|
|
861
|
-
schema: action.options.schema,
|
|
862
|
-
requestBodyType: action.getBodyContentType(),
|
|
863
|
-
secured: action.options.secure ?? true,
|
|
864
|
-
method: action.method === "GET" ? void 0 : action.method,
|
|
865
|
-
prefix: action.prefix,
|
|
866
|
-
path: action.path,
|
|
867
|
-
handler: (config, options = {}) => action.run(config, options)
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
/**
|
|
872
|
-
* First API - Get all API links for the user.
|
|
873
|
-
*
|
|
874
|
-
* This is based on the user's permissions.
|
|
875
|
-
*/
|
|
876
|
-
links = $route({
|
|
877
|
-
path: LinkProvider.path.apiLinks,
|
|
878
|
-
schema: { response: apiLinksResponseSchema },
|
|
879
|
-
handler: ({ user, headers }) => {
|
|
880
|
-
return this.getUserApiLinks({
|
|
881
|
-
user,
|
|
882
|
-
authorization: headers.authorization
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
|
-
/**
|
|
887
|
-
* Second API - Get schema for a specific API link.
|
|
888
|
-
*
|
|
889
|
-
* Note: Body/Response schema are not included in `links` API because it's TOO BIG.
|
|
890
|
-
* I mean for 150+ links, you got 50ms of serialization time.
|
|
891
|
-
*/
|
|
892
|
-
schema = $route({
|
|
893
|
-
path: LinkProvider.path.apiSchema,
|
|
894
|
-
schema: {
|
|
895
|
-
params: t.object({ name: t.text() }),
|
|
896
|
-
response: t.json()
|
|
897
|
-
},
|
|
898
|
-
handler: ({ params, user, headers }) => {
|
|
899
|
-
return this.getSchemaByName(params.name, {
|
|
900
|
-
user,
|
|
901
|
-
authorization: headers.authorization
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
});
|
|
905
|
-
async getSchemaByName(name, options = {}) {
|
|
906
|
-
const authorization = options.authorization;
|
|
907
|
-
const api = await this.getUserApiLinks({
|
|
908
|
-
user: options.user,
|
|
909
|
-
authorization
|
|
910
|
-
});
|
|
911
|
-
for (const link of api.links) if (link.name === name) {
|
|
912
|
-
if (link.service) return this.remoteProvider.getRemotes().find((it) => it.name === link.service)?.schema({
|
|
913
|
-
name,
|
|
914
|
-
authorization
|
|
915
|
-
});
|
|
916
|
-
return this.linkProvider.getServerLinks().find((it) => it.name === name)?.schema ?? {};
|
|
917
|
-
}
|
|
918
|
-
return {};
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Retrieves API links for the user based on their permissions.
|
|
922
|
-
* Will check on local links and remote links.
|
|
923
|
-
*/
|
|
924
|
-
async getUserApiLinks(options) {
|
|
925
|
-
const { user } = options;
|
|
926
|
-
let permissions;
|
|
927
|
-
let permissionMap;
|
|
928
|
-
const hasSecurity = this.alepha.has(SecurityProvider);
|
|
929
|
-
if (hasSecurity && user) {
|
|
930
|
-
permissions = this.alepha.inject(SecurityProvider).getPermissions(user);
|
|
931
|
-
permissionMap = new Map(permissions.map((it) => [`${it.group}:${it.name}`, it]));
|
|
932
|
-
}
|
|
933
|
-
const userLinks = [];
|
|
934
|
-
for (const permission of permissions ?? []) if (!permission.path && !permission.method && permission.name && permission.group) userLinks.push({
|
|
935
|
-
path: "",
|
|
936
|
-
name: permission.name,
|
|
937
|
-
group: permission.group
|
|
938
|
-
});
|
|
939
|
-
for (const link of this.linkProvider.getServerLinks()) {
|
|
940
|
-
if (link.host) continue;
|
|
941
|
-
if (hasSecurity && link.secured) {
|
|
942
|
-
if (!user) continue;
|
|
943
|
-
if (typeof link.secured === "object" && link.secured.realm) {
|
|
944
|
-
if (user.realm !== link.secured.realm) continue;
|
|
945
|
-
} else if (permissionMap) {
|
|
946
|
-
if (!permissionMap.has(`${link.group}:${link.name}`)) continue;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
userLinks.push({
|
|
950
|
-
name: link.name,
|
|
951
|
-
group: link.group,
|
|
952
|
-
requestBodyType: link.requestBodyType,
|
|
953
|
-
method: link.method,
|
|
954
|
-
path: link.path
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
this.serverTimingProvider.beginTiming("fetchRemoteLinks");
|
|
958
|
-
const promises = this.remoteProvider.getRemotes().filter((it) => it.proxy).map(async (remote) => {
|
|
959
|
-
const { links, prefix } = await remote.links(options);
|
|
960
|
-
return links.map((link) => {
|
|
961
|
-
let path = link.path.replace(prefix ?? "/api", "");
|
|
962
|
-
if (link.service) path = `/${link.service}${path}`;
|
|
963
|
-
return {
|
|
964
|
-
...link,
|
|
965
|
-
path,
|
|
966
|
-
proxy: true,
|
|
967
|
-
service: remote.name
|
|
968
|
-
};
|
|
969
|
-
});
|
|
970
|
-
});
|
|
971
|
-
userLinks.push(...(await Promise.all(promises)).flat());
|
|
972
|
-
this.serverTimingProvider.endTiming("fetchRemoteLinks");
|
|
973
|
-
return {
|
|
974
|
-
prefix: this.env.SERVER_API_PREFIX,
|
|
975
|
-
links: userLinks
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
};
|
|
979
|
-
|
|
980
|
-
//#endregion
|
|
981
|
-
//#region src/server-links/index.ts
|
|
982
|
-
/**
|
|
983
|
-
* Provides server-side link management and remote capabilities for client-server interactions.
|
|
984
|
-
*
|
|
985
|
-
* The server-links module enables declarative link definitions using `$remote` and `$client` descriptors,
|
|
986
|
-
* facilitating seamless API endpoint management and client-server communication. It integrates with server
|
|
987
|
-
* security features to ensure safe and controlled access to resources.
|
|
988
|
-
*
|
|
989
|
-
* @see {@link $remote}
|
|
990
|
-
* @see {@link $client}
|
|
991
|
-
* @module alepha.server.links
|
|
992
|
-
*/
|
|
993
|
-
const AlephaServerLinks = $module({
|
|
994
|
-
name: "alepha.server.links",
|
|
995
|
-
descriptors: [$remote, $client],
|
|
996
|
-
services: [
|
|
997
|
-
AlephaServer,
|
|
998
|
-
ServerLinksProvider,
|
|
999
|
-
RemoteDescriptorProvider,
|
|
1000
|
-
LinkProvider
|
|
1001
|
-
]
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
26
|
//#endregion
|
|
1005
27
|
//#region src/server-auth/schemas/tokensSchema.ts
|
|
1006
28
|
const tokensSchema = t.object({
|
|
@@ -1033,7 +55,7 @@ const userinfoResponseSchema = t.object({
|
|
|
1033
55
|
//#region src/server-auth/index.browser.ts
|
|
1034
56
|
const AlephaServerAuth = $module({
|
|
1035
57
|
name: "alepha.server.auth",
|
|
1036
|
-
|
|
58
|
+
primitives: [],
|
|
1037
59
|
services: []
|
|
1038
60
|
});
|
|
1039
61
|
|