@getstrata/bootstrap 2.1.0 → 2.2.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/bootstrap/app.d.ts +7 -1
  4. package/dist/bootstrap/config.d.ts +1 -1
  5. package/dist/bootstrap/context.d.ts +8 -2
  6. package/dist/bootstrap/dependencies.d.ts +1 -1
  7. package/dist/bootstrap/discoverJobs.d.ts +11 -3
  8. package/dist/bootstrap/discoverListeners.d.ts +7 -3
  9. package/dist/bootstrap/discoverModels.d.ts +8 -0
  10. package/dist/bootstrap/discoverModules.d.ts +4 -1
  11. package/dist/bootstrap/httpKernel.d.ts +11 -2
  12. package/dist/bootstrap/inProcessCron.d.ts +2 -1
  13. package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
  14. package/dist/bootstrap/modules.d.ts +1 -1
  15. package/dist/bootstrap/providers/tracing.d.ts +3 -0
  16. package/dist/bootstrap/public-api.d.ts +9 -4
  17. package/dist/bootstrap/web/index.d.ts +1 -1
  18. package/dist/bootstrap/web/server.d.ts +15 -7
  19. package/dist/entries/buildModuleRoutes.js +218 -71
  20. package/dist/entries/buildWebModuleRoutes.js +218 -71
  21. package/dist/entries/cache/modelCacheTags.js +157 -51
  22. package/dist/entries/config.js +8 -0
  23. package/dist/entries/context.js +427 -188
  24. package/dist/entries/createWebRoutes.js +220 -73
  25. package/dist/entries/dependencies.js +429 -190
  26. package/dist/entries/discoverJobs.js +132 -50
  27. package/dist/entries/discoverListeners.js +85 -37
  28. package/dist/entries/discoverModels.js +39 -0
  29. package/dist/entries/discoverModules.js +158 -51
  30. package/dist/entries/health.js +4 -0
  31. package/dist/entries/httpKernel.js +58 -17
  32. package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
  33. package/dist/entries/providers.js +308 -164
  34. package/dist/entries/secretsGuard.js +2 -2
  35. package/dist/entries/web/routing.js +58 -17
  36. package/dist/entries/web/server.js +71 -14
  37. package/dist/index.js +600 -222
  38. package/package.json +8 -3
@@ -53,8 +53,12 @@ import {
53
53
  CORE_CACHE_TOKEN,
54
54
  CORE_CONFIG_TOKEN,
55
55
  CORE_EVENT_BUS_TOKEN,
56
+ CORE_HTTP_CLEANUP_TOKEN,
56
57
  CORE_POLICY_GATE_TOKEN,
58
+ CORE_PUBLIC_TENANCY_TOKEN,
57
59
  CORE_QUEUE_TOKEN,
60
+ CORE_TENANT_RESOLVER_TOKEN,
61
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
58
62
  CORE_TOKEN_SERVICE_TOKEN
59
63
  } from "@getstrata/core/contracts/serviceTokens";
60
64
  var APP_PORT_CONFIG_KEY = "app.port";
