@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
package/dist/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/public-api.ts
3
- import { appSchedule, runDueScheduledTasks, Schedule } from "@getstrata/core/scheduler/schedule";
3
+ import {
4
+ appSchedule,
5
+ RedisSchedulerLeaseStore,
6
+ runDueScheduledTasks,
7
+ Schedule,
8
+ SchedulerLeaseLostError
9
+ } from "@getstrata/core/scheduler/schedule";
4
10
 
5
11
  // ../../src/bootstrap/applicationRegistry.ts
6
12
  import {
@@ -65,8 +71,12 @@ import {
65
71
  CORE_CACHE_TOKEN,
66
72
  CORE_CONFIG_TOKEN,
67
73
  CORE_EVENT_BUS_TOKEN,
74
+ CORE_HTTP_CLEANUP_TOKEN,
68
75
  CORE_POLICY_GATE_TOKEN,
76
+ CORE_PUBLIC_TENANCY_TOKEN,
69
77
  CORE_QUEUE_TOKEN,
78
+ CORE_TENANT_RESOLVER_TOKEN,
79
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
70
80
  CORE_TOKEN_SERVICE_TOKEN
71
81
  } from "@getstrata/core/contracts/serviceTokens";
72
82
  var APP_PORT_CONFIG_KEY = "app.port";
@@ -131,8 +141,39 @@ function resolveRegisterRateLimit() {
131
141
  // ../../src/bootstrap/httpKernel.ts
132
142
  class HttpKernel {
133
143
  dependencies;
144
+ ownedThrottles = new Set;
145
+ disposed = false;
134
146
  constructor(dependencies) {
135
147
  this.dependencies = dependencies;
148
+ if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
149
+ dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
150
+ }
151
+ }
152
+ dispose() {
153
+ this.disposed = true;
154
+ const owned = [...this.ownedThrottles];
155
+ this.ownedThrottles.clear();
156
+ const errors = [];
157
+ for (const throttle of owned.reverse()) {
158
+ try {
159
+ throttle.dispose();
160
+ } catch (error) {
161
+ errors.push(error);
162
+ }
163
+ }
164
+ if (errors.length)
165
+ throw new AggregateError(errors, "HTTP throttle cleanup failed.");
166
+ }
167
+ ownThrottle(throttle) {
168
+ if (this.disposed) {
169
+ if ("dispose" in throttle && typeof throttle.dispose === "function")
170
+ throttle.dispose();
171
+ throw new Error("HTTP kernel is disposed.");
172
+ }
173
+ if ("dispose" in throttle && typeof throttle.dispose === "function") {
174
+ this.ownedThrottles.add(throttle);
175
+ }
176
+ return throttle;
136
177
  }
137
178
  globalMiddleware(errorFormat = "json") {
138
179
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
@@ -147,7 +188,10 @@ class HttpKernel {
147
188
  errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
148
189
  createAuthMiddleware(auth),
149
190
  createMembershipMiddleware(),
150
- createTenantMiddleware()
191
+ createTenantMiddleware({
192
+ publicTenancy: this.dependencies.container.has(CORE_PUBLIC_TENANCY_TOKEN) ? this.dependencies.container.resolve(CORE_PUBLIC_TENANCY_TOKEN) : undefined,
193
+ resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
194
+ })
151
195
  ];
152
196
  }
153
197
  group(name) {
@@ -162,25 +206,28 @@ class HttpKernel {
162
206
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
163
207
  return [csrf];
164
208
  }
209
+ const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
165
210
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
166
211
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
167
212
  if (!redisUrl) {
168
213
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
169
214
  return [
170
- createMemoryThrottleMiddleware({
215
+ this.ownThrottle(createMemoryThrottleMiddleware({
171
216
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
172
- decaySeconds: 60
173
- }),
217
+ decaySeconds: 60,
218
+ quotaPolicy
219
+ })),
174
220
  csrf
175
221
  ];
176
222
  }
177
223
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
178
224
  return [
179
- createThrottleMiddleware({
225
+ this.ownThrottle(createThrottleMiddleware({
180
226
  redisUrl,
181
227
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
182
- decaySeconds: 60
183
- }),
228
+ decaySeconds: 60,
229
+ quotaPolicy
230
+ })),
184
231
  csrf
185
232
  ];
186
233
  }
@@ -217,8 +264,8 @@ class HttpKernel {
217
264
  return handler(request);
218
265
  });
219
266
  }
220
- wrapWebPublicRead(handler) {
221
- if (isPublicReadsEnabled()) {
267
+ wrapWebPublicRead(handler, options = {}) {
268
+ if (options.allowAnonymous ?? isPublicReadsEnabled()) {
222
269
  return this.wrapWeb(handler);
223
270
  }
224
271
  return this.wrapWebAuthenticated(handler);
@@ -264,8 +311,8 @@ class HttpKernel {
264
311
  const middleware = [...this.group("authenticated"), ...this.verifiedMiddleware()];
265
312
  return withMiddleware(...middleware)(handler);
266
313
  }
267
- wrapPublicRead(handler) {
268
- if (isPublicReadsEnabled()) {
314
+ wrapPublicRead(handler, options = {}) {
315
+ if (options.allowAnonymous ?? isPublicReadsEnabled()) {
269
316
  return handler;
270
317
  }
271
318
  return this.wrapAuthenticated(handler);
@@ -310,25 +357,25 @@ class HttpKernel {
310
357
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
311
358
  const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
312
359
  if (scope === "login") {
313
- return withMiddleware(createLoginThrottleMiddleware({
360
+ return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
314
361
  ...redisUrl ? { redisUrl } : {},
315
362
  maxAttempts: rateLimit.maxAttempts,
316
363
  decaySeconds: rateLimit.decaySeconds
317
- }))(handler);
364
+ })))(handler);
318
365
  }
319
366
  if (redisUrl) {
320
- return withMiddleware(createThrottleMiddleware({
367
+ return withMiddleware(this.ownThrottle(createThrottleMiddleware({
321
368
  redisUrl,
322
369
  maxAttempts: rateLimit.maxAttempts,
323
370
  decaySeconds: rateLimit.decaySeconds,
324
371
  keyPrefix: memoryKeyPrefix
325
- }))(handler);
372
+ })))(handler);
326
373
  }
327
- return withMiddleware(createMemoryThrottleMiddleware({
374
+ return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
328
375
  maxAttempts: rateLimit.maxAttempts,
329
376
  decaySeconds: rateLimit.decaySeconds,
330
377
  keyPrefix: memoryKeyPrefix
331
- }))(handler);
378
+ })))(handler);
332
379
  }
