@getstrata/bootstrap 0.2.62 → 0.2.63

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/bootstrap changelog
2
2
 
3
+ ## 0.2.63
4
+
5
+ - `HttpKernel.wrapWebGuest()` `home` accepts `string | ((user) => string | Promise<string>)` so signed-in guest redirects can follow the current team. Default remains `/organizations`.
6
+
3
7
  ## 0.2.62
4
8
 
5
9
  - `HttpKernel.wrapWebPasswordConfirm()` is Laravel `password.confirm` for HTML routes. Peer `@getstrata/core` `^0.5.75`.
package/README.md CHANGED
@@ -32,7 +32,7 @@ import { createHttpKernel, createAppContext, coreProviders } from "@getstrata/bo
32
32
 
33
33
  Sibling HTMX apps should bind `createCookieSessionAuthManager` from `@getstrata/bootstrap/web/session` instead of HMAC `SessionGuard`. Use `signIn` / `signOut` (or the redirect helpers) instead of calling `CookieSessionStore` from controllers. Pass `mapUser` to map roles in the app. Pass `loadSessionUser` when the default `learn_subscriber` / `is_admin` SELECT does not match your schema. WorkHub’s table is `sessions` (`0031_create_sessions`); its loader is `loadWorkhubSessionUser` (maps `users.role`, decrypts email). WorkHub web login itself stays on HMAC `SessionGuard`.
34
34
 
35
- `wrapWeb` applies the web group (CSRF + flash) and `withErrorHandling`, so CSRF `ForbiddenError` becomes an HTML 403 in `FRONTEND_MODE=server-htmx`. `wrapWebGuest` is Laravel `guest` / `RedirectIfAuthenticated` (signed-in users go to `/organizations` by default). `wrapWebLogin` / `wrapWebRegister` include that web group plus throttle — do not wrap them with `wrapWeb` again. The throttle callback should return an HTML form at 429 (WorkHub’s `/login` and `/register` do).
35
+ `wrapWeb` applies the web group (CSRF + flash) and `withErrorHandling`, so CSRF `ForbiddenError` becomes an HTML 403 in `FRONTEND_MODE=server-htmx`. `wrapWebGuest` is Laravel `guest` / `RedirectIfAuthenticated` (signed-in users go to `/organizations` by default; pass a string or `(user) => path` to override). `wrapWebLogin` / `wrapWebRegister` include that web group plus throttle — do not wrap them with `wrapWeb` again. The throttle callback should return an HTML form at 429 (WorkHub’s `/login` and `/register` do).
36
36
 
37
37
  These subpaths remain WorkHub-oriented and are not a generic starter API: `@getstrata/bootstrap/createRoutes` (includes SCIM), `@getstrata/bootstrap/schedule`, and `@getstrata/bootstrap/createWebRoutes` (redirects `/` to `/organizations`).
38
38
 
@@ -1,7 +1,9 @@
1
+ import type { AuthUser } from "@getstrata/core/auth/authContext";
1
2
  import type { Policy } from "@getstrata/core/auth/policy";
2
3
  import type { Middleware, RouteHandler } from "@getstrata/core/http/middleware";
3
4
  import type { AppDependencies } from "./contracts";
4
5
  type MiddlewareGroupName = "api" | "authenticated" | "web";
6
+ type WebGuestHome = string | ((user: AuthUser) => string | Promise<string>);
5
7
  declare class HttpKernel {
6
8
  private readonly dependencies;
7
9
  constructor(dependencies: AppDependencies);
@@ -11,7 +13,7 @@ declare class HttpKernel {
11
13
  wrapApi(handler: RouteHandler): RouteHandler;
12
14
  wrapWeb(handler: RouteHandler): RouteHandler;
13
15
  /** Laravel `guest` / `RedirectIfAuthenticated` — signed-in users go to `home`. */
14
- wrapWebGuest(handler: RouteHandler, home?: string): RouteHandler;
16
+ wrapWebGuest(handler: RouteHandler, home?: WebGuestHome): RouteHandler;
15
17
  wrapWebPublicRead(handler: RouteHandler): RouteHandler;
16
18
  wrapWebAuthenticated(handler: RouteHandler): RouteHandler;
17
19
  /** Signed-in HTML without Laravel `verified` (logout, verification notice). */
@@ -36,5 +38,5 @@ declare class HttpKernel {
36
38
  private wrapThrottle;
37
39
  }
38
40
  declare function createHttpKernel(dependencies: AppDependencies): HttpKernel;
39
- export type { MiddlewareGroupName };
41
+ export type { MiddlewareGroupName, WebGuestHome };
40
42
  export { createHttpKernel, HttpKernel };
@@ -189,7 +189,8 @@ class HttpKernel {
189
189
  if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
190
190
  return Response.redirect("/email/verify", 302);
191
191
  }
192
- return Response.redirect(home, 302);
192
+ const location = typeof home === "function" ? await home(user) : home;
193
+ return Response.redirect(location, 302);
193
194
  }
194
195
  return handler(request);
195
196
  });
@@ -192,7 +192,8 @@ class HttpKernel {
192
192
  if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
193
193
  return Response.redirect("/email/verify", 302);
194
194
  }
195
- return Response.redirect(home, 302);
195
+ const location = typeof home === "function" ? await home(user) : home;
196
+ return Response.redirect(location, 302);
196
197
  }
197
198
  return handler(request);
198
199
  });
