@getstrata/bootstrap 0.2.28 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/bootstrap/http/securedRouteModelBinding.d.ts +2 -2
  2. package/dist/bootstrap/schedule.d.ts +2 -1
  3. package/dist/bootstrap/web/forms.d.ts +1 -1
  4. package/dist/entries/applicationRegistry.js +2 -0
  5. package/dist/entries/buildModuleRoutes.js +2 -0
  6. package/dist/entries/buildWebModuleRoutes.js +2 -0
  7. package/dist/entries/cache/modelCacheTags.js +2 -0
  8. package/dist/entries/config.js +2 -0
  9. package/dist/entries/context.js +77 -3158
  10. package/dist/entries/contracts.js +2 -0
  11. package/dist/entries/createRoutes.js +1468 -0
  12. package/dist/entries/createSpaRoutes.js +2 -0
  13. package/dist/entries/createWebRoutes.js +2 -0
  14. package/dist/entries/dependencies.js +77 -3158
  15. package/dist/entries/discoverModules.js +2 -0
  16. package/dist/entries/health.js +3 -0
  17. package/dist/entries/http/securedRouteModelBinding.js +6 -293
  18. package/dist/entries/httpKernel.js +2 -0
  19. package/dist/entries/listeners/invalidateCacheOnModelWrite.js +3 -1
  20. package/dist/entries/membershipService.js +2 -0
  21. package/dist/entries/metricsRoutes.js +2 -0
  22. package/dist/entries/providers/view.js +6 -623
  23. package/dist/entries/providers.js +70 -3158
  24. package/dist/entries/queue/defaultJobs.js +7 -465
  25. package/dist/entries/routeRegistry.js +2 -0
  26. package/dist/entries/schedule.js +49 -0
  27. package/dist/entries/secretsGuard.js +9 -0
  28. package/dist/entries/web/forms.js +4 -18
  29. package/dist/entries/web/routing.js +18 -304
  30. package/dist/entries/web/server.js +2 -0
  31. package/dist/entries/web/session.js +3 -26
  32. package/dist/entries/web/slug.js +2 -0
  33. package/dist/index-sfreg6q3.js +0 -0
  34. package/dist/index.js +70 -3323
  35. package/package.json +12 -2
