@jskit-ai/users-core 0.1.4

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 (148) hide show
  1. package/package.descriptor.mjs +464 -0
  2. package/package.json +35 -0
  3. package/src/server/UsersCoreServiceProvider.js +74 -0
  4. package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
  5. package/src/server/accountNotifications/accountNotificationsService.js +41 -0
  6. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
  7. package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
  8. package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
  9. package/src/server/accountPreferences/accountPreferencesService.js +41 -0
  10. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
  11. package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
  12. package/src/server/accountProfile/accountProfileActions.js +137 -0
  13. package/src/server/accountProfile/accountProfileService.js +124 -0
  14. package/src/server/accountProfile/avatarService.js +141 -0
  15. package/src/server/accountProfile/avatarStorageService.js +132 -0
  16. package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
  17. package/src/server/accountProfile/registerAccountProfile.js +62 -0
  18. package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
  19. package/src/server/accountSecurity/accountSecurityActions.js +144 -0
  20. package/src/server/accountSecurity/accountSecurityService.js +103 -0
  21. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
  22. package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
  23. package/src/server/common/README.md +21 -0
  24. package/src/server/common/contributors/README.md +11 -0
  25. package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
  26. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
  27. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
  28. package/src/server/common/diTokens.js +21 -0
  29. package/src/server/common/formatters/README.md +11 -0
  30. package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
  31. package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
  32. package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
  33. package/src/server/common/formatters/workspaceFormatter.js +46 -0
  34. package/src/server/common/registerCommonRepositories.js +45 -0
  35. package/src/server/common/registerSharedApi.js +9 -0
  36. package/src/server/common/repositories/README.md +24 -0
  37. package/src/server/common/repositories/repositoryUtils.js +50 -0
  38. package/src/server/common/repositories/userProfilesRepository.js +251 -0
  39. package/src/server/common/repositories/userSettingsRepository.js +179 -0
  40. package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
  41. package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
  42. package/src/server/common/repositories/workspacesRepository.js +183 -0
  43. package/src/server/common/routes/README.md +11 -0
  44. package/src/server/common/services/README.md +12 -0
  45. package/src/server/common/services/accountContextService.js +31 -0
  46. package/src/server/common/services/authProfileSyncService.js +128 -0
  47. package/src/server/common/services/workspaceContextService.js +270 -0
  48. package/src/server/common/support/deepFreeze.js +17 -0
  49. package/src/server/common/support/realtimeServiceEvents.js +94 -0
  50. package/src/server/common/support/resolveActionUser.js +11 -0
  51. package/src/server/common/support/workspaceRoutePaths.js +17 -0
  52. package/src/server/common/validators/README.md +11 -0
  53. package/src/server/common/validators/authenticatedUserValidator.js +42 -0
  54. package/src/server/common/validators/routeParamsValidator.js +62 -0
  55. package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
  56. package/src/server/consoleSettings/consoleService.js +36 -0
  57. package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
  58. package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
  59. package/src/server/consoleSettings/consoleSettingsService.js +40 -0
  60. package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
  61. package/src/server/registerWorkspaceBootstrap.js +36 -0
  62. package/src/server/registerWorkspaceCore.js +95 -0
  63. package/src/server/support/resolveWorkspace.js +16 -0
  64. package/src/server/support/workspaceActionSurfaces.js +135 -0
  65. package/src/server/support/workspaceInvitationsPolicy.js +45 -0
  66. package/src/server/support/workspaceRouteInput.js +22 -0
  67. package/src/server/workspaceBootstrapContributor.js +401 -0
  68. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
  69. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
  70. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
  71. package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
  72. package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
  73. package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
  74. package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
  75. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
  76. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
  77. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
  78. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
  79. package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
  80. package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
  81. package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
  82. package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
  83. package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
  84. package/src/shared/events/usersEvents.js +19 -0
  85. package/src/shared/index.js +91 -0
  86. package/src/shared/operationMessages.js +16 -0
  87. package/src/shared/resources/consoleSettingsFields.js +55 -0
  88. package/src/shared/resources/consoleSettingsResource.js +139 -0
  89. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  90. package/src/shared/resources/userProfileResource.js +148 -0
  91. package/src/shared/resources/userSettingsFields.js +71 -0
  92. package/src/shared/resources/userSettingsResource.js +416 -0
  93. package/src/shared/resources/workspaceMembersResource.js +352 -0
  94. package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
  95. package/src/shared/resources/workspaceResource.js +149 -0
  96. package/src/shared/resources/workspaceSettingsFields.js +60 -0
  97. package/src/shared/resources/workspaceSettingsResource.js +178 -0
  98. package/src/shared/roles.js +136 -0
  99. package/src/shared/settings.js +31 -0
  100. package/src/shared/support/usersApiPaths.js +34 -0
  101. package/src/shared/support/usersVisibility.js +45 -0
  102. package/src/shared/support/workspacePathModel.js +145 -0
  103. package/src/shared/tenancyMode.js +35 -0
  104. package/src/shared/tenancyProfile.js +73 -0
  105. package/templates/config/workspaceRoles.js +30 -0
  106. package/templates/migrations/users_core_console_owner.cjs +39 -0
  107. package/templates/migrations/users_core_initial.cjs +118 -0
  108. package/templates/migrations/users_core_profile_username.cjs +98 -0
  109. package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
  110. package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
  111. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
  112. package/test/authProfileSyncService.test.js +119 -0
  113. package/test/avatarService.test.js +114 -0
  114. package/test/avatarStorageService.test.js +61 -0
  115. package/test/consoleService.test.js +57 -0
  116. package/test/consoleSettingsService.test.js +86 -0
  117. package/test/exportsContract.test.js +38 -0
  118. package/test/registerAvatarMultipartSupport.test.js +64 -0
  119. package/test/registerServiceRealtimeEvents.test.js +160 -0
  120. package/test/registerWorkspaceDirectory.test.js +26 -0
  121. package/test/registerWorkspaceSettings.test.js +44 -0
  122. package/test/resourcesCanonical.test.js +90 -0
  123. package/test/roles.test.js +74 -0
  124. package/test/settingsFieldRegistriesSingleton.test.js +24 -0
  125. package/test/tenancyProfile.test.js +67 -0
  126. package/test/userSettingsResource.test.js +31 -0
  127. package/test/usersApiPaths.test.js +31 -0
  128. package/test/usersRouteRequestInputValidator.test.js +556 -0
  129. package/test/usersRouteResources.test.js +113 -0
  130. package/test/usersRouteValidators.test.js +49 -0
  131. package/test/usersVisibility.test.js +22 -0
  132. package/test/workspaceActionContextContributor.test.js +251 -0
  133. package/test/workspaceActionSurfaces.test.js +105 -0
  134. package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
  135. package/test/workspaceBootstrapContributor.test.js +466 -0
  136. package/test/workspaceInvitationsPolicy.test.js +71 -0
  137. package/test/workspaceInvitesRepository.test.js +111 -0
  138. package/test/workspaceMembersService.test.js +400 -0
  139. package/test/workspacePathModel.test.js +93 -0
  140. package/test/workspacePendingInvitationsResource.test.js +38 -0
  141. package/test/workspacePendingInvitationsService.test.js +151 -0
  142. package/test/workspaceRouteVisibilityResolver.test.js +83 -0
  143. package/test/workspaceService.test.js +480 -0
  144. package/test/workspaceSettingsActions.test.js +42 -0
  145. package/test/workspaceSettingsRepository.test.js +156 -0
  146. package/test/workspaceSettingsResource.test.js +156 -0
  147. package/test/workspaceSettingsService.test.js +120 -0
  148. package/test-support/registerDefaultSettingsFields.js +3 -0
