@getstrata/bootstrap 0.2.66 → 0.4.2
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.
- package/CHANGELOG.md +45 -10
- package/README.md +10 -10
- package/dist/_.._/_.._/index.html +2 -2
- package/dist/bootstrap/createSpaRoutes.d.ts +9 -2
- package/dist/bootstrap/createWebRoutes.d.ts +7 -3
- package/dist/bootstrap/dogfoodApp.d.ts +7 -0
- package/dist/bootstrap/httpKernel.d.ts +4 -4
- package/dist/bootstrap/public-api.d.ts +2 -1
- package/dist/bootstrap/schemaTarget.d.ts +5 -0
- package/dist/bootstrap/server.d.ts +1 -1
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/bootstrap/web/session.d.ts +18 -3
- package/dist/entries/buildModuleRoutes.js +8 -3
- package/dist/entries/buildWebModuleRoutes.js +8 -3
- package/dist/entries/cache/modelCacheTags.js +6 -1
- package/dist/entries/context.js +52 -166
- package/dist/entries/createRoutes.js +83 -888
- package/dist/entries/createSpaRoutes.js +48 -24
- package/dist/entries/createWebRoutes.js +13 -7
- package/dist/entries/dependencies.js +52 -166
- package/dist/entries/discoverModules.js +6 -1
- package/dist/entries/httpKernel.js +2 -2
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +6 -1
- package/dist/entries/providers/view.js +5 -160
- package/dist/entries/providers.js +52 -166
- package/dist/entries/secretsGuard.js +13 -8
- package/dist/entries/web/routing.js +4 -3
- package/dist/entries/web/session.js +58 -25
- package/dist/index.js +132 -206
- package/package.json +5 -5
- package/dist/bootstrap/scimRoutes.d.ts +0 -24
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/createRoutes.ts
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { isSpaEnabled as isSpaEnabled2, isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
|
|
5
|
+
import { jsonResponse as jsonResponse3 } from "@getstrata/core/http/response";
|
|
6
|
+
import { isSpaEnabled as isSpaEnabled2, isViewsEnabled as isViewsEnabled4 } from "@getstrata/core/runtime/frontendMode";
|
|
8
7
|
import { notFoundHtmlResponse as notFoundHtmlResponse2 } from "@getstrata/core/view";
|
|
9
8
|
|
|
10
9
|
// ../../index.html
|
|
11
|
-
var strata_default = __jsonParse("{\"index\":\"_.._/_.._/index.html\",\"files\":[{\"input\":\"../../index.html\",\"path\":\"./index-sfreg6q3.js\",\"loader\":\"js\",\"isEntry\":true,\"headers\":{\"etag\":\"Oc_YLpCmV6M\",\"content-type\":\"text/javascript;charset=utf-8\"}},{\"input\":\"../../index.html\",\"path\":\"_.._/_.._/index.html\",\"loader\":\"html\",\"isEntry\":true,\"headers\":{\"etag\":\"
|
|
10
|
+
var strata_default = __jsonParse("{\"index\":\"_.._/_.._/index.html\",\"files\":[{\"input\":\"../../index.html\",\"path\":\"./index-sfreg6q3.js\",\"loader\":\"js\",\"isEntry\":true,\"headers\":{\"etag\":\"Oc_YLpCmV6M\",\"content-type\":\"text/javascript;charset=utf-8\"}},{\"input\":\"../../index.html\",\"path\":\"_.._/_.._/index.html\",\"loader\":\"html\",\"isEntry\":true,\"headers\":{\"etag\":\"ojosdcZCk7g\",\"content-type\":\"text/html;charset=utf-8\"}}]}");
|
|
12
11
|
|
|
13
12
|
// ../../src/config/app.ts
|
|
14
13
|
var appConfig = {
|
|
@@ -19,28 +18,6 @@ var appConfig = {
|
|
|
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";
|
|
@@ -68,7 +45,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
68
45
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
69
46
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
70
47
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
71
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
48
|
+
import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
|
|
72
49
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
73
50
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
74
51
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -216,7 +193,7 @@ class HttpKernel {
|
|
|
216
193
|
return withMiddleware(...middleware)(handler);
|
|
217
194
|
}
|
|
218
195
|
wrapApi(handler) {
|
|
219
|
-
return this.wrap(["api", "authenticated"], handler);
|
|
196
|
+
return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
|
|
220
197
|
}
|
|
221
198
|
wrapWeb(handler) {
|
|
222
199
|
return withErrorHandling(this.wrap("web", handler));
|
|
@@ -381,7 +358,12 @@ function readDiscoverModulesState() {
|
|
|
381
358
|
return state;
|
|
382
359
|
}
|
|
383
360
|
function configureModulesDirectory(modulesDir) {
|
|
384
|
-
readDiscoverModulesState()
|
|
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();
|
|
@@ -515,44 +497,68 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
515
497
|
// ../../src/bootstrap/createSpaRoutes.ts
|
|
516
498
|
import { join as join2 } from "path";
|
|
517
499
|
import { jsonResponse } from "@getstrata/core/http/response";
|
|
518
|
-
import { isSpaEnabled } from "@getstrata/core/runtime/frontendMode";
|
|
500
|
+
import { isSpaEnabled, isViewsEnabled as isViewsEnabled2, readSpaPrefix } from "@getstrata/core/runtime/frontendMode";
|
|
519
501
|
var SPA_DIST_DIRECTORY = join2(process.cwd(), "frontend/dist");
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
502
|
+
function relativeSpaPath(pathname, prefix) {
|
|
503
|
+
if (pathname === prefix || pathname === `${prefix}/`) {
|
|
504
|
+
return "";
|
|
505
|
+
}
|
|
506
|
+
if (pathname.startsWith(`${prefix}/`)) {
|
|
507
|
+
return pathname.slice(prefix.length + 1);
|
|
508
|
+
}
|
|
509
|
+
return "";
|
|
510
|
+
}
|
|
511
|
+
function createSpaDocumentHandler(prefix, distDirectory) {
|
|
512
|
+
const indexFilePath = join2(distDirectory, "index.html");
|
|
513
|
+
return async (request) => {
|
|
514
|
+
const pathname = new URL(request.url).pathname;
|
|
515
|
+
if (pathname.startsWith("/api/")) {
|
|
516
|
+
return new Response("Not found", { status: 404 });
|
|
517
|
+
}
|
|
518
|
+
const relativePath = relativeSpaPath(pathname, prefix);
|
|
519
|
+
const assetFile = Bun.file(join2(distDirectory, relativePath));
|
|
520
|
+
if (relativePath.length > 0 && await assetFile.exists()) {
|
|
521
|
+
return new Response(assetFile);
|
|
522
|
+
}
|
|
523
|
+
const indexFile = Bun.file(indexFilePath);
|
|
524
|
+
if (await indexFile.exists()) {
|
|
525
|
+
return new Response(indexFile, {
|
|
526
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return jsonResponse({
|
|
530
|
+
error: "SPA build not found. Run `bun run frontend:build` in your app."
|
|
531
|
+
}, { status: 503 });
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function createSpaRoutes(_dependencies, options = {}) {
|
|
535
|
+
const prefix = options.prefix ?? readSpaPrefix();
|
|
536
|
+
const distDirectory = options.distDirectory ?? SPA_DIST_DIRECTORY;
|
|
537
|
+
const wrap = options.wrap ?? ((handler2) => handler2);
|
|
538
|
+
const handler = wrap(createSpaDocumentHandler(prefix, distDirectory));
|
|
539
|
+
const routes = {
|
|
540
|
+
[prefix]: handler,
|
|
541
|
+
[`${prefix}/`]: handler,
|
|
542
|
+
[`${prefix}/*`]: handler
|
|
541
543
|
};
|
|
544
|
+
if (!isViewsEnabled2()) {
|
|
545
|
+
routes["/"] = async () => Response.redirect(`${prefix}/`, 302);
|
|
546
|
+
}
|
|
547
|
+
return routes;
|
|
542
548
|
}
|
|
543
|
-
function mergeSpaRoutes(dependencies, routes) {
|
|
549
|
+
function mergeSpaRoutes(dependencies, routes, options) {
|
|
544
550
|
if (!isSpaEnabled()) {
|
|
545
551
|
return routes;
|
|
546
552
|
}
|
|
547
553
|
return {
|
|
548
|
-
...createSpaRoutes(dependencies),
|
|
554
|
+
...createSpaRoutes(dependencies, options),
|
|
549
555
|
...routes
|
|
550
556
|
};
|
|
551
557
|
}
|
|
552
558
|
|
|
553
559
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
554
560
|
import { join as join3 } from "path";
|
|
555
|
-
import { isViewsEnabled as
|
|
561
|
+
import { isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
|
|
556
562
|
import { notFoundHtmlResponse } from "@getstrata/core/view";
|
|
557
563
|
|
|
558
564
|
// ../../src/bootstrap/buildWebModuleRoutes.ts
|
|
@@ -582,9 +588,10 @@ function buildWebModuleRoutes(dependencies, options = {}) {
|
|
|
582
588
|
function registerRoute(method, path, middleware) {
|
|
583
589
|
routeRegistry.register({ method, path, middleware });
|
|
584
590
|
}
|
|
585
|
-
function createWebRoutes(dependencies) {
|
|
591
|
+
function createWebRoutes(dependencies, options = {}) {
|
|
586
592
|
const wrappedRoutes = buildWebModuleRoutes(dependencies, {
|
|
587
|
-
clearRegistry: false
|
|
593
|
+
clearRegistry: false,
|
|
594
|
+
modules: options.modules
|
|
588
595
|
});
|
|
589
596
|
registerRoute("GET", "/", ["global", "web"]);
|
|
590
597
|
wrappedRoutes["/assets/*"] = async (request) => {
|
|
@@ -600,16 +607,21 @@ function createWebRoutes(dependencies) {
|
|
|
600
607
|
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
601
608
|
return wrappedRoutes;
|
|
602
609
|
}
|
|
603
|
-
function mergeWebRoutes(dependencies, routes) {
|
|
604
|
-
if (!
|
|
610
|
+
function mergeWebRoutes(dependencies, routes, options = {}) {
|
|
611
|
+
if (!isViewsEnabled3()) {
|
|
605
612
|
return routes;
|
|
606
613
|
}
|
|
607
614
|
return {
|
|
608
|
-
...createWebRoutes(dependencies),
|
|
615
|
+
...createWebRoutes(dependencies, options),
|
|
609
616
|
...routes
|
|
610
617
|
};
|
|
611
618
|
}
|
|
612
619
|
|
|
620
|
+
// ../../src/bootstrap/dogfoodApp.ts
|
|
621
|
+
function isHiroappDogfood() {
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
|
|
613
625
|
// ../../src/bootstrap/health.ts
|
|
614
626
|
import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
|
|
615
627
|
import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
|
|
@@ -745,858 +757,41 @@ function createMetricsRoutes() {
|
|
|
745
757
|
};
|
|
746
758
|
}
|
|
747
759
|
|
|
748
|
-
// ../../src/bootstrap/scimRoutes.ts
|
|
749
|
-
import { createScimAuthMiddleware } from "@getstrata/core/auth/scimAuthMiddleware";
|
|
750
|
-
import { withMiddleware as withMiddleware2 } from "@getstrata/core/http/routeMiddleware";
|
|
751
|
-
import { createScimThrottleMiddleware } from "@getstrata/core/http/scimThrottleMiddleware";
|
|
752
|
-
|
|
753
|
-
// ../../src/modules/scim/controller.ts
|
|
754
|
-
import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
|
|
755
|
-
|
|
756
|
-
// ../../src/modules/scim/scimResponse.ts
|
|
757
|
-
import {
|
|
758
|
-
applyConditionalGet,
|
|
759
|
-
assertIfMatch,
|
|
760
|
-
etagFromResource,
|
|
761
|
-
isEtagEnabled
|
|
762
|
-
} from "@getstrata/core/http/etag";
|
|
763
|
-
import { jsonResponse as jsonResponse3 } from "@getstrata/core/http/response";
|
|
764
|
-
function scimResponse(data, options = {}) {
|
|
765
|
-
const status = options.status ?? 200;
|
|
766
|
-
const response = jsonResponse3(data, {
|
|
767
|
-
status,
|
|
768
|
-
headers: {
|
|
769
|
-
"content-type": "application/scim+json"
|
|
770
|
-
}
|
|
771
|
-
});
|
|
772
|
-
if (!isEtagEnabled() || !options.etagSource) {
|
|
773
|
-
return response;
|
|
774
|
-
}
|
|
775
|
-
const etag = etagFromResource(options.etagSource);
|
|
776
|
-
if (options.request) {
|
|
777
|
-
return applyConditionalGet(options.request, response, etag);
|
|
778
|
-
}
|
|
779
|
-
const headers = new Headers(response.headers);
|
|
780
|
-
headers.set("ETag", etag);
|
|
781
|
-
headers.set("Cache-Control", "private, must-revalidate");
|
|
782
|
-
return new Response(response.body, {
|
|
783
|
-
status: response.status,
|
|
784
|
-
statusText: response.statusText,
|
|
785
|
-
headers
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
function assertScimIfMatch(request, etagSource) {
|
|
789
|
-
if (!isEtagEnabled()) {
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
assertIfMatch(request, etagFromResource(etagSource), { required: true });
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// ../../src/modules/scim/service.ts
|
|
796
|
-
import { hashPassword as hashPassword3 } from "@getstrata/core/auth/password";
|
|
797
|
-
import { resolveService } from "@getstrata/core/contracts/di";
|
|
798
|
-
import { NotFoundError as NotFoundError7 } from "@getstrata/core/errors/http";
|
|
799
|
-
import { currentTenantId as currentTenantId4 } from "@getstrata/core/tenant/tenantContext";
|
|
800
|
-
|
|
801
|
-
// ../../src/domain/scim.ts
|
|
802
|
-
var SCIM_SCHEMAS = {
|
|
803
|
-
user: "urn:ietf:params:scim:schemas:core:2.0:User",
|
|
804
|
-
group: "urn:ietf:params:scim:schemas:core:2.0:Group",
|
|
805
|
-
listResponse: "urn:ietf:params:scim:api:messages:2.0:ListResponse",
|
|
806
|
-
patchOp: "urn:ietf:params:scim:api:messages:2.0:PatchOp",
|
|
807
|
-
serviceProviderConfig: "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
// ../../src/modules/organization/memberRepository.ts
|
|
811
|
-
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
812
|
-
|
|
813
|
-
// ../../src/config/database.ts
|
|
814
|
-
function readInteger(name, fallback) {
|
|
815
|
-
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
816
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
817
|
-
}
|
|
818
|
-
var databaseConfig = {
|
|
819
|
-
url: process.env.DATABASE_URL ?? "",
|
|
820
|
-
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
821
|
-
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
822
|
-
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
823
|
-
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
// ../../src/core/runtime/asyncContextStore.ts
|
|
827
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
828
|
-
function createAsyncContextStore(key) {
|
|
829
|
-
const symbol = Symbol.for(key);
|
|
830
|
-
const globalRecord = globalThis;
|
|
831
|
-
const existing = globalRecord[symbol];
|
|
832
|
-
if (existing) {
|
|
833
|
-
return existing;
|
|
834
|
-
}
|
|
835
|
-
const store = new AsyncLocalStorage;
|
|
836
|
-
globalRecord[symbol] = store;
|
|
837
|
-
return store;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// ../../src/core/database/connectionContext.ts
|
|
841
|
-
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
842
|
-
function getActiveDatabaseConnection(fallback) {
|
|
843
|
-
return activeConnection.getStore() ?? fallback;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// ../../src/core/database/queryProxy.ts
|
|
847
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
848
|
-
function createDatabaseQueryProxy(pool) {
|
|
849
|
-
function resolveDatabase() {
|
|
850
|
-
return getActiveDatabaseConnection(pool);
|
|
851
|
-
}
|
|
852
|
-
function resolveDatabaseForProperty(property) {
|
|
853
|
-
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
854
|
-
return pool;
|
|
855
|
-
}
|
|
856
|
-
return resolveDatabase();
|
|
857
|
-
}
|
|
858
|
-
return new Proxy(function database() {}, {
|
|
859
|
-
apply(_target, _thisArg, args) {
|
|
860
|
-
return resolveDatabase()(...args);
|
|
861
|
-
},
|
|
862
|
-
get(_target, property) {
|
|
863
|
-
const connection = resolveDatabaseForProperty(property);
|
|
864
|
-
const value = connection[property];
|
|
865
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// ../../src/core/database/defaultConnection.ts
|
|
871
|
-
var defaultPool = {
|
|
872
|
-
connection: null
|
|
873
|
-
};
|
|
874
|
-
var defaultQuery = {
|
|
875
|
-
connection: null
|
|
876
|
-
};
|
|
877
|
-
function registerDefaultDatabasePool(connection) {
|
|
878
|
-
defaultPool.connection = connection;
|
|
879
|
-
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
880
|
-
}
|
|
881
|
-
function getDefaultDatabaseQuery() {
|
|
882
|
-
if (!defaultQuery.connection) {
|
|
883
|
-
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
884
|
-
}
|
|
885
|
-
return defaultQuery.connection;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// ../../src/db/connection/createConnection.ts
|
|
889
|
-
var {SQL } = globalThis.Bun;
|
|
890
|
-
function createDatabaseConnection(config) {
|
|
891
|
-
if (!config.url) {
|
|
892
|
-
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
893
|
-
}
|
|
894
|
-
return new SQL({
|
|
895
|
-
url: config.url,
|
|
896
|
-
max: config.poolMax,
|
|
897
|
-
idleTimeout: config.idleTimeoutSeconds,
|
|
898
|
-
maxLifetime: config.maxLifetimeSeconds,
|
|
899
|
-
connectionTimeout: config.connectionTimeoutSeconds
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// ../../src/db/connection/index.ts
|
|
904
|
-
var connectionHolder = {
|
|
905
|
-
connection: null
|
|
906
|
-
};
|
|
907
|
-
function getDatabase() {
|
|
908
|
-
if (!connectionHolder.connection) {
|
|
909
|
-
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
910
|
-
registerDefaultDatabasePool(connectionHolder.connection);
|
|
911
|
-
}
|
|
912
|
-
return connectionHolder.connection;
|
|
913
|
-
}
|
|
914
|
-
function getDb() {
|
|
915
|
-
getDatabase();
|
|
916
|
-
return getDefaultDatabaseQuery();
|
|
917
|
-
}
|
|
918
|
-
var db = new Proxy(function database() {}, {
|
|
919
|
-
apply(_target, _thisArg, args) {
|
|
920
|
-
return getDb()(...args);
|
|
921
|
-
},
|
|
922
|
-
get(_target, property) {
|
|
923
|
-
const connection = getDb();
|
|
924
|
-
const value = connection[property];
|
|
925
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
926
|
-
}
|
|
927
|
-
});
|
|
928
|
-
var connection_default = db;
|
|
929
|
-
|
|
930
|
-
// ../../src/modules/organization/memberRepository.ts
|
|
931
|
-
class OrganizationMemberRepository {
|
|
932
|
-
constructor() {}
|
|
933
|
-
async findMembership(userId, organizationId) {
|
|
934
|
-
const rows = await connection_default`
|
|
935
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
936
|
-
FROM organization_member
|
|
937
|
-
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
938
|
-
LIMIT 1
|
|
939
|
-
`;
|
|
940
|
-
return rows[0] ?? null;
|
|
941
|
-
}
|
|
942
|
-
async listForUser(userId) {
|
|
943
|
-
return await connection_default`
|
|
944
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
945
|
-
FROM organization_member
|
|
946
|
-
WHERE user_id = ${userId}
|
|
947
|
-
ORDER BY organization_id
|
|
948
|
-
`;
|
|
949
|
-
}
|
|
950
|
-
async listForOrganization(organizationId) {
|
|
951
|
-
return await connection_default`
|
|
952
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
953
|
-
FROM organization_member
|
|
954
|
-
WHERE organization_id = ${organizationId}
|
|
955
|
-
ORDER BY id
|
|
956
|
-
`;
|
|
957
|
-
}
|
|
958
|
-
async addMember(input) {
|
|
959
|
-
const rows = await connection_default`
|
|
960
|
-
INSERT INTO organization_member (organization_id, user_id, role)
|
|
961
|
-
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
962
|
-
RETURNING id, organization_id, user_id, role, created_at
|
|
963
|
-
`;
|
|
964
|
-
const row = rows[0];
|
|
965
|
-
if (!row) {
|
|
966
|
-
throw new Error("Organization member insert did not return a row.");
|
|
967
|
-
}
|
|
968
|
-
return row;
|
|
969
|
-
}
|
|
970
|
-
async updateMemberRole(organizationId, userId, role) {
|
|
971
|
-
const rows = await connection_default`
|
|
972
|
-
UPDATE organization_member
|
|
973
|
-
SET role = ${role}
|
|
974
|
-
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
975
|
-
RETURNING id, organization_id, user_id, role, created_at
|
|
976
|
-
`;
|
|
977
|
-
const row = rows[0];
|
|
978
|
-
if (!row) {
|
|
979
|
-
throw new NotFoundError(`Organization member ${userId} not found.`);
|
|
980
|
-
}
|
|
981
|
-
return row;
|
|
982
|
-
}
|
|
983
|
-
async removeMember(organizationId, userId) {
|
|
984
|
-
const rows = await connection_default`
|
|
985
|
-
DELETE FROM organization_member
|
|
986
|
-
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
987
|
-
RETURNING id
|
|
988
|
-
`;
|
|
989
|
-
return rows.length > 0;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
var memberRepository_default = OrganizationMemberRepository;
|
|
993
|
-
|
|
994
|
-
// ../../src/modules/organization/repository.ts
|
|
995
|
-
import { BaseRepository } from "@getstrata/core/database/baseRepository";
|
|
996
|
-
import { NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
|
|
997
|
-
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
998
|
-
|
|
999
|
-
// ../../src/modules/organization/table.ts
|
|
1000
|
-
import { defineTable } from "@getstrata/core/database/table";
|
|
1001
|
-
|
|
1002
|
-
// ../../src/domain/workhub.ts
|
|
1003
|
-
var ORGANIZATION_TABLE = "organization";
|
|
1004
|
-
|
|
1005
|
-
// ../../src/modules/organization/table.ts
|
|
1006
|
-
var organizationTable = defineTable({
|
|
1007
|
-
name: ORGANIZATION_TABLE,
|
|
1008
|
-
primaryKey: "id",
|
|
1009
|
-
columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
|
|
1010
|
-
softDeletes: true,
|
|
1011
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
// ../../src/modules/organization/repository.ts
|
|
1015
|
-
class OrganizationRepository extends BaseRepository {
|
|
1016
|
-
constructor() {
|
|
1017
|
-
super(organizationTable);
|
|
1018
|
-
}
|
|
1019
|
-
async findBySlug(slug) {
|
|
1020
|
-
return await this.firstOrNull({ slug });
|
|
1021
|
-
}
|
|
1022
|
-
async listForTenant(options) {
|
|
1023
|
-
return await this.findAll({
|
|
1024
|
-
limit: options.limit,
|
|
1025
|
-
offset: options.offset,
|
|
1026
|
-
where: { tenant_id: options.tenantId ?? currentTenantId() }
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
async countForTenant(tenantId = currentTenantId()) {
|
|
1030
|
-
return await this.countWhere({ tenant_id: tenantId });
|
|
1031
|
-
}
|
|
1032
|
-
async findForTenantOrThrow(id, tenantId = currentTenantId()) {
|
|
1033
|
-
const organization = await this.findById(id);
|
|
1034
|
-
if (!organization || organization.tenant_id !== tenantId) {
|
|
1035
|
-
throw new NotFoundError2(`SCIM group ${id} not found.`);
|
|
1036
|
-
}
|
|
1037
|
-
return organization;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
var repository_default = OrganizationRepository;
|
|
1041
|
-
|
|
1042
|
-
// ../../src/modules/user/provider.ts
|
|
1043
|
-
import {
|
|
1044
|
-
CORE_ABILITY_CHECKER_TOKEN as CORE_ABILITY_CHECKER_TOKEN2,
|
|
1045
|
-
CORE_AUTH_USER_DIRECTORY_TOKEN as CORE_AUTH_USER_DIRECTORY_TOKEN2,
|
|
1046
|
-
CORE_TOKEN_SERVICE_TOKEN as CORE_TOKEN_SERVICE_TOKEN2
|
|
1047
|
-
} from "@getstrata/bootstrap/config";
|
|
1048
|
-
import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
|
|
1049
|
-
import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
|
|
1050
|
-
import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
|
|
1051
|
-
import { getRequiredDependency } from "@getstrata/core/contracts/di";
|
|
1052
|
-
|
|
1053
|
-
// ../../src/modules/user/apiTokenRepository.ts
|
|
1054
|
-
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
|
|
1055
|
-
|
|
1056
|
-
// ../../src/modules/user/apiTokenTable.ts
|
|
1057
|
-
import { defineTable as defineTable2 } from "@getstrata/core/database/table";
|
|
1058
|
-
var apiTokenTable = defineTable2({
|
|
1059
|
-
name: "api_token",
|
|
1060
|
-
primaryKey: "id",
|
|
1061
|
-
columns: [
|
|
1062
|
-
"id",
|
|
1063
|
-
"user_id",
|
|
1064
|
-
"name",
|
|
1065
|
-
"token_hash",
|
|
1066
|
-
"abilities",
|
|
1067
|
-
"last_used_at",
|
|
1068
|
-
"expires_at",
|
|
1069
|
-
"created_at"
|
|
1070
|
-
],
|
|
1071
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
// ../../src/modules/user/authService.ts
|
|
1075
|
-
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
1076
|
-
import { hashPassword, verifyPassword } from "@getstrata/core/auth/password";
|
|
1077
|
-
import { normalizeEmail } from "@getstrata/core/crypto/fieldEncryption";
|
|
1078
|
-
import { protectMfaSecret, revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
1079
|
-
import { UnauthorizedError as UnauthorizedError2, ValidationError } from "@getstrata/core/errors/http";
|
|
1080
|
-
import {
|
|
1081
|
-
generateRecoveryCodes,
|
|
1082
|
-
hashRecoveryCode,
|
|
1083
|
-
recoveryCodeMatches
|
|
1084
|
-
} from "@getstrata/core/security/recoveryCodes";
|
|
1085
|
-
import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
|
|
1086
|
-
import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
|
|
1087
|
-
import { buildOtpauthUrl, generateTotpSecret, verifyTotp } from "@getstrata/core/security/totp";
|
|
1088
|
-
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
1089
|
-
|
|
1090
|
-
// ../../src/core/auth/abilityCatalog.ts
|
|
1091
|
-
var MEMBER_ABILITIES = [
|
|
1092
|
-
"organizations:read",
|
|
1093
|
-
"organizations:create",
|
|
1094
|
-
"projects:read",
|
|
1095
|
-
"projects:create",
|
|
1096
|
-
"tasks:read",
|
|
1097
|
-
"tasks:create",
|
|
1098
|
-
"comments:read",
|
|
1099
|
-
"comments:create",
|
|
1100
|
-
"attachments:read",
|
|
1101
|
-
"attachments:create",
|
|
1102
|
-
"auth:tokens:read",
|
|
1103
|
-
"auth:tokens:write",
|
|
1104
|
-
"auth:tokens:delete"
|
|
1105
|
-
];
|
|
1106
|
-
var ADMIN_ABILITIES = [
|
|
1107
|
-
...MEMBER_ABILITIES,
|
|
1108
|
-
"organizations:create",
|
|
1109
|
-
"organizations:update",
|
|
1110
|
-
"organizations:delete",
|
|
1111
|
-
"projects:update",
|
|
1112
|
-
"projects:delete",
|
|
1113
|
-
"tasks:update",
|
|
1114
|
-
"tasks:delete",
|
|
1115
|
-
"comments:update",
|
|
1116
|
-
"comments:delete",
|
|
1117
|
-
"attachments:delete",
|
|
1118
|
-
"webhooks:read",
|
|
1119
|
-
"webhooks:write",
|
|
1120
|
-
"audit:read"
|
|
1121
|
-
];
|
|
1122
|
-
// ../../src/modules/user/browserSessions.ts
|
|
1123
|
-
import {
|
|
1124
|
-
createSessionCookieDetails,
|
|
1125
|
-
readSession
|
|
1126
|
-
} from "@getstrata/core/auth/sessionCookie";
|
|
1127
|
-
import { repositoryConnection as db2 } from "@getstrata/core/database/repositoryConnection";
|
|
1128
|
-
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
1129
|
-
|
|
1130
|
-
// ../../src/modules/user/mfaRequiredError.ts
|
|
1131
|
-
import { UnauthorizedError } from "@getstrata/core/errors/http";
|
|
1132
|
-
|
|
1133
|
-
// ../../src/modules/user/currentOrganizationService.ts
|
|
1134
|
-
import { assertResourceInCurrentTenant } from "@getstrata/core/auth/membershipScope";
|
|
1135
|
-
import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
|
|
1136
|
-
import { NotFoundError as NotFoundError3 } from "@getstrata/core/errors/http";
|
|
1137
|
-
|
|
1138
|
-
// ../../src/modules/user/notificationRepository.ts
|
|
1139
|
-
import { BaseRepository as BaseRepository3 } from "@getstrata/core/database/baseRepository";
|
|
1140
|
-
|
|
1141
|
-
// ../../src/modules/user/notificationTable.ts
|
|
1142
|
-
import { defineTable as defineTable3 } from "@getstrata/core/database/table";
|
|
1143
|
-
var notificationTable = defineTable3({
|
|
1144
|
-
name: "notification",
|
|
1145
|
-
primaryKey: "id",
|
|
1146
|
-
columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
|
|
1147
|
-
defaultOrderBy: { column: "created_at", direction: "DESC" }
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
// ../../src/modules/user/notificationService.ts
|
|
1151
|
-
import { NotFoundError as NotFoundError4 } from "@getstrata/core/errors/http";
|
|
1152
|
-
|
|
1153
|
-
// ../../src/modules/user/oauthIdentityRepository.ts
|
|
1154
|
-
import { BaseRepository as BaseRepository4 } from "@getstrata/core/database/baseRepository";
|
|
1155
|
-
import { defineTable as defineTable4 } from "@getstrata/core/database/table";
|
|
1156
|
-
var oauthIdentityTable = defineTable4({
|
|
1157
|
-
name: "oauth_identity",
|
|
1158
|
-
primaryKey: "id",
|
|
1159
|
-
columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
// ../../src/modules/user/passwordResetService.ts
|
|
1163
|
-
import { hashPassword as hashPassword2 } from "@getstrata/core/auth/password";
|
|
1164
|
-
import { hashApiToken } from "@getstrata/core/auth/tokenHash";
|
|
1165
|
-
import { repositoryConnection as db3 } from "@getstrata/core/database/repositoryConnection";
|
|
1166
|
-
import { ValidationError as ValidationError2 } from "@getstrata/core/errors/http";
|
|
1167
|
-
import { absoluteTemporarySignedUrl } from "@getstrata/core/http/signedUrl";
|
|
1168
|
-
import { mailer } from "@getstrata/core/mail/mailer";
|
|
1169
|
-
import { sendMarkdownMail } from "@getstrata/core/mail/markdownMailable";
|
|
1170
|
-
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
1171
|
-
import { logSecurityEvent as logSecurityEvent2 } from "@getstrata/core/security/securityEvents";
|
|
1172
|
-
import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
|
|
1173
|
-
var RESET_TTL_SECONDS = 60 * 60;
|
|
1174
|
-
var VERIFY_TTL_SECONDS = 60 * 60 * 24;
|
|
1175
|
-
|
|
1176
|
-
// ../../src/modules/user/profilePhotoService.ts
|
|
1177
|
-
import { BadRequestError as BadRequestError2, NotFoundError as NotFoundError5 } from "@getstrata/core/errors/http";
|
|
1178
|
-
import { isImageMimeType, resizeImageContents } from "@getstrata/core/media/imageTransform";
|
|
1179
|
-
|
|
1180
|
-
// ../../src/modules/user/repository.ts
|
|
1181
|
-
import {
|
|
1182
|
-
emailLookupForQuery,
|
|
1183
|
-
protectEmail,
|
|
1184
|
-
revealEmail
|
|
1185
|
-
} from "@getstrata/core/crypto/fieldEncryption";
|
|
1186
|
-
import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
|
|
1187
|
-
import { BaseRepository as BaseRepository5 } from "@getstrata/core/database/baseRepository";
|
|
1188
|
-
import { currentTenantId as currentTenantId3 } from "@getstrata/core/tenant/tenantContext";
|
|
1189
|
-
|
|
1190
|
-
// ../../src/modules/user/table.ts
|
|
1191
|
-
import { defineTable as defineTable5 } from "@getstrata/core/database/table";
|
|
1192
|
-
var userTable = defineTable5({
|
|
1193
|
-
name: "users",
|
|
1194
|
-
primaryKey: "id",
|
|
1195
|
-
columns: [
|
|
1196
|
-
"id",
|
|
1197
|
-
"name",
|
|
1198
|
-
"email",
|
|
1199
|
-
"email_lookup",
|
|
1200
|
-
"role",
|
|
1201
|
-
"tenant_id",
|
|
1202
|
-
"password_hash",
|
|
1203
|
-
"email_verified_at",
|
|
1204
|
-
"mfa_secret",
|
|
1205
|
-
"mfa_enabled",
|
|
1206
|
-
"mfa_recovery_codes",
|
|
1207
|
-
"profile_photo_path",
|
|
1208
|
-
"session_valid_after",
|
|
1209
|
-
"current_organization_id",
|
|
1210
|
-
"created_at",
|
|
1211
|
-
"updated_at"
|
|
1212
|
-
],
|
|
1213
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
|
-
// ../../src/modules/user/repository.ts
|
|
1217
|
-
class UserRepository extends BaseRepository5 {
|
|
1218
|
-
constructor() {
|
|
1219
|
-
super(userTable);
|
|
1220
|
-
}
|
|
1221
|
-
decode(record) {
|
|
1222
|
-
return {
|
|
1223
|
-
...record,
|
|
1224
|
-
email: revealEmail(record.email),
|
|
1225
|
-
mfa_secret: revealMfaSecret2(record.mfa_secret)
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
async findById(id) {
|
|
1229
|
-
const record = await super.findById(id);
|
|
1230
|
-
return record ? this.decode(record) : null;
|
|
1231
|
-
}
|
|
1232
|
-
async findAll(options = {}) {
|
|
1233
|
-
const records = await super.findAll(options);
|
|
1234
|
-
return records.map((record) => this.decode(record));
|
|
1235
|
-
}
|
|
1236
|
-
async create(values) {
|
|
1237
|
-
const email = values.email;
|
|
1238
|
-
if (!email) {
|
|
1239
|
-
throw new Error("Email is required.");
|
|
1240
|
-
}
|
|
1241
|
-
const protectedEmail = protectEmail(email);
|
|
1242
|
-
const record = await super.create({
|
|
1243
|
-
...values,
|
|
1244
|
-
tenant_id: values.tenant_id ?? currentTenantId3(),
|
|
1245
|
-
email: protectedEmail.storedEmail,
|
|
1246
|
-
email_lookup: protectedEmail.emailLookup,
|
|
1247
|
-
password_hash: values.password_hash ?? ""
|
|
1248
|
-
});
|
|
1249
|
-
return this.decode(record);
|
|
1250
|
-
}
|
|
1251
|
-
async updateByIdOrThrow(id, values, errorFactory) {
|
|
1252
|
-
const changes = { ...values };
|
|
1253
|
-
if (values.email !== undefined) {
|
|
1254
|
-
const protectedEmail = protectEmail(values.email);
|
|
1255
|
-
changes.email = protectedEmail.storedEmail;
|
|
1256
|
-
changes.email_lookup = protectedEmail.emailLookup;
|
|
1257
|
-
}
|
|
1258
|
-
const record = await super.updateByIdOrThrow(id, changes, errorFactory);
|
|
1259
|
-
return this.decode(record);
|
|
1260
|
-
}
|
|
1261
|
-
async findByEmail(email) {
|
|
1262
|
-
const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
|
|
1263
|
-
const record = records[0];
|
|
1264
|
-
return record ? this.decode(record) : null;
|
|
1265
|
-
}
|
|
1266
|
-
async countForTenant(tenantId = currentTenantId3()) {
|
|
1267
|
-
return await this.countWhere({ tenant_id: tenantId });
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
var repository_default2 = UserRepository;
|
|
1271
|
-
|
|
1272
|
-
// ../../src/modules/user/tokenService.ts
|
|
1273
|
-
import { ADMIN_ABILITIES as ADMIN_ABILITIES2 } from "@getstrata/core/auth/abilityCatalog";
|
|
1274
|
-
import { hashApiToken as hashApiToken2 } from "@getstrata/core/auth/tokenHash";
|
|
1275
|
-
import { ForbiddenError, NotFoundError as NotFoundError6, ValidationError as ValidationError3 } from "@getstrata/core/errors/http";
|
|
1276
|
-
import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
|
|
1277
|
-
|
|
1278
|
-
// ../../src/modules/user/provider.ts
|
|
1279
|
-
var userRepositoryToken = "user.repository";
|
|
1280
|
-
|
|
1281
|
-
// ../../src/modules/scim/service.ts
|
|
1282
|
-
class ScimService {
|
|
1283
|
-
users;
|
|
1284
|
-
organizations;
|
|
1285
|
-
members;
|
|
1286
|
-
constructor(users, organizations, members) {
|
|
1287
|
-
this.users = users;
|
|
1288
|
-
this.organizations = organizations;
|
|
1289
|
-
this.members = members;
|
|
1290
|
-
}
|
|
1291
|
-
serviceProviderConfig() {
|
|
1292
|
-
return {
|
|
1293
|
-
schemas: [SCIM_SCHEMAS.serviceProviderConfig],
|
|
1294
|
-
patch: { supported: true },
|
|
1295
|
-
bulk: { supported: false },
|
|
1296
|
-
filter: { supported: false },
|
|
1297
|
-
changePassword: { supported: false },
|
|
1298
|
-
sort: { supported: false },
|
|
1299
|
-
etag: { supported: true },
|
|
1300
|
-
authenticationSchemes: [
|
|
1301
|
-
{
|
|
1302
|
-
type: "oauthbearertoken",
|
|
1303
|
-
name: "OAuth Bearer Token",
|
|
1304
|
-
description: "SCIM bearer token authentication"
|
|
1305
|
-
}
|
|
1306
|
-
]
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
async listUsers(startIndex = 1, count = 100) {
|
|
1310
|
-
const tenantId = currentTenantId4();
|
|
1311
|
-
const offset = Math.max(startIndex - 1, 0);
|
|
1312
|
-
const records = await this.users.findAll({
|
|
1313
|
-
limit: count,
|
|
1314
|
-
offset,
|
|
1315
|
-
where: { tenant_id: tenantId }
|
|
1316
|
-
});
|
|
1317
|
-
const total = await this.users.countForTenant(tenantId);
|
|
1318
|
-
return {
|
|
1319
|
-
schemas: [SCIM_SCHEMAS.listResponse],
|
|
1320
|
-
totalResults: total,
|
|
1321
|
-
startIndex,
|
|
1322
|
-
itemsPerPage: records.length,
|
|
1323
|
-
Resources: records.map((user) => this.toScimUser(user))
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
async getUser(id) {
|
|
1327
|
-
const user = await this.findUserRecord(id);
|
|
1328
|
-
return this.toScimUser(user);
|
|
1329
|
-
}
|
|
1330
|
-
async findUserRecord(id) {
|
|
1331
|
-
const user = await this.users.findById(id);
|
|
1332
|
-
if (!user || user.tenant_id !== currentTenantId4()) {
|
|
1333
|
-
throw new NotFoundError7(`SCIM user ${id} not found.`);
|
|
1334
|
-
}
|
|
1335
|
-
return user;
|
|
1336
|
-
}
|
|
1337
|
-
async createUser(payload) {
|
|
1338
|
-
const email = payload.userName ?? payload.emails?.find((entry) => entry.primary)?.value ?? payload.emails?.[0]?.value;
|
|
1339
|
-
if (!email) {
|
|
1340
|
-
throw new Error("SCIM userName or email is required.");
|
|
1341
|
-
}
|
|
1342
|
-
const passwordHash = await hashPassword3(crypto.randomUUID());
|
|
1343
|
-
const user = await this.users.create({
|
|
1344
|
-
name: payload.name?.formatted ?? email.split("@")[0] ?? "SCIM User",
|
|
1345
|
-
email,
|
|
1346
|
-
role: "member",
|
|
1347
|
-
tenant_id: currentTenantId4(),
|
|
1348
|
-
password_hash: passwordHash,
|
|
1349
|
-
created_at: new Date,
|
|
1350
|
-
updated_at: new Date
|
|
1351
|
-
});
|
|
1352
|
-
return this.toScimUser(user);
|
|
1353
|
-
}
|
|
1354
|
-
async patchUser(id, operations) {
|
|
1355
|
-
const user = await this.findUserRecord(id);
|
|
1356
|
-
const changes = { updated_at: new Date };
|
|
1357
|
-
for (const operation of operations) {
|
|
1358
|
-
if (operation.op.toLowerCase() === "replace" && operation.path === "active") {
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
if (operation.op.toLowerCase() === "replace" && operation.path === "displayName") {
|
|
1362
|
-
changes.name = String(operation.value ?? user.name);
|
|
1363
|
-
}
|
|
1364
|
-
if (operation.op.toLowerCase() === "replace" && (operation.path === "userName" || operation.path === 'emails[type eq "work"].value')) {
|
|
1365
|
-
changes.email = String(operation.value ?? user.email);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
const updated = await this.users.updateByIdOrThrow(id, changes);
|
|
1369
|
-
return this.toScimUser(updated);
|
|
1370
|
-
}
|
|
1371
|
-
async deleteUser(id) {
|
|
1372
|
-
const user = await this.findUserRecord(id);
|
|
1373
|
-
const deleted = await this.users.deleteById(id);
|
|
1374
|
-
if (!deleted) {
|
|
1375
|
-
throw new NotFoundError7(`SCIM user ${id} not found.`);
|
|
1376
|
-
}
|
|
1377
|
-
return { id: user.id, updated_at: user.updated_at };
|
|
1378
|
-
}
|
|
1379
|
-
async listGroups(startIndex = 1, count = 100) {
|
|
1380
|
-
const tenantId = currentTenantId4();
|
|
1381
|
-
const offset = Math.max(startIndex - 1, 0);
|
|
1382
|
-
const rows = await this.organizations.listForTenant({ limit: count, offset, tenantId });
|
|
1383
|
-
const total = await this.organizations.countForTenant(tenantId);
|
|
1384
|
-
const resources = await Promise.all(rows.map((row) => this.toScimGroup(row)));
|
|
1385
|
-
return {
|
|
1386
|
-
schemas: [SCIM_SCHEMAS.listResponse],
|
|
1387
|
-
totalResults: total,
|
|
1388
|
-
startIndex,
|
|
1389
|
-
itemsPerPage: resources.length,
|
|
1390
|
-
Resources: resources
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
async getGroup(id) {
|
|
1394
|
-
const organization = await this.findOrganizationRecord(id);
|
|
1395
|
-
return await this.toScimGroup(organization);
|
|
1396
|
-
}
|
|
1397
|
-
async findOrganizationRecord(id) {
|
|
1398
|
-
return await this.organizations.findForTenantOrThrow(id, currentTenantId4());
|
|
1399
|
-
}
|
|
1400
|
-
async patchGroup(id, operations) {
|
|
1401
|
-
for (const operation of operations) {
|
|
1402
|
-
if (operation.op.toLowerCase() !== "add" || operation.path !== "members") {
|
|
1403
|
-
continue;
|
|
1404
|
-
}
|
|
1405
|
-
const members = Array.isArray(operation.value) ? operation.value : operation.value ? [operation.value] : [];
|
|
1406
|
-
for (const member of members) {
|
|
1407
|
-
const userId = Number.parseInt(String(member.value ?? ""), 10);
|
|
1408
|
-
if (Number.isInteger(userId) && userId > 0) {
|
|
1409
|
-
await this.members.addMember({ organizationId: id, userId, role: "member" });
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
const organization = await this.findOrganizationRecord(id);
|
|
1414
|
-
return await this.toScimGroup(organization);
|
|
1415
|
-
}
|
|
1416
|
-
async toScimGroup(organization) {
|
|
1417
|
-
const members = await this.members.listForOrganization(organization.id);
|
|
1418
|
-
return {
|
|
1419
|
-
schemas: [SCIM_SCHEMAS.group],
|
|
1420
|
-
id: String(organization.id),
|
|
1421
|
-
displayName: organization.name,
|
|
1422
|
-
externalId: organization.slug,
|
|
1423
|
-
members: members.map((member) => ({
|
|
1424
|
-
value: String(member.user_id),
|
|
1425
|
-
display: String(member.user_id)
|
|
1426
|
-
})),
|
|
1427
|
-
meta: {
|
|
1428
|
-
resourceType: "Group",
|
|
1429
|
-
lastModified: organization.updated_at.toISOString()
|
|
1430
|
-
}
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
toScimUser(user) {
|
|
1434
|
-
return {
|
|
1435
|
-
schemas: [SCIM_SCHEMAS.user],
|
|
1436
|
-
id: String(user.id),
|
|
1437
|
-
userName: user.email,
|
|
1438
|
-
name: { formatted: user.name },
|
|
1439
|
-
displayName: user.name,
|
|
1440
|
-
active: true,
|
|
1441
|
-
emails: [{ value: user.email, primary: true, type: "work" }],
|
|
1442
|
-
roles: [{ value: user.role, primary: true }],
|
|
1443
|
-
meta: {
|
|
1444
|
-
resourceType: "User",
|
|
1445
|
-
created: user.created_at?.toISOString(),
|
|
1446
|
-
lastModified: user.updated_at?.toISOString()
|
|
1447
|
-
}
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
function createScimService(dependencies) {
|
|
1452
|
-
return new ScimService(resolveService(dependencies, userRepositoryToken), new repository_default, new memberRepository_default);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// ../../src/modules/scim/controller.ts
|
|
1456
|
-
class ScimController {
|
|
1457
|
-
service;
|
|
1458
|
-
constructor(dependencies, service = createScimService(dependencies)) {
|
|
1459
|
-
this.service = service;
|
|
1460
|
-
}
|
|
1461
|
-
serviceProviderConfig = withErrorHandling2(async () => {
|
|
1462
|
-
return scimResponse(this.service.serviceProviderConfig());
|
|
1463
|
-
});
|
|
1464
|
-
listUsers = withErrorHandling2(async (request) => {
|
|
1465
|
-
const url = new URL(request.url);
|
|
1466
|
-
const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
|
|
1467
|
-
const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
|
|
1468
|
-
return scimResponse(await this.service.listUsers(startIndex, count));
|
|
1469
|
-
});
|
|
1470
|
-
createUser = withErrorHandling2(async (request) => {
|
|
1471
|
-
const payload = await request.json();
|
|
1472
|
-
const user = await this.service.createUser(payload);
|
|
1473
|
-
const id = Number.parseInt(user.id, 10);
|
|
1474
|
-
const record = await this.service.findUserRecord(id);
|
|
1475
|
-
return scimResponse(user, { status: 201, etagSource: record });
|
|
1476
|
-
});
|
|
1477
|
-
showUser = withErrorHandling2(async (request) => {
|
|
1478
|
-
const params = request.params;
|
|
1479
|
-
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1480
|
-
const record = await this.service.findUserRecord(id);
|
|
1481
|
-
const user = await this.service.getUser(id);
|
|
1482
|
-
return scimResponse(user, { request, etagSource: record });
|
|
1483
|
-
});
|
|
1484
|
-
patchUser = withErrorHandling2(async (request) => {
|
|
1485
|
-
const params = request.params;
|
|
1486
|
-
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1487
|
-
const record = await this.service.findUserRecord(id);
|
|
1488
|
-
assertScimIfMatch(request, record);
|
|
1489
|
-
const body = await request.json();
|
|
1490
|
-
const user = await this.service.patchUser(id, body.Operations ?? []);
|
|
1491
|
-
const updated = await this.service.findUserRecord(id);
|
|
1492
|
-
return scimResponse(user, { etagSource: updated });
|
|
1493
|
-
});
|
|
1494
|
-
deleteUser = withErrorHandling2(async (request) => {
|
|
1495
|
-
const params = request.params;
|
|
1496
|
-
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1497
|
-
const record = await this.service.findUserRecord(id);
|
|
1498
|
-
assertScimIfMatch(request, record);
|
|
1499
|
-
await this.service.deleteUser(id);
|
|
1500
|
-
return new Response(null, { status: 204 });
|
|
1501
|
-
});
|
|
1502
|
-
listGroups = withErrorHandling2(async (request) => {
|
|
1503
|
-
const url = new URL(request.url);
|
|
1504
|
-
const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
|
|
1505
|
-
const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
|
|
1506
|
-
return scimResponse(await this.service.listGroups(startIndex, count));
|
|
1507
|
-
});
|
|
1508
|
-
showGroup = withErrorHandling2(async (request) => {
|
|
1509
|
-
const params = request.params;
|
|
1510
|
-
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1511
|
-
const record = await this.service.findOrganizationRecord(id);
|
|
1512
|
-
const group = await this.service.getGroup(id);
|
|
1513
|
-
return scimResponse(group, { request, etagSource: record });
|
|
1514
|
-
});
|
|
1515
|
-
patchGroup = withErrorHandling2(async (request) => {
|
|
1516
|
-
const params = request.params;
|
|
1517
|
-
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1518
|
-
const record = await this.service.findOrganizationRecord(id);
|
|
1519
|
-
assertScimIfMatch(request, record);
|
|
1520
|
-
const body = await request.json();
|
|
1521
|
-
const group = await this.service.patchGroup(id, body.Operations ?? []);
|
|
1522
|
-
const updated = await this.service.findOrganizationRecord(id);
|
|
1523
|
-
return scimResponse(group, { etagSource: updated });
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
var controller_default = ScimController;
|
|
1527
|
-
|
|
1528
|
-
// ../../src/bootstrap/scimRoutes.ts
|
|
1529
|
-
function createScimRoutes(dependencies) {
|
|
1530
|
-
const redisUrl = process.env.REDIS_URL?.trim() ?? "";
|
|
1531
|
-
const secured = withMiddleware2(createScimThrottleMiddleware({
|
|
1532
|
-
redisUrl: redisUrl || undefined,
|
|
1533
|
-
maxAttempts: Number(process.env.SCIM_RATE_LIMIT_PER_MINUTE ?? "60"),
|
|
1534
|
-
decaySeconds: 60
|
|
1535
|
-
}), createScimAuthMiddleware());
|
|
1536
|
-
const bind = (method) => {
|
|
1537
|
-
return secured(async (request) => {
|
|
1538
|
-
const controller = new controller_default(dependencies);
|
|
1539
|
-
return await method(controller)(request);
|
|
1540
|
-
});
|
|
1541
|
-
};
|
|
1542
|
-
return {
|
|
1543
|
-
"/scim/v2/ServiceProviderConfig": {
|
|
1544
|
-
GET: bind((controller) => controller.serviceProviderConfig)
|
|
1545
|
-
},
|
|
1546
|
-
"/scim/v2/Users": {
|
|
1547
|
-
GET: bind((controller) => controller.listUsers),
|
|
1548
|
-
POST: bind((controller) => controller.createUser)
|
|
1549
|
-
},
|
|
1550
|
-
"/scim/v2/Users/:id": {
|
|
1551
|
-
GET: bind((controller) => controller.showUser),
|
|
1552
|
-
PATCH: bind((controller) => controller.patchUser),
|
|
1553
|
-
DELETE: bind((controller) => controller.deleteUser)
|
|
1554
|
-
},
|
|
1555
|
-
"/scim/v2/Groups": {
|
|
1556
|
-
GET: bind((controller) => controller.listGroups)
|
|
1557
|
-
},
|
|
1558
|
-
"/scim/v2/Groups/:id": {
|
|
1559
|
-
GET: bind((controller) => controller.showGroup),
|
|
1560
|
-
PATCH: bind((controller) => controller.patchGroup)
|
|
1561
|
-
}
|
|
1562
|
-
};
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
760
|
// ../../src/bootstrap/createRoutes.ts
|
|
1566
761
|
function registerRoute2(method, path, middleware) {
|
|
1567
762
|
routeRegistry.register({ method, path, middleware });
|
|
1568
763
|
}
|
|
1569
764
|
function createRoutes(dependencies) {
|
|
1570
|
-
const
|
|
765
|
+
const apiPrefix = isHiroappDogfood() ? "" : appConfig.apiPrefix;
|
|
1571
766
|
const wrappedModuleRoutes = buildModuleRoutes(dependencies, {
|
|
1572
|
-
apiPrefix
|
|
1573
|
-
clearRegistry: true
|
|
767
|
+
apiPrefix,
|
|
768
|
+
clearRegistry: true,
|
|
769
|
+
modules: []
|
|
1574
770
|
});
|
|
1575
771
|
const healthRoutes = createHealthRoutes(dependencies);
|
|
1576
772
|
const metricsRoutes = createMetricsRoutes();
|
|
1577
|
-
const scimRoutes = isFeatureEnabled("scim") ? applyMiddlewareToRoutes3(registerOpenApiRouteMap(createScimRoutes(dependencies), ["global", "scim"]), kernel.globalMiddleware()) : {};
|
|
1578
773
|
registerRoute2("GET", "/health", []);
|
|
1579
774
|
registerRoute2("GET", "/ready", []);
|
|
1580
775
|
registerRoute2("GET", "/metrics", []);
|
|
776
|
+
const notFoundApiPath = `${apiPrefix || "/api"}/*`;
|
|
1581
777
|
const baseRoutes = {
|
|
1582
778
|
...healthRoutes,
|
|
1583
779
|
...metricsRoutes,
|
|
1584
|
-
...
|
|
1585
|
-
...isViewsEnabled3() || isSpaEnabled2() ? {} : { "/": strata_default },
|
|
780
|
+
...isViewsEnabled4() || isSpaEnabled2() ? {} : { "/": strata_default },
|
|
1586
781
|
...wrappedModuleRoutes,
|
|
1587
|
-
[
|
|
1588
|
-
registerRoute2("GET",
|
|
1589
|
-
return
|
|
782
|
+
[notFoundApiPath]: async () => {
|
|
783
|
+
registerRoute2("GET", notFoundApiPath, ["global", "api"]);
|
|
784
|
+
return jsonResponse3({ error: "Not Found" }, { status: 404 });
|
|
1590
785
|
},
|
|
1591
786
|
"/*": async () => {
|
|
1592
787
|
registerRoute2("GET", "/*", []);
|
|
1593
|
-
if (
|
|
788
|
+
if (isViewsEnabled4()) {
|
|
1594
789
|
return notFoundHtmlResponse2();
|
|
1595
790
|
}
|
|
1596
791
|
if (isSpaEnabled2()) {
|
|
1597
|
-
return
|
|
792
|
+
return jsonResponse3({ error: "Not Found" }, { status: 404 });
|
|
1598
793
|
}
|
|
1599
|
-
return
|
|
794
|
+
return jsonResponse3({ error: "Not Found" }, { status: 404 });
|
|
1600
795
|
}
|
|
1601
796
|
};
|
|
1602
797
|
return mergeSpaRoutes(dependencies, mergeWebRoutes(dependencies, baseRoutes));
|