@getstrata/bootstrap 2.1.0 → 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 +25 -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/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 +5 -0
- 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/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 +426 -188
- package/dist/entries/createWebRoutes.js +214 -69
- package/dist/entries/dependencies.js +428 -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 +590 -216
- package/package.json +8 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
3
|
-
import { join as
|
|
3
|
+
import { join as join3 } from "path";
|
|
4
4
|
import { isViewsEnabled as isViewsEnabled2 } from "@getstrata/core/runtime/frontendMode";
|
|
5
5
|
import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
|
|
6
6
|
import { notFoundHtmlResponse } from "@getstrata/core/view";
|
|
@@ -59,8 +59,11 @@ import {
|
|
|
59
59
|
CORE_CACHE_TOKEN,
|
|
60
60
|
CORE_CONFIG_TOKEN,
|
|
61
61
|
CORE_EVENT_BUS_TOKEN,
|
|
62
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
62
63
|
CORE_POLICY_GATE_TOKEN,
|
|
63
64
|
CORE_QUEUE_TOKEN,
|
|
65
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
66
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
64
67
|
CORE_TOKEN_SERVICE_TOKEN
|
|
65
68
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
66
69
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -126,8 +129,39 @@ function resolveRegisterRateLimit() {
|
|
|
126
129
|
// ../../src/bootstrap/httpKernel.ts
|
|
127
130
|
class HttpKernel {
|
|
128
131
|
dependencies;
|
|
132
|
+
ownedThrottles = new Set;
|
|
133
|
+
disposed = false;
|
|
129
134
|
constructor(dependencies) {
|
|
130
135
|
this.dependencies = dependencies;
|
|
136
|
+
if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
|
|
137
|
+
dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
dispose() {
|
|
141
|
+
this.disposed = true;
|
|
142
|
+
const owned = [...this.ownedThrottles];
|
|
143
|
+
this.ownedThrottles.clear();
|
|
144
|
+
const errors = [];
|
|
145
|
+
for (const throttle of owned.reverse()) {
|
|
146
|
+
try {
|
|
147
|
+
throttle.dispose();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
errors.push(error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (errors.length)
|
|
153
|
+
throw new AggregateError(errors, "HTTP throttle cleanup failed.");
|
|
154
|
+
}
|
|
155
|
+
ownThrottle(throttle) {
|
|
156
|
+
if (this.disposed) {
|
|
157
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function")
|
|
158
|
+
throttle.dispose();
|
|
159
|
+
throw new Error("HTTP kernel is disposed.");
|
|
160
|
+
}
|
|
161
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function") {
|
|
162
|
+
this.ownedThrottles.add(throttle);
|
|
163
|
+
}
|
|
164
|
+
return throttle;
|
|
131
165
|
}
|
|
132
166
|
globalMiddleware(errorFormat = "json") {
|
|
133
167
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
@@ -142,7 +176,9 @@ class HttpKernel {
|
|
|
142
176
|
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
143
177
|
createAuthMiddleware(auth),
|
|
144
178
|
createMembershipMiddleware(),
|
|
145
|
-
createTenantMiddleware(
|
|
179
|
+
createTenantMiddleware({
|
|
180
|
+
resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
|
|
181
|
+
})
|
|
146
182
|
];
|
|
147
183
|
}
|
|
148
184
|
group(name) {
|
|
@@ -157,25 +193,28 @@ class HttpKernel {
|
|
|
157
193
|
if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
158
194
|
return [csrf];
|
|
159
195
|
}
|
|
196
|
+
const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
|
|
160
197
|
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
161
198
|
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
162
199
|
if (!redisUrl) {
|
|
163
200
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
164
201
|
return [
|
|
165
|
-
createMemoryThrottleMiddleware({
|
|
202
|
+
this.ownThrottle(createMemoryThrottleMiddleware({
|
|
166
203
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
167
|
-
decaySeconds: 60
|
|
168
|
-
|
|
204
|
+
decaySeconds: 60,
|
|
205
|
+
quotaPolicy
|
|
206
|
+
})),
|
|
169
207
|
csrf
|
|
170
208
|
];
|
|
171
209
|
}
|
|
172
210
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
173
211
|
return [
|
|
174
|
-
createThrottleMiddleware({
|
|
212
|
+
this.ownThrottle(createThrottleMiddleware({
|
|
175
213
|
redisUrl,
|
|
176
214
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
177
|
-
decaySeconds: 60
|
|
178
|
-
|
|
215
|
+
decaySeconds: 60,
|
|
216
|
+
quotaPolicy
|
|
217
|
+
})),
|
|
179
218
|
csrf
|
|
180
219
|
];
|
|
181
220
|
}
|
|
@@ -305,25 +344,25 @@ class HttpKernel {
|
|
|
305
344
|
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
306
345
|
const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
|
|
307
346
|
if (scope === "login") {
|
|
308
|
-
return withMiddleware(createLoginThrottleMiddleware({
|
|
347
|
+
return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
|
|
309
348
|
...redisUrl ? { redisUrl } : {},
|
|
310
349
|
maxAttempts: rateLimit.maxAttempts,
|
|
311
350
|
decaySeconds: rateLimit.decaySeconds
|
|
312
|
-
}))(handler);
|
|
351
|
+
})))(handler);
|
|
313
352
|
}
|
|
314
353
|
if (redisUrl) {
|
|
315
|
-
return withMiddleware(createThrottleMiddleware({
|
|
354
|
+
return withMiddleware(this.ownThrottle(createThrottleMiddleware({
|
|
316
355
|
redisUrl,
|
|
317
356
|
maxAttempts: rateLimit.maxAttempts,
|
|
318
357
|
decaySeconds: rateLimit.decaySeconds,
|
|
319
358
|
keyPrefix: memoryKeyPrefix
|
|
320
|
-
}))(handler);
|
|
359
|
+
})))(handler);
|
|
321
360
|
}
|
|
322
|
-
return withMiddleware(createMemoryThrottleMiddleware({
|
|
361
|
+
return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
|
|
323
362
|
maxAttempts: rateLimit.maxAttempts,
|
|
324
363
|
decaySeconds: rateLimit.decaySeconds,
|
|
325
364
|
keyPrefix: memoryKeyPrefix
|
|
326
|
-
}))(handler);
|
|
365
|
+
})))(handler);
|
|
327
366
|
}
|
|
328
367
|
}
|
|
329
368
|
function createHttpKernel(dependencies) {
|
|
@@ -331,74 +370,180 @@ function createHttpKernel(dependencies) {
|
|
|
331
370
|
}
|
|
332
371
|
|
|
333
372
|
// ../../src/bootstrap/discoverModules.ts
|
|
334
|
-
import {
|
|
335
|
-
import { join } from "path";
|
|
373
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
374
|
+
import { join as join2, resolve } from "path";
|
|
375
|
+
|
|
376
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
377
|
+
import { readdir } from "fs/promises";
|
|
378
|
+
import { join, relative } from "path";
|
|
336
379
|
import { pathToFileURL } from "url";
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
380
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
381
|
+
const excluded = new Set([
|
|
382
|
+
"index.ts",
|
|
383
|
+
"index.js",
|
|
384
|
+
"index.mts",
|
|
385
|
+
"index.mjs",
|
|
386
|
+
"register.ts",
|
|
387
|
+
"register.js",
|
|
388
|
+
"register.mts",
|
|
389
|
+
"register.mjs",
|
|
390
|
+
...exclude
|
|
391
|
+
]);
|
|
392
|
+
const files = [];
|
|
393
|
+
async function visit(path) {
|
|
394
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
395
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
396
|
+
for (const entry of entries) {
|
|
397
|
+
const file = join(path, entry.name);
|
|
398
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
399
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
400
|
+
continue;
|
|
401
|
+
if (entry.isDirectory())
|
|
402
|
+
await visit(file);
|
|
403
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
404
|
+
files.push(file);
|
|
405
|
+
}
|
|
359
406
|
}
|
|
360
|
-
|
|
361
|
-
|
|
407
|
+
try {
|
|
408
|
+
await visit(directory);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error.code !== "ENOENT")
|
|
411
|
+
throw error;
|
|
412
|
+
if (error.path !== directory)
|
|
413
|
+
throw error;
|
|
362
414
|
}
|
|
363
|
-
|
|
415
|
+
return files;
|
|
364
416
|
}
|
|
365
|
-
async function
|
|
366
|
-
const modulesDirectory = resolveModulesDirectory(options);
|
|
367
|
-
let moduleNames;
|
|
417
|
+
async function importInfrastructure(file) {
|
|
368
418
|
try {
|
|
369
|
-
|
|
419
|
+
return await import(pathToFileURL(file).href);
|
|
370
420
|
} catch (error) {
|
|
371
|
-
|
|
372
|
-
|
|
421
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
426
|
+
var KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
427
|
+
function state() {
|
|
428
|
+
const globals = globalThis;
|
|
429
|
+
globals[KEY] ??= { appModules: [] };
|
|
430
|
+
return globals[KEY];
|
|
431
|
+
}
|
|
432
|
+
function configureModulesDirectory(modulesDir) {
|
|
433
|
+
const current = state();
|
|
434
|
+
const directory = resolve(modulesDir);
|
|
435
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
436
|
+
current.appModules.length = 0;
|
|
437
|
+
current.modulesReady = undefined;
|
|
438
|
+
current.source = undefined;
|
|
439
|
+
}
|
|
440
|
+
current.configuredManifest = undefined;
|
|
441
|
+
current.configuredModulesDir = directory;
|
|
442
|
+
}
|
|
443
|
+
function configureModulesManifest(manifest) {
|
|
444
|
+
const current = state();
|
|
445
|
+
if (current.configuredManifest !== manifest) {
|
|
446
|
+
current.appModules.length = 0;
|
|
447
|
+
current.modulesReady = undefined;
|
|
448
|
+
current.source = undefined;
|
|
449
|
+
}
|
|
450
|
+
current.configuredManifest = manifest;
|
|
451
|
+
}
|
|
452
|
+
function validateModule(value, location) {
|
|
453
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
454
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
455
|
+
const module = value;
|
|
456
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
457
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
458
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
459
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
460
|
+
for (const hook of ["routes", "webRoutes"])
|
|
461
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
462
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
463
|
+
if (module.providers !== undefined) {
|
|
464
|
+
if (!Array.isArray(module.providers))
|
|
465
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
466
|
+
for (const provider of module.providers) {
|
|
467
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
468
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
373
469
|
}
|
|
374
|
-
throw error;
|
|
375
470
|
}
|
|
376
|
-
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
377
|
-
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
378
|
-
const loaded = await import(moduleUrl);
|
|
379
|
-
return loaded.default;
|
|
380
|
-
}));
|
|
381
|
-
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
382
471
|
}
|
|
383
|
-
async function
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
472
|
+
async function load(options) {
|
|
473
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
|
|
474
|
+
const entries = [];
|
|
475
|
+
if (manifest)
|
|
476
|
+
for (const [index, value] of manifest.entries())
|
|
477
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
478
|
+
else {
|
|
479
|
+
const directory = options.modulesDir ?? state().configuredModulesDir;
|
|
480
|
+
if (!directory)
|
|
481
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
482
|
+
let folders;
|
|
483
|
+
try {
|
|
484
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error.code === "ENOENT")
|
|
487
|
+
return [];
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
const excluded = new Set(options.exclude ?? []);
|
|
491
|
+
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)) {
|
|
492
|
+
const path = join2(directory, folder.name);
|
|
493
|
+
const files = await readdir2(path);
|
|
494
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
495
|
+
if (candidates.length !== 1)
|
|
496
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
497
|
+
const file = join2(path, candidates[0]);
|
|
498
|
+
const exports = await importInfrastructure(file);
|
|
499
|
+
entries.push({ value: exports.default, location: file });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const names = new Map;
|
|
503
|
+
const modules = [];
|
|
504
|
+
for (const { value, location } of entries) {
|
|
505
|
+
validateModule(value, location);
|
|
506
|
+
const previous = names.get(value.name);
|
|
507
|
+
if (previous)
|
|
508
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
509
|
+
names.set(value.name, location);
|
|
510
|
+
modules.push(value);
|
|
511
|
+
}
|
|
512
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
513
|
+
}
|
|
514
|
+
function ensureModulesLoaded(options = {}) {
|
|
515
|
+
const current = state();
|
|
516
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
517
|
+
options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
|
|
518
|
+
[...options.exclude ?? []].sort()
|
|
519
|
+
]);
|
|
520
|
+
if (current.modulesReady && current.source === key)
|
|
521
|
+
return current.modulesReady;
|
|
522
|
+
current.appModules.length = 0;
|
|
523
|
+
current.source = key;
|
|
524
|
+
const pending = load(options).then((modules) => {
|
|
525
|
+
if (current.modulesReady !== pending)
|
|
526
|
+
return modules;
|
|
527
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
528
|
+
return current.appModules;
|
|
529
|
+
}).catch((error) => {
|
|
530
|
+
if (current.modulesReady === pending)
|
|
531
|
+
current.modulesReady = undefined;
|
|
532
|
+
throw error;
|
|
391
533
|
});
|
|
392
|
-
|
|
534
|
+
current.modulesReady = pending;
|
|
535
|
+
return pending;
|
|
393
536
|
}
|
|
394
537
|
function discoverModules() {
|
|
395
|
-
return
|
|
538
|
+
return state().appModules;
|
|
396
539
|
}
|
|
397
540
|
function resetDiscoverModulesForTests() {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
541
|
+
const current = state();
|
|
542
|
+
current.configuredModulesDir = undefined;
|
|
543
|
+
current.configuredManifest = undefined;
|
|
544
|
+
current.appModules.length = 0;
|
|
545
|
+
current.modulesReady = undefined;
|
|
546
|
+
current.source = undefined;
|
|
402
547
|
}
|
|
403
548
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
404
549
|
function prefixRouteMap(prefix, routes) {
|
|
@@ -532,7 +677,7 @@ function createWebRoutes(dependencies, options = {}) {
|
|
|
532
677
|
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
533
678
|
const pathname = new URL(request.url).pathname;
|
|
534
679
|
try {
|
|
535
|
-
const filePath = assertUrlPathUnderRoot(
|
|
680
|
+
const filePath = assertUrlPathUnderRoot(join3(process.cwd(), "public"), pathname);
|
|
536
681
|
const file = Bun.file(filePath);
|
|
537
682
|
if (!await file.exists()) {
|
|
538
683
|
return notFoundHtmlResponse();
|