@getstrata/bootstrap 2.0.9 → 2.2.0
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/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/dist/bootstrap/app.d.ts +7 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/context.d.ts +8 -2
- package/dist/bootstrap/contracts.d.ts +1 -1
- package/dist/bootstrap/dependencies.d.ts +1 -1
- package/dist/bootstrap/discoverJobs.d.ts +11 -3
- package/dist/bootstrap/discoverListeners.d.ts +7 -3
- package/dist/bootstrap/discoverModels.d.ts +8 -0
- package/dist/bootstrap/discoverModules.d.ts +4 -1
- package/dist/bootstrap/httpKernel.d.ts +25 -20
- package/dist/bootstrap/inProcessCron.d.ts +2 -1
- package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
- package/dist/bootstrap/modules.d.ts +1 -1
- package/dist/bootstrap/providers/tracing.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +9 -4
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/bootstrap/web/server.d.ts +15 -7
- package/dist/entries/buildModuleRoutes.js +212 -67
- package/dist/entries/buildWebModuleRoutes.js +212 -67
- package/dist/entries/cache/modelCacheTags.js +157 -51
- package/dist/entries/config.js +6 -0
- package/dist/entries/context.js +427 -188
- package/dist/entries/contracts.js +2 -0
- package/dist/entries/createWebRoutes.js +214 -69
- package/dist/entries/dependencies.js +429 -190
- package/dist/entries/discoverJobs.js +132 -50
- package/dist/entries/discoverListeners.js +85 -37
- package/dist/entries/discoverModels.js +39 -0
- package/dist/entries/discoverModules.js +158 -51
- package/dist/entries/health.js +3 -0
- package/dist/entries/httpKernel.js +52 -13
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
- package/dist/entries/providers.js +307 -164
- package/dist/entries/web/routing.js +52 -13
- package/dist/entries/web/server.js +69 -14
- package/dist/index.js +591 -216
- package/package.json +8 -3
|
@@ -56,8 +56,11 @@ import {
|
|
|
56
56
|
CORE_CACHE_TOKEN,
|
|
57
57
|
CORE_CONFIG_TOKEN,
|
|
58
58
|
CORE_EVENT_BUS_TOKEN,
|
|
59
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
59
60
|
CORE_POLICY_GATE_TOKEN,
|
|
60
61
|
CORE_QUEUE_TOKEN,
|
|
62
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
63
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
61
64
|
CORE_TOKEN_SERVICE_TOKEN
|
|
62
65
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
63
66
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -123,8 +126,39 @@ function resolveRegisterRateLimit() {
|
|
|
123
126
|
// ../../src/bootstrap/httpKernel.ts
|
|
124
127
|
class HttpKernel {
|
|
125
128
|
dependencies;
|
|
129
|
+
ownedThrottles = new Set;
|
|
130
|
+
disposed = false;
|
|
126
131
|
constructor(dependencies) {
|
|
127
132
|
this.dependencies = dependencies;
|
|
133
|
+
if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
|
|
134
|
+
dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
dispose() {
|
|
138
|
+
this.disposed = true;
|
|
139
|
+
const owned = [...this.ownedThrottles];
|
|
140
|
+
this.ownedThrottles.clear();
|
|
141
|
+
const errors = [];
|
|
142
|
+
for (const throttle of owned.reverse()) {
|
|
143
|
+
try {
|
|
144
|
+
throttle.dispose();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
errors.push(error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (errors.length)
|
|
150
|
+
throw new AggregateError(errors, "HTTP throttle cleanup failed.");
|
|
151
|
+
}
|
|
152
|
+
ownThrottle(throttle) {
|
|
153
|
+
if (this.disposed) {
|
|
154
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function")
|
|
155
|
+
throttle.dispose();
|
|
156
|
+
throw new Error("HTTP kernel is disposed.");
|
|
157
|
+
}
|
|
158
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function") {
|
|
159
|
+
this.ownedThrottles.add(throttle);
|
|
160
|
+
}
|
|
161
|
+
return throttle;
|
|
128
162
|
}
|
|
129
163
|
globalMiddleware(errorFormat = "json") {
|
|
130
164
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
@@ -139,7 +173,9 @@ class HttpKernel {
|
|
|
139
173
|
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
140
174
|
createAuthMiddleware(auth),
|
|
141
175
|
createMembershipMiddleware(),
|
|
142
|
-
createTenantMiddleware(
|
|
176
|
+
createTenantMiddleware({
|
|
177
|
+
resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
|
|
178
|
+
})
|
|
143
179
|
];
|
|
144
180
|
}
|
|
145
181
|
group(name) {
|
|
@@ -154,25 +190,28 @@ class HttpKernel {
|
|
|
154
190
|
if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
155
191
|
return [csrf];
|
|
156
192
|
}
|
|
193
|
+
const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
|
|
157
194
|
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
158
195
|
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
159
196
|
if (!redisUrl) {
|
|
160
197
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
161
198
|
return [
|
|
162
|
-
createMemoryThrottleMiddleware({
|
|
199
|
+
this.ownThrottle(createMemoryThrottleMiddleware({
|
|
163
200
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
164
|
-
decaySeconds: 60
|
|
165
|
-
|
|
201
|
+
decaySeconds: 60,
|
|
202
|
+
quotaPolicy
|
|
203
|
+
})),
|
|
166
204
|
csrf
|
|
167
205
|
];
|
|
168
206
|
}
|
|
169
207
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
170
208
|
return [
|
|
171
|
-
createThrottleMiddleware({
|
|
209
|
+
this.ownThrottle(createThrottleMiddleware({
|
|
172
210
|
redisUrl,
|
|
173
211
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
174
|
-
decaySeconds: 60
|
|
175
|
-
|
|
212
|
+
decaySeconds: 60,
|
|
213
|
+
quotaPolicy
|
|
214
|
+
})),
|
|
176
215
|
csrf
|
|
177
216
|
];
|
|
178
217
|
}
|
|
@@ -302,25 +341,25 @@ class HttpKernel {
|
|
|
302
341
|
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
303
342
|
const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
|
|
304
343
|
if (scope === "login") {
|
|
305
|
-
return withMiddleware(createLoginThrottleMiddleware({
|
|
344
|
+
return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
|
|
306
345
|
...redisUrl ? { redisUrl } : {},
|
|
307
346
|
maxAttempts: rateLimit.maxAttempts,
|
|
308
347
|
decaySeconds: rateLimit.decaySeconds
|
|
309
|
-
}))(handler);
|
|
348
|
+
})))(handler);
|
|
310
349
|
}
|
|
311
350
|
if (redisUrl) {
|
|
312
|
-
return withMiddleware(createThrottleMiddleware({
|
|
351
|
+
return withMiddleware(this.ownThrottle(createThrottleMiddleware({
|
|
313
352
|
redisUrl,
|
|
314
353
|
maxAttempts: rateLimit.maxAttempts,
|
|
315
354
|
decaySeconds: rateLimit.decaySeconds,
|
|
316
355
|
keyPrefix: memoryKeyPrefix
|
|
317
|
-
}))(handler);
|
|
356
|
+
})))(handler);
|
|
318
357
|
}
|
|
319
|
-
return withMiddleware(createMemoryThrottleMiddleware({
|
|
358
|
+
return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
|
|
320
359
|
maxAttempts: rateLimit.maxAttempts,
|
|
321
360
|
decaySeconds: rateLimit.decaySeconds,
|
|
322
361
|
keyPrefix: memoryKeyPrefix
|
|
323
|
-
}))(handler);
|
|
362
|
+
})))(handler);
|
|
324
363
|
}
|
|
325
364
|
}
|
|
326
365
|
function createHttpKernel(dependencies) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/web/server.ts
|
|
3
|
+
import { resolveMaxBodyBytes } from "@getstrata/core/http/bodySizeLimitMiddleware";
|
|
3
4
|
import { createCorsMiddleware } from "@getstrata/core/http/corsMiddleware";
|
|
4
5
|
import { currentRequestMeta, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
5
6
|
import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
|
|
@@ -11,29 +12,32 @@ function socketAddress(server, request) {
|
|
|
11
12
|
async function missingHtmlResponse() {
|
|
12
13
|
return notFoundHtmlResponse();
|
|
13
14
|
}
|
|
14
|
-
function wrapRouteHandler(handler, path) {
|
|
15
|
+
function wrapRouteHandler(handler, path, onRequest, track = (work) => work()) {
|
|
15
16
|
return async (request, server) => {
|
|
16
|
-
return await runWithRequestMeta({
|
|
17
|
+
return await track(async () => await runWithRequestMeta({
|
|
17
18
|
...currentRequestMeta(),
|
|
18
19
|
request,
|
|
19
20
|
routeTemplate: path,
|
|
20
21
|
ipAddress: socketAddress(server, request),
|
|
21
22
|
userAgent: request.headers.get("user-agent")
|
|
22
23
|
}, async () => {
|
|
24
|
+
const intercepted = await onRequest?.(request, server);
|
|
25
|
+
if (intercepted instanceof Response)
|
|
26
|
+
return intercepted;
|
|
23
27
|
const response = await handler(request);
|
|
24
28
|
return response ?? await missingHtmlResponse();
|
|
25
|
-
});
|
|
29
|
+
}));
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
|
-
function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
32
|
+
function convertAppRoutesToBunRoutes(routes, cors = {}, onRequest, track) {
|
|
29
33
|
const bunRoutes = {};
|
|
30
34
|
const middleware = createCorsMiddleware(cors);
|
|
31
35
|
const preflight = (request) => middleware(request, async () => new Response(null, { status: 405 }));
|
|
32
36
|
for (const [path, handler] of Object.entries(routes)) {
|
|
33
37
|
if (typeof handler === "function") {
|
|
34
38
|
bunRoutes[path] = {
|
|
35
|
-
GET: wrapRouteHandler(handler, path),
|
|
36
|
-
OPTIONS: wrapRouteHandler(preflight, path)
|
|
39
|
+
GET: wrapRouteHandler(handler, path, onRequest, track),
|
|
40
|
+
OPTIONS: wrapRouteHandler(preflight, path, onRequest, track)
|
|
37
41
|
};
|
|
38
42
|
continue;
|
|
39
43
|
}
|
|
@@ -43,10 +47,10 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
|
43
47
|
if (typeof methodHandler !== "function") {
|
|
44
48
|
continue;
|
|
45
49
|
}
|
|
46
|
-
methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path);
|
|
50
|
+
methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path, onRequest, track);
|
|
47
51
|
}
|
|
48
52
|
if (Object.keys(methods).length > 0) {
|
|
49
|
-
methods.OPTIONS ??= wrapRouteHandler(preflight, path);
|
|
53
|
+
methods.OPTIONS ??= wrapRouteHandler(preflight, path, onRequest, track);
|
|
50
54
|
bunRoutes[path] = methods;
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -54,19 +58,47 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
|
54
58
|
return bunRoutes;
|
|
55
59
|
}
|
|
56
60
|
function createWebServer(options) {
|
|
61
|
+
if (options.lifecycle?.isShuttingDown)
|
|
62
|
+
throw new Error("Cannot start HTTP during shutdown.");
|
|
63
|
+
const onRequest = (request, server) => {
|
|
64
|
+
if (options.lifecycle?.isShuttingDown)
|
|
65
|
+
return new Response("Service Unavailable", { status: 503 });
|
|
66
|
+
return options.onRequest?.(request, server);
|
|
67
|
+
};
|
|
68
|
+
for (const field of ["fetch", "routes", "port", "unix"])
|
|
69
|
+
if (options.native && field in options.native)
|
|
70
|
+
throw new TypeError(`native.${field} is owned by createWebServer.`);
|
|
71
|
+
const maxRequestBodySize = options.native?.maxRequestBodySize ?? resolveMaxBodyBytes();
|
|
72
|
+
if (!Number.isSafeInteger(maxRequestBodySize) || maxRequestBodySize <= 0)
|
|
73
|
+
throw new RangeError("maxRequestBodySize must be a positive safe integer.");
|
|
74
|
+
const active = new Set;
|
|
75
|
+
const track = (work) => {
|
|
76
|
+
if (!options.lifecycle)
|
|
77
|
+
return work();
|
|
78
|
+
const running = work();
|
|
79
|
+
active.add(running);
|
|
80
|
+
return running.finally(() => {
|
|
81
|
+
active.delete(running);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
57
84
|
const publicDir = options.publicDir ?? "./public";
|
|
58
|
-
const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors) : undefined;
|
|
59
|
-
|
|
85
|
+
const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors, onRequest, track) : undefined;
|
|
86
|
+
const { websocket, ...native } = options.native ?? {};
|
|
87
|
+
const configuration = {
|
|
88
|
+
...native,
|
|
89
|
+
maxRequestBodySize,
|
|
60
90
|
port: options.port,
|
|
61
91
|
...bunRoutes ? { routes: bunRoutes } : {},
|
|
62
92
|
async fetch(request, server) {
|
|
63
|
-
return await runWithRequestMeta({
|
|
93
|
+
return await track(async () => await runWithRequestMeta({
|
|
64
94
|
...currentRequestMeta(),
|
|
65
95
|
request,
|
|
66
96
|
ipAddress: socketAddress(server, request),
|
|
67
97
|
userAgent: request.headers.get("user-agent")
|
|
68
98
|
}, async () => {
|
|
69
|
-
await
|
|
99
|
+
const intercepted = await onRequest(request, server);
|
|
100
|
+
if (intercepted instanceof Response)
|
|
101
|
+
return intercepted;
|
|
70
102
|
const url = new URL(request.url);
|
|
71
103
|
if (url.pathname.startsWith("/assets/")) {
|
|
72
104
|
try {
|
|
@@ -80,13 +112,36 @@ function createWebServer(options) {
|
|
|
80
112
|
}
|
|
81
113
|
}
|
|
82
114
|
if (options.handle) {
|
|
83
|
-
const response = await options.handle(request);
|
|
115
|
+
const response = await options.handle(request, server);
|
|
116
|
+
if (response === undefined && websocket)
|
|
117
|
+
return;
|
|
84
118
|
return response ?? await missingHtmlResponse();
|
|
85
119
|
}
|
|
86
120
|
return await missingHtmlResponse();
|
|
87
|
-
});
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const server = websocket ? Bun.serve({ ...configuration, websocket }) : Bun.serve({
|
|
125
|
+
...configuration,
|
|
126
|
+
async fetch(request, server) {
|
|
127
|
+
return await configuration.fetch(request, server) ?? await missingHtmlResponse();
|
|
88
128
|
}
|
|
89
129
|
});
|
|
130
|
+
if (options.lifecycle) {
|
|
131
|
+
const lifecycle = options.lifecycle;
|
|
132
|
+
const name = `http:${crypto.randomUUID()}`;
|
|
133
|
+
let draining;
|
|
134
|
+
lifecycle.register(`${name}:stop`, () => {
|
|
135
|
+
draining ??= server.stop(false);
|
|
136
|
+
draining.catch(() => {});
|
|
137
|
+
}, "stop");
|
|
138
|
+
lifecycle.register(`${name}:drain`, async () => {
|
|
139
|
+
await draining;
|
|
140
|
+
await Promise.allSettled([...active]);
|
|
141
|
+
}, "drain");
|
|
142
|
+
lifecycle.register(`${name}:force`, () => server.stop(true), "force");
|
|
143
|
+
}
|
|
144
|
+
return server;
|
|
90
145
|
}
|
|
91
146
|
export {
|
|
92
147
|
convertAppRoutesToBunRoutes,
|