@jskit-ai/users-core 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.descriptor.mjs +19 -19
  2. package/package.json +6 -6
  3. package/src/server/accountNotifications/accountNotificationsService.js +3 -3
  4. package/src/server/accountNotifications/registerAccountNotifications.js +1 -1
  5. package/src/server/accountPreferences/accountPreferencesService.js +3 -3
  6. package/src/server/accountPreferences/registerAccountPreferences.js +1 -1
  7. package/src/server/accountProfile/accountProfileActions.js +8 -2
  8. package/src/server/accountProfile/accountProfileService.js +10 -10
  9. package/src/server/accountProfile/avatarService.js +9 -9
  10. package/src/server/accountProfile/bootAccountProfileRoutes.js +5 -3
  11. package/src/server/accountProfile/registerAccountProfile.js +2 -2
  12. package/src/server/accountSecurity/accountSecurityService.js +3 -3
  13. package/src/server/accountSecurity/registerAccountSecurity.js +1 -1
  14. package/src/server/common/contributors/workspaceActionContextContributor.js +24 -17
  15. package/src/server/common/registerCommonRepositories.js +3 -3
  16. package/src/server/common/repositories/{userProfilesRepository.js → usersRepository.js} +1 -1
  17. package/src/server/common/services/accountContextService.js +4 -4
  18. package/src/server/common/services/authProfileSyncService.js +10 -10
  19. package/src/server/registerWorkspaceBootstrap.js +1 -1
  20. package/src/server/registerWorkspaceCore.js +5 -2
  21. package/src/server/workspaceBootstrapContributor.js +4 -4
  22. package/src/shared/roles.js +31 -6
  23. package/templates/config/roles.js +27 -0
  24. package/test/authProfileSyncService.test.js +3 -3
  25. package/test/avatarService.test.js +2 -2
  26. package/test/roles.test.js +90 -5
  27. package/test/workspaceActionContextContributor.test.js +98 -5
  28. package/test/workspaceBootstrapContributor.test.js +7 -7
  29. package/test/workspaceMembersService.test.js +4 -2
  30. package/test/workspaceService.test.js +12 -8
  31. package/test/workspaceSettingsResource.test.js +4 -2
  32. package/templates/config/workspaceRoles.js +0 -30
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-core",
4
- version: "0.1.32",
4
+ version: "0.1.33",
5
5
  kind: "runtime",
6
6
  description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
