@jskit-ai/users-core 0.1.32 → 0.1.35

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 (48) hide show
  1. package/package.descriptor.mjs +16 -245
  2. package/package.json +7 -7
  3. package/src/server/UsersCoreServiceProvider.js +4 -28
  4. package/src/server/UsersWorkspacesServiceProvider.js +44 -0
  5. package/src/server/accountNotifications/accountNotificationsService.js +3 -3
  6. package/src/server/accountNotifications/registerAccountNotifications.js +1 -1
  7. package/src/server/accountPreferences/accountPreferencesService.js +3 -3
  8. package/src/server/accountPreferences/registerAccountPreferences.js +1 -1
  9. package/src/server/accountProfile/accountProfileActions.js +8 -2
  10. package/src/server/accountProfile/accountProfileService.js +10 -10
  11. package/src/server/accountProfile/avatarService.js +9 -9
  12. package/src/server/accountProfile/bootAccountProfileRoutes.js +5 -3
  13. package/src/server/accountProfile/registerAccountProfile.js +2 -2
  14. package/src/server/accountSecurity/accountSecurityService.js +3 -3
  15. package/src/server/accountSecurity/registerAccountSecurity.js +1 -1
  16. package/src/server/common/contributors/workspaceActionContextContributor.js +24 -17
  17. package/src/server/common/registerCommonRepositories.js +3 -22
  18. package/src/server/common/repositories/userSettingsRepository.js +1 -12
  19. package/src/server/common/repositories/{userProfilesRepository.js → usersRepository.js} +1 -1
  20. package/src/server/common/services/accountContextService.js +4 -4
  21. package/src/server/common/services/authProfileSyncService.js +10 -10
  22. package/src/server/registerUsersBootstrap.js +22 -0
  23. package/src/server/registerUsersCore.js +30 -0
  24. package/src/server/registerWorkspaceBootstrap.js +3 -6
  25. package/src/server/registerWorkspaceCore.js +5 -17
  26. package/src/server/registerWorkspaceRepositories.js +26 -0
  27. package/src/server/usersBootstrapContributor.js +248 -0
  28. package/src/server/workspaceBootstrapContributor.js +65 -259
  29. package/src/shared/roles.js +31 -6
  30. package/src/shared/settings.js +1 -2
  31. package/templates/migrations/users_core_generic_initial.cjs +69 -0
  32. package/test/authProfileSyncService.test.js +3 -3
  33. package/test/avatarService.test.js +2 -2
  34. package/test/registerUsersCore.test.js +42 -0
  35. package/test/roles.test.js +90 -5
  36. package/test/usersBootstrapContributor.test.js +172 -0
  37. package/test/usersRouteRequestInputValidator.test.js +7 -390
  38. package/test/workspaceActionContextContributor.test.js +98 -5
  39. package/test/workspaceBootstrapContributor.test.js +34 -346
  40. package/test/workspaceMembersService.test.js +4 -2
  41. package/test/workspaceService.test.js +12 -8
  42. package/test/workspaceSettingsResource.test.js +4 -2
  43. package/test-support/registerDefaultSettingsFields.js +1 -1
  44. package/templates/config/workspaceRoles.js +0 -30
  45. package/templates/migrations/users_core_initial.cjs +0 -123
  46. package/templates/migrations/users_core_workspace_settings_single_name_source.cjs +0 -71
  47. package/templates/migrations/users_core_workspaces_drop_color.cjs +0 -85
  48. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -197
@@ -1,10 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createWorkspaceBootstrapContributor } from "../src/server/workspaceBootstrapContributor.js";
4
- import {
5
- TENANCY_MODE_PERSONAL,
6
- WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
7
- } from "../src/shared/tenancyProfile.js";
8
4
 
