@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
|
@@ -46,8 +46,11 @@ import {
|
|
|
46
46
|
CORE_CACHE_TOKEN,
|
|
47
47
|
CORE_CONFIG_TOKEN,
|
|
48
48
|
CORE_EVENT_BUS_TOKEN,
|
|
49
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
49
50
|
CORE_POLICY_GATE_TOKEN,
|
|
50
51
|
CORE_QUEUE_TOKEN,
|
|
52
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
53
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
51
54
|
CORE_TOKEN_SERVICE_TOKEN
|
|
52
55
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
53
56
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -113,8 +116,39 @@ function resolveRegisterRateLimit() {
|
|
|
113
116
|
// ../../src/bootstrap/httpKernel.ts
|
|
114
117
|
class HttpKernel {
|
|
115
118
|
dependencies;
|
|
119
|
+
ownedThrottles = new Set;
|
|
120
|
+
disposed = false;
|
|
116
121
|
constructor(dependencies) {
|
|
117
122
|
this.dependencies = dependencies;
|
|
123
|
+
if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
|
|
124
|
+
dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
dispose() {
|
|
128
|
+
this.disposed = true;
|
|
129
|
+
const owned = [...this.ownedThrottles];
|
|
130
|
+
this.ownedThrottles.clear();
|
|
131
|
+
const errors = [];
|
|
132
|
+
for (const throttle of owned.reverse()) {
|
|
133
|
+
try {
|
|
134
|
+
throttle.dispose();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
errors.push(error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (errors.length)
|
|
140
|
+
throw new AggregateError(errors, "HTTP throttle cleanup failed.");
|
|
141
|
+
}
|
|
142
|
+
ownThrottle(throttle) {
|
|
143
|
+
if (this.disposed) {
|
|
144
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function")
|
|
145
|
+
throttle.dispose();
|
|
146
|
+
throw new Error("HTTP kernel is disposed.");
|
|
147
|
+
}
|
|
148
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function") {
|
|
149
|
+
this.ownedThrottles.add(throttle);
|
|
150
|
+
}
|
|
151
|
+
return throttle;
|
|
118
152
|
}
|
|
119
153
|
globalMiddleware(errorFormat = "json") {
|
|
120
154
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
@@ -129,7 +163,9 @@ class HttpKernel {
|
|
|
129
163
|
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
130
164
|
createAuthMiddleware(auth),
|
|
131
165
|
createMembershipMiddleware(),
|
|
132
|
-
createTenantMiddleware(
|
|
166
|
+
createTenantMiddleware({
|
|
167
|
+
resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
|
|
168
|
+
})
|
|
133
169
|
];
|
|
134
170
|
}
|
|
135
171
|
group(name) {
|
|
@@ -144,25 +180,28 @@ class HttpKernel {
|
|
|
144
180
|
if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
145
181
|
return [csrf];
|
|
146
182
|
}
|
|
183
|
+
const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
|
|
147
184
|
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
148
185
|
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
149
186
|
if (!redisUrl) {
|
|
150
187
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
151
188
|
return [
|
|
152
|
-
createMemoryThrottleMiddleware({
|
|
189
|
+
this.ownThrottle(createMemoryThrottleMiddleware({
|
|
153
190
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
154
|
-
decaySeconds: 60
|
|
155
|
-
|
|
191
|
+
decaySeconds: 60,
|
|
192
|
+
quotaPolicy
|
|
193
|
+
})),
|
|
156
194
|
csrf
|
|
157
195
|
];
|
|
158
196
|
}
|
|
159
197
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
160
198
|
return [
|
|
161
|
-
createThrottleMiddleware({
|
|
199
|
+
this.ownThrottle(createThrottleMiddleware({
|
|
162
200
|
redisUrl,
|
|
163
201
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
164
|
-
decaySeconds: 60
|
|
165
|
-
|
|
202
|
+
decaySeconds: 60,
|
|
203
|
+
quotaPolicy
|
|
204
|
+
})),
|
|
166
205
|
csrf
|
|
167
206
|
];
|
|
168
207
|
}
|
|
@@ -292,25 +331,25 @@ class HttpKernel {
|
|
|
292
331
|
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
293
332
|
const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
|
|
294
333
|
if (scope === "login") {
|
|
295
|
-
return withMiddleware(createLoginThrottleMiddleware({
|
|
334
|
+
return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
|
|
296
335
|
...redisUrl ? { redisUrl } : {},
|
|
297
336
|
maxAttempts: rateLimit.maxAttempts,
|
|
298
337
|
decaySeconds: rateLimit.decaySeconds
|
|
299
|
-
}))(handler);
|
|
338
|
+
})))(handler);
|
|
300
339
|
}
|
|
301
340
|
if (redisUrl) {
|
|
302
|
-
return withMiddleware(createThrottleMiddleware({
|
|
341
|
+
return withMiddleware(this.ownThrottle(createThrottleMiddleware({
|
|
303
342
|
redisUrl,
|
|
304
343
|
maxAttempts: rateLimit.maxAttempts,
|
|
305
344
|
decaySeconds: rateLimit.decaySeconds,
|
|
306
345
|
keyPrefix: memoryKeyPrefix
|
|
307
|
-
}))(handler);
|
|
346
|
+
})))(handler);
|
|
308
347
|
}
|
|
309
|
-
return withMiddleware(createMemoryThrottleMiddleware({
|
|
348
|
+
return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
|
|
310
349
|
maxAttempts: rateLimit.maxAttempts,
|
|
311
350
|
decaySeconds: rateLimit.decaySeconds,
|
|
312
351
|
keyPrefix: memoryKeyPrefix
|
|
313
|
-
}))(handler);
|
|
352
|
+
})))(handler);
|
|
314
353
|
}
|
|
315
354
|
}
|
|
316
355
|
function createHttpKernel(dependencies) {
|
|
@@ -18,74 +18,180 @@ import {
|
|
|
18
18
|
} from "@getstrata/core/runtime/applicationRegistry";
|
|
19
19
|
|
|
20
20
|
// ../../src/bootstrap/discoverModules.ts
|
|
21
|
-
import {
|
|
22
|
-
import { join } from "path";
|
|
21
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
22
|
+
import { join as join2, resolve } from "path";
|
|
23
|
+
|
|
24
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
25
|
+
import { readdir } from "fs/promises";
|
|
26
|
+
import { join, relative } from "path";
|
|
23
27
|
import { pathToFileURL } from "url";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
29
|
+
const excluded = new Set([
|
|
30
|
+
"index.ts",
|
|
31
|
+
"index.js",
|
|
32
|
+
"index.mts",
|
|
33
|
+
"index.mjs",
|
|
34
|
+
"register.ts",
|
|
35
|
+
"register.js",
|
|
36
|
+
"register.mts",
|
|
37
|
+
"register.mjs",
|
|
38
|
+
...exclude
|
|
39
|
+
]);
|
|
40
|
+
const files = [];
|
|
41
|
+
async function visit(path) {
|
|
42
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
43
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const file = join(path, entry.name);
|
|
46
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
47
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
48
|
+
continue;
|
|
49
|
+
if (entry.isDirectory())
|
|
50
|
+
await visit(file);
|
|
51
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
52
|
+
files.push(file);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await visit(directory);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.code !== "ENOENT")
|
|
59
|
+
throw error;
|
|
60
|
+
if (error.path !== directory)
|
|
61
|
+
throw error;
|
|
29
62
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
async function importInfrastructure(file) {
|
|
66
|
+
try {
|
|
67
|
+
return await import(pathToFileURL(file).href);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
74
|
+
var KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
75
|
+
function state() {
|
|
76
|
+
const globals = globalThis;
|
|
77
|
+
globals[KEY] ??= { appModules: [] };
|
|
78
|
+
return globals[KEY];
|
|
33
79
|
}
|
|
34
80
|
function configureModulesDirectory(modulesDir) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
const current = state();
|
|
82
|
+
const directory = resolve(modulesDir);
|
|
83
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
84
|
+
current.appModules.length = 0;
|
|
85
|
+
current.modulesReady = undefined;
|
|
86
|
+
current.source = undefined;
|
|
39
87
|
}
|
|
40
|
-
|
|
88
|
+
current.configuredManifest = undefined;
|
|
89
|
+
current.configuredModulesDir = directory;
|
|
41
90
|
}
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
|
|
91
|
+
function configureModulesManifest(manifest) {
|
|
92
|
+
const current = state();
|
|
93
|
+
if (current.configuredManifest !== manifest) {
|
|
94
|
+
current.appModules.length = 0;
|
|
95
|
+
current.modulesReady = undefined;
|
|
96
|
+
current.source = undefined;
|
|
46
97
|
}
|
|
47
|
-
|
|
48
|
-
|
|
98
|
+
current.configuredManifest = manifest;
|
|
99
|
+
}
|
|
100
|
+
function validateModule(value, location) {
|
|
101
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
102
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
103
|
+
const module = value;
|
|
104
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
105
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
106
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
107
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
108
|
+
for (const hook of ["routes", "webRoutes"])
|
|
109
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
110
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
111
|
+
if (module.providers !== undefined) {
|
|
112
|
+
if (!Array.isArray(module.providers))
|
|
113
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
114
|
+
for (const provider of module.providers) {
|
|
115
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
116
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
117
|
+
}
|
|
49
118
|
}
|
|
50
|
-
throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
|
|
51
119
|
}
|
|
52
|
-
async function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
120
|
+
async function load(options) {
|
|
121
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
|
|
122
|
+
const entries = [];
|
|
123
|
+
if (manifest)
|
|
124
|
+
for (const [index, value] of manifest.entries())
|
|
125
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
126
|
+
else {
|
|
127
|
+
const directory = options.modulesDir ?? state().configuredModulesDir;
|
|
128
|
+
if (!directory)
|
|
129
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
130
|
+
let folders;
|
|
131
|
+
try {
|
|
132
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.code === "ENOENT")
|
|
135
|
+
return [];
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
const excluded = new Set(options.exclude ?? []);
|
|
139
|
+
for (const folder of folders.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
140
|
+
const path = join2(directory, folder.name);
|
|
141
|
+
const files = await readdir2(path);
|
|
142
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
143
|
+
if (candidates.length !== 1)
|
|
144
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
145
|
+
const file = join2(path, candidates[0]);
|
|
146
|
+
const exports = await importInfrastructure(file);
|
|
147
|
+
entries.push({ value: exports.default, location: file });
|
|
60
148
|
}
|
|
61
|
-
throw error;
|
|
62
149
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (state.appModules.length > 0) {
|
|
73
|
-
return state.appModules;
|
|
150
|
+
const names = new Map;
|
|
151
|
+
const modules = [];
|
|
152
|
+
for (const { value, location } of entries) {
|
|
153
|
+
validateModule(value, location);
|
|
154
|
+
const previous = names.get(value.name);
|
|
155
|
+
if (previous)
|
|
156
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
157
|
+
names.set(value.name, location);
|
|
158
|
+
modules.push(value);
|
|
74
159
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
160
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
161
|
+
}
|
|
162
|
+
function ensureModulesLoaded(options = {}) {
|
|
163
|
+
const current = state();
|
|
164
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
165
|
+
options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
|
|
166
|
+
[...options.exclude ?? []].sort()
|
|
167
|
+
]);
|
|
168
|
+
if (current.modulesReady && current.source === key)
|
|
169
|
+
return current.modulesReady;
|
|
170
|
+
current.appModules.length = 0;
|
|
171
|
+
current.source = key;
|
|
172
|
+
const pending = load(options).then((modules) => {
|
|
173
|
+
if (current.modulesReady !== pending)
|
|
174
|
+
return modules;
|
|
175
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
176
|
+
return current.appModules;
|
|
177
|
+
}).catch((error) => {
|
|
178
|
+
if (current.modulesReady === pending)
|
|
179
|
+
current.modulesReady = undefined;
|
|
180
|
+
throw error;
|
|
78
181
|
});
|
|
79
|
-
|
|
182
|
+
current.modulesReady = pending;
|
|
183
|
+
return pending;
|
|
80
184
|
}
|
|
81
185
|
function discoverModules() {
|
|
82
|
-
return
|
|
186
|
+
return state().appModules;
|
|
83
187
|
}
|
|
84
188
|
function resetDiscoverModulesForTests() {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
189
|
+
const current = state();
|
|
190
|
+
current.configuredModulesDir = undefined;
|
|
191
|
+
current.configuredManifest = undefined;
|
|
192
|
+
current.appModules.length = 0;
|
|
193
|
+
current.modulesReady = undefined;
|
|
194
|
+
current.source = undefined;
|
|
89
195
|
}
|
|
90
196
|
|
|
91
197
|
// ../../src/bootstrap/cache/modelCacheTags.ts
|