@@ -475,15 +475,160 @@ var storageProvider = {
475
475
  var storage_default = storageProvider;
476
476
 
477
477
  // ../../src/bootstrap/providers/view.ts
478
+ import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
478
479
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
480
+ import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
479
481
  import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
480
482
  import {
481
483
  configureWebErrorView,
484
+ configureWebLayoutData,
482
485
  DEFAULT_VIEWS_DIRECTORY,
483
486
  EtaViewEngine,
484
487
  errorTemplateName,
485
488
  resolveWebLayoutData
486
489
  } from "@getstrata/core/view";
490
+
491
+ // ../../src/modules/organization/repository.ts
492
+ import { BaseRepository } from "@getstrata/core/database/baseRepository";
493
+ import { NotFoundError } from "@getstrata/core/errors/http";
494
+ import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
495
+
496
+ // ../../src/modules/organization/table.ts
497
+ import { defineTable } from "@getstrata/core/database/table";
498
+
499
+ // ../../src/domain/workhub.ts
500
+ var ORGANIZATION_TABLE = "organization";
501
+
502
+ // ../../src/modules/organization/table.ts
503
+ var organizationTable = defineTable({
504
+ name: ORGANIZATION_TABLE,
505
+ primaryKey: "id",
506
+ columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
507
+ softDeletes: true,
508
+ defaultOrderBy: { column: "id", direction: "ASC" }
509
+ });
510
+
511
+ // ../../src/modules/organization/repository.ts
512
+ class OrganizationRepository extends BaseRepository {
513
+ constructor() {
514
+ super(organizationTable);
515
+ }
516
+ async findBySlug(slug) {
517
+ return await this.firstOrNull({ slug });
518
+ }
519
+ async listForTenant(options) {
520
+ return await this.findAll({
521
+ limit: options.limit,
522
+ offset: options.offset,
523
+ where: { tenant_id: options.tenantId ?? currentTenantId() }
524
+ });
525
+ }
526
+ async countForTenant(tenantId = currentTenantId()) {
527
+ return await this.countWhere({ tenant_id: tenantId });
528
+ }
529
+ async findForTenantOrThrow(id, tenantId = currentTenantId()) {
530
+ const organization = await this.findById(id);
531
+ if (!organization || organization.tenant_id !== tenantId) {
532
+ throw new NotFoundError(`SCIM group ${id} not found.`);
533
+ }
534
+ return organization;
535
+ }
536
+ }
537
+ var repository_default = OrganizationRepository;
538
+
539
+ // ../../src/modules/user/repository.ts
540
+ import {
541
+ emailLookupForQuery,
542
+ protectEmail,
543
+ revealEmail
544
+ } from "@getstrata/core/crypto/fieldEncryption";
545
+ import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
546
+ import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
547
+ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
548
+
549
+ // ../../src/modules/user/table.ts
550
+ import { defineTable as defineTable2 } from "@getstrata/core/database/table";
551
+ var userTable = defineTable2({
552
+ name: "users",
553
+ primaryKey: "id",
554
+ columns: [
555
+ "id",
556
+ "name",
557
+ "email",
558
+ "email_lookup",
559
+ "role",
560
+ "tenant_id",
561
+ "password_hash",
562
+ "email_verified_at",
563
+ "mfa_secret",
564
+ "mfa_enabled",
565
+ "mfa_recovery_codes",
566
+ "profile_photo_path",
567
+ "session_valid_after",
568
+ "current_organization_id",
569
+ "created_at",
570
+ "updated_at"
571
+ ],
572
+ defaultOrderBy: { column: "id", direction: "ASC" }
573
+ });
574
+
575
+ // ../../src/modules/user/repository.ts
576
+ class UserRepository extends BaseRepository2 {
577
+ constructor() {
578
+ super(userTable);
579
+ }
580
+ decode(record) {
581
+ return {
582
+ ...record,
583
+ email: revealEmail(record.email),
584
+ mfa_secret: revealMfaSecret(record.mfa_secret)
585
+ };
586
+ }
587
+ async findById(id) {
588
+ const record = await super.findById(id);
589
+ return record ? this.decode(record) : null;
590
+ }
591
+ async findAll(options = {}) {
592
+ const records = await super.findAll(options);
593
+ return records.map((record) => this.decode(record));
594
+ }
595
+ async create(values) {
596
+ const email = values.email;
597
+ if (!email) {
598
+ throw new Error("Email is required.");
599
+ }
600
+ const protectedEmail = protectEmail(email);
601
+ const record = await super.create({
602
+ ...values,
603
+ tenant_id: values.tenant_id ?? currentTenantId2(),
604
+ email: protectedEmail.storedEmail,
605
+ email_lookup: protectedEmail.emailLookup,
606
+ password_hash: values.password_hash ?? ""
607
+ });
608
+ return this.decode(record);
609
+ }
610
+ async updateByIdOrThrow(id, values, errorFactory) {
611
+ const changes = { ...values };
612
+ if (values.email !== undefined) {
613
+ const protectedEmail = protectEmail(values.email);
614
+ changes.email = protectedEmail.storedEmail;
615
+ changes.email_lookup = protectedEmail.emailLookup;
616
+ }
617
+ const record = await super.updateByIdOrThrow(id, changes, errorFactory);
618
+ return this.decode(record);
619
+ }
620
+ async findByEmail(email) {
621
+ const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
622
+ const record = records[0];
623
+ return record ? this.decode(record) : null;
624
+ }
625
+ async countForTenant(tenantId = currentTenantId2()) {
626
+ return await this.countWhere({ tenant_id: tenantId });
627
+ }
628
+ }
629
+ var repository_default2 = UserRepository;
630
+
631
+ // ../../src/bootstrap/providers/view.ts
487
632
  var CORE_VIEW_TOKEN = "core.view";
488
633
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
489
634
  var viewProvider = {
@@ -496,6 +641,25 @@ var viewProvider = {
496
641
  config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
497
642
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
498
643
  container.set(CORE_VIEW_TOKEN, engine);
644
+ configureWebLayoutData({
645
+ extra: async (user) => {
646
+ const appName = appDisplayName();
647
+ if (!user || typeof user.id !== "number") {
648
+ return { appName, currentOrganization: null, organizations: [] };
649
+ }
650
+ try {
651
+ const record = await new repository_default2().findByIdOrThrow(user.id);
652
+ const memberships = await resolveMembershipLookup().listForUser(record.id);
653
+ const organizationsRepo = new repository_default;
654
+ const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
655
+ const currentId = record.current_organization_id ?? null;
656
+ const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
657
+ return { appName, currentOrganization, organizations };
658
+ } catch {
659
+ return { appName, currentOrganization: null, organizations: [] };
660
+ }
661
+ }
662
+ });
499
663
  configureWebErrorView({
500
664
  render: async (input) => engine.render(errorTemplateName(input.status), {
501
665
  title: input.title,
@@ -229,7 +229,8 @@ class HttpKernel {
229
229
  if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
230
230
  return Response.redirect("/email/verify", 302);
231
231
  }
232
- return Response.redirect(home, 302);
232
+ const location = typeof home === "function" ? await home(user) : home;
233
+ return Response.redirect(location, 302);
233
234
  }
234
235
  return handler(request);
235
236
  });
@@ -797,7 +798,7 @@ function assertScimIfMatch(request, etagSource) {
797
798
  // ../../src/modules/scim/service.ts
798
799
  import { hashPassword as hashPassword3 } from "@getstrata/core/auth/password";
799
800
  import { resolveService } from "@getstrata/core/contracts/di";
800
- import { NotFoundError as NotFoundError5 } from "@getstrata/core/errors/http";
801
+ import { NotFoundError as NotFoundError7 } from "@getstrata/core/errors/http";
801
802
  import { currentTenantId as currentTenantId4 } from "@getstrata/core/tenant/tenantContext";
802
803
 
803
804
  // ../../src/domain/scim.ts
@@ -1050,6 +1051,7 @@ import {
1050
1051
  import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
1051
1052
  import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
1052
1053
  import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
1054
+ import { getRequiredDependency } from "@getstrata/core/contracts/di";
1053
1055
 
1054
1056
  // ../../src/modules/user/apiTokenRepository.ts
1055
1057
  import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
@@ -1073,9 +1075,16 @@ var apiTokenTable = defineTable2({
1073
1075
  });
1074
1076
 
1075
1077
  // ../../src/modules/user/authService.ts
1078
+ import { currentAuthUser } from "@getstrata/core/auth/authContext";
1076
1079
  import { hashPassword, verifyPassword } from "@getstrata/core/auth/password";
1080
+ import { normalizeEmail } from "@getstrata/core/crypto/fieldEncryption";
1077
1081
  import { protectMfaSecret, revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
1078
- import { UnauthorizedError, ValidationError } from "@getstrata/core/errors/http";
1082
+ import { UnauthorizedError as UnauthorizedError2, ValidationError } from "@getstrata/core/errors/http";
1083
+ import {
1084
+ generateRecoveryCodes,
1085
+ hashRecoveryCode,
1086
+ recoveryCodeMatches
1087
+ } from "@getstrata/core/security/recoveryCodes";
1079
1088
  import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
1080
1089
  import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
1081
1090
  import { buildOtpauthUrl, generateTotpSecret, verifyTotp } from "@getstrata/core/security/totp";
@@ -1084,6 +1093,7 @@ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tena
1084
1093
  // ../../src/core/auth/abilityCatalog.ts
1085
1094
  var MEMBER_ABILITIES = [
1086
1095
  "organizations:read",
1096
+ "organizations:create",
1087
1097
  "projects:read",
1088
1098
  "projects:create",
1089
1099
  "tasks:read",
@@ -1093,7 +1103,8 @@ var MEMBER_ABILITIES = [
1093
1103
  "attachments:read",
1094
1104
  "attachments:create",
1095
1105
  "auth:tokens:read",
1096
- "auth:tokens:write"
1106
+ "auth:tokens:write",
1107
+ "auth:tokens:delete"
1097
1108
  ];
1098
1109
  var ADMIN_ABILITIES = [
1099
1110
  ...MEMBER_ABILITIES,
@@ -1111,6 +1122,14 @@ var ADMIN_ABILITIES = [
1111
1122
  "webhooks:write",
1112
1123
  "audit:read"
1113
1124
  ];
1125
+ // ../../src/modules/user/mfaRequiredError.ts
1126
+ import { UnauthorizedError } from "@getstrata/core/errors/http";
1127
+
1128
+ // ../../src/modules/user/currentOrganizationService.ts
1129
+ import { assertResourceInCurrentTenant } from "@getstrata/core/auth/membershipScope";
1130
+ import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
1131
+ import { NotFoundError as NotFoundError3 } from "@getstrata/core/errors/http";
1132
+
1114
1133
  // ../../src/modules/user/notificationRepository.ts
1115
1134
  import { BaseRepository as BaseRepository3 } from "@getstrata/core/database/baseRepository";
1116
1135
 
@@ -1124,7 +1143,7 @@ var notificationTable = defineTable3({
1124
1143
  });
1125
1144
 
1126
1145
  // ../../src/modules/user/notificationService.ts
1127
- import { NotFoundError as NotFoundError3 } from "@getstrata/core/errors/http";
1146
+ import { NotFoundError as NotFoundError4 } from "@getstrata/core/errors/http";
1128
1147
 
1129
1148
  // ../../src/modules/user/oauthIdentityRepository.ts
1130
1149
  import { BaseRepository as BaseRepository4 } from "@getstrata/core/database/baseRepository";
@@ -1143,11 +1162,16 @@ import { ValidationError as ValidationError2 } from "@getstrata/core/errors/http
1143
1162
  import { absoluteTemporarySignedUrl } from "@getstrata/core/http/signedUrl";
1144
1163
  import { mailer } from "@getstrata/core/mail/mailer";
1145
1164
  import { sendMarkdownMail } from "@getstrata/core/mail/markdownMailable";
1165
+ import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
1146
1166
  import { logSecurityEvent as logSecurityEvent2 } from "@getstrata/core/security/securityEvents";
1147
1167
  import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
1148
1168
  var RESET_TTL_SECONDS = 60 * 60;
1149
1169
  var VERIFY_TTL_SECONDS = 60 * 60 * 24;
1150
1170
 
1171
+ // ../../src/modules/user/profilePhotoService.ts
1172
+ import { BadRequestError, NotFoundError as NotFoundError5 } from "@getstrata/core/errors/http";
1173
+ import { isImageMimeType, resizeImageContents } from "@getstrata/core/media/imageTransform";
1174
+
1151
1175
  // ../../src/modules/user/repository.ts
1152
1176
  import {
1153
1177
  emailLookupForQuery,
@@ -1174,15 +1198,76 @@ var userTable = defineTable5({
1174
1198
  "email_verified_at",
1175
1199
  "mfa_secret",
1176
1200
  "mfa_enabled",
1201
+ "mfa_recovery_codes",
1202
+ "profile_photo_path",
1203
+ "session_valid_after",
1204
+ "current_organization_id",
1177
1205
  "created_at",
1178
1206
  "updated_at"
1179
1207
  ],
1180
1208
  defaultOrderBy: { column: "id", direction: "ASC" }
1181
1209
  });
1182
1210
 
1211
+ // ../../src/modules/user/repository.ts
1212
+ class UserRepository extends BaseRepository5 {
1213
+ constructor() {
1214
+ super(userTable);
1215
+ }
1216
+ decode(record) {
1217
+ return {
1218
+ ...record,
1219
+ email: revealEmail(record.email),
1220
+ mfa_secret: revealMfaSecret2(record.mfa_secret)
1221
+ };
1222
+ }
1223
+ async findById(id) {
1224
+ const record = await super.findById(id);
1225
+ return record ? this.decode(record) : null;
1226
+ }
1227
+ async findAll(options = {}) {
1228
+ const records = await super.findAll(options);
1229
+ return records.map((record) => this.decode(record));
1230
+ }
1231
+ async create(values) {
1232
+ const email = values.email;
1233
+ if (!email) {
1234
+ throw new Error("Email is required.");
1235
+ }
1236
+ const protectedEmail = protectEmail(email);
1237
+ const record = await super.create({
1238
+ ...values,
1239
+ tenant_id: values.tenant_id ?? currentTenantId3(),
1240
+ email: protectedEmail.storedEmail,
1241
+ email_lookup: protectedEmail.emailLookup,
1242
+ password_hash: values.password_hash ?? ""
1243
+ });
1244
+ return this.decode(record);
1245
+ }
1246
+ async updateByIdOrThrow(id, values, errorFactory) {
1247
+ const changes = { ...values };
1248
+ if (values.email !== undefined) {
1249
+ const protectedEmail = protectEmail(values.email);
1250
+ changes.email = protectedEmail.storedEmail;
1251
+ changes.email_lookup = protectedEmail.emailLookup;
1252
+ }
1253
+ const record = await super.updateByIdOrThrow(id, changes, errorFactory);
1254
+ return this.decode(record);
1255
+ }
1256
+ async findByEmail(email) {
1257
+ const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
1258
+ const record = records[0];
1259
+ return record ? this.decode(record) : null;
1260
+ }
1261
+ async countForTenant(tenantId = currentTenantId3()) {
1262
+ return await this.countWhere({ tenant_id: tenantId });
1263
+ }
1264
+ }
1265
+ var repository_default2 = UserRepository;
1266
+
1183
1267
  // ../../src/modules/user/tokenService.ts
1268
+ import { ADMIN_ABILITIES as ADMIN_ABILITIES2 } from "@getstrata/core/auth/abilityCatalog";
1184
1269
  import { hashApiToken as hashApiToken2 } from "@getstrata/core/auth/tokenHash";
1185
- import { ForbiddenError, NotFoundError as NotFoundError4 } from "@getstrata/core/errors/http";
1270
+ import { ForbiddenError, NotFoundError as NotFoundError6, ValidationError as ValidationError3 } from "@getstrata/core/errors/http";
1186
1271
  import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
1187
1272
 
1188
1273
  // ../../src/modules/user/provider.ts
@@ -1240,7 +1325,7 @@ class ScimService {
1240
1325
  async findUserRecord(id) {
1241
1326
  const user = await this.users.findById(id);
1242
1327
  if (!user || user.tenant_id !== currentTenantId4()) {
1243
- throw new NotFoundError5(`SCIM user ${id} not found.`);
1328
+ throw new NotFoundError7(`SCIM user ${id} not found.`);
1244
1329
  }
1245
1330
  return user;
1246
1331
  }
@@ -1282,7 +1367,7 @@ class ScimService {
1282
1367
  const user = await this.findUserRecord(id);
1283
1368
  const deleted = await this.users.deleteById(id);
1284
1369
  if (!deleted) {
1285
- throw new NotFoundError5(`SCIM user ${id} not found.`);
1370
+ throw new NotFoundError7(`SCIM user ${id} not found.`);
1286
1371
  }
1287
1372
  return { id: user.id, updated_at: user.updated_at };
1288
1373
  }
@@ -197,7 +197,8 @@ class HttpKernel {
197
197
  if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
198
198
  return Response.redirect("/email/verify", 302);
199
199
  }
200
- return Response.redirect(home, 302);
200
+ const location = typeof home === "function" ? await home(user) : home;
201
+ return Response.redirect(location, 302);
201
202
  }
202
203
  return handler(request);
203
204
  });
@@ -475,15 +475,160 @@ var storageProvider = {
475
475
  var storage_default = storageProvider;
476
476
 
477
477
  // ../../src/bootstrap/providers/view.ts
478
+ import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
478
479
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
480
+ import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
479
481
  import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
480
482
  import {
481
483
  configureWebErrorView,
484
+ configureWebLayoutData,
482
485
  DEFAULT_VIEWS_DIRECTORY,
483
486
  EtaViewEngine,
484
487
  errorTemplateName,
485
488
  resolveWebLayoutData
486
489
  } from "@getstrata/core/view";
490
+
491
+ // ../../src/modules/organization/repository.ts
492
+ import { BaseRepository } from "@getstrata/core/database/baseRepository";
493
+ import { NotFoundError } from "@getstrata/core/errors/http";
494
+ import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
495
+
496
+ // ../../src/modules/organization/table.ts
497
+ import { defineTable } from "@getstrata/core/database/table";
498
+
499
+ // ../../src/domain/workhub.ts
500
+ var ORGANIZATION_TABLE = "organization";
501
+
502
+ // ../../src/modules/organization/table.ts
503
+ var organizationTable = defineTable({
504
+ name: ORGANIZATION_TABLE,
505
+ primaryKey: "id",
506
+ columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
507
+ softDeletes: true,
508
+ defaultOrderBy: { column: "id", direction: "ASC" }
509
+ });
510
+
511
+ // ../../src/modules/organization/repository.ts
512
+ class OrganizationRepository extends BaseRepository {
513
+ constructor() {
514
+ super(organizationTable);
515
+ }
516
+ async findBySlug(slug) {
517
+ return await this.firstOrNull({ slug });
518
+ }
519
+ async listForTenant(options) {
520
+ return await this.findAll({
521
+ limit: options.limit,
522
+ offset: options.offset,
523
+ where: { tenant_id: options.tenantId ?? currentTenantId() }
524
+ });
525
+ }
526
+ async countForTenant(tenantId = currentTenantId()) {
527
+ return await this.countWhere({ tenant_id: tenantId });
528
+ }
529
+ async findForTenantOrThrow(id, tenantId = currentTenantId()) {
530
+ const organization = await this.findById(id);
531
+ if (!organization || organization.tenant_id !== tenantId) {
532
+ throw new NotFoundError(`SCIM group ${id} not found.`);
533
+ }
534
+ return organization;
535
+ }
536
+ }
537
+ var repository_default = OrganizationRepository;
538
+
539
+ // ../../src/modules/user/repository.ts
540
+ import {
541
+ emailLookupForQuery,
542
+ protectEmail,
543
+ revealEmail
544
+ } from "@getstrata/core/crypto/fieldEncryption";
545
+ import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
546
+ import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
547
+ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
548
+
549
+ // ../../src/modules/user/table.ts
550
+ import { defineTable as defineTable2 } from "@getstrata/core/database/table";
551
+ var userTable = defineTable2({
552
+ name: "users",
553
+ primaryKey: "id",
554
+ columns: [
555
+ "id",
556
+ "name",
557
+ "email",
558
+ "email_lookup",
559
+ "role",
560
+ "tenant_id",
561
+ "password_hash",
562
+ "email_verified_at",
563
+ "mfa_secret",
564
+ "mfa_enabled",
565
+ "mfa_recovery_codes",
566
+ "profile_photo_path",
567
+ "session_valid_after",
568
+ "current_organization_id",
569
+ "created_at",
570
+ "updated_at"
571
+ ],
572
+ defaultOrderBy: { column: "id", direction: "ASC" }
573
+ });
574
+
575
+ // ../../src/modules/user/repository.ts
576
+ class UserRepository extends BaseRepository2 {
577
+ constructor() {
578
+ super(userTable);
579
+ }
580
+ decode(record) {
581
+ return {
582
+ ...record,
583
+ email: revealEmail(record.email),
584
+ mfa_secret: revealMfaSecret(record.mfa_secret)
585
+ };
586
+ }
587
+ async findById(id) {
588
+ const record = await super.findById(id);
589
+ return record ? this.decode(record) : null;
590
+ }
591
+ async findAll(options = {}) {
592
+ const records = await super.findAll(options);
593
+ return records.map((record) => this.decode(record));
594
+ }
595
+ async create(values) {
596
+ const email = values.email;
597
+ if (!email) {
598
+ throw new Error("Email is required.");
599
+ }
600
+ const protectedEmail = protectEmail(email);
601
+ const record = await super.create({
602
+ ...values,
603
+ tenant_id: values.tenant_id ?? currentTenantId2(),
604
+ email: protectedEmail.storedEmail,
605
+ email_lookup: protectedEmail.emailLookup,
606
+ password_hash: values.password_hash ?? ""
607
+ });
608
+ return this.decode(record);
609
+ }
610
+ async updateByIdOrThrow(id, values, errorFactory) {
611
+ const changes = { ...values };
612
+ if (values.email !== undefined) {
613
+ const protectedEmail = protectEmail(values.email);
614
+ changes.email = protectedEmail.storedEmail;
615
+ changes.email_lookup = protectedEmail.emailLookup;
616
+ }
617
+ const record = await super.updateByIdOrThrow(id, changes, errorFactory);
618
+ return this.decode(record);
619
+ }
620
+ async findByEmail(email) {
621
+ const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
622
+ const record = records[0];
623
+ return record ? this.decode(record) : null;
624
+ }
625
+ async countForTenant(tenantId = currentTenantId2()) {
626
+ return await this.countWhere({ tenant_id: tenantId });
627
+ }
628
+ }
629
+ var repository_default2 = UserRepository;
630
+
631
+ // ../../src/bootstrap/providers/view.ts
487
632
  var CORE_VIEW_TOKEN = "core.view";
488
633
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
489
634
  var viewProvider = {
@@ -496,6 +641,25 @@ var viewProvider = {
496
641
  config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
497
642
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
498
643
  container.set(CORE_VIEW_TOKEN, engine);
644
+ configureWebLayoutData({
645
+ extra: async (user) => {
646
+ const appName = appDisplayName();
647
+ if (!user || typeof user.id !== "number") {
648
+ return { appName, currentOrganization: null, organizations: [] };
649
+ }
650
+ try {
651
+ const record = await new repository_default2().findByIdOrThrow(user.id);
652
+ const memberships = await resolveMembershipLookup().listForUser(record.id);
653
+ const organizationsRepo = new repository_default;
654
+ const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
655
+ const currentId = record.current_organization_id ?? null;
656
+ const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
657
+ return { appName, currentOrganization, organizations };
658
+ } catch {
659
+ return { appName, currentOrganization: null, organizations: [] };
660
+ }
661
+ }
662
+ });
499
663
  configureWebErrorView({
500
664
  render: async (input) => engine.render(errorTemplateName(input.status), {
501
665
  title: input.title,
@@ -185,7 +185,8 @@ class HttpKernel {
185
185
  if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
186
186
  return Response.redirect("/email/verify", 302);
187
187
  }
188
- return Response.redirect(home, 302);
188
+ const location = typeof home === "function" ? await home(user) : home;
189
+ return Response.redirect(location, 302);
189
190
  }
190
191
  return handler(request);
191
192
  });
@@ -2,15 +2,160 @@
2
2
  var __jsonParse = (a) => JSON.parse(a);
3
3
 
4
4
  // ../../src/bootstrap/providers/view.ts
5
+ import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
5
6
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
7
+ import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
6
8
  import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
7
9
  import {
8
10
  configureWebErrorView,
11
+ configureWebLayoutData,
9
12
  DEFAULT_VIEWS_DIRECTORY,
10
13
  EtaViewEngine,
11
14
  errorTemplateName,
12
15
  resolveWebLayoutData
13
16
  } from "@getstrata/core/view";
17
+
18
+ // ../../src/modules/organization/repository.ts
19
+ import { BaseRepository } from "@getstrata/core/database/baseRepository";
20
+ import { NotFoundError } from "@getstrata/core/errors/http";
21
+ import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
22
+
23
+ // ../../src/modules/organization/table.ts
24
+ import { defineTable } from "@getstrata/core/database/table";
25
+
26
+ // ../../src/domain/workhub.ts
27
+ var ORGANIZATION_TABLE = "organization";
28
+
29
+ // ../../src/modules/organization/table.ts
30
+ var organizationTable = defineTable({
31
+ name: ORGANIZATION_TABLE,
32
+ primaryKey: "id",
33
+ columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
34
+ softDeletes: true,
35
+ defaultOrderBy: { column: "id", direction: "ASC" }
36
+ });
37
+
38
+ // ../../src/modules/organization/repository.ts
39
+ class OrganizationRepository extends BaseRepository {
40
+ constructor() {
41
+ super(organizationTable);
42
+ }
43
+ async findBySlug(slug) {
44
+ return await this.firstOrNull({ slug });
45
+ }
46
+ async listForTenant(options) {
47
+ return await this.findAll({
48
+ limit: options.limit,
49
+ offset: options.offset,
50
+ where: { tenant_id: options.tenantId ?? currentTenantId() }
51
+ });
52
+ }
53
+ async countForTenant(tenantId = currentTenantId()) {
54
+ return await this.countWhere({ tenant_id: tenantId });
55
+ }
56
+ async findForTenantOrThrow(id, tenantId = currentTenantId()) {
57
+ const organization = await this.findById(id);
58
+ if (!organization || organization.tenant_id !== tenantId) {
59
+ throw new NotFoundError(`SCIM group ${id} not found.`);
60
+ }
61
+ return organization;
62
+ }
63
+ }
64
+ var repository_default = OrganizationRepository;
65
+
66
+ // ../../src/modules/user/repository.ts
67
+ import {
68
+ emailLookupForQuery,
69
+ protectEmail,
70
+ revealEmail
71
+ } from "@getstrata/core/crypto/fieldEncryption";
72
+ import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
73
+ import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
74
+ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
75
+
76
+ // ../../src/modules/user/table.ts
77
+ import { defineTable as defineTable2 } from "@getstrata/core/database/table";
78
+ var userTable = defineTable2({
79
+ name: "users",
80
+ primaryKey: "id",
81
+ columns: [
82
+ "id",
83
+ "name",
84
+ "email",
85
+ "email_lookup",
86
+ "role",
87
+ "tenant_id",
88
+ "password_hash",
89
+ "email_verified_at",
90
+ "mfa_secret",
91
+ "mfa_enabled",
92
+ "mfa_recovery_codes",
93
+ "profile_photo_path",
94
+ "session_valid_after",
95
+ "current_organization_id",
96
+ "created_at",
97
+ "updated_at"
98
+ ],
99
+ defaultOrderBy: { column: "id", direction: "ASC" }
100
+ });
101
+
102
+ // ../../src/modules/user/repository.ts
103
+ class UserRepository extends BaseRepository2 {
104
+ constructor() {
105
+ super(userTable);
106
+ }
107
+ decode(record) {
108
+ return {
109
+ ...record,
110
+ email: revealEmail(record.email),
111
+ mfa_secret: revealMfaSecret(record.mfa_secret)
112
+ };
113
+ }
114
+ async findById(id) {
115
+ const record = await super.findById(id);
116
+ return record ? this.decode(record) : null;
117
+ }
118
+ async findAll(options = {}) {
119
+ const records = await super.findAll(options);
120
+ return records.map((record) => this.decode(record));
121
+ }
122
+ async create(values) {
123
+ const email = values.email;
124
+ if (!email) {
125
+ throw new Error("Email is required.");
126
+ }
127
+ const protectedEmail = protectEmail(email);
128
+ const record = await super.create({
129
+ ...values,
130
+ tenant_id: values.tenant_id ?? currentTenantId2(),
131
+ email: protectedEmail.storedEmail,
132
+ email_lookup: protectedEmail.emailLookup,
133
+ password_hash: values.password_hash ?? ""
134
+ });
135
+ return this.decode(record);
136
+ }
137
+ async updateByIdOrThrow(id, values, errorFactory) {
138
+ const changes = { ...values };
139
+ if (values.email !== undefined) {
140
+ const protectedEmail = protectEmail(values.email);
141
+ changes.email = protectedEmail.storedEmail;
142
+ changes.email_lookup = protectedEmail.emailLookup;
143
+ }
144
+ const record = await super.updateByIdOrThrow(id, changes, errorFactory);
145
+ return this.decode(record);
146
+ }
147
+ async findByEmail(email) {
148
+ const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
149
+ const record = records[0];
150
+ return record ? this.decode(record) : null;
151
+ }
152
+ async countForTenant(tenantId = currentTenantId2()) {
153
+ return await this.countWhere({ tenant_id: tenantId });
154
+ }
155
+ }
156
+ var repository_default2 = UserRepository;
157
+
158
+ // ../../src/bootstrap/providers/view.ts
14
159
  var CORE_VIEW_TOKEN = "core.view";
15
160
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
16
161
  var viewProvider = {
@@ -23,6 +168,25 @@ var viewProvider = {
23
168
  config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
24
169
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
25
170
  container.set(CORE_VIEW_TOKEN, engine);
171
+ configureWebLayoutData({
172
+ extra: async (user) => {
173
+ const appName = appDisplayName();
174
+ if (!user || typeof user.id !== "number") {
175
+ return { appName, currentOrganization: null, organizations: [] };
176
+ }
177
+ try {
178
+ const record = await new repository_default2().findByIdOrThrow(user.id);
179
+ const memberships = await resolveMembershipLookup().listForUser(record.id);
180
+ const organizationsRepo = new repository_default;
181
+ const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
182
+ const currentId = record.current_organization_id ?? null;
183
+ const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
184
+ return { appName, currentOrganization, organizations };
185
+ } catch {
186
+ return { appName, currentOrganization: null, organizations: [] };
187
+ }
188
+ }
189
+ });
26
190
  configureWebErrorView({
27
191
  render: async (input) => engine.render(errorTemplateName(input.status), {
28
192
  title: input.title,
@@ -465,15 +465,160 @@ var storageProvider = {
465
465
  var storage_default = storageProvider;
466
466
 
467
467
  // ../../src/bootstrap/providers/view.ts
468
+ import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
468
469
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
470
+ import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
469
471
  import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
470
472
  import {
471
473
  configureWebErrorView,
474
+ configureWebLayoutData,
472
475
  DEFAULT_VIEWS_DIRECTORY,
473
476
  EtaViewEngine,
474
477
  errorTemplateName,
475
478
  resolveWebLayoutData
476
479
  } from "@getstrata/core/view";
480
+
481
+ // ../../src/modules/organization/repository.ts
482
+ import { BaseRepository } from "@getstrata/core/database/baseRepository";
483
+ import { NotFoundError } from "@getstrata/core/errors/http";
484
+ import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
485
+
486
+ // ../../src/modules/organization/table.ts
487
+ import { defineTable } from "@getstrata/core/database/table";
488
+
489
+ // ../../src/domain/workhub.ts
490
+ var ORGANIZATION_TABLE = "organization";
491
+
492
+ // ../../src/modules/organization/table.ts
493
+ var organizationTable = defineTable({
494
+ name: ORGANIZATION_TABLE,
495
+ primaryKey: "id",
496
+ columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
497
+ softDeletes: true,
498
+ defaultOrderBy: { column: "id", direction: "ASC" }
499
+ });
500
+
501
+ // ../../src/modules/organization/repository.ts
502
+ class OrganizationRepository extends BaseRepository {
503
+ constructor() {
504
+ super(organizationTable);
505
+ }
506
+ async findBySlug(slug) {
507
+ return await this.firstOrNull({ slug });
508
+ }
509
+ async listForTenant(options) {
510
+ return await this.findAll({
511
+ limit: options.limit,
512
+ offset: options.offset,
513
+ where: { tenant_id: options.tenantId ?? currentTenantId() }
514
+ });
515
+ }
516
+ async countForTenant(tenantId = currentTenantId()) {
517
+ return await this.countWhere({ tenant_id: tenantId });
518
+ }
519
+ async findForTenantOrThrow(id, tenantId = currentTenantId()) {
520
+ const organization = await this.findById(id);
521
+ if (!organization || organization.tenant_id !== tenantId) {
522
+ throw new NotFoundError(`SCIM group ${id} not found.`);
523
+ }
524
+ return organization;
525
+ }
526
+ }
527
+ var repository_default = OrganizationRepository;
528
+
529
+ // ../../src/modules/user/repository.ts
530
+ import {
531
+ emailLookupForQuery,
532
+ protectEmail,
533
+ revealEmail
534
+ } from "@getstrata/core/crypto/fieldEncryption";
535
+ import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
536
+ import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
537
+ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
538
+
539
+ // ../../src/modules/user/table.ts
540
+ import { defineTable as defineTable2 } from "@getstrata/core/database/table";
541
+ var userTable = defineTable2({
542
+ name: "users",
543
+ primaryKey: "id",
544
+ columns: [
545
+ "id",
546
+ "name",
547
+ "email",
548
+ "email_lookup",
549
+ "role",
550
+ "tenant_id",
551
+ "password_hash",
552
+ "email_verified_at",
553
+ "mfa_secret",
554
+ "mfa_enabled",
555
+ "mfa_recovery_codes",
556
+ "profile_photo_path",
557
+ "session_valid_after",
558
+ "current_organization_id",
559
+ "created_at",
560
+ "updated_at"
561
+ ],
562
+ defaultOrderBy: { column: "id", direction: "ASC" }
563
+ });
564
+
565
+ // ../../src/modules/user/repository.ts
566
+ class UserRepository extends BaseRepository2 {
567
+ constructor() {
568
+ super(userTable);
569
+ }
570
+ decode(record) {
571
+ return {
572
+ ...record,
573
+ email: revealEmail(record.email),
574
+ mfa_secret: revealMfaSecret(record.mfa_secret)
575
+ };
576
+ }
577
+ async findById(id) {
578
+ const record = await super.findById(id);
579
+ return record ? this.decode(record) : null;
580
+ }
581
+ async findAll(options = {}) {
582
+ const records = await super.findAll(options);
583
+ return records.map((record) => this.decode(record));
584
+ }
585
+ async create(values) {
586
+ const email = values.email;
587
+ if (!email) {
588
+ throw new Error("Email is required.");
589
+ }
590
+ const protectedEmail = protectEmail(email);
591
+ const record = await super.create({
592
+ ...values,
593
+ tenant_id: values.tenant_id ?? currentTenantId2(),
594
+ email: protectedEmail.storedEmail,
595
+ email_lookup: protectedEmail.emailLookup,
596
+ password_hash: values.password_hash ?? ""
597
+ });
598
+ return this.decode(record);
599
+ }
600
+ async updateByIdOrThrow(id, values, errorFactory) {
601
+ const changes = { ...values };
602
+ if (values.email !== undefined) {
603
+ const protectedEmail = protectEmail(values.email);
604
+ changes.email = protectedEmail.storedEmail;
605
+ changes.email_lookup = protectedEmail.emailLookup;
606
+ }
607
+ const record = await super.updateByIdOrThrow(id, changes, errorFactory);
608
+ return this.decode(record);
609
+ }
610
+ async findByEmail(email) {
611
+ const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
612
+ const record = records[0];
613
+ return record ? this.decode(record) : null;
614
+ }
615
+ async countForTenant(tenantId = currentTenantId2()) {
616
+ return await this.countWhere({ tenant_id: tenantId });
617
+ }
618
+ }
619
+ var repository_default2 = UserRepository;
620
+
621
+ // ../../src/bootstrap/providers/view.ts
477
622
  var CORE_VIEW_TOKEN = "core.view";
478
623
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
479
624
  var viewProvider = {
@@ -486,6 +631,25 @@ var viewProvider = {
486
631
  config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
487
632
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
488
633
  container.set(CORE_VIEW_TOKEN, engine);
634
+ configureWebLayoutData({
635
+ extra: async (user) => {
636
+ const appName = appDisplayName();
637
+ if (!user || typeof user.id !== "number") {
638
+ return { appName, currentOrganization: null, organizations: [] };
639
+ }
640
+ try {
641
+ const record = await new repository_default2().findByIdOrThrow(user.id);
642
+ const memberships = await resolveMembershipLookup().listForUser(record.id);
643
+ const organizationsRepo = new repository_default;
644
+ const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
645
+ const currentId = record.current_organization_id ?? null;
646
+ const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
647
+ return { appName, currentOrganization, organizations };
648
+ } catch {
649
+ return { appName, currentOrganization: null, organizations: [] };
650
+ }
651
+ }
652
+ });
489
653
  configureWebErrorView({
490
654
  render: async (input) => engine.render(errorTemplateName(input.status), {
491
655
  title: input.title,
@@ -194,7 +194,8 @@ class HttpKernel {
194
194
  if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
195
195
  return Response.redirect("/email/verify", 302);
196
196
  }
197
- return Response.redirect(home, 302);
197
+ const location = typeof home === "function" ? await home(user) : home;
198
+ return Response.redirect(location, 302);
198
199
  }
199
200
  return handler(request);
200
201
  });
package/dist/index.js CHANGED
@@ -201,7 +201,8 @@ class HttpKernel {
201
201
  if (isEmailVerificationRequired() && !hasVerifiedEmail(user)) {
202
202
  return Response.redirect("/email/verify", 302);
203
203
  }
204
- return Response.redirect(home, 302);
204
+ const location = typeof home === "function" ? await home(user) : home;
205
+ return Response.redirect(location, 302);
205
206
  }
206
207
  return handler(request);
207
208
  });
@@ -865,15 +866,160 @@ var storageProvider = {
865
866
  var storage_default = storageProvider;
866
867
 
867
868
  // ../../src/bootstrap/providers/view.ts
869
+ import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
868
870
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
871
+ import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
869
872
  import { isViewsEnabled as isViewsEnabled2 } from "@getstrata/core/runtime/frontendMode";
870
873
  import {
871
874
  configureWebErrorView,
875
+ configureWebLayoutData,
872
876
  DEFAULT_VIEWS_DIRECTORY,
873
877
  EtaViewEngine,
874
878
  errorTemplateName,
875
879
  resolveWebLayoutData
876
880
  } from "@getstrata/core/view";
881
+
882
+ // ../../src/modules/organization/repository.ts
883
+ import { BaseRepository } from "@getstrata/core/database/baseRepository";
884
+ import { NotFoundError } from "@getstrata/core/errors/http";
885
+ import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
886
+
887
+ // ../../src/modules/organization/table.ts
888
+ import { defineTable } from "@getstrata/core/database/table";
889
+
890
+ // ../../src/domain/workhub.ts
891
+ var ORGANIZATION_TABLE = "organization";
892
+
893
+ // ../../src/modules/organization/table.ts
894
+ var organizationTable = defineTable({
895
+ name: ORGANIZATION_TABLE,
896
+ primaryKey: "id",
897
+ columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
898
+ softDeletes: true,
899
+ defaultOrderBy: { column: "id", direction: "ASC" }
900
+ });
901
+
902
+ // ../../src/modules/organization/repository.ts
903
+ class OrganizationRepository extends BaseRepository {
904
+ constructor() {
905
+ super(organizationTable);
906
+ }
907
+ async findBySlug(slug) {
908
+ return await this.firstOrNull({ slug });
909
+ }
910
+ async listForTenant(options) {
911
+ return await this.findAll({
912
+ limit: options.limit,
913
+ offset: options.offset,
914
+ where: { tenant_id: options.tenantId ?? currentTenantId() }
915
+ });
916
+ }
917
+ async countForTenant(tenantId = currentTenantId()) {
918
+ return await this.countWhere({ tenant_id: tenantId });
919
+ }
920
+ async findForTenantOrThrow(id, tenantId = currentTenantId()) {
921
+ const organization = await this.findById(id);
922
+ if (!organization || organization.tenant_id !== tenantId) {
923
+ throw new NotFoundError(`SCIM group ${id} not found.`);
924
+ }
925
+ return organization;
926
+ }
927
+ }
928
+ var repository_default = OrganizationRepository;
929
+
930
+ // ../../src/modules/user/repository.ts
931
+ import {
932
+ emailLookupForQuery,
933
+ protectEmail,
934
+ revealEmail
935
+ } from "@getstrata/core/crypto/fieldEncryption";
936
+ import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
937
+ import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
938
+ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
939
+
940
+ // ../../src/modules/user/table.ts
941
+ import { defineTable as defineTable2 } from "@getstrata/core/database/table";
942
+ var userTable = defineTable2({
943
+ name: "users",
944
+ primaryKey: "id",
945
+ columns: [
946
+ "id",
947
+ "name",
948
+ "email",
949
+ "email_lookup",
950
+ "role",
951
+ "tenant_id",
952
+ "password_hash",
953
+ "email_verified_at",
954
+ "mfa_secret",
955
+ "mfa_enabled",
956
+ "mfa_recovery_codes",
957
+ "profile_photo_path",
958
+ "session_valid_after",
959
+ "current_organization_id",
960
+ "created_at",
961
+ "updated_at"
962
+ ],
963
+ defaultOrderBy: { column: "id", direction: "ASC" }
964
+ });
965
+
966
+ // ../../src/modules/user/repository.ts
967
+ class UserRepository extends BaseRepository2 {
968
+ constructor() {
969
+ super(userTable);
970
+ }
971
+ decode(record) {
972
+ return {
973
+ ...record,
974
+ email: revealEmail(record.email),
975
+ mfa_secret: revealMfaSecret(record.mfa_secret)
976
+ };
977
+ }
978
+ async findById(id) {
979
+ const record = await super.findById(id);
980
+ return record ? this.decode(record) : null;
981
+ }
982
+ async findAll(options = {}) {
983
+ const records = await super.findAll(options);
984
+ return records.map((record) => this.decode(record));
985
+ }
986
+ async create(values) {
987
+ const email = values.email;
988
+ if (!email) {
989
+ throw new Error("Email is required.");
990
+ }
991
+ const protectedEmail = protectEmail(email);
992
+ const record = await super.create({
993
+ ...values,
994
+ tenant_id: values.tenant_id ?? currentTenantId2(),
995
+ email: protectedEmail.storedEmail,
996
+ email_lookup: protectedEmail.emailLookup,
997
+ password_hash: values.password_hash ?? ""
998
+ });
999
+ return this.decode(record);
1000
+ }
1001
+ async updateByIdOrThrow(id, values, errorFactory) {
1002
+ const changes = { ...values };
1003
+ if (values.email !== undefined) {
1004
+ const protectedEmail = protectEmail(values.email);
1005
+ changes.email = protectedEmail.storedEmail;
1006
+ changes.email_lookup = protectedEmail.emailLookup;
1007
+ }
1008
+ const record = await super.updateByIdOrThrow(id, changes, errorFactory);
1009
+ return this.decode(record);
1010
+ }
1011
+ async findByEmail(email) {
1012
+ const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
1013
+ const record = records[0];
1014
+ return record ? this.decode(record) : null;
1015
+ }
1016
+ async countForTenant(tenantId = currentTenantId2()) {
1017
+ return await this.countWhere({ tenant_id: tenantId });
1018
+ }
1019
+ }
1020
+ var repository_default2 = UserRepository;
1021
+
1022
+ // ../../src/bootstrap/providers/view.ts
877
1023
  var CORE_VIEW_TOKEN = "core.view";
878
1024
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
879
1025
  var viewProvider = {
@@ -886,6 +1032,25 @@ var viewProvider = {
886
1032
  config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
887
1033
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
888
1034
  container.set(CORE_VIEW_TOKEN, engine);
1035
+ configureWebLayoutData({
1036
+ extra: async (user) => {
1037
+ const appName = appDisplayName();
1038
+ if (!user || typeof user.id !== "number") {
1039
+ return { appName, currentOrganization: null, organizations: [] };
1040
+ }
1041
+ try {
1042
+ const record = await new repository_default2().findByIdOrThrow(user.id);
1043
+ const memberships = await resolveMembershipLookup().listForUser(record.id);
1044
+ const organizationsRepo = new repository_default;
1045
+ const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
1046
+ const currentId = record.current_organization_id ?? null;
1047
+ const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
1048
+ return { appName, currentOrganization, organizations };
1049
+ } catch {
1050
+ return { appName, currentOrganization: null, organizations: [] };
1051
+ }
1052
+ }
1053
+ });
889
1054
  configureWebErrorView({
890
1055
  render: async (input) => engine.render(errorTemplateName(input.status), {
891
1056
  title: input.title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.62",
3
+ "version": "0.2.63",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -173,7 +173,7 @@
173
173
  "build:bundle": "bun build index.ts --outdir dist --target bun --external bun --external eta --external @getstrata/core",
174
174
  "build:types": "tsc -p tsconfig.types.json && bun ../../scripts/prune-bootstrap-dist-types.ts",
175
175
  "prepublishOnly": "bun run build",
176
- "build:subpaths": "bun build entries/applicationRegistry.ts entries/buildModuleRoutes.ts entries/buildWebModuleRoutes.ts entries/cache/modelCacheTags.ts entries/config.ts entries/context.ts entries/contracts.ts entries/createWebRoutes.ts entries/createRoutes.ts entries/createSpaRoutes.ts entries/dependencies.ts entries/discoverModules.ts entries/health.ts entries/http/securedRouteModelBinding.ts entries/httpKernel.ts entries/listeners/invalidateCacheOnModelWrite.ts entries/membershipService.ts entries/metricsRoutes.ts entries/queue/defaultJobs.ts entries/providers.ts entries/providers/view.ts entries/routeRegistry.ts entries/schedule.ts entries/secretsGuard.ts entries/web/forms.ts entries/web/routing.ts entries/web/server.ts entries/web/session.ts entries/web/slug.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core --external @getstrata/bootstrap --external @getstrata/bootstrap/applicationRegistry --external @getstrata/bootstrap/buildModuleRoutes --external @getstrata/bootstrap/buildWebModuleRoutes --external @getstrata/bootstrap/cache/modelCacheTags --external @getstrata/bootstrap/config --external @getstrata/bootstrap/context --external @getstrata/bootstrap/contracts --external @getstrata/bootstrap/createWebRoutes --external @getstrata/bootstrap/createRoutes --external @getstrata/bootstrap/createSpaRoutes --external @getstrata/bootstrap/dependencies --external @getstrata/bootstrap/discoverModules --external @getstrata/bootstrap/health --external @getstrata/bootstrap/http/securedRouteModelBinding --external @getstrata/bootstrap/httpKernel --external @getstrata/bootstrap/listeners/invalidateCacheOnModelWrite --external @getstrata/bootstrap/membershipService --external @getstrata/bootstrap/metricsRoutes --external @getstrata/bootstrap/queue/defaultJobs --external @getstrata/bootstrap/providers --external @getstrata/bootstrap/providers/view --external @getstrata/bootstrap/routeRegistry --external @getstrata/bootstrap/schedule --external @getstrata/bootstrap/secretsGuard --external @getstrata/bootstrap/web/forms --external @getstrata/bootstrap/web/routing --external @getstrata/bootstrap/web/server --external @getstrata/bootstrap/web/session --external @getstrata/bootstrap/web/slug --external @getstrata/core/auth/accessControl --external @getstrata/core/auth/abilityChecker --external @getstrata/core/auth/authContext --external @getstrata/core/auth/emailVerification --external @getstrata/core/auth/guard --external @getstrata/core/auth/membershipContext --external @getstrata/core/auth/membershipMiddleware --external @getstrata/core/auth/membershipScope --external @getstrata/core/auth/membershipService --external @getstrata/core/auth/oauth/oidcProvider --external @getstrata/core/auth/oauth/providers --external @getstrata/core/auth/oauth/samlProvider --external @getstrata/core/auth/oauth/types --external @getstrata/core/auth/password --external @getstrata/core/auth/passwordConfirmCookie --external @getstrata/core/auth/policy --external @getstrata/core/auth/scimAuthMiddleware --external @getstrata/core/auth/sessionCookie --external @getstrata/core/auth/sessionGuard --external @getstrata/core/auth/tokenHash --external @getstrata/core/audit/exportAuditLogs --external @getstrata/core/audit/siemFormatter --external @getstrata/core/admin/formatValue --external @getstrata/core/admin/registry --external @getstrata/core/admin/types --external @getstrata/core/cache/tags --external @getstrata/core/cache/createCacheStore --external @getstrata/core/cache/repository --external @getstrata/core/cache/simpleCache --external @getstrata/core/cache/simpleCacheStore --external @getstrata/core/config/envSchema --external @getstrata/core/contracts/serviceTokens --external @getstrata/core/contracts/container --external @getstrata/core/contracts/di --external @getstrata/core/crypto/fieldEncryption --external @getstrata/core/crypto/mfaSecret --external @getstrata/core/database --external @getstrata/core/database/baseRepository --external @getstrata/core/database/bindConnection --external @getstrata/core/database/boundConnection --external @getstrata/core/database/bunSql --external @getstrata/core/database/connection --external @getstrata/core/database/defaultConnection --external @getstrata/core/database/errors --external @getstrata/core/database/factory --external @getstrata/core/database/migrations --external @getstrata/core/database/migrations/types --external @getstrata/core/database/model --external @getstrata/core/database/query --external @getstrata/core/database/relationships --external @getstrata/core/database/repositoryConnection --external @getstrata/core/database/repositoryQuery --external @getstrata/core/database/seeders --external @getstrata/core/database/seeders/types --external @getstrata/core/database/schema --external @getstrata/core/database/table --external @getstrata/core/database/transaction --external @getstrata/core/database/types --external @getstrata/core/database/whereBuilder --external @getstrata/core/errors/http --external @getstrata/core/events --external @getstrata/core/facades --external @getstrata/core/http --external @getstrata/core/http/authMiddleware --external @getstrata/core/http/authorizeMiddleware --external @getstrata/core/http/bodySizeLimitMiddleware --external @getstrata/core/http/clientIp --external @getstrata/core/http/cookies --external @getstrata/core/http/contentNegotiation --external @getstrata/core/http/contentSecurityPolicy --external @getstrata/core/http/conditionalResponse --external @getstrata/core/http/corsMiddleware --external @getstrata/core/http/csrfMiddleware --external @getstrata/core/http/csrfProtection --external @getstrata/core/http/csrfToken --external @getstrata/core/http/etag --external @getstrata/core/http/flashSession --external @getstrata/core/http/flashMiddleware --external @getstrata/core/http/formRequest --external @getstrata/core/http/middleware --external @getstrata/core/http/metricsMiddleware --external @getstrata/core/http/loginThrottleMiddleware --external @getstrata/core/http/memoryThrottleMiddleware --external @getstrata/core/http/pagination --external @getstrata/core/http/parseFormBody --external @getstrata/core/http/parseMultipartUpload --external @getstrata/core/http/requireAbilityMiddleware --external @getstrata/core/http/requireAuthMiddleware --external @getstrata/core/http/requireGlobalAdminMiddleware --external @getstrata/core/http/requireWebAuthMiddleware --external @getstrata/core/http/requireVerifiedMiddleware --external @getstrata/core/http/requirePasswordConfirmMiddleware --external @getstrata/core/http/resources --external @getstrata/core/http/response --external @getstrata/core/http/route --external @getstrata/core/http/routeMiddleware --external @getstrata/core/http/routeModelBinding --external @getstrata/core/http/safeInternalPath --external @getstrata/core/http/signedUrl --external @getstrata/core/http/scimThrottleMiddleware --external @getstrata/core/http/securityHeadersMiddleware --external @getstrata/core/http/securedRouteModelBinding --external @getstrata/core/http/requestMetaContext --external @getstrata/core/http/webErrorResponse --external @getstrata/core/http/webFormRequest --external @getstrata/core/http/throttleMiddleware --external @getstrata/core/http/validation --external @getstrata/core/jobs/dispatchWebhookJob --external @getstrata/core/jobs/exportAuditLogsJob --external @getstrata/core/jobs/invalidateCacheTagsJob --external @getstrata/core/lifecycle/gracefulShutdown --external @getstrata/core/logging/logger --external @getstrata/core/logging/requestLoggingMiddleware --external @getstrata/core/mail/mailer --external @getstrata/core/mail/markdownMail --external @getstrata/core/mail/markdownMailable --external @getstrata/core/mail/sanitizeMailHtml --external @getstrata/core/media/imageTransform --external @getstrata/core/metrics/prometheus --external @getstrata/core/notifications --external @getstrata/core/openapi/generator --external @getstrata/core/openapi/registeredRoute --external @getstrata/core/openapi/validate --external @getstrata/core/pagination --external @getstrata/core/queue --external @getstrata/core/queue/createAppQueue --external @getstrata/core/queue/failedJobRepository --external @getstrata/core/queue/failedJobService --external @getstrata/core/queue/jobRegistry --external @getstrata/core/queue/jobRunner --external @getstrata/core/queue/queueMetrics --external @getstrata/core/queue/publicQueue --external @getstrata/core/queue/redisQueue --external @getstrata/core/queue/types --external @getstrata/core/runtime/appKeyPrefix --external @getstrata/core/runtime/frontendMode --external @getstrata/core/runtime/applicationRegistry --external @getstrata/core/runtime/asyncContextStore --external @getstrata/core/scheduler/schedule --external @getstrata/core/scheduler/osCron --external @getstrata/core/security/oauthState --external @getstrata/core/security/publicReads --external @getstrata/core/security/safeFetch --external @getstrata/core/security/safeUrl --external @getstrata/core/security/scimTenantTokens --external @getstrata/core/security/securityEvents --external @getstrata/core/security/stripeWebhook --external @getstrata/core/security/timingSafeCompare --external @getstrata/core/security/tokenExpiry --external @getstrata/core/security/totp --external @getstrata/core/storage/storage --external @getstrata/core/tenant/tenancyConfig --external @getstrata/core/tenant/tenantContext --external @getstrata/core/tenant/tenantDatabaseScope --external @getstrata/core/tenant/databaseTenantContext --external @getstrata/core/tenant/tenantMiddleware --external @getstrata/core/terminal/runShell --external @getstrata/core/tracing/traceContext --external @getstrata/core/tracing/tracingMiddleware --external @getstrata/core/validation/rules --external @getstrata/core/view",
176
+ "build:subpaths": "bun build entries/applicationRegistry.ts entries/buildModuleRoutes.ts entries/buildWebModuleRoutes.ts entries/cache/modelCacheTags.ts entries/config.ts entries/context.ts entries/contracts.ts entries/createWebRoutes.ts entries/createRoutes.ts entries/createSpaRoutes.ts entries/dependencies.ts entries/discoverModules.ts entries/health.ts entries/http/securedRouteModelBinding.ts entries/httpKernel.ts entries/listeners/invalidateCacheOnModelWrite.ts entries/membershipService.ts entries/metricsRoutes.ts entries/queue/defaultJobs.ts entries/providers.ts entries/providers/view.ts entries/routeRegistry.ts entries/schedule.ts entries/secretsGuard.ts entries/web/forms.ts entries/web/routing.ts entries/web/server.ts entries/web/session.ts entries/web/slug.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core --external @getstrata/bootstrap --external @getstrata/bootstrap/applicationRegistry --external @getstrata/bootstrap/buildModuleRoutes --external @getstrata/bootstrap/buildWebModuleRoutes --external @getstrata/bootstrap/cache/modelCacheTags --external @getstrata/bootstrap/config --external @getstrata/bootstrap/context --external @getstrata/bootstrap/contracts --external @getstrata/bootstrap/createWebRoutes --external @getstrata/bootstrap/createRoutes --external @getstrata/bootstrap/createSpaRoutes --external @getstrata/bootstrap/dependencies --external @getstrata/bootstrap/discoverModules --external @getstrata/bootstrap/health --external @getstrata/bootstrap/http/securedRouteModelBinding --external @getstrata/bootstrap/httpKernel --external @getstrata/bootstrap/listeners/invalidateCacheOnModelWrite --external @getstrata/bootstrap/membershipService --external @getstrata/bootstrap/metricsRoutes --external @getstrata/bootstrap/queue/defaultJobs --external @getstrata/bootstrap/providers --external @getstrata/bootstrap/providers/view --external @getstrata/bootstrap/routeRegistry --external @getstrata/bootstrap/schedule --external @getstrata/bootstrap/secretsGuard --external @getstrata/bootstrap/web/forms --external @getstrata/bootstrap/web/routing --external @getstrata/bootstrap/web/server --external @getstrata/bootstrap/web/session --external @getstrata/bootstrap/web/slug --external @getstrata/core/auth/accessControl --external @getstrata/core/auth/abilityChecker --external @getstrata/core/auth/authContext --external @getstrata/core/auth/emailVerification --external @getstrata/core/auth/guard --external @getstrata/core/auth/membershipContext --external @getstrata/core/auth/membershipMiddleware --external @getstrata/core/auth/membershipScope --external @getstrata/core/auth/membershipService --external @getstrata/core/auth/oauth/oidcProvider --external @getstrata/core/auth/oauth/providers --external @getstrata/core/auth/oauth/samlProvider --external @getstrata/core/auth/oauth/types --external @getstrata/core/auth/password --external @getstrata/core/auth/intendedUrlCookie --external @getstrata/core/auth/passwordConfirmCookie --external @getstrata/core/auth/policy --external @getstrata/core/auth/scimAuthMiddleware --external @getstrata/core/auth/sessionCookie --external @getstrata/core/auth/sessionGuard --external @getstrata/core/auth/tokenHash --external @getstrata/core/audit/exportAuditLogs --external @getstrata/core/audit/siemFormatter --external @getstrata/core/admin/formatValue --external @getstrata/core/admin/registry --external @getstrata/core/admin/types --external @getstrata/core/cache/tags --external @getstrata/core/cache/createCacheStore --external @getstrata/core/cache/repository --external @getstrata/core/cache/simpleCache --external @getstrata/core/cache/simpleCacheStore --external @getstrata/core/config/envSchema --external @getstrata/core/contracts/serviceTokens --external @getstrata/core/contracts/container --external @getstrata/core/contracts/di --external @getstrata/core/crypto/fieldEncryption --external @getstrata/core/crypto/mfaSecret --external @getstrata/core/database --external @getstrata/core/database/baseRepository --external @getstrata/core/database/bindConnection --external @getstrata/core/database/boundConnection --external @getstrata/core/database/bunSql --external @getstrata/core/database/connection --external @getstrata/core/database/defaultConnection --external @getstrata/core/database/errors --external @getstrata/core/database/factory --external @getstrata/core/database/migrations --external @getstrata/core/database/migrations/types --external @getstrata/core/database/model --external @getstrata/core/database/query --external @getstrata/core/database/relationships --external @getstrata/core/database/repositoryConnection --external @getstrata/core/database/repositoryQuery --external @getstrata/core/database/seeders --external @getstrata/core/database/seeders/types --external @getstrata/core/database/schema --external @getstrata/core/database/table --external @getstrata/core/database/transaction --external @getstrata/core/database/types --external @getstrata/core/database/whereBuilder --external @getstrata/core/errors/http --external @getstrata/core/events --external @getstrata/core/facades --external @getstrata/core/http --external @getstrata/core/http/authMiddleware --external @getstrata/core/http/authorizeMiddleware --external @getstrata/core/http/bodySizeLimitMiddleware --external @getstrata/core/http/clientIp --external @getstrata/core/http/cookies --external @getstrata/core/http/contentNegotiation --external @getstrata/core/http/contentSecurityPolicy --external @getstrata/core/http/conditionalResponse --external @getstrata/core/http/corsMiddleware --external @getstrata/core/http/csrfMiddleware --external @getstrata/core/http/csrfProtection --external @getstrata/core/http/csrfToken --external @getstrata/core/http/etag --external @getstrata/core/http/flashSession --external @getstrata/core/http/flashMiddleware --external @getstrata/core/http/formRequest --external @getstrata/core/http/middleware --external @getstrata/core/http/metricsMiddleware --external @getstrata/core/http/loginThrottleMiddleware --external @getstrata/core/http/memoryThrottleMiddleware --external @getstrata/core/http/pagination --external @getstrata/core/http/parseFormBody --external @getstrata/core/http/parseMultipartUpload --external @getstrata/core/http/requireAbilityMiddleware --external @getstrata/core/http/requireAuthMiddleware --external @getstrata/core/http/requireGlobalAdminMiddleware --external @getstrata/core/http/requireWebAuthMiddleware --external @getstrata/core/http/requireVerifiedMiddleware --external @getstrata/core/http/requirePasswordConfirmMiddleware --external @getstrata/core/http/resources --external @getstrata/core/http/response --external @getstrata/core/http/route --external @getstrata/core/http/routeMiddleware --external @getstrata/core/http/routeModelBinding --external @getstrata/core/http/safeInternalPath --external @getstrata/core/http/signedUrl --external @getstrata/core/http/scimThrottleMiddleware --external @getstrata/core/http/securityHeadersMiddleware --external @getstrata/core/http/securedRouteModelBinding --external @getstrata/core/http/requestMetaContext --external @getstrata/core/http/webErrorResponse --external @getstrata/core/http/webFormRequest --external @getstrata/core/http/throttleMiddleware --external @getstrata/core/http/validation --external @getstrata/core/jobs/dispatchWebhookJob --external @getstrata/core/jobs/exportAuditLogsJob --external @getstrata/core/jobs/invalidateCacheTagsJob --external @getstrata/core/lifecycle/gracefulShutdown --external @getstrata/core/logging/logger --external @getstrata/core/logging/requestLoggingMiddleware --external @getstrata/core/mail/mailer --external @getstrata/core/mail/markdownMail --external @getstrata/core/mail/markdownMailable --external @getstrata/core/mail/sanitizeMailHtml --external @getstrata/core/media/imageTransform --external @getstrata/core/metrics/prometheus --external @getstrata/core/notifications --external @getstrata/core/openapi/generator --external @getstrata/core/openapi/registeredRoute --external @getstrata/core/openapi/validate --external @getstrata/core/pagination --external @getstrata/core/queue --external @getstrata/core/queue/createAppQueue --external @getstrata/core/queue/failedJobRepository --external @getstrata/core/queue/failedJobService --external @getstrata/core/queue/jobRegistry --external @getstrata/core/queue/jobRunner --external @getstrata/core/queue/queueMetrics --external @getstrata/core/queue/publicQueue --external @getstrata/core/queue/redisQueue --external @getstrata/core/queue/types --external @getstrata/core/runtime/appKeyPrefix --external @getstrata/core/runtime/frontendMode --external @getstrata/core/runtime/applicationRegistry --external @getstrata/core/runtime/asyncContextStore --external @getstrata/core/scheduler/schedule --external @getstrata/core/scheduler/osCron --external @getstrata/core/security/oauthState --external @getstrata/core/security/publicReads --external @getstrata/core/security/recoveryCodes --external @getstrata/core/security/safeFetch --external @getstrata/core/security/safeUrl --external @getstrata/core/security/scimTenantTokens --external @getstrata/core/security/securityEvents --external @getstrata/core/security/stripeWebhook --external @getstrata/core/security/timingSafeCompare --external @getstrata/core/security/tokenExpiry --external @getstrata/core/security/totp --external @getstrata/core/storage/storage --external @getstrata/core/tenant/tenancyConfig --external @getstrata/core/tenant/tenantContext --external @getstrata/core/tenant/tenantDatabaseScope --external @getstrata/core/tenant/databaseTenantContext --external @getstrata/core/tenant/tenantMiddleware --external @getstrata/core/terminal/runShell --external @getstrata/core/tracing/traceContext --external @getstrata/core/tracing/tracingMiddleware --external @getstrata/core/validation/rules --external @getstrata/core/view",
177
177
  "build:shims": "true"
178
178
  },
179
179
  "publishConfig": {