@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
@@ -25,8 +25,12 @@ import {
25
25
  CORE_CACHE_TOKEN,
26
26
  CORE_CONFIG_TOKEN,
27
27
  CORE_EVENT_BUS_TOKEN,
28
+ CORE_HTTP_CLEANUP_TOKEN,
28
29
  CORE_POLICY_GATE_TOKEN,
30
+ CORE_PUBLIC_TENANCY_TOKEN,
29
31
  CORE_QUEUE_TOKEN,
32
+ CORE_TENANT_RESOLVER_TOKEN,
33
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
30
34
  CORE_TOKEN_SERVICE_TOKEN
31
35
  } from "@getstrata/core/contracts/serviceTokens";
32
36
  var APP_PORT_CONFIG_KEY = "app.port";
@@ -73,7 +77,7 @@ import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
73
77
  import { CacheRepository } from "@getstrata/core/cache/repository";
74
78
  var cacheProvider = {
75
79
  name: "core.cache",
76
- register({ container, config, dependencies }) {
80
+ register({ container, config, dependencies, onCleanup }) {
77
81
  container.singleton(CORE_CACHE_TOKEN, () => {
78
82
  const store = createCacheStore({
79
83
  driver: config.require(CACHE_DRIVER_CONFIG_KEY),
@@ -81,7 +85,9 @@ var cacheProvider = {
81
85
  maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
82
86
  redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
83
87
  });
84
- return new CacheRepository(store);
88
+ const cache = new CacheRepository(store);
89
+ onCleanup(() => cache.close());
90
+ return cache;
85
91
  });
86
92
  dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
87
93
  }
@@ -278,53 +284,101 @@ var eventsProvider = {
278
284
  var events_default = eventsProvider;
279
285
 
280
286
  // ../../src/bootstrap/discoverListeners.ts
281
- import { existsSync, readdirSync } from "fs";
282
- import { createRequire } from "module";
283
- import { join } from "path";
284
- var requireListener = createRequire(import.meta.url);
285
- var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
286
- function readDiscoverListenersState() {
287
- const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
288
- if (existing) {
289
- return existing;
287
+ import { join as join2, resolve } from "path";
288
+
289
+ // ../../src/bootstrap/infrastructureDiscovery.ts
290
+ import { readdir } from "fs/promises";
291
+ import { join, relative } from "path";
292
+ import { pathToFileURL } from "url";
293
+ async function infrastructureFiles(directory, exclude = []) {
294
+ const excluded = new Set([
295
+ "index.ts",
296
+ "index.js",
297
+ "index.mts",
298
+ "index.mjs",
299
+ "register.ts",
300
+ "register.js",
301
+ "register.mts",
302
+ "register.mjs",
303
+ ...exclude
304
+ ]);
305
+ const files = [];
306
+ async function visit(path) {
307
+ const entries = await readdir(path, { withFileTypes: true });
308
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
309
+ for (const entry of entries) {
310
+ const file = join(path, entry.name);
311
+ const key = relative(directory, file).replaceAll("\\", "/");
312
+ if (excluded.has(entry.name) || excluded.has(key))
313
+ continue;
314
+ if (entry.isDirectory())
315
+ await visit(file);
316
+ else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
317
+ files.push(file);
318
+ }
290
319
  }
291
- const state = {};
292
- globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
293
- return state;
294
- }
295
- function resolveListenersDirectory() {
296
- const fromCwd = join(process.cwd(), "src", "listeners");
297
- if (existsSync(fromCwd)) {
298
- return fromCwd;
320
+ try {
321
+ await visit(directory);
322
+ } catch (error) {
323
+ if (error.code !== "ENOENT")
324
+ throw error;
325
+ if (error.path !== directory)
326
+ throw error;
299
327
  }
300
- return join(import.meta.dir, "../listeners");
328
+ return files;
301
329
  }
302
- function loadDiscoveredListeners() {
303
- const listenersDirectory = resolveListenersDirectory();
304
- let entries;
330
+ async function importInfrastructure(file) {
305
331
  try {
306
- entries = readdirSync(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
332
+ return await import(pathToFileURL(file).href);
307
333
  } catch (error) {
308
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
309
- return [];
334
+ throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
335
+ }
336
+ }
337
+
338
+ // ../../src/bootstrap/discoverListeners.ts
339
+ var KEY = Symbol.for("@getstrata/discoverListenersState");
340
+ function state() {
341
+ const globals = globalThis;
342
+ globals[KEY] ??= { loads: new Map };
343
+ return globals[KEY];
344
+ }
345
+ async function load(options) {
346
+ const listeners = new Set;
347
+ const add = (value, location) => {
348
+ if (typeof value !== "function")
349
+ throw new TypeError(`Invalid listener registrar at ${location}: expected a default function.`);
350
+ listeners.add(value);
351
+ };
352
+ if (options.manifest)
353
+ for (const [index, value] of options.manifest.entries())
354
+ add(value, `listener manifest[${index}]`);
355
+ else
356
+ for (const file of await infrastructureFiles(options.directory ?? join2(process.cwd(), "src", "listeners"), options.exclude)) {
357
+ const exports = await importInfrastructure(file);
358
+ const value = exports.default;
359
+ add(value && typeof value === "object" && "default" in value ? value.default : value, file);
310
360
  }
361
+ return [...listeners];
362
+ }
363
+ function discoverListeners(options = {}) {
364
+ const cache = state().loads;
365
+ const key = options.manifest ?? JSON.stringify([
366
+ resolve(options.directory ?? join2(process.cwd(), "src", "listeners")),
367
+ [...options.exclude ?? []].sort()
368
+ ]);
369
+ const existing = cache.get(key);
370
+ if (existing)
371
+ return existing;
372
+ const pending = load(options).catch((error) => {
373
+ if (cache.get(key) === pending)
374
+ cache.delete(key);
311
375
  throw error;
312
- }
313
- const listeners = entries.map((fileName) => {
314
- const filePath = join(listenersDirectory, fileName);
315
- const loaded = requireListener(filePath);
316
- return loaded.default;
317
376
  });
318
- return listeners.filter((listener) => typeof listener === "function");
319
- }
320
- function discoverListeners() {
321
- const state = readDiscoverListenersState();
322
- state.appListeners ??= loadDiscoveredListeners();
323
- return state.appListeners;
377
+ cache.set(key, pending);
378
+ return pending;
324
379
  }
325
380
  function resetDiscoverListenersForTests() {
326
- const state = readDiscoverListenersState();
327
- state.appListeners = undefined;
381
+ state().loads = new Map;
328
382
  }
329
383
 
330
384
  // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
@@ -346,74 +400,129 @@ import {
346
400
  } from "@getstrata/core/runtime/applicationRegistry";
347
401
 
348
402
  // ../../src/bootstrap/discoverModules.ts
349
- import { readdirSync as readdirSync2 } from "fs";
350
- import { join as join2 } from "path";
351
- import { pathToFileURL } from "url";
352
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
353
- function readDiscoverModulesState() {
354
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
355
- if (existing) {
356
- return existing;
357
- }
358
- const state = { appModules: [] };
359
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
360
- return state;
403
+ import { readdir as readdir2 } from "fs/promises";
404
+ import { join as join3, resolve as resolve2 } from "path";
405
+ var KEY2 = Symbol.for("@getstrata/discoverModulesState");
406
+ function state2() {
407
+ const globals = globalThis;
408
+ globals[KEY2] ??= { appModules: [] };
409
+ return globals[KEY2];
361
410
  }
362
411
  function configureModulesDirectory(modulesDir) {
363
- const state = readDiscoverModulesState();
364
- if (state.configuredModulesDir !== modulesDir) {
365
- state.appModules.length = 0;
366
- state.modulesReady = undefined;
412
+ const current = state2();
413
+ const directory = resolve2(modulesDir);
414
+ if (current.configuredModulesDir !== directory || current.configuredManifest) {
415
+ current.appModules.length = 0;
416
+ current.modulesReady = undefined;
417
+ current.source = undefined;
367
418
  }
368
- state.configuredModulesDir = modulesDir;
419
+ current.configuredManifest = undefined;
420
+ current.configuredModulesDir = directory;
369
421
  }
370
- function resolveModulesDirectory(options) {
371
- const state = readDiscoverModulesState();
372
- if (options?.modulesDir) {
373
- return options.modulesDir;
422
+ function configureModulesManifest(manifest) {
423
+ const current = state2();
424
+ if (current.configuredManifest !== manifest) {
425
+ current.appModules.length = 0;
426
+ current.modulesReady = undefined;
427
+ current.source = undefined;
374
428
  }
375
- if (state.configuredModulesDir) {
376
- return state.configuredModulesDir;
377
- }
378
- throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
429
+ current.configuredManifest = manifest;
379
430
  }
380
- async function loadDiscoveredModules(options) {
381
- const modulesDirectory = resolveModulesDirectory(options);
382
- let moduleNames;
383
- try {
384
- moduleNames = readdirSync2(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
385
- } catch (error) {
386
- if (error.code === "ENOENT") {
387
- return [];
431
+ function validateModule(value, location) {
432
+ if (!value || typeof value !== "object" || Array.isArray(value))
433
+ throw new TypeError(`Invalid application module at ${location}.`);
434
+ const module = value;
435
+ if (typeof module.name !== "string" || !module.name.trim())
436
+ throw new TypeError(`Invalid module name at ${location}.`);
437
+ if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
438
+ throw new TypeError(`Invalid module order at ${location}.`);
439
+ for (const hook of ["routes", "webRoutes"])
440
+ if (module[hook] !== undefined && typeof module[hook] !== "function")
441
+ throw new TypeError(`Invalid ${hook} at ${location}.`);
442
+ if (module.providers !== undefined) {
443
+ if (!Array.isArray(module.providers))
444
+ throw new TypeError(`Invalid providers at ${location}.`);
445
+ for (const provider of module.providers) {
446
+ if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
447
+ throw new TypeError(`Invalid provider at ${location}.`);
388
448
  }
389
- throw error;
390
449
  }
391
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
392
- const moduleUrl = pathToFileURL(join2(modulesDirectory, moduleName, "index.ts")).href;
393
- const loaded = await import(moduleUrl);
394
- return loaded.default;
395
- }));
396
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
397
450
  }
398
- async function ensureModulesLoaded(options) {
399
- const state = readDiscoverModulesState();
400
- if (state.appModules.length > 0) {
401
- return state.appModules;
451
+ async function load2(options) {
452
+ const manifest = options.manifest ?? (options.modulesDir === undefined ? state2().configuredManifest : undefined);
453
+ const entries = [];
454
+ if (manifest)
455
+ for (const [index, value] of manifest.entries())
456
+ entries.push({ value, location: `module manifest[${index}]` });
457
+ else {
458
+ const directory = options.modulesDir ?? state2().configuredModulesDir;
459
+ if (!directory)
460
+ throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
461
+ let folders;
462
+ try {
463
+ folders = await readdir2(directory, { withFileTypes: true });
464
+ } catch (error) {
465
+ if (error.code === "ENOENT")
466
+ return [];
467
+ throw error;
468
+ }
469
+ const excluded = new Set(options.exclude ?? []);
470
+ 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)) {
471
+ const path = join3(directory, folder.name);
472
+ const files = await readdir2(path);
473
+ const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
474
+ if (candidates.length !== 1)
475
+ throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
476
+ const file = join3(path, candidates[0]);
477
+ const exports = await importInfrastructure(file);
478
+ entries.push({ value: exports.default, location: file });
479
+ }
402
480
  }
403
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
404
- state.appModules.splice(0, state.appModules.length, ...modules);
405
- return state.appModules;
481
+ const names = new Map;
482
+ const modules = [];
483
+ for (const { value, location } of entries) {
484
+ validateModule(value, location);
485
+ const previous = names.get(value.name);
486
+ if (previous)
487
+ throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
488
+ names.set(value.name, location);
489
+ modules.push(value);
490
+ }
491
+ return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
492
+ }
493
+ function ensureModulesLoaded(options = {}) {
494
+ const current = state2();
495
+ const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
496
+ options.modulesDir ? resolve2(options.modulesDir) : current.configuredModulesDir,
497
+ [...options.exclude ?? []].sort()
498
+ ]);
499
+ if (current.modulesReady && current.source === key)
500
+ return current.modulesReady;
501
+ current.appModules.length = 0;
502
+ current.source = key;
503
+ const pending = load2(options).then((modules) => {
504
+ if (current.modulesReady !== pending)
505
+ return modules;
506
+ current.appModules.splice(0, current.appModules.length, ...modules);
507
+ return current.appModules;
508
+ }).catch((error) => {
509
+ if (current.modulesReady === pending)
510
+ current.modulesReady = undefined;
511
+ throw error;
406
512
  });
407
- return state.modulesReady;
513
+ current.modulesReady = pending;
514
+ return pending;
408
515
  }
409
516
  function discoverModules() {
410
- return readDiscoverModulesState().appModules;
517
+ return state2().appModules;
411
518
  }
412
519
  function resetDiscoverModulesForTests() {
413
- const state = readDiscoverModulesState();
414
- state.configuredModulesDir = undefined;
415
- state.appModules.length = 0;
416
- state.modulesReady = undefined;
520
+ const current = state2();
521
+ current.configuredModulesDir = undefined;
522
+ current.configuredManifest = undefined;
523
+ current.appModules.length = 0;
524
+ current.modulesReady = undefined;
525
+ current.source = undefined;
417
526
  }
418
527
 
419
528
  // ../../src/bootstrap/cache/modelCacheTags.ts
@@ -530,22 +639,12 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options =
530
639
  }
531
640
 
532
641
  // ../../src/bootstrap/providers/listeners.ts
533
- var registeredListenerGroups = new Set;
534
- function registerListenerGroup(name, register) {
535
- if (registeredListenerGroups.has(name)) {
536
- return;
537
- }
538
- registeredListenerGroups.add(name);
539
- register();
540
- }
541
642
  var listenersProvider = {
542
643
  name: "core.listeners",
543
- boot() {
544
- registerListenerGroup("cache.invalidate-on-model-write", () => {
545
- registerInvalidateCacheOnModelWriteListeners();
546
- });
547
- for (const registerListener of discoverListeners()) {
548
- registerListener();
644
+ async boot({ onCleanup }) {
645
+ onCleanup(registerInvalidateCacheOnModelWriteListeners());
646
+ for (const registerListener of await discoverListeners()) {
647
+ await registerListener();
549
648
  }
550
649
  }
551
650
  };
@@ -569,68 +668,99 @@ import {
569
668
  } from "@getstrata/core/queue/createAppQueue";
570
669
 
571
670
  // ../../src/bootstrap/discoverJobs.ts
572
- import { existsSync as existsSync2, readdirSync as readdirSync3 } from "fs";
573
- import { createRequire as createRequire2 } from "module";
574
- import { join as join3 } from "path";
671
+ import { join as join4, resolve as resolve3 } from "path";
672
+ import { Job } from "@getstrata/core/queue";
575
673
  import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
576
- var requireJob = createRequire2(import.meta.url);
577
- var DISCOVER_JOBS_STATE_KEY = Symbol.for("@getstrata/discoverJobsState");
578
- function readDiscoverJobsState() {
579
- const existing = globalThis[DISCOVER_JOBS_STATE_KEY];
580
- if (existing) {
581
- return existing;
582
- }
583
- const state = {};
584
- globalThis[DISCOVER_JOBS_STATE_KEY] = state;
585
- return state;
586
- }
587
- function resolveJobsDirectory() {
588
- const fromCwd = join3(process.cwd(), "src", "jobs");
589
- if (existsSync2(fromCwd)) {
590
- return fromCwd;
591
- }
592
- return join3(import.meta.dir, "../jobs");
674
+ var KEY3 = Symbol.for("@getstrata/discoverJobsState");
675
+ function state3() {
676
+ const globals = globalThis;
677
+ globals[KEY3] ??= { loads: new Map, registered: new Map };
678
+ return globals[KEY3];
593
679
  }
594
- function isDiscoveredJobClass(value) {
595
- if (typeof value !== "function") {
596
- return false;
597
- }
598
- const jobName = value.jobName;
599
- return typeof jobName === "string" && jobName.length > 0;
680
+ function validateClass(value, location) {
681
+ if (typeof value !== "function" || typeof value.jobName !== "string" || !value.jobName.trim() || !(value.prototype instanceof Job) && typeof value.prototype?.handle !== "function")
682
+ throw new TypeError(`Invalid job at ${location}: expected a class with nonempty static jobName and handle().`);
600
683
  }
601
- function loadDiscoveredJobClasses() {
602
- const jobsDirectory = resolveJobsDirectory();
603
- let entries;
604
- try {
605
- entries = readdirSync3(jobsDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
606
- } catch (error) {
607
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
608
- return [];
684
+ async function load3(options, current) {
685
+ const jobs = new Map;
686
+ const add = (name, identity, create, location) => {
687
+ const previous = jobs.get(name);
688
+ if (previous && previous.identity !== identity)
689
+ throw new Error(`Duplicate job "${name}" at ${location}; first declared at ${previous.location}.`);
690
+ if (!previous)
691
+ jobs.set(name, { identity, create, location });
692
+ };
693
+ if (options.manifest) {
694
+ for (const [index, entry] of options.manifest.entries()) {
695
+ const location = `job manifest[${index}]`;
696
+ if (typeof entry === "function") {
697
+ validateClass(entry, location);
698
+ add(entry.jobName, entry, () => new entry, location);
699
+ } else {
700
+ if (!entry || typeof entry.name !== "string" || !entry.name.trim() || typeof entry.create !== "function")
701
+ throw new TypeError(`Invalid job factory at ${location}.`);
702
+ add(entry.name, entry.create, entry.create, location);
703
+ }
704
+ }
705
+ } else {
706
+ for (const file of await infrastructureFiles(options.directory ?? join4(process.cwd(), "src", "jobs"), options.exclude)) {
707
+ const exports = await importInfrastructure(file);
708
+ const defaultValue = exports.default;
709
+ const legacy = defaultValue && typeof defaultValue === "object" && "default" in defaultValue ? defaultValue.default : defaultValue;
710
+ if (legacy !== undefined)
711
+ validateClass(legacy, `${file}:default`);
712
+ let found = false;
713
+ for (const [name, value] of Object.entries({
714
+ ...exports,
715
+ ...legacy === undefined ? {} : { default: legacy }
716
+ })) {
717
+ if (name !== "default" && !(typeof value === "function" && ("jobName" in value)))
718
+ continue;
719
+ validateClass(value, `${file}:${name}`);
720
+ found = true;
721
+ add(value.jobName, value, () => new value, `${file}:${name}`);
722
+ }
723
+ if (!found)
724
+ throw new TypeError(`No job exports at ${file}; exclude helper modules explicitly.`);
609
725
  }
610
- throw error;
611
726
  }
612
- return entries.flatMap((fileName) => {
613
- const filePath = join3(jobsDirectory, fileName);
614
- const loaded = requireJob(filePath);
615
- return isDiscoveredJobClass(loaded.default) ? [loaded.default] : [];
616
- });
617
- }
618
- function discoverJobs() {
619
- const state = readDiscoverJobsState();
620
- if (state.names) {
621
- return state.names;
727
+ if (!current())
728
+ return [...jobs.keys()];
729
+ const registered = state3().registered;
730
+ for (const [name, entry] of jobs) {
731
+ const previous = registered.get(name);
732
+ if (previous && previous.identity !== entry.identity)
733
+ throw new Error(`Duplicate discovered job "${name}" at ${entry.location}; first declared at ${previous.location}.`);
622
734
  }
623
- const names = [];
624
- for (const JobClass of loadDiscoveredJobClasses()) {
625
- jobRegistry.register(JobClass.jobName, () => new JobClass);
626
- names.push(JobClass.jobName);
735
+ const existing = new Set(jobRegistry.names());
736
+ for (const [name, entry] of jobs) {
737
+ if (!existing.has(name)) {
738
+ jobRegistry.register(name, entry.create);
739
+ registered.set(name, entry);
740
+ }
627
741
  }
628
- state.names = names;
629
- return names;
742
+ return [...jobs.keys()];
743
+ }
744
+ function discoverJobs(options = {}) {
745
+ const cache = state3().loads;
746
+ const key = options.manifest ?? JSON.stringify([
747
+ resolve3(options.directory ?? join4(process.cwd(), "src", "jobs")),
748
+ [...options.exclude ?? []].sort()
749
+ ]);
750
+ const existing = cache.get(key);
751
+ if (existing)
752
+ return existing;
753
+ const pending = load3(options, () => state3().loads === cache).catch((error) => {
754
+ if (cache.get(key) === pending)
755
+ cache.delete(key);
756
+ throw error;
757
+ });
758
+ cache.set(key, pending);
759
+ return pending;
630
760
  }
631
761
  function resetDiscoverJobsForTests() {
632
- const state = readDiscoverJobsState();
633
- state.names = undefined;
762
+ state3().loads = new Map;
763
+ state3().registered = new Map;
634
764
  }
635
765
 
636
766
  // ../../src/bootstrap/queue/defaultJobs.ts
@@ -648,16 +778,17 @@ function registerDefaultJobs() {
648
778
  // ../../src/bootstrap/providers/queue.ts
649
779
  var queueProvider = {
650
780
  name: "core.queue",
651
- register({ container, config }) {
781
+ async register({ container, config, onCleanup }) {
652
782
  const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
653
783
  const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
654
784
  config.set("queue.driver", driver);
655
785
  const failedJobs = createFailedJobService();
656
786
  container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
657
- container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, () => {
658
- registerDefaultJobs();
659
- discoverJobs();
660
- }));
787
+ registerDefaultJobs();
788
+ await discoverJobs();
789
+ const queue = createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs);
790
+ onCleanup(() => queue.close?.(), driver === "async" ? "drain" : "close");
791
+ container.set(CORE_QUEUE_TOKEN, queue);
661
792
  }
662
793
  };
663
794
  var queue_default = queueProvider;
@@ -672,6 +803,18 @@ var storageProvider = {
672
803
  };
673
804
  var storage_default = storageProvider;
674
805
 
806
+ // ../../src/bootstrap/providers/tracing.ts
807
+ import { acquireTracingRuntime } from "@getstrata/core/tracing/tracingMiddleware";
808
+ var tracingProvider = {
809
+ name: "core.tracing",
810
+ register({ onCleanup }) {
811
+ const ownership = acquireTracingRuntime();
812
+ onCleanup(() => ownership.flush(), "flush");
813
+ onCleanup(() => ownership.release(), "close");
814
+ }
815
+ };
816
+ var tracing_default = tracingProvider;
817
+
675
818
  // ../../src/bootstrap/providers/view.ts
676
819
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
677
820
  import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
@@ -716,6 +859,7 @@ var viewProvider = {
716
859
  // ../../src/bootstrap/providers/index.ts
717
860
  var coreProviders = [
718
861
  config_default,
862
+ tracing_default,
719
863
  cache_default,
720
864
  storage_default,
721
865
  auth_default,
@@ -126,8 +126,8 @@ function assertFeatureProductionSecrets(env) {
126
126
  throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
127
127
  }
128
128
  }
129
- if (envFlagEnabled(env.FEATURE_PUBLIC_READS)) {
130
- throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
129
+ if (envFlagEnabled(env.TENANT_DEV_HEADERS)) {
130
+ throw new Error("Production startup blocked: set TENANT_DEV_HEADERS=false.");
131
131
  }
132
132
  if (!env.SIEM_EXPORT_URL?.trim() && envFlagEnabled(env.FEATURE_SIEM_EXPORT)) {
133
133
  console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");