@@ -120,8 +124,39 @@ function resolveRegisterRateLimit() {
120
124
  // ../../src/bootstrap/httpKernel.ts
121
125
  class HttpKernel {
122
126
  dependencies;
127
+ ownedThrottles = new Set;
128
+ disposed = false;
123
129
  constructor(dependencies) {
124
130
  this.dependencies = dependencies;
131
+ if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
132
+ dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
133
+ }
134
+ }
135
+ dispose() {
136
+ this.disposed = true;
137
+ const owned = [...this.ownedThrottles];
138
+ this.ownedThrottles.clear();
139
+ const errors = [];
140
+ for (const throttle of owned.reverse()) {
141
+ try {
142
+ throttle.dispose();
143
+ } catch (error) {
144
+ errors.push(error);
145
+ }
146
+ }
147
+ if (errors.length)
148
+ throw new AggregateError(errors, "HTTP throttle cleanup failed.");
149
+ }
150
+ ownThrottle(throttle) {
151
+ if (this.disposed) {
152
+ if ("dispose" in throttle && typeof throttle.dispose === "function")
153
+ throttle.dispose();
154
+ throw new Error("HTTP kernel is disposed.");
155
+ }
156
+ if ("dispose" in throttle && typeof throttle.dispose === "function") {
157
+ this.ownedThrottles.add(throttle);
158
+ }
159
+ return throttle;
125
160
  }
126
161
  globalMiddleware(errorFormat = "json") {
127
162
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
@@ -136,7 +171,10 @@ class HttpKernel {
136
171
  errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
137
172
  createAuthMiddleware(auth),
138
173
  createMembershipMiddleware(),
139
- createTenantMiddleware()
174
+ createTenantMiddleware({
175
+ publicTenancy: this.dependencies.container.has(CORE_PUBLIC_TENANCY_TOKEN) ? this.dependencies.container.resolve(CORE_PUBLIC_TENANCY_TOKEN) : undefined,
176
+ resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
177
+ })
140
178
  ];
141
179
  }
142
180
  group(name) {
@@ -151,25 +189,28 @@ class HttpKernel {
151
189
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
152
190
  return [csrf];
153
191
  }
192
+ const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
154
193
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
155
194
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
156
195
  if (!redisUrl) {
157
196
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
158
197
  return [
159
- createMemoryThrottleMiddleware({
198
+ this.ownThrottle(createMemoryThrottleMiddleware({
160
199
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
161
- decaySeconds: 60
162
- }),
200
+ decaySeconds: 60,
201
+ quotaPolicy
202
+ })),
163
203
  csrf
164
204
  ];
165
205
  }
166
206
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
167
207
  return [
168
- createThrottleMiddleware({
208
+ this.ownThrottle(createThrottleMiddleware({
169
209
  redisUrl,
170
210
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
171
- decaySeconds: 60
172
- }),
211
+ decaySeconds: 60,
212
+ quotaPolicy
213
+ })),
173
214
  csrf
174
215
  ];
175
216
  }
@@ -206,8 +247,8 @@ class HttpKernel {
206
247
  return handler(request);
207
248
  });
208
249
  }
209
- wrapWebPublicRead(handler) {
210
- if (isPublicReadsEnabled()) {
250
+ wrapWebPublicRead(handler, options = {}) {
251
+ if (options.allowAnonymous ?? isPublicReadsEnabled()) {
211
252
  return this.wrapWeb(handler);
212
253
  }
213
254
  return this.wrapWebAuthenticated(handler);
@@ -253,8 +294,8 @@ class HttpKernel {
253
294
  const middleware = [...this.group("authenticated"), ...this.verifiedMiddleware()];
254
295
  return withMiddleware(...middleware)(handler);
255
296
  }
256
- wrapPublicRead(handler) {
257
- if (isPublicReadsEnabled()) {
297
+ wrapPublicRead(handler, options = {}) {
298
+ if (options.allowAnonymous ?? isPublicReadsEnabled()) {
258
299
  return handler;
259
300
  }
260
301
  return this.wrapAuthenticated(handler);
@@ -299,25 +340,25 @@ class HttpKernel {
299
340
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
300
341
  const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
301
342
  if (scope === "login") {
302
- return withMiddleware(createLoginThrottleMiddleware({
343
+ return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
303
344
  ...redisUrl ? { redisUrl } : {},
304
345
  maxAttempts: rateLimit.maxAttempts,
305
346
  decaySeconds: rateLimit.decaySeconds
306
- }))(handler);
347
+ })))(handler);
307
348
  }
308
349
  if (redisUrl) {
309
- return withMiddleware(createThrottleMiddleware({
350
+ return withMiddleware(this.ownThrottle(createThrottleMiddleware({
310
351
  redisUrl,
311
352
  maxAttempts: rateLimit.maxAttempts,
312
353
  decaySeconds: rateLimit.decaySeconds,
313
354
  keyPrefix: memoryKeyPrefix
314
- }))(handler);
355
+ })))(handler);
315
356
  }
316
- return withMiddleware(createMemoryThrottleMiddleware({
357
+ return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
317
358
  maxAttempts: rateLimit.maxAttempts,
318
359
  decaySeconds: rateLimit.decaySeconds,
319
360
  keyPrefix: memoryKeyPrefix
320
- }))(handler);
361
+ })))(handler);
321
362
  }
322
363
  }