333
380
  }
334
381
  function createHttpKernel(dependencies) {
@@ -336,68 +383,172 @@ function createHttpKernel(dependencies) {
336
383
  }
337
384
 
338
385
  // ../../src/bootstrap/discoverModules.ts
339
- import { readdirSync } from "fs";
340
- import { join } from "path";
386
+ import { readdir as readdir2 } from "fs/promises";
387
+ import { join as join2, resolve } from "path";
388
+
389
+ // ../../src/bootstrap/infrastructureDiscovery.ts
390
+ import { readdir } from "fs/promises";
391
+ import { join, relative } from "path";
341
392
  import { pathToFileURL } from "url";
342
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
343
- function readDiscoverModulesState() {
344
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
345
- if (existing) {
346
- return existing;
393
+ async function infrastructureFiles(directory, exclude = []) {
394
+ const excluded = new Set([
395
+ "index.ts",
396
+ "index.js",
397
+ "index.mts",
398
+ "index.mjs",
399
+ "register.ts",
400
+ "register.js",
401
+ "register.mts",
402
+ "register.mjs",
403
+ ...exclude
404
+ ]);
405
+ const files = [];
406
+ async function visit(path) {
407
+ const entries = await readdir(path, { withFileTypes: true });
408
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
409
+ for (const entry of entries) {
410
+ const file = join(path, entry.name);
411
+ const key = relative(directory, file).replaceAll("\\", "/");
412
+ if (excluded.has(entry.name) || excluded.has(key))
413
+ continue;
414
+ if (entry.isDirectory())
415
+ await visit(file);
416
+ else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
417
+ files.push(file);
418
+ }
347
419
  }
348
- const state = { appModules: [] };
349
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
350
- return state;
351
- }
352
- function configureModulesDirectory(modulesDir) {
353
- const state = readDiscoverModulesState();
354
- if (state.configuredModulesDir !== modulesDir) {
355
- state.appModules.length = 0;
356
- state.modulesReady = undefined;
420
+ try {
421
+ await visit(directory);
422
+ } catch (error) {
423
+ if (error.code !== "ENOENT")
424
+ throw error;
425
+ if (error.path !== directory)
426
+ throw error;
357
427
  }
358
- state.configuredModulesDir = modulesDir;
428
+ return files;
359
429
  }
360
- function resolveModulesDirectory(options) {
361
- const state = readDiscoverModulesState();
362
- if (options?.modulesDir) {
363
- return options.modulesDir;
430
+ async function importInfrastructure(file) {
431
+ try {
432
+ return await import(pathToFileURL(file).href);
433
+ } catch (error) {
434
+ throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
364
435
  }
365
- if (state.configuredModulesDir) {
366
- return state.configuredModulesDir;
436
+ }
437
+
438
+ // ../../src/bootstrap/discoverModules.ts
439
+ var KEY = Symbol.for("@getstrata/discoverModulesState");
440
+ function state() {
441
+ const globals = globalThis;
442
+ globals[KEY] ??= { appModules: [] };
443
+ return globals[KEY];
444
+ }
445
+ function configureModulesDirectory(modulesDir) {
446
+ const current = state();
447
+ const directory = resolve(modulesDir);
448
+ if (current.configuredModulesDir !== directory || current.configuredManifest) {
449
+ current.appModules.length = 0;
450
+ current.modulesReady = undefined;
451
+ current.source = undefined;
452
+ }
453
+ current.configuredManifest = undefined;
454
+ current.configuredModulesDir = directory;
455
+ }
456
+ function configureModulesManifest(manifest) {
457
+ const current = state();
458
+ if (current.configuredManifest !== manifest) {
459
+ current.appModules.length = 0;
460
+ current.modulesReady = undefined;
461
+ current.source = undefined;
462
+ }
463
+ current.configuredManifest = manifest;
464
+ }
465
+ function validateModule(value, location) {
466
+ if (!value || typeof value !== "object" || Array.isArray(value))
467
+ throw new TypeError(`Invalid application module at ${location}.`);
468
+ const module = value;
469
+ if (typeof module.name !== "string" || !module.name.trim())
470
+ throw new TypeError(`Invalid module name at ${location}.`);
471
+ if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
472
+ throw new TypeError(`Invalid module order at ${location}.`);
473
+ for (const hook of ["routes", "webRoutes"])
474
+ if (module[hook] !== undefined && typeof module[hook] !== "function")
475
+ throw new TypeError(`Invalid ${hook} at ${location}.`);
476
+ if (module.providers !== undefined) {
477
+ if (!Array.isArray(module.providers))
478
+ throw new TypeError(`Invalid providers at ${location}.`);
479
+ for (const provider of module.providers) {
480
+ if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
481
+ throw new TypeError(`Invalid provider at ${location}.`);
482
+ }
367
483
  }
368
- throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
369
484
  }
370
- async function loadDiscoveredModules(options) {
371
- const modulesDirectory = resolveModulesDirectory(options);
372
- let moduleNames;
373
- try {
374
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
375
- } catch (error) {
376
- if (error.code === "ENOENT") {
377
- return [];
485
+ async function load(options) {
486
+ const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
487
+ const entries = [];
488
+ if (manifest)
489
+ for (const [index, value] of manifest.entries())
490
+ entries.push({ value, location: `module manifest[${index}]` });
491
+ else {
492
+ const directory = options.modulesDir ?? state().configuredModulesDir;
493
+ if (!directory)
494
+ throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
495
+ let folders;
496
+ try {
497
+ folders = await readdir2(directory, { withFileTypes: true });
498
+ } catch (error) {
499
+ if (error.code === "ENOENT")
500
+ return [];
501
+ throw error;
502
+ }
503
+ const excluded = new Set(options.exclude ?? []);
504
+ 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)) {
505
+ const path = join2(directory, folder.name);
506
+ const files = await readdir2(path);
507
+ const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
508
+ if (candidates.length !== 1)
509
+ throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
510
+ const file = join2(path, candidates[0]);
511
+ const exports = await importInfrastructure(file);
512
+ entries.push({ value: exports.default, location: file });
378
513
  }
379
- throw error;
380
514
  }
381
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
382
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
383
- const loaded = await import(moduleUrl);
384
- return loaded.default;
385
- }));
386
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
387
- }
388
- async function ensureModulesLoaded(options) {
389
- const state = readDiscoverModulesState();
390
- if (state.appModules.length > 0) {
391
- return state.appModules;
392
- }
393
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
394
- state.appModules.splice(0, state.appModules.length, ...modules);
395
- return state.appModules;
515
+ const names = new Map;
516
+ const modules = [];
517
+ for (const { value, location } of entries) {
518
+ validateModule(value, location);
519
+ const previous = names.get(value.name);
520
+ if (previous)
521
+ throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
522
+ names.set(value.name, location);
523
+ modules.push(value);
524
+ }
525
+ return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
526
+ }
527
+ function ensureModulesLoaded(options = {}) {
528
+ const current = state();
529
+ const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
530
+ options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
531
+ [...options.exclude ?? []].sort()
532
+ ]);
533
+ if (current.modulesReady && current.source === key)
534
+ return current.modulesReady;
535
+ current.appModules.length = 0;
536
+ current.source = key;
537
+ const pending = load(options).then((modules) => {
538
+ if (current.modulesReady !== pending)
539
+ return modules;
540
+ current.appModules.splice(0, current.appModules.length, ...modules);
541
+ return current.appModules;
542
+ }).catch((error) => {
543
+ if (current.modulesReady === pending)
544
+ current.modulesReady = undefined;
545
+ throw error;
396
546
  });
397
- return state.modulesReady;
547
+ current.modulesReady = pending;
548
+ return pending;
398
549
  }
399
550
  function discoverModules() {
400
- return readDiscoverModulesState().appModules;
551
+ return state().appModules;
401
552
  }
402
553
  // ../../src/bootstrap/prefixRouteMap.ts
403
554
  function prefixRouteMap(prefix, routes) {
@@ -527,6 +678,10 @@ function cacheTagsForModelWrite(tableName, action) {
527
678
  function discoverModelTableNames() {
528
679
  return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
529
680
  }
681
+ // ../../src/bootstrap/context.ts
682
+ import { CORE_HTTP_CLEANUP_TOKEN as CORE_HTTP_CLEANUP_TOKEN2 } from "@getstrata/core/contracts/serviceTokens";
683
+ import { clearActiveApplicationContext } from "@getstrata/core/runtime/applicationRegistry";
684
+
530
685
  // ../../src/bootstrap/contracts.ts
531
686
  import {
532
687
  ConfigStore,
@@ -587,7 +742,7 @@ import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
587
742
  import { CacheRepository } from "@getstrata/core/cache/repository";
588
743
  var cacheProvider = {
589
744
  name: "core.cache",
590
- register({ container, config, dependencies }) {
745
+ register({ container, config, dependencies, onCleanup }) {
591
746
  container.singleton(CORE_CACHE_TOKEN, () => {
592
747
  const store = createCacheStore({
593
748
  driver: config.require(CACHE_DRIVER_CONFIG_KEY),
@@ -595,7 +750,9 @@ var cacheProvider = {
595
750
  maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
596
751
  redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
597
752
  });
598
- return new CacheRepository(store);
753
+ const cache = new CacheRepository(store);
754
+ onCleanup(() => cache.close());
755
+ return cache;
599
756
  });
600
757
  dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
601
758
  }
@@ -792,49 +949,47 @@ var eventsProvider = {
792
949
  var events_default = eventsProvider;
793
950
 
794
951
  // ../../src/bootstrap/discoverListeners.ts
795
- import { existsSync, readdirSync as readdirSync2 } from "fs";
796
- import { createRequire } from "module";
797
- import { join as join2 } from "path";
798
- var requireListener = createRequire(import.meta.url);
799
- var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
800
- function readDiscoverListenersState() {
801
- const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
802
- if (existing) {
803
- return existing;
804
- }
805
- const state = {};
806
- globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
807
- return state;
808
- }
809
- function resolveListenersDirectory() {
810
- const fromCwd = join2(process.cwd(), "src", "listeners");
811
- if (existsSync(fromCwd)) {
812
- return fromCwd;
813
- }
814
- return join2(import.meta.dir, "../listeners");
815
- }
816
- function loadDiscoveredListeners() {
817
- const listenersDirectory = resolveListenersDirectory();
818
- let entries;
819
- try {
820
- entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
821
- } catch (error) {
822
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
823
- return [];
952
+ import { join as join3, resolve as resolve2 } from "path";
953
+ var KEY2 = Symbol.for("@getstrata/discoverListenersState");
954
+ function state2() {
955
+ const globals = globalThis;
956
+ globals[KEY2] ??= { loads: new Map };
957
+ return globals[KEY2];
958
+ }
959
+ async function load2(options) {
960
+ const listeners = new Set;
961
+ const add = (value, location) => {
962
+ if (typeof value !== "function")
963
+ throw new TypeError(`Invalid listener registrar at ${location}: expected a default function.`);
964
+ listeners.add(value);
965
+ };
966
+ if (options.manifest)
967
+ for (const [index, value] of options.manifest.entries())
968
+ add(value, `listener manifest[${index}]`);
969
+ else
970
+ for (const file of await infrastructureFiles(options.directory ?? join3(process.cwd(), "src", "listeners"), options.exclude)) {
971
+ const exports = await importInfrastructure(file);
972
+ const value = exports.default;
973
+ add(value && typeof value === "object" && "default" in value ? value.default : value, file);
824
974
  }
975
+ return [...listeners];
976
+ }
977
+ function discoverListeners(options = {}) {
978
+ const cache = state2().loads;
979
+ const key = options.manifest ?? JSON.stringify([
980
+ resolve2(options.directory ?? join3(process.cwd(), "src", "listeners")),
981
+ [...options.exclude ?? []].sort()
982
+ ]);
983
+ const existing = cache.get(key);
984
+ if (existing)
985
+ return existing;
986
+ const pending = load2(options).catch((error) => {
987
+ if (cache.get(key) === pending)
988
+ cache.delete(key);
825
989
  throw error;
826
- }
827
- const listeners = entries.map((fileName) => {
828
- const filePath = join2(listenersDirectory, fileName);
829
- const loaded = requireListener(filePath);
830
- return loaded.default;
831
990
  });
832
- return listeners.filter((listener) => typeof listener === "function");
833
- }
834
- function discoverListeners() {
835
- const state = readDiscoverListenersState();
836
- state.appListeners ??= loadDiscoveredListeners();
837
- return state.appListeners;
991
+ cache.set(key, pending);
992
+ return pending;
838
993
  }
839
994
 
840
995
  // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
@@ -927,22 +1082,12 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options =
927
1082
  }
928
1083
 
929
1084
  // ../../src/bootstrap/providers/listeners.ts
930
- var registeredListenerGroups = new Set;
931
- function registerListenerGroup(name, register) {
932
- if (registeredListenerGroups.has(name)) {
933
- return;
934
- }
935
- registeredListenerGroups.add(name);
936
- register();
937
- }
938
1085
  var listenersProvider = {
939
1086
  name: "core.listeners",
940
- boot() {
941
- registerListenerGroup("cache.invalidate-on-model-write", () => {
942
- registerInvalidateCacheOnModelWriteListeners();
943
- });
944
- for (const registerListener of discoverListeners()) {
945
- registerListener();
1087
+ async boot({ onCleanup }) {
1088
+ onCleanup(registerInvalidateCacheOnModelWriteListeners());
1089
+ for (const registerListener of await discoverListeners()) {
1090
+ await registerListener();
946
1091
  }
947
1092
  }
948
1093
  };
@@ -966,68 +1111,99 @@ import {
966
1111
  } from "@getstrata/core/queue/createAppQueue";
967
1112
 
968
1113
  // ../../src/bootstrap/discoverJobs.ts
969
- import { existsSync as existsSync2, readdirSync as readdirSync3 } from "fs";
970
- import { createRequire as createRequire2 } from "module";
971
- import { join as join3 } from "path";
1114
+ import { join as join4, resolve as resolve3 } from "path";
1115
+ import { Job } from "@getstrata/core/queue";
972
1116
  import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
973
- var requireJob = createRequire2(import.meta.url);
974
- var DISCOVER_JOBS_STATE_KEY = Symbol.for("@getstrata/discoverJobsState");
975
- function readDiscoverJobsState() {
976
- const existing = globalThis[DISCOVER_JOBS_STATE_KEY];
977
- if (existing) {
978
- return existing;
979
- }
980
- const state = {};
981
- globalThis[DISCOVER_JOBS_STATE_KEY] = state;
982
- return state;
983
- }
984
- function resolveJobsDirectory() {
985
- const fromCwd = join3(process.cwd(), "src", "jobs");
986
- if (existsSync2(fromCwd)) {
987
- return fromCwd;
1117
+ var KEY3 = Symbol.for("@getstrata/discoverJobsState");
1118
+ function state3() {
1119
+ const globals = globalThis;
1120
+ globals[KEY3] ??= { loads: new Map, registered: new Map };
1121
+ return globals[KEY3];
1122
+ }
1123
+ function validateClass(value, location) {
1124
+ if (typeof value !== "function" || typeof value.jobName !== "string" || !value.jobName.trim() || !(value.prototype instanceof Job) && typeof value.prototype?.handle !== "function")
1125
+ throw new TypeError(`Invalid job at ${location}: expected a class with nonempty static jobName and handle().`);
1126
+ }
1127
+ async function load3(options, current) {
1128
+ const jobs = new Map;
1129
+ const add = (name, identity, create, location) => {
1130
+ const previous = jobs.get(name);
1131
+ if (previous && previous.identity !== identity)
1132
+ throw new Error(`Duplicate job "${name}" at ${location}; first declared at ${previous.location}.`);
1133
+ if (!previous)
1134
+ jobs.set(name, { identity, create, location });
1135
+ };
1136
+ if (options.manifest) {
1137
+ for (const [index, entry] of options.manifest.entries()) {
1138
+ const location = `job manifest[${index}]`;
1139
+ if (typeof entry === "function") {
1140
+ validateClass(entry, location);
1141
+ add(entry.jobName, entry, () => new entry, location);
1142
+ } else {
1143
+ if (!entry || typeof entry.name !== "string" || !entry.name.trim() || typeof entry.create !== "function")
1144
+ throw new TypeError(`Invalid job factory at ${location}.`);
1145
+ add(entry.name, entry.create, entry.create, location);
1146
+ }
1147
+ }
1148
+ } else {
1149
+ for (const file of await infrastructureFiles(options.directory ?? join4(process.cwd(), "src", "jobs"), options.exclude)) {
1150
+ const exports = await importInfrastructure(file);
1151
+ const defaultValue = exports.default;
1152
+ const legacy = defaultValue && typeof defaultValue === "object" && "default" in defaultValue ? defaultValue.default : defaultValue;
1153
+ if (legacy !== undefined)
1154
+ validateClass(legacy, `${file}:default`);
1155
+ let found = false;
1156
+ for (const [name, value] of Object.entries({
1157
+ ...exports,
1158
+ ...legacy === undefined ? {} : { default: legacy }
1159
+ })) {
1160
+ if (name !== "default" && !(typeof value === "function" && ("jobName" in value)))
1161
+ continue;
1162
+ validateClass(value, `${file}:${name}`);
1163
+ found = true;
1164
+ add(value.jobName, value, () => new value, `${file}:${name}`);
1165
+ }
1166
+ if (!found)
1167
+ throw new TypeError(`No job exports at ${file}; exclude helper modules explicitly.`);
1168
+ }
988
1169
  }
989
- return join3(import.meta.dir, "../jobs");
990
- }
991
- function isDiscoveredJobClass(value) {
992
- if (typeof value !== "function") {
993
- return false;
1170
+ if (!current())
1171
+ return [...jobs.keys()];
1172
+ const registered = state3().registered;
1173
+ for (const [name, entry] of jobs) {
1174
+ const previous = registered.get(name);
1175
+ if (previous && previous.identity !== entry.identity)
1176
+ throw new Error(`Duplicate discovered job "${name}" at ${entry.location}; first declared at ${previous.location}.`);
1177
+ }
1178
+ const existing = new Set(jobRegistry.names());
1179
+ for (const [name, entry] of jobs) {
1180
+ if (!existing.has(name)) {
1181
+ jobRegistry.register(name, entry.create);
1182
+ registered.set(name, entry);
1183
+ }
994
1184
  }
995
- const jobName = value.jobName;
996
- return typeof jobName === "string" && jobName.length > 0;
1185
+ return [...jobs.keys()];
997
1186
  }
998
- function loadDiscoveredJobClasses() {
999
- const jobsDirectory = resolveJobsDirectory();
1000
- let entries;
1001
- try {
1002
- entries = readdirSync3(jobsDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
1003
- } catch (error) {
1004
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1005
- return [];
1006
- }
1187
+ function discoverJobs(options = {}) {
1188
+ const cache = state3().loads;
1189
+ const key = options.manifest ?? JSON.stringify([
1190
+ resolve3(options.directory ?? join4(process.cwd(), "src", "jobs")),
1191
+ [...options.exclude ?? []].sort()
1192
+ ]);
1193
+ const existing = cache.get(key);
1194
+ if (existing)
1195
+ return existing;
1196
+ const pending = load3(options, () => state3().loads === cache).catch((error) => {
1197
+ if (cache.get(key) === pending)
1198
+ cache.delete(key);
1007
1199
  throw error;
1008
- }
1009
- return entries.flatMap((fileName) => {
1010
- const filePath = join3(jobsDirectory, fileName);
1011
- const loaded = requireJob(filePath);
1012
- return isDiscoveredJobClass(loaded.default) ? [loaded.default] : [];
1013
1200
  });
1014
- }
1015
- function discoverJobs() {
1016
- const state = readDiscoverJobsState();
1017
- if (state.names) {
1018
- return state.names;
1019
- }
1020
- const names = [];
1021
- for (const JobClass of loadDiscoveredJobClasses()) {
1022
- jobRegistry.register(JobClass.jobName, () => new JobClass);
1023
- names.push(JobClass.jobName);
1024
- }
1025
- state.names = names;
1026
- return names;
1201
+ cache.set(key, pending);
1202
+ return pending;
1027
1203
  }
1028
1204
  function resetDiscoverJobsForTests() {
1029
- const state = readDiscoverJobsState();
1030
- state.names = undefined;
1205
+ state3().loads = new Map;
1206
+ state3().registered = new Map;
1031
1207
  }
1032
1208
 
1033
1209
  // ../../src/bootstrap/queue/defaultJobs.ts
@@ -1045,16 +1221,17 @@ function registerDefaultJobs() {
1045
1221
  // ../../src/bootstrap/providers/queue.ts
1046
1222
  var queueProvider = {
1047
1223
  name: "core.queue",
1048
- register({ container, config }) {
1224
+ async register({ container, config, onCleanup }) {
1049
1225
  const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
1050
1226
  const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
1051
1227
  config.set("queue.driver", driver);
1052
1228
  const failedJobs = createFailedJobService();
1053
1229
  container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
1054
- container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, () => {
1055
- registerDefaultJobs();
1056
- discoverJobs();
1057
- }));
1230
+ registerDefaultJobs();
1231
+ await discoverJobs();
1232
+ const queue = createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs);
1233
+ onCleanup(() => queue.close?.(), driver === "async" ? "drain" : "close");
1234
+ container.set(CORE_QUEUE_TOKEN, queue);
1058
1235
  }
1059
1236
  };
1060
1237
  var queue_default = queueProvider;
@@ -1069,6 +1246,18 @@ var storageProvider = {
1069
1246
  };
1070
1247
  var storage_default = storageProvider;
1071
1248
 
1249
+ // ../../src/bootstrap/providers/tracing.ts
1250
+ import { acquireTracingRuntime } from "@getstrata/core/tracing/tracingMiddleware";
1251
+ var tracingProvider = {
1252
+ name: "core.tracing",
1253
+ register({ onCleanup }) {
1254
+ const ownership = acquireTracingRuntime();
1255
+ onCleanup(() => ownership.flush(), "flush");
1256
+ onCleanup(() => ownership.release(), "close");
1257
+ }
1258
+ };
1259
+ var tracing_default = tracingProvider;
1260
+
1072
1261
  // ../../src/bootstrap/providers/view.ts
1073
1262
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
1074
1263
  import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
@@ -1113,6 +1302,7 @@ var viewProvider = {
1113
1302
  // ../../src/bootstrap/providers/index.ts
1114
1303
  var coreProviders = [
1115
1304
  config_default,
1305
+ tracing_default,
1116
1306
  cache_default,
1117
1307
  storage_default,
1118
1308
  auth_default,
@@ -1127,36 +1317,127 @@ var coreProviders = [
1127
1317
  function collectProviders(modules = discoverModules()) {
1128
1318
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
1129
1319
  }
1130
- function runProviderPhase(providers, phase, context) {
1131
- for (const provider of providers) {
1132
- provider[phase]?.(context);
1133
- }
1320
+ async function runProviderPhase(providers, phase, context) {
1321
+ for (const provider of providers)
1322
+ await provider[phase]?.(context);
1134
1323
  }
1135
- function createAppContext() {
1324
+ var readyContext;
1325
+ async function createAppContext(providers) {
1326
+ if (!providers)
1327
+ await ensureModulesLoaded();
1328
+ const configured = [...providers ?? collectProviders()];
1136
1329
  const container = new ServiceContainer;
1137
1330
  const config = new ConfigStore;
1138
- const dependencies = {
1139
- container
1331
+ const dependencies = { container };
1332
+ const cleanups = [];
1333
+ const drains = [];
1334
+ let draining;
1335
+ const drain = () => {
1336
+ draining ??= (async () => {
1337
+ const errors = [];
1338
+ for (const handler of drains.splice(0).reverse()) {
1339
+ try {
1340
+ await handler();
1341
+ } catch (error) {
1342
+ errors.push(error);
1343
+ }
1344
+ }
1345
+ if (errors.length)
1346
+ throw new AggregateError(errors, "Provider drain failed.");
1347
+ })();
1348
+ return draining;
1140
1349
  };
1141
- const context = {
1142
- container,
1143
- config,
1144
- dependencies
1350
+ const flushes = [];
1351
+ let flushing;
1352
+ const flush = () => {
1353
+ flushing ??= (async () => {
1354
+ const errors = [];
1355
+ for (const handler of flushes.splice(0).reverse()) {
1356
+ try {
1357
+ await handler();
1358
+ } catch (error) {
1359
+ errors.push(error);
1360
+ }
1361
+ }
1362
+ if (errors.length)
1363
+ throw new AggregateError(errors, "Provider flush failed.");
1364
+ })();
1365
+ return flushing;
1366
+ };
1367
+ let disposal;
1368
+ let disposed = false;
1369
+ let initialized;
1370
+ const dispose = () => {
1371
+ disposed = true;
1372
+ disposal ??= (async () => {
1373
+ await drain();
1374
+ const errors = [];
1375
+ try {
1376
+ await flush();
1377
+ } catch (error) {
1378
+ errors.push(error);
1379
+ }
1380
+ if (initialized) {
1381
+ clearActiveApplicationContext(initialized);
1382
+ if (readyContext === initialized)
1383
+ readyContext = undefined;
1384
+ }
1385
+ for (const handler of cleanups.splice(0).reverse()) {
1386
+ try {
1387
+ await handler();
1388
+ } catch (error) {
1389
+ errors.push(error);
1390
+ }
1391
+ }
1392
+ if (errors.length)
1393
+ throw new AggregateError(errors, "Provider cleanup failed.");
1394
+ })();
1395
+ return disposal;
1145
1396
  };
1146
- const providers = collectProviders();
1147
- runProviderPhase(providers, "register", context);
1148
- runProviderPhase(providers, "boot", context);
1149
- assertAppDependenciesComplete(dependencies);
1150
- const appContext = {
1397
+ const context = {
1151
1398
  container,
1152
1399
  config,
1153
- dependencies
1400
+ dependencies,
1401
+ onCleanup(handler, phase = "close") {
1402
+ if (disposed)
1403
+ throw new Error("Cannot register cleanup on a disposed application context.");
1404
+ if (phase !== "drain" && phase !== "flush" && phase !== "close")
1405
+ throw new TypeError("Unknown provider cleanup phase.");
1406
+ if (phase === "drain" && draining || phase === "flush" && flushing)
1407
+ throw new Error("Cannot register cleanup after its phase started.");
1408
+ (phase === "drain" ? drains : phase === "flush" ? flushes : cleanups).push(handler);
1409
+ }
1154
1410
  };
1155
- setActiveApplicationContext(appContext);
1156
- return appContext;
1411
+ container.set(CORE_HTTP_CLEANUP_TOKEN2, (handler) => context.onCleanup(handler));
1412
+ try {
1413
+ await runProviderPhase(configured, "register", context);
1414
+ await runProviderPhase(configured, "boot", context);
1415
+ assertAppDependenciesComplete(dependencies);
1416
+ const appContext = {
1417
+ container,
1418
+ config,
1419
+ dependencies,
1420
+ drain,
1421
+ flush,
1422
+ dispose
1423
+ };
1424
+ initialized = appContext;
1425
+ setActiveApplicationContext(appContext);
1426
+ readyContext = appContext;
1427
+ return appContext;
1428
+ } catch (error) {
1429
+ try {
1430
+ await dispose();
1431
+ } catch (cleanupError) {
1432
+ throw new AggregateError([error, cleanupError], "Application startup and cleanup failed.", {
1433
+ cause: error
1434
+ });
1435
+ }
1436
+ throw error;
1437
+ }
1157
1438
  }
1158
1439
  // ../../src/bootstrap/createWebRoutes.ts
1159
- import { join as join4 } from "path";
1440
+ import { join as join5 } from "path";
1160
1441
  import { isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
1161
1442
  import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
1162
1443
  import { notFoundHtmlResponse } from "@getstrata/core/view";
@@ -1173,7 +1454,7 @@ function createWebRoutes(dependencies, options = {}) {
1173
1454
  registerRoute("GET", "/assets/*", ["global", "web"]);
1174
1455
  const pathname = new URL(request.url).pathname;
1175
1456
  try {
1176
- const filePath = assertUrlPathUnderRoot(join4(process.cwd(), "public"), pathname);
1457
+ const filePath = assertUrlPathUnderRoot(join5(process.cwd(), "public"), pathname);
1177
1458
  const file = Bun.file(filePath);
1178
1459
  if (!await file.exists()) {
1179
1460
  return notFoundHtmlResponse();
@@ -1196,8 +1477,43 @@ function mergeWebRoutes(dependencies, routes, options = {}) {
1196
1477
  };
1197
1478
  }
1198
1479
  // ../../src/bootstrap/dependencies.ts
1199
- function createAppDependencies() {
1200
- return createAppContext().dependencies;
1480
+ async function createAppDependencies() {
1481
+ return (await createAppContext()).dependencies;
1482
+ }
1483
+ // ../../src/bootstrap/discoverModels.ts
1484
+ import { readdir as readdir3 } from "fs/promises";
1485
+ import { join as join6 } from "path";
1486
+ import { pathToFileURL as pathToFileURL2 } from "url";
1487
+ import { bootModels, Model } from "@getstrata/core/database/model";
1488
+ async function discoverModels(directory = join6(process.cwd(), "src", "models"), options = {}) {
1489
+ const excluded = new Set([
1490
+ "register.ts",
1491
+ "register.js",
1492
+ "index.ts",
1493
+ "index.js",
1494
+ ...options.exclude ?? []
1495
+ ]);
1496
+ const models = new Set;
1497
+ async function visit(path) {
1498
+ const entries = await readdir3(path, { withFileTypes: true });
1499
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1500
+ for (const entry of entries) {
1501
+ const file = join6(path, entry.name);
1502
+ if (entry.isDirectory())
1503
+ await visit(file);
1504
+ else if (!excluded.has(entry.name) && entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.d\.[cm]?ts$/.test(entry.name)) {
1505
+ const exports = await import(pathToFileURL2(file).href);
1506
+ for (const value of Object.values(exports)) {
1507
+ if (typeof value === "function" && value.prototype instanceof Model)
1508
+ models.add(value);
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+ await visit(directory);
1514
+ const result = [...models];
1515
+ bootModels(result);
1516
+ return result;
1201
1517
  }
1202
1518
  // ../../src/bootstrap/health.ts
1203
1519
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
@@ -1429,8 +1745,8 @@ function assertFeatureProductionSecrets(env) {
1429
1745
  throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
1430
1746
  }
1431
1747
  }
1432
- if (envFlagEnabled3(env.FEATURE_PUBLIC_READS)) {
1433
- throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
1748
+ if (envFlagEnabled3(env.TENANT_DEV_HEADERS)) {
1749
+ throw new Error("Production startup blocked: set TENANT_DEV_HEADERS=false.");
1434
1750
  }
1435
1751
  if (!env.SIEM_EXPORT_URL?.trim() && envFlagEnabled3(env.FEATURE_SIEM_EXPORT)) {
1436
1752
  console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
@@ -1580,6 +1896,7 @@ function createRouteKernel(dependencies) {
1580
1896
  return createHttpKernel(dependencies);
1581
1897
  }
1582
1898
  // ../../src/bootstrap/web/server.ts
1899
+ import { resolveMaxBodyBytes } from "@getstrata/core/http/bodySizeLimitMiddleware";
1583
1900
  import { createCorsMiddleware as createCorsMiddleware2 } from "@getstrata/core/http/corsMiddleware";
1584
1901
  import { currentRequestMeta as currentRequestMeta2, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
1585
1902
  import { assertUrlPathUnderRoot as assertUrlPathUnderRoot2 } from "@getstrata/core/security/safePath";
@@ -1591,29 +1908,33 @@ function socketAddress(server, request) {
1591
1908
  async function missingHtmlResponse() {
1592
1909
  return notFoundHtmlResponse2();
1593
1910
  }
1594
- function wrapRouteHandler(handler, path) {
1911
+ function wrapRouteHandler(handler, path, onRequest, track = (work) => work()) {
1595
1912
  return async (request, server) => {
1596
- return await runWithRequestMeta({
1913
+ return await track(async () => await runWithRequestMeta({
1597
1914
  ...currentRequestMeta2(),
1598
1915
  request,
1599
1916
  routeTemplate: path,
1600
1917
  ipAddress: socketAddress(server, request),
1918
+ peerAddress: socketAddress(server, request),
1601
1919
  userAgent: request.headers.get("user-agent")
1602
1920
  }, async () => {
1921
+ const intercepted = await onRequest?.(request, server);
1922
+ if (intercepted instanceof Response)
1923
+ return intercepted;
1603
1924
  const response = await handler(request);
1604
1925
  return response ?? await missingHtmlResponse();
1605
- });
1926
+ }));
1606
1927
  };
1607
1928
  }
1608
- function convertAppRoutesToBunRoutes(routes, cors = {}) {
1929
+ function convertAppRoutesToBunRoutes(routes, cors = {}, onRequest, track) {
1609
1930
  const bunRoutes = {};
1610
1931
  const middleware = createCorsMiddleware2(cors);
1611
1932
  const preflight = (request) => middleware(request, async () => new Response(null, { status: 405 }));
1612
1933
  for (const [path, handler] of Object.entries(routes)) {
1613
1934
  if (typeof handler === "function") {
1614
1935
  bunRoutes[path] = {
1615
- GET: wrapRouteHandler(handler, path),
1616
- OPTIONS: wrapRouteHandler(preflight, path)
1936
+ GET: wrapRouteHandler(handler, path, onRequest, track),
1937
+ OPTIONS: wrapRouteHandler(preflight, path, onRequest, track)
1617
1938
  };
1618
1939
  continue;
1619
1940
  }
@@ -1623,10 +1944,10 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
1623
1944
  if (typeof methodHandler !== "function") {
1624
1945
  continue;
1625
1946
  }
1626
- methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path);
1947
+ methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path, onRequest, track);
1627
1948
  }
1628
1949
  if (Object.keys(methods).length > 0) {
1629
- methods.OPTIONS ??= wrapRouteHandler(preflight, path);
1950
+ methods.OPTIONS ??= wrapRouteHandler(preflight, path, onRequest, track);
1630
1951
  bunRoutes[path] = methods;
1631
1952
  }
1632
1953
  }
@@ -1634,19 +1955,48 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
1634
1955
  return bunRoutes;
1635
1956
  }
1636
1957
  function createWebServer(options) {
1958
+ if (options.lifecycle?.isShuttingDown)
1959
+ throw new Error("Cannot start HTTP during shutdown.");
1960
+ const onRequest = (request, server) => {
1961
+ if (options.lifecycle?.isShuttingDown)
1962
+ return new Response("Service Unavailable", { status: 503 });
1963
+ return options.onRequest?.(request, server);
1964
+ };
1965
+ for (const field of ["fetch", "routes", "port", "unix"])
1966
+ if (options.native && field in options.native)
1967
+ throw new TypeError(`native.${field} is owned by createWebServer.`);
1968
+ const maxRequestBodySize = options.native?.maxRequestBodySize ?? resolveMaxBodyBytes();
1969
+ if (!Number.isSafeInteger(maxRequestBodySize) || maxRequestBodySize <= 0)
1970
+ throw new RangeError("maxRequestBodySize must be a positive safe integer.");
1971
+ const active = new Set;
1972
+ const track = (work) => {
1973
+ if (!options.lifecycle)
1974
+ return work();
1975
+ const running = work();
1976
+ active.add(running);
1977
+ return running.finally(() => {
1978
+ active.delete(running);
1979
+ });
1980
+ };
1637
1981
  const publicDir = options.publicDir ?? "./public";
1638
- const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors) : undefined;
1639
- return Bun.serve({
1982
+ const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors, onRequest, track) : undefined;
1983
+ const { websocket, ...native } = options.native ?? {};
1984
+ const configuration = {
1985
+ ...native,
1986
+ maxRequestBodySize,
1640
1987
  port: options.port,
1641
1988
  ...bunRoutes ? { routes: bunRoutes } : {},
1642
1989
  async fetch(request, server) {
1643
- return await runWithRequestMeta({
1990
+ return await track(async () => await runWithRequestMeta({
1644
1991
  ...currentRequestMeta2(),
1645
1992
  request,
1646
1993
  ipAddress: socketAddress(server, request),
1994
+ peerAddress: socketAddress(server, request),
1647
1995
  userAgent: request.headers.get("user-agent")
1648
1996
  }, async () => {
1649
- await options.onRequest?.(request);
1997
+ const intercepted = await onRequest(request, server);
1998
+ if (intercepted instanceof Response)
1999
+ return intercepted;
1650
2000
  const url = new URL(request.url);
1651
2001
  if (url.pathname.startsWith("/assets/")) {
1652
2002
  try {
@@ -1660,13 +2010,36 @@ function createWebServer(options) {
1660
2010
  }
1661
2011
  }
1662
2012
  if (options.handle) {
1663
- const response = await options.handle(request);
2013
+ const response = await options.handle(request, server);
2014
+ if (response === undefined && websocket)
2015
+ return;
1664
2016
  return response ?? await missingHtmlResponse();
1665
2017
  }
1666
2018
  return await missingHtmlResponse();
1667
- });
2019
+ }));
2020
+ }
2021
+ };
2022
+ const server = websocket ? Bun.serve({ ...configuration, websocket }) : Bun.serve({
2023
+ ...configuration,
2024
+ async fetch(request, server) {
2025
+ return await configuration.fetch(request, server) ?? await missingHtmlResponse();
1668
2026
  }
1669
2027
  });
2028
+ if (options.lifecycle) {
2029
+ const lifecycle = options.lifecycle;
2030
+ const name = `http:${crypto.randomUUID()}`;
2031
+ let draining;
2032
+ lifecycle.register(`${name}:stop`, () => {
2033
+ draining ??= server.stop(false);
2034
+ draining.catch(() => {});
2035
+ }, "stop");
2036
+ lifecycle.register(`${name}:drain`, async () => {
2037
+ await draining;
2038
+ await Promise.allSettled([...active]);
2039
+ }, "drain");
2040
+ lifecycle.register(`${name}:force`, () => server.stop(true), "force");
2041
+ }
2042
+ return server;
1670
2043
  }
1671
2044
  // ../../src/bootstrap/web/session.ts
1672
2045
  import { createHmac, randomBytes } from "crypto";
@@ -1945,8 +2318,10 @@ export {
1945
2318
  DATABASE_URL_CONFIG_KEY,
1946
2319
  DEFAULT_APP_PORT,
1947
2320
  REDIS_URL_CONFIG_KEY,
2321
+ RedisSchedulerLeaseStore,
1948
2322
  RouteRegistry,
1949
2323
  Schedule,
2324
+ SchedulerLeaseLostError,
1950
2325
  ServiceContainer,
1951
2326
  appSchedule,
1952
2327
  assertAppDependenciesComplete,
@@ -1958,6 +2333,7 @@ export {
1958
2333
  checkRedis,
1959
2334
  collectProviders,
1960
2335
  configureModulesDirectory,
2336
+ configureModulesManifest,
1961
2337
  coreProviders,
1962
2338
  createAppContext,
1963
2339
  createAppDependencies,
@@ -1970,6 +2346,7 @@ export {
1970
2346
  createWebServer,
1971
2347
  discoverJobs,
1972
2348
  discoverModelTableNames,
2349
+ discoverModels,
1973
2350
  discoverModules,
1974
2351
  ensureModulesLoaded,
1975
2352
  getRequiredDependency,
@@ -1998,6 +2375,7 @@ export {
1998
2375
  setActiveApplicationContext,
1999
2376
  slugify,
2000
2377
  toRouteRequest,
2378
+ tracing_default as tracingProvider,
2001
2379
  wrapSecuredRouteModelByKey,
2002
2380
  wrapWebLogin,
2003
2381
  wrapWebRegister