@@ -0,0 +1,1468 @@
1
+ // @bun
2
+ var __jsonParse = (a) => JSON.parse(a);
3
+
4
+ // ../../src/bootstrap/createRoutes.ts
5
+ import { jsonResponse as jsonResponse4 } from "@getstrata/core/http";
6
+ import { applyMiddlewareToRoutes as applyMiddlewareToRoutes3 } from "@getstrata/core/http/middleware";
7
+ import { htmlResponse as htmlResponse2 } from "@getstrata/core/view";
8
+
9
+ // ../../index.html
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\":\"rWY90LWIcx8\",\"content-type\":\"text/html;charset=utf-8\"}}]}");
11
+
12
+ // ../../src/config/app.ts
13
+ var appConfig = {
14
+ name: "WorkHub",
15
+ env: process.env.APP_ENV ?? "local",
16
+ debug: (process.env.APP_DEBUG ?? "true") !== "false",
17
+ url: process.env.APP_URL ?? "http://localhost:3000",
18
+ apiPrefix: process.env.API_PREFIX ?? "/api/v1"
19
+ };
20
+
21
+ // ../../src/config/features.ts
22
+ function readFeatureFlags() {
23
+ return {
24
+ webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
25
+ fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
26
+ auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
27
+ oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
28
+ samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
29
+ scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
30
+ billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
31
+ siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
32
+ publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
33
+ emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
34
+ mfa: (process.env.FEATURE_MFA ?? "false") === "true"
35
+ };
36
+ }
37
+ var featureFlags = readFeatureFlags();
38
+ function isFeatureEnabled(feature) {
39
+ return readFeatureFlags()[feature];
40
+ }
41
+
42
+ // ../../src/config/frontend.ts
43
+ function readFrontendMode() {
44
+ const mode = (process.env.FRONTEND_MODE ?? "api").trim();
45
+ if (mode === "server-htmx") {
46
+ return "server-htmx";
47
+ }
48
+ if (mode === "spa-react") {
49
+ return "spa-react";
50
+ }
51
+ return "api";
52
+ }
53
+ function isViewsEnabled() {
54
+ return readFrontendMode() === "server-htmx";
55
+ }
56
+ function isSpaEnabled() {
57
+ return readFrontendMode() === "spa-react";
58
+ }
59
+
60
+ // ../../src/bootstrap/buildModuleRoutes.ts
61
+ import { conditionalJsonResponse } from "@getstrata/core/http";
62
+ import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
63
+
64
+ // ../../src/bootstrap/httpKernel.ts
65
+ import {
66
+ createAuthMiddleware,
67
+ createAuthorizeMiddleware,
68
+ createBodySizeLimitMiddleware,
69
+ createCorsMiddleware,
70
+ createCsrfMiddleware,
71
+ createFlashMiddleware,
72
+ createLoginThrottleMiddleware,
73
+ createMembershipMiddleware,
74
+ createMemoryThrottleMiddleware,
75
+ createMetricsMiddleware,
76
+ createRequestLoggingMiddleware,
77
+ createRequireAbilityMiddleware,
78
+ createRequireAuthMiddleware,
79
+ createRequireGlobalAdminMiddleware,
80
+ createRequireWebAuthMiddleware,
81
+ createSecurityHeadersMiddleware,
82
+ createTenantMiddleware,
83
+ createThrottleMiddleware,
84
+ createTracingMiddleware,
85
+ isPublicReadsEnabled,
86
+ requestIdMiddleware,
87
+ withMiddleware
88
+ } from "@getstrata/core";
89
+
90
+ // ../../src/config/rateLimit.ts
91
+ var LOCAL_LOGIN_RATE_LIMIT = {
92
+ maxAttempts: 100,
93
+ decaySeconds: 60
94
+ };
95
+ var PRODUCTION_LOGIN_RATE_LIMIT = {
96
+ maxAttempts: 5,
97
+ decaySeconds: 900
98
+ };
99
+ function isLocalAppEnv() {
100
+ return (process.env.APP_ENV ?? "local") === "local";
101
+ }
102
+ function parsePositiveInt(value, fallback) {
103
+ const parsed = Number(value);
104
+ if (!Number.isFinite(parsed) || parsed <= 0) {
105
+ return fallback;
106
+ }
107
+ return Math.trunc(parsed);
108
+ }
109
+ function resolveLoginRateLimit() {
110
+ const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
111
+ return {
112
+ maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
113
+ decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
114
+ };
115
+ }
116
+ function resolveRegisterRateLimit() {
117
+ const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
118
+ return {
119
+ maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
120
+ decaySeconds: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS ?? process.env.REGISTER_RATE_LIMIT_WINDOW_MS ? String(Number(process.env.REGISTER_RATE_LIMIT_WINDOW_MS) / 1000) : undefined, defaults.decaySeconds)
121
+ };
122
+ }
123
+
124
+ // ../../src/bootstrap/config.ts
125
+ import {
126
+ CORE_AUTH_TOKEN,
127
+ CORE_CACHE_TOKEN,
128
+ CORE_CONFIG_TOKEN,
129
+ CORE_EVENT_BUS_TOKEN,
130
+ CORE_POLICY_GATE_TOKEN,
131
+ CORE_QUEUE_TOKEN,
132
+ CORE_TOKEN_SERVICE_TOKEN
133
+ } from "@getstrata/core/contracts/serviceTokens";
134
+ var APP_PORT_CONFIG_KEY = "app.port";
135
+ var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
136
+ var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
137
+ var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
138
+ var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
139
+ var DATABASE_URL_CONFIG_KEY = "database.url";
140
+ var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
141
+ var DEFAULT_APP_PORT = 3000;
142
+ var DEFAULT_CACHE_TTL_MS = 3600000;
143
+ var DEFAULT_CACHE_MAX_ENTRIES = 100;
144
+ var DEFAULT_CACHE_DRIVER = "array";
145
+ var DEFAULT_API_TOKEN = "";
146
+ var DEFAULT_QUEUE_DRIVER = "sync";
147
+
148
+ // ../../src/bootstrap/httpKernel.ts
149
+ class HttpKernel {
150
+ dependencies;
151
+ constructor(dependencies) {
152
+ this.dependencies = dependencies;
153
+ }
154
+ globalMiddleware() {
155
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
156
+ return [
157
+ createCorsMiddleware(),
158
+ createSecurityHeadersMiddleware(),
159
+ createBodySizeLimitMiddleware(),
160
+ createTracingMiddleware(),
161
+ createMetricsMiddleware(),
162
+ createRequestLoggingMiddleware(),
163
+ requestIdMiddleware,
164
+ createAuthMiddleware(auth),
165
+ createMembershipMiddleware(),
166
+ createTenantMiddleware()
167
+ ];
168
+ }
169
+ group(name) {
170
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
171
+ switch (name) {
172
+ case "authenticated":
173
+ return [createRequireAuthMiddleware(auth)];
174
+ case "web":
175
+ return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
176
+ case "api": {
177
+ if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
178
+ return [];
179
+ }
180
+ const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
181
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
182
+ if (!redisUrl) {
183
+ const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
184
+ return [
185
+ createMemoryThrottleMiddleware({
186
+ maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
187
+ decaySeconds: 60
188
+ })
189
+ ];
190
+ }
191
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
192
+ return [
193
+ createThrottleMiddleware({
194
+ redisUrl,
195
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
196
+ decaySeconds: 60
197
+ })
198
+ ];
199
+ }
200
+ default:
201
+ throw new Error(`Unknown middleware group "${name}".`);
202
+ }
203
+ }
204
+ wrap(groups, handler) {
205
+ const names = Array.isArray(groups) ? groups : [groups];
206
+ const middleware = names.flatMap((name) => this.group(name));
207
+ if (middleware.length === 0) {
208
+ return handler;
209
+ }
210
+ return withMiddleware(...middleware)(handler);
211
+ }
212
+ wrapApi(handler) {
213
+ return this.wrap(["api", "authenticated"], handler);
214
+ }
215
+ wrapWeb(handler) {
216
+ return handler;
217
+ }
218
+ wrapWebPublicRead(handler) {
219
+ if (isPublicReadsEnabled()) {
220
+ return this.wrapWeb(handler);
221
+ }
222
+ return this.wrapWebAuthenticated(handler);
223
+ }
224
+ wrapWebAuthenticated(handler) {
225
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
226
+ return withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
227
+ }
228
+ wrapWebAbility(ability, handler) {
229
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
230
+ const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
231
+ const requireAbility = createRequireAbilityMiddleware(abilityChecker);
232
+ const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
233
+ return withMiddleware(...middleware)(handler);
234
+ }
235
+ wrapWebGlobalAdmin(handler) {
236
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
237
+ const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
238
+ return withMiddleware(...middleware)(handler);
239
+ }
240
+ wrapAuthenticated(handler) {
241
+ return this.wrap("authenticated", handler);
242
+ }
243
+ wrapPublicRead(handler) {
244
+ if (isPublicReadsEnabled()) {
245
+ return handler;
246
+ }
247
+ return this.wrapAuthenticated(handler);
248
+ }
249
+ wrapGlobalAdmin(handler) {
250
+ const middleware = [...this.group("authenticated"), createRequireGlobalAdminMiddleware()];
251
+ return withMiddleware(...middleware)(handler);
252
+ }
253
+ wrapAbility(ability, handler) {
254
+ const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
255
+ const requireAbility = createRequireAbilityMiddleware(abilityChecker);
256
+ const middleware = [...this.group("authenticated"), requireAbility(ability)];
257
+ return withMiddleware(...middleware)(handler);
258
+ }
259
+ wrapPolicy(resource, action, handler) {
260
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
261
+ const gate = this.dependencies.container.resolve(CORE_POLICY_GATE_TOKEN);
262
+ return withMiddleware(createAuthorizeMiddleware(gate, auth, resource, action))(handler);
263
+ }
264
+ wrapLogin(handler) {
265
+ return this.wrapThrottle("login", resolveLoginRateLimit(), handler);
266
+ }
267
+ wrapRegister(handler) {
268
+ return this.wrapThrottle("register", resolveRegisterRateLimit(), handler);
269
+ }
270
+ wrapThrottle(scope, rateLimit, handler) {
271
+ const middleware = [];
272
+ const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
273
+ if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
274
+ const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
275
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
276
+ if (redisUrl) {
277
+ const throttle = scope === "login" ? createLoginThrottleMiddleware({
278
+ redisUrl,
279
+ maxAttempts: rateLimit.maxAttempts,
280
+ decaySeconds: rateLimit.decaySeconds
281
+ }) : createThrottleMiddleware({
282
+ redisUrl,
283
+ maxAttempts: rateLimit.maxAttempts,
284
+ decaySeconds: rateLimit.decaySeconds,
285
+ keyPrefix: memoryKeyPrefix
286
+ });
287
+ middleware.push(throttle);
288
+ } else {
289
+ middleware.push(createMemoryThrottleMiddleware({
290
+ maxAttempts: rateLimit.maxAttempts,
291
+ decaySeconds: rateLimit.decaySeconds,
292
+ keyPrefix: memoryKeyPrefix
293
+ }));
294
+ }
295
+ } else {
296
+ middleware.push(createMemoryThrottleMiddleware({
297
+ maxAttempts: rateLimit.maxAttempts,
298
+ decaySeconds: rateLimit.decaySeconds,
299
+ keyPrefix: memoryKeyPrefix
300
+ }));
301
+ }
302
+ if (middleware.length === 0) {
303
+ return handler;
304
+ }
305
+ return withMiddleware(...middleware)(handler);
306
+ }
307
+ }
308
+ function createHttpKernel(dependencies) {
309
+ return new HttpKernel(dependencies);
310
+ }
311
+
312
+ // ../../src/bootstrap/discoverModules.ts
313
+ import { readdirSync } from "fs";
314
+ import { join } from "path";
315
+ import { pathToFileURL } from "url";
316
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
317
+ function readDiscoverModulesState() {
318
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
319
+ if (existing) {
320
+ return existing;
321
+ }
322
+ const state = { appModules: [] };
323
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
324
+ return state;
325
+ }
326
+ function configureModulesDirectory(modulesDir) {
327
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
328
+ }
329
+ function resolveModulesDirectory(options) {
330
+ const state = readDiscoverModulesState();
331
+ if (options?.modulesDir) {
332
+ return options.modulesDir;
333
+ }
334
+ if (state.configuredModulesDir) {
335
+ return state.configuredModulesDir;
336
+ }
337
+ return join(import.meta.dir, "../modules");
338
+ }
339
+ async function loadDiscoveredModules(options) {
340
+ const modulesDirectory = resolveModulesDirectory(options);
341
+ let moduleNames;
342
+ try {
343
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
344
+ } catch (error) {
345
+ if (error.code === "ENOENT") {
346
+ return [];
347
+ }
348
+ throw error;
349
+ }
350
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
351
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
352
+ const loaded = await import(moduleUrl);
353
+ return loaded.default;
354
+ }));
355
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
356
+ }
357
+ async function ensureModulesLoaded(options) {
358
+ const state = readDiscoverModulesState();
359
+ if (state.appModules.length > 0) {
360
+ return state.appModules;
361
+ }
362
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
363
+ state.appModules.splice(0, state.appModules.length, ...modules);
364
+ return state.appModules;
365
+ });
366
+ return state.modulesReady;
367
+ }
368
+ function discoverModules() {
369
+ return readDiscoverModulesState().appModules;
370
+ }
371
+ // ../../src/bootstrap/prefixRouteMap.ts
372
+ function prefixRouteMap(prefix, routes) {
373
+ const normalizedPrefix = prefix.replace(/\/$/, "");
374
+ const prefixed = {};
375
+ for (const [path, handler] of Object.entries(routes)) {
376
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
377
+ prefixed[`${normalizedPrefix}${normalizedPath}`] = handler;
378
+ }
379
+ return prefixed;
380
+ }
381
+
382
+ // ../../src/bootstrap/routeRegistry.ts
383
+ class RouteRegistry {
384
+ routes = [];
385
+ register(route) {
386
+ this.routes.push(route);
387
+ }
388
+ clear() {
389
+ this.routes.length = 0;
390
+ }
391
+ list() {
392
+ return [...this.routes].sort((left, right) => left.path.localeCompare(right.path));
393
+ }
394
+ }
395
+ var ROUTE_REGISTRY_KEY = Symbol.for("@getstrata/routeRegistry");
396
+ function readSharedRouteRegistry() {
397
+ const globalRegistry = globalThis[ROUTE_REGISTRY_KEY];
398
+ if (globalRegistry) {
399
+ return globalRegistry;
400
+ }
401
+ const registry = new RouteRegistry;
402
+ globalThis[ROUTE_REGISTRY_KEY] = registry;
403
+ return registry;
404
+ }
405
+ var routeRegistry = readSharedRouteRegistry();
406
+
407
+ // ../../src/bootstrap/buildModuleRoutes.ts
408
+ function registerOpenApiRoute(method, path, middleware) {
409
+ routeRegistry.register({ method, path, middleware });
410
+ }
411
+ function registerOpenApiRouteMap(routes, middleware) {
412
+ const registered = {};
413
+ for (const [path, handler] of Object.entries(routes)) {
414
+ if (handler && typeof handler === "object" && !Array.isArray(handler)) {
415
+ const methodMap = handler;
416
+ registered[path] = methodMap;
417
+ for (const method of Object.keys(methodMap)) {
418
+ registerOpenApiRoute(method.toUpperCase(), path, middleware);
419
+ }
420
+ continue;
421
+ }
422
+ registered[path] = handler;
423
+ registerOpenApiRoute("GET", path, middleware);
424
+ }
425
+ return registered;
426
+ }
427
+ function createCachedJson(dependencies) {
428
+ return async (cacheKey, loader, tags = [], request) => {
429
+ const data = tags.length > 0 ? await dependencies.cache.tags(...tags).remember(cacheKey, loader) : await dependencies.cache.remember(cacheKey, loader);
430
+ return conditionalJsonResponse(request, data);
431
+ };
432
+ }
433
+ function buildModuleRoutes(dependencies, options = {}) {
434
+ const { apiPrefix = "", modules = discoverModules(), clearRegistry = true } = options;
435
+ if (clearRegistry) {
436
+ routeRegistry.clear();
437
+ }
438
+ const kernel = createHttpKernel(dependencies);
439
+ const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
440
+ const cachedJson = createCachedJson(dependencies);
441
+ const moduleRoutes = {};
442
+ for (const module of modules) {
443
+ if (!module.routes) {
444
+ continue;
445
+ }
446
+ Object.assign(moduleRoutes, module.routes({ dependencies, cachedJson, kernel }));
447
+ }
448
+ const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
449
+ return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
450
+ }
451
+
452
+ // ../../src/bootstrap/createSpaRoutes.ts
453
+ import { join as join2 } from "path";
454
+ import { jsonResponse } from "@getstrata/core/http";
455
+ var SPA_DIST_DIRECTORY = join2(process.cwd(), "frontend/dist");
456
+ var SPA_INDEX_FILE = join2(SPA_DIST_DIRECTORY, "index.html");
457
+ function createSpaRoutes(_dependencies) {
458
+ return {
459
+ "/app/*": async (request) => {
460
+ const pathname = new URL(request.url).pathname;
461
+ const relativePath = pathname.replace(/^\/app\//, "");
462
+ const assetFile = Bun.file(join2(SPA_DIST_DIRECTORY, relativePath));
463
+ if (relativePath.length > 0 && await assetFile.exists()) {
464
+ return new Response(assetFile);
465
+ }
466
+ const indexFile = Bun.file(SPA_INDEX_FILE);
467
+ if (await indexFile.exists()) {
468
+ return new Response(indexFile, {
469
+ headers: { "Content-Type": "text/html; charset=utf-8" }
470
+ });
471
+ }
472
+ return jsonResponse({
473
+ error: "SPA build not found. Run `cd frontend && bun install && bun run build`."
474
+ }, { status: 503 });
475
+ },
476
+ "/": async () => Response.redirect("/app/", 302)
477
+ };
478
+ }
479
+ function mergeSpaRoutes(dependencies, routes) {
480
+ if (!isSpaEnabled()) {
481
+ return routes;
482
+ }
483
+ return {
484
+ ...createSpaRoutes(dependencies),
485
+ ...routes
486
+ };
487
+ }
488
+
489
+ // ../../src/bootstrap/createWebRoutes.ts
490
+ import { join as join3 } from "path";
491
+ import { htmlResponse } from "@getstrata/core/view";
492
+
493
+ // ../../src/bootstrap/buildWebModuleRoutes.ts
494
+ import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/core/http/middleware";
495
+ function buildWebModuleRoutes(dependencies, options = {}) {
496
+ const { modules = discoverModules(), clearRegistry = true, seedRoutes = {} } = options;
497
+ if (clearRegistry) {
498
+ routeRegistry.clear();
499
+ }
500
+ const kernel = createHttpKernel(dependencies);
501
+ const middleware = [...kernel.globalMiddleware(), ...kernel.group("web")];
502
+ const moduleRoutes = { ...seedRoutes };
503
+ for (const module of modules) {
504
+ if (!module.webRoutes) {
505
+ continue;
506
+ }
507
+ Object.assign(moduleRoutes, module.webRoutes({
508
+ dependencies,
509
+ cachedJson: async () => new Response(""),
510
+ kernel
511
+ }));
512
+ }
513
+ return applyMiddlewareToRoutes2(registerOpenApiRouteMap(moduleRoutes, ["global", "web"]), middleware);
514
+ }
515
+
516
+ // ../../src/bootstrap/createWebRoutes.ts
517
+ function registerRoute(method, path, middleware) {
518
+ routeRegistry.register({ method, path, middleware });
519
+ }
520
+ function createWebRoutes(dependencies) {
521
+ const wrappedRoutes = buildWebModuleRoutes(dependencies, {
522
+ clearRegistry: false,
523
+ seedRoutes: {
524
+ "/": () => Response.redirect("/organizations", 302)
525
+ }
526
+ });
527
+ registerRoute("GET", "/", ["global", "web"]);
528
+ wrappedRoutes["/assets/*"] = async (request) => {
529
+ registerRoute("GET", "/assets/*", ["global", "web"]);
530
+ const pathname = new URL(request.url).pathname;
531
+ const relativePath = pathname.replace(/^\//, "");
532
+ const file = Bun.file(join3(process.cwd(), "public", relativePath));
533
+ if (!await file.exists()) {
534
+ return htmlResponse("Not Found", { status: 404 });
535
+ }
536
+ return new Response(file);
537
+ };
538
+ registerRoute("GET", "/assets/*", ["global", "web"]);
539
+ return wrappedRoutes;
540
+ }
541
+ function mergeWebRoutes(dependencies, routes) {
542
+ if (!isViewsEnabled()) {
543
+ return routes;
544
+ }
545
+ return {
546
+ ...createWebRoutes(dependencies),
547
+ ...routes
548
+ };
549
+ }
550
+
551
+ // ../../src/bootstrap/health.ts
552
+ import { jsonResponse as jsonResponse2 } from "@getstrata/core/http";
553
+ var {RedisClient } = globalThis.Bun;
554
+
555
+ // ../../src/config/database.ts
556
+ function readInteger(name, fallback) {
557
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
558
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
559
+ }
560
+ var databaseConfig = {
561
+ url: process.env.DATABASE_URL ?? "",
562
+ poolMax: readInteger("DB_POOL_MAX", 10),
563
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
564
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
565
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
566
+ };
567
+
568
+ // ../../src/core/runtime/asyncContextStore.ts
569
+ import { AsyncLocalStorage } from "async_hooks";
570
+ function createAsyncContextStore(key) {
571
+ const symbol = Symbol.for(key);
572
+ const globalRecord = globalThis;
573
+ const existing = globalRecord[symbol];
574
+ if (existing) {
575
+ return existing;
576
+ }
577
+ const store = new AsyncLocalStorage;
578
+ globalRecord[symbol] = store;
579
+ return store;
580
+ }
581
+
582
+ // ../../src/core/database/connectionContext.ts
583
+ var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
584
+ function getActiveDatabaseConnection(fallback) {
585
+ return activeConnection.getStore() ?? fallback;
586
+ }
587
+
588
+ // ../../src/core/database/queryProxy.ts
589
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
590
+ function createDatabaseQueryProxy(pool) {
591
+ function resolveDatabase() {
592
+ return getActiveDatabaseConnection(pool);
593
+ }
594
+ function resolveDatabaseForProperty(property) {
595
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
596
+ return pool;
597
+ }
598
+ return resolveDatabase();
599
+ }
600
+ return new Proxy(function database() {}, {
601
+ apply(_target, _thisArg, args) {
602
+ return resolveDatabase()(...args);
603
+ },
604
+ get(_target, property) {
605
+ const connection = resolveDatabaseForProperty(property);
606
+ const value = connection[property];
607
+ return typeof value === "function" ? value.bind(connection) : value;
608
+ }
609
+ });
610
+ }
611
+
612
+ // ../../src/core/database/defaultConnection.ts
613
+ var defaultPool = {
614
+ connection: null
615
+ };
616
+ var defaultQuery = {
617
+ connection: null
618
+ };
619
+ function registerDefaultDatabasePool(connection) {
620
+ defaultPool.connection = connection;
621
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
622
+ }
623
+ function getDefaultDatabaseQuery() {
624
+ if (!defaultQuery.connection) {
625
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
626
+ }
627
+ return defaultQuery.connection;
628
+ }
629
+
630
+ // ../../src/db/connection/createConnection.ts
631
+ var {SQL } = globalThis.Bun;
632
+ function createDatabaseConnection(config) {
633
+ if (!config.url) {
634
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
635
+ }
636
+ return new SQL({
637
+ url: config.url,
638
+ max: config.poolMax,
639
+ idleTimeout: config.idleTimeoutSeconds,
640
+ maxLifetime: config.maxLifetimeSeconds,
641
+ connectionTimeout: config.connectionTimeoutSeconds
642
+ });
643
+ }
644
+
645
+ // ../../src/db/connection/index.ts
646
+ var connectionHolder = {
647
+ connection: null
648
+ };
649
+ function getDatabase() {
650
+ if (!connectionHolder.connection) {
651
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
652
+ registerDefaultDatabasePool(connectionHolder.connection);
653
+ }
654
+ return connectionHolder.connection;
655
+ }
656
+ function getDb() {
657
+ getDatabase();
658
+ return getDefaultDatabaseQuery();
659
+ }
660
+ async function pingDatabase(connection = getDatabase()) {
661
+ try {
662
+ await connection`SELECT 1`;
663
+ return true;
664
+ } catch {
665
+ return false;
666
+ }
667
+ }
668
+ async function ensureDatabaseConnection() {
669
+ if (await pingDatabase()) {
670
+ return getDatabase();
671
+ }
672
+ await getDatabase().close().catch(() => {
673
+ return;
674
+ });
675
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
676
+ registerDefaultDatabasePool(connectionHolder.connection);
677
+ return getDatabase();
678
+ }
679
+ var db = new Proxy(function database() {}, {
680
+ apply(_target, _thisArg, args) {
681
+ return getDb()(...args);
682
+ },
683
+ get(_target, property) {
684
+ const connection = getDb();
685
+ const value = connection[property];
686
+ return typeof value === "function" ? value.bind(connection) : value;
687
+ }
688
+ });
689
+ var connection_default = db;
690
+
691
+ // ../../src/bootstrap/health.ts
692
+ function resolveRedisUrl(dependencies) {
693
+ if (!dependencies.container.has(CORE_CONFIG_TOKEN)) {
694
+ return process.env.REDIS_URL?.trim() || undefined;
695
+ }
696
+ const config = dependencies.container.resolve(CORE_CONFIG_TOKEN);
697
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim();
698
+ return redisUrl || undefined;
699
+ }
700
+ async function checkDatabase() {
701
+ await ensureDatabaseConnection();
702
+ return await pingDatabase();
703
+ }
704
+ async function checkRedis(redisUrl) {
705
+ try {
706
+ const client = new RedisClient(redisUrl);
707
+ const response = await client.ping();
708
+ return response === "PONG";
709
+ } catch {
710
+ return false;
711
+ }
712
+ }
713
+ function createHealthRoutes(dependencies) {
714
+ return {
715
+ "/health": async () => jsonResponse2({ status: "ok" }),
716
+ "/ready": async () => {
717
+ const checks = {
718
+ database: await checkDatabase() ? "ok" : "error"
719
+ };
720
+ const redisUrl = resolveRedisUrl(dependencies);
721
+ if (redisUrl) {
722
+ checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
723
+ } else {
724
+ checks.redis = "skipped";
725
+ }
726
+ const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
727
+ return jsonResponse2({
728
+ status: ready ? "ready" : "not_ready",
729
+ checks
730
+ }, { status: ready ? 200 : 503 });
731
+ }
732
+ };
733
+ }
734
+
735
+ // ../../src/bootstrap/metricsRoutes.ts
736
+ import { prometheusRegistry } from "@getstrata/core/metrics/prometheus";
737
+ function createMetricsRoutes() {
738
+ return {
739
+ "/metrics": async () => new Response(prometheusRegistry.renderMetrics(), {
740
+ status: 200,
741
+ headers: {
742
+ "content-type": "text/plain; version=0.0.4; charset=utf-8"
743
+ }
744
+ })
745
+ };
746
+ }
747
+
748
+ // ../../src/bootstrap/scimRoutes.ts
749
+ import {
750
+ createScimAuthMiddleware,
751
+ createScimThrottleMiddleware,
752
+ withMiddleware as withMiddleware2
753
+ } from "@getstrata/core";
754
+
755
+ // ../../src/modules/scim/controller.ts
756
+ import { withErrorHandling } from "@getstrata/core/http";
757
+
758
+ // ../../src/modules/scim/scimResponse.ts
759
+ import {
760
+ applyConditionalGet,
761
+ assertIfMatch,
762
+ etagFromResource,
763
+ isEtagEnabled
764
+ } from "@getstrata/core";
765
+ import { jsonResponse as jsonResponse3 } from "@getstrata/core/http";
766
+ function scimResponse(data, options = {}) {
767
+ const status = options.status ?? 200;
768
+ const response = jsonResponse3(data, {
769
+ status,
770
+ headers: {
771
+ "content-type": "application/scim+json"
772
+ }
773
+ });
774
+ if (!isEtagEnabled() || !options.etagSource) {
775
+ return response;
776
+ }
777
+ const etag = etagFromResource(options.etagSource);
778
+ if (options.request) {
779
+ return applyConditionalGet(options.request, response, etag);
780
+ }
781
+ const headers = new Headers(response.headers);
782
+ headers.set("ETag", etag);
783
+ headers.set("Cache-Control", "private, must-revalidate");
784
+ return new Response(response.body, {
785
+ status: response.status,
786
+ statusText: response.statusText,
787
+ headers
788
+ });
789
+ }
790
+ function assertScimIfMatch(request, etagSource) {
791
+ if (!isEtagEnabled()) {
792
+ return;
793
+ }
794
+ assertIfMatch(request, etagFromResource(etagSource), { required: true });
795
+ }
796
+
797
+ // ../../src/modules/scim/service.ts
798
+ import { currentTenantId as currentTenantId4 } from "@getstrata/core";
799
+ import { hashPassword } from "@getstrata/core/auth/password";
800
+ import { resolveService } from "@getstrata/core/contracts/di";
801
+ import { NotFoundError as NotFoundError4 } from "@getstrata/core/errors/http";
802
+
803
+ // ../../src/domain/scim.ts
804
+ var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
805
+ var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
806
+ var SCIM_SCHEMAS = {
807
+ user: "urn:ietf:params:scim:schemas:core:2.0:User",
808
+ group: "urn:ietf:params:scim:schemas:core:2.0:Group",
809
+ listResponse: "urn:ietf:params:scim:api:messages:2.0:ListResponse",
810
+ patchOp: "urn:ietf:params:scim:api:messages:2.0:PatchOp",
811
+ serviceProviderConfig: "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
812
+ };
813
+
814
+ // ../../src/modules/organization/memberRepository.ts
815
+ class OrganizationMemberRepository {
816
+ constructor() {}
817
+ async findMembership(userId, organizationId) {
818
+ const rows = await connection_default`
819
+ SELECT id, organization_id, user_id, role, created_at
820
+ FROM organization_member
821
+ WHERE user_id = ${userId} AND organization_id = ${organizationId}
822
+ LIMIT 1
823
+ `;
824
+ return rows[0] ?? null;
825
+ }
826
+ async listForUser(userId) {
827
+ return await connection_default`
828
+ SELECT id, organization_id, user_id, role, created_at
829
+ FROM organization_member
830
+ WHERE user_id = ${userId}
831
+ ORDER BY organization_id
832
+ `;
833
+ }
834
+ async listForOrganization(organizationId) {
835
+ return await connection_default`
836
+ SELECT id, organization_id, user_id, role, created_at
837
+ FROM organization_member
838
+ WHERE organization_id = ${organizationId}
839
+ ORDER BY id
840
+ `;
841
+ }
842
+ async addMember(input) {
843
+ const rows = await connection_default`
844
+ INSERT INTO organization_member (organization_id, user_id, role)
845
+ VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
846
+ RETURNING id, organization_id, user_id, role, created_at
847
+ `;
848
+ const row = rows[0];
849
+ if (!row) {
850
+ throw new Error("Organization member insert did not return a row.");
851
+ }
852
+ return row;
853
+ }
854
+ async removeMember(organizationId, userId) {
855
+ const rows = await connection_default`
856
+ DELETE FROM organization_member
857
+ WHERE organization_id = ${organizationId} AND user_id = ${userId}
858
+ RETURNING id
859
+ `;
860
+ return rows.length > 0;
861
+ }
862
+ }
863
+ var memberRepository_default = OrganizationMemberRepository;
864
+
865
+ // ../../src/modules/organization/repository.ts
866
+ import { BaseRepository } from "@getstrata/core/database";
867
+ import { NotFoundError } from "@getstrata/core/errors/http";
868
+ import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
869
+
870
+ // ../../src/modules/organization/table.ts
871
+ import { defineTable } from "@getstrata/core/database";
872
+
873
+ // ../../src/domain/workhub.ts
874
+ var ORGANIZATION_TABLE = "organization";
875
+
876
+ // ../../src/modules/organization/table.ts
877
+ var organizationTable = defineTable({
878
+ name: ORGANIZATION_TABLE,
879
+ primaryKey: "id",
880
+ columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
881
+ softDeletes: true,
882
+ defaultOrderBy: { column: "id", direction: "ASC" }
883
+ });
884
+
885
+ // ../../src/modules/organization/repository.ts
886
+ class OrganizationRepository extends BaseRepository {
887
+ constructor() {
888
+ super(organizationTable);
889
+ }
890
+ async findBySlug(slug) {
891
+ return await this.firstOrNull({ slug });
892
+ }
893
+ async listForTenant(options) {
894
+ return await this.findAll({
895
+ limit: options.limit,
896
+ offset: options.offset,
897
+ where: { tenant_id: options.tenantId ?? currentTenantId() }
898
+ });
899
+ }
900
+ async countForTenant(tenantId = currentTenantId()) {
901
+ return await this.countWhere({ tenant_id: tenantId });
902
+ }
903
+ async findForTenantOrThrow(id, tenantId = currentTenantId()) {
904
+ const organization = await this.findById(id);
905
+ if (!organization || organization.tenant_id !== tenantId) {
906
+ throw new NotFoundError(`SCIM group ${id} not found.`);
907
+ }
908
+ return organization;
909
+ }
910
+ }
911
+ var repository_default = OrganizationRepository;
912
+
913
+ // ../../src/modules/user/provider.ts
914
+ import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
915
+ import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
916
+ import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
917
+
918
+ // ../../src/modules/user/apiTokenRepository.ts
919
+ import { BaseRepository as BaseRepository2 } from "@getstrata/core/database";
920
+
921
+ // ../../src/modules/user/apiTokenTable.ts
922
+ import { defineTable as defineTable2 } from "@getstrata/core/database";
923
+ var apiTokenTable = defineTable2({
924
+ name: "api_token",
925
+ primaryKey: "id",
926
+ columns: [
927
+ "id",
928
+ "user_id",
929
+ "name",
930
+ "token_hash",
931
+ "abilities",
932
+ "last_used_at",
933
+ "expires_at",
934
+ "created_at"
935
+ ],
936
+ defaultOrderBy: { column: "id", direction: "ASC" }
937
+ });
938
+
939
+ // ../../src/modules/user/authService.ts
940
+ import { verifyPassword } from "@getstrata/core/auth/password";
941
+ import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
942
+ import { UnauthorizedError } from "@getstrata/core/errors/http";
943
+ import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
944
+ import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
945
+ import { verifyTotp } from "@getstrata/core/security/totp";
946
+ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
947
+
948
+ // ../../src/domain/abilities.ts
949
+ var MEMBER_ABILITIES = [
950
+ "organizations:read",
951
+ "projects:read",
952
+ "projects:create",
953
+ "tasks:read",
954
+ "tasks:create",
955
+ "comments:read",
956
+ "comments:create",
957
+ "attachments:read",
958
+ "attachments:create",
959
+ "auth:tokens:read",
960
+ "auth:tokens:write"
961
+ ];
962
+ var ADMIN_ABILITIES = [
963
+ ...MEMBER_ABILITIES,
964
+ "organizations:create",
965
+ "organizations:update",
966
+ "organizations:delete",
967
+ "projects:update",
968
+ "projects:delete",
969
+ "tasks:update",
970
+ "tasks:delete",
971
+ "comments:update",
972
+ "comments:delete",
973
+ "attachments:delete",
974
+ "webhooks:read",
975
+ "webhooks:write",
976
+ "audit:read"
977
+ ];
978
+ var PLATFORM_ADMIN_ABILITIES = ["*"];
979
+ function resolveAbilitiesForRole(role) {
980
+ if (role === "admin") {
981
+ return [...PLATFORM_ADMIN_ABILITIES];
982
+ }
983
+ return [...MEMBER_ABILITIES];
984
+ }
985
+
986
+ // ../../src/modules/user/authService.ts
987
+ class AuthService {
988
+ users;
989
+ tokens;
990
+ oauthIdentities;
991
+ oauthProviders = new Map;
992
+ constructor(users, tokens, oauthIdentities) {
993
+ this.users = users;
994
+ this.tokens = tokens;
995
+ this.oauthIdentities = oauthIdentities;
996
+ }
997
+ registerOAuthProvider(provider) {
998
+ this.oauthProviders.set(provider.name, provider);
999
+ }
1000
+ getOAuthProvider(name) {
1001
+ return this.oauthProviders.get(name);
1002
+ }
1003
+ async loginWithPassword(email, password, options = {}) {
1004
+ const user = await this.users.findByEmail(email);
1005
+ if (!user?.password_hash) {
1006
+ logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
1007
+ throw new UnauthorizedError("Invalid credentials.");
1008
+ }
1009
+ const valid = await verifyPassword(password, user.password_hash);
1010
+ if (!valid) {
1011
+ logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
1012
+ throw new UnauthorizedError("Invalid credentials.");
1013
+ }
1014
+ if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
1015
+ logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
1016
+ throw new UnauthorizedError("Email address is not verified.");
1017
+ }
1018
+ if (isFeatureEnabled("mfa") && user.mfa_enabled) {
1019
+ const mfaSecret = revealMfaSecret(user.mfa_secret);
1020
+ if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
1021
+ logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
1022
+ throw new UnauthorizedError("Invalid MFA code.");
1023
+ }
1024
+ }
1025
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
1026
+ return await this.tokens.createToken(user.id, {
1027
+ name: "password-login",
1028
+ abilities: resolveAbilitiesForRole(user.role),
1029
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
1030
+ });
1031
+ }
1032
+ async loginWithOAuth(providerName, code) {
1033
+ const provider = this.oauthProviders.get(providerName);
1034
+ if (!provider) {
1035
+ throw new UnauthorizedError("Unsupported OAuth provider.");
1036
+ }
1037
+ const profile = await provider.exchangeCode(code);
1038
+ const user = await this.findOrCreateOAuthUser(providerName, profile);
1039
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
1040
+ return await this.tokens.createToken(user.id, {
1041
+ name: `${providerName}-oauth`,
1042
+ abilities: resolveAbilitiesForRole(user.role),
1043
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
1044
+ });
1045
+ }
1046
+ buildOAuthAuthorizationUrl(providerName, state) {
1047
+ const provider = this.oauthProviders.get(providerName);
1048
+ if (!provider) {
1049
+ throw new UnauthorizedError("Unsupported OAuth provider.");
1050
+ }
1051
+ return provider.getAuthorizationUrl(state);
1052
+ }
1053
+ async findOrCreateOAuthUser(providerName, profile) {
1054
+ const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
1055
+ if (existingIdentity) {
1056
+ return await this.users.findByIdOrThrow(existingIdentity.user_id);
1057
+ }
1058
+ const existingUser = await this.users.findByEmail(profile.email);
1059
+ const user = existingUser ?? await this.users.create({
1060
+ name: profile.name,
1061
+ email: profile.email,
1062
+ role: "member",
1063
+ tenant_id: currentTenantId2(),
1064
+ email_verified_at: new Date,
1065
+ created_at: new Date,
1066
+ updated_at: new Date
1067
+ });
1068
+ await this.oauthIdentities.create({
1069
+ user_id: user.id,
1070
+ provider: providerName,
1071
+ provider_user_id: profile.providerUserId,
1072
+ email: profile.email,
1073
+ created_at: new Date
1074
+ });
1075
+ return user;
1076
+ }
1077
+ }
1078
+
1079
+ // ../../src/modules/user/notificationRepository.ts
1080
+ import { BaseRepository as BaseRepository3 } from "@getstrata/core/database";
1081
+
1082
+ // ../../src/modules/user/notificationTable.ts
1083
+ import { defineTable as defineTable3 } from "@getstrata/core/database";
1084
+ var notificationTable = defineTable3({
1085
+ name: "notification",
1086
+ primaryKey: "id",
1087
+ columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
1088
+ defaultOrderBy: { column: "created_at", direction: "DESC" }
1089
+ });
1090
+
1091
+ // ../../src/modules/user/notificationService.ts
1092
+ import { NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
1093
+
1094
+ // ../../src/modules/user/oauthIdentityRepository.ts
1095
+ import { BaseRepository as BaseRepository4, defineTable as defineTable4 } from "@getstrata/core/database";
1096
+ var oauthIdentityTable = defineTable4({
1097
+ name: "oauth_identity",
1098
+ primaryKey: "id",
1099
+ columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
1100
+ });
1101
+
1102
+ // ../../src/modules/user/repository.ts
1103
+ import {
1104
+ emailLookupForQuery,
1105
+ protectEmail,
1106
+ revealEmail
1107
+ } from "@getstrata/core/crypto/fieldEncryption";
1108
+ import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
1109
+ import { BaseRepository as BaseRepository5 } from "@getstrata/core/database";
1110
+ import { currentTenantId as currentTenantId3 } from "@getstrata/core/tenant/tenantContext";
1111
+
1112
+ // ../../src/modules/user/table.ts
1113
+ import { defineTable as defineTable5 } from "@getstrata/core/database";
1114
+ var userTable = defineTable5({
1115
+ name: "users",
1116
+ primaryKey: "id",
1117
+ columns: [
1118
+ "id",
1119
+ "name",
1120
+ "email",
1121
+ "email_lookup",
1122
+ "role",
1123
+ "tenant_id",
1124
+ "password_hash",
1125
+ "email_verified_at",
1126
+ "mfa_secret",
1127
+ "mfa_enabled",
1128
+ "created_at",
1129
+ "updated_at"
1130
+ ],
1131
+ defaultOrderBy: { column: "id", direction: "ASC" }
1132
+ });
1133
+
1134
+ // ../../src/modules/user/tokenService.ts
1135
+ import { hashApiToken } from "@getstrata/core/auth/tokenHash";
1136
+ import { ForbiddenError, NotFoundError as NotFoundError3 } from "@getstrata/core/errors/http";
1137
+ import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
1138
+
1139
+ // ../../src/modules/user/provider.ts
1140
+ var userRepositoryToken = "user.repository";
1141
+ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
1142
+
1143
+ // ../../src/modules/scim/service.ts
1144
+ class ScimService {
1145
+ users;
1146
+ organizations;
1147
+ members;
1148
+ constructor(users, organizations, members) {
1149
+ this.users = users;
1150
+ this.organizations = organizations;
1151
+ this.members = members;
1152
+ }
1153
+ serviceProviderConfig() {
1154
+ return {
1155
+ schemas: [SCIM_SCHEMAS.serviceProviderConfig],
1156
+ patch: { supported: true },
1157
+ bulk: { supported: false },
1158
+ filter: { supported: false },
1159
+ changePassword: { supported: false },
1160
+ sort: { supported: false },
1161
+ etag: { supported: true },
1162
+ authenticationSchemes: [
1163
+ {
1164
+ type: "oauthbearertoken",
1165
+ name: "OAuth Bearer Token",
1166
+ description: "SCIM bearer token authentication"
1167
+ }
1168
+ ]
1169
+ };
1170
+ }
1171
+ async listUsers(startIndex = 1, count = 100) {
1172
+ const tenantId = currentTenantId4();
1173
+ const offset = Math.max(startIndex - 1, 0);
1174
+ const records = await this.users.findAll({
1175
+ limit: count,
1176
+ offset,
1177
+ where: { tenant_id: tenantId }
1178
+ });
1179
+ const total = await this.users.countForTenant(tenantId);
1180
+ return {
1181
+ schemas: [SCIM_SCHEMAS.listResponse],
1182
+ totalResults: total,
1183
+ startIndex,
1184
+ itemsPerPage: records.length,
1185
+ Resources: records.map((user) => this.toScimUser(user))
1186
+ };
1187
+ }
1188
+ async getUser(id) {
1189
+ const user = await this.findUserRecord(id);
1190
+ return this.toScimUser(user);
1191
+ }
1192
+ async findUserRecord(id) {
1193
+ const user = await this.users.findById(id);
1194
+ if (!user || user.tenant_id !== currentTenantId4()) {
1195
+ throw new NotFoundError4(`SCIM user ${id} not found.`);
1196
+ }
1197
+ return user;
1198
+ }
1199
+ async createUser(payload) {
1200
+ const email = payload.userName ?? payload.emails?.find((entry) => entry.primary)?.value ?? payload.emails?.[0]?.value;
1201
+ if (!email) {
1202
+ throw new Error("SCIM userName or email is required.");
1203
+ }
1204
+ const passwordHash = await hashPassword(crypto.randomUUID());
1205
+ const user = await this.users.create({
1206
+ name: payload.name?.formatted ?? email.split("@")[0] ?? "SCIM User",
1207
+ email,
1208
+ role: "member",
1209
+ tenant_id: currentTenantId4(),
1210
+ password_hash: passwordHash,
1211
+ created_at: new Date,
1212
+ updated_at: new Date
1213
+ });
1214
+ return this.toScimUser(user);
1215
+ }
1216
+ async patchUser(id, operations) {
1217
+ const user = await this.findUserRecord(id);
1218
+ const changes = { updated_at: new Date };
1219
+ for (const operation of operations) {
1220
+ if (operation.op.toLowerCase() === "replace" && operation.path === "active") {
1221
+ continue;
1222
+ }
1223
+ if (operation.op.toLowerCase() === "replace" && operation.path === "displayName") {
1224
+ changes.name = String(operation.value ?? user.name);
1225
+ }
1226
+ if (operation.op.toLowerCase() === "replace" && (operation.path === "userName" || operation.path === 'emails[type eq "work"].value')) {
1227
+ changes.email = String(operation.value ?? user.email);
1228
+ }
1229
+ }
1230
+ const updated = await this.users.updateByIdOrThrow(id, changes);
1231
+ return this.toScimUser(updated);
1232
+ }
1233
+ async deleteUser(id) {
1234
+ const user = await this.findUserRecord(id);
1235
+ const deleted = await this.users.deleteById(id);
1236
+ if (!deleted) {
1237
+ throw new NotFoundError4(`SCIM user ${id} not found.`);
1238
+ }
1239
+ return { id: user.id, updated_at: user.updated_at };
1240
+ }
1241
+ async listGroups(startIndex = 1, count = 100) {
1242
+ const tenantId = currentTenantId4();
1243
+ const offset = Math.max(startIndex - 1, 0);
1244
+ const rows = await this.organizations.listForTenant({ limit: count, offset, tenantId });
1245
+ const total = await this.organizations.countForTenant(tenantId);
1246
+ const resources = await Promise.all(rows.map((row) => this.toScimGroup(row)));
1247
+ return {
1248
+ schemas: [SCIM_SCHEMAS.listResponse],
1249
+ totalResults: total,
1250
+ startIndex,
1251
+ itemsPerPage: resources.length,
1252
+ Resources: resources
1253
+ };
1254
+ }
1255
+ async getGroup(id) {
1256
+ const organization = await this.findOrganizationRecord(id);
1257
+ return await this.toScimGroup(organization);
1258
+ }
1259
+ async findOrganizationRecord(id) {
1260
+ return await this.organizations.findForTenantOrThrow(id, currentTenantId4());
1261
+ }
1262
+ async patchGroup(id, operations) {
1263
+ for (const operation of operations) {
1264
+ if (operation.op.toLowerCase() !== "add" || operation.path !== "members") {
1265
+ continue;
1266
+ }
1267
+ const members = Array.isArray(operation.value) ? operation.value : operation.value ? [operation.value] : [];
1268
+ for (const member of members) {
1269
+ const userId = Number.parseInt(String(member.value ?? ""), 10);
1270
+ if (Number.isInteger(userId) && userId > 0) {
1271
+ await this.members.addMember({ organizationId: id, userId, role: "member" });
1272
+ }
1273
+ }
1274
+ }
1275
+ const organization = await this.findOrganizationRecord(id);
1276
+ return await this.toScimGroup(organization);
1277
+ }
1278
+ async toScimGroup(organization) {
1279
+ const members = await this.members.listForOrganization(organization.id);
1280
+ return {
1281
+ schemas: [SCIM_SCHEMAS.group],
1282
+ id: String(organization.id),
1283
+ displayName: organization.name,
1284
+ externalId: organization.slug,
1285
+ members: members.map((member) => ({
1286
+ value: String(member.user_id),
1287
+ display: String(member.user_id)
1288
+ })),
1289
+ meta: {
1290
+ resourceType: "Group",
1291
+ lastModified: organization.updated_at.toISOString()
1292
+ }
1293
+ };
1294
+ }
1295
+ toScimUser(user) {
1296
+ return {
1297
+ schemas: [SCIM_SCHEMAS.user],
1298
+ id: String(user.id),
1299
+ userName: user.email,
1300
+ name: { formatted: user.name },
1301
+ displayName: user.name,
1302
+ active: true,
1303
+ emails: [{ value: user.email, primary: true, type: "work" }],
1304
+ roles: [{ value: user.role, primary: true }],
1305
+ meta: {
1306
+ resourceType: "User",
1307
+ created: user.created_at?.toISOString(),
1308
+ lastModified: user.updated_at?.toISOString()
1309
+ }
1310
+ };
1311
+ }
1312
+ }
1313
+ function createScimService(dependencies) {
1314
+ return new ScimService(resolveService(dependencies, userRepositoryToken), new repository_default, new memberRepository_default);
1315
+ }
1316
+
1317
+ // ../../src/modules/scim/controller.ts
1318
+ class ScimController {
1319
+ service;
1320
+ constructor(dependencies, service = createScimService(dependencies)) {
1321
+ this.service = service;
1322
+ }
1323
+ serviceProviderConfig = withErrorHandling(async () => {
1324
+ return scimResponse(this.service.serviceProviderConfig());
1325
+ });
1326
+ listUsers = withErrorHandling(async (request) => {
1327
+ const url = new URL(request.url);
1328
+ const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
1329
+ const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
1330
+ return scimResponse(await this.service.listUsers(startIndex, count));
1331
+ });
1332
+ createUser = withErrorHandling(async (request) => {
1333
+ const payload = await request.json();
1334
+ const user = await this.service.createUser(payload);
1335
+ const id = Number.parseInt(user.id, 10);
1336
+ const record = await this.service.findUserRecord(id);
1337
+ return scimResponse(user, { status: 201, etagSource: record });
1338
+ });
1339
+ showUser = withErrorHandling(async (request) => {
1340
+ const params = request.params;
1341
+ const id = Number.parseInt(params?.id ?? "", 10);
1342
+ const record = await this.service.findUserRecord(id);
1343
+ const user = await this.service.getUser(id);
1344
+ return scimResponse(user, { request, etagSource: record });
1345
+ });
1346
+ patchUser = withErrorHandling(async (request) => {
1347
+ const params = request.params;
1348
+ const id = Number.parseInt(params?.id ?? "", 10);
1349
+ const record = await this.service.findUserRecord(id);
1350
+ assertScimIfMatch(request, record);
1351
+ const body = await request.json();
1352
+ const user = await this.service.patchUser(id, body.Operations ?? []);
1353
+ const updated = await this.service.findUserRecord(id);
1354
+ return scimResponse(user, { etagSource: updated });
1355
+ });
1356
+ deleteUser = withErrorHandling(async (request) => {
1357
+ const params = request.params;
1358
+ const id = Number.parseInt(params?.id ?? "", 10);
1359
+ const record = await this.service.findUserRecord(id);
1360
+ assertScimIfMatch(request, record);
1361
+ await this.service.deleteUser(id);
1362
+ return new Response(null, { status: 204 });
1363
+ });
1364
+ listGroups = withErrorHandling(async (request) => {
1365
+ const url = new URL(request.url);
1366
+ const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
1367
+ const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
1368
+ return scimResponse(await this.service.listGroups(startIndex, count));
1369
+ });
1370
+ showGroup = withErrorHandling(async (request) => {
1371
+ const params = request.params;
1372
+ const id = Number.parseInt(params?.id ?? "", 10);
1373
+ const record = await this.service.findOrganizationRecord(id);
1374
+ const group = await this.service.getGroup(id);
1375
+ return scimResponse(group, { request, etagSource: record });
1376
+ });
1377
+ patchGroup = withErrorHandling(async (request) => {
1378
+ const params = request.params;
1379
+ const id = Number.parseInt(params?.id ?? "", 10);
1380
+ const record = await this.service.findOrganizationRecord(id);
1381
+ assertScimIfMatch(request, record);
1382
+ const body = await request.json();
1383
+ const group = await this.service.patchGroup(id, body.Operations ?? []);
1384
+ const updated = await this.service.findOrganizationRecord(id);
1385
+ return scimResponse(group, { etagSource: updated });
1386
+ });
1387
+ }
1388
+ var controller_default = ScimController;
1389
+
1390
+ // ../../src/bootstrap/scimRoutes.ts
1391
+ function createScimRoutes(dependencies) {
1392
+ const redisUrl = process.env.REDIS_URL?.trim() ?? "";
1393
+ const secured = withMiddleware2(createScimThrottleMiddleware({
1394
+ redisUrl: redisUrl || undefined,
1395
+ maxAttempts: Number(process.env.SCIM_RATE_LIMIT_PER_MINUTE ?? "60"),
1396
+ decaySeconds: 60
1397
+ }), createScimAuthMiddleware());
1398
+ const bind = (method) => {
1399
+ return secured(async (request) => {
1400
+ const controller = new controller_default(dependencies);
1401
+ return await method(controller)(request);
1402
+ });
1403
+ };
1404
+ return {
1405
+ "/scim/v2/ServiceProviderConfig": {
1406
+ GET: bind((controller) => controller.serviceProviderConfig)
1407
+ },
1408
+ "/scim/v2/Users": {
1409
+ GET: bind((controller) => controller.listUsers),
1410
+ POST: bind((controller) => controller.createUser)
1411
+ },
1412
+ "/scim/v2/Users/:id": {
1413
+ GET: bind((controller) => controller.showUser),
1414
+ PATCH: bind((controller) => controller.patchUser),
1415
+ DELETE: bind((controller) => controller.deleteUser)
1416
+ },
1417
+ "/scim/v2/Groups": {
1418
+ GET: bind((controller) => controller.listGroups)
1419
+ },
1420
+ "/scim/v2/Groups/:id": {
1421
+ GET: bind((controller) => controller.showGroup),
1422
+ PATCH: bind((controller) => controller.patchGroup)
1423
+ }
1424
+ };
1425
+ }
1426
+
1427
+ // ../../src/bootstrap/createRoutes.ts
1428
+ function registerRoute2(method, path, middleware) {
1429
+ routeRegistry.register({ method, path, middleware });
1430
+ }
1431
+ function createRoutes(dependencies) {
1432
+ const kernel = createHttpKernel(dependencies);
1433
+ const wrappedModuleRoutes = buildModuleRoutes(dependencies, {
1434
+ apiPrefix: appConfig.apiPrefix,
1435
+ clearRegistry: true
1436
+ });
1437
+ const healthRoutes = createHealthRoutes(dependencies);
1438
+ const metricsRoutes = createMetricsRoutes();
1439
+ const scimRoutes = isFeatureEnabled("scim") ? applyMiddlewareToRoutes3(registerOpenApiRouteMap(createScimRoutes(dependencies), ["global", "scim"]), kernel.globalMiddleware()) : {};
1440
+ registerRoute2("GET", "/health", []);
1441
+ registerRoute2("GET", "/ready", []);
1442
+ registerRoute2("GET", "/metrics", []);
1443
+ const baseRoutes = {
1444
+ ...healthRoutes,
1445
+ ...metricsRoutes,
1446
+ ...scimRoutes,
1447
+ ...isViewsEnabled() || isSpaEnabled() ? {} : { "/": strata_default },
1448
+ ...wrappedModuleRoutes,
1449
+ [`${appConfig.apiPrefix}/*`]: async () => {
1450
+ registerRoute2("GET", `${appConfig.apiPrefix}/*`, ["global", "api"]);
1451
+ return jsonResponse4({ error: "Not Found" }, { status: 404 });
1452
+ },
1453
+ "/*": async () => {
1454
+ registerRoute2("GET", "/*", []);
1455
+ if (isViewsEnabled()) {
1456
+ return htmlResponse2("Not Found", { status: 404 });
1457
+ }
1458
+ if (isSpaEnabled()) {
1459
+ return jsonResponse4({ error: "Not Found" }, { status: 404 });
1460
+ }
1461
+ return jsonResponse4({ error: "Not Found" }, { status: 404 });
1462
+ }
1463
+ };
1464
+ return mergeSpaRoutes(dependencies, mergeWebRoutes(dependencies, baseRoutes));
1465
+ }
1466
+ export {
1467
+ createRoutes
1468
+ };