323
364
  function createHttpKernel(dependencies) {
@@ -325,74 +366,180 @@ function createHttpKernel(dependencies) {
325
366
  }
326
367
 
327
368
  // ../../src/bootstrap/discoverModules.ts
328
- import { readdirSync } from "fs";
329
- import { join } from "path";
369
+ import { readdir as readdir2 } from "fs/promises";
370
+ import { join as join2, resolve } from "path";
371
+
372
+ // ../../src/bootstrap/infrastructureDiscovery.ts
373
+ import { readdir } from "fs/promises";
374
+ import { join, relative } from "path";
330
375
  import { pathToFileURL } from "url";
331
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
332
- function readDiscoverModulesState() {
333
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
334
- if (existing) {
335
- return existing;
336
- }
337
- const state = { appModules: [] };
338
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
339
- return state;
340
- }
341
- function configureModulesDirectory(modulesDir) {
342
- const state = readDiscoverModulesState();
343
- if (state.configuredModulesDir !== modulesDir) {
344
- state.appModules.length = 0;
345
- state.modulesReady = undefined;
346
- }
347
- state.configuredModulesDir = modulesDir;
348
- }
349
- function resolveModulesDirectory(options) {
350
- const state = readDiscoverModulesState();
351
- if (options?.modulesDir) {
352
- return options.modulesDir;
376
+ async function infrastructureFiles(directory, exclude = []) {
377
+ const excluded = new Set([
378
+ "index.ts",
379
+ "index.js",
380
+ "index.mts",
381
+ "index.mjs",
382
+ "register.ts",
383
+ "register.js",
384
+ "register.mts",
385
+ "register.mjs",
386
+ ...exclude
387
+ ]);
388
+ const files = [];
389
+ async function visit(path) {
390
+ const entries = await readdir(path, { withFileTypes: true });
391
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
392
+ for (const entry of entries) {
393
+ const file = join(path, entry.name);
394
+ const key = relative(directory, file).replaceAll("\\", "/");
395
+ if (excluded.has(entry.name) || excluded.has(key))
396
+ continue;
397
+ if (entry.isDirectory())
398
+ await visit(file);
399
+ else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
400
+ files.push(file);
401
+ }
353
402
  }
354
- if (state.configuredModulesDir) {
355
- return state.configuredModulesDir;
403
+ try {
404
+ await visit(directory);
405
+ } catch (error) {
406
+ if (error.code !== "ENOENT")
407
+ throw error;
408
+ if (error.path !== directory)
409
+ throw error;
356
410
  }
357
- throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
411
+ return files;
358
412
  }
359
- async function loadDiscoveredModules(options) {
360
- const modulesDirectory = resolveModulesDirectory(options);
361
- let moduleNames;
413
+ async function importInfrastructure(file) {
362
414
  try {
363
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
415
+ return await import(pathToFileURL(file).href);
364
416
  } catch (error) {
365
- if (error.code === "ENOENT") {
366
- return [];
417
+ throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
418
+ }
419
+ }
420
+
421
+ // ../../src/bootstrap/discoverModules.ts
422
+ var KEY = Symbol.for("@getstrata/discoverModulesState");
423
+ function state() {
424
+ const globals = globalThis;
425
+ globals[KEY] ??= { appModules: [] };
426
+ return globals[KEY];
427
+ }
428
+ function configureModulesDirectory(modulesDir) {
429
+ const current = state();
430
+ const directory = resolve(modulesDir);
431
+ if (current.configuredModulesDir !== directory || current.configuredManifest) {
432
+ current.appModules.length = 0;
433
+ current.modulesReady = undefined;
434
+ current.source = undefined;
435
+ }
436
+ current.configuredManifest = undefined;
437
+ current.configuredModulesDir = directory;
438
+ }
439
+ function configureModulesManifest(manifest) {
440
+ const current = state();
441
+ if (current.configuredManifest !== manifest) {
442
+ current.appModules.length = 0;
443
+ current.modulesReady = undefined;
444
+ current.source = undefined;
445
+ }
446
+ current.configuredManifest = manifest;
447
+ }
448
+ function validateModule(value, location) {
449
+ if (!value || typeof value !== "object" || Array.isArray(value))
450
+ throw new TypeError(`Invalid application module at ${location}.`);
451
+ const module = value;
452
+ if (typeof module.name !== "string" || !module.name.trim())
453
+ throw new TypeError(`Invalid module name at ${location}.`);
454
+ if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
455
+ throw new TypeError(`Invalid module order at ${location}.`);
456
+ for (const hook of ["routes", "webRoutes"])
457
+ if (module[hook] !== undefined && typeof module[hook] !== "function")
458
+ throw new TypeError(`Invalid ${hook} at ${location}.`);
459
+ if (module.providers !== undefined) {
460
+ if (!Array.isArray(module.providers))
461
+ throw new TypeError(`Invalid providers at ${location}.`);
462
+ for (const provider of module.providers) {
463
+ if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
464
+ throw new TypeError(`Invalid provider at ${location}.`);
367
465
  }
368
- throw error;
369
466
  }
370
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
371
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
372
- const loaded = await import(moduleUrl);
373
- return loaded.default;
374
- }));
375
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
376
467
  }
377
- async function ensureModulesLoaded(options) {
378
- const state = readDiscoverModulesState();
379
- if (state.appModules.length > 0) {
380
- return state.appModules;
381
- }
382
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
383
- state.appModules.splice(0, state.appModules.length, ...modules);
384
- return state.appModules;
468
+ async function load(options) {
469
+ const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
470
+ const entries = [];
471
+ if (manifest)
472
+ for (const [index, value] of manifest.entries())
473
+ entries.push({ value, location: `module manifest[${index}]` });
474
+ else {
475
+ const directory = options.modulesDir ?? state().configuredModulesDir;
476
+ if (!directory)
477
+ throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
478
+ let folders;
479
+ try {
480
+ folders = await readdir2(directory, { withFileTypes: true });
481
+ } catch (error) {
482
+ if (error.code === "ENOENT")
483
+ return [];
484
+ throw error;
485
+ }
486
+ const excluded = new Set(options.exclude ?? []);
487
+ 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)) {
488
+ const path = join2(directory, folder.name);
489
+ const files = await readdir2(path);
490
+ const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
491
+ if (candidates.length !== 1)
492
+ throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
493
+ const file = join2(path, candidates[0]);
494
+ const exports = await importInfrastructure(file);
495
+ entries.push({ value: exports.default, location: file });
496
+ }
497
+ }
498
+ const names = new Map;
499
+ const modules = [];
500
+ for (const { value, location } of entries) {
501
+ validateModule(value, location);
502
+ const previous = names.get(value.name);
503
+ if (previous)
504
+ throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
505
+ names.set(value.name, location);
506
+ modules.push(value);
507
+ }
508
+ return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
509
+ }
510
+ function ensureModulesLoaded(options = {}) {
511
+ const current = state();
512
+ const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
513
+ options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
514
+ [...options.exclude ?? []].sort()
515
+ ]);
516
+ if (current.modulesReady && current.source === key)
517
+ return current.modulesReady;
518
+ current.appModules.length = 0;
519
+ current.source = key;
520
+ const pending = load(options).then((modules) => {
521
+ if (current.modulesReady !== pending)
522
+ return modules;
523
+ current.appModules.splice(0, current.appModules.length, ...modules);
524
+ return current.appModules;
525
+ }).catch((error) => {
526
+ if (current.modulesReady === pending)
527
+ current.modulesReady = undefined;
528
+ throw error;
385
529
  });
386
- return state.modulesReady;
530
+ current.modulesReady = pending;
531
+ return pending;
387
532
  }
