alepha 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-jobs/index.d.ts +26 -26
- package/dist/api-users/index.d.ts +1 -1
- package/dist/cli/{dist-Sz2EXvQX.cjs → dist-Dl9Vl7Ur.js} +17 -13
- package/dist/cli/{dist-BBPjuQ56.js.map → dist-Dl9Vl7Ur.js.map} +1 -1
- package/dist/cli/index.d.ts +3 -11
- package/dist/cli/index.js +106 -74
- package/dist/cli/index.js.map +1 -1
- package/dist/email/index.js +71 -73
- package/dist/email/index.js.map +1 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/index.d.ts +4 -4
- package/dist/retry/index.d.ts +1 -1
- package/dist/retry/index.js +2 -2
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/security/index.d.ts +28 -28
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server-health/index.d.ts +17 -17
- package/dist/server-metrics/index.js +170 -174
- package/dist/server-metrics/index.js.map +1 -1
- package/dist/server-security/index.d.ts +9 -9
- package/dist/vite/index.js +4 -5
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +7 -7
- package/package.json +52 -103
- package/src/cli/apps/AlephaPackageBuilderCli.ts +7 -2
- package/src/cli/assets/appRouterTs.ts +9 -0
- package/src/cli/assets/indexHtml.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +10 -0
- package/src/cli/commands/CoreCommands.ts +6 -5
- package/src/cli/commands/DrizzleCommands.ts +65 -57
- package/src/cli/commands/VerifyCommands.ts +1 -1
- package/src/cli/services/ProjectUtils.ts +44 -38
- package/src/orm/providers/DrizzleKitProvider.ts +1 -1
- package/src/retry/descriptors/$retry.ts +5 -3
- package/src/server/providers/NodeHttpServerProvider.ts +1 -1
- package/src/vite/helpers/boot.ts +3 -3
- package/dist/api-files/index.cjs +0 -1293
- package/dist/api-files/index.cjs.map +0 -1
- package/dist/api-files/index.d.cts +0 -829
- package/dist/api-jobs/index.cjs +0 -274
- package/dist/api-jobs/index.cjs.map +0 -1
- package/dist/api-jobs/index.d.cts +0 -654
- package/dist/api-notifications/index.cjs +0 -380
- package/dist/api-notifications/index.cjs.map +0 -1
- package/dist/api-notifications/index.d.cts +0 -289
- package/dist/api-parameters/index.cjs +0 -66
- package/dist/api-parameters/index.cjs.map +0 -1
- package/dist/api-parameters/index.d.cts +0 -84
- package/dist/api-users/index.cjs +0 -6009
- package/dist/api-users/index.cjs.map +0 -1
- package/dist/api-users/index.d.cts +0 -4740
- package/dist/api-verifications/index.cjs +0 -407
- package/dist/api-verifications/index.cjs.map +0 -1
- package/dist/api-verifications/index.d.cts +0 -207
- package/dist/batch/index.cjs +0 -408
- package/dist/batch/index.cjs.map +0 -1
- package/dist/batch/index.d.cts +0 -330
- package/dist/bin/index.cjs +0 -17
- package/dist/bin/index.cjs.map +0 -1
- package/dist/bin/index.d.cts +0 -1
- package/dist/bucket/index.cjs +0 -303
- package/dist/bucket/index.cjs.map +0 -1
- package/dist/bucket/index.d.cts +0 -355
- package/dist/cache/index.cjs +0 -241
- package/dist/cache/index.cjs.map +0 -1
- package/dist/cache/index.d.cts +0 -202
- package/dist/cache-redis/index.cjs +0 -84
- package/dist/cache-redis/index.cjs.map +0 -1
- package/dist/cache-redis/index.d.cts +0 -40
- package/dist/cli/chunk-DSlc6foC.cjs +0 -43
- package/dist/cli/dist-BBPjuQ56.js +0 -2778
- package/dist/cli/dist-Sz2EXvQX.cjs.map +0 -1
- package/dist/cli/index.cjs +0 -1241
- package/dist/cli/index.cjs.map +0 -1
- package/dist/cli/index.d.cts +0 -422
- package/dist/command/index.cjs +0 -693
- package/dist/command/index.cjs.map +0 -1
- package/dist/command/index.d.cts +0 -340
- package/dist/core/index.cjs +0 -2264
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -1927
- package/dist/datetime/index.cjs +0 -318
- package/dist/datetime/index.cjs.map +0 -1
- package/dist/datetime/index.d.cts +0 -145
- package/dist/email/index.cjs +0 -10874
- package/dist/email/index.cjs.map +0 -1
- package/dist/email/index.d.cts +0 -186
- package/dist/fake/index.cjs +0 -34641
- package/dist/fake/index.cjs.map +0 -1
- package/dist/fake/index.d.cts +0 -74
- package/dist/file/index.cjs +0 -1212
- package/dist/file/index.cjs.map +0 -1
- package/dist/file/index.d.cts +0 -698
- package/dist/lock/index.cjs +0 -226
- package/dist/lock/index.cjs.map +0 -1
- package/dist/lock/index.d.cts +0 -361
- package/dist/lock-redis/index.cjs +0 -113
- package/dist/lock-redis/index.cjs.map +0 -1
- package/dist/lock-redis/index.d.cts +0 -24
- package/dist/logger/index.cjs +0 -521
- package/dist/logger/index.cjs.map +0 -1
- package/dist/logger/index.d.cts +0 -281
- package/dist/orm/index.cjs +0 -2986
- package/dist/orm/index.cjs.map +0 -1
- package/dist/orm/index.d.cts +0 -2213
- package/dist/queue/index.cjs +0 -1044
- package/dist/queue/index.cjs.map +0 -1
- package/dist/queue/index.d.cts +0 -1265
- package/dist/queue-redis/index.cjs +0 -873
- package/dist/queue-redis/index.cjs.map +0 -1
- package/dist/queue-redis/index.d.cts +0 -82
- package/dist/redis/index.cjs +0 -153
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -82
- package/dist/retry/index.cjs +0 -146
- package/dist/retry/index.cjs.map +0 -1
- package/dist/retry/index.d.cts +0 -172
- package/dist/router/index.cjs +0 -111
- package/dist/router/index.cjs.map +0 -1
- package/dist/router/index.d.cts +0 -46
- package/dist/scheduler/index.cjs +0 -576
- package/dist/scheduler/index.cjs.map +0 -1
- package/dist/scheduler/index.d.cts +0 -145
- package/dist/security/index.cjs +0 -2402
- package/dist/security/index.cjs.map +0 -1
- package/dist/security/index.d.cts +0 -598
- package/dist/server/index.cjs +0 -1680
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.d.cts +0 -810
- package/dist/server-auth/index.cjs +0 -3146
- package/dist/server-auth/index.cjs.map +0 -1
- package/dist/server-auth/index.d.cts +0 -1164
- package/dist/server-cache/index.cjs +0 -252
- package/dist/server-cache/index.cjs.map +0 -1
- package/dist/server-cache/index.d.cts +0 -164
- package/dist/server-compress/index.cjs +0 -141
- package/dist/server-compress/index.cjs.map +0 -1
- package/dist/server-compress/index.d.cts +0 -38
- package/dist/server-cookies/index.cjs +0 -234
- package/dist/server-cookies/index.cjs.map +0 -1
- package/dist/server-cookies/index.d.cts +0 -144
- package/dist/server-cors/index.cjs +0 -201
- package/dist/server-cors/index.cjs.map +0 -1
- package/dist/server-cors/index.d.cts +0 -140
- package/dist/server-health/index.cjs +0 -62
- package/dist/server-health/index.cjs.map +0 -1
- package/dist/server-health/index.d.cts +0 -58
- package/dist/server-helmet/index.cjs +0 -131
- package/dist/server-helmet/index.cjs.map +0 -1
- package/dist/server-helmet/index.d.cts +0 -97
- package/dist/server-links/index.cjs +0 -992
- package/dist/server-links/index.cjs.map +0 -1
- package/dist/server-links/index.d.cts +0 -513
- package/dist/server-metrics/index.cjs +0 -4535
- package/dist/server-metrics/index.cjs.map +0 -1
- package/dist/server-metrics/index.d.cts +0 -35
- package/dist/server-multipart/index.cjs +0 -237
- package/dist/server-multipart/index.cjs.map +0 -1
- package/dist/server-multipart/index.d.cts +0 -50
- package/dist/server-proxy/index.cjs +0 -186
- package/dist/server-proxy/index.cjs.map +0 -1
- package/dist/server-proxy/index.d.cts +0 -234
- package/dist/server-rate-limit/index.cjs +0 -241
- package/dist/server-rate-limit/index.cjs.map +0 -1
- package/dist/server-rate-limit/index.d.cts +0 -183
- package/dist/server-security/index.cjs +0 -316
- package/dist/server-security/index.cjs.map +0 -1
- package/dist/server-security/index.d.cts +0 -173
- package/dist/server-static/index.cjs +0 -170
- package/dist/server-static/index.cjs.map +0 -1
- package/dist/server-static/index.d.cts +0 -121
- package/dist/server-swagger/index.cjs +0 -1021
- package/dist/server-swagger/index.cjs.map +0 -1
- package/dist/server-swagger/index.d.cts +0 -382
- package/dist/sms/index.cjs +0 -221
- package/dist/sms/index.cjs.map +0 -1
- package/dist/sms/index.d.cts +0 -130
- package/dist/thread/index.cjs +0 -350
- package/dist/thread/index.cjs.map +0 -1
- package/dist/thread/index.d.cts +0 -260
- package/dist/topic/index.cjs +0 -282
- package/dist/topic/index.cjs.map +0 -1
- package/dist/topic/index.d.cts +0 -523
- package/dist/topic-redis/index.cjs +0 -71
- package/dist/topic-redis/index.cjs.map +0 -1
- package/dist/topic-redis/index.d.cts +0 -42
- package/dist/vite/index.cjs +0 -1077
- package/dist/vite/index.cjs.map +0 -1
- package/dist/vite/index.d.cts +0 -542
- package/dist/websocket/index.cjs +0 -1117
- package/dist/websocket/index.cjs.map +0 -1
- package/dist/websocket/index.d.cts +0 -861
|
@@ -1,1021 +0,0 @@
|
|
|
1
|
-
let alepha = require("alepha");
|
|
2
|
-
let alepha_security = require("alepha/security");
|
|
3
|
-
let alepha_server = require("alepha/server");
|
|
4
|
-
let node_crypto = require("node:crypto");
|
|
5
|
-
let alepha_logger = require("alepha/logger");
|
|
6
|
-
let alepha_cache = require("alepha/cache");
|
|
7
|
-
let alepha_datetime = require("alepha/datetime");
|
|
8
|
-
let node_fs = require("node:fs");
|
|
9
|
-
let node_fs_promises = require("node:fs/promises");
|
|
10
|
-
let node_path = require("node:path");
|
|
11
|
-
let alepha_file = require("alepha/file");
|
|
12
|
-
let node_url = require("node:url");
|
|
13
|
-
|
|
14
|
-
//#region src/server-security/providers/ServerBasicAuthProvider.ts
|
|
15
|
-
var ServerBasicAuthProvider = class {
|
|
16
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
17
|
-
log = (0, alepha_logger.$logger)();
|
|
18
|
-
routerProvider = (0, alepha.$inject)(alepha_server.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 = (0, alepha.$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 = (0, alepha.$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 = (0, alepha.$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 alepha_server.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 alepha_server.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
|
-
(0, node_crypto.timingSafeEqual)(input, input);
|
|
107
|
-
return 0;
|
|
108
|
-
}
|
|
109
|
-
return (0, node_crypto.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 (0, alepha.createDescriptor)(BasicAuthDescriptor, options);
|
|
130
|
-
};
|
|
131
|
-
var BasicAuthDescriptor = class extends alepha.Descriptor {
|
|
132
|
-
serverBasicAuthProvider = (0, alepha.$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[alepha.KIND] = BasicAuthDescriptor;
|
|
151
|
-
|
|
152
|
-
//#endregion
|
|
153
|
-
//#region src/server-security/providers/ServerSecurityProvider.ts
|
|
154
|
-
var ServerSecurityProvider = class {
|
|
155
|
-
log = (0, alepha_logger.$logger)();
|
|
156
|
-
securityProvider = (0, alepha.$inject)(alepha_security.SecurityProvider);
|
|
157
|
-
jwtProvider = (0, alepha.$inject)(alepha_security.JwtProvider);
|
|
158
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
159
|
-
onConfigure = (0, alepha.$hook)({
|
|
160
|
-
on: "configure",
|
|
161
|
-
handler: async () => {
|
|
162
|
-
for (const action of this.alepha.descriptors(alepha_server.$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 = (0, alepha.$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(alepha_security.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 = (0, alepha.$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(alepha_security.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 alepha_server.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 alepha_server.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 alepha_server.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: (0, node_crypto.randomUUID)(),
|
|
265
|
-
name: "Test",
|
|
266
|
-
roles: this.securityProvider.getRoles().map((role) => role.name)
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
onClientRequest = (0, alepha.$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 = (0, alepha.$module)({
|
|
301
|
-
name: "alepha.server.security",
|
|
302
|
-
descriptors: [
|
|
303
|
-
alepha_security.$realm,
|
|
304
|
-
alepha_security.$role,
|
|
305
|
-
alepha_security.$permission,
|
|
306
|
-
$basicAuth
|
|
307
|
-
],
|
|
308
|
-
services: [
|
|
309
|
-
alepha_server.AlephaServer,
|
|
310
|
-
alepha_security.AlephaSecurity,
|
|
311
|
-
ServerSecurityProvider,
|
|
312
|
-
ServerBasicAuthProvider
|
|
313
|
-
]
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
//#endregion
|
|
317
|
-
//#region src/server-cache/providers/ServerCacheProvider.ts
|
|
318
|
-
alepha_server.ActionDescriptor.prototype.invalidate = async function() {
|
|
319
|
-
await this.alepha.inject(ServerCacheProvider).invalidate(this.route);
|
|
320
|
-
};
|
|
321
|
-
var ServerCacheProvider = class {
|
|
322
|
-
log = (0, alepha_logger.$logger)();
|
|
323
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
324
|
-
time = (0, alepha.$inject)(alepha_datetime.DateTimeProvider);
|
|
325
|
-
cache = (0, alepha_cache.$cache)({ provider: "memory" });
|
|
326
|
-
generateETag(content) {
|
|
327
|
-
return `"${(0, node_crypto.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 = (0, alepha.$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 = (0, alepha.$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 = (0, alepha.$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 = (0, alepha.$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 = (0, alepha.$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 = (0, alepha.$module)({
|
|
554
|
-
name: "alepha.server.cache",
|
|
555
|
-
services: [alepha_cache.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 (0, alepha.createDescriptor)(ServeDescriptor, options);
|
|
565
|
-
};
|
|
566
|
-
var ServeDescriptor = class extends alepha.Descriptor {};
|
|
567
|
-
$serve[alepha.KIND] = ServeDescriptor;
|
|
568
|
-
|
|
569
|
-
//#endregion
|
|
570
|
-
//#region src/server-static/providers/ServerStaticProvider.ts
|
|
571
|
-
var ServerStaticProvider = class {
|
|
572
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
573
|
-
routerProvider = (0, alepha.$inject)(alepha_server.ServerRouterProvider);
|
|
574
|
-
dateTimeProvider = (0, alepha.$inject)(alepha_datetime.DateTimeProvider);
|
|
575
|
-
fileDetector = (0, alepha.$inject)(alepha_file.FileDetector);
|
|
576
|
-
log = (0, alepha_logger.$logger)();
|
|
577
|
-
directories = [];
|
|
578
|
-
configure = (0, alepha.$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 (!(0, node_path.isAbsolute)(root)) root = (0, node_path.join)(process.cwd(), root);
|
|
588
|
-
this.log.debug("Serve static files", {
|
|
589
|
-
prefix,
|
|
590
|
-
root
|
|
591
|
-
});
|
|
592
|
-
await (0, node_fs_promises.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 ${(0, node_path.join)(prefix, path)} -> ${(0, node_path.join)(root, path)}`);
|
|
597
|
-
return {
|
|
598
|
-
path: (0, node_path.join)(prefix, encodeURI(path)),
|
|
599
|
-
handler: await this.createFileHandler((0, node_path.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: (0, node_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 (0, node_fs.createReadStream)((0, node_path.join)(root, "index.html"));
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
async createFileHandler(filepath, options) {
|
|
630
|
-
const filename = (0, node_path.basename)(filepath);
|
|
631
|
-
const hasGzip = await (0, node_fs_promises.access)(`${filepath}.gz`).then(() => true).catch(() => false);
|
|
632
|
-
const hasBr = await (0, node_fs_promises.access)(`${filepath}.br`).then(() => true).catch(() => false);
|
|
633
|
-
const fileStat = await (0, node_fs_promises.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 (0, node_fs.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 (0, node_fs_promises.readdir)(dir, { withFileTypes: true });
|
|
692
|
-
return (await Promise.all(entries.map((dirent) => {
|
|
693
|
-
if (ignoreDotEnvFiles && dirent.name.startsWith(".")) return [];
|
|
694
|
-
const fullPath = (0, node_path.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 = (0, alepha.$module)({
|
|
709
|
-
name: "alepha.server.static",
|
|
710
|
-
descriptors: [$serve],
|
|
711
|
-
services: [alepha_server.AlephaServer, ServerStaticProvider]
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
//#endregion
|
|
715
|
-
//#region src/server-swagger/descriptors/$swagger.ts
|
|
716
|
-
/**
|
|
717
|
-
* Creates an OpenAPI/Swagger documentation descriptor with interactive UI.
|
|
718
|
-
*
|
|
719
|
-
* Automatically generates API documentation from your $action descriptors and serves
|
|
720
|
-
* an interactive Swagger UI for testing endpoints. Supports customization, tag filtering,
|
|
721
|
-
* and OAuth configuration.
|
|
722
|
-
*
|
|
723
|
-
* @example
|
|
724
|
-
* ```ts
|
|
725
|
-
* class App {
|
|
726
|
-
* docs = $swagger({
|
|
727
|
-
* prefix: "/api-docs",
|
|
728
|
-
* info: {
|
|
729
|
-
* title: "My API",
|
|
730
|
-
* version: "1.0.0",
|
|
731
|
-
* description: "REST API documentation"
|
|
732
|
-
* },
|
|
733
|
-
* excludeTags: ["internal"],
|
|
734
|
-
* ui: { root: "/swagger" }
|
|
735
|
-
* });
|
|
736
|
-
* }
|
|
737
|
-
* ```
|
|
738
|
-
*/
|
|
739
|
-
const $swagger = (options = {}) => {
|
|
740
|
-
return (0, alepha.createDescriptor)(SwaggerDescriptor, options);
|
|
741
|
-
};
|
|
742
|
-
var SwaggerDescriptor = class extends alepha.Descriptor {};
|
|
743
|
-
$swagger[alepha.KIND] = SwaggerDescriptor;
|
|
744
|
-
|
|
745
|
-
//#endregion
|
|
746
|
-
//#region src/server-swagger/providers/ServerSwaggerProvider.ts
|
|
747
|
-
/**
|
|
748
|
-
* Swagger provider configuration atom
|
|
749
|
-
*/
|
|
750
|
-
const swaggerOptions = (0, alepha.$atom)({
|
|
751
|
-
name: "alepha.server.swagger.options",
|
|
752
|
-
schema: alepha.t.object({ excludeKeys: alepha.t.optional(alepha.t.array(alepha.t.string(), { description: "Keys to exclude from swagger schema" })) }),
|
|
753
|
-
default: { excludeKeys: [] }
|
|
754
|
-
});
|
|
755
|
-
var ServerSwaggerProvider = class {
|
|
756
|
-
serverStaticProvider = (0, alepha.$inject)(ServerStaticProvider);
|
|
757
|
-
serverRouterProvider = (0, alepha.$inject)(alepha_server.ServerRouterProvider);
|
|
758
|
-
serverProvider = (0, alepha.$inject)(alepha_server.ServerProvider);
|
|
759
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
760
|
-
log = (0, alepha_logger.$logger)();
|
|
761
|
-
options = (0, alepha.$use)(swaggerOptions);
|
|
762
|
-
fs = (0, alepha.$inject)(alepha_file.FileSystemProvider);
|
|
763
|
-
json;
|
|
764
|
-
configure = (0, alepha.$hook)({
|
|
765
|
-
on: "configure",
|
|
766
|
-
priority: "last",
|
|
767
|
-
handler: async (alepha$1) => {
|
|
768
|
-
const options = alepha$1.descriptors($swagger)?.[0]?.options;
|
|
769
|
-
if (!options) return;
|
|
770
|
-
this.json = await this.createSwagger(options);
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
async createSwagger(options) {
|
|
774
|
-
if (options.disabled) return;
|
|
775
|
-
const json = this.configureOpenApi(this.alepha.descriptors(alepha_server.$action), options);
|
|
776
|
-
if (options.rewrite) options.rewrite(json);
|
|
777
|
-
const prefix = options.prefix ?? "/docs";
|
|
778
|
-
this.configureSwaggerApi(prefix, json);
|
|
779
|
-
if (options.ui !== false) await this.configureSwaggerUi(prefix, options);
|
|
780
|
-
return json;
|
|
781
|
-
}
|
|
782
|
-
configureOpenApi(actions, doc) {
|
|
783
|
-
const openApi = {
|
|
784
|
-
openapi: "3.0.0",
|
|
785
|
-
info: doc.info ?? {
|
|
786
|
-
title: "API Documentation",
|
|
787
|
-
version: "1.0.0"
|
|
788
|
-
},
|
|
789
|
-
paths: {},
|
|
790
|
-
components: {}
|
|
791
|
-
};
|
|
792
|
-
const hasSecurity = this.alepha.has(alepha_security.AlephaSecurity);
|
|
793
|
-
if (hasSecurity && openApi.components) openApi.components.securitySchemes = { bearerAuth: {
|
|
794
|
-
type: "http",
|
|
795
|
-
scheme: "bearer",
|
|
796
|
-
bearerFormat: "JWT"
|
|
797
|
-
} };
|
|
798
|
-
const excludeTags = doc.excludeTags ?? [];
|
|
799
|
-
const schemas = {};
|
|
800
|
-
const schema = (source) => {
|
|
801
|
-
if ("title" in source && typeof source.title === "string") {
|
|
802
|
-
schemas[source.title] = copy(source);
|
|
803
|
-
return { $ref: `#/components/schemas/${source.title}` };
|
|
804
|
-
}
|
|
805
|
-
return copy(source);
|
|
806
|
-
};
|
|
807
|
-
const copy = (obj) => {
|
|
808
|
-
const newValue = JSON.parse(JSON.stringify(obj));
|
|
809
|
-
this.removePrivateFields(newValue, [...this.options.excludeKeys || [], "~options"]);
|
|
810
|
-
return newValue;
|
|
811
|
-
};
|
|
812
|
-
for (const route of actions) {
|
|
813
|
-
if (!route.options.schema) continue;
|
|
814
|
-
const response = this.getResponseSchema(route);
|
|
815
|
-
if (!response) continue;
|
|
816
|
-
if (excludeTags.includes(route.group)) continue;
|
|
817
|
-
if (route.options.hide) continue;
|
|
818
|
-
const operation = {
|
|
819
|
-
operationId: route.name,
|
|
820
|
-
summary: route.options.summary,
|
|
821
|
-
description: route.options.description,
|
|
822
|
-
tags: [route.group.replaceAll(":", " / ")],
|
|
823
|
-
responses: { [response.status]: {
|
|
824
|
-
description: "",
|
|
825
|
-
content: response.type ? { [response.type]: { schema: schema(response.schema) } } : void 0
|
|
826
|
-
} }
|
|
827
|
-
};
|
|
828
|
-
if (route.options.secure !== false && hasSecurity) operation.security = [{ bearerAuth: [] }];
|
|
829
|
-
const g = alepha.t.raw;
|
|
830
|
-
if (g.IsObject(route.options.schema.body) || g.IsArray(route.options.schema.body)) if (g.IsObject(route.options.schema.body) && this.isBodyMultipart(route.options.schema.body)) operation.requestBody = {
|
|
831
|
-
required: true,
|
|
832
|
-
content: { "multipart/form-data": { schema: schema(route.options.schema.body) } }
|
|
833
|
-
};
|
|
834
|
-
else operation.requestBody = {
|
|
835
|
-
required: true,
|
|
836
|
-
content: { "application/json": { schema: schema(route.options.schema.body) } }
|
|
837
|
-
};
|
|
838
|
-
if (g.IsObject(route.options.schema.query)) {
|
|
839
|
-
operation.parameters ??= [];
|
|
840
|
-
for (const [key, value] of Object.entries(route.options.schema.query.properties)) operation.parameters.push({
|
|
841
|
-
name: key,
|
|
842
|
-
in: "query",
|
|
843
|
-
required: false,
|
|
844
|
-
schema: schema(value)
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
if (g.IsObject(route.options.schema.params)) {
|
|
848
|
-
operation.parameters ??= [];
|
|
849
|
-
for (const [key, value] of Object.entries(route.options.schema.params.properties)) {
|
|
850
|
-
const description = "description" in value && typeof value.description === "string" ? value.description : void 0;
|
|
851
|
-
const ref = schema(value);
|
|
852
|
-
delete ref.description;
|
|
853
|
-
operation.parameters.push({
|
|
854
|
-
name: key,
|
|
855
|
-
in: "path",
|
|
856
|
-
required: true,
|
|
857
|
-
description,
|
|
858
|
-
schema: ref
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
const url = route.prefix + this.replacePathParams(route.path);
|
|
863
|
-
openApi.paths[url] = {
|
|
864
|
-
...openApi.paths[url],
|
|
865
|
-
[route.method.toLowerCase()]: operation
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
if (openApi.components) openApi.components.schemas = schemas;
|
|
869
|
-
return JSON.parse(JSON.stringify(openApi));
|
|
870
|
-
}
|
|
871
|
-
isBodyMultipart(schema) {
|
|
872
|
-
for (const key in schema.properties) if ((0, alepha.isTypeFile)(schema.properties[key])) return true;
|
|
873
|
-
return false;
|
|
874
|
-
}
|
|
875
|
-
replacePathParams(url) {
|
|
876
|
-
return url.replace(/:\w+/g, (match) => {
|
|
877
|
-
return `{${match.slice(1)}}`;
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
getResponseSchema(route) {
|
|
881
|
-
const schema = route.options.schema?.response;
|
|
882
|
-
if (!schema) return { status: 204 };
|
|
883
|
-
if (alepha.t.schema.isObject(schema) || alepha.t.schema.isArray(schema)) return {
|
|
884
|
-
schema,
|
|
885
|
-
status: 200,
|
|
886
|
-
type: "application/json"
|
|
887
|
-
};
|
|
888
|
-
if (alepha.t.schema.isString(schema)) return {
|
|
889
|
-
schema,
|
|
890
|
-
status: 200,
|
|
891
|
-
type: "text/plain"
|
|
892
|
-
};
|
|
893
|
-
if ((0, alepha.isTypeFile)(schema)) return {
|
|
894
|
-
schema,
|
|
895
|
-
status: 200,
|
|
896
|
-
type: "application/octet-stream"
|
|
897
|
-
};
|
|
898
|
-
const status = Object.keys(schema)[0];
|
|
899
|
-
if (alepha.t.schema.isObject(schema[status]) || (0, alepha.isTypeFile)(schema[status])) return {
|
|
900
|
-
schema,
|
|
901
|
-
type: alepha.t.schema.isObject(schema[status]) ? "application/json" : "application/octet-stream",
|
|
902
|
-
status: Number(status)
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
configureSwaggerApi(prefix, json) {
|
|
906
|
-
this.serverRouterProvider.createRoute({
|
|
907
|
-
method: "GET",
|
|
908
|
-
path: `${prefix}/json`,
|
|
909
|
-
cache: { etag: true },
|
|
910
|
-
schema: { response: alepha.t.json() },
|
|
911
|
-
handler: () => json
|
|
912
|
-
});
|
|
913
|
-
this.log.info(`Swagger API available at ${prefix}/json`);
|
|
914
|
-
}
|
|
915
|
-
async configureSwaggerUi(prefix, options) {
|
|
916
|
-
const ui = typeof options.ui === "object" ? options.ui : {};
|
|
917
|
-
const initializer = `
|
|
918
|
-
window.onload = function() {
|
|
919
|
-
window.ui = SwaggerUIBundle({
|
|
920
|
-
url: "/docs/json",
|
|
921
|
-
dom_id: '#swagger-ui',
|
|
922
|
-
deepLinking: true,
|
|
923
|
-
presets: [
|
|
924
|
-
SwaggerUIBundle.presets.apis,
|
|
925
|
-
SwaggerUIStandalonePreset
|
|
926
|
-
],
|
|
927
|
-
plugins: [
|
|
928
|
-
SwaggerUIBundle.plugins.DownloadUrl
|
|
929
|
-
],
|
|
930
|
-
layout: "BaseLayout"
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
document.body.style.backgroundColor = "#f2f2f2";
|
|
934
|
-
|
|
935
|
-
const options = ${JSON.stringify(ui)};
|
|
936
|
-
|
|
937
|
-
if (options.initOAuth) {
|
|
938
|
-
ui.initOAuth(options.initOAuth);
|
|
939
|
-
}
|
|
940
|
-
};
|
|
941
|
-
`.trim();
|
|
942
|
-
const dirname = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
943
|
-
const root = await this.getAssetPath(ui.root, (0, node_path.join)(dirname, "../../assets/swagger-ui"), (0, node_path.join)(dirname, "../../../../assets/swagger-ui"));
|
|
944
|
-
if (!root) {
|
|
945
|
-
this.log.warn(`Failed to locate Swagger UI assets for path ${prefix}`);
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
await this.serverStaticProvider.createStaticServer({
|
|
949
|
-
path: prefix,
|
|
950
|
-
root
|
|
951
|
-
});
|
|
952
|
-
this.serverRouterProvider.createRoute({
|
|
953
|
-
method: "GET",
|
|
954
|
-
path: `${prefix}/swagger-initializer.js`,
|
|
955
|
-
cache: { etag: true },
|
|
956
|
-
handler: ({ reply }) => {
|
|
957
|
-
reply.headers["content-type"] = "application/javascript; charset=utf-8";
|
|
958
|
-
return initializer;
|
|
959
|
-
}
|
|
960
|
-
});
|
|
961
|
-
this.log.info(`Swagger UI available at ${this.serverProvider.hostname}${prefix}/`);
|
|
962
|
-
}
|
|
963
|
-
async getAssetPath(...paths) {
|
|
964
|
-
for (const path of paths) {
|
|
965
|
-
if (!path) continue;
|
|
966
|
-
if (await this.fs.exists(path)) return path;
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
removePrivateFields(obj, excludeList) {
|
|
970
|
-
if (obj === null || typeof obj !== "object") return obj;
|
|
971
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
972
|
-
const traverse = (o) => {
|
|
973
|
-
if (visited.has(o)) return;
|
|
974
|
-
visited.add(o);
|
|
975
|
-
if (Array.isArray(o)) for (let i = 0; i < o.length; i++) {
|
|
976
|
-
const item = o[i];
|
|
977
|
-
if (item !== null && typeof item === "object") traverse(item);
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
for (const excludeKey of excludeList) if (excludeKey in o) delete o[excludeKey];
|
|
981
|
-
for (const key in o) {
|
|
982
|
-
const item = o[key];
|
|
983
|
-
if (item !== null && typeof item === "object") traverse(item);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
};
|
|
987
|
-
traverse(obj);
|
|
988
|
-
return obj;
|
|
989
|
-
}
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
//#endregion
|
|
993
|
-
//#region src/server-swagger/index.ts
|
|
994
|
-
/**
|
|
995
|
-
* Plugin for Alepha Server that provides Swagger documentation capabilities.
|
|
996
|
-
* It generates OpenAPI v3 documentation for the server's endpoints ($action).
|
|
997
|
-
* It also provides a Swagger UI for interactive API documentation.
|
|
998
|
-
*
|
|
999
|
-
* @see {@link ServerSwaggerProvider}
|
|
1000
|
-
* @module alepha.server.swagger
|
|
1001
|
-
*/
|
|
1002
|
-
const AlephaServerSwagger = (0, alepha.$module)({
|
|
1003
|
-
name: "alepha.server.swagger",
|
|
1004
|
-
descriptors: [$swagger],
|
|
1005
|
-
services: [ServerSwaggerProvider],
|
|
1006
|
-
register: (alepha$1) => {
|
|
1007
|
-
alepha$1.with(alepha_server.AlephaServer);
|
|
1008
|
-
alepha$1.with(AlephaServerCache);
|
|
1009
|
-
alepha$1.with(AlephaServerStatic);
|
|
1010
|
-
alepha$1.with(ServerSwaggerProvider);
|
|
1011
|
-
alepha$1.state.push("alepha.build.assets", "alepha");
|
|
1012
|
-
}
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
//#endregion
|
|
1016
|
-
exports.$swagger = $swagger;
|
|
1017
|
-
exports.AlephaServerSwagger = AlephaServerSwagger;
|
|
1018
|
-
exports.ServerSwaggerProvider = ServerSwaggerProvider;
|
|
1019
|
-
exports.SwaggerDescriptor = SwaggerDescriptor;
|
|
1020
|
-
exports.swaggerOptions = swaggerOptions;
|
|
1021
|
-
//# sourceMappingURL=index.cjs.map
|