@getstrata/bootstrap 0.2.62 → 0.2.64

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,13 @@
1
1
  # @getstrata/bootstrap changelog
2
2
 
3
+ ## 0.2.64
4
+
5
+ - `registerDefaultJobs()` no longer registers WorkHub `webhook.dispatch`. Apps that dispatch model webhooks should call `registerWebhookJobs()` (WorkHub’s webhook provider and `queue:work` do).
6
+
7
+ ## 0.2.63
8
+
9
+ - `HttpKernel.wrapWebGuest()` `home` accepts `string | ((user) => string | Promise<string>)` so signed-in guest redirects can follow the current team. Default remains `/organizations`.
10
+
3
11
  ## 0.2.62
4
12
 
5
13
  - `HttpKernel.wrapWebPasswordConfirm()` is Laravel `password.confirm` for HTML routes. Peer `@getstrata/core` `^0.5.75`.
package/README.md CHANGED
@@ -32,7 +32,9 @@ 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
+
37
+ `registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. WorkHub webhook dispatch is `registerWebhookJobs()` in the app webhook provider.
36
38
 
37
39
  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
40
 
@@ -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
  });
@@ -437,7 +437,6 @@ import {
437
437
  } from "@getstrata/core/queue/createAppQueue";
438
438
 
439
439
  // ../../src/bootstrap/queue/defaultJobs.ts
440
- import { DispatchWebhookJob } from "@getstrata/core/jobs/dispatchWebhookJob";
441
440
  import { ExportAuditLogsJob } from "@getstrata/core/jobs/exportAuditLogsJob";
442
441
  import { InvalidateCacheTagsJob as InvalidateCacheTagsJob2 } from "@getstrata/core/jobs/invalidateCacheTagsJob";
443
442
  import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
@@ -446,7 +445,6 @@ function registerDefaultJobs() {
446
445
  jobRegistry.register("cache.invalidate-tags", () => {
447
446
  return new InvalidateCacheTagsJob2(resolveApplicationCache2());
448
447
  });
449
- jobRegistry.register("webhook.dispatch", () => new DispatchWebhookJob);
450
448
  jobRegistry.register("audit.export", () => new ExportAuditLogsJob);
451
449
  }
452
450
 
@@ -475,15 +473,160 @@ var storageProvider = {
475
473
  var storage_default = storageProvider;
476
474
 
477
475
  // ../../src/bootstrap/providers/view.ts
476
+ import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
478
477
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
478
+ import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
479
479
  import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
480
480
  import {
481
481
  configureWebErrorView,
482
+ configureWebLayoutData,
482
483
  DEFAULT_VIEWS_DIRECTORY,
483
484
  EtaViewEngine,
484
485
  errorTemplateName,
485
486
  resolveWebLayoutData
486
487
  } from "@getstrata/core/view";
488
+
489
+ // ../../src/modules/organization/repository.ts
490
+ import { BaseRepository } from "@getstrata/core/database/baseRepository";
491
+ import { NotFoundError } from "@getstrata/core/errors/http";
492
+ import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
493
+
494
+ // ../../src/modules/organization/table.ts
495
+ import { defineTable } from "@getstrata/core/database/table";
496
+
497
+ // ../../src/domain/workhub.ts
498
+ var ORGANIZATION_TABLE = "organization";
499
+
500
+ // ../../src/modules/organization/table.ts
501
+ var organizationTable = defineTable({
502
+ name: ORGANIZATION_TABLE,
503
+ primaryKey: "id",
504
+ columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
505
+ softDeletes: true,
506
+ defaultOrderBy: { column: "id", direction: "ASC" }
507
+ });
508
+
509
+ // ../../src/modules/organization/repository.ts
510
+ class OrganizationRepository extends BaseRepository {
511
+ constructor() {
512
+ super(organizationTable);
513
+ }
514
+ async findBySlug(slug) {
515
+ return await this.firstOrNull({ slug });
516
+ }
517
+ async listForTenant(options) {
518
+ return await this.findAll({
519
+ limit: options.limit,
520
+ offset: options.offset,
521
+ where: { tenant_id: options.tenantId ?? currentTenantId() }
522
+ });
523
+ }
524
+ async countForTenant(tenantId = currentTenantId()) {
525
+ return await this.countWhere({ tenant_id: tenantId });
526
+ }
527
+ async findForTenantOrThrow(id, tenantId = currentTenantId()) {
528
+ const organization = await this.findById(id);
529
+ if (!organization || organization.tenant_id !== tenantId) {
530
+ throw new NotFoundError(`SCIM group ${id} not found.`);
531
+ }
532
+ return organization;
533
+ }
534
+ }
535
+ var repository_default = OrganizationRepository;
536
+
537
+ // ../../src/modules/user/repository.ts
538
+ import {
539
+ emailLookupForQuery,
540
+ protectEmail,
541
+ revealEmail
542
+ } from "@getstrata/core/crypto/fieldEncryption";
543
+ import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
544
+ import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
545
+ import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
546
+
547
+ // ../../src/modules/user/table.ts
548
+ import { defineTable as defineTable2 } from "@getstrata/core/database/table";
549
+ var userTable = defineTable2({
550
+ name: "users",
551
+ primaryKey: "id",
552
+ columns: [
553
+ "id",
554
+ "name",
555
+ "email",
556
+ "email_lookup",
557
+ "role",
558
+ "tenant_id",
559
+ "password_hash",
560
+ "email_verified_at",
561
+ "mfa_secret",
562
+ "mfa_enabled",
563
+ "mfa_recovery_codes",
564
+ "profile_photo_path",
565
+ "session_valid_after",
566
+ "current_organization_id",
567
+ "created_at",
568
+ "updated_at"
569
+ ],
570
+ defaultOrderBy: { column: "id", direction: "ASC" }
571
+ });
572
+
573
+ // ../../src/modules/user/repository.ts
574
+ class UserRepository extends BaseRepository2 {
575
+ constructor() {
576
+ super(userTable);
577
+ }
578
+ decode(record) {
579
+ return {
580
+ ...record,
581
+ email: revealEmail(record.email),
582
+ mfa_secret: revealMfaSecret(record.mfa_secret)
583
+ };
584
+ }
585
+ async findById(id) {
586
+ const record = await super.findById(id);
587
+ return record ? this.decode(record) : null;
588
+ }
589
+ async findAll(options = {}) {
590
+ const records = await super.findAll(options);
591
+ return records.map((record) => this.decode(record));
592
+ }
593
+ async create(values) {
594
+ const email = values.email;
595
+ if (!email) {
596
+ throw new Error("Email is required.");
597
+ }
598
+ const protectedEmail = protectEmail(email);
599
+ const record = await super.create({
600
+ ...values,
601
+ tenant_id: values.tenant_id ?? currentTenantId2(),
602
+ email: protectedEmail.storedEmail,
603
+ email_lookup: protectedEmail.emailLookup,
604
+ password_hash: values.password_hash ?? ""
605
+ });
606
+ return this.decode(record);
607
+ }
608
+ async updateByIdOrThrow(id, values, errorFactory) {
609
+ const changes = { ...values };
610
+ if (values.email !== undefined) {
611
+ const protectedEmail = protectEmail(values.email);
612
+ changes.email = protectedEmail.storedEmail;
613
+ changes.email_lookup = protectedEmail.emailLookup;
614
+ }
615
+ const record = await super.updateByIdOrThrow(id, changes, errorFactory);
616
+ return this.decode(record);
617
+ }
618
+ async findByEmail(email) {
619
+ const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
620
+ const record = records[0];
621
+ return record ? this.decode(record) : null;
622
+ }
623
+ async countForTenant(tenantId = currentTenantId2()) {
624
+ return await this.countWhere({ tenant_id: tenantId });
625
+ }
626
+ }
627
+ var repository_default2 = UserRepository;
628
+
629
+ // ../../src/bootstrap/providers/view.ts
487
630
  var CORE_VIEW_TOKEN = "core.view";
488
631
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
489
632
  var viewProvider = {
@@ -496,6 +639,25 @@ var viewProvider = {
496
639
  config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
497
640
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
498
641
  container.set(CORE_VIEW_TOKEN, engine);
642
+ configureWebLayoutData({
643
+ extra: async (user) => {
644
+ const appName = appDisplayName();
645
+ if (!user || typeof user.id !== "number") {
646
+ return { appName, currentOrganization: null, organizations: [] };
647
+ }
648
+ try {
649
+ const record = await new repository_default2().findByIdOrThrow(user.id);
650
+ const memberships = await resolveMembershipLookup().listForUser(record.id);
651
+ const organizationsRepo = new repository_default;
652
+ const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
653
+ const currentId = record.current_organization_id ?? null;
654
+ const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
655
+ return { appName, currentOrganization, organizations };
656
+ } catch {
657
+ return { appName, currentOrganization: null, organizations: [] };
658
+ }
659
+ }
660
+ });
499
661
  configureWebErrorView({
500
662
  render: async (input) => engine.render(errorTemplateName(input.status), {
501
663
  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
  });