7
7
  dependsOn: [
@@ -95,7 +95,7 @@ export default Object.freeze({
95
95
  {
96
96
  method: "GET",
97
97
  path: "/api/w/:workspaceSlug/roles",
98
- summary: "Get workspace role catalog by workspace slug."
98
+ summary: "Get role catalog by workspace slug."
99
99
  },
100
100
  {
101
101
  method: "GET",
@@ -198,11 +198,11 @@ export default Object.freeze({
198
198
  mutations: {
199
199
  dependencies: {
200
200
  runtime: {
201
- "@jskit-ai/auth-core": "0.1.22",
202
- "@jskit-ai/database-runtime": "0.1.23",
203
- "@jskit-ai/http-runtime": "0.1.22",
204
- "@jskit-ai/kernel": "0.1.23",
205
- "@jskit-ai/uploads-runtime": "0.1.1",
201
+ "@jskit-ai/auth-core": "0.1.23",
202
+ "@jskit-ai/database-runtime": "0.1.24",
203
+ "@jskit-ai/http-runtime": "0.1.23",
204
+ "@jskit-ai/kernel": "0.1.24",
205
+ "@jskit-ai/uploads-runtime": "0.1.2",
206
206
  "@fastify/type-provider-typebox": "^6.1.0",
207
207
  "typebox": "^1.0.81"
208
208
  },
@@ -290,12 +290,12 @@ export default Object.freeze({
290
290
  id: "users-core-app-owned-user-settings-fields"
291
291
  },
292
292
  {
293
- from: "templates/config/workspaceRoles.js",
294
- to: "config/workspaceRoles.js",
293
+ from: "templates/config/roles.js",
294
+ to: "config/roles.js",
295
295
  preserveOnRemove: true,
296
- reason: "Install app-owned workspace role catalog in a dedicated config file.",
296
+ reason: "Install app-owned role catalog in a dedicated config file.",
297
297
  category: "users-core",
298
- id: "users-core-app-owned-workspace-roles-config"
298
+ id: "users-core-app-owned-role-catalog-config"
299
299
  }
300
300
  ],
301
301
  text: [
@@ -362,11 +362,11 @@ export default Object.freeze({
362
362
  op: "append-text",
363
363
  file: "config/public.js",
364
364
  position: "top",
365
- skipIfContains: "import { workspaceRoles } from \"./workspaceRoles.js\";",
366
- value: "import { workspaceRoles } from \"./workspaceRoles.js\";\n",
367
- reason: "Load app-owned workspace role catalog from dedicated config file.",
365
+ skipIfContains: "import { roleCatalog } from \"./roles.js\";",
366
+ value: "import { roleCatalog } from \"./roles.js\";\n",
367
+ reason: "Load app-owned role catalog from dedicated config file.",
368
368
  category: "users-core",
369
- id: "users-core-workspace-roles-public-import"
369
+ id: "users-core-role-catalog-public-import"
370
370
  },
371
371
  {
372
372
  op: "append-text",
@@ -428,11 +428,11 @@ export default Object.freeze({
428
428
  op: "append-text",
429
429
  file: "config/public.js",
430
430
  position: "bottom",
431
- skipIfContains: "config.workspaceRoles = workspaceRoles;",
432
- value: "\nconfig.workspaceRoles = workspaceRoles;\n",
433
- reason: "Bind app-owned workspace role catalog onto public config.",
431
+ skipIfContains: "config.roleCatalog = roleCatalog;",
432
+ value: "\nconfig.roleCatalog = roleCatalog;\n",
433
+ reason: "Bind app-owned role catalog onto public config.",
434
434
  category: "users-core",
435
- id: "users-core-workspace-roles-public-config"
435
+ id: "users-core-role-catalog-public-config"
436
436
  },
437
437
  {
438
438
  op: "append-text",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-core",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -24,11 +24,11 @@
24
24
  "./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
25
25
  },
26
26
  "dependencies": {
27
- "@jskit-ai/auth-core": "0.1.22",
28
- "@jskit-ai/database-runtime": "0.1.23",
29
- "@jskit-ai/http-runtime": "0.1.22",
30
- "@jskit-ai/kernel": "0.1.23",
31
- "@jskit-ai/uploads-runtime": "0.1.1",
27
+ "@jskit-ai/auth-core": "0.1.23",
28
+ "@jskit-ai/database-runtime": "0.1.24",
29
+ "@jskit-ai/http-runtime": "0.1.23",
30
+ "@jskit-ai/kernel": "0.1.24",
31
+ "@jskit-ai/uploads-runtime": "0.1.2",
32
32
  "@fastify/type-provider-typebox": "^6.1.0",
33
33
  "typebox": "^1.0.81"
34
34
  }
@@ -9,15 +9,15 @@ import {
9
9
 
10
10
  function createService({
11
11
  userSettingsRepository,
12
- userProfilesRepository,
12
+ usersRepository,
13
13
  authService
14
14
  } = {}) {
15
- if (!userSettingsRepository || !userProfilesRepository) {
15
+ if (!userSettingsRepository || !usersRepository) {
16
16
  throw new Error("accountNotificationsService requires repositories.");
17
17
  }
18
18
 
19
19
  async function updateNotifications(request, user, payload = {}, options = {}) {
20
- const profile = await resolveUserProfile(userProfilesRepository, user);
20
+ const profile = await resolveUserProfile(usersRepository, user);
21
21
  if (!profile) {
22
22
  throw new AppError(404, "User profile was not found.");
23
23
  }
@@ -14,7 +14,7 @@ function registerAccountNotifications(app) {
14
14
  (scope) =>
15
15
  createAccountNotificationsService({
16
16
  userSettingsRepository: scope.make("userSettingsRepository"),
17
- userProfilesRepository: scope.make("userProfilesRepository"),
17
+ usersRepository: scope.make("usersRepository"),
18
18
  authService: scope.make("authService")
19
19
  }),
20
20
  {
@@ -9,15 +9,15 @@ import {
9
9
 
10
10
  function createService({
11
11
  userSettingsRepository,
12
- userProfilesRepository,
12
+ usersRepository,
13
13
  authService
14
14
  } = {}) {
15
- if (!userSettingsRepository || !userProfilesRepository) {
15
+ if (!userSettingsRepository || !usersRepository) {
16
16
  throw new Error("accountPreferencesService requires repositories.");
17
17
  }
18
18
 
19
19
  async function updatePreferences(request, user, payload = {}, options = {}) {
20
- const profile = await resolveUserProfile(userProfilesRepository, user);
20
+ const profile = await resolveUserProfile(usersRepository, user);
21
21
  if (!profile) {
22
22
  throw new AppError(404, "User profile was not found.");
23
23
  }
@@ -14,7 +14,7 @@ function registerAccountPreferences(app) {
14
14
  (scope) =>
15
15
  createAccountPreferencesService({
16
16
  userSettingsRepository: scope.make("userSettingsRepository"),
17
- userProfilesRepository: scope.make("userProfilesRepository"),
17
+ usersRepository: scope.make("usersRepository"),
18
18
  authService: scope.make("authService")
19
19
  }),
20
20
  {
@@ -95,10 +95,17 @@ const accountProfileActions = Object.freeze([
95
95
  },
96
96
  observability: {},
97
97
  async execute(input, context, deps) {
98
+ const avatarUpload = {
99
+ stream: input.stream,
100
+ mimeType: input.mimeType,
101
+ fileName: input.fileName,
102
+ uploadDimension: input.uploadDimension
103
+ };
104
+
98
105
  return deps.accountProfileService.uploadAvatar(
99
106
  resolveRequest(context),
100
107
  resolveActionUser(context, input),
101
- input,
108
+ avatarUpload,
102
109
  {
103
110
  context
104
111
  }
@@ -125,7 +132,6 @@ const accountProfileActions = Object.freeze([
125
132
  return deps.accountProfileService.deleteAvatar(
126
133
  resolveRequest(context),
127
134
  resolveActionUser(context, input),
128
- input,
129
135
  {
130
136
  context
131
137
  }
@@ -9,16 +9,16 @@ import {
9
9
 
10
10
  function createService({
11
11
  userSettingsRepository,
12
- userProfilesRepository,
12
+ usersRepository,
13
13
  authService,
14
14
  avatarService
15
15
  } = {}) {
16
- if (!userSettingsRepository || !userProfilesRepository || !avatarService) {
16
+ if (!userSettingsRepository || !usersRepository || !avatarService) {
17
17
  throw new Error("accountProfileService requires repositories and avatarService.");
18
18
  }
19
19
 
20
20
  async function getForUser(request, user, options = {}) {
21
- const profile = await resolveUserProfile(userProfilesRepository, user);
21
+ const profile = await resolveUserProfile(usersRepository, user);
22
22
  if (!profile) {
23
23
  throw new AppError(404, "User profile was not found.");
24
24
  }
@@ -35,7 +35,7 @@ function createService({
35
35
  }
36
36
 
37
37
  async function updateProfile(request, user, payload = {}, options = {}) {
38
- const profile = await resolveUserProfile(userProfilesRepository, user);
38
+ const profile = await resolveUserProfile(usersRepository, user);
39
39
  if (!profile) {
40
40
  throw new AppError(404, "User profile was not found.");
41
41
  }
@@ -49,7 +49,7 @@ function createService({
49
49
  }
50
50
 
51
51
  if (!updatedProfile) {
52
- updatedProfile = await userProfilesRepository.updateDisplayNameById(profile.id, payload.displayName);
52
+ updatedProfile = await usersRepository.updateDisplayNameById(profile.id, payload.displayName);
53
53
  }
54
54
 
55
55
  const settings = await userSettingsRepository.ensureForUserId(updatedProfile.id);
@@ -66,11 +66,11 @@ function createService({
66
66
  };
67
67
  }
68
68
 
69
- async function uploadAvatar(request, user, payload = {}, options = {}) {
69
+ async function uploadAvatar(request, user, avatarUpload = {}, options = {}) {
70
70
  void options;
71
71
 
72
- const avatarUpload = await avatarService.uploadForUser(user, payload);
73
- const profile = avatarUpload?.profile || null;
72
+ const result = await avatarService.uploadForUser(user, avatarUpload);
73
+ const profile = result?.profile || null;
74
74
  if (!profile) {
75
75
  throw new AppError(500, "Avatar upload completed without a profile result.");
76
76
  }
@@ -86,7 +86,7 @@ function createService({
86
86
  });
87
87
  }
88
88
 
89
- async function deleteAvatar(request, user, _payload = {}, options = {}) {
89
+ async function deleteAvatar(request, user, options = {}) {
90
90
  void options;
91
91
 
92
92
  const profile = await avatarService.clearForUser(user);
@@ -101,7 +101,7 @@ function createService({
101
101
  });
102
102
  }
103
103
 
104
- async function readAvatar(_request, user, _payload = {}, options = {}) {
104
+ async function readAvatar(_request, user, options = {}) {
105
105
  void options;
106
106
 
107
107
  const avatar = await avatarService.readForUser(user);
@@ -21,9 +21,9 @@ async function readAvatarBuffer(stream, { maxBytes = DEFAULT_AVATAR_POLICY.maxUp
21
21
  });
22
22
  }
23
23
 
24
- function createService({ userProfilesRepository, avatarStorageService, avatarPolicy } = {}) {
25
- if (!userProfilesRepository) {
26
- throw new TypeError("avatarService requires userProfilesRepository.");
24
+ function createService({ usersRepository, avatarStorageService, avatarPolicy } = {}) {
25
+ if (!usersRepository) {
26
+ throw new TypeError("avatarService requires usersRepository.");
27
27
  }
28
28
  if (!avatarStorageService) {
29
29
  throw new TypeError("avatarService requires avatarStorageService.");
@@ -32,21 +32,21 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
32
32
  const resolvedAvatarPolicy = resolveAvatarPolicy(avatarPolicy);
33
33
 
34
34
  async function resolveProfile(user) {
35
- const profile = await resolveUserProfile(userProfilesRepository, user);
35
+ const profile = await resolveUserProfile(usersRepository, user);
36
36
  if (!profile) {
37
37
  throw new AppError(404, "User profile was not found.");
38
38
  }
39
39
  return profile;
40
40
  }
41
41
 
42
- async function uploadForUser(user, payload = {}) {
42
+ async function uploadForUser(user, avatarUpload = {}) {
43
43
  const profile = await resolveProfile(user);
44
- validateUploadMimeType(payload?.mimeType, resolvedAvatarPolicy, {
44
+ validateUploadMimeType(avatarUpload?.mimeType, resolvedAvatarPolicy, {
45
45
  fieldName: "avatar",
46
46
  label: "Avatar"
47
47
  });
48
48
 
49
- const buffer = await readAvatarBuffer(payload.stream, {
49
+ const buffer = await readAvatarBuffer(avatarUpload?.stream, {
50
50
  maxBytes: resolvedAvatarPolicy.maxUploadBytes
51
51
  });
52
52
 
@@ -57,7 +57,7 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
57
57
  buffer
58
58
  });
59
59
 
60
- const updatedProfile = await userProfilesRepository.updateAvatarById(profile.id, {
60
+ const updatedProfile = await usersRepository.updateAvatarById(profile.id, {
61
61
  avatarStorageKey: savedAvatar.storageKey,
62
62
  avatarVersion,
63
63
  avatarUpdatedAt: new Date(avatarVersionMs)
@@ -73,7 +73,7 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
73
73
  if (profile.avatarStorageKey) {
74
74
  await avatarStorageService.deleteAvatar(profile.avatarStorageKey);
75
75
  }
76
- return userProfilesRepository.clearAvatarById(profile.id);
76
+ return usersRepository.clearAvatarById(profile.id);
77
77
  }
78
78
 
79
79
  async function readForUser(user) {
@@ -1,4 +1,5 @@
1
1
  import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
+ import { DEFAULT_IMAGE_UPLOAD_MAX_BYTES } from "@jskit-ai/uploads-runtime/shared";
2
3
  import { readSingleMultipartFile } from "@jskit-ai/uploads-runtime/server/multipart/readSingleMultipartFile";
3
4
  import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
4
5
  import { userProfileResource } from "../../shared/resources/userProfileResource.js";
@@ -77,7 +78,7 @@ function bootAccountProfileRoutes(app) {
77
78
  }
78
79
  },
79
80
  async function (request, reply) {
80
- const avatar = await accountProfileService.readAvatar(request, request.user, {}, {
81
+ const avatar = await accountProfileService.readAvatar(request, request.user, {
81
82
  context: {
82
83
  actor: request.user
83
84
  }
@@ -114,10 +115,11 @@ function bootAccountProfileRoutes(app) {
114
115
  },
115
116
  async function (request, reply) {
116
117
  const filePart = await readSingleMultipartFile(request, {
117
- fieldNames: ["avatar"],
118
+ fieldName: "avatar",
118
119
  required: true,
119
120
  fieldErrorKey: "avatar",
120
- label: "Avatar"
121
+ label: "Avatar",
122
+ maxBytes: DEFAULT_IMAGE_UPLOAD_MAX_BYTES
121
123
  });
122
124
 
123
125
  const uploadDimension = filePart.fields?.uploadDimension?.value;
@@ -20,7 +20,7 @@ function registerAccountProfile(app) {
20
20
 
21
21
  app.singleton("users.avatar.service", (scope) =>
22
22
  createAvatarService({
23
- userProfilesRepository: scope.make("userProfilesRepository"),
23
+ usersRepository: scope.make("usersRepository"),
24
24
  avatarStorageService: scope.make("users.avatar.storage.service")
25
25
  })
26
26
  );
@@ -30,7 +30,7 @@ function registerAccountProfile(app) {
30
30
  (scope) =>
31
31
  createAccountProfileService({
32
32
  userSettingsRepository: scope.make("userSettingsRepository"),
33
- userProfilesRepository: scope.make("userProfilesRepository"),
33
+ usersRepository: scope.make("usersRepository"),
34
34
  authService: scope.make("authService"),
35
35
  avatarService: scope.make("users.avatar.service")
36
36
  }),
@@ -10,10 +10,10 @@ import {
10
10
 
11
11
  function createService({
12
12
  userSettingsRepository,
13
- userProfilesRepository,
13
+ usersRepository,
14
14
  authService
15
15
  } = {}) {
16
- if (!userSettingsRepository || !userProfilesRepository) {
16
+ if (!userSettingsRepository || !usersRepository) {
17
17
  throw new Error("accountSecurityService requires repositories.");
18
18
  }
19
19
 
@@ -40,7 +40,7 @@ function createService({
40
40
  throw new AppError(501, "Password method toggle is not available.");
41
41
  }
42
42
 
43
- const profile = await resolveUserProfile(userProfilesRepository, user);
43
+ const profile = await resolveUserProfile(usersRepository, user);
44
44
  if (!profile) {
45
45
  throw new AppError(404, "User profile was not found.");
46
46
  }
@@ -11,7 +11,7 @@ function registerAccountSecurity(app) {
11
11
  const authService = scope.has("authService") ? scope.make("authService") : null;
12
12
  return createAccountSecurityService({
13
13
  userSettingsRepository: scope.make("userSettingsRepository"),
14
- userProfilesRepository: scope.make("userProfilesRepository"),
14
+ usersRepository: scope.make("usersRepository"),
15
15
  authService
16
16
  });
17
17
  });
@@ -2,6 +2,7 @@ import {
2
2
  normalizeObject,
3
3
  requireServiceMethod
4
4
  } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
5
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
5
6
  import {
6
7
  checkRouteVisibility,
7
8
  USERS_ROUTE_VISIBILITY_PUBLIC,
@@ -9,45 +10,51 @@ import {
9
10
  USERS_ROUTE_VISIBILITY_WORKSPACE_USER
10
11
  } from "../../../shared/support/usersVisibility.js";
11
12
  import { resolveActionUser } from "../support/resolveActionUser.js";
12
-
13
- const WORKSPACE_CONTEXT_ACTION_IDS = Object.freeze([
14
- "workspace.roles.list",
15
- "workspace.settings.read",
16
- "workspace.settings.update",
17
- "workspace.members.list",
18
- "workspace.member.role.update",
19
- "workspace.member.remove",
20
- "workspace.invites.list",
21
- "workspace.invite.create",
22
- "workspace.invite.revoke"
23
- ]);
24
13
  const WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET = new Set([
25
14
  USERS_ROUTE_VISIBILITY_WORKSPACE,
26
15
  USERS_ROUTE_VISIBILITY_WORKSPACE_USER
27
16
  ]);
28
17
 
29
- function createWorkspaceActionContextContributor({ workspaceService } = {}) {
18
+ function normalizeWorkspaceSurfaceIds(surfaceIds = []) {
19
+ const source = Array.isArray(surfaceIds) ? surfaceIds : [];
20
+ const normalized = new Set();
21
+
22
+ for (const entry of source) {
23
+ const surfaceId = normalizeSurfaceId(entry);
24
+ if (!surfaceId) {
25
+ continue;
26
+ }
27
+ normalized.add(surfaceId);
28
+ }
29
+
30
+ return normalized;
31
+ }
32
+
33
+ function createWorkspaceActionContextContributor({ workspaceService, workspaceSurfaceIds = [] } = {}) {
30
34
  const contributorId = "users.workspace.context";
35
+ const workspaceSurfaceIdSet = normalizeWorkspaceSurfaceIds(workspaceSurfaceIds);
31
36
 
32
37
  requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
33
38
 
34
39
  return Object.freeze({
35
40
  contributorId,
36
- async contribute({ actionId, input, context, request } = {}) {
41
+ async contribute({ definition = null, input, context, request } = {}) {
37
42
  const payload = normalizeObject(input);
38
43
  if (!Object.hasOwn(payload, "workspaceSlug")) {
39
44
  return {};
40
45
  }
41
46
 
42
- const actionName = String(actionId || "").trim();
43
- const hasLegacyWorkspaceActionId = WORKSPACE_CONTEXT_ACTION_IDS.includes(actionName);
47
+ const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
48
+ const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
49
+ const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
50
+ const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
44
51
  const routeVisibilityInput =
45
52
  request && request.routeOptions && request.routeOptions.config
46
53
  ? request.routeOptions.config.visibility
47
54
  : USERS_ROUTE_VISIBILITY_PUBLIC;
48
55
  const routeVisibility = checkRouteVisibility(routeVisibilityInput);
49
56
  const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
50
- if (!hasLegacyWorkspaceActionId && !hasWorkspaceRouteVisibility) {
57
+ if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
51
58
  return {};
52
59
  }
53
60
 
@@ -1,4 +1,4 @@
1
- import { createRepository as createUserProfilesRepository } from "./repositories/userProfilesRepository.js";
1
+ import { createRepository as createUsersRepository } from "./repositories/usersRepository.js";
2
2
  import { createRepository as createUserSettingsRepository } from "./repositories/userSettingsRepository.js";
3
3
  import { createRepository as createWorkspacesRepository } from "./repositories/workspacesRepository.js";
4
4
  import { createRepository as createWorkspaceMembershipsRepository } from "./repositories/workspaceMembershipsRepository.js";
@@ -10,9 +10,9 @@ function registerCommonRepositories(app) {
10
10
  throw new Error("registerCommonRepositories requires application singleton().");
11
11
  }
12
12
 
13
- app.singleton("userProfilesRepository", (scope) => {
13
+ app.singleton("usersRepository", (scope) => {
14
14
  const knex = scope.make("jskit.database.knex");
15
- return createUserProfilesRepository(knex);
15
+ return createUsersRepository(knex);
16
16
  });
17
17
 
18
18
  app.singleton("userSettingsRepository", (scope) => {
@@ -104,7 +104,7 @@ async function resolveUniqueUsername(client, baseUsername, { excludeUserId = 0 }
104
104
 
105
105
  function createRepository(knex) {
106
106
  if (typeof knex !== "function") {
107
- throw new TypeError("userProfilesRepository requires knex.");
107
+ throw new TypeError("usersRepository requires knex.");
108
108
  }
109
109
 
110
110
  async function findById(userId, options = {}) {
@@ -1,9 +1,9 @@
1
- import { normalizeIdentity } from "../repositories/userProfilesRepository.js";
1
+ import { normalizeIdentity } from "../repositories/usersRepository.js";
2
2
 
3
- async function resolveUserProfile(userProfilesRepository, user) {
3
+ async function resolveUserProfile(usersRepository, user) {
4
4
  const identity = normalizeIdentity(user);
5
5
  if (identity) {
6
- const profile = await userProfilesRepository.findByIdentity(identity);
6
+ const profile = await usersRepository.findByIdentity(identity);
7
7
  if (profile) {
8
8
  return profile;
9
9
  }
@@ -11,7 +11,7 @@ async function resolveUserProfile(userProfilesRepository, user) {
11
11
 
12
12
  const userId = Number(user?.id);
13
13
  if (Number.isInteger(userId) && userId > 0) {
14
- const profileById = await userProfilesRepository.findById(userId);
14
+ const profileById = await usersRepository.findById(userId);
15
15
  if (profileById) {
16
16
  return profileById;
17
17
  }
@@ -1,5 +1,5 @@
1
1
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
2
- import { normalizeIdentity } from "../repositories/userProfilesRepository.js";
2
+ import { normalizeIdentity } from "../repositories/usersRepository.js";
3
3
 
4
4
  function buildNormalizedIdentityKey(identityLike) {
5
5
  const identity = normalizeIdentity(identityLike);
@@ -53,12 +53,12 @@ function requireSynchronizedProfile(profile) {
53
53
  throw new Error("Profile synchronization failed.");
54
54
  }
55
55
 
56
- function createService({ userProfilesRepository, workspaceProvisioningService = null, userSettingsRepository = null } = {}) {
57
- if (!userProfilesRepository || typeof userProfilesRepository.findByIdentity !== "function") {
58
- throw new Error("authProfileSyncService requires userProfilesRepository.findByIdentity().");
56
+ function createService({ usersRepository, workspaceProvisioningService = null, userSettingsRepository = null } = {}) {
57
+ if (!usersRepository || typeof usersRepository.findByIdentity !== "function") {
58
+ throw new Error("authProfileSyncService requires usersRepository.findByIdentity().");
59
59
  }
60
- if (typeof userProfilesRepository.upsert !== "function") {
61
- throw new Error("authProfileSyncService requires userProfilesRepository.upsert().");
60
+ if (typeof usersRepository.upsert !== "function") {
61
+ throw new Error("authProfileSyncService requires usersRepository.upsert().");
62
62
  }
63
63
  if (!userSettingsRepository || typeof userSettingsRepository.ensureForUserId !== "function") {
64
64
  throw new Error("authProfileSyncService requires userSettingsRepository.ensureForUserId().");
@@ -66,7 +66,7 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
66
66
 
67
67
  async function findByIdentity(identityLike, options = {}) {
68
68
  const normalized = buildNormalizedIdentityKey(identityLike);
69
- return userProfilesRepository.findByIdentity(
69
+ return usersRepository.findByIdentity(
70
70
  {
71
71
  provider: normalized.authProvider,
72
72
  providerUserId: normalized.authProviderUserSid
@@ -77,7 +77,7 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
77
77
 
78
78
  async function upsertByIdentity(profileLike, options = {}) {
79
79
  const normalized = buildNormalizedIdentityProfile(profileLike);
80
- return userProfilesRepository.upsert(
80
+ return usersRepository.upsert(
81
81
  {
82
82
  authProvider: normalized.authProvider,
83
83
  authProviderUserSid: normalized.authProviderUserSid,
@@ -118,8 +118,8 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
118
118
  if (options?.trx) {
119
119
  return runSync(options.trx);
120
120
  }
121
- if (typeof userProfilesRepository.withTransaction === "function") {
122
- return userProfilesRepository.withTransaction((trx) => runSync(trx));
121
+ if (typeof usersRepository.withTransaction === "function") {
122
+ return usersRepository.withTransaction((trx) => runSync(trx));
123
123
  }
124
124
  return runSync();
125
125
  }
@@ -17,7 +17,7 @@ function registerWorkspaceBootstrap(app) {
17
17
  ? scope.make("users.workspace.pending-invitations.service")
18
18
  : null,
19
19
  workspaceInvitationsEnabled,
20
- userProfilesRepository: scope.make("userProfilesRepository"),
20
+ usersRepository: scope.make("usersRepository"),
21
21
  userSettingsRepository: scope.make("userSettingsRepository"),
22
22
  appConfig: resolveAppConfig(scope),
23
23
  tenancyProfile: scope.make("users.tenancy.profile"),
@@ -10,6 +10,7 @@ import { createWorkspaceActionContextContributor } from "./common/contributors/w
10
10
  import { createWorkspaceRouteVisibilityResolver } from "./common/contributors/workspaceRouteVisibilityResolver.js";
11
11
  import { createWorkspaceAuthPolicyContextResolver } from "./common/contributors/workspaceAuthPolicyContextResolver.js";
12
12
  import { resolveWorkspaceInvitationsPolicy } from "./support/workspaceInvitationsPolicy.js";
13
+ import { resolveWorkspaceSurfaceIdsFromAppConfig } from "./support/workspaceActionSurfaces.js";
13
14
 
14
15
 
15
16
  function registerWorkspaceCore(app) {
@@ -29,7 +30,7 @@ function registerWorkspaceCore(app) {
29
30
 
30
31
  app.singleton("users.profile.sync.service", (scope) => {
31
32
  return createAuthProfileSyncService({
32
- userProfilesRepository: scope.make("userProfilesRepository"),
33
+ usersRepository: scope.make("usersRepository"),
33
34
  userSettingsRepository: scope.make("userSettingsRepository"),
34
35
  workspaceProvisioningService: scope.make("users.workspace.service")
35
36
  });
@@ -62,8 +63,10 @@ function registerWorkspaceCore(app) {
62
63
  });
63
64
 
64
65
  registerActionContextContributor(app, "users.core.workspace.actionContextContributor", (scope) => {
66
+ const appConfig = resolveAppConfig(scope);
65
67
  return createWorkspaceActionContextContributor({
66
- workspaceService: scope.make("users.workspace.service")
68
+ workspaceService: scope.make("users.workspace.service"),
69
+ workspaceSurfaceIds: resolveWorkspaceSurfaceIdsFromAppConfig(appConfig)
67
70
  });
68
71
  });
69
72
 
@@ -230,7 +230,7 @@ function mapUserSettingsBootstrap(settings = {}) {
230
230
  function createWorkspaceBootstrapContributor({
231
231
  workspaceService,
232
232
  workspacePendingInvitationsService,
233
- userProfilesRepository,
233
+ usersRepository,
234
234
  userSettingsRepository,
235
235
  workspaceInvitationsEnabled = false,
236
236
  appConfig = {},
@@ -255,8 +255,8 @@ function createWorkspaceBootstrapContributor({
255
255
  serviceLabel: "workspacePendingInvitationsService"
256
256
  });
257
257
  }
258
- requireServiceMethod(userProfilesRepository, "findByIdentity", contributorId, {
259
- serviceLabel: "userProfilesRepository"
258
+ requireServiceMethod(usersRepository, "findByIdentity", contributorId, {
259
+ serviceLabel: "usersRepository"
260
260
  });
261
261
  requireServiceMethod(userSettingsRepository, "ensureForUserId", contributorId, {
262
262
  serviceLabel: "userSettingsRepository"
@@ -317,7 +317,7 @@ function createWorkspaceBootstrapContributor({
317
317
 
318
318
  if (normalizedUser) {
319
319
  const latestProfile =
320
- (await userProfilesRepository.findByIdentity({
320
+ (await usersRepository.findByIdentity({
321
321
  provider: normalizedUser.authProvider,
322
322
  providerUserId: normalizedUser.authProviderUserSid
323
323
  })) || normalizedUser;
@@ -20,10 +20,35 @@ function normalizeRoleId(value) {
20
20
  .toLowerCase();
21
21
  }
22
22
 
23
- function createRoleDescriptor(roleSid, configuredDefinition) {
23
+ function resolveInheritedRolePermissions(roleSid, configuredRoles = {}, seenRoleIds = new Set()) {
24
+ if (seenRoleIds.has(roleSid)) {
25
+ throw new TypeError(`roleCatalog role "${roleSid}" has circular inheritance.`);
26
+ }
27
+
28
+ const source = asRecord(configuredRoles[roleSid]);
29
+ const inheritedRoleId = normalizeRoleId(source.inherits);
30
+ const directPermissions = normalizePermissionList(source.permissions);
31
+ if (!inheritedRoleId) {
32
+ return directPermissions;
33
+ }
34
+
35
+ if (!Object.hasOwn(configuredRoles, inheritedRoleId)) {
36
+ throw new TypeError(`roleCatalog role "${roleSid}" inherits unknown role "${inheritedRoleId}".`);
37
+ }
38
+
39
+ const nextSeenRoleIds = new Set(seenRoleIds);
40
+ nextSeenRoleIds.add(roleSid);
41
+
42
+ return normalizePermissionList([
43
+ ...resolveInheritedRolePermissions(inheritedRoleId, configuredRoles, nextSeenRoleIds),
44
+ ...directPermissions
45
+ ]);
46
+ }
47
+
48
+ function createRoleDescriptor(roleSid, configuredDefinition, configuredRoles = {}) {
24
49
  const source = asRecord(configuredDefinition);
25
50
  const assignable = roleSid === OWNER_ROLE_ID ? false : source.assignable === true;
26
- const permissions = normalizePermissionList(source.permissions);
51
+ const permissions = resolveInheritedRolePermissions(roleSid, configuredRoles);
27
52
 
28
53
  return Object.freeze({
29
54
  id: roleSid,
@@ -38,12 +63,12 @@ function listConfiguredRoleIds(appConfig = {}) {
38
63
  }
39
64
 
40
65
  function resolveConfiguredDefaultInviteRole(appConfig = {}) {
41
- return normalizeRoleId(appConfig?.workspaceRoles?.defaultInviteRole);
66
+ return normalizeRoleId(appConfig?.roleCatalog?.workspace?.defaultInviteRole);
42
67
  }
43
68
 
44
69
  function normalizeConfiguredRoles(appConfig = {}) {
45
- const workspaceRoles = asRecord(appConfig?.workspaceRoles);
46
- const configuredRoles = asRecord(workspaceRoles.roles);
70
+ const roleCatalog = asRecord(appConfig?.roleCatalog);
71
+ const configuredRoles = asRecord(roleCatalog.roles);
47
72
  const normalizedRoles = {};
48
73
 
49
74
  for (const [roleSid, roleDefinition] of Object.entries(configuredRoles)) {
@@ -60,7 +85,7 @@ function normalizeConfiguredRoles(appConfig = {}) {
60
85
  function createWorkspaceRoleCatalog(appConfig = {}) {
61
86
  const configuredRoles = normalizeConfiguredRoles(appConfig);
62
87
  const roleIds = listConfiguredRoleIds(appConfig);
63
- const roles = roleIds.map((roleSid) => createRoleDescriptor(roleSid, configuredRoles[roleSid]));
88
+ const roles = roleIds.map((roleSid) => createRoleDescriptor(roleSid, configuredRoles[roleSid], configuredRoles));
64
89
  const assignableRoleIds = roles.filter((role) => role.assignable).map((role) => role.id);
65
90
  const configuredDefaultInviteRole = resolveConfiguredDefaultInviteRole(appConfig);
66
91
  const defaultInviteRole = assignableRoleIds.includes(configuredDefaultInviteRole)
@@ -0,0 +1,27 @@
1
+ export const roleCatalog = {
2
+ workspace: {
3
+ defaultInviteRole: "member"
4
+ },
5
+ roles: {
6
+ owner: {
7
+ assignable: false,
8
+ permissions: ["*"]
9
+ },
10
+ admin: {
11
+ assignable: true,
12
+ inherits: "member",
13
+ permissions: [
14
+ "workspace.roles.view",
15
+ "workspace.settings.update",
16
+ "workspace.members.view",
17
+ "workspace.members.invite",
18
+ "workspace.members.manage",
19
+ "workspace.invites.revoke"
20
+ ]
21
+ },
22
+ member: {
23
+ assignable: true,
24
+ permissions: ["workspace.settings.view"]
25
+ }
26
+ }
27
+ };
@@ -7,7 +7,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
7
7
  const transaction = { trxId: "tx-1" };
8
8
 
9
9
  const service = createService({
10
- userProfilesRepository: {
10
+ usersRepository: {
11
11
  async findByIdentity(_identity, options = {}) {
12
12
  calls.push({ step: "find", trx: options.trx || null });
13
13
  return null;
@@ -64,7 +64,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
64
64
  let provisionCalls = 0;
65
65
 
66
66
  const service = createService({
67
- userProfilesRepository: {
67
+ usersRepository: {
68
68
  async findByIdentity() {
69
69
  return {
70
70
  id: 7,
@@ -109,7 +109,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
109
109
  test("authProfileSyncService.findByIdentity normalizes provider identity input", async () => {
110
110
  let capturedIdentity = null;
111
111
  const service = createService({
112
- userProfilesRepository: {
112
+ usersRepository: {
113
113
  async findByIdentity(identity) {
114
114
  capturedIdentity = identity;
115
115
  return null;
@@ -59,7 +59,7 @@ test("avatarService uploadForUser stores bytes and updates profile avatar fields
59
59
  };
60
60
 
61
61
  const avatarService = createService({
62
- userProfilesRepository: repository,
62
+ usersRepository: repository,
63
63
  avatarStorageService
64
64
  });
65
65
 
@@ -99,7 +99,7 @@ test("avatarService clearForUser removes stored avatar and clears profile fields
99
99
  };
100
100
 
101
101
  const avatarService = createService({
102
- userProfilesRepository: repository,
102
+ usersRepository: repository,
103
103
  avatarStorageService
104
104
  });
105
105
 
@@ -7,7 +7,7 @@ import {
7
7
  hasPermission
8
8
  } from "../src/shared/roles.js";
9
9
 
10
- test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.workspaceRoles", () => {
10
+ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.roleCatalog", () => {
11
11
  const emptyCatalog = createWorkspaceRoleCatalog();
12
12
  assert.deepEqual(emptyCatalog.roles, []);
13
13
  assert.deepEqual(emptyCatalog.assignableRoleIds, []);
@@ -15,8 +15,10 @@ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.w
15
15
  assert.equal(emptyCatalog.collaborationEnabled, false);
16
16
 
17
17
  const appConfig = {
18
- workspaceRoles: {
19
- defaultInviteRole: "editor",
18
+ roleCatalog: {
19
+ workspace: {
20
+ defaultInviteRole: "editor"
21
+ },
20
22
  roles: {
21
23
  owner: {
22
24
  assignable: false,
@@ -24,7 +26,7 @@ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.w
24
26
  },
25
27
  editor: {
26
28
  assignable: true,
27
- permissions: ["crud_contacts.*"]
29
+ permissions: ["crud.contacts.*"]
28
30
  }
29
31
  }
30
32
  }
@@ -35,7 +37,90 @@ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.w
35
37
  assert.equal(roleCatalog.defaultInviteRole, "editor");
36
38
  assert.equal(roleCatalog.assignableRoleIds.includes("editor"), true);
37
39
  assert.deepEqual(resolveRolePermissions("owner", appConfig), ["workspace.settings.update"]);
38
- assert.equal(hasPermission(editorRole?.permissions, "crud_contacts.update"), true);
40
+ assert.equal(hasPermission(editorRole?.permissions, "crud.contacts.update"), true);
41
+ });
42
+
43
+ test("createWorkspaceRoleCatalog resolves inherited role permissions with parent permissions first", () => {
44
+ const appConfig = {
45
+ roleCatalog: {
46
+ workspace: {
47
+ defaultInviteRole: "member"
48
+ },
49
+ roles: {
50
+ member: {
51
+ assignable: true,
52
+ permissions: [
53
+ "workspace.settings.view",
54
+ "crud.contacts.list"
55
+ ]
56
+ },
57
+ admin: {
58
+ assignable: true,
59
+ inherits: "member",
60
+ permissions: [
61
+ "workspace.settings.update",
62
+ "workspace.members.manage",
63
+ "workspace.settings.view"
64
+ ]
65
+ }
66
+ }
67
+ }
68
+ };
69
+
70
+ const roleCatalog = createWorkspaceRoleCatalog(appConfig);
71
+ const adminRole = roleCatalog.roles.find((role) => role.id === "admin");
72
+
73
+ assert.deepEqual(adminRole, {
74
+ id: "admin",
75
+ assignable: true,
76
+ permissions: [
77
+ "workspace.settings.view",
78
+ "crud.contacts.list",
79
+ "workspace.settings.update",
80
+ "workspace.members.manage"
81
+ ]
82
+ });
83
+ });
84
+
85
+ test("createWorkspaceRoleCatalog rejects unknown inherited roles", () => {
86
+ assert.throws(
87
+ () =>
88
+ createWorkspaceRoleCatalog({
89
+ roleCatalog: {
90
+ roles: {
91
+ admin: {
92
+ assignable: true,
93
+ inherits: "member",
94
+ permissions: []
95
+ }
96
+ }
97
+ }
98
+ }),
99
+ /inherits unknown role "member"/
100
+ );
101
+ });
102
+
103
+ test("createWorkspaceRoleCatalog rejects circular inherited roles", () => {
104
+ assert.throws(
105
+ () =>
106
+ createWorkspaceRoleCatalog({
107
+ roleCatalog: {
108
+ roles: {
109
+ member: {
110
+ assignable: true,
111
+ inherits: "admin",
112
+ permissions: []
113
+ },
114
+ admin: {
115
+ assignable: true,
116
+ inherits: "member",
117
+ permissions: []
118
+ }
119
+ }
120
+ }
121
+ }),
122
+ /circular inheritance/
123
+ );
39
124
  });
40
125
 
41
126
  test("cloneWorkspaceRoleCatalog normalizes role ids and returns detached arrays", () => {
@@ -24,7 +24,8 @@ test("workspace action context contributor resolves workspace context for worksp
24
24
  permissions: ["workspace.settings.update"]
25
25
  };
26
26
  }
27
- }
27
+ },
28
+ workspaceSurfaceIds: ["admin", "app"]
28
29
  });
29
30
 
30
31
  const request = {
@@ -35,7 +36,10 @@ test("workspace action context contributor resolves workspace context for worksp
35
36
  };
36
37
 
37
38
  const contribution = await contributor.contribute({
38
- actionId: "workspace.settings.update",
39
+ definition: {
40
+ id: "workspace.settings.update",
41
+ surfaces: ["admin", "app"]
42
+ },
39
43
  input: {
40
44
  workspaceSlug: "Acme"
41
45
  },
@@ -118,7 +122,8 @@ test("workspace action context contributor always resolves and stores resolved c
118
122
  permissions: ["workspace.settings.update"]
119
123
  };
120
124
  }
121
- }
125
+ },
126
+ workspaceSurfaceIds: ["admin", "app"]
122
127
  });
123
128
 
124
129
  const request = {
@@ -128,7 +133,10 @@ test("workspace action context contributor always resolves and stores resolved c
128
133
  };
129
134
 
130
135
  const contribution = await contributor.contribute({
131
- actionId: "workspace.members.list",
136
+ definition: {
137
+ id: "workspace.members.list",
138
+ surfaces: ["admin", "app"]
139
+ },
132
140
  input: {
133
141
  workspaceSlug: "acme"
134
142
  },
@@ -205,7 +213,10 @@ test("workspace action context contributor resolves context for workspace-visibl
205
213
  };
206
214
 
207
215
  const contribution = await contributor.contribute({
208
- actionId: "assistant.conversations.list",
216
+ definition: {
217
+ id: "assistant.conversations.list",
218
+ surfaces: ["admin"]
219
+ },
209
220
  input: {
210
221
  workspaceSlug: "acme"
211
222
  },
@@ -249,3 +260,85 @@ test("workspace action context contributor resolves context for workspace-visibl
249
260
  permissions: ["assistant.chat.use"]
250
261
  });
251
262
  });
263
+
264
+ test("workspace action context contributor resolves context for workspace surfaces even when route visibility is public", async () => {
265
+ const calls = [];
266
+ const contributor = createWorkspaceActionContextContributor({
267
+ workspaceService: {
268
+ async resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options) {
269
+ calls.push({ user, workspaceSlug, options });
270
+ return {
271
+ workspace: {
272
+ id: 77,
273
+ slug: "acme"
274
+ },
275
+ membership: {
276
+ roleSid: "member"
277
+ },
278
+ permissions: ["crud.breeds.list"]
279
+ };
280
+ }
281
+ },
282
+ workspaceSurfaceIds: ["admin", "app"]
283
+ });
284
+
285
+ const request = {
286
+ user: {
287
+ id: 42
288
+ },
289
+ routeOptions: {
290
+ config: {
291
+ surface: "admin",
292
+ visibility: "public"
293
+ }
294
+ }
295
+ };
296
+
297
+ const contribution = await contributor.contribute({
298
+ definition: {
299
+ id: "crud.breeds.list",
300
+ surfaces: ["admin"]
301
+ },
302
+ input: {
303
+ workspaceSlug: "acme"
304
+ },
305
+ context: {
306
+ requestMeta: {
307
+ request
308
+ }
309
+ },
310
+ request
311
+ });
312
+
313
+ assert.deepEqual(calls, [
314
+ {
315
+ user: request.user,
316
+ workspaceSlug: "acme",
317
+ options: {
318
+ request
319
+ }
320
+ }
321
+ ]);
322
+ assert.deepEqual(contribution, {
323
+ requestMeta: {
324
+ resolvedWorkspaceContext: {
325
+ workspace: {
326
+ id: 77,
327
+ slug: "acme"
328
+ },
329
+ membership: {
330
+ roleSid: "member"
331
+ },
332
+ permissions: ["crud.breeds.list"]
333
+ }
334
+ },
335
+ workspace: {
336
+ id: 77,
337
+ slug: "acme"
338
+ },
339
+ membership: {
340
+ roleSid: "member"
341
+ },
342
+ permissions: ["crud.breeds.list"]
343
+ });
344
+ });
@@ -39,7 +39,7 @@ test("workspace bootstrap contributor passes actor context to pending invites se
39
39
  return [];
40
40
  }
41
41
  },
42
- userProfilesRepository: {
42
+ usersRepository: {
43
43
  async findByIdentity() {
44
44
  return profile;
45
45
  }
@@ -101,7 +101,7 @@ test("workspace bootstrap contributor seeds the initial console owner on authent
101
101
  return [];
102
102
  }
103
103
  },
104
- userProfilesRepository: {
104
+ usersRepository: {
105
105
  async findByIdentity() {
106
106
  return profile;
107
107
  }
@@ -162,7 +162,7 @@ test("workspace bootstrap contributor emits canonical tenancy profile from users
162
162
  return [];
163
163
  }
164
164
  },
165
- userProfilesRepository: {
165
+ usersRepository: {
166
166
  async findByIdentity() {
167
167
  return null;
168
168
  }
@@ -228,7 +228,7 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
228
228
  return [];
229
229
  }
230
230
  },
231
- userProfilesRepository: {
231
+ usersRepository: {
232
232
  async findByIdentity() {
233
233
  return profile;
234
234
  }
@@ -291,7 +291,7 @@ test("workspace bootstrap contributor returns global payload with requestedWorks
291
291
  return [];
292
292
  }
293
293
  },
294
- userProfilesRepository: {
294
+ usersRepository: {
295
295
  async findByIdentity() {
296
296
  return profile;
297
297
  }
@@ -363,7 +363,7 @@ test("workspace bootstrap contributor returns requestedWorkspace=not_found when
363
363
  return [];
364
364
  }
365
365
  },
366
- userProfilesRepository: {
366
+ usersRepository: {
367
367
  async findByIdentity() {
368
368
  return profile;
369
369
  }
@@ -427,7 +427,7 @@ test("workspace bootstrap contributor returns requestedWorkspace=unauthenticated
427
427
  return [];
428
428
  }
429
429
  },
430
- userProfilesRepository: {
430
+ usersRepository: {
431
431
  async findByIdentity() {
432
432
  return null;
433
433
  }
@@ -16,8 +16,10 @@ function authorizedOptions(permissions = []) {
16
16
 
17
17
  function createRoleCatalog() {
18
18
  return createWorkspaceRoleCatalog({
19
- workspaceRoles: {
20
- defaultInviteRole: "member",
19
+ roleCatalog: {
20
+ workspace: {
21
+ defaultInviteRole: "member"
22
+ },
21
23
  roles: {
22
24
  owner: {
23
25
  assignable: false,
@@ -2,9 +2,11 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { createService } from "../src/server/common/services/workspaceContextService.js";
4
4
 
5
- function createWorkspaceRoles() {
5
+ function createRoleCatalog() {
6
6
  return {
7
- defaultInviteRole: "member",
7
+ workspace: {
8
+ defaultInviteRole: "member"
9
+ },
8
10
  roles: {
9
11
  owner: {
10
12
  assignable: false,
@@ -21,7 +23,7 @@ function createWorkspaceRoles() {
21
23
  function createWorkspaceServiceFixture({
22
24
  tenancyMode = "workspaces",
23
25
  tenancyPolicy = {},
24
- workspaceRoles = createWorkspaceRoles(),
26
+ roleCatalog = createRoleCatalog(),
25
27
  additionalWorkspaces = [],
26
28
  userWorkspaceRows = null,
27
29
  membershipResolver = null,
@@ -69,7 +71,7 @@ function createWorkspaceServiceFixture({
69
71
  appConfig: {
70
72
  tenancyMode,
71
73
  tenancyPolicy,
72
- workspaceRoles: workspaceRoles && typeof workspaceRoles === "object" ? { ...workspaceRoles } : workspaceRoles
74
+ roleCatalog: roleCatalog && typeof roleCatalog === "object" ? { ...roleCatalog } : roleCatalog
73
75
  },
74
76
  workspacesRepository: {
75
77
  async findBySlug(slug) {
@@ -404,7 +406,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
404
406
  const service = createService({
405
407
  appConfig: {
406
408
  tenancyMode: "personal",
407
- workspaceRoles: createWorkspaceRoles()
409
+ roleCatalog: createRoleCatalog()
408
410
  },
409
411
  workspacesRepository: {
410
412
  async findBySlug(slug) {
@@ -468,10 +470,12 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
468
470
  assert.deepEqual(context.permissions, ["*"]);
469
471
  });
470
472
 
471
- test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions from appConfig.workspaceRoles", async () => {
473
+ test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions from appConfig.roleCatalog", async () => {
472
474
  const { service } = createWorkspaceServiceFixture({
473
- workspaceRoles: {
474
- defaultInviteRole: "member",
475
+ roleCatalog: {
476
+ workspace: {
477
+ defaultInviteRole: "member"
478
+ },
475
479
  roles: {
476
480
  owner: {
477
481
  assignable: false,
@@ -8,8 +8,10 @@ import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
8
8
 
9
9
  function createRoleCatalog() {
10
10
  return createWorkspaceRoleCatalog({
11
- workspaceRoles: {
12
- defaultInviteRole: "member",
11
+ roleCatalog: {
12
+ workspace: {
13
+ defaultInviteRole: "member"
14
+ },
13
15
  roles: {
14
16
  owner: {
15
17
  assignable: false,
@@ -1,30 +0,0 @@
1
- export const workspaceRoles = {};
2
-
3
- workspaceRoles.defaultInviteRole = "member";
4
- workspaceRoles.roles = {};
5
-
6
- workspaceRoles.roles.owner = {
7
- assignable: false,
8
- permissions: []
9
- };
10
- workspaceRoles.roles.owner.permissions.push("*");
11
-
12
- workspaceRoles.roles.admin = {
13
- assignable: true,
14
- permissions: []
15
- };
16
- workspaceRoles.roles.admin.permissions.push(
17
- "workspace.roles.view",
18
- "workspace.settings.view",
19
- "workspace.settings.update",
20
- "workspace.members.view",
21
- "workspace.members.invite",
22
- "workspace.members.manage",
23
- "workspace.invites.revoke"
24
- );
25
-
26
- workspaceRoles.roles.member = {
27
- assignable: true,
28
- permissions: []
29
- };
30
- workspaceRoles.roles.member.permissions.push("workspace.settings.view");