@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.
- package/package.descriptor.mjs +464 -0
- package/package.json +35 -0
- package/src/server/UsersCoreServiceProvider.js +74 -0
- package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
- package/src/server/accountNotifications/accountNotificationsService.js +41 -0
- package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
- package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
- package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
- package/src/server/accountPreferences/accountPreferencesService.js +41 -0
- package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
- package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
- package/src/server/accountProfile/accountProfileActions.js +137 -0
- package/src/server/accountProfile/accountProfileService.js +124 -0
- package/src/server/accountProfile/avatarService.js +141 -0
- package/src/server/accountProfile/avatarStorageService.js +132 -0
- package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
- package/src/server/accountProfile/registerAccountProfile.js +62 -0
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
- package/src/server/accountSecurity/accountSecurityActions.js +144 -0
- package/src/server/accountSecurity/accountSecurityService.js +103 -0
- package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
- package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
- package/src/server/common/README.md +21 -0
- package/src/server/common/contributors/README.md +11 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
- package/src/server/common/diTokens.js +21 -0
- package/src/server/common/formatters/README.md +11 -0
- package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
- package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
- package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
- package/src/server/common/formatters/workspaceFormatter.js +46 -0
- package/src/server/common/registerCommonRepositories.js +45 -0
- package/src/server/common/registerSharedApi.js +9 -0
- package/src/server/common/repositories/README.md +24 -0
- package/src/server/common/repositories/repositoryUtils.js +50 -0
- package/src/server/common/repositories/userProfilesRepository.js +251 -0
- package/src/server/common/repositories/userSettingsRepository.js +179 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
- package/src/server/common/repositories/workspacesRepository.js +183 -0
- package/src/server/common/routes/README.md +11 -0
- package/src/server/common/services/README.md +12 -0
- package/src/server/common/services/accountContextService.js +31 -0
- package/src/server/common/services/authProfileSyncService.js +128 -0
- package/src/server/common/services/workspaceContextService.js +270 -0
- package/src/server/common/support/deepFreeze.js +17 -0
- package/src/server/common/support/realtimeServiceEvents.js +94 -0
- package/src/server/common/support/resolveActionUser.js +11 -0
- package/src/server/common/support/workspaceRoutePaths.js +17 -0
- package/src/server/common/validators/README.md +11 -0
- package/src/server/common/validators/authenticatedUserValidator.js +42 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
- package/src/server/consoleSettings/consoleService.js +36 -0
- package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
- package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
- package/src/server/consoleSettings/consoleSettingsService.js +40 -0
- package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
- package/src/server/registerWorkspaceBootstrap.js +36 -0
- package/src/server/registerWorkspaceCore.js +95 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +135 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +401 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
- package/src/shared/events/usersEvents.js +19 -0
- package/src/shared/index.js +91 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/consoleSettingsFields.js +55 -0
- package/src/shared/resources/consoleSettingsResource.js +139 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/userProfileResource.js +148 -0
- package/src/shared/resources/userSettingsFields.js +71 -0
- package/src/shared/resources/userSettingsResource.js +416 -0
- package/src/shared/resources/workspaceMembersResource.js +352 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
- package/src/shared/resources/workspaceResource.js +149 -0
- package/src/shared/resources/workspaceSettingsFields.js +60 -0
- package/src/shared/resources/workspaceSettingsResource.js +178 -0
- package/src/shared/roles.js +136 -0
- package/src/shared/settings.js +31 -0
- package/src/shared/support/usersApiPaths.js +34 -0
- package/src/shared/support/usersVisibility.js +45 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/config/workspaceRoles.js +30 -0
- package/templates/migrations/users_core_console_owner.cjs +39 -0
- package/templates/migrations/users_core_initial.cjs +118 -0
- package/templates/migrations/users_core_profile_username.cjs +98 -0
- package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
- package/test/authProfileSyncService.test.js +119 -0
- package/test/avatarService.test.js +114 -0
- package/test/avatarStorageService.test.js +61 -0
- package/test/consoleService.test.js +57 -0
- package/test/consoleSettingsService.test.js +86 -0
- package/test/exportsContract.test.js +38 -0
- package/test/registerAvatarMultipartSupport.test.js +64 -0
- package/test/registerServiceRealtimeEvents.test.js +160 -0
- package/test/registerWorkspaceDirectory.test.js +26 -0
- package/test/registerWorkspaceSettings.test.js +44 -0
- package/test/resourcesCanonical.test.js +90 -0
- package/test/roles.test.js +74 -0
- package/test/settingsFieldRegistriesSingleton.test.js +24 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/userSettingsResource.test.js +31 -0
- package/test/usersApiPaths.test.js +31 -0
- package/test/usersRouteRequestInputValidator.test.js +556 -0
- package/test/usersRouteResources.test.js +113 -0
- package/test/usersRouteValidators.test.js +49 -0
- package/test/usersVisibility.test.js +22 -0
- package/test/workspaceActionContextContributor.test.js +251 -0
- package/test/workspaceActionSurfaces.test.js +105 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +466 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +400 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +480 -0
- package/test/workspaceSettingsActions.test.js +42 -0
- package/test/workspaceSettingsRepository.test.js +156 -0
- package/test/workspaceSettingsResource.test.js +156 -0
- package/test/workspaceSettingsService.test.js +120 -0
- 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
|
+
});
|