@getstrata/bootstrap 0.2.66 → 0.2.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,19 +96,33 @@ class CookieSessionStore {
96
96
  sql() {
97
97
  return resolveSql(this.sqlSource);
98
98
  }
99
- async create(user) {
99
+ async create(user, meta = {}) {
100
100
  const id = randomBytes(32).toString("hex");
101
101
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
102
- await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
103
- id,
104
- user.id,
105
- expires
106
- ]);
102
+ await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
103
+ VALUES ($1, $2, $3, $4, $5, NOW())`, [id, user.id, expires, meta.userAgent ?? null, meta.ipAddress ?? null]);
107
104
  return id;
108
105
  }
109
106
  async destroy(sessionId) {
110
107
  await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
111
108
  }
109
+ async destroyOtherSessions(userId, keepSessionId) {
110
+ await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = $1 AND id <> $2`, [
111
+ userId,
112
+ keepSessionId
113
+ ]);
114
+ }
115
+ async listForUser(userId) {
116
+ return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
117
+ FROM sessions
118
+ WHERE user_id = $1 AND expires_at > NOW()
119
+ ORDER BY last_active_at DESC NULLS LAST, expires_at DESC`, [userId]);
120
+ }
121
+ async touch(sessionId) {
122
+ await this.sql().unsafe(`UPDATE sessions SET last_active_at = NOW() WHERE id = $1`, [
123
+ sessionId
124
+ ]);
125
+ }
112
126
  async read(request) {
113
127
  const sessionId = this.sessionIdFromRequest(request);
114
128
  if (!sessionId)
@@ -142,8 +156,8 @@ class CookieSessionAuthManager extends AuthManager {
142
156
  super(new CookieSessionGuard(store, mapUser));
143
157
  this.store = store;
144
158
  }
145
- async signIn(user) {
146
- const sessionId = await this.store.create(user);
159
+ async signIn(user, meta = {}) {
160
+ const sessionId = await this.store.create(user, meta);
147
161
  return { sessionId, setCookie: this.store.cookieHeader(user, sessionId) };
148
162
  }
149
163
  async signOut(request) {
@@ -153,8 +167,8 @@ class CookieSessionAuthManager extends AuthManager {
153
167
  }
154
168
  return { setCookie: this.store.clearCookieHeader() };
155
169
  }
156
- async signInRedirect(user, location, status = 302) {
157
- const { setCookie } = await this.signIn(user);
170
+ async signInRedirect(user, location, status = 302, meta = {}) {
171
+ const { setCookie } = await this.signIn(user, meta);
158
172
  return redirectWithCookie(location, setCookie, status);
159
173
  }
160
174
  async signOutRedirect(request, location, status = 302) {
package/dist/index.js CHANGED
@@ -353,7 +353,12 @@ function readDiscoverModulesState() {
353
353
  return state;
354
354
  }
355
355
  function configureModulesDirectory(modulesDir) {
356
- readDiscoverModulesState().configuredModulesDir = modulesDir;
356
+ const state = readDiscoverModulesState();
357
+ if (state.configuredModulesDir !== modulesDir) {
358
+ state.appModules.length = 0;
359
+ state.modulesReady = undefined;
360
+ }
361
+ state.configuredModulesDir = modulesDir;
357
362
  }
358
363
  function resolveModulesDirectory(options) {
359
364
  const state = readDiscoverModulesState();
@@ -871,7 +876,6 @@ var storageProvider = {
871
876
  var storage_default = storageProvider;
872
877
 
873
878
  // ../../src/bootstrap/providers/view.ts
874
- import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
875
879
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
876
880
  import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
877
881
  import { isViewsEnabled as isViewsEnabled2 } from "@getstrata/core/runtime/frontendMode";
@@ -883,148 +887,6 @@ import {
883
887
  errorTemplateName,
884
888
  resolveWebLayoutData
885
889
  } from "@getstrata/core/view";
886
-
887
- // ../../src/modules/organization/repository.ts
888
- import { BaseRepository } from "@getstrata/core/database/baseRepository";
889
- import { NotFoundError } from "@getstrata/core/errors/http";
890
- import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
891
-
892
- // ../../src/modules/organization/table.ts
893
- import { defineTable } from "@getstrata/core/database/table";
894
-
895
- // ../../src/domain/workhub.ts
896
- var ORGANIZATION_TABLE = "organization";
897
-
898
- // ../../src/modules/organization/table.ts
899
- var organizationTable = defineTable({
900
- name: ORGANIZATION_TABLE,
901
- primaryKey: "id",
902
- columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
903
- softDeletes: true,
904
- defaultOrderBy: { column: "id", direction: "ASC" }
905
- });
906
-
907
- // ../../src/modules/organization/repository.ts
908
- class OrganizationRepository extends BaseRepository {
909
- constructor() {
910
- super(organizationTable);
911
- }
912
- async findBySlug(slug) {
913
- return await this.firstOrNull({ slug });
914
- }
915
- async listForTenant(options) {
916
- return await this.findAll({
917
- limit: options.limit,
918
- offset: options.offset,
919
- where: { tenant_id: options.tenantId ?? currentTenantId() }
920
- });
921
- }
922
- async countForTenant(tenantId = currentTenantId()) {
923
- return await this.countWhere({ tenant_id: tenantId });
924
- }
925
- async findForTenantOrThrow(id, tenantId = currentTenantId()) {
926
- const organization = await this.findById(id);
927
- if (!organization || organization.tenant_id !== tenantId) {
928
- throw new NotFoundError(`SCIM group ${id} not found.`);
929
- }
930
- return organization;
931
- }
932
- }
933
- var repository_default = OrganizationRepository;
934
-
935
- // ../../src/modules/user/repository.ts
936
- import {
937
- emailLookupForQuery,
938
- protectEmail,
939
- revealEmail
940
- } from "@getstrata/core/crypto/fieldEncryption";
941
- import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
942
- import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
943
- import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
944
-
945
- // ../../src/modules/user/table.ts
946
- import { defineTable as defineTable2 } from "@getstrata/core/database/table";
947
- var userTable = defineTable2({
948
- name: "users",
949
- primaryKey: "id",
950
- columns: [
951
- "id",
952
- "name",
953
- "email",
954
- "email_lookup",
955
- "role",
956
- "tenant_id",
957
- "password_hash",
958
- "email_verified_at",
959
- "mfa_secret",
960
- "mfa_enabled",
961
- "mfa_recovery_codes",
962
- "profile_photo_path",
963
- "session_valid_after",
964
- "current_organization_id",
965
- "created_at",
966
- "updated_at"
967
- ],
968
- defaultOrderBy: { column: "id", direction: "ASC" }
969
- });
970
-
971
- // ../../src/modules/user/repository.ts
972
- class UserRepository extends BaseRepository2 {
973
- constructor() {
974
- super(userTable);
975
- }
976
- decode(record) {
977
- return {
978
- ...record,
979
- email: revealEmail(record.email),
980
- mfa_secret: revealMfaSecret(record.mfa_secret)
981
- };
982
- }
983
- async findById(id) {
984
- const record = await super.findById(id);
985
- return record ? this.decode(record) : null;
986
- }
987
- async findAll(options = {}) {
988
- const records = await super.findAll(options);
989
- return records.map((record) => this.decode(record));
990
- }
991
- async create(values) {
992
- const email = values.email;
993
- if (!email) {
994
- throw new Error("Email is required.");
995
- }
996
- const protectedEmail = protectEmail(email);
997
- const record = await super.create({
998
- ...values,
999
- tenant_id: values.tenant_id ?? currentTenantId2(),
1000
- email: protectedEmail.storedEmail,
1001
- email_lookup: protectedEmail.emailLookup,
1002
- password_hash: values.password_hash ?? ""
1003
- });
1004
- return this.decode(record);
1005
- }
1006
- async updateByIdOrThrow(id, values, errorFactory) {
1007
- const changes = { ...values };
1008
- if (values.email !== undefined) {
1009
- const protectedEmail = protectEmail(values.email);
1010
- changes.email = protectedEmail.storedEmail;
1011
- changes.email_lookup = protectedEmail.emailLookup;
1012
- }
1013
- const record = await super.updateByIdOrThrow(id, changes, errorFactory);
1014
- return this.decode(record);
1015
- }
1016
- async findByEmail(email) {
1017
- const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
1018
- const record = records[0];
1019
- return record ? this.decode(record) : null;
1020
- }
1021
- async countForTenant(tenantId = currentTenantId2()) {
1022
- return await this.countWhere({ tenant_id: tenantId });
1023
- }
1024
- }
1025
- var repository_default2 = UserRepository;
1026
-
1027
- // ../../src/bootstrap/providers/view.ts
1028
890
  var CORE_VIEW_TOKEN = "core.view";
1029
891
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
1030
892
  var viewProvider = {
@@ -1038,23 +900,11 @@ var viewProvider = {
1038
900
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
1039
901
  container.set(CORE_VIEW_TOKEN, engine);
1040
902
  configureWebLayoutData({
1041
- extra: async (user) => {
1042
- const appName = appDisplayName();
1043
- if (!user || typeof user.id !== "number") {
1044
- return { appName, currentOrganization: null, organizations: [] };
1045
- }
1046
- try {
1047
- const record = await new repository_default2().findByIdOrThrow(user.id);
1048
- const memberships = await resolveMembershipLookup().listForUser(record.id);
1049
- const organizationsRepo = new repository_default;
1050
- const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
1051
- const currentId = record.current_organization_id ?? null;
1052
- const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
1053
- return { appName, currentOrganization, organizations };
1054
- } catch {
1055
- return { appName, currentOrganization: null, organizations: [] };
1056
- }
1057
- }
903
+ extra: async () => ({
904
+ appName: appDisplayName(),
905
+ currentOrganization: null,
906
+ organizations: []
907
+ })
1058
908
  });
1059
909
  configureWebErrorView({
1060
910
  render: async (input) => engine.render(errorTemplateName(input.status), {
@@ -1118,9 +968,10 @@ import { notFoundHtmlResponse } from "@getstrata/core/view";
1118
968
  function registerRoute(method, path, middleware) {
1119
969
  routeRegistry.register({ method, path, middleware });
1120
970
  }
1121
- function createWebRoutes(dependencies) {
971
+ function createWebRoutes(dependencies, options = {}) {
1122
972
  const wrappedRoutes = buildWebModuleRoutes(dependencies, {
1123
- clearRegistry: false
973
+ clearRegistry: false,
974
+ modules: options.modules
1124
975
  });
1125
976
  registerRoute("GET", "/", ["global", "web"]);
1126
977
  wrappedRoutes["/assets/*"] = async (request) => {
@@ -1136,12 +987,12 @@ function createWebRoutes(dependencies) {
1136
987
  registerRoute("GET", "/assets/*", ["global", "web"]);
1137
988
  return wrappedRoutes;
1138
989
  }
1139
- function mergeWebRoutes(dependencies, routes) {
990
+ function mergeWebRoutes(dependencies, routes, options = {}) {
1140
991
  if (!isViewsEnabled3()) {
1141
992
  return routes;
1142
993
  }
1143
994
  return {
1144
- ...createWebRoutes(dependencies),
995
+ ...createWebRoutes(dependencies, options),
1145
996
  ...routes
1146
997
  };
1147
998
  }
@@ -1592,19 +1443,33 @@ class CookieSessionStore {
1592
1443
  sql() {
1593
1444
  return resolveSql(this.sqlSource);
1594
1445
  }
1595
- async create(user) {
1446
+ async create(user, meta = {}) {
1596
1447
  const id = randomBytes(32).toString("hex");
1597
1448
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
1598
- await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
1599
- id,
1600
- user.id,
1601
- expires
1602
- ]);
1449
+ await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
1450
+ VALUES ($1, $2, $3, $4, $5, NOW())`, [id, user.id, expires, meta.userAgent ?? null, meta.ipAddress ?? null]);
1603
1451
  return id;
