@getstrata/bootstrap 0.2.48 → 0.2.50

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.
@@ -63,6 +63,7 @@ import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
63
63
 
64
64
  // ../../src/bootstrap/httpKernel.ts
65
65
  import { createMembershipMiddleware } from "@getstrata/core/auth/membershipMiddleware";
66
+ import { resolveAbilityChecker } from "@getstrata/core/contracts/serviceTokens";
66
67
  import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
67
68
  import { createAuthorizeMiddleware } from "@getstrata/core/http/authorizeMiddleware";
68
69
  import { createBodySizeLimitMiddleware } from "@getstrata/core/http/bodySizeLimitMiddleware";
@@ -77,6 +78,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
77
78
  import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
78
79
  import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
79
80
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
81
+ import { withErrorHandling } from "@getstrata/core/http/response";
80
82
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
81
83
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
82
84
  import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
@@ -104,24 +106,38 @@ function parsePositiveInt(value, fallback) {
104
106
  }
105
107
  return Math.trunc(parsed);
106
108
  }
109
+ function parseWindowSeconds(secondsValue, msValue, fallback) {
110
+ if (secondsValue !== undefined && secondsValue.trim() !== "") {
111
+ return parsePositiveInt(secondsValue, fallback);
112
+ }
113
+ if (msValue !== undefined && msValue.trim() !== "") {
114
+ const parsedMs = Number(msValue);
115
+ if (Number.isFinite(parsedMs) && parsedMs > 0) {
116
+ return Math.max(1, Math.trunc(parsedMs / 1000));
117
+ }
118
+ }
119
+ return fallback;
120
+ }
107
121
  function resolveLoginRateLimit() {
108
122
  const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
109
123
  return {
110
124
  maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
111
- decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
125
+ decaySeconds: parseWindowSeconds(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, process.env.LOGIN_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
112
126
  };
113
127
  }
114
128
  function resolveRegisterRateLimit() {
115
129
  const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
116
130
  return {
117
131
  maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
118
- 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)
132
+ decaySeconds: parseWindowSeconds(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS, process.env.REGISTER_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
119
133
  };
120
134
  }
121
135
 
122
136
  // ../../src/bootstrap/config.ts
123
137
  import {
138
+ CORE_ABILITY_CHECKER_TOKEN,
124
139
  CORE_AUTH_TOKEN,
140
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
125
141
  CORE_CACHE_TOKEN,
126
142
  CORE_CONFIG_TOKEN,
127
143
  CORE_EVENT_BUS_TOKEN,
@@ -211,7 +227,7 @@ class HttpKernel {
211
227
  return this.wrap(["api", "authenticated"], handler);
212
228
  }
213
229
  wrapWeb(handler) {
214
- return this.wrap("web", handler);
230
+ return withErrorHandling(this.wrap("web", handler));
215
231
  }
216
232
  wrapWebPublicRead(handler) {
217
233
  if (isPublicReadsEnabled()) {
@@ -221,19 +237,19 @@ class HttpKernel {
221
237
  }
222
238
  wrapWebAuthenticated(handler) {
223
239
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
224
- return this.wrap("web", withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
240
+ return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
225
241
  }
226
242
  wrapWebAbility(ability, handler) {
227
243
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
228
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
244
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
229
245
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
230
246
  const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
231
- return this.wrap("web", withMiddleware(...middleware)(handler));
247
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
232
248
  }
233
249
  wrapWebGlobalAdmin(handler) {
234
250
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
235
251
  const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
236
- return this.wrap("web", withMiddleware(...middleware)(handler));
252
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
237
253
  }
238
254
  wrapAuthenticated(handler) {
239
255
  return this.wrap("authenticated", handler);
@@ -249,7 +265,7 @@ class HttpKernel {
249
265
  return withMiddleware(...middleware)(handler);
250
266
  }
251
267
  wrapAbility(ability, handler) {
252
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
268
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
253
269
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
254
270
  const middleware = [...this.group("authenticated"), requireAbility(ability)];
255
271
  return withMiddleware(...middleware)(handler);
@@ -474,7 +490,7 @@ function createSpaRoutes(_dependencies) {
474
490
  });
475
491
  }
476
492
  return jsonResponse({
477
- error: "SPA build not found. Run `cd frontend && bun install && bun run build`."
493
+ error: "SPA build not found. Run `bun run build:frontend`."
478
494
  }, { status: 503 });
479
495
  },
480
496
  "/": async () => Response.redirect("/app/", 302)
@@ -553,157 +569,46 @@ function mergeWebRoutes(dependencies, routes) {
553
569
  }
554
570
 
555
571
  // ../../src/bootstrap/health.ts
572
+ import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
573
+ import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
556
574
  import { jsonResponse as jsonResponse2 } from "@getstrata/core/http/response";
557
575
  var {RedisClient } = globalThis.Bun;
558
-
559
- // ../../src/config/database.ts
560
- function readInteger(name, fallback) {
561
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
562
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
563
- }
564
- var databaseConfig = {
565
- url: process.env.DATABASE_URL ?? "",
566
- poolMax: readInteger("DB_POOL_MAX", 10),
567
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
568
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
569
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
570
- };
571
-
572
- // ../../src/core/runtime/asyncContextStore.ts
573
- import { AsyncLocalStorage } from "async_hooks";
574
- function createAsyncContextStore(key) {
575
- const symbol = Symbol.for(key);
576
- const globalRecord = globalThis;
577
- const existing = globalRecord[symbol];
578
- if (existing) {
579
- return existing;
580
- }
581
- const store = new AsyncLocalStorage;
582
- globalRecord[symbol] = store;
583
- return store;
584
- }
585
-
586
- // ../../src/core/database/connectionContext.ts
587
- var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
588
- function getActiveDatabaseConnection(fallback) {
589
- return activeConnection.getStore() ?? fallback;
590
- }
591
-
592
- // ../../src/core/database/queryProxy.ts
593
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
594
- function createDatabaseQueryProxy(pool) {
595
- function resolveDatabase() {
596
- return getActiveDatabaseConnection(pool);
597
- }
598
- function resolveDatabaseForProperty(property) {
599
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
600
- return pool;
601
- }
602
- return resolveDatabase();
603
- }
604
- return new Proxy(function database() {}, {
605
- apply(_target, _thisArg, args) {
606
- return resolveDatabase()(...args);
607
- },
608
- get(_target, property) {
609
- const connection = resolveDatabaseForProperty(property);
610
- const value = connection[property];
611
- return typeof value === "function" ? value.bind(connection) : value;
612
- }
613
- });
614
- }
615
-
616
- // ../../src/core/database/defaultConnection.ts
617
- var defaultPool = {
618
- connection: null
619
- };
620
- var defaultQuery = {
621
- connection: null
622
- };
623
- function registerDefaultDatabasePool(connection) {
624
- defaultPool.connection = connection;
625
- defaultQuery.connection = createDatabaseQueryProxy(connection);
626
- }
627
- function getDefaultDatabaseQuery() {
628
- if (!defaultQuery.connection) {
629
- throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
576
+ function resolveRedisUrl(dependencies) {
577
+ if (!dependencies.container.has(CORE_CONFIG_TOKEN)) {
578
+ return process.env.REDIS_URL?.trim() || undefined;
630
579
  }
631
- return defaultQuery.connection;
580
+ const config = dependencies.container.resolve(CORE_CONFIG_TOKEN);
581
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim();
582
+ return redisUrl || undefined;
632
583
  }
633
-
634
- // ../../src/db/connection/createConnection.ts
635
- var {SQL } = globalThis.Bun;
636
- function createDatabaseConnection(config) {
637
- if (!config.url) {
638
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
584
+ function resolveHealthDatabase() {
585
+ const bound = getBoundDatabaseConnection();
586
+ if (bound) {
587
+ return bound;
639
588
  }
640
- return new SQL({
641
- url: config.url,
642
- max: config.poolMax,
643
- idleTimeout: config.idleTimeoutSeconds,
644
- maxLifetime: config.maxLifetimeSeconds,
645
- connectionTimeout: config.connectionTimeoutSeconds
646
- });
647
- }
648
-
649
- // ../../src/db/connection/index.ts
650
- var connectionHolder = {
651
- connection: null
652
- };
653
- function getDatabase() {
654
- if (!connectionHolder.connection) {
655
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
656
- registerDefaultDatabasePool(connectionHolder.connection);
589
+ try {
590
+ return getDefaultDatabasePool();
591
+ } catch {
592
+ return null;
657
593
  }
658
- return connectionHolder.connection;
659
594
  }
660
- function getDb() {
661
- getDatabase();
662
- return getDefaultDatabaseQuery();
663
- }
664
- async function pingDatabase(connection = getDatabase()) {
595
+ async function pingDatabaseClient(connection) {
665
596
  try {
666
- await connection`SELECT 1`;
597
+ await connection.unsafe("SELECT 1");
667
598
  return true;
668
599
  } catch {
669
600
  return false;
670
601
  }
671
602
  }
672
- async function ensureDatabaseConnection() {
673
- if (await pingDatabase()) {
674
- return getDatabase();
603
+ async function checkDatabase() {
604
+ const connection = resolveHealthDatabase();
605
+ if (!connection) {
606
+ return false;
675
607
  }
676
- await getDatabase().close().catch(() => {
677
- return;
678
- });
679
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
680
- registerDefaultDatabasePool(connectionHolder.connection);
681
- return getDatabase();
608
+ return await pingDatabaseClient(connection);
682
609
  }
683
- var db = new Proxy(function database() {}, {
684
- apply(_target, _thisArg, args) {
685
- return getDb()(...args);
686
- },
687
- get(_target, property) {
688
- const connection = getDb();
689
- const value = connection[property];
690
- return typeof value === "function" ? value.bind(connection) : value;
691
- }
692
- });
693
- var connection_default = db;
694
-
695
- // ../../src/bootstrap/health.ts
696
- function resolveRedisUrl(dependencies) {
697
- if (!dependencies.container.has(CORE_CONFIG_TOKEN)) {
698
- return process.env.REDIS_URL?.trim() || undefined;
699
- }
700
- const config = dependencies.container.resolve(CORE_CONFIG_TOKEN);
701
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim();
702
- return redisUrl || undefined;
703
- }
704
- async function checkDatabase() {
705
- await ensureDatabaseConnection();
706
- return await pingDatabase();
610
+ async function pingDatabase() {
611
+ return await checkDatabase();
707
612
  }
708
613
  async function checkRedis(redisUrl) {
709
614
  try {
@@ -714,23 +619,46 @@ async function checkRedis(redisUrl) {
714
619
  return false;
715
620
  }
716
621
  }
717
- function createHealthRoutes(dependencies) {
622
+ async function resolveExtraFields(extra) {
623
+ if (!extra) {
624
+ return {};
625
+ }
626
+ return typeof extra === "function" ? await extra() : extra;
627
+ }
628
+ async function collectDependencyChecks(dependencies) {
629
+ const checks = {
630
+ database: await checkDatabase() ? "ok" : "error"
631
+ };
632
+ const redisUrl = resolveRedisUrl(dependencies);
633
+ if (redisUrl) {
634
+ checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
635
+ } else {
636
+ checks.redis = "skipped";
637
+ }
638
+ const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
639
+ return { checks, ready };
640
+ }
641
+ function createHealthRoutes(dependencies, options = {}) {
718
642
  return {
719
- "/health": async () => jsonResponse2({ status: "ok" }),
720
- "/ready": async () => {
721
- const checks = {
722
- database: await checkDatabase() ? "ok" : "error"
723
- };
724
- const redisUrl = resolveRedisUrl(dependencies);
725
- if (redisUrl) {
726
- checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
727
- } else {
728
- checks.redis = "skipped";
643
+ "/health": async () => {
644
+ const extra = await resolveExtraFields(options.extra);
645
+ if (!options.pingOnHealth) {
646
+ return jsonResponse2({ status: "ok", ...extra });
729
647
  }
730
- const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
648
+ const { checks, ready } = await collectDependencyChecks(dependencies);
649
+ return jsonResponse2({
650
+ status: ready ? "ok" : "error",
651
+ checks,
652
+ ...extra
653
+ }, { status: ready ? 200 : 503 });
654
+ },
655
+ "/ready": async () => {
656
+ const extra = await resolveExtraFields(options.extra);
657
+ const { checks, ready } = await collectDependencyChecks(dependencies);
731
658
  return jsonResponse2({
732
659
  status: ready ? "ready" : "not_ready",
733
- checks
660
+ checks,
661
+ ...extra
734
662
  }, { status: ready ? 200 : 503 });
735
663
  }
736
664
  };
@@ -781,7 +709,7 @@ import { withMiddleware as withMiddleware2 } from "@getstrata/core/http/routeMid
781
709
  import { createScimThrottleMiddleware } from "@getstrata/core/http/scimThrottleMiddleware";
782
710
 
783
711
  // ../../src/modules/scim/controller.ts
784
- import { withErrorHandling } from "@getstrata/core/http/response";
712
+ import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
785
713
 
786
714
  // ../../src/modules/scim/scimResponse.ts
787
715
  import {
@@ -837,6 +765,123 @@ var SCIM_SCHEMAS = {
837
765
  serviceProviderConfig: "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
838
766
  };
839
767
 
768
+ // ../../src/config/database.ts
769
+ function readInteger(name, fallback) {
770
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
771
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
772
+ }
773
+ var databaseConfig = {
774
+ url: process.env.DATABASE_URL ?? "",
775
+ poolMax: readInteger("DB_POOL_MAX", 10),
776
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
777
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
778
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
779
+ };
780
+
781
+ // ../../src/core/runtime/asyncContextStore.ts
782
+ import { AsyncLocalStorage } from "async_hooks";
783
+ function createAsyncContextStore(key) {
784
+ const symbol = Symbol.for(key);
785
+ const globalRecord = globalThis;
786
+ const existing = globalRecord[symbol];
787
+ if (existing) {
788
+ return existing;
789
+ }
790
+ const store = new AsyncLocalStorage;
791
+ globalRecord[symbol] = store;
792
+ return store;
793
+ }
794
+
795
+ // ../../src/core/database/connectionContext.ts
796
+ var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
797
+ function getActiveDatabaseConnection(fallback) {
798
+ return activeConnection.getStore() ?? fallback;
799
+ }
800
+
801
+ // ../../src/core/database/queryProxy.ts
802
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
803
+ function createDatabaseQueryProxy(pool) {
804
+ function resolveDatabase() {
805
+ return getActiveDatabaseConnection(pool);
806
+ }
807
+ function resolveDatabaseForProperty(property) {
808
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
809
+ return pool;
810
+ }
811
+ return resolveDatabase();
812
+ }
813
+ return new Proxy(function database() {}, {
814
+ apply(_target, _thisArg, args) {
815
+ return resolveDatabase()(...args);
816
+ },
817
+ get(_target, property) {
818
+ const connection = resolveDatabaseForProperty(property);
819
+ const value = connection[property];
820
+ return typeof value === "function" ? value.bind(connection) : value;
821
+ }
822
+ });
823
+ }
824
+
825
+ // ../../src/core/database/defaultConnection.ts
826
+ var defaultPool = {
827
+ connection: null
828
+ };
829
+ var defaultQuery = {
830
+ connection: null
831
+ };
832
+ function registerDefaultDatabasePool(connection) {
833
+ defaultPool.connection = connection;
834
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
835
+ }
836
+ function getDefaultDatabaseQuery() {
837
+ if (!defaultQuery.connection) {
838
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
839
+ }
840
+ return defaultQuery.connection;
841
+ }
842
+
843
+ // ../../src/db/connection/createConnection.ts
844
+ var {SQL } = globalThis.Bun;
845
+ function createDatabaseConnection(config) {
846
+ if (!config.url) {
847
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
848
+ }
849
+ return new SQL({
850
+ url: config.url,
851
+ max: config.poolMax,
852
+ idleTimeout: config.idleTimeoutSeconds,
853
+ maxLifetime: config.maxLifetimeSeconds,
854
+ connectionTimeout: config.connectionTimeoutSeconds
855
+ });
856
+ }
857
+
858
+ // ../../src/db/connection/index.ts
859
+ var connectionHolder = {
860
+ connection: null
861
+ };
862
+ function getDatabase() {
863
+ if (!connectionHolder.connection) {
864
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
865
+ registerDefaultDatabasePool(connectionHolder.connection);
866
+ }
867
+ return connectionHolder.connection;
868
+ }
869
+ function getDb() {
870
+ getDatabase();
871
+ return getDefaultDatabaseQuery();
872
+ }
873
+ var db = new Proxy(function database() {}, {
874
+ apply(_target, _thisArg, args) {
875
+ return getDb()(...args);
876
+ },
877
+ get(_target, property) {
878
+ const connection = getDb();
879
+ const value = connection[property];
880
+ return typeof value === "function" ? value.bind(connection) : value;
881
+ }
882
+ });
883
+ var connection_default = db;
884
+
840
885
  // ../../src/modules/organization/memberRepository.ts
841
886
  class OrganizationMemberRepository {
842
887
  constructor() {}
@@ -937,7 +982,11 @@ class OrganizationRepository extends BaseRepository {
937
982
  var repository_default = OrganizationRepository;
938
983
 
939
984
  // ../../src/modules/user/provider.ts
940
- import { CORE_TOKEN_SERVICE_TOKEN as CORE_TOKEN_SERVICE_TOKEN2 } from "@getstrata/bootstrap/config";
985
+ import {
986
+ CORE_ABILITY_CHECKER_TOKEN as CORE_ABILITY_CHECKER_TOKEN2,
987
+ CORE_AUTH_USER_DIRECTORY_TOKEN as CORE_AUTH_USER_DIRECTORY_TOKEN2,
988
+ CORE_TOKEN_SERVICE_TOKEN as CORE_TOKEN_SERVICE_TOKEN2
989
+ } from "@getstrata/bootstrap/config";
941
990
  import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
942
991
  import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
943
992
  import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
@@ -1246,30 +1295,30 @@ class ScimController {
1246
1295
  constructor(dependencies, service = createScimService(dependencies)) {
1247
1296
  this.service = service;
1248
1297
  }
1249
- serviceProviderConfig = withErrorHandling(async () => {
1298
+ serviceProviderConfig = withErrorHandling2(async () => {
1250
1299
  return scimResponse(this.service.serviceProviderConfig());
1251
1300
  });
1252
- listUsers = withErrorHandling(async (request) => {
1301
+ listUsers = withErrorHandling2(async (request) => {
1253
1302
  const url = new URL(request.url);
1254
1303
  const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
1255
1304
  const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
1256
1305
  return scimResponse(await this.service.listUsers(startIndex, count));
1257
1306
  });
1258
- createUser = withErrorHandling(async (request) => {
1307
+ createUser = withErrorHandling2(async (request) => {
1259
1308
  const payload = await request.json();
1260
1309
  const user = await this.service.createUser(payload);
1261
1310
  const id = Number.parseInt(user.id, 10);
1262
1311
  const record = await this.service.findUserRecord(id);
1263
1312
  return scimResponse(user, { status: 201, etagSource: record });
1264
1313
  });
1265
- showUser = withErrorHandling(async (request) => {
1314
+ showUser = withErrorHandling2(async (request) => {
1266
1315
  const params = request.params;
1267
1316
  const id = Number.parseInt(params?.id ?? "", 10);
1268
1317
  const record = await this.service.findUserRecord(id);
1269
1318
  const user = await this.service.getUser(id);
1270
1319
  return scimResponse(user, { request, etagSource: record });
1271
1320
  });
1272
- patchUser = withErrorHandling(async (request) => {
1321
+ patchUser = withErrorHandling2(async (request) => {
1273
1322
  const params = request.params;
1274
1323
  const id = Number.parseInt(params?.id ?? "", 10);
1275
1324
  const record = await this.service.findUserRecord(id);
@@ -1279,7 +1328,7 @@ class ScimController {
1279
1328
  const updated = await this.service.findUserRecord(id);
1280
1329
  return scimResponse(user, { etagSource: updated });
1281
1330
  });
1282
- deleteUser = withErrorHandling(async (request) => {
1331
+ deleteUser = withErrorHandling2(async (request) => {
1283
1332
  const params = request.params;
1284
1333
  const id = Number.parseInt(params?.id ?? "", 10);
1285
1334
  const record = await this.service.findUserRecord(id);
@@ -1287,20 +1336,20 @@ class ScimController {
1287
1336
  await this.service.deleteUser(id);
1288
1337
  return new Response(null, { status: 204 });
1289
1338
  });
1290
- listGroups = withErrorHandling(async (request) => {
1339
+ listGroups = withErrorHandling2(async (request) => {
1291
1340
  const url = new URL(request.url);
1292
1341
  const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
1293
1342
  const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
1294
1343
  return scimResponse(await this.service.listGroups(startIndex, count));
1295
1344
  });
1296
- showGroup = withErrorHandling(async (request) => {
1345
+ showGroup = withErrorHandling2(async (request) => {
1297
1346
  const params = request.params;
1298
1347
  const id = Number.parseInt(params?.id ?? "", 10);
1299
1348
  const record = await this.service.findOrganizationRecord(id);
1300
1349
  const group = await this.service.getGroup(id);
1301
1350
  return scimResponse(group, { request, etagSource: record });
1302
1351
  });
1303
- patchGroup = withErrorHandling(async (request) => {
1352
+ patchGroup = withErrorHandling2(async (request) => {
1304
1353
  const params = request.params;
1305
1354
  const id = Number.parseInt(params?.id ?? "", 10);
1306
1355
  const record = await this.service.findOrganizationRecord(id);
@@ -42,7 +42,7 @@ function createSpaRoutes(_dependencies) {
42
42
  });
43
43
  }
44
44
  return jsonResponse({
45
- error: "SPA build not found. Run `cd frontend && bun install && bun run build`."
45
+ error: "SPA build not found. Run `bun run build:frontend`."
46
46
  }, { status: 503 });
47
47
  },
48
48
  "/": async () => Response.redirect("/app/", 302)
@@ -32,6 +32,7 @@ import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
32
32
 
33
33
  // ../../src/bootstrap/httpKernel.ts
34
34
  import { createMembershipMiddleware } from "@getstrata/core/auth/membershipMiddleware";
35
+ import { resolveAbilityChecker } from "@getstrata/core/contracts/serviceTokens";
35
36
  import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
36
37
  import { createAuthorizeMiddleware } from "@getstrata/core/http/authorizeMiddleware";
37
38
  import { createBodySizeLimitMiddleware } from "@getstrata/core/http/bodySizeLimitMiddleware";
@@ -46,6 +47,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
46
47
  import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
47
48
  import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
48
49
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
50
+ import { withErrorHandling } from "@getstrata/core/http/response";
49
51
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
50
52
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
51
53
  import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
@@ -73,24 +75,38 @@ function parsePositiveInt(value, fallback) {
73
75
  }
74
76
  return Math.trunc(parsed);
75
77
  }
78
+ function parseWindowSeconds(secondsValue, msValue, fallback) {
79
+ if (secondsValue !== undefined && secondsValue.trim() !== "") {
80
+ return parsePositiveInt(secondsValue, fallback);
81
+ }
82
+ if (msValue !== undefined && msValue.trim() !== "") {
83
+ const parsedMs = Number(msValue);
84
+ if (Number.isFinite(parsedMs) && parsedMs > 0) {
85
+ return Math.max(1, Math.trunc(parsedMs / 1000));
86
+ }
87
+ }
88
+ return fallback;
89
+ }
76
90
  function resolveLoginRateLimit() {
77
91
  const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
78
92
  return {
79
93
  maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
80
- decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
94
+ decaySeconds: parseWindowSeconds(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, process.env.LOGIN_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
81
95
  };
82
96
  }
83
97
  function resolveRegisterRateLimit() {
84
98
  const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
85
99
  return {
86
100
  maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
87
- 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)
101
+ decaySeconds: parseWindowSeconds(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS, process.env.REGISTER_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
88
102
  };
89
103
  }
90
104
 
91
105
  // ../../src/bootstrap/config.ts
92
106
  import {
107
+ CORE_ABILITY_CHECKER_TOKEN,
93
108
  CORE_AUTH_TOKEN,
109
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
94
110
  CORE_CACHE_TOKEN,
95
111
  CORE_CONFIG_TOKEN,
96
112
  CORE_EVENT_BUS_TOKEN,
@@ -180,7 +196,7 @@ class HttpKernel {
180
196
  return this.wrap(["api", "authenticated"], handler);
181
197
  }
182
198
  wrapWeb(handler) {
183
- return this.wrap("web", handler);
199
+ return withErrorHandling(this.wrap("web", handler));
184
200
  }
185
201
  wrapWebPublicRead(handler) {
186
202
  if (isPublicReadsEnabled()) {
@@ -190,19 +206,19 @@ class HttpKernel {
190
206
  }
191
207
  wrapWebAuthenticated(handler) {
192
208
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
193
- return this.wrap("web", withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
209
+ return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
194
210
  }
195
211
  wrapWebAbility(ability, handler) {
196
212
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
197
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
213
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
198
214
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
199
215
  const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
200
- return this.wrap("web", withMiddleware(...middleware)(handler));
216
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
201
217
  }
202
218
  wrapWebGlobalAdmin(handler) {
203
219
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
204
220
  const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
205
- return this.wrap("web", withMiddleware(...middleware)(handler));
221
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
206
222
  }
207
223
  wrapAuthenticated(handler) {
208
224
  return this.wrap("authenticated", handler);
@@ -218,7 +234,7 @@ class HttpKernel {
218
234
  return withMiddleware(...middleware)(handler);
219
235
  }
220
236
  wrapAbility(ability, handler) {
221
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
237
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
222
238
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
223
239
  const middleware = [...this.group("authenticated"), requireAbility(ability)];
224
240
  return withMiddleware(...middleware)(handler);