@@ -0,0 +1,83 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createWorkspaceRouteVisibilityResolver } from "../src/server/common/contributors/workspaceRouteVisibilityResolver.js";
4
+
5
+ test("workspace route visibility resolver contributes workspace_user scope and actor ownership", async () => {
6
+ const resolver = createWorkspaceRouteVisibilityResolver({
7
+ workspaceService: {
8
+ async resolveWorkspaceContextForUserBySlug() {
9
+ throw new Error("should not be called");
10
+ }
11
+ }
12
+ });
13
+
14
+ const contribution = await resolver.resolve({
15
+ visibility: "workspace_user",
16
+ context: {
17
+ actor: {
18
+ id: "user_42"
19
+ },
20
+ workspace: {
21
+ id: 11
22
+ }
23
+ }
24
+ });
25
+
26
+ assert.deepEqual(contribution, {
27
+ scopeKind: "workspace_user",
28
+ requiresActorScope: true,
29
+ scopeOwnerId: 11,
30
+ userOwnerId: "user_42"
31
+ });
32
+ });
33
+
34
+ test("workspace route visibility resolver keeps workspace-only visibility actor-agnostic", async () => {
35
+ const resolver = createWorkspaceRouteVisibilityResolver({
36
+ workspaceService: {
37
+ async resolveWorkspaceContextForUserBySlug() {
38
+ throw new Error("should not be called");
39
+ }
40
+ }
41
+ });
42
+
43
+ const contribution = await resolver.resolve({
44
+ visibility: "workspace",
45
+ context: {
46
+ workspace: {
47
+ id: 11
48
+ }
49
+ }
50
+ });
51
+
52
+ assert.deepEqual(contribution, {
53
+ scopeKind: "workspace",
54
+ requiresActorScope: false,
55
+ scopeOwnerId: 11
56
+ });
57
+ });
58
+
59
+ test("workspace route visibility resolver still marks workspace_user as actor-scoped when workspace is unresolved", async () => {
60
+ const resolver = createWorkspaceRouteVisibilityResolver({
61
+ workspaceService: {
62
+ async resolveWorkspaceContextForUserBySlug() {
63
+ return {};
64
+ }
65
+ }
66
+ });
67
+
68
+ const contribution = await resolver.resolve({
69
+ visibility: "workspace_user",
70
+ context: {
71
+ actor: {
72
+ id: "user_99"
73
+ }
74
+ },
75
+ input: {}
76
+ });
77
+
78
+ assert.deepEqual(contribution, {
79
+ scopeKind: "workspace_user",
80
+ requiresActorScope: true,
81
+ userOwnerId: "user_99"
82
+ });
83
+ });
@@ -0,0 +1,480 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createService } from "../src/server/common/services/workspaceContextService.js";
4
+
5
+ function createWorkspaceRoles() {
6
+ return {
7
+ defaultInviteRole: "member",
8
+ roles: {
9
+ owner: {
10
+ assignable: false,
11
+ permissions: ["*"]
12
+ },
13
+ member: {
14
+ assignable: true,
15
+ permissions: ["workspace.settings.view"]
16
+ }
17
+ }
18
+ };
19
+ }
20
+
21
+ function createWorkspaceServiceFixture({
22
+ tenancyMode = "workspace",
23
+ tenancyPolicy = {},
24
+ workspaceRoles = createWorkspaceRoles(),
25
+ additionalWorkspaces = [],
26
+ userWorkspaceRows = null,
27
+ membershipResolver = null,
28
+ personalWorkspace = {
29
+ id: 1,
30
+ slug: "tonymobily3",
31
+ name: "TonyMobily3",
32
+ ownerUserId: 7,
33
+ isPersonal: true,
34
+ avatarUrl: "",
35
+ color: "#0F6B54"
36
+ }
37
+ } = {}) {
38
+ const calls = {
39
+ findPersonalByOwnerUserId: 0,
40
+ listForUserId: 0,
41
+ insert: 0,
42
+ ensureOwnerMembership: 0
43
+ };
44
+ let nextWorkspaceId = 10;
45
+ const personalWorkspaceState =
46
+ personalWorkspace && typeof personalWorkspace === "object" ? { ...personalWorkspace } : null;
47
+ const insertedPayloads = [];
48
+
49
+ const workspaceBySlug = new Map();
50
+ if (personalWorkspaceState?.slug) {
51
+ workspaceBySlug.set(String(personalWorkspaceState.slug).trim().toLowerCase(), {
52
+ ...personalWorkspaceState
53
+ });
54
+ }
55
+ for (const workspace of Array.isArray(additionalWorkspaces) ? additionalWorkspaces : []) {
56
+ if (!workspace || typeof workspace !== "object") {
57
+ continue;
58
+ }
59
+ const slug = String(workspace.slug || "").trim().toLowerCase();
60
+ if (!slug) {
61
+ continue;
62
+ }
63
+ workspaceBySlug.set(slug, {
64
+ ...workspace
65
+ });
66
+ }
67
+
68
+ const service = createService({
69
+ appConfig: {
70
+ tenancyMode,
71
+ tenancyPolicy,
72
+ workspaceRoles: workspaceRoles && typeof workspaceRoles === "object" ? { ...workspaceRoles } : workspaceRoles
73
+ },
74
+ workspacesRepository: {
75
+ async findBySlug(slug) {
76
+ const normalizedSlug = String(slug || "").trim().toLowerCase();
77
+ const workspace = workspaceBySlug.get(normalizedSlug);
78
+ if (!workspace) {
79
+ return null;
80
+ }
81
+ return { ...workspace };
82
+ },
83
+ async findPersonalByOwnerUserId() {
84
+ calls.findPersonalByOwnerUserId += 1;
85
+ return personalWorkspaceState ? { ...personalWorkspaceState } : null;
86
+ },
87
+ async listForUserId() {
88
+ calls.listForUserId += 1;
89
+ if (Array.isArray(userWorkspaceRows)) {
90
+ return userWorkspaceRows;
91
+ }
92
+ return [
93
+ {
94
+ id: 1,
95
+ slug: "tonymobily3",
96
+ name: "TonyMobily3",
97
+ avatarUrl: "",
98
+ color: "#0F6B54",
99
+ roleId: "owner",
100
+ membershipStatus: "active"
101
+ },
102
+ {
103
+ id: 2,
104
+ slug: "pending-workspace",
105
+ name: "Pending Workspace",
106
+ avatarUrl: "",
107
+ color: "#0F6B54",
108
+ roleId: "member",
109
+ membershipStatus: "pending"
110
+ }
111
+ ];
112
+ },
113
+ async insert(payload) {
114
+ calls.insert += 1;
115
+ insertedPayloads.push(payload);
116
+ const workspaceId = nextWorkspaceId++;
117
+ const inserted = {
118
+ id: workspaceId,
119
+ slug: String(payload.slug || ""),
120
+ name: String(payload.name || ""),
121
+ ownerUserId: Number(payload.ownerUserId),
122
+ isPersonal: payload.isPersonal === true,
123
+ avatarUrl: String(payload.avatarUrl || ""),
124
+ color: String(payload.color || "#0F6B54")
125
+ };
126
+ workspaceBySlug.set(String(inserted.slug).trim().toLowerCase(), inserted);
127
+ return inserted;
128
+ }
129
+ },
130
+ workspaceMembershipsRepository: {
131
+ async ensureOwnerMembership() {
132
+ calls.ensureOwnerMembership += 1;
133
+ },
134
+ async findByWorkspaceIdAndUserId(workspaceId, userId) {
135
+ if (typeof membershipResolver === "function") {
136
+ return membershipResolver(workspaceId, userId);
137
+ }
138
+ return {
139
+ workspaceId,
140
+ userId,
141
+ roleId: "owner",
142
+ status: "active"
143
+ };
144
+ }
145
+ },
146
+ workspaceSettingsRepository: {
147
+ async ensureForWorkspaceId() {
148
+ return {
149
+ invitesEnabled: true
150
+ };
151
+ }
152
+ }
153
+ });
154
+
155
+ return { service, calls, insertedPayloads };
156
+ }
157
+
158
+ test("workspaceService no longer exposes bootstrap payload assembly", () => {
159
+ const { service } = createWorkspaceServiceFixture();
160
+ assert.equal(service.buildBootstrapPayload, undefined);
161
+ });
162
+
163
+ test("workspaceService.listWorkspacesForUser returns only accessible workspaces", async () => {
164
+ const { service, calls } = createWorkspaceServiceFixture();
165
+ const workspaces = await service.listWorkspacesForUser({
166
+ id: 7,
167
+ email: "chiaramobily@gmail.com",
168
+ displayName: "Chiara"
169
+ });
170
+
171
+ assert.equal(workspaces.length, 1);
172
+ assert.equal(workspaces[0].slug, "tonymobily3");
173
+ assert.equal(workspaces[0].roleId, "owner");
174
+ assert.equal(calls.listForUserId, 1);
175
+ assert.equal(calls.insert, 0);
176
+ });
177
+
178
+ test("workspaceService.listWorkspacesForUser no longer provisions personal workspace in workspace mode", async () => {
179
+ const { service, calls } = createWorkspaceServiceFixture({
180
+ tenancyMode: "workspace",
181
+ personalWorkspace: null
182
+ });
183
+
184
+ await service.listWorkspacesForUser({
185
+ id: 7,
186
+ email: "chiaramobily@gmail.com",
187
+ displayName: "Chiara"
188
+ });
189
+
190
+ assert.equal(calls.findPersonalByOwnerUserId, 0);
191
+ assert.equal(calls.insert, 0);
192
+ });
193
+
194
+ test("workspaceService.listWorkspacesForUser returns all active memberships in personal tenancy", async () => {
195
+ const { service, calls } = createWorkspaceServiceFixture({
196
+ tenancyMode: "personal",
197
+ userWorkspaceRows: [
198
+ {
199
+ id: 1,
200
+ slug: "chiaramobily",
201
+ name: "Chiara Personal",
202
+ avatarUrl: "",
203
+ color: "#0F6B54",
204
+ roleId: "owner",
205
+ membershipStatus: "active"
206
+ },
207
+ {
208
+ id: 2,
209
+ slug: "tonymobily",
210
+ name: "Tony Workspace",
211
+ avatarUrl: "",
212
+ color: "#0F6B54",
213
+ roleId: "member",
214
+ membershipStatus: "active"
215
+ },
216
+ {
217
+ id: 3,
218
+ slug: "pending-workspace",
219
+ name: "Pending Workspace",
220
+ avatarUrl: "",
221
+ color: "#0F6B54",
222
+ roleId: "member",
223
+ membershipStatus: "pending"
224
+ }
225
+ ]
226
+ });
227
+
228
+ const workspaces = await service.listWorkspacesForUser({
229
+ id: 7,
230
+ email: "chiaramobily@gmail.com",
231
+ displayName: "Chiara"
232
+ });
233
+
234
+ assert.deepEqual(
235
+ workspaces.map((workspace) => workspace.slug),
236
+ ["chiaramobily", "tonymobily"]
237
+ );
238
+ assert.equal(calls.findPersonalByOwnerUserId, 0);
239
+ assert.equal(calls.listForUserId, 1);
240
+ });
241
+
242
+ test("workspaceService.provisionWorkspaceForNewUser provisions personal workspace only in personal tenancy", async () => {
243
+ const { service, calls, insertedPayloads } = createWorkspaceServiceFixture({
244
+ tenancyMode: "personal",
245
+ personalWorkspace: null
246
+ });
247
+
248
+ const workspace = await service.provisionWorkspaceForNewUser({
249
+ id: 7,
250
+ email: "chiaramobily@gmail.com",
251
+ displayName: "Chiara"
252
+ });
253
+
254
+ assert.equal(Number(workspace.ownerUserId), 7);
255
+ assert.equal(calls.findPersonalByOwnerUserId, 1);
256
+ assert.equal(calls.insert, 1);
257
+ assert.equal(calls.ensureOwnerMembership, 1);
258
+ assert.equal(insertedPayloads[0].isPersonal, true);
259
+ });
260
+
261
+ test("workspaceService.provisionWorkspaceForNewUser is a no-op outside personal tenancy", async () => {
262
+ const { service, calls } = createWorkspaceServiceFixture({
263
+ tenancyMode: "workspace"
264
+ });
265
+
266
+ const result = await service.provisionWorkspaceForNewUser({
267
+ id: 7,
268
+ email: "chiaramobily@gmail.com",
269
+ displayName: "Chiara"
270
+ });
271
+
272
+ assert.equal(result, null);
273
+ assert.equal(calls.insert, 0);
274
+ });
275
+
276
+ test("workspaceService.createWorkspaceForAuthenticatedUser creates non-personal workspace in workspace tenancy", async () => {
277
+ const { service, calls, insertedPayloads } = createWorkspaceServiceFixture({
278
+ tenancyMode: "workspace",
279
+ tenancyPolicy: {
280
+ workspace: {
281
+ allowSelfCreate: true
282
+ }
283
+ }
284
+ });
285
+
286
+ const workspace = await service.createWorkspaceForAuthenticatedUser(
287
+ {
288
+ id: 7,
289
+ email: "chiaramobily@gmail.com",
290
+ displayName: "Chiara"
291
+ },
292
+ {
293
+ name: "Operations Team",
294
+ slug: "ops-team"
295
+ }
296
+ );
297
+
298
+ assert.equal(workspace.slug, "ops-team");
299
+ assert.equal(calls.insert, 1);
300
+ assert.equal(calls.ensureOwnerMembership, 1);
301
+ assert.equal(insertedPayloads[0].isPersonal, false);
302
+ assert.equal(insertedPayloads[0].ownerUserId, 7);
303
+ });
304
+
305
+ test("workspaceService.createWorkspaceForAuthenticatedUser rejects creation when self-create policy is disabled", async () => {
306
+ const { service } = createWorkspaceServiceFixture({
307
+ tenancyMode: "workspace"
308
+ });
309
+
310
+ await assert.rejects(
311
+ () =>
312
+ service.createWorkspaceForAuthenticatedUser(
313
+ {
314
+ id: 7,
315
+ email: "chiaramobily@gmail.com",
316
+ displayName: "Chiara"
317
+ },
318
+ {
319
+ name: "Operations Team"
320
+ }
321
+ ),
322
+ /Workspace creation is disabled for this tenancy mode/
323
+ );
324
+ });
325
+
326
+ test("workspaceService.resolveWorkspaceContextForUserBySlug returns workspace-not-found when requested slug does not exist", async () => {
327
+ const { service } = createWorkspaceServiceFixture({
328
+ tenancyMode: "personal",
329
+ personalWorkspace: null
330
+ });
331
+
332
+ await assert.rejects(
333
+ () =>
334
+ service.resolveWorkspaceContextForUserBySlug(
335
+ {
336
+ id: 7,
337
+ email: "chiaramobily@gmail.com",
338
+ displayName: "Chiara"
339
+ },
340
+ "tonymobily3"
341
+ ),
342
+ /Workspace not found/
343
+ );
344
+ });
345
+
346
+ test("workspaceService.resolveWorkspaceContextForUserBySlug allows personal tenancy access when membership is active", async () => {
347
+ const { service } = createWorkspaceServiceFixture({
348
+ tenancyMode: "personal",
349
+ personalWorkspace: {
350
+ id: 1,
351
+ slug: "my-personal",
352
+ name: "My Personal",
353
+ ownerUserId: 7,
354
+ isPersonal: true,
355
+ avatarUrl: "",
356
+ color: "#0F6B54"
357
+ },
358
+ additionalWorkspaces: [
359
+ {
360
+ id: 42,
361
+ slug: "team-alpha",
362
+ name: "Team Alpha",
363
+ ownerUserId: 99,
364
+ isPersonal: false,
365
+ avatarUrl: "",
366
+ color: "#0F6B54"
367
+ }
368
+ ]
369
+ });
370
+
371
+ const context = await service.resolveWorkspaceContextForUserBySlug(
372
+ {
373
+ id: 7,
374
+ email: "chiaramobily@gmail.com",
375
+ displayName: "Chiara"
376
+ },
377
+ "team-alpha"
378
+ );
379
+
380
+ assert.equal(context.workspace.slug, "team-alpha");
381
+ assert.equal(context.membership.roleId, "owner");
382
+ assert.deepEqual(context.permissions, ["*"]);
383
+ });
384
+
385
+ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access even when membership row is missing", async () => {
386
+ let ensuredMembershipCount = 0;
387
+ let membershipRecord = null;
388
+
389
+ const service = createService({
390
+ appConfig: {
391
+ tenancyMode: "personal",
392
+ workspaceRoles: createWorkspaceRoles()
393
+ },
394
+ workspacesRepository: {
395
+ async findBySlug(slug) {
396
+ if (String(slug) !== "tonymobily") {
397
+ return null;
398
+ }
399
+ return {
400
+ id: 1,
401
+ slug: "tonymobily",
402
+ name: "TonyMobily",
403
+ ownerUserId: 7,
404
+ isPersonal: true,
405
+ avatarUrl: "",
406
+ color: "#0F6B54"
407
+ };
408
+ },
409
+ async findPersonalByOwnerUserId() {
410
+ return null;
411
+ },
412
+ async listForUserId() {
413
+ return [];
414
+ },
415
+ async insert() {
416
+ throw new Error("not implemented");
417
+ }
418
+ },
419
+ workspaceMembershipsRepository: {
420
+ async findByWorkspaceIdAndUserId() {
421
+ return membershipRecord;
422
+ },
423
+ async ensureOwnerMembership(workspaceId, userId) {
424
+ ensuredMembershipCount += 1;
425
+ membershipRecord = {
426
+ workspaceId,
427
+ userId,
428
+ roleId: "owner",
429
+ status: "active"
430
+ };
431
+ return membershipRecord;
432
+ }
433
+ },
434
+ workspaceSettingsRepository: {
435
+ async ensureForWorkspaceId() {
436
+ return {
437
+ invitesEnabled: true
438
+ };
439
+ }
440
+ }
441
+ });
442
+
443
+ const context = await service.resolveWorkspaceContextForUserBySlug(
444
+ {
445
+ id: 7,
446
+ email: "chiaramobily@gmail.com",
447
+ displayName: "Chiara"
448
+ },
449
+ "tonymobily"
450
+ );
451
+
452
+ assert.equal(ensuredMembershipCount, 1);
453
+ assert.equal(context.membership.roleId, "owner");
454
+ assert.deepEqual(context.permissions, ["*"]);
455
+ });
456
+
457
+ test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions from appConfig.workspaceRoles", async () => {
458
+ const { service } = createWorkspaceServiceFixture({
459
+ workspaceRoles: {
460
+ defaultInviteRole: "member",
461
+ roles: {
462
+ owner: {
463
+ assignable: false,
464
+ permissions: ["workspace.settings.update"]
465
+ }
466
+ }
467
+ }
468
+ });
469
+
470
+ const context = await service.resolveWorkspaceContextForUserBySlug(
471
+ {
472
+ id: 7,
473
+ email: "chiaramobily@gmail.com",
474
+ displayName: "Chiara"
475
+ },
476
+ "tonymobily3"
477
+ );
478
+
479
+ assert.deepEqual(context.permissions, ["workspace.settings.update"]);
480
+ });
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import "../test-support/registerDefaultSettingsFields.js";
4
+ import { workspaceDirectoryActions } from "../src/server/workspaceDirectory/workspaceDirectoryActions.js";
5
+ import { workspacePendingInvitationsActions } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js";
6
+ import { workspaceMembersActions } from "../src/server/workspaceMembers/workspaceMembersActions.js";
7
+ import { workspaceSettingsActions } from "../src/server/workspaceSettings/workspaceSettingsActions.js";
8
+ import { workspaceResource } from "../src/shared/resources/workspaceResource.js";
9
+
10
+ test("workspace settings actions live in their own action array", () => {
11
+ assert.deepEqual(
12
+ workspaceSettingsActions.map((action) => action.id),
13
+ ["workspace.settings.read", "workspace.settings.update"]
14
+ );
15
+ assert.equal(workspaceSettingsActions[0].surfacesFrom, "workspace");
16
+ assert.equal(workspaceSettingsActions[1].surfacesFrom, "workspace");
17
+ assert.deepEqual(workspaceSettingsActions[1].channels, ["api", "assistant_tool", "automation", "internal"]);
18
+ assert.equal(workspaceSettingsActions[1].extensions?.assistant?.description, "Update workspace settings.");
19
+ });
20
+
21
+ test("workspace actions array no longer owns workspace settings actions", () => {
22
+ const otherWorkspaceActionIds = [
23
+ ...workspaceDirectoryActions,
24
+ ...workspacePendingInvitationsActions,
25
+ ...workspaceMembersActions
26
+ ].map((action) => action.id);
27
+
28
+ assert.equal(
29
+ otherWorkspaceActionIds.includes("workspace.settings.read"),
30
+ false
31
+ );
32
+ assert.equal(
33
+ otherWorkspaceActionIds.includes("workspace.settings.update"),
34
+ false
35
+ );
36
+ });
37
+
38
+ test("workspace directory actions use the canonical workspace list resource output", () => {
39
+ const listAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.list");
40
+ assert.ok(listAction);
41
+ assert.equal(listAction.outputValidator, workspaceResource.operations.list.outputValidator);
42
+ });