@getstrata/bootstrap 0.2.22 → 0.2.23

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 (234) hide show
  1. package/dist/bootstrap/applicationRegistry.d.ts +1 -1
  2. package/dist/bootstrap/buildModuleRoutes.d.ts +10 -0
  3. package/dist/bootstrap/config.d.ts +1 -1
  4. package/dist/bootstrap/contracts.d.ts +4 -4
  5. package/dist/bootstrap/public-api.d.ts +2 -0
  6. package/dist/entries/applicationRegistry.js +12 -115
  7. package/dist/entries/buildModuleRoutes.js +626 -0
  8. package/dist/entries/config.js +9 -9
  9. package/dist/entries/context.js +114 -178
  10. package/dist/entries/contracts.js +10 -85
  11. package/dist/entries/createWebRoutes.js +9 -9
  12. package/dist/entries/dependencies.js +114 -178
  13. package/dist/entries/http/securedRouteModelBinding.js +4 -34
  14. package/dist/entries/httpKernel.js +9 -9
  15. package/dist/entries/membershipService.js +0 -34
  16. package/dist/entries/providers/view.js +9 -9
  17. package/dist/entries/providers.js +104 -115
  18. package/dist/entries/queue/defaultJobs.js +0 -34
  19. package/dist/entries/web/routing.js +20 -41
  20. package/dist/index.js +1670 -1659
  21. package/package.json +9 -4
  22. package/dist/cli/commands/scheduleRun.d.ts +0 -3
  23. package/dist/config/app.d.ts +0 -10
  24. package/dist/config/auth.d.ts +0 -7
  25. package/dist/config/contentSecurityPolicy.d.ts +0 -5
  26. package/dist/config/cors.d.ts +0 -9
  27. package/dist/config/database.d.ts +0 -10
  28. package/dist/config/features.d.ts +0 -18
  29. package/dist/config/frontend.d.ts +0 -6
  30. package/dist/config/queue.d.ts +0 -8
  31. package/dist/config/rateLimit.d.ts +0 -10
  32. package/dist/config/uploads.d.ts +0 -5
  33. package/dist/core/admin/formatValue.d.ts +0 -3
  34. package/dist/core/admin/index.d.ts +0 -3
  35. package/dist/core/admin/registry.d.ts +0 -11
  36. package/dist/core/admin/types.d.ts +0 -24
  37. package/dist/core/audit/exportAuditLogs.d.ts +0 -8
  38. package/dist/core/audit/siemFormatter.d.ts +0 -30
  39. package/dist/core/auth/abilityChecker.d.ts +0 -6
  40. package/dist/core/auth/accessControl.d.ts +0 -8
  41. package/dist/core/auth/authContext.d.ts +0 -11
  42. package/dist/core/auth/guard.d.ts +0 -36
  43. package/dist/core/auth/membershipContext.d.ts +0 -15
  44. package/dist/core/auth/membershipContextMiddleware.d.ts +0 -3
  45. package/dist/core/auth/membershipMiddleware.d.ts +0 -2
  46. package/dist/core/auth/membershipScope.d.ts +0 -16
  47. package/dist/core/auth/membershipService.d.ts +0 -23
  48. package/dist/core/auth/oauth/oidcProvider.d.ts +0 -18
  49. package/dist/core/auth/oauth/providers.d.ts +0 -22
  50. package/dist/core/auth/oauth/samlProvider.d.ts +0 -9
  51. package/dist/core/auth/oauth/types.d.ts +0 -11
  52. package/dist/core/auth/password.d.ts +0 -3
  53. package/dist/core/auth/policy.d.ts +0 -15
  54. package/dist/core/auth/resolveMembershipService.d.ts +0 -3
  55. package/dist/core/auth/scimAuthMiddleware.d.ts +0 -3
  56. package/dist/core/auth/sessionCookie.d.ts +0 -6
  57. package/dist/core/auth/sessionGuard.d.ts +0 -9
  58. package/dist/core/auth/tokenHash.d.ts +0 -3
  59. package/dist/core/cache/createCacheStore.d.ts +0 -11
  60. package/dist/core/cache/redisCacheStore.d.ts +0 -23
  61. package/dist/core/cache/repository.d.ts +0 -17
  62. package/dist/core/cache/simpleCache.d.ts +0 -23
  63. package/dist/core/cache/simpleCacheStore.d.ts +0 -16
  64. package/dist/core/cache/store.d.ts +0 -12
  65. package/dist/core/cache/taggedCache.d.ts +0 -9
  66. package/dist/core/cache/tags.d.ts +0 -11
  67. package/dist/core/config/envSchema.d.ts +0 -12
  68. package/dist/core/contracts/container.d.ts +0 -30
  69. package/dist/core/contracts/di.d.ts +0 -31
  70. package/dist/core/contracts/serviceContainer.d.ts +0 -1
  71. package/dist/core/contracts/serviceTokens.d.ts +0 -8
  72. package/dist/core/crypto/fieldEncryption.d.ts +0 -13
  73. package/dist/core/crypto/mfaSecret.d.ts +0 -3
  74. package/dist/core/crypto/nonCryptographicHash.d.ts +0 -2
  75. package/dist/core/database/baseRepository.d.ts +0 -67
  76. package/dist/core/database/bindConnection.d.ts +0 -3
  77. package/dist/core/database/boundConnection.d.ts +0 -5
  78. package/dist/core/database/connection.d.ts +0 -7
  79. package/dist/core/database/connectionContext.d.ts +0 -8
  80. package/dist/core/database/defaultConnection.d.ts +0 -6
  81. package/dist/core/database/errors.d.ts +0 -12
  82. package/dist/core/database/index.d.ts +0 -18
  83. package/dist/core/database/migrations/advisoryLock.d.ts +0 -6
  84. package/dist/core/database/migrations/runner.d.ts +0 -25
  85. package/dist/core/database/migrations/types.d.ts +0 -15
  86. package/dist/core/database/model.d.ts +0 -57
  87. package/dist/core/database/query.d.ts +0 -63
  88. package/dist/core/database/queryProxy.d.ts +0 -3
  89. package/dist/core/database/relationships.d.ts +0 -100
  90. package/dist/core/database/repositoryConnection.d.ts +0 -4
  91. package/dist/core/database/repositoryQuery.d.ts +0 -38
  92. package/dist/core/database/schema/blueprint.d.ts +0 -47
  93. package/dist/core/database/schema/columnDefinition.d.ts +0 -36
  94. package/dist/core/database/schema/driver.d.ts +0 -8
  95. package/dist/core/database/schema/errors.d.ts +0 -4
  96. package/dist/core/database/schema/grammars/compileStatements.d.ts +0 -8
  97. package/dist/core/database/schema/grammars/createGrammar.d.ts +0 -4
  98. package/dist/core/database/schema/grammars/grammar.d.ts +0 -10
  99. package/dist/core/database/schema/grammars/index.d.ts +0 -7
  100. package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +0 -2
  101. package/dist/core/database/schema/grammars/postgresGrammar.d.ts +0 -2
  102. package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +0 -2
  103. package/dist/core/database/schema/index.d.ts +0 -12
  104. package/dist/core/database/schema/schema.d.ts +0 -21
  105. package/dist/core/database/seeders/runner.d.ts +0 -6
  106. package/dist/core/database/seeders/types.d.ts +0 -8
  107. package/dist/core/database/table.d.ts +0 -13
  108. package/dist/core/database/transaction.d.ts +0 -3
  109. package/dist/core/database/types.d.ts +0 -66
  110. package/dist/core/database/whereBuilder.d.ts +0 -17
  111. package/dist/core/errors/http.d.ts +0 -33
  112. package/dist/core/events/eventBus.d.ts +0 -10
  113. package/dist/core/events/index.d.ts +0 -4
  114. package/dist/core/facades/index.d.ts +0 -10
  115. package/dist/core/http/authMiddleware.d.ts +0 -5
  116. package/dist/core/http/authorizeMiddleware.d.ts +0 -5
  117. package/dist/core/http/bodySizeLimitMiddleware.d.ts +0 -4
  118. package/dist/core/http/conditionalResponse.d.ts +0 -2
  119. package/dist/core/http/contentNegotiation.d.ts +0 -2
  120. package/dist/core/http/cookies.d.ts +0 -4
  121. package/dist/core/http/corsMiddleware.d.ts +0 -3
  122. package/dist/core/http/csrfMiddleware.d.ts +0 -3
  123. package/dist/core/http/csrfProtection.d.ts +0 -12
  124. package/dist/core/http/csrfToken.d.ts +0 -14
  125. package/dist/core/http/etag.d.ts +0 -18
  126. package/dist/core/http/flashMiddleware.d.ts +0 -3
  127. package/dist/core/http/flashSession.d.ts +0 -12
  128. package/dist/core/http/formRequest.d.ts +0 -10
  129. package/dist/core/http/index.d.ts +0 -27
  130. package/dist/core/http/loginThrottleMiddleware.d.ts +0 -11
  131. package/dist/core/http/memoryThrottleMiddleware.d.ts +0 -9
  132. package/dist/core/http/metricsMiddleware.d.ts +0 -4
  133. package/dist/core/http/middleware.d.ts +0 -8
  134. package/dist/core/http/pagination.d.ts +0 -11
  135. package/dist/core/http/parseFormBody.d.ts +0 -6
  136. package/dist/core/http/parseMultipartUpload.d.ts +0 -10
  137. package/dist/core/http/requestMetaContext.d.ts +0 -15
  138. package/dist/core/http/requireAbilityMiddleware.d.ts +0 -4
  139. package/dist/core/http/requireAuthMiddleware.d.ts +0 -4
  140. package/dist/core/http/requireGlobalAdminMiddleware.d.ts +0 -3
  141. package/dist/core/http/requireWebAuthMiddleware.d.ts +0 -4
  142. package/dist/core/http/resources.d.ts +0 -7
  143. package/dist/core/http/route.d.ts +0 -6
  144. package/dist/core/http/routeMiddleware.d.ts +0 -3
  145. package/dist/core/http/routeModelBinding.d.ts +0 -3
  146. package/dist/core/http/scimThrottleMiddleware.d.ts +0 -9
  147. package/dist/core/http/securedRouteModelBinding.d.ts +0 -11
  148. package/dist/core/http/securityHeadersMiddleware.d.ts +0 -3
  149. package/dist/core/http/throttleMiddleware.d.ts +0 -11
  150. package/dist/core/http/validation.d.ts +0 -23
  151. package/dist/core/http/webErrorResponse.d.ts +0 -5
  152. package/dist/core/http/webFormRequest.d.ts +0 -7
  153. package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -13
  154. package/dist/core/jobs/invalidateCacheTagsJob.d.ts +0 -12
  155. package/dist/core/lifecycle/gracefulShutdown.d.ts +0 -6
  156. package/dist/core/logging/logger.d.ts +0 -17
  157. package/dist/core/logging/requestLoggingMiddleware.d.ts +0 -3
  158. package/dist/core/mail/mailer.d.ts +0 -38
  159. package/dist/core/mail/markdownMail.d.ts +0 -15
  160. package/dist/core/mail/markdownMailable.d.ts +0 -12
  161. package/dist/core/metrics/prometheus.d.ts +0 -26
  162. package/dist/core/notifications/dispatcher.d.ts +0 -13
  163. package/dist/core/notifications/index.d.ts +0 -3
  164. package/dist/core/notifications/notification.d.ts +0 -7
  165. package/dist/core/notifications/types.d.ts +0 -25
  166. package/dist/core/openapi/registeredRoute.d.ts +0 -6
  167. package/dist/core/pagination/index.d.ts +0 -27
  168. package/dist/core/queue/createAppQueue.d.ts +0 -5
  169. package/dist/core/queue/failedJobRepository.d.ts +0 -6
  170. package/dist/core/queue/failedJobService.d.ts +0 -16
  171. package/dist/core/queue/failedJobTable.d.ts +0 -3
  172. package/dist/core/queue/index.d.ts +0 -19
  173. package/dist/core/queue/jobRegistry.d.ts +0 -13
  174. package/dist/core/queue/jobRunner.d.ts +0 -9
  175. package/dist/core/queue/publicQueue.d.ts +0 -16
  176. package/dist/core/queue/queueMetrics.d.ts +0 -15
  177. package/dist/core/queue/redisQueue.d.ts +0 -29
  178. package/dist/core/queue/resilientQueue.d.ts +0 -9
  179. package/dist/core/queue/types.d.ts +0 -8
  180. package/dist/core/runtime/applicationRegistry.d.ts +0 -17
  181. package/dist/core/runtime/asyncContextStore.d.ts +0 -3
  182. package/dist/core/scheduler/schedule.d.ts +0 -15
  183. package/dist/core/security/publicReads.d.ts +0 -3
  184. package/dist/core/security/safeFetch.d.ts +0 -10
  185. package/dist/core/security/safeUrl.d.ts +0 -20
  186. package/dist/core/security/scimTenantTokens.d.ts +0 -3
  187. package/dist/core/security/securityEvents.d.ts +0 -6
  188. package/dist/core/security/timingSafeCompare.d.ts +0 -2
  189. package/dist/core/security/tokenExpiry.d.ts +0 -2
  190. package/dist/core/security/totp.d.ts +0 -3
  191. package/dist/core/storage/storage.d.ts +0 -44
  192. package/dist/core/tenant/databaseTenantContext.d.ts +0 -2
  193. package/dist/core/tenant/resolveTenant.d.ts +0 -3
  194. package/dist/core/tenant/tenantContext.d.ts +0 -13
  195. package/dist/core/tenant/tenantDatabaseScope.d.ts +0 -8
  196. package/dist/core/tenant/tenantMiddleware.d.ts +0 -6
  197. package/dist/core/tracing/otel.d.ts +0 -26
  198. package/dist/core/tracing/traceContext.d.ts +0 -9
  199. package/dist/core/tracing/tracingMiddleware.d.ts +0 -3
  200. package/dist/core/validation/rules.d.ts +0 -17
  201. package/dist/core/view/etaViewEngine.d.ts +0 -15
  202. package/dist/core/view/htmlResponse.d.ts +0 -6
  203. package/dist/core/view/index.d.ts +0 -5
  204. package/dist/core/view/viewEngine.d.ts +0 -6
  205. package/dist/core/view/webLayoutData.d.ts +0 -17
  206. package/dist/db/connection/createConnection.d.ts +0 -6
  207. package/dist/db/connection/index.d.ts +0 -11
  208. package/dist/domain/abilities.d.ts +0 -5
  209. package/dist/domain/auth.d.ts +0 -4
  210. package/dist/domain/scim.d.ts +0 -9
  211. package/dist/domain/workhub.d.ts +0 -35
  212. package/dist/framework/public-api.d.ts +0 -116
  213. package/dist/modules/organization/memberRepository.d.ts +0 -14
  214. package/dist/modules/organization/memberTypes.d.ts +0 -9
  215. package/dist/modules/organization/repository.d.ts +0 -14
  216. package/dist/modules/organization/table.d.ts +0 -3
  217. package/dist/modules/organization/types.d.ts +0 -10
  218. package/dist/modules/scim/controller.d.ts +0 -16
  219. package/dist/modules/scim/scimResponse.d.ts +0 -10
  220. package/dist/modules/scim/service.d.ts +0 -214
  221. package/dist/modules/user/apiTokenRepository.d.ts +0 -8
  222. package/dist/modules/user/apiTokenTable.d.ts +0 -3
  223. package/dist/modules/user/authService.d.ts +0 -23
  224. package/dist/modules/user/notificationRepository.d.ts +0 -6
  225. package/dist/modules/user/notificationService.d.ts +0 -25
  226. package/dist/modules/user/notificationTable.d.ts +0 -3
  227. package/dist/modules/user/notificationTypes.d.ts +0 -12
  228. package/dist/modules/user/oauthIdentityRepository.d.ts +0 -9
  229. package/dist/modules/user/provider.d.ts +0 -11
  230. package/dist/modules/user/repository.d.ts +0 -13
  231. package/dist/modules/user/table.d.ts +0 -3
  232. package/dist/modules/user/tokenService.d.ts +0 -29
  233. package/dist/modules/user/types.d.ts +0 -46
  234. package/dist/types/services.d.ts +0 -16
package/dist/index.js CHANGED
@@ -503,242 +503,624 @@ async function scheduleRunCommand() {
503
503
  }
504
504
  await runDueScheduledTasks(appSchedule);
505
505
  }