1604
1452
  }
1605
1453
  async destroy(sessionId) {
1606
1454
  await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
1607
1455
  }
1456
+ async destroyOtherSessions(userId, keepSessionId) {
1457
+ await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = $1 AND id <> $2`, [
1458
+ userId,
1459
+ keepSessionId
1460
+ ]);
1461
+ }
1462
+ async listForUser(userId) {
1463
+ return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
1464
+ FROM sessions
1465
+ WHERE user_id = $1 AND expires_at > NOW()
1466
+ ORDER BY last_active_at DESC NULLS LAST, expires_at DESC`, [userId]);
1467
+ }
1468
+ async touch(sessionId) {
1469
+ await this.sql().unsafe(`UPDATE sessions SET last_active_at = NOW() WHERE id = $1`, [
1470
+ sessionId
1471
+ ]);
1472
+ }
1608
1473
  async read(request) {
1609
1474
  const sessionId = this.sessionIdFromRequest(request);
1610
1475
  if (!sessionId)
@@ -1638,8 +1503,8 @@ class CookieSessionAuthManager extends AuthManager2 {
1638
1503
  super(new CookieSessionGuard(store, mapUser));
1639
1504
  this.store = store;
1640
1505
  }
1641
- async signIn(user) {
1642
- const sessionId = await this.store.create(user);
1506
+ async signIn(user, meta = {}) {
1507
+ const sessionId = await this.store.create(user, meta);
1643
1508
  return { sessionId, setCookie: this.store.cookieHeader(user, sessionId) };
1644
1509
  }
1645
1510
  async signOut(request) {
@@ -1649,8 +1514,8 @@ class CookieSessionAuthManager extends AuthManager2 {
1649
1514
  }
1650
1515
  return { setCookie: this.store.clearCookieHeader() };
1651
1516
  }
1652
- async signInRedirect(user, location, status = 302) {
1653
- const { setCookie } = await this.signIn(user);
1517
+ async signInRedirect(user, location, status = 302, meta = {}) {
1518
+ const { setCookie } = await this.signIn(user, meta);
1654
1519
  return redirectWithCookie(location, setCookie, status);
1655
1520
  }
1656
1521
  async signOutRedirect(request, location, status = 302) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.66",
3
+ "version": "0.2.68",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,24 +0,0 @@
1
- import type { RouteHandler } from "@getstrata/core/http/middleware";
2
- import type { AppDependencies } from "./contracts";
3
- declare function createScimRoutes(dependencies: AppDependencies): {
4
- "/scim/v2/ServiceProviderConfig": {
5
- GET: RouteHandler;
6
- };
7
- "/scim/v2/Users": {
8
- GET: RouteHandler;
9
- POST: RouteHandler;
10
- };
11
- "/scim/v2/Users/:id": {
12
- GET: RouteHandler;
13
- PATCH: RouteHandler;
14
- DELETE: RouteHandler;
15
- };
16
- "/scim/v2/Groups": {
17
- GET: RouteHandler;
18
- };
19
- "/scim/v2/Groups/:id": {
20
- GET: RouteHandler;
21
- PATCH: RouteHandler;
22
- };
23
- };
24
- export { createScimRoutes };