@getstrata/bootstrap 0.2.65 → 0.2.68

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.
@@ -2,8 +2,7 @@
2
2
  var __jsonParse = (a) => JSON.parse(a);
3
3
 
4
4
  // ../../src/bootstrap/createRoutes.ts
5
- import { applyMiddlewareToRoutes as applyMiddlewareToRoutes3 } from "@getstrata/core/http/middleware";
6
- import { jsonResponse as jsonResponse4 } from "@getstrata/core/http/response";
5
+ import { jsonResponse as jsonResponse3 } from "@getstrata/core/http/response";
7
6
  import { isSpaEnabled as isSpaEnabled2, isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
8
7
  import { notFoundHtmlResponse as notFoundHtmlResponse2 } from "@getstrata/core/view";
9
8
 
@@ -12,35 +11,13 @@ var strata_default = __jsonParse("{\"index\":\"_.._/_.._/index.html\",\"files\":
12
11
 
13
12
  // ../../src/config/app.ts
14
13
  var appConfig = {
15
- name: process.env.APP_NAME?.trim() || "WorkHub",
14
+ name: process.env.APP_NAME?.trim() || "Strata",
16
15
  env: process.env.APP_ENV ?? "local",
17
16
  debug: (process.env.APP_DEBUG ?? "true") !== "false",
18
17
  url: process.env.APP_URL ?? "http://localhost:3000",
19
18
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
20
19
  };
21
20
 
22
- // ../../src/config/features.ts
23
- function readFeatureFlags() {
24
- return {
25
- webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
26
- fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
27
- auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
28
- oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
29
- samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
30
- scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
31
- billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
32
- siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
33
- publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
34
- emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
35
- mfa: (process.env.FEATURE_MFA ?? "false") === "true",
36
- registration: (process.env.FEATURE_REGISTRATION ?? "true") !== "false"
37
- };
38
- }
39
- var featureFlags = readFeatureFlags();
40
- function isFeatureEnabled(feature) {
41
- return readFeatureFlags()[feature];
42
- }
43
-
44
21
  // ../../src/bootstrap/buildModuleRoutes.ts
45
22
  import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
46
23
  import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
@@ -79,7 +56,33 @@ import { isPublicReadsEnabled } from "@getstrata/core/security/publicReads";
79
56
  import { createTenantMiddleware } from "@getstrata/core/tenant/tenantMiddleware";
80
57
  import { createTracingMiddleware } from "@getstrata/core/tracing/tracingMiddleware";
81
58
 
82
- // ../../src/config/rateLimit.ts
59
+ // ../../src/bootstrap/config.ts
60
+ import {
61
+ CORE_ABILITY_CHECKER_TOKEN,
62
+ CORE_AUTH_TOKEN,
63
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
64
+ CORE_CACHE_TOKEN,
65
+ CORE_CONFIG_TOKEN,
66
+ CORE_EVENT_BUS_TOKEN,
67
+ CORE_POLICY_GATE_TOKEN,
68
+ CORE_QUEUE_TOKEN,
69
+ CORE_TOKEN_SERVICE_TOKEN
70
+ } from "@getstrata/core/contracts/serviceTokens";
71
+ var APP_PORT_CONFIG_KEY = "app.port";
72
+ var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
73
+ var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
74
+ var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
75
+ var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
76
+ var DATABASE_URL_CONFIG_KEY = "database.url";
77
+ var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
78
+ var DEFAULT_APP_PORT = 3000;
79
+ var DEFAULT_CACHE_TTL_MS = 3600000;
80
+ var DEFAULT_CACHE_MAX_ENTRIES = 100;
81
+ var DEFAULT_CACHE_DRIVER = "array";
82
+ var DEFAULT_API_TOKEN = "";
83
+ var DEFAULT_QUEUE_DRIVER = "sync";
84
+
85
+ // ../../src/bootstrap/rateLimit.ts
83
86
  var LOCAL_LOGIN_RATE_LIMIT = {
84
87
  maxAttempts: 100,
85
88
  decaySeconds: 60
@@ -125,32 +128,6 @@ function resolveRegisterRateLimit() {
125
128
  };
126
129
  }
127
130
 
128
- // ../../src/bootstrap/config.ts
129
- import {
130
- CORE_ABILITY_CHECKER_TOKEN,
131
- CORE_AUTH_TOKEN,
132
- CORE_AUTH_USER_DIRECTORY_TOKEN,
133
- CORE_CACHE_TOKEN,
134
- CORE_CONFIG_TOKEN,
135
- CORE_EVENT_BUS_TOKEN,
136
- CORE_POLICY_GATE_TOKEN,
137
- CORE_QUEUE_TOKEN,
138
- CORE_TOKEN_SERVICE_TOKEN
139
- } from "@getstrata/core/contracts/serviceTokens";
140
- var APP_PORT_CONFIG_KEY = "app.port";
141
- var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
142
- var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
143
- var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
144
- var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
145
- var DATABASE_URL_CONFIG_KEY = "database.url";
146
- var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
147
- var DEFAULT_APP_PORT = 3000;
148
- var DEFAULT_CACHE_TTL_MS = 3600000;
149
- var DEFAULT_CACHE_MAX_ENTRIES = 100;
150
- var DEFAULT_CACHE_DRIVER = "array";
151
- var DEFAULT_API_TOKEN = "";
152
- var DEFAULT_QUEUE_DRIVER = "sync";
153
-
154
131
  // ../../src/bootstrap/httpKernel.ts
155
132
  class HttpKernel {
156
133
  dependencies;
@@ -221,7 +198,7 @@ class HttpKernel {
221
198
  wrapWeb(handler) {
222
199
  return withErrorHandling(this.wrap("web", handler));
223
200
  }
224
- wrapWebGuest(handler, home = "/organizations") {
201
+ wrapWebGuest(handler, home = "/") {
225
202
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
226
203
  return this.wrapWeb(async (request) => {
227
204
  const user = await auth.resolve(request);
@@ -381,7 +358,12 @@ function readDiscoverModulesState() {
381
358
  return state;
382
359
  }
383
360
  function configureModulesDirectory(modulesDir) {
384
- readDiscoverModulesState().configuredModulesDir = modulesDir;
361
+ const state = readDiscoverModulesState();
362
+ if (state.configuredModulesDir !== modulesDir) {
363
+ state.appModules.length = 0;
364
+ state.modulesReady = undefined;
365
+ }
366
+ state.configuredModulesDir = modulesDir;
385
367
  }
386
368
  function resolveModulesDirectory(options) {
387
369
  const state = readDiscoverModulesState();
@@ -391,7 +373,7 @@ function resolveModulesDirectory(options) {
391
373
  if (state.configuredModulesDir) {
392
374
  return state.configuredModulesDir;
393
375
  }
394
- throw new Error("configureModulesDirectory() must be called before discovering modules. WorkHub and the starter do this from preload.");
376
+ throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
395
377
  }
396
378
  async function loadDiscoveredModules(options) {
397
379
  const modulesDirectory = resolveModulesDirectory(options);
@@ -582,12 +564,10 @@ function buildWebModuleRoutes(dependencies, options = {}) {
582
564
  function registerRoute(method, path, middleware) {
583
565
  routeRegistry.register({ method, path, middleware });
584
566
  }
585
- function createWebRoutes(dependencies) {
567
+ function createWebRoutes(dependencies, options = {}) {
586
568
  const wrappedRoutes = buildWebModuleRoutes(dependencies, {
587
569
  clearRegistry: false,
588
- seedRoutes: {
589
- "/": () => Response.redirect("/organizations", 302)
590
- }
570
+ modules: options.modules
591
571
  });
592
572
  registerRoute("GET", "/", ["global", "web"]);
593
573
  wrappedRoutes["/assets/*"] = async (request) => {
@@ -603,16 +583,25 @@ function createWebRoutes(dependencies) {
603
583
  registerRoute("GET", "/assets/*", ["global", "web"]);
604
584
  return wrappedRoutes;
605
585
  }
606
- function mergeWebRoutes(dependencies, routes) {
586
+ function mergeWebRoutes(dependencies, routes, options = {}) {
607
587
  if (!isViewsEnabled2()) {
608
588
  return routes;
609
589
  }
610
590
  return {
611
- ...createWebRoutes(dependencies),
591
+ ...createWebRoutes(dependencies, options),
612
592
  ...routes
613
593
  };
614
594
  }
615
595
 
596
+ // ../../src/bootstrap/dogfoodApp.ts
597
+ function readDogfoodApp() {
598
+ const value = (process.env.DOGFOOD_APP ?? "hiroapp").trim().toLowerCase();
599
+ return value === "workhub" ? "workhub" : "hiroapp";
600
+ }
601
+ function isHiroappDogfood() {
602
+ return readDogfoodApp() === "hiroapp";
603
+ }
604
+
616
605
  // ../../src/bootstrap/health.ts
617
606
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
618
607
  import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
@@ -748,840 +737,31 @@ function createMetricsRoutes() {
748
737
  };
749
738
  }
750
739
 
751
- // ../../src/bootstrap/scimRoutes.ts
752
- import { createScimAuthMiddleware } from "@getstrata/core/auth/scimAuthMiddleware";
753
- import { withMiddleware as withMiddleware2 } from "@getstrata/core/http/routeMiddleware";
754
- import { createScimThrottleMiddleware } from "@getstrata/core/http/scimThrottleMiddleware";
755
-
756
- // ../../src/modules/scim/controller.ts
757
- import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
758
-
759
- // ../../src/modules/scim/scimResponse.ts
760
- import {
761
- applyConditionalGet,
762
- assertIfMatch,
763
- etagFromResource,
764
- isEtagEnabled
765
- } from "@getstrata/core/http/etag";
766
- import { jsonResponse as jsonResponse3 } from "@getstrata/core/http/response";
767
- function scimResponse(data, options = {}) {
768
- const status = options.status ?? 200;
769
- const response = jsonResponse3(data, {
770
- status,
771
- headers: {
772
- "content-type": "application/scim+json"
773
- }
774
- });
775
- if (!isEtagEnabled() || !options.etagSource) {
776
- return response;
777
- }
778
- const etag = etagFromResource(options.etagSource);
779
- if (options.request) {
780
- return applyConditionalGet(options.request, response, etag);
781
- }
782
- const headers = new Headers(response.headers);
783
- headers.set("ETag", etag);
784
- headers.set("Cache-Control", "private, must-revalidate");
785
- return new Response(response.body, {
786
- status: response.status,
787
- statusText: response.statusText,
788
- headers
789
- });
790
- }
791
- function assertScimIfMatch(request, etagSource) {
792
- if (!isEtagEnabled()) {
793
- return;
794
- }
795
- assertIfMatch(request, etagFromResource(etagSource), { required: true });
796
- }
797
-
798
- // ../../src/modules/scim/service.ts
799
- import { hashPassword as hashPassword3 } from "@getstrata/core/auth/password";
800
- import { resolveService } from "@getstrata/core/contracts/di";
801
- import { NotFoundError as NotFoundError7 } from "@getstrata/core/errors/http";
802
- import { currentTenantId as currentTenantId4 } from "@getstrata/core/tenant/tenantContext";
803
-
804
- // ../../src/domain/scim.ts
805
- var SCIM_SCHEMAS = {
806
- user: "urn:ietf:params:scim:schemas:core:2.0:User",
807
- group: "urn:ietf:params:scim:schemas:core:2.0:Group",
808
- listResponse: "urn:ietf:params:scim:api:messages:2.0:ListResponse",
809
- patchOp: "urn:ietf:params:scim:api:messages:2.0:PatchOp",
810
- serviceProviderConfig: "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
811
- };
812
-
813
- // ../../src/modules/organization/memberRepository.ts
814
- import { NotFoundError } from "@getstrata/core/errors/http";
815
-
816
- // ../../src/config/database.ts
817
- function readInteger(name, fallback) {
818
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
819
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
820
- }
821
- var databaseConfig = {
822
- url: process.env.DATABASE_URL ?? "",
823
- poolMax: readInteger("DB_POOL_MAX", 10),
824
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
825
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
826
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
827
- };
828
-
829
- // ../../src/core/runtime/asyncContextStore.ts
830
- import { AsyncLocalStorage } from "async_hooks";
831
- function createAsyncContextStore(key) {
832
- const symbol = Symbol.for(key);
833
- const globalRecord = globalThis;
834
- const existing = globalRecord[symbol];
835
- if (existing) {
836
- return existing;
837
- }
838
- const store = new AsyncLocalStorage;
839
- globalRecord[symbol] = store;
840
- return store;
841
- }
842
-
843
- // ../../src/core/database/connectionContext.ts
844
- var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
845
- function getActiveDatabaseConnection(fallback) {
846
- return activeConnection.getStore() ?? fallback;
847
- }
848
-
849
- // ../../src/core/database/queryProxy.ts
850
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
851
- function createDatabaseQueryProxy(pool) {
852
- function resolveDatabase() {
853
- return getActiveDatabaseConnection(pool);
854
- }
855
- function resolveDatabaseForProperty(property) {
856
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
857
- return pool;
858
- }
859
- return resolveDatabase();
860
- }
861
- return new Proxy(function database() {}, {
862
- apply(_target, _thisArg, args) {
863
- return resolveDatabase()(...args);
864
- },
865
- get(_target, property) {
866
- const connection = resolveDatabaseForProperty(property);
867
- const value = connection[property];
868
- return typeof value === "function" ? value.bind(connection) : value;
869
- }
870
- });
871
- }
872
-
873
- // ../../src/core/database/defaultConnection.ts
874
- var defaultPool = {
875
- connection: null
876
- };
877
- var defaultQuery = {
878
- connection: null
879
- };
880
- function registerDefaultDatabasePool(connection) {
881
- defaultPool.connection = connection;
882
- defaultQuery.connection = createDatabaseQueryProxy(connection);
883
- }
884
- function getDefaultDatabaseQuery() {
885
- if (!defaultQuery.connection) {
886
- throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
887
- }
888
- return defaultQuery.connection;
889
- }
890
-
891
- // ../../src/db/connection/createConnection.ts
892
- var {SQL } = globalThis.Bun;
893
- function createDatabaseConnection(config) {
894
- if (!config.url) {
895
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
896
- }
897
- return new SQL({
898
- url: config.url,
899
- max: config.poolMax,
900
- idleTimeout: config.idleTimeoutSeconds,
901
- maxLifetime: config.maxLifetimeSeconds,
902
- connectionTimeout: config.connectionTimeoutSeconds
903
- });
904
- }
905
-
906
- // ../../src/db/connection/index.ts
907
- var connectionHolder = {
908
- connection: null
909
- };
910
- function getDatabase() {
911
- if (!connectionHolder.connection) {
912
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
913
- registerDefaultDatabasePool(connectionHolder.connection);
914
- }
915
- return connectionHolder.connection;
916
- }
917
- function getDb() {
918
- getDatabase();
919
- return getDefaultDatabaseQuery();
920
- }
921
- var db = new Proxy(function database() {}, {
922
- apply(_target, _thisArg, args) {
923
- return getDb()(...args);
924
- },
925
- get(_target, property) {
926
- const connection = getDb();
927
- const value = connection[property];
928
- return typeof value === "function" ? value.bind(connection) : value;
929
- }
930
- });
931
- var connection_default = db;
932
-
933
- // ../../src/modules/organization/memberRepository.ts
934
- class OrganizationMemberRepository {
935
- constructor() {}
936
- async findMembership(userId, organizationId) {
937
- const rows = await connection_default`
938
- SELECT id, organization_id, user_id, role, created_at
939
- FROM organization_member
940
- WHERE user_id = ${userId} AND organization_id = ${organizationId}
941
- LIMIT 1
942
- `;
943
- return rows[0] ?? null;
944
- }
945
- async listForUser(userId) {
946
- return await connection_default`
947
- SELECT id, organization_id, user_id, role, created_at
948
- FROM organization_member
949
- WHERE user_id = ${userId}
950
- ORDER BY organization_id
951
- `;
952
- }
953
- async listForOrganization(organizationId) {
954
- return await connection_default`
955
- SELECT id, organization_id, user_id, role, created_at
956
- FROM organization_member
957
- WHERE organization_id = ${organizationId}
958
- ORDER BY id
959
- `;
960
- }
961
- async addMember(input) {
962
- const rows = await connection_default`
963
- INSERT INTO organization_member (organization_id, user_id, role)
964
- VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
965
- RETURNING id, organization_id, user_id, role, created_at
966
- `;
967
- const row = rows[0];
968
- if (!row) {
969
- throw new Error("Organization member insert did not return a row.");
970
- }
971
- return row;
972
- }
973
- async updateMemberRole(organizationId, userId, role) {
974
- const rows = await connection_default`
975
- UPDATE organization_member
976
- SET role = ${role}
977
- WHERE organization_id = ${organizationId} AND user_id = ${userId}
978
- RETURNING id, organization_id, user_id, role, created_at
979
- `;
980
- const row = rows[0];
981
- if (!row) {
982
- throw new NotFoundError(`Organization member ${userId} not found.`);
983
- }
984
- return row;
985
- }
986
- async removeMember(organizationId, userId) {
987
- const rows = await connection_default`
988
- DELETE FROM organization_member
989
- WHERE organization_id = ${organizationId} AND user_id = ${userId}
990
- RETURNING id
991
- `;
992
- return rows.length > 0;
993
- }
994
- }
995
- var memberRepository_default = OrganizationMemberRepository;
996
-
997
- // ../../src/modules/organization/repository.ts
998
- import { BaseRepository } from "@getstrata/core/database/baseRepository";
999
- import { NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
1000
- import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
1001
-
1002
- // ../../src/modules/organization/table.ts
1003
- import { defineTable } from "@getstrata/core/database/table";
1004
-
1005
- // ../../src/domain/workhub.ts
1006
- var ORGANIZATION_TABLE = "organization";
1007
-
1008
- // ../../src/modules/organization/table.ts
1009
- var organizationTable = defineTable({
1010
- name: ORGANIZATION_TABLE,
1011
- primaryKey: "id",
1012
- columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
1013
- softDeletes: true,
1014
- defaultOrderBy: { column: "id", direction: "ASC" }
1015
- });
1016
-
1017
- // ../../src/modules/organization/repository.ts
1018
- class OrganizationRepository extends BaseRepository {
1019
- constructor() {
1020
- super(organizationTable);
1021
- }
1022
- async findBySlug(slug) {
1023
- return await this.firstOrNull({ slug });
1024
- }
1025
- async listForTenant(options) {
1026
- return await this.findAll({
1027
- limit: options.limit,
1028
- offset: options.offset,
1029
- where: { tenant_id: options.tenantId ?? currentTenantId() }
1030
- });
1031
- }
1032
- async countForTenant(tenantId = currentTenantId()) {
1033
- return await this.countWhere({ tenant_id: tenantId });
1034
- }
1035
- async findForTenantOrThrow(id, tenantId = currentTenantId()) {
1036
- const organization = await this.findById(id);
1037
- if (!organization || organization.tenant_id !== tenantId) {
1038
- throw new NotFoundError2(`SCIM group ${id} not found.`);
1039
- }
1040
- return organization;
1041
- }
1042
- }
1043
- var repository_default = OrganizationRepository;
1044
-
1045
- // ../../src/modules/user/provider.ts
1046
- import {
1047
- CORE_ABILITY_CHECKER_TOKEN as CORE_ABILITY_CHECKER_TOKEN2,
1048
- CORE_AUTH_USER_DIRECTORY_TOKEN as CORE_AUTH_USER_DIRECTORY_TOKEN2,
1049
- CORE_TOKEN_SERVICE_TOKEN as CORE_TOKEN_SERVICE_TOKEN2
1050
- } from "@getstrata/bootstrap/config";
1051
- import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
1052
- import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
1053
- import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
1054
- import { getRequiredDependency } from "@getstrata/core/contracts/di";
1055
-
1056
- // ../../src/modules/user/apiTokenRepository.ts
1057
- import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
1058
-
1059
- // ../../src/modules/user/apiTokenTable.ts
1060
- import { defineTable as defineTable2 } from "@getstrata/core/database/table";
1061
- var apiTokenTable = defineTable2({
1062
- name: "api_token",
1063
- primaryKey: "id",
1064
- columns: [
1065
- "id",
1066
- "user_id",
1067
- "name",
1068
- "token_hash",
1069
- "abilities",
1070
- "last_used_at",
1071
- "expires_at",
1072
- "created_at"
1073
- ],
1074
- defaultOrderBy: { column: "id", direction: "ASC" }
1075
- });
1076
-
1077
- // ../../src/modules/user/authService.ts
1078
- import { currentAuthUser } from "@getstrata/core/auth/authContext";
1079
- import { hashPassword, verifyPassword } from "@getstrata/core/auth/password";
1080
- import { normalizeEmail } from "@getstrata/core/crypto/fieldEncryption";
1081
- import { protectMfaSecret, revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
1082
- import { UnauthorizedError as UnauthorizedError2, ValidationError } from "@getstrata/core/errors/http";
1083
- import {
1084
- generateRecoveryCodes,
1085
- hashRecoveryCode,
1086
- recoveryCodeMatches
1087
- } from "@getstrata/core/security/recoveryCodes";
1088
- import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
1089
- import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
1090
- import { buildOtpauthUrl, generateTotpSecret, verifyTotp } from "@getstrata/core/security/totp";
1091
- import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
1092
-
1093
- // ../../src/core/auth/abilityCatalog.ts
1094
- var MEMBER_ABILITIES = [
1095
- "organizations:read",
1096
- "organizations:create",
1097
- "projects:read",
1098
- "projects:create",
1099
- "tasks:read",
1100
- "tasks:create",
1101
- "comments:read",
1102
- "comments:create",
1103
- "attachments:read",
1104
- "attachments:create",
1105
- "auth:tokens:read",
1106
- "auth:tokens:write",
1107
- "auth:tokens:delete"
1108
- ];
1109
- var ADMIN_ABILITIES = [
1110
- ...MEMBER_ABILITIES,
1111
- "organizations:create",
1112
- "organizations:update",
1113
- "organizations:delete",
1114
- "projects:update",
1115
- "projects:delete",
1116
- "tasks:update",
1117
- "tasks:delete",
1118
- "comments:update",
1119
- "comments:delete",
1120
- "attachments:delete",
1121
- "webhooks:read",
1122
- "webhooks:write",
1123
- "audit:read"
1124
- ];
1125
- // ../../src/modules/user/mfaRequiredError.ts
1126
- import { UnauthorizedError } from "@getstrata/core/errors/http";
1127
-
1128
- // ../../src/modules/user/currentOrganizationService.ts
1129
- import { assertResourceInCurrentTenant } from "@getstrata/core/auth/membershipScope";
1130
- import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
1131
- import { NotFoundError as NotFoundError3 } from "@getstrata/core/errors/http";
1132
-
1133
- // ../../src/modules/user/notificationRepository.ts
1134
- import { BaseRepository as BaseRepository3 } from "@getstrata/core/database/baseRepository";
1135
-
1136
- // ../../src/modules/user/notificationTable.ts
1137
- import { defineTable as defineTable3 } from "@getstrata/core/database/table";
1138
- var notificationTable = defineTable3({
1139
- name: "notification",
1140
- primaryKey: "id",
1141
- columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
1142
- defaultOrderBy: { column: "created_at", direction: "DESC" }
1143
- });
1144
-
1145
- // ../../src/modules/user/notificationService.ts
1146
- import { NotFoundError as NotFoundError4 } from "@getstrata/core/errors/http";
1147
-
1148
- // ../../src/modules/user/oauthIdentityRepository.ts
1149
- import { BaseRepository as BaseRepository4 } from "@getstrata/core/database/baseRepository";
1150
- import { defineTable as defineTable4 } from "@getstrata/core/database/table";
1151
- var oauthIdentityTable = defineTable4({
1152
- name: "oauth_identity",
1153
- primaryKey: "id",
1154
- columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
1155
- });
1156
-
1157
- // ../../src/modules/user/passwordResetService.ts
1158
- import { hashPassword as hashPassword2 } from "@getstrata/core/auth/password";
1159
- import { hashApiToken } from "@getstrata/core/auth/tokenHash";
1160
- import { repositoryConnection as db2 } from "@getstrata/core/database/repositoryConnection";
1161
- import { ValidationError as ValidationError2 } from "@getstrata/core/errors/http";
1162
- import { absoluteTemporarySignedUrl } from "@getstrata/core/http/signedUrl";
1163
- import { mailer } from "@getstrata/core/mail/mailer";
1164
- import { sendMarkdownMail } from "@getstrata/core/mail/markdownMailable";
1165
- import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
1166
- import { logSecurityEvent as logSecurityEvent2 } from "@getstrata/core/security/securityEvents";
1167
- import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
1168
- var RESET_TTL_SECONDS = 60 * 60;
1169
- var VERIFY_TTL_SECONDS = 60 * 60 * 24;
1170
-
1171
- // ../../src/modules/user/profilePhotoService.ts
1172
- import { BadRequestError, NotFoundError as NotFoundError5 } from "@getstrata/core/errors/http";
1173
- import { isImageMimeType, resizeImageContents } from "@getstrata/core/media/imageTransform";
1174
-
1175
- // ../../src/modules/user/repository.ts
1176
- import {
1177
- emailLookupForQuery,
1178
- protectEmail,
1179
- revealEmail
1180
- } from "@getstrata/core/crypto/fieldEncryption";
1181
- import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
1182
- import { BaseRepository as BaseRepository5 } from "@getstrata/core/database/baseRepository";
1183
- import { currentTenantId as currentTenantId3 } from "@getstrata/core/tenant/tenantContext";
1184
-
1185
- // ../../src/modules/user/table.ts
1186
- import { defineTable as defineTable5 } from "@getstrata/core/database/table";
1187
- var userTable = defineTable5({
1188
- name: "users",
1189
- primaryKey: "id",
1190
- columns: [
1191
- "id",
1192
- "name",
1193
- "email",
1194
- "email_lookup",
1195
- "role",
1196
- "tenant_id",
1197
- "password_hash",
1198
- "email_verified_at",
1199
- "mfa_secret",
1200
- "mfa_enabled",
1201
- "mfa_recovery_codes",
1202
- "profile_photo_path",
1203
- "session_valid_after",
1204
- "current_organization_id",
1205
- "created_at",
1206
- "updated_at"
1207
- ],
1208
- defaultOrderBy: { column: "id", direction: "ASC" }
1209
- });
1210
-
1211
- // ../../src/modules/user/repository.ts
1212
- class UserRepository extends BaseRepository5 {
1213
- constructor() {
1214
- super(userTable);
1215
- }
1216
- decode(record) {
1217
- return {
1218
- ...record,
1219
- email: revealEmail(record.email),
1220
- mfa_secret: revealMfaSecret2(record.mfa_secret)
1221
- };
1222
- }
1223
- async findById(id) {
1224
- const record = await super.findById(id);
1225
- return record ? this.decode(record) : null;
1226
- }
1227
- async findAll(options = {}) {
1228
- const records = await super.findAll(options);
1229
- return records.map((record) => this.decode(record));
1230
- }
1231
- async create(values) {
1232
- const email = values.email;
1233
- if (!email) {
1234
- throw new Error("Email is required.");
1235
- }
1236
- const protectedEmail = protectEmail(email);
1237
- const record = await super.create({
1238
- ...values,
1239
- tenant_id: values.tenant_id ?? currentTenantId3(),
1240
- email: protectedEmail.storedEmail,
1241
- email_lookup: protectedEmail.emailLookup,
1242
- password_hash: values.password_hash ?? ""
1243
- });
1244
- return this.decode(record);
1245
- }
1246
- async updateByIdOrThrow(id, values, errorFactory) {
1247
- const changes = { ...values };
1248
- if (values.email !== undefined) {
1249
- const protectedEmail = protectEmail(values.email);
1250
- changes.email = protectedEmail.storedEmail;
1251
- changes.email_lookup = protectedEmail.emailLookup;
1252
- }
1253
- const record = await super.updateByIdOrThrow(id, changes, errorFactory);
1254
- return this.decode(record);
1255
- }
1256
- async findByEmail(email) {
1257
- const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
1258
- const record = records[0];
1259
- return record ? this.decode(record) : null;
1260
- }
1261
- async countForTenant(tenantId = currentTenantId3()) {
1262
- return await this.countWhere({ tenant_id: tenantId });
1263
- }
1264
- }
1265
- var repository_default2 = UserRepository;
1266
-
1267
- // ../../src/modules/user/tokenService.ts
1268
- import { ADMIN_ABILITIES as ADMIN_ABILITIES2 } from "@getstrata/core/auth/abilityCatalog";
1269
- import { hashApiToken as hashApiToken2 } from "@getstrata/core/auth/tokenHash";
1270
- import { ForbiddenError, NotFoundError as NotFoundError6, ValidationError as ValidationError3 } from "@getstrata/core/errors/http";
1271
- import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
1272
-
1273
- // ../../src/modules/user/provider.ts
1274
- var userRepositoryToken = "user.repository";
1275
-
1276
- // ../../src/modules/scim/service.ts
1277
- class ScimService {
1278
- users;
1279
- organizations;
1280
- members;
1281
- constructor(users, organizations, members) {
1282
- this.users = users;
1283
- this.organizations = organizations;
1284
- this.members = members;
1285
- }
1286
- serviceProviderConfig() {
1287
- return {
1288
- schemas: [SCIM_SCHEMAS.serviceProviderConfig],
1289
- patch: { supported: true },
1290
- bulk: { supported: false },
1291
- filter: { supported: false },
1292
- changePassword: { supported: false },
1293
- sort: { supported: false },
1294
- etag: { supported: true },
1295
- authenticationSchemes: [
1296
- {
1297
- type: "oauthbearertoken",
1298
- name: "OAuth Bearer Token",
1299
- description: "SCIM bearer token authentication"
1300
- }
1301
- ]
1302
- };
1303
- }
1304
- async listUsers(startIndex = 1, count = 100) {
1305
- const tenantId = currentTenantId4();
1306
- const offset = Math.max(startIndex - 1, 0);
1307
- const records = await this.users.findAll({
1308
- limit: count,
1309
- offset,
1310
- where: { tenant_id: tenantId }
1311
- });
1312
- const total = await this.users.countForTenant(tenantId);
1313
- return {
1314
- schemas: [SCIM_SCHEMAS.listResponse],
1315
- totalResults: total,
1316
- startIndex,
1317
- itemsPerPage: records.length,
1318
- Resources: records.map((user) => this.toScimUser(user))
1319
- };
1320
- }
1321
- async getUser(id) {
1322
- const user = await this.findUserRecord(id);
1323
- return this.toScimUser(user);
1324
- }
1325
- async findUserRecord(id) {
1326
- const user = await this.users.findById(id);
1327
- if (!user || user.tenant_id !== currentTenantId4()) {
1328
- throw new NotFoundError7(`SCIM user ${id} not found.`);
1329
- }
1330
- return user;
1331
- }
1332
- async createUser(payload) {
1333
- const email = payload.userName ?? payload.emails?.find((entry) => entry.primary)?.value ?? payload.emails?.[0]?.value;
1334
- if (!email) {
1335
- throw new Error("SCIM userName or email is required.");
1336
- }
1337
- const passwordHash = await hashPassword3(crypto.randomUUID());
1338
- const user = await this.users.create({
1339
- name: payload.name?.formatted ?? email.split("@")[0] ?? "SCIM User",
1340
- email,
1341
- role: "member",
1342
- tenant_id: currentTenantId4(),
1343
- password_hash: passwordHash,
1344
- created_at: new Date,
1345
- updated_at: new Date
1346
- });
1347
- return this.toScimUser(user);
1348
- }
1349
- async patchUser(id, operations) {
1350
- const user = await this.findUserRecord(id);
1351
- const changes = { updated_at: new Date };
1352
- for (const operation of operations) {
1353
- if (operation.op.toLowerCase() === "replace" && operation.path === "active") {
1354
- continue;
1355
- }
1356
- if (operation.op.toLowerCase() === "replace" && operation.path === "displayName") {
1357
- changes.name = String(operation.value ?? user.name);
1358
- }
1359
- if (operation.op.toLowerCase() === "replace" && (operation.path === "userName" || operation.path === 'emails[type eq "work"].value')) {
1360
- changes.email = String(operation.value ?? user.email);
1361
- }
1362
- }
1363
- const updated = await this.users.updateByIdOrThrow(id, changes);
1364
- return this.toScimUser(updated);
1365
- }
1366
- async deleteUser(id) {
1367
- const user = await this.findUserRecord(id);
1368
- const deleted = await this.users.deleteById(id);
1369
- if (!deleted) {
1370
- throw new NotFoundError7(`SCIM user ${id} not found.`);
1371
- }
1372
- return { id: user.id, updated_at: user.updated_at };
1373
- }
1374
- async listGroups(startIndex = 1, count = 100) {
1375
- const tenantId = currentTenantId4();
1376
- const offset = Math.max(startIndex - 1, 0);
1377
- const rows = await this.organizations.listForTenant({ limit: count, offset, tenantId });
1378
- const total = await this.organizations.countForTenant(tenantId);
1379
- const resources = await Promise.all(rows.map((row) => this.toScimGroup(row)));
1380
- return {
1381
- schemas: [SCIM_SCHEMAS.listResponse],
1382
- totalResults: total,
1383
- startIndex,
1384
- itemsPerPage: resources.length,
1385
- Resources: resources
1386
- };
1387
- }
1388
- async getGroup(id) {
1389
- const organization = await this.findOrganizationRecord(id);
1390
- return await this.toScimGroup(organization);
1391
- }
1392
- async findOrganizationRecord(id) {
1393
- return await this.organizations.findForTenantOrThrow(id, currentTenantId4());
1394
- }
1395
- async patchGroup(id, operations) {
1396
- for (const operation of operations) {
1397
- if (operation.op.toLowerCase() !== "add" || operation.path !== "members") {
1398
- continue;
1399
- }
1400
- const members = Array.isArray(operation.value) ? operation.value : operation.value ? [operation.value] : [];
1401
- for (const member of members) {
1402
- const userId = Number.parseInt(String(member.value ?? ""), 10);
1403
- if (Number.isInteger(userId) && userId > 0) {
1404
- await this.members.addMember({ organizationId: id, userId, role: "member" });
1405
- }
1406
- }
1407
- }
1408
- const organization = await this.findOrganizationRecord(id);
1409
- return await this.toScimGroup(organization);
1410
- }
1411
- async toScimGroup(organization) {
1412
- const members = await this.members.listForOrganization(organization.id);
1413
- return {
1414
- schemas: [SCIM_SCHEMAS.group],
1415
- id: String(organization.id),
1416
- displayName: organization.name,
1417
- externalId: organization.slug,
1418
- members: members.map((member) => ({
1419
- value: String(member.user_id),
1420
- display: String(member.user_id)
1421
- })),
1422
- meta: {
1423
- resourceType: "Group",
1424
- lastModified: organization.updated_at.toISOString()
1425
- }
1426
- };
1427
- }
1428
- toScimUser(user) {
1429
- return {
1430
- schemas: [SCIM_SCHEMAS.user],
1431
- id: String(user.id),
1432
- userName: user.email,
1433
- name: { formatted: user.name },
1434
- displayName: user.name,
1435
- active: true,
1436
- emails: [{ value: user.email, primary: true, type: "work" }],
1437
- roles: [{ value: user.role, primary: true }],
1438
- meta: {
1439
- resourceType: "User",
1440
- created: user.created_at?.toISOString(),
1441
- lastModified: user.updated_at?.toISOString()
1442
- }
1443
- };
1444
- }
1445
- }
1446
- function createScimService(dependencies) {
1447
- return new ScimService(resolveService(dependencies, userRepositoryToken), new repository_default, new memberRepository_default);
1448
- }
1449
-
1450
- // ../../src/modules/scim/controller.ts
1451
- class ScimController {
1452
- service;
1453
- constructor(dependencies, service = createScimService(dependencies)) {
1454
- this.service = service;
1455
- }
1456
- serviceProviderConfig = withErrorHandling2(async () => {
1457
- return scimResponse(this.service.serviceProviderConfig());
1458
- });
1459
- listUsers = withErrorHandling2(async (request) => {
1460
- const url = new URL(request.url);
1461
- const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
1462
- const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
1463
- return scimResponse(await this.service.listUsers(startIndex, count));
1464
- });
1465
- createUser = withErrorHandling2(async (request) => {
1466
- const payload = await request.json();
1467
- const user = await this.service.createUser(payload);
1468
- const id = Number.parseInt(user.id, 10);
1469
- const record = await this.service.findUserRecord(id);
1470
- return scimResponse(user, { status: 201, etagSource: record });
1471
- });
1472
- showUser = withErrorHandling2(async (request) => {
1473
- const params = request.params;
1474
- const id = Number.parseInt(params?.id ?? "", 10);
1475
- const record = await this.service.findUserRecord(id);
1476
- const user = await this.service.getUser(id);
1477
- return scimResponse(user, { request, etagSource: record });
1478
- });
1479
- patchUser = withErrorHandling2(async (request) => {
1480
- const params = request.params;
1481
- const id = Number.parseInt(params?.id ?? "", 10);
1482
- const record = await this.service.findUserRecord(id);
1483
- assertScimIfMatch(request, record);
1484
- const body = await request.json();
1485
- const user = await this.service.patchUser(id, body.Operations ?? []);
1486
- const updated = await this.service.findUserRecord(id);
1487
- return scimResponse(user, { etagSource: updated });
1488
- });
1489
- deleteUser = withErrorHandling2(async (request) => {
1490
- const params = request.params;
1491
- const id = Number.parseInt(params?.id ?? "", 10);
1492
- const record = await this.service.findUserRecord(id);
1493
- assertScimIfMatch(request, record);
1494
- await this.service.deleteUser(id);
1495
- return new Response(null, { status: 204 });
1496
- });
1497
- listGroups = withErrorHandling2(async (request) => {
1498
- const url = new URL(request.url);
1499
- const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
1500
- const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
1501
- return scimResponse(await this.service.listGroups(startIndex, count));
1502
- });
1503
- showGroup = withErrorHandling2(async (request) => {
1504
- const params = request.params;
1505
- const id = Number.parseInt(params?.id ?? "", 10);
1506
- const record = await this.service.findOrganizationRecord(id);
1507
- const group = await this.service.getGroup(id);
1508
- return scimResponse(group, { request, etagSource: record });
1509
- });
1510
- patchGroup = withErrorHandling2(async (request) => {
1511
- const params = request.params;
1512
- const id = Number.parseInt(params?.id ?? "", 10);
1513
- const record = await this.service.findOrganizationRecord(id);
1514
- assertScimIfMatch(request, record);
1515
- const body = await request.json();
1516
- const group = await this.service.patchGroup(id, body.Operations ?? []);
1517
- const updated = await this.service.findOrganizationRecord(id);
1518
- return scimResponse(group, { etagSource: updated });
1519
- });
1520
- }
1521
- var controller_default = ScimController;
1522
-
1523
- // ../../src/bootstrap/scimRoutes.ts
1524
- function createScimRoutes(dependencies) {
1525
- const redisUrl = process.env.REDIS_URL?.trim() ?? "";
1526
- const secured = withMiddleware2(createScimThrottleMiddleware({
1527
- redisUrl: redisUrl || undefined,
1528
- maxAttempts: Number(process.env.SCIM_RATE_LIMIT_PER_MINUTE ?? "60"),
1529
- decaySeconds: 60
1530
- }), createScimAuthMiddleware());
1531
- const bind = (method) => {
1532
- return secured(async (request) => {
1533
- const controller = new controller_default(dependencies);
1534
- return await method(controller)(request);
1535
- });
1536
- };
1537
- return {
1538
- "/scim/v2/ServiceProviderConfig": {
1539
- GET: bind((controller) => controller.serviceProviderConfig)
1540
- },
1541
- "/scim/v2/Users": {
1542
- GET: bind((controller) => controller.listUsers),
1543
- POST: bind((controller) => controller.createUser)
1544
- },
1545
- "/scim/v2/Users/:id": {
1546
- GET: bind((controller) => controller.showUser),
1547
- PATCH: bind((controller) => controller.patchUser),
1548
- DELETE: bind((controller) => controller.deleteUser)
1549
- },
1550
- "/scim/v2/Groups": {
1551
- GET: bind((controller) => controller.listGroups)
1552
- },
1553
- "/scim/v2/Groups/:id": {
1554
- GET: bind((controller) => controller.showGroup),
1555
- PATCH: bind((controller) => controller.patchGroup)
1556
- }
1557
- };
1558
- }
1559
-
1560
740
  // ../../src/bootstrap/createRoutes.ts
1561
741
  function registerRoute2(method, path, middleware) {
1562
742
  routeRegistry.register({ method, path, middleware });
1563
743
  }
1564
744
  function createRoutes(dependencies) {
1565
- const kernel = createHttpKernel(dependencies);
745
+ const apiPrefix = isHiroappDogfood() ? "" : appConfig.apiPrefix;
1566
746
  const wrappedModuleRoutes = buildModuleRoutes(dependencies, {
1567
- apiPrefix: appConfig.apiPrefix,
1568
- clearRegistry: true
747
+ apiPrefix,
748
+ clearRegistry: true,
749
+ modules: []
1569
750
  });
1570
751
  const healthRoutes = createHealthRoutes(dependencies);
1571
752
  const metricsRoutes = createMetricsRoutes();
1572
- const scimRoutes = isFeatureEnabled("scim") ? applyMiddlewareToRoutes3(registerOpenApiRouteMap(createScimRoutes(dependencies), ["global", "scim"]), kernel.globalMiddleware()) : {};
1573
753
  registerRoute2("GET", "/health", []);
1574
754
  registerRoute2("GET", "/ready", []);
1575
755
  registerRoute2("GET", "/metrics", []);
756
+ const notFoundApiPath = `${apiPrefix || "/api"}/*`;
1576
757
  const baseRoutes = {
1577
758
  ...healthRoutes,
1578
759
  ...metricsRoutes,
1579
- ...scimRoutes,
1580
760
  ...isViewsEnabled3() || isSpaEnabled2() ? {} : { "/": strata_default },
1581
761
  ...wrappedModuleRoutes,
1582
- [`${appConfig.apiPrefix}/*`]: async () => {
1583
- registerRoute2("GET", `${appConfig.apiPrefix}/*`, ["global", "api"]);
1584
- return jsonResponse4({ error: "Not Found" }, { status: 404 });
762
+ [notFoundApiPath]: async () => {
763
+ registerRoute2("GET", notFoundApiPath, ["global", "api"]);
764
+ return jsonResponse3({ error: "Not Found" }, { status: 404 });
1585
765
  },
1586
766
  "/*": async () => {
1587
767
  registerRoute2("GET", "/*", []);
@@ -1589,9 +769,9 @@ function createRoutes(dependencies) {
1589
769
  return notFoundHtmlResponse2();
1590
770
  }
1591
771
  if (isSpaEnabled2()) {
1592
- return jsonResponse4({ error: "Not Found" }, { status: 404 });
772
+ return jsonResponse3({ error: "Not Found" }, { status: 404 });
1593
773
  }
1594
- return jsonResponse4({ error: "Not Found" }, { status: 404 });
774
+ return jsonResponse3({ error: "Not Found" }, { status: 404 });
1595
775
  }
1596
776
  };
1597
777
  return mergeSpaRoutes(dependencies, mergeWebRoutes(dependencies, baseRoutes));