506
- // ../../src/core/contracts/di.ts
507
- var requiredDependencyKeys = [
508
- "container",
509
- "cache",
510
- "storage"
511
- ];
512
- function getRequiredDependency(dependencies, key) {
513
- const dependency = dependencies[key];
514
- if (dependency === undefined) {
515
- throw new Error(`Required dependency "${key}" is not registered.`);
516
- }
517
- return dependency;
518
- }
519
- function assertAppDependenciesComplete(dependencies) {
520
- for (const key of requiredDependencyKeys) {
521
- getRequiredDependency(dependencies, key);
522
- }
523
- }
524
- function resolveService(dependencies, token) {
525
- return dependencies.container.resolve(token);
506
+ // ../../src/bootstrap/applicationRegistry.ts
507
+ import {
508
+ resolveApplicationAuth,
509
+ resolveApplicationCache,
510
+ resolveApplicationConfig,
511
+ resolveApplicationDependencies,
512
+ resolveApplicationEventBus,
513
+ resolveApplicationLogger,
514
+ resolveApplicationPolicyGate,
515
+ resolveApplicationQueue,
516
+ setActiveApplicationContext
517
+ } from "@getstrata/core/runtime/applicationRegistry.ts";
518
+ // ../../src/core/crypto/nonCryptographicHash.ts
519
+ function nonCryptographicDigest(input) {
520
+ return Bun.hash(input).toString(16);
526
521
  }
527
522
 
528
- // ../../src/core/contracts/serviceTokens.ts
529
- var CORE_CONFIG_TOKEN = "core.config";
530
- var CORE_CACHE_TOKEN = "core.cache";
531
- var CORE_QUEUE_TOKEN = "core.queue";
532
- var CORE_EVENT_BUS_TOKEN = "core.eventBus";
533
- var CORE_POLICY_GATE_TOKEN = "core.policyGate";
534
- var CORE_AUTH_TOKEN = "core.auth";
535
- var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
536
-
537
- // ../../src/core/runtime/applicationRegistry.ts
538
- var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
539
- var activeContext;
540
- function readStoredApplicationContext() {
541
- if (activeContext) {
542
- return activeContext;
543
- }
544
- const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
545
- if (globalContext) {
546
- activeContext = globalContext;
547
- }
548
- return activeContext;
549
- }
550
- function setActiveApplicationContext(context) {
551
- activeContext = context;
552
- globalThis[APPLICATION_CONTEXT_KEY] = context;
553
- }
554
- function requireActiveApplicationContext() {
555
- const context = readStoredApplicationContext();
556
- if (!context) {
557
- throw new Error("The application context has not been bootstrapped.");
558
- }
559
- return context;
560
- }
561
- function resolveApplicationCache() {
562
- return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
563
- }
564
- function resolveApplicationQueue() {
565
- return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
566
- }
567
- function resolveApplicationEventBus() {
568
- return requireActiveApplicationContext().container.resolve(CORE_EVENT_BUS_TOKEN);
523
+ // ../../src/core/http/etag.ts
524
+ function isEtagEnabled() {
525
+ return (process.env.FEATURE_ETAG ?? "true") !== "false";
569
526
  }
570
- function resolveApplicationAuth() {
571
- return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
527
+ function formatWeakEtag(digest) {
528
+ return `W/"${digest}"`;
572
529
  }
573
- function resolveApplicationPolicyGate() {
574
- return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
530
+ function computeEtagFromJson(data) {
531
+ const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
532
+ return formatWeakEtag(digest);
575
533
  }
576
- function resolveApplicationConfig() {
577
- return requireActiveApplicationContext().config;
534
+ function etagFromResource(resource) {
535
+ const version = resource.updated_at ?? resource.created_at ?? "";
536
+ const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
537
+ const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
538
+ return formatWeakEtag(digest);
578
539
  }
579
- function resolveApplicationLogger() {
580
- return appLogger;
540
+ function normalizeEtag(value) {
541
+ return value.trim();
581
542
  }
582
- function resolveApplicationDependencies() {
583
- return requireActiveApplicationContext().dependencies;
543
+ function etagValuesMatch(left, right) {
544
+ return normalizeEtag(left) === normalizeEtag(right);
584
545
  }
585
- // ../../src/bootstrap/discoverModules.ts
586
- import { readdirSync } from "fs";
587
- import { join } from "path";
588
- import { pathToFileURL } from "url";
589
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
590
- function readDiscoverModulesState() {
591
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
592
- if (existing) {
593
- return existing;
546
+ function parseEtagList(header) {
547
+ if (!header) {
548
+ return [];
594
549
  }
595
- const state = { appModules: [] };
596
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
597
- return state;
550
+ return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
598
551
  }
599
- function configureModulesDirectory(modulesDir) {
600
- readDiscoverModulesState().configuredModulesDir = modulesDir;
552
+ function ifNoneMatchSatisfied(request, etag) {
553
+ const header = request.headers.get("if-none-match");
554
+ if (!header) {
555
+ return false;
556
+ }
557
+ if (header.trim() === "*") {
558
+ return true;
559
+ }
560
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
601
561
  }
602
- function resolveModulesDirectory(options) {
603
- const state = readDiscoverModulesState();
604
- if (options?.modulesDir) {
605
- return options.modulesDir;
562
+ function ifMatchSatisfied(request, etag) {
563
+ const header = request.headers.get("if-match");
564
+ if (!header) {
565
+ return false;
606
566
  }
607
- if (state.configuredModulesDir) {
608
- return state.configuredModulesDir;
567
+ if (header.trim() === "*") {
568
+ return true;
609
569
  }
610
- return join(import.meta.dir, "../modules");
570
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
611
571
  }
612
- async function loadDiscoveredModules(options) {
613
- const modulesDirectory = resolveModulesDirectory(options);
614
- let moduleNames;
615
- try {
616
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
617
- } catch (error) {
618
- if (error.code === "ENOENT") {
619
- return [];
572
+ function assertIfMatch(request, etag, options = {}) {
573
+ const header = request.headers.get("if-match");
574
+ if (!header) {
575
+ if (options.required) {
576
+ throw new PreconditionFailedError("If-Match header is required.");
620
577
  }
621
- throw error;
578
+ return;
622
579
  }
623
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
624
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
625
- const loaded = await import(moduleUrl);
626
- return loaded.default;
627
- }));
628
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
629
- }
630
- async function ensureModulesLoaded(options) {
631
- const state = readDiscoverModulesState();
632
- if (state.appModules.length > 0) {
633
- return state.appModules;
580
+ if (!ifMatchSatisfied(request, etag)) {
581
+ throw new PreconditionFailedError("Resource ETag does not match If-Match.");
634
582
  }
635
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
636
- state.appModules.splice(0, state.appModules.length, ...modules);
637
- return state.appModules;
638
- });
639
- return state.modulesReady;
640
- }
641
- function discoverModules() {
642
- return readDiscoverModulesState().appModules;
643
583
  }
644
-
645
- // ../../src/bootstrap/cache/modelCacheTags.ts
646
- function cacheTagsForModelWrite(tableName, action) {
647
- const module = discoverModules().find((entry) => entry.tableName === tableName);
648
- const baseTags = module?.cacheTags ?? [`${tableName}s`];
649
- const isDelete = action === "deleted" || action === "force-deleted";
650
- const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
651
- return [...new Set([...baseTags, ...extraTags])];
584
+ function applyEtagHeaders(headers, etag) {
585
+ const next = new Headers(headers);
586
+ next.set("ETag", etag);
587
+ next.set("Cache-Control", "private, must-revalidate");
588
+ next.append("Vary", "Authorization");
589
+ next.append("Vary", "X-Tenant-Id");
590
+ return next;
652
591
  }
653
- function discoverModelTableNames() {
654
- return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
592
+ function notModifiedResponse(etag) {
593
+ return new Response(null, {
594
+ status: 304,
595
+ headers: applyEtagHeaders(new Headers, etag)
596
+ });
655
597
  }
656
- // ../../src/bootstrap/config.ts
657
- var APP_PORT_CONFIG_KEY = "app.port";
658
- var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
659
- var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
660
- var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
661
- var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
662
- var DATABASE_URL_CONFIG_KEY = "database.url";
663
- var DEFAULT_APP_PORT = 3000;
664
- var DEFAULT_CACHE_TTL_MS = 3600000;
665
- var DEFAULT_CACHE_MAX_ENTRIES = 100;
666
- var DEFAULT_CACHE_DRIVER = "array";
667
- var DEFAULT_API_TOKEN = "";
668
- var DEFAULT_QUEUE_DRIVER = "sync";
669
- // ../../src/core/contracts/container.ts
670
- class ServiceContainer {
671
- services = new Map;
672
- singletonFactories = new Map;
673
- bindings = new Map;
674
- set(key, value) {
675
- this.singletonFactories.delete(key);
676
- this.bindings.delete(key);
677
- this.services.set(key, value);
678
- return value;
679
- }
680
- singleton(key, factory) {
681
- this.bindings.delete(key);
682
- this.services.delete(key);
683
- this.singletonFactories.set(key, factory);
684
- }
685
- bind(key, factory) {
686
- this.singletonFactories.delete(key);
687
- this.services.delete(key);
688
- this.bindings.set(key, factory);
598
+ function applyConditionalGet(request, response, etag) {
599
+ if (!isEtagEnabled()) {
600
+ return response;
689
601
  }
690
- get(key) {
691
- if (this.services.has(key)) {
692
- return this.services.get(key);
693
- }
694
- const singletonFactory = this.singletonFactories.get(key);
695
- if (singletonFactory) {
696
- const value = singletonFactory(this);
697
- this.services.set(key, value);
698
- return value;
699
- }
700
- const binding = this.bindings.get(key);
701
- if (binding) {
702
- return binding(this);
703
- }
704
- throw new Error(`Service "${key}" is not registered.`);
602
+ if (ifNoneMatchSatisfied(request, etag)) {
603
+ return notModifiedResponse(etag);
705
604
  }
706
- resolve(key) {
707
- return this.get(key);
605
+ const headers = applyEtagHeaders(new Headers(response.headers), etag);
606
+ return new Response(response.body, {
607
+ status: response.status,
608
+ statusText: response.statusText,
609
+ headers
610
+ });
611
+ }
612
+
613
+ // ../../src/core/http/conditionalResponse.ts
614
+ function jsonResponse(data, init = {}) {
615
+ return Response.json(data, {
616
+ status: init.status ?? 200,
617
+ headers: init.headers
618
+ });
619
+ }
620
+ function conditionalJsonResponse(request, data, init = {}) {
621
+ if (!request || !isEtagEnabled()) {
622
+ return jsonResponse(data, init);
708
623
  }
709
- has(key) {
710
- return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
624
+ const etag = computeEtagFromJson(data);
625
+ if (ifNoneMatchSatisfied(request, etag)) {
626
+ return notModifiedResponse(etag);
711
627
  }
628
+ const response = jsonResponse(data, init);
629
+ const headers = new Headers(response.headers);
630
+ headers.set("ETag", etag);
631
+ headers.set("Cache-Control", "private, must-revalidate");
632
+ headers.append("Vary", "Authorization");
633
+ headers.append("Vary", "X-Tenant-Id");
634
+ return new Response(response.body, {
635
+ status: response.status,
636
+ statusText: response.statusText,
637
+ headers
638
+ });
712
639
  }
713
640
 
714
- class ConfigStore {
715
- values = new Map;
716
- set(key, value) {
717
- this.values.set(key, value);
718
- return value;
719
- }
720
- get(key) {
721
- return this.values.get(key);
641
+ // ../../src/core/http/middleware.ts
642
+ function isRouteHandler(value) {
643
+ return typeof value === "function";
644
+ }
645
+ function isMethodRouteMap(value) {
646
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
647
+ return false;
722
648
  }
723
- require(key) {
724
- if (!this.values.has(key)) {
725
- throw new Error(`Config key "${key}" is not defined.`);
649
+ const entries = Object.entries(value);
650
+ return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
651
+ }
652
+ function composeMiddleware(...middleware) {
653
+ return (handler) => {
654
+ return async (request) => {
655
+ let index = 0;
656
+ const dispatch = async () => {
657
+ if (index >= middleware.length) {
658
+ return await handler(request);
659
+ }
660
+ const current = middleware[index];
661
+ index += 1;
662
+ if (!current) {
663
+ return await handler(request);
664
+ }
665
+ return await current(request, dispatch);
666
+ };
667
+ return await dispatch();
668
+ };
669
+ };
670
+ }
671
+ function wrapRouteHandler(handler, middleware) {
672
+ if (isMethodRouteMap(handler)) {
673
+ const wrapped = {};
674
+ for (const [method, routeHandler] of Object.entries(handler)) {
675
+ wrapped[method] = composeMiddleware(...middleware)(routeHandler);
726
676
  }
727
- return this.values.get(key);
677
+ return wrapped;
728
678
  }
729
- has(key) {
730
- return this.values.has(key);
679
+ if (isRouteHandler(handler)) {
680
+ return composeMiddleware(...middleware)(handler);
731
681
  }
682
+ return handler;
732
683
  }
733
- // ../../src/config/auth.ts
734
- var authConfig = {
735
- allowDevHeaders: (process.env.AUTH_DEV_HEADERS ?? "true") !== "false",
736
- tokenDefaultAbilities: ["*"]
737
- };
738
-
739
- // ../../src/domain/abilities.ts
740
- var MEMBER_ABILITIES = [
741
- "organizations:read",
684
+ function applyMiddlewareToRoutes(routes, middleware) {
685
+ const wrapped = {};
686
+ for (const [path, routeHandler] of Object.entries(routes)) {
687
+ wrapped[path] = wrapRouteHandler(routeHandler, middleware);
688
+ }
689
+ return wrapped;
690
+ }
691
+
692
+ // ../../src/bootstrap/httpKernel.ts
693
+ import {
694
+ createAuthMiddleware,
695
+ createAuthorizeMiddleware,
696
+ createBodySizeLimitMiddleware,
697
+ createCorsMiddleware,
698
+ createCsrfMiddleware,
699
+ createFlashMiddleware,
700
+ createLoginThrottleMiddleware,
701
+ createMembershipMiddleware,
702
+ createMemoryThrottleMiddleware,
703
+ createMetricsMiddleware,
704
+ createRequestLoggingMiddleware,
705
+ createRequireAbilityMiddleware,
706
+ createRequireAuthMiddleware,
707
+ createRequireGlobalAdminMiddleware,
708
+ createRequireWebAuthMiddleware,
709
+ createSecurityHeadersMiddleware,
710
+ createTenantMiddleware,
711
+ createThrottleMiddleware,
712
+ createTracingMiddleware,
713
+ isPublicReadsEnabled,
714
+ requestIdMiddleware,
715
+ withMiddleware
716
+ } from "@getstrata/core";
717
+
718
+ // ../../src/config/frontend.ts
719
+ function readFrontendMode() {
720
+ const mode = (process.env.FRONTEND_MODE ?? "api").trim();
721
+ if (mode === "server-htmx") {
722
+ return "server-htmx";
723
+ }
724
+ if (mode === "spa-react") {
725
+ return "spa-react";
726
+ }
727
+ return "api";
728
+ }
729
+ function isViewsEnabled() {
730
+ return readFrontendMode() === "server-htmx";
731
+ }
732
+
733
+ // ../../src/config/rateLimit.ts
734
+ var LOCAL_LOGIN_RATE_LIMIT = {
735
+ maxAttempts: 100,
736
+ decaySeconds: 60
737
+ };
738
+ var PRODUCTION_LOGIN_RATE_LIMIT = {
739
+ maxAttempts: 5,
740
+ decaySeconds: 900
741
+ };
742
+ function isLocalAppEnv() {
743
+ return (process.env.APP_ENV ?? "local") === "local";
744
+ }
745
+ function parsePositiveInt(value, fallback) {
746
+ const parsed = Number(value);
747
+ if (!Number.isFinite(parsed) || parsed <= 0) {
748
+ return fallback;
749
+ }
750
+ return Math.trunc(parsed);
751
+ }
752
+ function resolveLoginRateLimit() {
753
+ const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
754
+ return {
755
+ maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
756
+ decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
757
+ };
758
+ }
759
+ function resolveRegisterRateLimit() {
760
+ const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
761
+ return {
762
+ maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
763
+ decaySeconds: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS ?? process.env.REGISTER_RATE_LIMIT_WINDOW_MS ? String(Number(process.env.REGISTER_RATE_LIMIT_WINDOW_MS) / 1000) : undefined, defaults.decaySeconds)
764
+ };
765
+ }
766
+
767
+ // ../../src/bootstrap/config.ts
768
+ import {
769
+ CORE_AUTH_TOKEN,
770
+ CORE_CACHE_TOKEN,
771
+ CORE_CONFIG_TOKEN,
772
+ CORE_EVENT_BUS_TOKEN,
773
+ CORE_POLICY_GATE_TOKEN,
774
+ CORE_QUEUE_TOKEN,
775
+ CORE_TOKEN_SERVICE_TOKEN
776
+ } from "@getstrata/core/contracts/serviceTokens.ts";
777
+ var APP_PORT_CONFIG_KEY = "app.port";
778
+ var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
779
+ var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
780
+ var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
781
+ var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
782
+ var DATABASE_URL_CONFIG_KEY = "database.url";
783
+ var DEFAULT_APP_PORT = 3000;
784
+ var DEFAULT_CACHE_TTL_MS = 3600000;
785
+ var DEFAULT_CACHE_MAX_ENTRIES = 100;
786
+ var DEFAULT_CACHE_DRIVER = "array";
787
+ var DEFAULT_API_TOKEN = "";
788
+ var DEFAULT_QUEUE_DRIVER = "sync";
789
+
790
+ // ../../src/bootstrap/httpKernel.ts
791
+ class HttpKernel {
792
+ dependencies;
793
+ constructor(dependencies) {
794
+ this.dependencies = dependencies;
795
+ }
796
+ globalMiddleware() {
797
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
798
+ return [
799
+ createCorsMiddleware(),
800
+ createSecurityHeadersMiddleware(),
801
+ createBodySizeLimitMiddleware(),
802
+ createTracingMiddleware(),
803
+ createMetricsMiddleware(),
804
+ createRequestLoggingMiddleware(),
805
+ requestIdMiddleware,
806
+ createAuthMiddleware(auth),
807
+ createMembershipMiddleware(),
808
+ createTenantMiddleware()
809
+ ];
810
+ }
811
+ group(name) {
812
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
813
+ switch (name) {
814
+ case "authenticated":
815
+ return [createRequireAuthMiddleware(auth)];
816
+ case "web":
817
+ return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
818
+ case "api": {
819
+ if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
820
+ return [];
821
+ }
822
+ const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
823
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
824
+ if (!redisUrl) {
825
+ const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
826
+ return [
827
+ createMemoryThrottleMiddleware({
828
+ maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
829
+ decaySeconds: 60
830
+ })
831
+ ];
832
+ }
833
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
834
+ return [
835
+ createThrottleMiddleware({
836
+ redisUrl,
837
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
838
+ decaySeconds: 60
839
+ })
840
+ ];
841
+ }
842
+ default:
843
+ throw new Error(`Unknown middleware group "${name}".`);
844
+ }
845
+ }
846
+ wrap(groups, handler) {
847
+ const names = Array.isArray(groups) ? groups : [groups];
848
+ const middleware = names.flatMap((name) => this.group(name));
849
+ if (middleware.length === 0) {
850
+ return handler;
851
+ }
852
+ return withMiddleware(...middleware)(handler);
853
+ }
854
+ wrapApi(handler) {
855
+ return this.wrap(["api", "authenticated"], handler);
856
+ }
857
+ wrapWeb(handler) {
858
+ return handler;
859
+ }
860
+ wrapWebPublicRead(handler) {
861
+ if (isPublicReadsEnabled()) {
862
+ return this.wrapWeb(handler);
863
+ }
864
+ return this.wrapWebAuthenticated(handler);
865
+ }
866
+ wrapWebAuthenticated(handler) {
867
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
868
+ return withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
869
+ }
870
+ wrapWebAbility(ability, handler) {
871
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
872
+ const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
873
+ const requireAbility = createRequireAbilityMiddleware(abilityChecker);
874
+ const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
875
+ return withMiddleware(...middleware)(handler);
876
+ }
877
+ wrapWebGlobalAdmin(handler) {
878
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
879
+ const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
880
+ return withMiddleware(...middleware)(handler);
881
+ }
882
+ wrapAuthenticated(handler) {
883
+ return this.wrap("authenticated", handler);
884
+ }
885
+ wrapPublicRead(handler) {
886
+ if (isPublicReadsEnabled()) {
887
+ return handler;
888
+ }
889
+ return this.wrapAuthenticated(handler);
890
+ }
891
+ wrapGlobalAdmin(handler) {
892
+ const middleware = [...this.group("authenticated"), createRequireGlobalAdminMiddleware()];
893
+ return withMiddleware(...middleware)(handler);
894
+ }
895
+ wrapAbility(ability, handler) {
896
+ const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
897
+ const requireAbility = createRequireAbilityMiddleware(abilityChecker);
898
+ const middleware = [...this.group("authenticated"), requireAbility(ability)];
899
+ return withMiddleware(...middleware)(handler);
900
+ }
901
+ wrapPolicy(resource, action, handler) {
902
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
903
+ const gate = this.dependencies.container.resolve(CORE_POLICY_GATE_TOKEN);
904
+ return withMiddleware(createAuthorizeMiddleware(gate, auth, resource, action))(handler);
905
+ }
906
+ wrapLogin(handler) {
907
+ return this.wrapThrottle("login", resolveLoginRateLimit(), handler);
908
+ }
909
+ wrapRegister(handler) {
910
+ return this.wrapThrottle("register", resolveRegisterRateLimit(), handler);
911
+ }
912
+ wrapThrottle(scope, rateLimit, handler) {
913
+ const middleware = [];
914
+ const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
915
+ if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
916
+ const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
917
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
918
+ if (redisUrl) {
919
+ const throttle = scope === "login" ? createLoginThrottleMiddleware({
920
+ redisUrl,
921
+ maxAttempts: rateLimit.maxAttempts,
922
+ decaySeconds: rateLimit.decaySeconds
923
+ }) : createThrottleMiddleware({
924
+ redisUrl,
925
+ maxAttempts: rateLimit.maxAttempts,
926
+ decaySeconds: rateLimit.decaySeconds,
927
+ keyPrefix: memoryKeyPrefix
928
+ });
929
+ middleware.push(throttle);
930
+ } else {
931
+ middleware.push(createMemoryThrottleMiddleware({
932
+ maxAttempts: rateLimit.maxAttempts,
933
+ decaySeconds: rateLimit.decaySeconds,
934
+ keyPrefix: memoryKeyPrefix
935
+ }));
936
+ }
937
+ } else {
938
+ middleware.push(createMemoryThrottleMiddleware({
939
+ maxAttempts: rateLimit.maxAttempts,
940
+ decaySeconds: rateLimit.decaySeconds,
941
+ keyPrefix: memoryKeyPrefix
942
+ }));
943
+ }
944
+ if (middleware.length === 0) {
945
+ return handler;
946
+ }
947
+ return withMiddleware(...middleware)(handler);
948
+ }
949
+ }
950
+ function createHttpKernel(dependencies) {
951
+ return new HttpKernel(dependencies);
952
+ }
953
+
954
+ // ../../src/bootstrap/discoverModules.ts
955
+ import { readdirSync } from "fs";
956
+ import { join } from "path";
957
+ import { pathToFileURL } from "url";
958
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
959
+ function readDiscoverModulesState() {
960
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
961
+ if (existing) {
962
+ return existing;
963
+ }
964
+ const state = { appModules: [] };
965
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
966
+ return state;
967
+ }
968
+ function configureModulesDirectory(modulesDir) {
969
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
970
+ }
971
+ function resolveModulesDirectory(options) {
972
+ const state = readDiscoverModulesState();
973
+ if (options?.modulesDir) {
974
+ return options.modulesDir;
975
+ }
976
+ if (state.configuredModulesDir) {
977
+ return state.configuredModulesDir;
978
+ }
979
+ return join(import.meta.dir, "../modules");
980
+ }
981
+ async function loadDiscoveredModules(options) {
982
+ const modulesDirectory = resolveModulesDirectory(options);
983
+ let moduleNames;
984
+ try {
985
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
986
+ } catch (error) {
987
+ if (error.code === "ENOENT") {
988
+ return [];
989
+ }
990
+ throw error;
991
+ }
992
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
993
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
994
+ const loaded = await import(moduleUrl);
995
+ return loaded.default;
996
+ }));
997
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
998
+ }
999
+ async function ensureModulesLoaded(options) {
1000
+ const state = readDiscoverModulesState();
1001
+ if (state.appModules.length > 0) {
1002
+ return state.appModules;
1003
+ }
1004
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
1005
+ state.appModules.splice(0, state.appModules.length, ...modules);
1006
+ return state.appModules;
1007
+ });
1008
+ return state.modulesReady;
1009
+ }
1010
+ function discoverModules() {
1011
+ return readDiscoverModulesState().appModules;
1012
+ }
1013
+ // ../../src/bootstrap/prefixRouteMap.ts
1014
+ function prefixRouteMap(prefix, routes) {
1015
+ const normalizedPrefix = prefix.replace(/\/$/, "");
1016
+ const prefixed = {};
1017
+ for (const [path, handler] of Object.entries(routes)) {
1018
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1019
+ prefixed[`${normalizedPrefix}${normalizedPath}`] = handler;
1020
+ }
1021
+ return prefixed;
1022
+ }
1023
+
1024
+ // ../../src/bootstrap/routeRegistry.ts
1025
+ class RouteRegistry {
1026
+ routes = [];
1027
+ register(route) {
1028
+ this.routes.push(route);
1029
+ }
1030
+ clear() {
1031
+ this.routes.length = 0;
1032
+ }
1033
+ list() {
1034
+ return [...this.routes].sort((left, right) => left.path.localeCompare(right.path));
1035
+ }
1036
+ }
1037
+ var ROUTE_REGISTRY_KEY = Symbol.for("@getstrata/routeRegistry");
1038
+ function readSharedRouteRegistry() {
1039
+ const globalRegistry = globalThis[ROUTE_REGISTRY_KEY];
1040
+ if (globalRegistry) {
1041
+ return globalRegistry;
1042
+ }
1043
+ const registry = new RouteRegistry;
1044
+ globalThis[ROUTE_REGISTRY_KEY] = registry;
1045
+ return registry;
1046
+ }
1047
+ var routeRegistry = readSharedRouteRegistry();
1048
+
1049
+ // ../../src/bootstrap/buildModuleRoutes.ts
1050
+ function registerOpenApiRoute(method, path, middleware) {
1051
+ routeRegistry.register({ method, path, middleware });
1052
+ }
1053
+ function registerOpenApiRouteMap(routes, middleware) {
1054
+ const registered = {};
1055
+ for (const [path, handler] of Object.entries(routes)) {
1056
+ if (handler && typeof handler === "object" && !Array.isArray(handler)) {
1057
+ const methodMap = handler;
1058
+ registered[path] = methodMap;
1059
+ for (const method of Object.keys(methodMap)) {
1060
+ registerOpenApiRoute(method.toUpperCase(), path, middleware);
1061
+ }
1062
+ continue;
1063
+ }
1064
+ registered[path] = handler;
1065
+ registerOpenApiRoute("GET", path, middleware);
1066
+ }
1067
+ return registered;
1068
+ }
1069
+ function createCachedJson(dependencies) {
1070
+ return async (cacheKey, loader, tags = [], request) => {
1071
+ const data = tags.length > 0 ? await dependencies.cache.tags(...tags).remember(cacheKey, loader) : await dependencies.cache.remember(cacheKey, loader);
1072
+ return conditionalJsonResponse(request, data);
1073
+ };
1074
+ }
1075
+ function buildModuleRoutes(dependencies, options = {}) {
1076
+ const { apiPrefix = "", modules = discoverModules(), clearRegistry = true } = options;
1077
+ if (clearRegistry) {
1078
+ routeRegistry.clear();
1079
+ }
1080
+ const kernel = createHttpKernel(dependencies);
1081
+ const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
1082
+ const cachedJson = createCachedJson(dependencies);
1083
+ const moduleRoutes = {};
1084
+ for (const module of modules) {
1085
+ if (!module.routes) {
1086
+ continue;
1087
+ }
1088
+ Object.assign(moduleRoutes, module.routes({ dependencies, cachedJson, kernel }));
1089
+ }
1090
+ const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
1091
+ return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
1092
+ }
1093
+ // ../../src/bootstrap/cache/modelCacheTags.ts
1094
+ function cacheTagsForModelWrite(tableName, action) {
1095
+ const module = discoverModules().find((entry) => entry.tableName === tableName);
1096
+ const baseTags = module?.cacheTags ?? [`${tableName}s`];
1097
+ const isDelete = action === "deleted" || action === "force-deleted";
1098
+ const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
1099
+ return [...new Set([...baseTags, ...extraTags])];
1100
+ }
1101
+ function discoverModelTableNames() {
1102
+ return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
1103
+ }
1104
+ // ../../src/bootstrap/contracts.ts
1105
+ import {
1106
+ ConfigStore,
1107
+ ServiceContainer
1108
+ } from "@getstrata/core/contracts/container";
1109
+ import {
1110
+ assertAppDependenciesComplete,
1111
+ getRequiredDependency,
1112
+ resolveService
1113
+ } from "@getstrata/core/contracts/di";
1114
+
1115
+ // ../../src/config/auth.ts
1116
+ var authConfig = {
1117
+ allowDevHeaders: (process.env.AUTH_DEV_HEADERS ?? "true") !== "false",
1118
+ tokenDefaultAbilities: ["*"]
1119
+ };
1120
+
1121
+ // ../../src/domain/abilities.ts
1122
+ var MEMBER_ABILITIES = [
1123
+ "organizations:read",
742
1124
  "projects:read",
743
1125
  "projects:create",
744
1126
  "tasks:read",
@@ -3214,1539 +3596,1268 @@ class Blueprint {
3214
3596
  kind: "fullText"
3215
3597
  });
3216
3598
  }
3217
- ginIndex(column, name) {
3218
- this.indexes.push({
3219
- name,
3220
- columns: [column],
3221
- kind: "gin"
3222
- });
3223
- }
3224
- }
3225
- // ../../src/core/database/schema/errors.ts
3226
- class UnsupportedSchemaFeatureError extends Error {
3227
- constructor(feature, driver) {
3228
- super(`${feature} is not supported for the ${driver} driver`);
3229
- this.name = "UnsupportedSchemaFeatureError";
3230
- }
3231
- }
3232
- // ../../src/core/database/schema/grammars/grammar.ts
3233
- function compileColumnType(driver, column) {
3234
- switch (column.kind) {
3235
- case "id":
3236
- return compileIdType(driver);
3237
- case "string":
3238
- return compileStringType(driver, column.length);
3239
- case "text":
3240
- return compileTextType(driver);
3241
- case "boolean":
3242
- return compileBooleanType(driver);
3243
- case "integer":
3244
- case "foreignId":
3245
- return compileIntegerType(driver);
3246
- case "bigInteger":
3247
- return compileBigIntegerType(driver);
3248
- case "timestamp":
3249
- return compileTimestampType(driver);
3250
- case "json":
3251
- return compileJsonType(driver);
3252
- case "jsonb":
3253
- return compileJsonbType(driver);
3254
- default:
3255
- throw new Error(`Unsupported column kind: ${column.kind}`);
3256
- }
3257
- }
3258
- function compileIdType(driver) {
3259
- switch (driver) {
3260
- case "pgsql":
3261
- return "SERIAL";
3262
- case "mysql":
3263
- return "BIGINT UNSIGNED";
3264
- case "sqlite":
3265
- return "INTEGER";
3266
- }
3267
- }
3268
- function compileStringType(driver, length) {
3269
- switch (driver) {
3270
- case "pgsql":
3271
- return "TEXT";
3272
- case "mysql":
3273
- return length ? `VARCHAR(${length})` : "VARCHAR(255)";
3274
- case "sqlite":
3275
- return "TEXT";
3276
- }
3277
- }
3278
- function compileTextType(driver) {
3279
- switch (driver) {
3280
- case "pgsql":
3281
- case "sqlite":
3282
- return "TEXT";
3283
- case "mysql":
3284
- return "TEXT";
3285
- }
3286
- }
3287
- function compileBooleanType(driver) {
3288
- switch (driver) {
3289
- case "pgsql":
3290
- return "BOOLEAN";
3291
- case "mysql":
3292
- return "BOOLEAN";
3293
- case "sqlite":
3294
- return "INTEGER";
3295
- }
3296
- }
3297
- function compileIntegerType(driver) {
3298
- switch (driver) {
3299
- case "pgsql":
3300
- return "INTEGER";
3301
- case "mysql":
3302
- return "INT";
3303
- case "sqlite":
3304
- return "INTEGER";
3305
- }
3306
- }
3307
- function compileBigIntegerType(driver) {
3308
- switch (driver) {
3309
- case "pgsql":
3310
- return "BIGINT";
3311
- case "mysql":
3312
- return "BIGINT";
3313
- case "sqlite":
3314
- return "INTEGER";
3315
- }
3316
- }
3317
- function compileTimestampType(driver) {
3318
- switch (driver) {
3319
- case "pgsql":
3320
- return "TIMESTAMPTZ";
3321
- case "mysql":
3322
- return "TIMESTAMP";
3323
- case "sqlite":
3324
- return "TEXT";
3325
- }
3326
- }
3327
- function compileJsonType(driver) {
3328
- switch (driver) {
3329
- case "pgsql":
3330
- return "JSONB";
3331
- case "mysql":
3332
- return "JSON";
3333
- case "sqlite":
3334
- return "TEXT";
3335
- }
3336
- }
3337
- function compileJsonbType(driver) {
3338
- switch (driver) {
3339
- case "pgsql":
3340
- return "JSONB";
3341
- case "mysql":
3342
- return "JSON";
3343
- case "sqlite":
3344
- return "TEXT";
3345
- }
3346
- }
3347
-
3348
- // ../../src/core/database/schema/grammars/compileStatements.ts
3349
- function compileCreateTable(driver, blueprint) {
3350
- const table = quoteIdentifier(blueprint.table);
3351
- const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
3352
- for (const index of blueprint.indexes) {
3353
- if (index.kind === "unique" && index.columns.length > 1) {
3354
- const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3355
- parts.push(`UNIQUE (${columns})`);
3356
- }
3357
- }
3358
- const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
3359
- ${parts.join(`,
3360
- `)}
3361
- )`];
3362
- for (const index of blueprint.indexes) {
3363
- if (index.kind === "unique" && index.columns.length === 1) {
3364
- continue;
3365
- }
3366
- if (index.kind === "index") {
3367
- statements.push(compileIndex(driver, blueprint.table, index));
3368
- } else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
3369
- statements.push(...compileSpecialIndex(driver, blueprint.table, index));
3370
- }
3371
- }
3372
- return statements;
3373
- }
3374
- function compileAlterTable(driver, blueprint) {
3375
- const statements = [];
3376
- const table = quoteIdentifier(blueprint.table);
3377
- for (const column of blueprint.columns) {
3378
- const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
3379
- statements.push(`ALTER TABLE ${table}
3380
- ${addPrefix} ${compileColumn(driver, column, "alter")}`);
3381
- }
3382
- for (const columnName of blueprint.droppedColumns) {
3383
- const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
3384
- statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
3385
- }
3386
- for (const indexName of blueprint.droppedIndexes) {
3387
- statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
3388
- }
3389
- for (const index of blueprint.indexes) {
3390
- if (index.kind === "index" || index.kind === "unique") {
3391
- statements.push(compileIndex(driver, blueprint.table, index));
3392
- } else {
3393
- statements.push(...compileSpecialIndex(driver, blueprint.table, index));
3394
- }
3395
- }
3396
- return statements;
3397
- }
3398
- function compileDropTable(driver, tableName) {
3399
- const cascade = driver === "pgsql" ? " CASCADE" : "";
3400
- return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
3401
- }
3402
- function compileColumn(driver, column, mode) {
3403
- const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
3404
- if (column.autoIncrement && driver === "mysql") {
3405
- parts[1] = `${parts[1]} AUTO_INCREMENT`;
3406
- }
3407
- if (column.isPrimary && mode === "create") {
3408
- if (driver === "sqlite") {
3409
- parts.push("PRIMARY KEY AUTOINCREMENT");
3410
- } else {
3411
- parts.push("PRIMARY KEY");
3412
- }
3413
- } else if (!column.isNullable) {
3414
- parts.push("NOT NULL");
3415
- } else if (column.isNullable) {
3416
- parts.push("NULL");
3417
- }
3418
- if (column.defaultValue !== undefined) {
3419
- parts.push(`DEFAULT ${column.defaultValue}`);
3420
- }
3421
- if (column.isUnique) {
3422
- parts.push("UNIQUE");
3423
- }
3424
- if (column.checkExpression) {
3425
- parts.push(`CHECK (${column.checkExpression})`);
3426
- }
3427
- if (column.foreignKey) {
3428
- const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
3429
- const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
3430
- let clause = `REFERENCES ${reference}`;
3431
- if (onDelete === "cascade") {
3432
- clause += " ON DELETE CASCADE";
3433
- } else if (onDelete === "set null") {
3434
- clause += " ON DELETE SET NULL";
3435
- }
3436
- parts.push(clause);
3599
+ ginIndex(column, name) {
3600
+ this.indexes.push({
3601
+ name,
3602
+ columns: [column],
3603
+ kind: "gin"
3604
+ });
3437
3605
  }
3438
- return parts.join(" ");
3439
- }
3440
- function compileIndex(_driver, tableName, index) {
3441
- const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
3442
- const columns = index.columns.map((column) => {
3443
- const quoted = quoteIdentifier(column);
3444
- if (index.order === "desc") {
3445
- return `${quoted} DESC`;
3446
- }
3447
- return quoted;
3448
- }).join(", ");
3449
- const unique = index.kind === "unique" ? "UNIQUE " : "";
3450
- return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
3451
3606
  }
3452
- function compileSpecialIndex(driver, tableName, index) {
3453
- const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
3454
- const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3455
- switch (index.kind) {
3456
- case "partial":
3457
- case "uniquePartial": {
3458
- if (driver !== "pgsql") {
3459
- throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
3460
- }
3461
- const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
3462
- return [
3463
- `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
3464
- ];
3465
- }
3466
- case "gin": {
3467
- if (driver !== "pgsql") {
3468
- throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
3469
- }
3470
- return [
3471
- `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
3472
- ];
3473
- }
3474
- case "fullText": {
3475
- if (driver === "mysql") {
3476
- return [
3477
- `CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
3478
- ];
3479
- }
3480
- if (driver === "pgsql") {
3481
- throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
3482
- }
3483
- throw new UnsupportedSchemaFeatureError("fullText()", driver);
3484
- }
3485
- default:
3486
- return [];
3607
+ // ../../src/core/database/schema/errors.ts
3608
+ class UnsupportedSchemaFeatureError extends Error {
3609
+ constructor(feature, driver) {
3610
+ super(`${feature} is not supported for the ${driver} driver`);
3611
+ this.name = "UnsupportedSchemaFeatureError";
3487
3612
  }
3488
3613
  }
3489
- function defaultIndexName(tableName, columns, kind) {
3490
- return `idx_${tableName}_${columns.join("_")}_${kind}`;
3491
- }
3492
- function compileBlueprint(driver, blueprint) {
3493
- switch (blueprint.action) {
3494
- case "create":
3495
- return compileCreateTable(driver, blueprint);
3496
- case "alter":
3497
- return compileAlterTable(driver, blueprint);
3498
- case "drop":
3499
- return compileDropTable(driver, blueprint.table);
3614
+ // ../../src/core/database/schema/grammars/grammar.ts
3615
+ function compileColumnType(driver, column) {
3616
+ switch (column.kind) {
3617
+ case "id":
3618
+ return compileIdType(driver);
3619
+ case "string":
3620
+ return compileStringType(driver, column.length);
3621
+ case "text":
3622
+ return compileTextType(driver);
3623
+ case "boolean":
3624
+ return compileBooleanType(driver);
3625
+ case "integer":
3626
+ case "foreignId":
3627
+ return compileIntegerType(driver);
3628
+ case "bigInteger":
3629
+ return compileBigIntegerType(driver);
3630
+ case "timestamp":
3631
+ return compileTimestampType(driver);
3632
+ case "json":
3633
+ return compileJsonType(driver);
3634
+ case "jsonb":
3635
+ return compileJsonbType(driver);
3500
3636
  default:
3501
- throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
3637
+ throw new Error(`Unsupported column kind: ${column.kind}`);
3502
3638
  }
3503
3639
  }
3504
- // ../../src/core/database/schema/grammars/createGrammar.ts
3505
- function createGrammar(driver) {
3506
- return {
3507
- driver,
3508
- compile(blueprint) {
3509
- return compileBlueprint(driver, blueprint);
3510
- }
3511
- };
3512
- }
3513
-
3514
- // ../../src/core/database/schema/grammars/mysqlGrammar.ts
3515
- var MySqlGrammar = createGrammar("mysql");
3516
-
3517
- // ../../src/core/database/schema/grammars/postgresGrammar.ts
3518
- var PostgresGrammar = createGrammar("pgsql");
3519
-
3520
- // ../../src/core/database/schema/grammars/sqliteGrammar.ts
3521
- var SqliteGrammar = createGrammar("sqlite");
3522
-
3523
- // ../../src/core/database/schema/grammars/index.ts
3524
- function grammarForDriver(driver) {
3640
+ function compileIdType(driver) {
3525
3641
  switch (driver) {
3526
3642
  case "pgsql":
3527
- return PostgresGrammar;
3643
+ return "SERIAL";
3528
3644
  case "mysql":
3529
- return MySqlGrammar;
3645
+ return "BIGINT UNSIGNED";
3530
3646
  case "sqlite":
3531
- return SqliteGrammar;
3532
- default:
3533
- throw new Error(`Unsupported database driver: ${driver}`);
3647
+ return "INTEGER";
3534
3648
  }
3535
3649
  }
3536
- // ../../src/core/database/schema/schema.ts
3537
- class SchemaBuilder {
3538
- #driver;
3539
- #statements = [];
3540
- constructor(driver) {
3541
- this.#driver = driver;
3542
- }
3543
- create(table, callback) {
3544
- const blueprint = new Blueprint(table, "create");
3545
- callback(blueprint);
3546
- this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
3547
- return this;
3548
- }
3549
- table(table, callback) {
3550
- const blueprint = new Blueprint(table, "alter");
3551
- callback(blueprint);
3552
- this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
3553
- return this;
3554
- }
3555
- drop(table) {
3556
- const blueprint = new Blueprint(table, "drop");
3557
- this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
3558
- return this;
3559
- }
3560
- toSql() {
3561
- return [...this.#statements];
3650
+ function compileStringType(driver, length) {
3651
+ switch (driver) {
3652
+ case "pgsql":
3653
+ return "TEXT";
3654
+ case "mysql":
3655
+ return length ? `VARCHAR(${length})` : "VARCHAR(255)";
3656
+ case "sqlite":
3657
+ return "TEXT";
3562
3658
  }
3563
- async execute(db2) {
3564
- for (const statement of this.#statements) {
3565
- await db2.unsafe(statement);
3566
- }
3659
+ }
3660
+ function compileTextType(driver) {
3661
+ switch (driver) {
3662
+ case "pgsql":
3663
+ case "sqlite":
3664
+ return "TEXT";
3665
+ case "mysql":
3666
+ return "TEXT";
3567
3667
  }
3568
3668
  }
3569
- // ../../src/core/database/table.ts
3570
- function defineTable5(definition) {
3571
- return definition;
3669
+ function compileBooleanType(driver) {
3670
+ switch (driver) {
3671
+ case "pgsql":
3672
+ return "BOOLEAN";
3673
+ case "mysql":
3674
+ return "BOOLEAN";
3675
+ case "sqlite":
3676
+ return "INTEGER";
3677
+ }
3572
3678
  }
3573
- // ../../src/core/queue/failedJobTable.ts
3574
- var failedJobTable = defineTable5({
3575
- name: "failed_job",
3576
- primaryKey: "id",
3577
- columns: ["id", "job_name", "payload", "exception", "failed_at"],
3578
- defaultOrderBy: { column: "failed_at", direction: "DESC" }
3579
- });
3580
-
3581
- // ../../src/core/queue/failedJobRepository.ts
3582
- class FailedJobRepository extends baseRepository_default {
3583
- constructor() {
3584
- super(failedJobTable);
3679
+ function compileIntegerType(driver) {
3680
+ switch (driver) {
3681
+ case "pgsql":
3682
+ return "INTEGER";
3683
+ case "mysql":
3684
+ return "INT";
3685
+ case "sqlite":
3686
+ return "INTEGER";
3585
3687
  }
3586
3688
  }
3587
- var failedJobRepository_default = FailedJobRepository;
3588
-
3589
- // ../../src/core/queue/failedJobService.ts
3590
- class FailedJobService {
3591
- repository;
3592
- constructor(repository) {
3593
- this.repository = repository;
3689
+ function compileBigIntegerType(driver) {
3690
+ switch (driver) {
3691
+ case "pgsql":
3692
+ return "BIGINT";
3693
+ case "mysql":
3694
+ return "BIGINT";
3695
+ case "sqlite":
3696
+ return "INTEGER";
3594
3697
  }
3595
- async recordFailure(input) {
3596
- return await this.repository.create({
3597
- job_name: input.jobName,
3598
- payload: input.payload,
3599
- exception: input.exception,
3600
- failed_at: new Date
3601
- });
3698
+ }
3699
+ function compileTimestampType(driver) {
3700
+ switch (driver) {
3701
+ case "pgsql":
3702
+ return "TIMESTAMPTZ";
3703
+ case "mysql":
3704
+ return "TIMESTAMP";
3705
+ case "sqlite":
3706
+ return "TEXT";
3602
3707
  }
3603
- listRecent(limit = 50) {
3604
- return this.repository.findAll({
3605
- limit,
3606
- orderBy: { column: "failed_at", direction: "DESC" }
3607
- });
3708
+ }
3709
+ function compileJsonType(driver) {
3710
+ switch (driver) {
3711
+ case "pgsql":
3712
+ return "JSONB";
3713
+ case "mysql":
3714
+ return "JSON";
3715
+ case "sqlite":
3716
+ return "TEXT";
3608
3717
  }
3609
- async retry(id) {
3610
- const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
3611
- await this.repository.deleteById(id);
3612
- return failedJob;
3718
+ }
3719
+ function compileJsonbType(driver) {
3720
+ switch (driver) {
3721
+ case "pgsql":
3722
+ return "JSONB";
3723
+ case "mysql":
3724
+ return "JSON";
3725
+ case "sqlite":
3726
+ return "TEXT";
3613
3727
  }
3614
- async delete(id) {
3615
- const deleted = await this.repository.deleteById(id);
3616
- if (!deleted) {
3617
- throw new Error(`Failed job ${id} not found.`);
3728
+ }
3729
+
3730
+ // ../../src/core/database/schema/grammars/compileStatements.ts
3731
+ function compileCreateTable(driver, blueprint) {
3732
+ const table = quoteIdentifier(blueprint.table);
3733
+ const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
3734
+ for (const index of blueprint.indexes) {
3735
+ if (index.kind === "unique" && index.columns.length > 1) {
3736
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3737
+ parts.push(`UNIQUE (${columns})`);
3618
3738
  }
3619
3739
  }
3620
- async flush() {
3621
- const jobs = await this.repository.findAll();
3622
- let deleted = 0;
3623
- for (const job of jobs) {
3624
- if (await this.repository.deleteById(job.id)) {
3625
- deleted += 1;
3626
- }
3740
+ const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
3741
+ ${parts.join(`,
3742
+ `)}
3743
+ )`];
3744
+ for (const index of blueprint.indexes) {
3745
+ if (index.kind === "unique" && index.columns.length === 1) {
3746
+ continue;
3747
+ }
3748
+ if (index.kind === "index") {
3749
+ statements.push(compileIndex(driver, blueprint.table, index));
3750
+ } else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
3751
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
3627
3752
  }
3628
- return deleted;
3629
3753
  }
3754
+ return statements;
3630
3755
  }
3631
- var failedJobService_default = FailedJobService;
3632
-
3633
- // ../../src/core/queue/jobRegistry.ts
3634
- class JobRegistry {
3635
- factories = new Map;
3636
- instances = new WeakMap;
3637
- register(name, factory) {
3638
- this.factories.set(name, factory);
3756
+ function compileAlterTable(driver, blueprint) {
3757
+ const statements = [];
3758
+ const table = quoteIdentifier(blueprint.table);
3759
+ for (const column of blueprint.columns) {
3760
+ const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
3761
+ statements.push(`ALTER TABLE ${table}
3762
+ ${addPrefix} ${compileColumn(driver, column, "alter")}`);
3639
3763
  }
3640
- resolveName(job) {
3641
- return this.instances.get(job);
3764
+ for (const columnName of blueprint.droppedColumns) {
3765
+ const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
3766
+ statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
3642
3767
  }
3643
- track(name, job) {
3644
- this.instances.set(job, name);
3645
- return job;
3768
+ for (const indexName of blueprint.droppedIndexes) {
3769
+ statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
3646
3770
  }
3647
- create(name) {
3648
- const factory = this.factories.get(name);
3649
- if (!factory) {
3650
- return;
3771
+ for (const index of blueprint.indexes) {
3772
+ if (index.kind === "index" || index.kind === "unique") {
3773
+ statements.push(compileIndex(driver, blueprint.table, index));
3774
+ } else {
3775
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
3651
3776
  }
3652
- return factory();
3653
- }
3654
- names() {
3655
- return [...this.factories.keys()];
3656
3777
  }
3778
+ return statements;
3657
3779
  }
3658
- var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
3659
- function readSharedJobRegistry() {
3660
- const globalRegistry = globalThis[JOB_REGISTRY_KEY];
3661
- if (globalRegistry) {
3662
- return globalRegistry;
3663
- }
3664
- const registry = new JobRegistry;
3665
- globalThis[JOB_REGISTRY_KEY] = registry;
3666
- return registry;
3780
+ function compileDropTable(driver, tableName) {
3781
+ const cascade = driver === "pgsql" ? " CASCADE" : "";
3782
+ return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
3667
3783
  }
3668
- var jobRegistry = readSharedJobRegistry();
3669
-
3670
- // ../../src/core/queue/jobRunner.ts
3671
- async function runQueueJob(envelope, failedJobs) {
3672
- const job = jobRegistry.create(envelope.name);
3673
- if (!job) {
3674
- throw new Error(`Unknown job "${envelope.name}".`);
3784
+ function compileColumn(driver, column, mode) {
3785
+ const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
3786
+ if (column.autoIncrement && driver === "mysql") {
3787
+ parts[1] = `${parts[1]} AUTO_INCREMENT`;
3675
3788
  }
3676
- const attempts = envelope.attempts ?? 0;
3677
- try {
3678
- await job.handle(envelope.payload);
3679
- } catch (error) {
3680
- const nextAttempt = attempts + 1;
3681
- const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
3682
- if (nextAttempt < maxAttempts) {
3683
- const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
3684
- await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
3685
- await runQueueJob({
3686
- ...envelope,
3687
- attempts: nextAttempt
3688
- }, failedJobs);
3689
- return;
3789
+ if (column.isPrimary && mode === "create") {
3790
+ if (driver === "sqlite") {
3791
+ parts.push("PRIMARY KEY AUTOINCREMENT");
3792
+ } else {
3793
+ parts.push("PRIMARY KEY");
3690
3794
  }
3691
- await failedJobs.recordFailure({
3692
- jobName: envelope.name,
3693
- payload: envelope.payload,
3694
- exception: error instanceof Error ? error.stack ?? error.message : String(error)
3695
- });
3696
- throw error;
3795
+ } else if (!column.isNullable) {
3796
+ parts.push("NOT NULL");
3797
+ } else if (column.isNullable) {
3798
+ parts.push("NULL");
3697
3799
  }
3698
- }
3699
-
3700
- // ../../src/core/queue/redisQueue.ts
3701
- var {RedisClient: RedisClient2 } = globalThis.Bun;
3702
- var QUEUE_LIST_KEY = "workhub:queue:default";
3703
- var QUEUE_HIGH_KEY = "workhub:queue:high";
3704
- var QUEUE_LOW_KEY = "workhub:queue:low";
3705
- function queueKeyForPriority(priority = "default") {
3706
- switch (priority) {
3707
- case "high":
3708
- return QUEUE_HIGH_KEY;
3709
- case "low":
3710
- return QUEUE_LOW_KEY;
3711
- default:
3712
- return QUEUE_LIST_KEY;
3800
+ if (column.defaultValue !== undefined) {
3801
+ parts.push(`DEFAULT ${column.defaultValue}`);
3713
3802
  }
3714
- }
3715
- class RedisQueue {
3716
- client;
3717
- constructor(redisUrl) {
3718
- this.client = new RedisClient2(redisUrl);
3803
+ if (column.isUnique) {
3804
+ parts.push("UNIQUE");
3719
3805
  }
3720
- async dispatch(job, payload) {
3721
- const name = jobRegistry.resolveName(job);
3722
- if (!name) {
3723
- throw new Error("Job is not registered with the queue worker registry.");
3806
+ if (column.checkExpression) {
3807
+ parts.push(`CHECK (${column.checkExpression})`);
3808
+ }
3809
+ if (column.foreignKey) {
3810
+ const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
3811
+ const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
3812
+ let clause = `REFERENCES ${reference}`;
3813
+ if (onDelete === "cascade") {
3814
+ clause += " ON DELETE CASCADE";
3815
+ } else if (onDelete === "set null") {
3816
+ clause += " ON DELETE SET NULL";
3724
3817
  }
3725
- const envelope = {
3726
- name,
3727
- payload,
3728
- attempts: 0
3729
- };
3730
- const queueKey = queueKeyForPriority(job.priority);
3731
- await this.client.lpush(queueKey, JSON.stringify(envelope));
3818
+ parts.push(clause);
3732
3819
  }
3820
+ return parts.join(" ");
3733
3821
  }
3734
-
3735
- // ../../src/core/queue/resilientQueue.ts
3736
- class ResilientQueue {
3737
- failedJobs;
3738
- asyncDispatch;
3739
- constructor(failedJobs, asyncDispatch = false) {
3740
- this.failedJobs = failedJobs;
3741
- this.asyncDispatch = asyncDispatch;
3742
- }
3743
- async dispatch(job, payload) {
3744
- const name = jobRegistry.resolveName(job);
3745
- if (!name) {
3746
- throw new Error("Job is not registered with the queue worker registry.");
3822
+ function compileIndex(_driver, tableName, index) {
3823
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
3824
+ const columns = index.columns.map((column) => {
3825
+ const quoted = quoteIdentifier(column);
3826
+ if (index.order === "desc") {
3827
+ return `${quoted} DESC`;
3747
3828
  }
3748
- const envelope = {
3749
- name,
3750
- payload,
3751
- attempts: 0
3752
- };
3753
- if (this.asyncDispatch) {
3754
- setTimeout(() => {
3755
- runQueueJob(envelope, this.failedJobs).catch((error) => {
3756
- console.error("[ResilientQueue] Job failed:", error);
3757
- });
3758
- }, 0);
3759
- return;
3829
+ return quoted;
3830
+ }).join(", ");
3831
+ const unique = index.kind === "unique" ? "UNIQUE " : "";
3832
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
3833
+ }
3834
+ function compileSpecialIndex(driver, tableName, index) {
3835
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
3836
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3837
+ switch (index.kind) {
3838
+ case "partial":
3839
+ case "uniquePartial": {
3840
+ if (driver !== "pgsql") {
3841
+ throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
3842
+ }
3843
+ const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
3844
+ return [
3845
+ `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
3846
+ ];
3847
+ }
3848
+ case "gin": {
3849
+ if (driver !== "pgsql") {
3850
+ throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
3851
+ }
3852
+ return [
3853
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
3854
+ ];
3855
+ }
3856
+ case "fullText": {
3857
+ if (driver === "mysql") {
3858
+ return [
3859
+ `CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
3860
+ ];
3861
+ }
3862
+ if (driver === "pgsql") {
3863
+ throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
3864
+ }
3865
+ throw new UnsupportedSchemaFeatureError("fullText()", driver);
3760
3866
  }
3761
- await runQueueJob(envelope, this.failedJobs);
3867
+ default:
3868
+ return [];
3762
3869
  }
3763
3870
  }
3764
-
3765
- // ../../src/core/queue/publicQueue.ts
3766
- function createFailedJobService() {
3767
- return new failedJobService_default(new failedJobRepository_default);
3871
+ function defaultIndexName(tableName, columns, kind) {
3872
+ return `idx_${tableName}_${columns.join("_")}_${kind}`;
3768
3873
  }
3769
- function createTrackedJob(name, job) {
3770
- return jobRegistry.track(name, job);
3874
+ function compileBlueprint(driver, blueprint) {
3875
+ switch (blueprint.action) {
3876
+ case "create":
3877
+ return compileCreateTable(driver, blueprint);
3878
+ case "alter":
3879
+ return compileAlterTable(driver, blueprint);
3880
+ case "drop":
3881
+ return compileDropTable(driver, blueprint.table);
3882
+ default:
3883
+ throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
3884
+ }
3771
3885
  }
3772
- function createProductionQueue(driver, options = {}) {
3773
- options.registerJobs?.();
3774
- const failedJobs = options.failedJobs ?? createFailedJobService();
3775
- if (driver === "redis") {
3776
- if (!options.redisUrl) {
3777
- throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
3886
+ // ../../src/core/database/schema/grammars/createGrammar.ts
3887
+ function createGrammar(driver) {
3888
+ return {
3889
+ driver,
3890
+ compile(blueprint) {
3891
+ return compileBlueprint(driver, blueprint);
3778
3892
  }
3779
- return new RedisQueue(options.redisUrl);
3780
- }
3781
- return new ResilientQueue(failedJobs, driver === "async");
3893
+ };
3782
3894
  }
3783
3895
 
3784
- // ../../src/core/queue/createAppQueue.ts
3785
- var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
3786
- function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
3787
- return createProductionQueue(driver, {
3788
- redisUrl,
3789
- failedJobs,
3790
- registerJobs
3791
- });
3792
- }
3896
+ // ../../src/core/database/schema/grammars/mysqlGrammar.ts
3897
+ var MySqlGrammar = createGrammar("mysql");
3793
3898
 
3794
- // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
3795
- var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
3796
- function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
3797
- for (const tableName of discoverModelTableNames()) {
3798
- for (const action of MODEL_WRITE_ACTIONS) {
3799
- bus.listen(modelEventName(tableName, action), async () => {
3800
- const tags = cacheTagsForModelWrite(tableName, action);
3801
- if (tags.length === 0) {
3802
- return;
3803
- }
3804
- let cache;
3805
- let queue;
3806
- try {
3807
- cache = resolveApplicationCache();
3808
- queue = resolveApplicationQueue();
3809
- } catch {
3810
- return;
3811
- }
3812
- const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
3813
- await queue.dispatch(job, { tags });
3814
- });
3815
- }
3816
- }
3817
- }
3899
+ // ../../src/core/database/schema/grammars/postgresGrammar.ts
3900
+ var PostgresGrammar = createGrammar("pgsql");
3818
3901
 
3819
- // ../../src/bootstrap/providers/listeners.ts
3820
- var registeredListenerGroups = new Set;
3821
- function registerListenerGroup(name, register) {
3822
- if (registeredListenerGroups.has(name)) {
3823
- return;
3902
+ // ../../src/core/database/schema/grammars/sqliteGrammar.ts
3903
+ var SqliteGrammar = createGrammar("sqlite");
3904
+
3905
+ // ../../src/core/database/schema/grammars/index.ts
3906
+ function grammarForDriver(driver) {
3907
+ switch (driver) {
3908
+ case "pgsql":
3909
+ return PostgresGrammar;
3910
+ case "mysql":
3911
+ return MySqlGrammar;
3912
+ case "sqlite":
3913
+ return SqliteGrammar;
3914
+ default:
3915
+ throw new Error(`Unsupported database driver: ${driver}`);
3824
3916
  }
3825
- registeredListenerGroups.add(name);
3826
- register();
3827
3917
  }
3828
- var listenersProvider = {
3829
- name: "core.listeners",
3830
- boot() {
3831
- registerListenerGroup("cache.invalidate-on-model-write", () => {
3832
- registerInvalidateCacheOnModelWriteListeners();
3833
- });
3834
- for (const [index, registerListener] of discoverListeners().entries()) {
3835
- registerListenerGroup(`app.listener.${index}`, registerListener);
3836
- }
3918
+ // ../../src/core/database/schema/schema.ts
3919
+ class SchemaBuilder {
3920
+ #driver;
3921
+ #statements = [];
3922
+ constructor(driver) {
3923
+ this.#driver = driver;
3837
3924
  }
3838
- };
3839
- var listeners_default = listenersProvider;
3840
-
3841
- // ../../src/core/auth/policy.ts
3842
- var BLOCKED_POLICY_ACTIONS = new Set([
3843
- "constructor",
3844
- "toString",
3845
- "valueOf",
3846
- "hasOwnProperty",
3847
- "isPrototypeOf",
3848
- "propertyIsEnumerable",
3849
- "__proto__"
3850
- ]);
3851
-
3852
- class PolicyGate {
3853
- constructor() {}
3854
- policies = new Map;
3855
- register(resource, policy) {
3856
- this.policies.set(resource, policy);
3925
+ create(table, callback) {
3926
+ const blueprint = new Blueprint(table, "create");
3927
+ callback(blueprint);
3928
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
3929
+ return this;
3857
3930
  }
3858
- allows(resource, action, user, model) {
3859
- const policy = this.policies.get(resource);
3860
- if (!policy) {
3861
- return false;
3862
- }
3863
- if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
3864
- return false;
3865
- }
3866
- const handler = policy[action];
3867
- if (typeof handler !== "function") {
3868
- return false;
3869
- }
3870
- const resolvedUser = user === undefined ? currentAuthUser() : user;
3871
- return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
3931
+ table(table, callback) {
3932
+ const blueprint = new Blueprint(table, "alter");
3933
+ callback(blueprint);
3934
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
3935
+ return this;
3872
3936
  }
3873
- authorize(resource, action, user, model) {
3874
- if (!this.allows(resource, action, user, model)) {
3875
- throw new ForbiddenError;
3876
- }
3937
+ drop(table) {
3938
+ const blueprint = new Blueprint(table, "drop");
3939
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
3940
+ return this;
3877
3941
  }
3878
- }
3879
-
3880
- // ../../src/bootstrap/providers/policy.ts
3881
- var policyProvider = {
3882
- name: "core.policy",
3883
- register({ container }) {
3884
- container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
3942
+ toSql() {
3943
+ return [...this.#statements];
3885
3944
  }
3886
- };
3887
- var policy_default = policyProvider;
3888
-
3889
- // ../../src/core/jobs/dispatchWebhookJob.ts
3890
- import { createHmac as createHmac2 } from "crypto";
3891
- class DispatchWebhookJob extends Job {
3892
- maxAttempts = 3;
3893
- backoffMs = 2000;
3894
- async handle(payload) {
3895
- const rows = await repositoryConnection`
3896
- SELECT id, url, secret
3897
- FROM webhook
3898
- WHERE id = ${payload.webhookId} AND active = TRUE
3899
- LIMIT 1
3900
- `;
3901
- const webhook = rows[0];
3902
- if (!webhook) {
3903
- return;
3904
- }
3905
- const body = JSON.stringify({ event: payload.event, payload: payload.payload });
3906
- const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
3907
- assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
3908
- let responseStatus = null;
3909
- let errorMessage = null;
3910
- try {
3911
- const response = await safeFetch(webhook.url, {
3912
- method: "POST",
3913
- headers: {
3914
- "content-type": "application/json",
3915
- "x-workhub-signature": signature
3916
- },
3917
- body
3918
- }, { allowHttp: appConfig.env !== "production" });
3919
- responseStatus = response.status;
3920
- if (!response.ok) {
3921
- throw new Error(`Webhook delivery failed with status ${response.status}.`);
3922
- }
3923
- } catch (error) {
3924
- errorMessage = error instanceof Error ? error.message : String(error);
3925
- await repositoryConnection`
3926
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
3927
- VALUES (
3928
- ${webhook.id},
3929
- ${payload.event},
3930
- ${JSON.stringify(payload.payload)}::jsonb,
3931
- ${responseStatus},
3932
- ${errorMessage}
3933
- )
3934
- `;
3935
- throw error instanceof Error ? error : new Error(errorMessage);
3945
+ async execute(db2) {
3946
+ for (const statement of this.#statements) {
3947
+ await db2.unsafe(statement);
3936
3948
  }
3937
- await repositoryConnection`
3938
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
3939
- VALUES (
3940
- ${webhook.id},
3941
- ${payload.event},
3942
- ${JSON.stringify(payload.payload)}::jsonb,
3943
- ${responseStatus}
3944
- )
3945
- `;
3946
3949
  }
3947
3950
  }
3948
- var dispatchWebhookJob_default = DispatchWebhookJob;
3949
-
3950
- // ../../src/bootstrap/queue/defaultJobs.ts
3951
- function registerDefaultJobs() {
3952
- jobRegistry.register("cache.invalidate-tags", () => {
3953
- return new invalidateCacheTagsJob_default(resolveApplicationCache());
3954
- });
3955
- jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
3956
- }
3957
-
3958
- // ../../src/bootstrap/providers/queue.ts
3959
- var queueProvider = {
3960
- name: "core.queue",
3961
- register({ container, config }) {
3962
- const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
3963
- const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
3964
- config.set("queue.driver", driver);
3965
- const failedJobs = createFailedJobService();
3966
- container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
3967
- container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
3968
- }
3969
- };
3970
- var queue_default = queueProvider;
3951
+ // ../../src/core/database/table.ts
3952
+ function defineTable5(definition) {
3953
+ return definition;
3954
+ }
3955
+ // ../../src/core/queue/failedJobTable.ts
3956
+ var failedJobTable = defineTable5({
3957
+ name: "failed_job",
3958
+ primaryKey: "id",
3959
+ columns: ["id", "job_name", "payload", "exception", "failed_at"],
3960
+ defaultOrderBy: { column: "failed_at", direction: "DESC" }
3961
+ });
3971
3962
 
3972
- // ../../src/core/storage/storage.ts
3973
- import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3974
- import { dirname, join as join3 } from "path";
3975
- var {S3Client } = globalThis.Bun;
3963
+ // ../../src/core/queue/failedJobRepository.ts
3964
+ class FailedJobRepository extends baseRepository_default {
3965
+ constructor() {
3966
+ super(failedJobTable);
3967
+ }
3968
+ }
3969
+ var failedJobRepository_default = FailedJobRepository;
3976
3970
 
3977
- class LocalStorageDriver {
3978
- rootDirectory;
3979
- constructor(rootDirectory) {
3980
- this.rootDirectory = rootDirectory;
3971
+ // ../../src/core/queue/failedJobService.ts
3972
+ class FailedJobService {
3973
+ repository;
3974
+ constructor(repository) {
3975
+ this.repository = repository;
3981
3976
  }
3982
- resolveRootDirectory() {
3983
- return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3977
+ async recordFailure(input) {
3978
+ return await this.repository.create({
3979
+ job_name: input.jobName,
3980
+ payload: input.payload,
3981
+ exception: input.exception,
3982
+ failed_at: new Date
3983
+ });
3984
3984
  }
3985
- resolvePath(path) {
3986
- return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3985
+ listRecent(limit = 50) {
3986
+ return this.repository.findAll({
3987
+ limit,
3988
+ orderBy: { column: "failed_at", direction: "DESC" }
3989
+ });
3987
3990
  }
3988
- async put(path, contents) {
3989
- const absolutePath = this.resolvePath(path);
3990
- await mkdir(dirname(absolutePath), { recursive: true });
3991
- await writeFile(absolutePath, contents);
3992
- return path;
3991
+ async retry(id) {
3992
+ const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
3993
+ await this.repository.deleteById(id);
3994
+ return failedJob;
3993
3995
  }
3994
- async get(path) {
3995
- try {
3996
- return await readFile(this.resolvePath(path));
3997
- } catch {
3998
- return null;
3996
+ async delete(id) {
3997
+ const deleted = await this.repository.deleteById(id);
3998
+ if (!deleted) {
3999
+ throw new Error(`Failed job ${id} not found.`);
3999
4000
  }
4000
4001
  }
4001
- async delete(path) {
4002
- try {
4003
- await unlink(this.resolvePath(path));
4004
- return true;
4005
- } catch {
4006
- return false;
4002
+ async flush() {
4003
+ const jobs = await this.repository.findAll();
4004
+ let deleted = 0;
4005
+ for (const job of jobs) {
4006
+ if (await this.repository.deleteById(job.id)) {
4007
+ deleted += 1;
4008
+ }
4007
4009
  }
4010
+ return deleted;
4008
4011
  }
4009
4012
  }
4013
+ var failedJobService_default = FailedJobService;
4010
4014
 
4011
- class S3StorageDriver {
4012
- client;
4013
- constructor(client) {
4014
- this.client = client;
4015
+ // ../../src/core/queue/jobRegistry.ts
4016
+ class JobRegistry {
4017
+ factories = new Map;
4018
+ instances = new WeakMap;
4019
+ register(name, factory) {
4020
+ this.factories.set(name, factory);
4015
4021
  }
4016
- async put(path, contents) {
4017
- await this.client.write(path.replace(/^\/+/, ""), contents);
4018
- return path;
4022
+ resolveName(job) {
4023
+ return this.instances.get(job);
4019
4024
  }
4020
- async get(path) {
4021
- const normalizedPath = path.replace(/^\/+/, "");
4022
- const file = this.client.file(normalizedPath);
4023
- if (!await file.exists()) {
4024
- return null;
4025
- }
4026
- return new Uint8Array(await file.arrayBuffer());
4025
+ track(name, job) {
4026
+ this.instances.set(job, name);
4027
+ return job;
4027
4028
  }
4028
- async delete(path) {
4029
- try {
4030
- await this.client.unlink(path.replace(/^\/+/, ""));
4031
- return true;
4032
- } catch {
4033
- return false;
4029
+ create(name) {
4030
+ const factory = this.factories.get(name);
4031
+ if (!factory) {
4032
+ return;
4034
4033
  }
4034
+ return factory();
4035
+ }
4036
+ names() {
4037
+ return [...this.factories.keys()];
4038
+ }
4039
+ }
4040
+ var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
4041
+ function readSharedJobRegistry() {
4042
+ const globalRegistry = globalThis[JOB_REGISTRY_KEY];
4043
+ if (globalRegistry) {
4044
+ return globalRegistry;
4035
4045
  }
4046
+ const registry = new JobRegistry;
4047
+ globalThis[JOB_REGISTRY_KEY] = registry;
4048
+ return registry;
4036
4049
  }
4050
+ var jobRegistry = readSharedJobRegistry();
4037
4051
 
4038
- class StorageManager {
4039
- driver;
4040
- constructor(driver) {
4041
- this.driver = driver;
4052
+ // ../../src/core/queue/jobRunner.ts
4053
+ async function runQueueJob(envelope, failedJobs) {
4054
+ const job = jobRegistry.create(envelope.name);
4055
+ if (!job) {
4056
+ throw new Error(`Unknown job "${envelope.name}".`);
4042
4057
  }
4043
- put(path, contents) {
4044
- return this.driver.put(path, contents);
4058
+ const attempts = envelope.attempts ?? 0;
4059
+ try {
4060
+ await job.handle(envelope.payload);
4061
+ } catch (error) {
4062
+ const nextAttempt = attempts + 1;
4063
+ const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
4064
+ if (nextAttempt < maxAttempts) {
4065
+ const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
4066
+ await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
4067
+ await runQueueJob({
4068
+ ...envelope,
4069
+ attempts: nextAttempt
4070
+ }, failedJobs);
4071
+ return;
4072
+ }
4073
+ await failedJobs.recordFailure({
4074
+ jobName: envelope.name,
4075
+ payload: envelope.payload,
4076
+ exception: error instanceof Error ? error.stack ?? error.message : String(error)
4077
+ });
4078
+ throw error;
4045
4079
  }
4046
- get(path) {
4047
- return this.driver.get(path);
4080
+ }
4081
+
4082
+ // ../../src/core/queue/redisQueue.ts
4083
+ var {RedisClient: RedisClient2 } = globalThis.Bun;
4084
+ var QUEUE_LIST_KEY = "workhub:queue:default";
4085
+ var QUEUE_HIGH_KEY = "workhub:queue:high";
4086
+ var QUEUE_LOW_KEY = "workhub:queue:low";
4087
+ function queueKeyForPriority(priority = "default") {
4088
+ switch (priority) {
4089
+ case "high":
4090
+ return QUEUE_HIGH_KEY;
4091
+ case "low":
4092
+ return QUEUE_LOW_KEY;
4093
+ default:
4094
+ return QUEUE_LIST_KEY;
4048
4095
  }
4049
- delete(path) {
4050
- return this.driver.delete(path);
4096
+ }
4097
+ class RedisQueue {
4098
+ client;
4099
+ constructor(redisUrl) {
4100
+ this.client = new RedisClient2(redisUrl);
4101
+ }
4102
+ async dispatch(job, payload) {
4103
+ const name = jobRegistry.resolveName(job);
4104
+ if (!name) {
4105
+ throw new Error("Job is not registered with the queue worker registry.");
4106
+ }
4107
+ const envelope = {
4108
+ name,
4109
+ payload,
4110
+ attempts: 0
4111
+ };
4112
+ const queueKey = queueKeyForPriority(job.priority);
4113
+ await this.client.lpush(queueKey, JSON.stringify(envelope));
4051
4114
  }
4052
4115
  }
4053
- function resolveS3Config() {
4054
- const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
4055
- const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
4056
- const bucket = process.env.AWS_BUCKET?.trim();
4057
- if (!accessKeyId || !secretAccessKey || !bucket) {
4058
- throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
4116
+
4117
+ // ../../src/core/queue/resilientQueue.ts
4118
+ class ResilientQueue {
4119
+ failedJobs;
4120
+ asyncDispatch;
4121
+ constructor(failedJobs, asyncDispatch = false) {
4122
+ this.failedJobs = failedJobs;
4123
+ this.asyncDispatch = asyncDispatch;
4124
+ }
4125
+ async dispatch(job, payload) {
4126
+ const name = jobRegistry.resolveName(job);
4127
+ if (!name) {
4128
+ throw new Error("Job is not registered with the queue worker registry.");
4129
+ }
4130
+ const envelope = {
4131
+ name,
4132
+ payload,
4133
+ attempts: 0
4134
+ };
4135
+ if (this.asyncDispatch) {
4136
+ setTimeout(() => {
4137
+ runQueueJob(envelope, this.failedJobs).catch((error) => {
4138
+ console.error("[ResilientQueue] Job failed:", error);
4139
+ });
4140
+ }, 0);
4141
+ return;
4142
+ }
4143
+ await runQueueJob(envelope, this.failedJobs);
4059
4144
  }
4060
- return {
4061
- accessKeyId,
4062
- secretAccessKey,
4063
- bucket,
4064
- ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
4065
- ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
4066
- };
4067
4145
  }
4068
- function createS3Client(config = resolveS3Config()) {
4069
- return new S3Client({
4070
- accessKeyId: config.accessKeyId,
4071
- secretAccessKey: config.secretAccessKey,
4072
- bucket: config.bucket,
4073
- ...config.region ? { region: config.region } : {},
4074
- ...config.endpoint ? { endpoint: config.endpoint } : {}
4075
- });
4146
+
4147
+ // ../../src/core/queue/publicQueue.ts
4148
+ function createFailedJobService() {
4149
+ return new failedJobService_default(new failedJobRepository_default);
4076
4150
  }
4077
- function createStorageDriver() {
4078
- const driver = process.env.STORAGE_DRIVER ?? "local";
4079
- if (driver === "s3") {
4080
- return new S3StorageDriver(createS3Client());
4151
+ function createTrackedJob(name, job) {
4152
+ return jobRegistry.track(name, job);
4153
+ }
4154
+ function createProductionQueue(driver, options = {}) {
4155
+ options.registerJobs?.();
4156
+ const failedJobs = options.failedJobs ?? createFailedJobService();
4157
+ if (driver === "redis") {
4158
+ if (!options.redisUrl) {
4159
+ throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
4160
+ }
4161
+ return new RedisQueue(options.redisUrl);
4081
4162
  }
4082
- return new LocalStorageDriver;
4163
+ return new ResilientQueue(failedJobs, driver === "async");
4083
4164
  }
4084
4165
 
4085
- // ../../src/bootstrap/providers/storage.ts
4086
- var storageProvider = {
4087
- name: "core.storage",
4088
- register({ dependencies }) {
4089
- dependencies.storage = new StorageManager(createStorageDriver());
4090
- }
4091
- };
4092
- var storage_default = storageProvider;
4166
+ // ../../src/core/queue/createAppQueue.ts
4167
+ var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
4168
+ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
4169
+ return createProductionQueue(driver, {
4170
+ redisUrl,
4171
+ failedJobs,
4172
+ registerJobs
4173
+ });
4174
+ }
4093
4175
 
4094
- // ../../src/config/frontend.ts
4095
- function readFrontendMode() {
4096
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
4097
- if (mode === "server-htmx") {
4098
- return "server-htmx";
4099
- }
4100
- if (mode === "spa-react") {
4101
- return "spa-react";
4176
+ // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
4177
+ var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
4178
+ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
4179
+ for (const tableName of discoverModelTableNames()) {
4180
+ for (const action of MODEL_WRITE_ACTIONS) {
4181
+ bus.listen(modelEventName(tableName, action), async () => {
4182
+ const tags = cacheTagsForModelWrite(tableName, action);
4183
+ if (tags.length === 0) {
4184
+ return;
4185
+ }
4186
+ let cache;
4187
+ let queue;
4188
+ try {
4189
+ cache = resolveApplicationCache();
4190
+ queue = resolveApplicationQueue();
4191
+ } catch {
4192
+ return;
4193
+ }
4194
+ const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
4195
+ await queue.dispatch(job, { tags });
4196
+ });
4197
+ }
4102
4198
  }
4103
- return "api";
4104
- }
4105
- function isViewsEnabled() {
4106
- return readFrontendMode() === "server-htmx";
4107
4199
  }
4108
4200
 
4109
- // ../../src/core/http/requestMetaContext.ts
4110
- var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
4111
- function currentRequestMeta() {
4112
- return requestMetaContext.getStore() ?? {
4113
- ipAddress: null,
4114
- userAgent: null
4115
- };
4201
+ // ../../src/bootstrap/providers/listeners.ts
4202
+ var registeredListenerGroups = new Set;
4203
+ function registerListenerGroup(name, register) {
4204
+ if (registeredListenerGroups.has(name)) {
4205
+ return;
4206
+ }
4207
+ registeredListenerGroups.add(name);
4208
+ register();
4116
4209
  }
4210
+ var listenersProvider = {
4211
+ name: "core.listeners",
4212
+ boot() {
4213
+ registerListenerGroup("cache.invalidate-on-model-write", () => {
4214
+ registerInvalidateCacheOnModelWriteListeners();
4215
+ });
4216
+ for (const [index, registerListener] of discoverListeners().entries()) {
4217
+ registerListenerGroup(`app.listener.${index}`, registerListener);
4218
+ }
4219
+ }
4220
+ };
4221
+ var listeners_default = listenersProvider;
4117
4222
 
4118
- // ../../src/core/view/etaViewEngine.ts
4119
- import { join as join4 } from "path";
4120
- import { Eta } from "eta";
4121
- var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
4122
- var DEFAULT_LAYOUT = "layouts/app.eta";
4223
+ // ../../src/core/auth/policy.ts
4224
+ var BLOCKED_POLICY_ACTIONS = new Set([
4225
+ "constructor",
4226
+ "toString",
4227
+ "valueOf",
4228
+ "hasOwnProperty",
4229
+ "isPrototypeOf",
4230
+ "propertyIsEnumerable",
4231
+ "__proto__"
4232
+ ]);
4123
4233
 
4124
- class EtaViewEngine {
4125
- eta;
4126
- resolveLayoutData;
4127
- constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
4128
- this.eta = new Eta({
4129
- views: viewsDirectory,
4130
- autoTrim: false
4131
- });
4132
- this.resolveLayoutData = resolveLayoutData;
4234
+ class PolicyGate {
4235
+ constructor() {}
4236
+ policies = new Map;
4237
+ register(resource, policy) {
4238
+ this.policies.set(resource, policy);
4133
4239
  }
4134
- async render(name, data = {}, options = {}) {
4135
- const template = name.endsWith(".eta") ? name : `${name}.eta`;
4136
- const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
4137
- const mergedData = { ...layoutData, ...data };
4138
- const body = await this.eta.renderAsync(template, mergedData);
4139
- const layout = options.layout ?? DEFAULT_LAYOUT;
4140
- if (layout === false) {
4141
- return body;
4240
+ allows(resource, action, user, model) {
4241
+ const policy = this.policies.get(resource);
4242
+ if (!policy) {
4243
+ return false;
4142
4244
  }
4143
- const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
4144
- return await this.eta.renderAsync(layoutTemplate, {
4145
- ...mergedData,
4146
- body
4147
- });
4148
- }
4149
- }
4150
- // ../../src/core/view/htmlResponse.ts
4151
- function htmlResponse(html, init = {}) {
4152
- return new Response(html, {
4153
- status: init.status ?? 200,
4154
- statusText: init.statusText,
4155
- headers: {
4156
- "Content-Type": "text/html; charset=utf-8"
4245
+ if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
4246
+ return false;
4157
4247
  }
4158
- });
4159
- }
4160
- // ../../src/core/http/cookies.ts
4161
- function readRequestCookie(request, name) {
4162
- const cookies = request.cookies;
4163
- if (cookies && typeof cookies.get === "function") {
4164
- const value = cookies.get(name);
4165
- if (value) {
4166
- return value;
4248
+ const handler = policy[action];
4249
+ if (typeof handler !== "function") {
4250
+ return false;
4167
4251
  }
4252
+ const resolvedUser = user === undefined ? currentAuthUser() : user;
4253
+ return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
4168
4254
  }
4169
- const header = request.headers.get("cookie");
4170
- if (!header) {
4171
- return null;
4172
- }
4173
- for (const part of header.split(";")) {
4174
- const idx = part.indexOf("=");
4175
- if (idx === -1)
4176
- continue;
4177
- const cookieName = part.slice(0, idx).trim();
4178
- if (cookieName !== name)
4179
- continue;
4180
- return decodeURIComponent(part.slice(idx + 1).trim());
4255
+ authorize(resource, action, user, model) {
4256
+ if (!this.allows(resource, action, user, model)) {
4257
+ throw new ForbiddenError;
4258
+ }
4181
4259
  }
4182
- return null;
4183
4260
  }
4184
4261
 
4185
- // ../../src/core/http/csrfToken.ts
4186
- var CSRF_COOKIE = "workhub_csrf";
4187
- var CSRF_TTL_MS = 60 * 60 * 1000;
4188
- function resolveCsrfSecret() {
4189
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
4190
- }
4191
- function csrfVerifyOptions() {
4192
- return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
4193
- }
4194
- function createCsrfTokenCookie() {
4195
- const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
4196
- return {
4197
- token,
4198
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
4199
- };
4200
- }
4201
- function resolveCsrfToken(request) {
4202
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
4203
- if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
4204
- return { token: cookieValue };
4262
+ // ../../src/bootstrap/providers/policy.ts
4263
+ var policyProvider = {
4264
+ name: "core.policy",
4265
+ register({ container }) {
4266
+ container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
4267
+ }
4268
+ };
4269
+ var policy_default = policyProvider;
4270
+
4271
+ // ../../src/core/jobs/dispatchWebhookJob.ts
4272
+ import { createHmac as createHmac2 } from "crypto";
4273
+ class DispatchWebhookJob extends Job {
4274
+ maxAttempts = 3;
4275
+ backoffMs = 2000;
4276
+ async handle(payload) {
4277
+ const rows = await repositoryConnection`
4278
+ SELECT id, url, secret
4279
+ FROM webhook
4280
+ WHERE id = ${payload.webhookId} AND active = TRUE
4281
+ LIMIT 1
4282
+ `;
4283
+ const webhook = rows[0];
4284
+ if (!webhook) {
4285
+ return;
4286
+ }
4287
+ const body = JSON.stringify({ event: payload.event, payload: payload.payload });
4288
+ const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
4289
+ assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
4290
+ let responseStatus = null;
4291
+ let errorMessage = null;
4292
+ try {
4293
+ const response = await safeFetch(webhook.url, {
4294
+ method: "POST",
4295
+ headers: {
4296
+ "content-type": "application/json",
4297
+ "x-workhub-signature": signature
4298
+ },
4299
+ body
4300
+ }, { allowHttp: appConfig.env !== "production" });
4301
+ responseStatus = response.status;
4302
+ if (!response.ok) {
4303
+ throw new Error(`Webhook delivery failed with status ${response.status}.`);
4304
+ }
4305
+ } catch (error) {
4306
+ errorMessage = error instanceof Error ? error.message : String(error);
4307
+ await repositoryConnection`
4308
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
4309
+ VALUES (
4310
+ ${webhook.id},
4311
+ ${payload.event},
4312
+ ${JSON.stringify(payload.payload)}::jsonb,
4313
+ ${responseStatus},
4314
+ ${errorMessage}
4315
+ )
4316
+ `;
4317
+ throw error instanceof Error ? error : new Error(errorMessage);
4318
+ }
4319
+ await repositoryConnection`
4320
+ INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
4321
+ VALUES (
4322
+ ${webhook.id},
4323
+ ${payload.event},
4324
+ ${JSON.stringify(payload.payload)}::jsonb,
4325
+ ${responseStatus}
4326
+ )
4327
+ `;
4205
4328
  }
4206
- return createCsrfTokenCookie();
4207
4329
  }
4208
- function resolveCsrfTokenForRequest(request) {
4209
- const metaToken = currentRequestMeta().csrfToken;
4210
- if (metaToken) {
4211
- return metaToken;
4330
+ var dispatchWebhookJob_default = DispatchWebhookJob;
4331
+
4332
+ // ../../src/core/contracts/di.ts
4333
+ function getRequiredDependency2(dependencies, key) {
4334
+ const dependency = dependencies[key];
4335
+ if (dependency === undefined) {
4336
+ throw new Error(`Required dependency "${key}" is not registered.`);
4212
4337
  }
4213
- return resolveCsrfToken(request).token;
4338
+ return dependency;
4214
4339
  }
4215
4340
 
4216
- // ../../src/core/http/flashSession.ts
4217
- import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
4218
- var FLASH_COOKIE = "workhub_flash";
4219
- var FLASH_TTL_MS = 60 * 1000;
4220
- function resolveFlashSecret() {
4221
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
4222
- }
4223
- function signFlashPayload(payload, issuedAt) {
4224
- const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
4225
- return `${payload}.${issuedAt}.${signature}`;
4226
- }
4227
- function readFlashCookie(request) {
4228
- const cookieHeader = request.headers.get("cookie");
4229
- if (!cookieHeader) {
4230
- return null;
4341
+ // ../../src/core/contracts/serviceTokens.ts
4342
+ var CORE_POLICY_GATE_TOKEN2 = "core.policyGate";
4343
+ var CORE_AUTH_TOKEN2 = "core.auth";
4344
+
4345
+ // ../../src/core/runtime/applicationRegistry.ts
4346
+ var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
4347
+ var activeContext;
4348
+ function readStoredApplicationContext() {
4349
+ if (activeContext) {
4350
+ return activeContext;
4231
4351
  }
4232
- for (const part of cookieHeader.split(";")) {
4233
- const [name, ...rest] = part.trim().split("=");
4234
- if (name === FLASH_COOKIE) {
4235
- return decodeURIComponent(rest.join("="));
4236
- }
4352
+ const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
4353
+ if (globalContext) {
4354
+ activeContext = globalContext;
4237
4355
  }
4238
- return null;
4356
+ return activeContext;
4239
4357
  }
4240
- function parseFlashCookie(cookieValue) {
4241
- const parts = cookieValue.split(".");
4242
- if (parts.length < 3) {
4243
- return null;
4244
- }
4245
- const signature = parts.pop();
4246
- const issuedAtRaw = parts.pop();
4247
- const payload = parts.join(".");
4248
- if (!signature || !issuedAtRaw || !payload) {
4249
- return null;
4250
- }
4251
- const issuedAt = Number.parseInt(issuedAtRaw, 10);
4252
- if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
4253
- return null;
4254
- }
4255
- const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
4256
- if (!expectedSignature) {
4257
- return null;
4258
- }
4259
- const expectedBuffer = Buffer.from(expectedSignature);
4260
- const actualBuffer = Buffer.from(signature);
4261
- if (expectedBuffer.length !== actualBuffer.length) {
4262
- return null;
4263
- }
4264
- if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4265
- return null;
4266
- }
4267
- try {
4268
- const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
4269
- if (!parsed?.message || typeof parsed.message !== "string") {
4270
- return null;
4271
- }
4272
- if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
4273
- return null;
4274
- }
4275
- return parsed;
4276
- } catch {
4277
- return null;
4358
+ function requireActiveApplicationContext() {
4359
+ const context = readStoredApplicationContext();
4360
+ if (!context) {
4361
+ throw new Error("The application context has not been bootstrapped.");
4278
4362
  }
4363
+ return context;
4279
4364
  }
4280
- function pullFlash(request) {
4281
- const cookieValue = readFlashCookie(request);
4282
- if (!cookieValue) {
4283
- return null;
4284
- }
4285
- return parseFlashCookie(cookieValue);
4365
+ function resolveApplicationCache2() {
4366
+ return getRequiredDependency2(requireActiveApplicationContext().dependencies, "cache");
4367
+ }
4368
+ function resolveApplicationAuth2() {
4369
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN2);
4370
+ }
4371
+ function resolveApplicationPolicyGate2() {
4372
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN2);
4373
+ }
4374
+ function resolveApplicationDependencies2() {
4375
+ return requireActiveApplicationContext().dependencies;
4286
4376
  }
4287
4377
 
4288
- // ../../src/core/view/webLayoutData.ts
4289
- async function resolveWebLayoutData(container, request) {
4290
- const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
4291
- const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
4292
- const authUser = currentAuthUser();
4293
- if (!authUser) {
4294
- return { authUser: null, csrfToken, flash };
4295
- }
4296
- const userId = Number(authUser.id);
4297
- if (!Number.isInteger(userId) || userId <= 0) {
4298
- return { authUser: null, csrfToken, flash };
4299
- }
4300
- if (!container.has(tokenServiceToken)) {
4301
- return {
4302
- authUser: {
4303
- id: userId,
4304
- email: "",
4305
- role: authUser.role ?? "member"
4306
- },
4307
- csrfToken,
4308
- flash
4309
- };
4310
- }
4311
- const tokenService = container.resolve(tokenServiceToken);
4312
- try {
4313
- const user = await tokenService.findByIdOrThrow(userId);
4314
- return {
4315
- authUser: {
4316
- id: userId,
4317
- email: user.email ?? "",
4318
- role: authUser.role ?? user.role ?? "member"
4319
- },
4320
- csrfToken,
4321
- flash
4322
- };
4323
- } catch {
4324
- return { authUser: null, csrfToken, flash };
4325
- }
4378
+ // ../../src/bootstrap/queue/defaultJobs.ts
4379
+ function registerDefaultJobs() {
4380
+ jobRegistry.register("cache.invalidate-tags", () => {
4381
+ return new invalidateCacheTagsJob_default(resolveApplicationCache2());
4382
+ });
4383
+ jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
4326
4384
  }
4327
- // ../../src/bootstrap/providers/view.ts
4328
- var CORE_VIEW_TOKEN = "core.view";
4329
- var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
4330
- var viewProvider = {
4331
- name: "view",
4385
+
4386
+ // ../../src/bootstrap/providers/queue.ts
4387
+ var queueProvider = {
4388
+ name: "core.queue",
4332
4389
  register({ container, config }) {
4333
- if (!isViewsEnabled()) {
4334
- return;
4335
- }
4336
- const viewsDirectory = process.env.VIEW_DIRECTORY?.trim() || DEFAULT_VIEWS_DIRECTORY;
4337
- config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
4338
- container.set(CORE_VIEW_TOKEN, new EtaViewEngine(viewsDirectory, () => resolveWebLayoutData(container, currentRequestMeta().request)));
4390
+ const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
4391
+ const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
4392
+ config.set("queue.driver", driver);
4393
+ const failedJobs = createFailedJobService();
4394
+ container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
4395
+ container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
4339
4396
  }
4340
4397
  };
4398
+ var queue_default = queueProvider;
4341
4399
 
4342
- // ../../src/bootstrap/providers/index.ts
4343
- var coreProviders = [
4344
- config_default,
4345
- cache_default,
4346
- storage_default,
4347
- auth_default,
4348
- events_default,
4349
- policy_default,
4350
- queue_default,
4351
- listeners_default,
4352
- viewProvider
4353
- ];
4354
-
4355
- // ../../src/domain/auth.ts
4356
- var TEST_ADMIN_API_TOKEN = "workhub-admin-test-token";
4357
- var TEST_MEMBER_API_TOKEN = "workhub-member-test-token";
4358
-
4359
- // ../../src/domain/scim.ts
4360
- var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
4361
- var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
4400
+ // ../../src/core/storage/storage.ts
4401
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
4402
+ import { dirname, join as join3 } from "path";
4403
+ var {S3Client } = globalThis.Bun;
4362
4404
 
4363
- // ../../src/bootstrap/secretsGuard.ts
4364
- var DEFAULT_TOKENS = new Set([TEST_ADMIN_API_TOKEN, TEST_MEMBER_API_TOKEN]);
4365
- var DEFAULT_SCIM_TOKENS = new Set([TEST_SCIM_BEARER_TOKEN, DEFAULT_SCIM_BEARER_TOKEN]);
4366
- function assertProductionSecrets(env = process.env) {
4367
- const appEnv = env.APP_ENV ?? appConfig.env;
4368
- if (appEnv !== "production") {
4369
- return;
4405
+ class LocalStorageDriver {
4406
+ rootDirectory;
4407
+ constructor(rootDirectory) {
4408
+ this.rootDirectory = rootDirectory;
4370
4409
  }
4371
- const adminToken = env.ADMIN_API_TOKEN ?? TEST_ADMIN_API_TOKEN;
4372
- const memberToken = env.MEMBER_API_TOKEN ?? TEST_MEMBER_API_TOKEN;
4373
- const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
4374
- const encryptionEnabled = env.FEATURE_FIELD_ENCRYPTION !== "false";
4375
- const devHeadersEnabled = (env.AUTH_DEV_HEADERS ?? "true") !== "false";
4376
- if (devHeadersEnabled) {
4377
- throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
4410
+ resolveRootDirectory() {
4411
+ return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
4378
4412
  }
4379
- if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
4380
- throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
4413
+ resolvePath(path) {
4414
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
4381
4415
  }
4382
- if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
4383
- throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
4416
+ async put(path, contents) {
4417
+ const absolutePath = this.resolvePath(path);
4418
+ await mkdir(dirname(absolutePath), { recursive: true });
4419
+ await writeFile(absolutePath, contents);
4420
+ return path;
4384
4421
  }
4385
- if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
4386
- throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
4422
+ async get(path) {
4423
+ try {
4424
+ return await readFile(this.resolvePath(path));
4425
+ } catch {
4426
+ return null;
4427
+ }
4387
4428
  }
4388
- if (!env.SIEM_EXPORT_URL?.trim() && isFeatureEnabled("siemExport")) {
4389
- console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
4429
+ async delete(path) {
4430
+ try {
4431
+ await unlink(this.resolvePath(path));
4432
+ return true;
4433
+ } catch {
4434
+ return false;
4435
+ }
4436
+ }
4437
+ }
4438
+
4439
+ class S3StorageDriver {
4440
+ client;
4441
+ constructor(client) {
4442
+ this.client = client;
4443
+ }
4444
+ async put(path, contents) {
4445
+ await this.client.write(path.replace(/^\/+/, ""), contents);
4446
+ return path;
4447
+ }
4448
+ async get(path) {
4449
+ const normalizedPath = path.replace(/^\/+/, "");
4450
+ const file = this.client.file(normalizedPath);
4451
+ if (!await file.exists()) {
4452
+ return null;
4453
+ }
4454
+ return new Uint8Array(await file.arrayBuffer());
4390
4455
  }
4391
- const billingEnabled = (env.FEATURE_BILLING ?? "true") !== "false";
4392
- if (billingEnabled && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
4393
- throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
4456
+ async delete(path) {
4457
+ try {
4458
+ await this.client.unlink(path.replace(/^\/+/, ""));
4459
+ return true;
4460
+ } catch {
4461
+ return false;
4462
+ }
4394
4463
  }
4395
- const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
4396
- if (corsOrigins.includes("*")) {
4397
- throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
4464
+ }
4465
+
4466
+ class StorageManager {
4467
+ driver;
4468
+ constructor(driver) {
4469
+ this.driver = driver;
4398
4470
  }
4399
- if ((env.FEATURE_PUBLIC_READS ?? "true") !== "false") {
4400
- throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
4471
+ put(path, contents) {
4472
+ return this.driver.put(path, contents);
4401
4473
  }
4402
- if (!env.OAUTH_STATE_SECRET?.trim()) {
4403
- throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
4474
+ get(path) {
4475
+ return this.driver.get(path);
4404
4476
  }
4405
- if (!env.TOKEN_HASH_PEPPER?.trim()) {
4406
- throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
4477
+ delete(path) {
4478
+ return this.driver.delete(path);
4407
4479
  }
4408
- if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
4409
- throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
4480
+ }
4481
+ function resolveS3Config() {
4482
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
4483
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
4484
+ const bucket = process.env.AWS_BUCKET?.trim();
4485
+ if (!accessKeyId || !secretAccessKey || !bucket) {
4486
+ throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
4410
4487
  }
4488
+ return {
4489
+ accessKeyId,
4490
+ secretAccessKey,
4491
+ bucket,
4492
+ ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
4493
+ ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
4494
+ };
4411
4495
  }
4412
-
4413
- // ../../src/bootstrap/context.ts
4414
- function collectProviders(modules = discoverModules()) {
4415
- return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
4496
+ function createS3Client(config = resolveS3Config()) {
4497
+ return new S3Client({
4498
+ accessKeyId: config.accessKeyId,
4499
+ secretAccessKey: config.secretAccessKey,
4500
+ bucket: config.bucket,
4501
+ ...config.region ? { region: config.region } : {},
4502
+ ...config.endpoint ? { endpoint: config.endpoint } : {}
4503
+ });
4416
4504
  }
4417
- function runProviderPhase(providers, phase, context) {
4418
- for (const provider of providers) {
4419
- provider[phase]?.(context);
4505
+ function createStorageDriver() {
4506
+ const driver = process.env.STORAGE_DRIVER ?? "local";
4507
+ if (driver === "s3") {
4508
+ return new S3StorageDriver(createS3Client());
4420
4509
  }
4510
+ return new LocalStorageDriver;
4421
4511
  }
4422
- function createAppContext() {
4423
- assertProductionSecrets();
4424
- const container = new ServiceContainer;
4425
- const config = new ConfigStore;
4426
- const dependencies = {
4427
- container
4428
- };
4429
- const context = {
4430
- container,
4431
- config,
4432
- dependencies
4433
- };
4434
- const providers = collectProviders();
4435
- runProviderPhase(providers, "register", context);
4436
- runProviderPhase(providers, "boot", context);
4437
- assertAppDependenciesComplete(dependencies);
4438
- const appContext = {
4439
- container,
4440
- config,
4441
- dependencies
4512
+
4513
+ // ../../src/bootstrap/providers/storage.ts
4514
+ var storageProvider = {
4515
+ name: "core.storage",
4516
+ register({ dependencies }) {
4517
+ dependencies.storage = new StorageManager(createStorageDriver());
4518
+ }
4519
+ };
4520
+ var storage_default = storageProvider;
4521
+
4522
+ // ../../src/core/http/requestMetaContext.ts
4523
+ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
4524
+ function currentRequestMeta() {
4525
+ return requestMetaContext.getStore() ?? {
4526
+ ipAddress: null,
4527
+ userAgent: null
4442
4528
  };
4443
- setActiveApplicationContext(appContext);
4444
- return appContext;
4445
4529
  }
4446
- // ../../src/bootstrap/createWebRoutes.ts
4447
- import { join as join5 } from "path";
4448
4530
 
4449
- // ../../src/core/http/middleware.ts
4450
- function isRouteHandler(value) {
4451
- return typeof value === "function";
4452
- }
4453
- function isMethodRouteMap(value) {
4454
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
4455
- return false;
4531
+ // ../../src/core/view/etaViewEngine.ts
4532
+ import { join as join4 } from "path";
4533
+ import { Eta } from "eta";
4534
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
4535
+ var DEFAULT_LAYOUT = "layouts/app.eta";
4536
+
4537
+ class EtaViewEngine {
4538
+ eta;
4539
+ resolveLayoutData;
4540
+ constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
4541
+ this.eta = new Eta({
4542
+ views: viewsDirectory,
4543
+ autoTrim: false
4544
+ });
4545
+ this.resolveLayoutData = resolveLayoutData;
4546
+ }
4547
+ async render(name, data = {}, options = {}) {
4548
+ const template = name.endsWith(".eta") ? name : `${name}.eta`;
4549
+ const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
4550
+ const mergedData = { ...layoutData, ...data };
4551
+ const body = await this.eta.renderAsync(template, mergedData);
4552
+ const layout = options.layout ?? DEFAULT_LAYOUT;
4553
+ if (layout === false) {
4554
+ return body;
4555
+ }
4556
+ const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
4557
+ return await this.eta.renderAsync(layoutTemplate, {
4558
+ ...mergedData,
4559
+ body
4560
+ });
4456
4561
  }
4457
- const entries = Object.entries(value);
4458
- return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
4459
4562
  }
4460
- function composeMiddleware(...middleware) {
4461
- return (handler) => {
4462
- return async (request) => {
4463
- let index = 0;
4464
- const dispatch = async () => {
4465
- if (index >= middleware.length) {
4466
- return await handler(request);
4467
- }
4468
- const current = middleware[index];
4469
- index += 1;
4470
- if (!current) {
4471
- return await handler(request);
4472
- }
4473
- return await current(request, dispatch);
4474
- };
4475
- return await dispatch();
4476
- };
4477
- };
4563
+ // ../../src/core/view/htmlResponse.ts
4564
+ function htmlResponse(html, init = {}) {
4565
+ return new Response(html, {
4566
+ status: init.status ?? 200,
4567
+ statusText: init.statusText,
4568
+ headers: {
4569
+ "Content-Type": "text/html; charset=utf-8"
4570
+ }
4571
+ });
4478
4572
  }
4479
- function wrapRouteHandler(handler, middleware) {
4480
- if (isMethodRouteMap(handler)) {
4481
- const wrapped = {};
4482
- for (const [method, routeHandler] of Object.entries(handler)) {
4483
- wrapped[method] = composeMiddleware(...middleware)(routeHandler);
4573
+ // ../../src/core/http/cookies.ts
4574
+ function readRequestCookie(request, name) {
4575
+ const cookies = request.cookies;
4576
+ if (cookies && typeof cookies.get === "function") {
4577
+ const value = cookies.get(name);
4578
+ if (value) {
4579
+ return value;
4484
4580
  }
4485
- return wrapped;
4486
- }
4487
- if (isRouteHandler(handler)) {
4488
- return composeMiddleware(...middleware)(handler);
4489
4581
  }
4490
- return handler;
4491
- }
4492
- function applyMiddlewareToRoutes(routes, middleware) {
4493
- const wrapped = {};
4494
- for (const [path, routeHandler] of Object.entries(routes)) {
4495
- wrapped[path] = wrapRouteHandler(routeHandler, middleware);
4582
+ const header = request.headers.get("cookie");
4583
+ if (!header) {
4584
+ return null;
4496
4585
  }
4497
- return wrapped;
4498
- }
4499
-
4500
- // ../../src/bootstrap/httpKernel.ts
4501
- import {
4502
- createAuthMiddleware,
4503
- createAuthorizeMiddleware,
4504
- createBodySizeLimitMiddleware,
4505
- createCorsMiddleware,
4506
- createCsrfMiddleware,
4507
- createFlashMiddleware,
4508
- createLoginThrottleMiddleware,
4509
- createMembershipMiddleware,
4510
- createMemoryThrottleMiddleware,
4511
- createMetricsMiddleware,
4512
- createRequestLoggingMiddleware,
4513
- createRequireAbilityMiddleware,
4514
- createRequireAuthMiddleware,
4515
- createRequireGlobalAdminMiddleware,
4516
- createRequireWebAuthMiddleware,
4517
- createSecurityHeadersMiddleware,
4518
- createTenantMiddleware,
4519
- createThrottleMiddleware,
4520
- createTracingMiddleware,
4521
- isPublicReadsEnabled,
4522
- requestIdMiddleware,
4523
- withMiddleware
4524
- } from "@getstrata/core";
4525
-
4526
- // ../../src/config/rateLimit.ts
4527
- var LOCAL_LOGIN_RATE_LIMIT = {
4528
- maxAttempts: 100,
4529
- decaySeconds: 60
4530
- };
4531
- var PRODUCTION_LOGIN_RATE_LIMIT = {
4532
- maxAttempts: 5,
4533
- decaySeconds: 900
4534
- };
4535
- function isLocalAppEnv() {
4536
- return (process.env.APP_ENV ?? "local") === "local";
4537
- }
4538
- function parsePositiveInt(value, fallback) {
4539
- const parsed = Number(value);
4540
- if (!Number.isFinite(parsed) || parsed <= 0) {
4541
- return fallback;
4586
+ for (const part of header.split(";")) {
4587
+ const idx = part.indexOf("=");
4588
+ if (idx === -1)
4589
+ continue;
4590
+ const cookieName = part.slice(0, idx).trim();
4591
+ if (cookieName !== name)
4592
+ continue;
4593
+ return decodeURIComponent(part.slice(idx + 1).trim());
4542
4594
  }
4543
- return Math.trunc(parsed);
4595
+ return null;
4544
4596
  }
4545
- function resolveLoginRateLimit() {
4546
- const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
4547
- return {
4548
- maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
4549
- decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
4550
- };
4597
+
4598
+ // ../../src/core/http/csrfToken.ts
4599
+ var CSRF_COOKIE = "workhub_csrf";
4600
+ var CSRF_TTL_MS = 60 * 60 * 1000;
4601
+ function resolveCsrfSecret() {
4602
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
4551
4603
  }
4552
- function resolveRegisterRateLimit() {
4553
- const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
4604
+ function csrfVerifyOptions() {
4605
+ return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
4606
+ }
4607
+ function createCsrfTokenCookie() {
4608
+ const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
4554
4609
  return {
4555
- maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
4556
- decaySeconds: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS ?? process.env.REGISTER_RATE_LIMIT_WINDOW_MS ? String(Number(process.env.REGISTER_RATE_LIMIT_WINDOW_MS) / 1000) : undefined, defaults.decaySeconds)
4610
+ token,
4611
+ cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
4557
4612
  };
4558
4613
  }
4559
-
4560
- // ../../src/bootstrap/httpKernel.ts
4561
- class HttpKernel {
4562
- dependencies;
4563
- constructor(dependencies) {
4564
- this.dependencies = dependencies;
4614
+ function resolveCsrfToken(request) {
4615
+ const cookieValue = readRequestCookie(request, CSRF_COOKIE);
4616
+ if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
4617
+ return { token: cookieValue };
4565
4618
  }
4566
- globalMiddleware() {
4567
- const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
4568
- return [
4569
- createCorsMiddleware(),
4570
- createSecurityHeadersMiddleware(),
4571
- createBodySizeLimitMiddleware(),
4572
- createTracingMiddleware(),
4573
- createMetricsMiddleware(),
4574
- createRequestLoggingMiddleware(),
4575
- requestIdMiddleware,
4576
- createAuthMiddleware(auth),
4577
- createMembershipMiddleware(),
4578
- createTenantMiddleware()
4579
- ];
4619
+ return createCsrfTokenCookie();
4620
+ }
4621
+ function resolveCsrfTokenForRequest(request) {
4622
+ const metaToken = currentRequestMeta().csrfToken;
4623
+ if (metaToken) {
4624
+ return metaToken;
4580
4625
  }
4581
- group(name) {
4582
- const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
4583
- switch (name) {
4584
- case "authenticated":
4585
- return [createRequireAuthMiddleware(auth)];
4586
- case "web":
4587
- return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
4588
- case "api": {
4589
- if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
4590
- return [];
4591
- }
4592
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
4593
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
4594
- if (!redisUrl) {
4595
- const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
4596
- return [
4597
- createMemoryThrottleMiddleware({
4598
- maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
4599
- decaySeconds: 60
4600
- })
4601
- ];
4602
- }
4603
- const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
4604
- return [
4605
- createThrottleMiddleware({
4606
- redisUrl,
4607
- maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
4608
- decaySeconds: 60
4609
- })
4610
- ];
4611
- }
4612
- default:
4613
- throw new Error(`Unknown middleware group "${name}".`);
4626
+ return resolveCsrfToken(request).token;
4627
+ }
4628
+
4629
+ // ../../src/core/http/flashSession.ts
4630
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
4631
+ var FLASH_COOKIE = "workhub_flash";
4632
+ var FLASH_TTL_MS = 60 * 1000;
4633
+ function resolveFlashSecret() {
4634
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
4635
+ }
4636
+ function signFlashPayload(payload, issuedAt) {
4637
+ const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
4638
+ return `${payload}.${issuedAt}.${signature}`;
4639
+ }
4640
+ function readFlashCookie(request) {
4641
+ const cookieHeader = request.headers.get("cookie");
4642
+ if (!cookieHeader) {
4643
+ return null;
4644
+ }
4645
+ for (const part of cookieHeader.split(";")) {
4646
+ const [name, ...rest] = part.trim().split("=");
4647
+ if (name === FLASH_COOKIE) {
4648
+ return decodeURIComponent(rest.join("="));
4614
4649
  }
4615
4650
  }
4616
- wrap(groups, handler) {
4617
- const names = Array.isArray(groups) ? groups : [groups];
4618
- const middleware = names.flatMap((name) => this.group(name));
4619
- if (middleware.length === 0) {
4620
- return handler;
4651
+ return null;
4652
+ }
4653
+ function parseFlashCookie(cookieValue) {
4654
+ const parts = cookieValue.split(".");
4655
+ if (parts.length < 3) {
4656
+ return null;
4657
+ }
4658
+ const signature = parts.pop();
4659
+ const issuedAtRaw = parts.pop();
4660
+ const payload = parts.join(".");
4661
+ if (!signature || !issuedAtRaw || !payload) {
4662
+ return null;
4663
+ }
4664
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
4665
+ if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
4666
+ return null;
4667
+ }
4668
+ const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
4669
+ if (!expectedSignature) {
4670
+ return null;
4671
+ }
4672
+ const expectedBuffer = Buffer.from(expectedSignature);
4673
+ const actualBuffer = Buffer.from(signature);
4674
+ if (expectedBuffer.length !== actualBuffer.length) {
4675
+ return null;
4676
+ }
4677
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4678
+ return null;
4679
+ }
4680
+ try {
4681
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
4682
+ if (!parsed?.message || typeof parsed.message !== "string") {
4683
+ return null;
4621
4684
  }
4622
- return withMiddleware(...middleware)(handler);
4685
+ if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
4686
+ return null;
4687
+ }
4688
+ return parsed;
4689
+ } catch {
4690
+ return null;
4623
4691
  }
4624
- wrapApi(handler) {
4625
- return this.wrap(["api", "authenticated"], handler);
4692
+ }
4693
+ function pullFlash(request) {
4694
+ const cookieValue = readFlashCookie(request);
4695
+ if (!cookieValue) {
4696
+ return null;
4626
4697
  }
4627
- wrapWeb(handler) {
4628
- return handler;
4698
+ return parseFlashCookie(cookieValue);
4699
+ }
4700
+
4701
+ // ../../src/core/view/webLayoutData.ts
4702
+ async function resolveWebLayoutData(container, request) {
4703
+ const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
4704
+ const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
4705
+ const authUser = currentAuthUser();
4706
+ if (!authUser) {
4707
+ return { authUser: null, csrfToken, flash };
4629
4708
  }
4630
- wrapWebPublicRead(handler) {
4631
- if (isPublicReadsEnabled()) {
4632
- return this.wrapWeb(handler);
4709
+ const userId = Number(authUser.id);
4710
+ if (!Number.isInteger(userId) || userId <= 0) {
4711
+ return { authUser: null, csrfToken, flash };
4712
+ }
4713
+ if (!container.has(tokenServiceToken)) {
4714
+ return {
4715
+ authUser: {
4716
+ id: userId,
4717
+ email: "",
4718
+ role: authUser.role ?? "member"
4719
+ },
4720
+ csrfToken,
4721
+ flash
4722
+ };
4723
+ }
4724
+ const tokenService = container.resolve(tokenServiceToken);
4725
+ try {
4726
+ const user = await tokenService.findByIdOrThrow(userId);
4727
+ return {
4728
+ authUser: {
4729
+ id: userId,
4730
+ email: user.email ?? "",
4731
+ role: authUser.role ?? user.role ?? "member"
4732
+ },
4733
+ csrfToken,
4734
+ flash
4735
+ };
4736
+ } catch {
4737
+ return { authUser: null, csrfToken, flash };
4738
+ }
4739
+ }
4740
+ // ../../src/bootstrap/providers/view.ts
4741
+ var CORE_VIEW_TOKEN = "core.view";
4742
+ var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
4743
+ var viewProvider = {
4744
+ name: "view",
4745
+ register({ container, config }) {
4746
+ if (!isViewsEnabled()) {
4747
+ return;
4633
4748
  }
4634
- return this.wrapWebAuthenticated(handler);
4749
+ const viewsDirectory = process.env.VIEW_DIRECTORY?.trim() || DEFAULT_VIEWS_DIRECTORY;
4750
+ config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
4751
+ container.set(CORE_VIEW_TOKEN, new EtaViewEngine(viewsDirectory, () => resolveWebLayoutData(container, currentRequestMeta().request)));
4635
4752
  }
4636
- wrapWebAuthenticated(handler) {
4637
- const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
4638
- return withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
4753
+ };
4754
+
4755
+ // ../../src/bootstrap/providers/index.ts
4756
+ var coreProviders = [
4757
+ config_default,
4758
+ cache_default,
4759
+ storage_default,
4760
+ auth_default,
4761
+ events_default,
4762
+ policy_default,
4763
+ queue_default,
4764
+ listeners_default,
4765
+ viewProvider
4766
+ ];
4767
+
4768
+ // ../../src/domain/auth.ts
4769
+ var TEST_ADMIN_API_TOKEN = "workhub-admin-test-token";
4770
+ var TEST_MEMBER_API_TOKEN = "workhub-member-test-token";
4771
+
4772
+ // ../../src/domain/scim.ts
4773
+ var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
4774
+ var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
4775
+
4776
+ // ../../src/bootstrap/secretsGuard.ts
4777
+ var DEFAULT_TOKENS = new Set([TEST_ADMIN_API_TOKEN, TEST_MEMBER_API_TOKEN]);
4778
+ var DEFAULT_SCIM_TOKENS = new Set([TEST_SCIM_BEARER_TOKEN, DEFAULT_SCIM_BEARER_TOKEN]);
4779
+ function assertProductionSecrets(env = process.env) {
4780
+ const appEnv = env.APP_ENV ?? appConfig.env;
4781
+ if (appEnv !== "production") {
4782
+ return;
4639
4783
  }
4640
- wrapWebAbility(ability, handler) {
4641
- const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
4642
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
4643
- const requireAbility = createRequireAbilityMiddleware(abilityChecker);
4644
- const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
4645
- return withMiddleware(...middleware)(handler);
4784
+ const adminToken = env.ADMIN_API_TOKEN ?? TEST_ADMIN_API_TOKEN;
4785
+ const memberToken = env.MEMBER_API_TOKEN ?? TEST_MEMBER_API_TOKEN;
4786
+ const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
4787
+ const encryptionEnabled = env.FEATURE_FIELD_ENCRYPTION !== "false";
4788
+ const devHeadersEnabled = (env.AUTH_DEV_HEADERS ?? "true") !== "false";
4789
+ if (devHeadersEnabled) {
4790
+ throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
4646
4791
  }
4647
- wrapWebGlobalAdmin(handler) {
4648
- const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
4649
- const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
4650
- return withMiddleware(...middleware)(handler);
4792
+ if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
4793
+ throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
4651
4794
  }
4652
- wrapAuthenticated(handler) {
4653
- return this.wrap("authenticated", handler);
4795
+ if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
4796
+ throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
4654
4797
  }
4655
- wrapPublicRead(handler) {
4656
- if (isPublicReadsEnabled()) {
4657
- return handler;
4658
- }
4659
- return this.wrapAuthenticated(handler);
4798
+ if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
4799
+ throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
4660
4800
  }
4661
- wrapGlobalAdmin(handler) {
4662
- const middleware = [...this.group("authenticated"), createRequireGlobalAdminMiddleware()];
4663
- return withMiddleware(...middleware)(handler);
4801
+ if (!env.SIEM_EXPORT_URL?.trim() && isFeatureEnabled("siemExport")) {
4802
+ console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
4664
4803
  }
4665
- wrapAbility(ability, handler) {
4666
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
4667
- const requireAbility = createRequireAbilityMiddleware(abilityChecker);
4668
- const middleware = [...this.group("authenticated"), requireAbility(ability)];
4669
- return withMiddleware(...middleware)(handler);
4804
+ const billingEnabled = (env.FEATURE_BILLING ?? "true") !== "false";
4805
+ if (billingEnabled && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
4806
+ throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
4670
4807
  }
4671
- wrapPolicy(resource, action, handler) {
4672
- const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
4673
- const gate = this.dependencies.container.resolve(CORE_POLICY_GATE_TOKEN);
4674
- return withMiddleware(createAuthorizeMiddleware(gate, auth, resource, action))(handler);
4808
+ const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
4809
+ if (corsOrigins.includes("*")) {
4810
+ throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
4675
4811
  }
4676
- wrapLogin(handler) {
4677
- return this.wrapThrottle("login", resolveLoginRateLimit(), handler);
4812
+ if ((env.FEATURE_PUBLIC_READS ?? "true") !== "false") {
4813
+ throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
4678
4814
  }
4679
- wrapRegister(handler) {
4680
- return this.wrapThrottle("register", resolveRegisterRateLimit(), handler);
4815
+ if (!env.OAUTH_STATE_SECRET?.trim()) {
4816
+ throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
4681
4817
  }
4682
- wrapThrottle(scope, rateLimit, handler) {
4683
- const middleware = [];
4684
- const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
4685
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
4686
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
4687
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
4688
- if (redisUrl) {
4689
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
4690
- redisUrl,
4691
- maxAttempts: rateLimit.maxAttempts,
4692
- decaySeconds: rateLimit.decaySeconds
4693
- }) : createThrottleMiddleware({
4694
- redisUrl,
4695
- maxAttempts: rateLimit.maxAttempts,
4696
- decaySeconds: rateLimit.decaySeconds,
4697
- keyPrefix: memoryKeyPrefix
4698
- });
4699
- middleware.push(throttle);
4700
- } else {
4701
- middleware.push(createMemoryThrottleMiddleware({
4702
- maxAttempts: rateLimit.maxAttempts,
4703
- decaySeconds: rateLimit.decaySeconds,
4704
- keyPrefix: memoryKeyPrefix
4705
- }));
4706
- }
4707
- } else {
4708
- middleware.push(createMemoryThrottleMiddleware({
4709
- maxAttempts: rateLimit.maxAttempts,
4710
- decaySeconds: rateLimit.decaySeconds,
4711
- keyPrefix: memoryKeyPrefix
4712
- }));
4713
- }
4714
- if (middleware.length === 0) {
4715
- return handler;
4716
- }
4717
- return withMiddleware(...middleware)(handler);
4818
+ if (!env.TOKEN_HASH_PEPPER?.trim()) {
4819
+ throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
4820
+ }
4821
+ if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
4822
+ throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
4718
4823
  }
4719
- }
4720
- function createHttpKernel(dependencies) {
4721
- return new HttpKernel(dependencies);
4722
4824
  }
4723
4825
 
4724
- // ../../src/bootstrap/routeRegistry.ts
4725
- class RouteRegistry {
4726
- routes = [];
4727
- register(route) {
4728
- this.routes.push(route);
4729
- }
4730
- clear() {
4731
- this.routes.length = 0;
4732
- }
4733
- list() {
4734
- return [...this.routes].sort((left, right) => left.path.localeCompare(right.path));
4735
- }
4826
+ // ../../src/bootstrap/context.ts
4827
+ function collectProviders(modules = discoverModules()) {
4828
+ return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
4736
4829
  }
4737
- var ROUTE_REGISTRY_KEY = Symbol.for("@getstrata/routeRegistry");
4738
- function readSharedRouteRegistry() {
4739
- const globalRegistry = globalThis[ROUTE_REGISTRY_KEY];
4740
- if (globalRegistry) {
4741
- return globalRegistry;
4830
+ function runProviderPhase(providers, phase, context) {
4831
+ for (const provider of providers) {
4832
+ provider[phase]?.(context);
4742
4833
  }
4743
- const registry = new RouteRegistry;
4744
- globalThis[ROUTE_REGISTRY_KEY] = registry;
4745
- return registry;
4746
4834
  }
4747
- var routeRegistry = readSharedRouteRegistry();
4748
-
4835
+ function createAppContext() {
4836
+ assertProductionSecrets();
4837
+ const container = new ServiceContainer;
4838
+ const config = new ConfigStore;
4839
+ const dependencies = {
4840
+ container
4841
+ };
4842
+ const context = {
4843
+ container,
4844
+ config,
4845
+ dependencies
4846
+ };
4847
+ const providers = collectProviders();
4848
+ runProviderPhase(providers, "register", context);
4849
+ runProviderPhase(providers, "boot", context);
4850
+ assertAppDependenciesComplete(dependencies);
4851
+ const appContext = {
4852
+ container,
4853
+ config,
4854
+ dependencies
4855
+ };
4856
+ setActiveApplicationContext(appContext);
4857
+ return appContext;
4858
+ }
4749
4859
  // ../../src/bootstrap/createWebRoutes.ts
4860
+ import { join as join5 } from "path";
4750
4861
  function registerRoute(method, path, middleware) {
4751
4862
  routeRegistry.register({ method, path, middleware });
4752
4863
  }
@@ -4810,97 +4921,6 @@ function mergeWebRoutes(dependencies, routes) {
4810
4921
  function createAppDependencies() {
4811
4922
  return createAppContext().dependencies;
4812
4923
  }
4813
- // ../../src/core/crypto/nonCryptographicHash.ts
4814
- function nonCryptographicDigest(input) {
4815
- return Bun.hash(input).toString(16);
4816
- }
4817
-
4818
- // ../../src/core/http/etag.ts
4819
- function isEtagEnabled() {
4820
- return (process.env.FEATURE_ETAG ?? "true") !== "false";
4821
- }
4822
- function formatWeakEtag(digest) {
4823
- return `W/"${digest}"`;
4824
- }
4825
- function etagFromResource(resource) {
4826
- const version = resource.updated_at ?? resource.created_at ?? "";
4827
- const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
4828
- const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
4829
- return formatWeakEtag(digest);
4830
- }
4831
- function normalizeEtag(value) {
4832
- return value.trim();
4833
- }
4834
- function etagValuesMatch(left, right) {
4835
- return normalizeEtag(left) === normalizeEtag(right);
4836
- }
4837
- function parseEtagList(header) {
4838
- if (!header) {
4839
- return [];
4840
- }
4841
- return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
4842
- }
4843
- function ifNoneMatchSatisfied(request, etag) {
4844
- const header = request.headers.get("if-none-match");
4845
- if (!header) {
4846
- return false;
4847
- }
4848
- if (header.trim() === "*") {
4849
- return true;
4850
- }
4851
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
4852
- }
4853
- function ifMatchSatisfied(request, etag) {
4854
- const header = request.headers.get("if-match");
4855
- if (!header) {
4856
- return false;
4857
- }
4858
- if (header.trim() === "*") {
4859
- return true;
4860
- }
4861
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
4862
- }
4863
- function assertIfMatch(request, etag, options = {}) {
4864
- const header = request.headers.get("if-match");
4865
- if (!header) {
4866
- if (options.required) {
4867
- throw new PreconditionFailedError("If-Match header is required.");
4868
- }
4869
- return;
4870
- }
4871
- if (!ifMatchSatisfied(request, etag)) {
4872
- throw new PreconditionFailedError("Resource ETag does not match If-Match.");
4873
- }
4874
- }
4875
- function applyEtagHeaders(headers, etag) {
4876
- const next = new Headers(headers);
4877
- next.set("ETag", etag);
4878
- next.set("Cache-Control", "private, must-revalidate");
4879
- next.append("Vary", "Authorization");
4880
- next.append("Vary", "X-Tenant-Id");
4881
- return next;
4882
- }
4883
- function notModifiedResponse(etag) {
4884
- return new Response(null, {
4885
- status: 304,
4886
- headers: applyEtagHeaders(new Headers, etag)
4887
- });
4888
- }
4889
- function applyConditionalGet(request, response, etag) {
4890
- if (!isEtagEnabled()) {
4891
- return response;
4892
- }
4893
- if (ifNoneMatchSatisfied(request, etag)) {
4894
- return notModifiedResponse(etag);
4895
- }
4896
- const headers = applyEtagHeaders(new Headers(response.headers), etag);
4897
- return new Response(response.body, {
4898
- status: response.status,
4899
- statusText: response.statusText,
4900
- headers
4901
- });
4902
- }
4903
-
4904
4924
  // ../../src/core/tenant/tenantContext.ts
4905
4925
  var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
4906
4926
 
@@ -4921,8 +4941,8 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
4921
4941
  return async (request) => {
4922
4942
  const id = parsePositiveIntParam(String(request.params[param]), String(param));
4923
4943
  const model = await resolver(id, request);
4924
- const gate = resolveApplicationPolicyGate();
4925
- const auth = resolveApplicationAuth();
4944
+ const gate = resolveApplicationPolicyGate2();
4945
+ const auth = resolveApplicationAuth2();
4926
4946
  const user = currentAuthUser() ?? await auth.resolve(request);
4927
4947
  gate.authorize(authorization.resource, authorization.action, user, model);
4928
4948
  if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
@@ -4944,8 +4964,8 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
4944
4964
  throw new BadRequestError(`Missing route parameter "${String(param)}".`);
4945
4965
  }
4946
4966
  const model = await resolver(key, request);
4947
- const gate = resolveApplicationPolicyGate();
4948
- const auth = resolveApplicationAuth();
4967
+ const gate = resolveApplicationPolicyGate2();
4968
+ const auth = resolveApplicationAuth2();
4949
4969
  const user = currentAuthUser() ?? await auth.resolve(request);
4950
4970
  gate.authorize(authorization.resource, authorization.action, user, model);
4951
4971
  if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
@@ -5096,22 +5116,12 @@ var membershipService_default = MembershipService;
5096
5116
 
5097
5117
  // ../../src/core/auth/resolveMembershipService.ts
5098
5118
  function resolveMembershipService() {
5099
- const dependencies = resolveApplicationDependencies();
5119
+ const dependencies = resolveApplicationDependencies2();
5100
5120
  if (dependencies.container.has("core.membership")) {
5101
5121
  return dependencies.container.resolve("core.membership");
5102
5122
  }
5103
5123
  return new membershipService_default;
5104
5124
  }
5105
- // ../../src/bootstrap/prefixRouteMap.ts
5106
- function prefixRouteMap(prefix, routes) {
5107
- const normalizedPrefix = prefix.replace(/\/$/, "");
5108
- const prefixed = {};
5109
- for (const [path, handler] of Object.entries(routes)) {
5110
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
5111
- prefixed[`${normalizedPrefix}${normalizedPath}`] = handler;
5112
- }
5113
- return prefixed;
5114
- }
5115
5125
  // ../../src/core/http/csrfProtection.ts
5116
5126
  var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
5117
5127
  function createCsrfProtection(secret, options = {}) {
@@ -5372,6 +5382,7 @@ export {
5372
5382
  configureModulesDirectory,
5373
5383
  collectProviders,
5374
5384
  cacheTagsForModelWrite,
5385
+ buildModuleRoutes,
5375
5386
  assertProductionSecrets,
5376
5387
  assertAppDependenciesComplete,
5377
5388
  appSchedule,