388
533
  function discoverModules() {
389
- return readDiscoverModulesState().appModules;
534
+ return state().appModules;
390
535
  }
391
536
  function resetDiscoverModulesForTests() {
392
- const state = readDiscoverModulesState();
393
- state.configuredModulesDir = undefined;
394
- state.appModules.length = 0;
395
- state.modulesReady = undefined;
537
+ const current = state();
538
+ current.configuredModulesDir = undefined;
539
+ current.configuredManifest = undefined;
540
+ current.appModules.length = 0;
541
+ current.modulesReady = undefined;
542
+ current.source = undefined;
396
543
  }
397
544
  // ../../src/bootstrap/prefixRouteMap.ts
398
545
  function prefixRouteMap(prefix, routes) {
@@ -1,73 +1,179 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/discoverModules.ts
3
- import { readdirSync } from "fs";
4
- import { join } from "path";
3
+ import { readdir as readdir2 } from "fs/promises";
4
+ import { join as join2, resolve } from "path";
5
+
6
+ // ../../src/bootstrap/infrastructureDiscovery.ts
7
+ import { readdir } from "fs/promises";
8
+ import { join, relative } from "path";
5
9
  import { pathToFileURL } from "url";
6
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
7
- function readDiscoverModulesState() {
8
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
9
- if (existing) {
10
- return existing;
10
+ async function infrastructureFiles(directory, exclude = []) {
11
+ const excluded = new Set([
12
+ "index.ts",
13
+ "index.js",
14
+ "index.mts",
15
+ "index.mjs",
16
+ "register.ts",
17
+ "register.js",
18
+ "register.mts",
19
+ "register.mjs",
20
+ ...exclude
21
+ ]);
22
+ const files = [];
23
+ async function visit(path) {
24
+ const entries = await readdir(path, { withFileTypes: true });
25
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
26
+ for (const entry of entries) {
27
+ const file = join(path, entry.name);
28
+ const key = relative(directory, file).replaceAll("\\", "/");
29
+ if (excluded.has(entry.name) || excluded.has(key))
30
+ continue;
31
+ if (entry.isDirectory())
32
+ await visit(file);
33
+ else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
34
+ files.push(file);
35
+ }
36
+ }
37
+ try {
38
+ await visit(directory);
39
+ } catch (error) {
40
+ if (error.code !== "ENOENT")
41
+ throw error;
42
+ if (error.path !== directory)
43
+ throw error;
11
44
  }
12
- const state = { appModules: [] };
13
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
14
- return state;
45
+ return files;
15
46
  }
16
- function configureModulesDirectory(modulesDir) {
17
- const state = readDiscoverModulesState();
18
- if (state.configuredModulesDir !== modulesDir) {
19
- state.appModules.length = 0;
20
- state.modulesReady = undefined;
47
+ async function importInfrastructure(file) {
48
+ try {
49
+ return await import(pathToFileURL(file).href);
50
+ } catch (error) {
51
+ throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
21
52
  }
22
- state.configuredModulesDir = modulesDir;
23
53
  }
24
- function resolveModulesDirectory(options) {
25
- const state = readDiscoverModulesState();
26
- if (options?.modulesDir) {
27
- return options.modulesDir;
54
+
55
+ // ../../src/bootstrap/discoverModules.ts
56
+ var KEY = Symbol.for("@getstrata/discoverModulesState");
57
+ function state() {
58
+ const globals = globalThis;
59
+ globals[KEY] ??= { appModules: [] };
60
+ return globals[KEY];
61
+ }
62
+ function configureModulesDirectory(modulesDir) {
63
+ const current = state();
64
+ const directory = resolve(modulesDir);
65
+ if (current.configuredModulesDir !== directory || current.configuredManifest) {
66
+ current.appModules.length = 0;
67
+ current.modulesReady = undefined;
68
+ current.source = undefined;
28
69
  }
29
- if (state.configuredModulesDir) {
30
- return state.configuredModulesDir;
70
+ current.configuredManifest = undefined;
71
+ current.configuredModulesDir = directory;
72
+ }
73
+ function configureModulesManifest(manifest) {
74
+ const current = state();
75
+ if (current.configuredManifest !== manifest) {
76
+ current.appModules.length = 0;
77
+ current.modulesReady = undefined;
78
+ current.source = undefined;
31
79
  }
32
- throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
80
+ current.configuredManifest = manifest;
33
81
  }
34
- async function loadDiscoveredModules(options) {
35
- const modulesDirectory = resolveModulesDirectory(options);
36
- let moduleNames;
37
- try {
38
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
39
- } catch (error) {
40
- if (error.code === "ENOENT") {
41
- return [];
82
+ function validateModule(value, location) {
83
+ if (!value || typeof value !== "object" || Array.isArray(value))
84
+ throw new TypeError(`Invalid application module at ${location}.`);
85
+ const module = value;
86
+ if (typeof module.name !== "string" || !module.name.trim())
87
+ throw new TypeError(`Invalid module name at ${location}.`);
88
+ if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
89
+ throw new TypeError(`Invalid module order at ${location}.`);
90
+ for (const hook of ["routes", "webRoutes"])
91
+ if (module[hook] !== undefined && typeof module[hook] !== "function")
92
+ throw new TypeError(`Invalid ${hook} at ${location}.`);
93
+ if (module.providers !== undefined) {
94
+ if (!Array.isArray(module.providers))
95
+ throw new TypeError(`Invalid providers at ${location}.`);
96
+ for (const provider of module.providers) {
97
+ if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
98
+ throw new TypeError(`Invalid provider at ${location}.`);
42
99
  }
43
- throw error;
44
100
  }
45
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
46
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
47
- const loaded = await import(moduleUrl);
48
- return loaded.default;
49
- }));
50
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
51
101
  }
52
- async function ensureModulesLoaded(options) {
53
- const state = readDiscoverModulesState();
54
- if (state.appModules.length > 0) {
55
- return state.appModules;
102
+ async function load(options) {
103
+ const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
104
+ const entries = [];
105
+ if (manifest)
106
+ for (const [index, value] of manifest.entries())
107
+ entries.push({ value, location: `module manifest[${index}]` });
108
+ else {
109
+ const directory = options.modulesDir ?? state().configuredModulesDir;
110
+ if (!directory)
111
+ throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
112
+ let folders;
113
+ try {
114
+ folders = await readdir2(directory, { withFileTypes: true });
115
+ } catch (error) {
116
+ if (error.code === "ENOENT")
117
+ return [];
118
+ throw error;
119
+ }
120
+ const excluded = new Set(options.exclude ?? []);
121
+ 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)) {
122
+ const path = join2(directory, folder.name);
123
+ const files = await readdir2(path);
124
+ const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
125
+ if (candidates.length !== 1)
126
+ throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
127
+ const file = join2(path, candidates[0]);
128
+ const exports = await importInfrastructure(file);
129
+ entries.push({ value: exports.default, location: file });
130
+ }
131
+ }
132
+ const names = new Map;
133
+ const modules = [];
134
+ for (const { value, location } of entries) {
135
+ validateModule(value, location);
136
+ const previous = names.get(value.name);
137
+ if (previous)
138
+ throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
139
+ names.set(value.name, location);
140
+ modules.push(value);
56
141
  }
57
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
58
- state.appModules.splice(0, state.appModules.length, ...modules);
59
- return state.appModules;
142
+ return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
143
+ }
144
+ function ensureModulesLoaded(options = {}) {
145
+ const current = state();
146
+ const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
147
+ options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
148
+ [...options.exclude ?? []].sort()
149
+ ]);
150
+ if (current.modulesReady && current.source === key)
151
+ return current.modulesReady;
152
+ current.appModules.length = 0;
153
+ current.source = key;
154
+ const pending = load(options).then((modules) => {
155
+ if (current.modulesReady !== pending)
156
+ return modules;
157
+ current.appModules.splice(0, current.appModules.length, ...modules);
158
+ return current.appModules;
159
+ }).catch((error) => {
160
+ if (current.modulesReady === pending)
161
+ current.modulesReady = undefined;
162
+ throw error;
60
163
  });
61
- return state.modulesReady;
164
+ current.modulesReady = pending;
165
+ return pending;
62
166
  }
63
167
  function discoverModules() {
64
- return readDiscoverModulesState().appModules;
168
+ return state().appModules;
65
169
  }
66
170
  function resetDiscoverModulesForTests() {
67
- const state = readDiscoverModulesState();
68
- state.configuredModulesDir = undefined;
69
- state.appModules.length = 0;
70
- state.modulesReady = undefined;
171
+ const current = state();
172
+ current.configuredModulesDir = undefined;
173
+ current.configuredManifest = undefined;
174
+ current.appModules.length = 0;
175
+ current.modulesReady = undefined;
176
+ current.source = undefined;
71
177
  }
72
178
 
73
179
  // ../../src/bootstrap/cache/modelCacheTags.ts
@@ -7,8 +7,12 @@ import {
7
7
  CORE_CACHE_TOKEN,
8
8
  CORE_CONFIG_TOKEN,
9
9
  CORE_EVENT_BUS_TOKEN,
10
+ CORE_HTTP_CLEANUP_TOKEN,
10
11
  CORE_POLICY_GATE_TOKEN,
12
+ CORE_PUBLIC_TENANCY_TOKEN,
11
13
  CORE_QUEUE_TOKEN,
14
+ CORE_TENANT_RESOLVER_TOKEN,
15
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
12
16
  CORE_TOKEN_SERVICE_TOKEN
13
17
  } from "@getstrata/core/contracts/serviceTokens";
14
18
  var APP_PORT_CONFIG_KEY = "app.port";
@@ -36,8 +40,12 @@ export {
36
40
  CORE_CACHE_TOKEN,
37
41
  CORE_CONFIG_TOKEN,
38
42
  CORE_EVENT_BUS_TOKEN,
43
+ CORE_HTTP_CLEANUP_TOKEN,
39
44
  CORE_POLICY_GATE_TOKEN,
45
+ CORE_PUBLIC_TENANCY_TOKEN,
40
46
  CORE_QUEUE_TOKEN,
47
+ CORE_TENANT_RESOLVER_TOKEN,
48
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
41
49
  CORE_TOKEN_SERVICE_TOKEN,
42
50
  DATABASE_URL_CONFIG_KEY,
43
51
  DEFAULT_API_TOKEN,