9
5
  function createAuthenticatedProfile(overrides = {}) {
10
6
  return {
@@ -39,27 +35,11 @@ test("workspace bootstrap contributor passes actor context to pending invites se
39
35
  return [];
40
36
  }
41
37
  },
42
- userProfilesRepository: {
43
- async findByIdentity() {
38
+ usersRepository: {
39
+ async findById() {
44
40
  return profile;
45
41
  }
46
42
  },
47
- userSettingsRepository: {
48
- async ensureForUserId() {
49
- return {
50
- theme: "system",
51
- locale: "en",
52
- timeZone: "UTC",
53
- dateFormat: "YYYY-MM-DD",
54
- numberFormat: "1,234.56",
55
- currencyCode: "USD",
56
- avatarSize: 64,
57
- productUpdates: true,
58
- accountActivity: true,
59
- securityAlerts: true
60
- };
61
- }
62
- },
63
43
  workspaceInvitationsEnabled: true,
64
44
  appConfig: {
65
45
  tenancyMode: "workspaces"
@@ -67,15 +47,12 @@ test("workspace bootstrap contributor passes actor context to pending invites se
67
47
  });
68
48
 
69
49
  await contributor.contribute({
70
- request: {
71
- async executeAction() {
72
- return {
73
- authenticated: true,
74
- profile
75
- };
50
+ payload: {
51
+ session: {
52
+ authenticated: true,
53
+ userId: profile.id
76
54
  }
77
- },
78
- reply: {}
55
+ }
79
56
  });
80
57
 
81
58
  assert.equal(pendingServiceCalls.length, 1);
@@ -83,133 +60,6 @@ test("workspace bootstrap contributor passes actor context to pending invites se
83
60
  assert.equal(pendingServiceCalls[0].options?.context?.actor?.id, profile.id);
84
61
  });
85
62
 
86
- test("workspace bootstrap contributor seeds the initial console owner on authenticated bootstrap", async () => {
87
- const profile = createAuthenticatedProfile({ id: 12 });
88
- const consoleOwnerSeeds = [];
89
-
90
- const contributor = createWorkspaceBootstrapContributor({
91
- workspaceService: {
92
- async listWorkspacesForUser() {
93
- return [];
94
- },
95
- async resolveWorkspaceContextForUserBySlug() {
96
- return null;
97
- }
98
- },
99
- workspacePendingInvitationsService: {
100
- async listPendingInvitesForUser() {
101
- return [];
102
- }
103
- },
104
- userProfilesRepository: {
105
- async findByIdentity() {
106
- return profile;
107
- }
108
- },
109
- userSettingsRepository: {
110
- async ensureForUserId() {
111
- return {
112
- theme: "system",
113
- locale: "en",
114
- timeZone: "UTC",
115
- dateFormat: "YYYY-MM-DD",
116
- numberFormat: "1,234.56",
117
- currencyCode: "USD",
118
- avatarSize: 64,
119
- productUpdates: true,
120
- accountActivity: true,
121
- securityAlerts: true
122
- };
123
- }
124
- },
125
- workspaceInvitationsEnabled: false,
126
- consoleService: {
127
- async ensureInitialConsoleMember(userId) {
128
- consoleOwnerSeeds.push(Number(userId));
129
- return Number(userId);
130
- }
131
- }
132
- });
133
-
134
- const payload = await contributor.contribute({
135
- request: {
136
- async executeAction() {
137
- return {
138
- authenticated: true,
139
- profile
140
- };
141
- }
142
- },
143
- reply: {}
144
- });
145
-
146
- assert.deepEqual(consoleOwnerSeeds, [12]);
147
- assert.equal(payload.surfaceAccess?.consoleowner, true);
148
- });
149
-
150
- test("workspace bootstrap contributor emits canonical tenancy profile from users-core", async () => {
151
- const contributor = createWorkspaceBootstrapContributor({
152
- workspaceService: {
153
- async listWorkspacesForUser() {
154
- return [];
155
- },
156
- async resolveWorkspaceContextForUserBySlug() {
157
- return null;
158
- }
159
- },
160
- workspacePendingInvitationsService: {
161
- async listPendingInvitesForUser() {
162
- return [];
163
- }
164
- },
165
- userProfilesRepository: {
166
- async findByIdentity() {
167
- return null;
168
- }
169
- },
170
- userSettingsRepository: {
171
- async ensureForUserId() {
172
- return {};
173
- }
174
- },
175
- workspaceInvitationsEnabled: false,
176
- tenancyProfile: {
177
- mode: TENANCY_MODE_PERSONAL,
178
- workspace: {
179
- enabled: true,
180
- autoProvision: true,
181
- allowSelfCreate: false,
182
- slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
183
- }
184
- },
185
- appConfig: {
186
- tenancyMode: "none"
187
- }
188
- });
189
-
190
- const payload = await contributor.contribute({
191
- request: {
192
- async executeAction() {
193
- return {
194
- authenticated: false
195
- };
196
- }
197
- },
198
- reply: {}
199
- });
200
-
201
- assert.deepEqual(payload.tenancy, {
202
- mode: TENANCY_MODE_PERSONAL,
203
- workspace: {
204
- enabled: true,
205
- autoProvision: true,
206
- allowSelfCreate: false,
207
- slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
208
- }
209
- });
210
- assert.equal(payload.app.tenancyMode, undefined);
211
- });
212
-
213
63
  test("workspace bootstrap contributor resolves workspace slug from bootstrap query", async () => {
214
64
  const profile = createAuthenticatedProfile();
215
65
  const calls = [];
@@ -228,163 +78,57 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
228
78
  return [];
229
79
  }
230
80
  },
231
- userProfilesRepository: {
232
- async findByIdentity() {
81
+ usersRepository: {
82
+ async findById() {
233
83
  return profile;
234
84
  }
235
85
  },
236
- userSettingsRepository: {
237
- async ensureForUserId() {
238
- return {
239
- theme: "system",
240
- locale: "en",
241
- timeZone: "UTC",
242
- dateFormat: "YYYY-MM-DD",
243
- numberFormat: "1,234.56",
244
- currencyCode: "USD",
245
- avatarSize: 64,
246
- productUpdates: true,
247
- accountActivity: true,
248
- securityAlerts: true
249
- };
250
- }
251
- },
252
86
  workspaceInvitationsEnabled: true,
253
87
  appConfig: {
254
88
  tenancyMode: "workspaces"
255
89
  }
256
90
  });
257
91
 
258
- await contributor.contribute({
92
+ const payload = await contributor.contribute({
259
93
  query: {
260
94
  workspaceSlug: " AcMe "
261
95
  },
262
- request: {
263
- async executeAction() {
264
- return {
265
- authenticated: true,
266
- profile
267
- };
96
+ payload: {
97
+ session: {
98
+ authenticated: true,
99
+ userId: profile.id
268
100
  }
269
- },
270
- reply: {}
271
- });
272
-
273
- assert.deepEqual(calls, ["acme"]);
274
- });
275
-
276
- test("workspace bootstrap contributor returns global payload with requestedWorkspace=forbidden when slug access is denied", async () => {
277
- const profile = createAuthenticatedProfile();
278
- const contributor = createWorkspaceBootstrapContributor({
279
- workspaceService: {
280
- async listWorkspacesForUser() {
281
- return [{ id: 3, slug: "chiara", name: "Chiara Workspace" }];
282
- },
283
- async resolveWorkspaceContextForUserBySlug() {
284
- const error = new Error("Forbidden.");
285
- error.status = 403;
286
- throw error;
287
- }
288
- },
289
- workspacePendingInvitationsService: {
290
- async listPendingInvitesForUser() {
291
- return [];
292
- }
293
- },
294
- userProfilesRepository: {
295
- async findByIdentity() {
296
- return profile;
297
- }
298
- },
299
- userSettingsRepository: {
300
- async ensureForUserId() {
301
- return {
302
- theme: "system",
303
- locale: "en",
304
- timeZone: "UTC",
305
- dateFormat: "YYYY-MM-DD",
306
- numberFormat: "1,234.56",
307
- currencyCode: "USD",
308
- avatarSize: 64,
309
- productUpdates: true,
310
- accountActivity: true,
311
- securityAlerts: true
312
- };
313
- }
314
- },
315
- workspaceInvitationsEnabled: true,
316
- appConfig: {
317
- tenancyMode: "workspaces"
318
101
  }
319
102
  });
320
103
 
321
- const payload = await contributor.contribute({
322
- query: {
323
- workspaceSlug: "tonymobily"
324
- },
325
- request: {
326
- async executeAction() {
327
- return {
328
- authenticated: true,
329
- profile
330
- };
331
- }
332
- },
333
- reply: {}
334
- });
335
-
336
- assert.equal(payload.session.authenticated, true);
337
- assert.deepEqual(payload.workspaces, [{ id: 3, slug: "chiara", name: "Chiara Workspace" }]);
104
+ assert.deepEqual(calls, ["acme"]);
338
105
  assert.deepEqual(payload.requestedWorkspace, {
339
- slug: "tonymobily",
340
- status: "forbidden"
106
+ slug: "acme",
107
+ status: "resolved"
341
108
  });
342
- assert.equal(payload.activeWorkspace, null);
343
- assert.equal(payload.membership, null);
344
- assert.deepEqual(payload.permissions, []);
345
- assert.equal(payload.workspaceSettings, null);
346
109
  });
347
110
 
348
- test("workspace bootstrap contributor returns requestedWorkspace=not_found when slug does not exist", async () => {
349
- const profile = createAuthenticatedProfile();
111
+ test("workspace bootstrap contributor reports unauthenticated requested workspace without generic bootstrap work", async () => {
350
112
  const contributor = createWorkspaceBootstrapContributor({
351
113
  workspaceService: {
352
114
  async listWorkspacesForUser() {
353
- return [{ id: 1, slug: "acme", name: "Acme Workspace" }];
115
+ assert.fail("listWorkspacesForUser should not run for unauthenticated payloads");
354
116
  },
355
117
  async resolveWorkspaceContextForUserBySlug() {
356
- const error = new Error("Workspace not found.");
357
- error.status = 404;
358
- throw error;
118
+ assert.fail("resolveWorkspaceContextForUserBySlug should not run for unauthenticated payloads");
359
119
  }
360
120
  },
361
121
  workspacePendingInvitationsService: {
362
122
  async listPendingInvitesForUser() {
363
- return [];
123
+ assert.fail("listPendingInvitesForUser should not run for unauthenticated payloads");
364
124
  }
365
125
  },
366
- userProfilesRepository: {
367
- async findByIdentity() {
368
- return profile;
369
- }
370
- },
371
- userSettingsRepository: {
372
- async ensureForUserId() {
373
- return {
374
- theme: "system",
375
- locale: "en",
376
- timeZone: "UTC",
377
- dateFormat: "YYYY-MM-DD",
378
- numberFormat: "1,234.56",
379
- currencyCode: "USD",
380
- avatarSize: 64,
381
- productUpdates: true,
382
- accountActivity: true,
383
- securityAlerts: true
384
- };
126
+ usersRepository: {
127
+ async findById() {
128
+ assert.fail("findById should not run for unauthenticated payloads");
385
129
  }
386
130
  },
387
- workspaceInvitationsEnabled: false,
131
+ workspaceInvitationsEnabled: true,
388
132
  appConfig: {
389
133
  tenancyMode: "workspaces"
390
134
  }
@@ -392,75 +136,19 @@ test("workspace bootstrap contributor returns requestedWorkspace=not_found when
392
136
 
393
137
  const payload = await contributor.contribute({
394
138
  query: {
395
- workspaceSlug: "missing-workspace"
139
+ workspaceSlug: "AcMe"
396
140
  },
397
- request: {
398
- async executeAction() {
399
- return {
400
- authenticated: true,
401
- profile
402
- };
141
+ payload: {
142
+ session: {
143
+ authenticated: false
403
144
  }
404
- },
405
- reply: {}
406
- });
407
-
408
- assert.deepEqual(payload.requestedWorkspace, {
409
- slug: "missing-workspace",
410
- status: "not_found"
411
- });
412
- assert.deepEqual(payload.workspaces, [{ id: 1, slug: "acme", name: "Acme Workspace" }]);
413
- });
414
-
415
- test("workspace bootstrap contributor returns requestedWorkspace=unauthenticated for anonymous workspace slug query", async () => {
416
- const contributor = createWorkspaceBootstrapContributor({
417
- workspaceService: {
418
- async listWorkspacesForUser() {
419
- return [];
420
- },
421
- async resolveWorkspaceContextForUserBySlug() {
422
- return null;
423
- }
424
- },
425
- workspacePendingInvitationsService: {
426
- async listPendingInvitesForUser() {
427
- return [];
428
- }
429
- },
430
- userProfilesRepository: {
431
- async findByIdentity() {
432
- return null;
433
- }
434
- },
435
- userSettingsRepository: {
436
- async ensureForUserId() {
437
- return {};
438
- }
439
- },
440
- workspaceInvitationsEnabled: false,
441
- appConfig: {
442
- tenancyMode: "workspaces"
443
145
  }
444
146
  });
445
147
 
446
- const payload = await contributor.contribute({
447
- query: {
448
- workspaceSlug: "tonymobily"
449
- },
450
- request: {
451
- async executeAction() {
452
- return {
453
- authenticated: false
454
- };
455
- }
456
- },
457
- reply: {}
458
- });
459
-
460
- assert.equal(payload.session.authenticated, false);
461
- assert.deepEqual(payload.requestedWorkspace, {
462
- slug: "tonymobily",
463
- status: "unauthenticated"
148
+ assert.deepEqual(payload, {
149
+ requestedWorkspace: {
150
+ slug: "acme",
151
+ status: "unauthenticated"
152
+ }
464
153
  });
465
- assert.deepEqual(payload.workspaces, []);
466
154
  });
@@ -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,3 +1,3 @@
1
- import "../templates/packages/main/src/shared/resources/workspaceSettingsFields.js";
1
+ import "../../workspaces-core/templates/packages/main/src/shared/resources/workspaceSettingsFields.js";
2
2
  import "../templates/packages/main/src/shared/resources/consoleSettingsFields.js";
3
3
  import "../templates/packages/main/src/shared/resources/userSettingsFields.js";
@@ -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");