@jskit-ai/workspaces-core 0.1.137 → 0.1.138
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.json +38 -238
- package/patterns/workspace-server/PATTERN.md +60 -0
- package/src/server/WorkspacesFeature.js +226 -0
- package/src/server/WorkspacesIntegrationsProvider.js +74 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +18 -6
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +25 -4
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
- package/src/server/common/services/workspaceContextService.js +7 -2
- package/src/server/common/support/realtimeServiceEvents.js +102 -61
- package/src/server/support/workspaceActionSurfaces.js +18 -13
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
- package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
- package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
- package/test/exportsContract.test.js +2 -1
- package/test/featureRuntime.test.js +116 -0
- package/test/packageMetadata.test.js +20 -54
- package/test/workspaceActionContextContributor.test.js +49 -8
- package/test/workspaceActionSurfaces.test.js +36 -1
- package/test/workspaceAuthPolicyContextResolver.test.js +61 -0
- package/test/workspaceRouteVisibilityResolver.test.js +31 -0
- package/test/workspaceService.test.js +62 -0
- package/test/workspaceSettingsActions.test.js +102 -16
- package/test/workspacesRouteRequestInputValidator.test.js +14 -65
- package/src/server/WorkspacesCoreServiceProvider.js +0 -64
- package/src/server/registerWorkspaceBootstrap.js +0 -27
- package/src/server/registerWorkspaceCore.js +0 -101
- package/src/server/registerWorkspaceRepositories.js +0 -30
- package/src/server/support/workspaceServerScopeSupport.js +0 -18
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -137
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -130
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -66
- package/test/registerServiceRealtimeEvents.test.js +0 -116
- package/test/registerWorkspaceBootstrap.test.js +0 -86
- package/test/registerWorkspaceDirectory.test.js +0 -31
- package/test/registerWorkspaceMembers.test.js +0 -140
- package/test/registerWorkspaceSettings.test.js +0 -40
- package/test/workspaceServerScopeSupport.test.js +0 -131
- /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
- /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
- /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
- /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
- /package/{templates → patterns/workspace-server/example}/packages/main/src/server/email/workspaceInviteEmail.js +0 -0
|
@@ -3,8 +3,34 @@ import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
|
3
3
|
import { resolveWorkspace } from "../support/resolveWorkspace.js";
|
|
4
4
|
import { resolveActionUser } from "../common/support/resolveActionUser.js";
|
|
5
5
|
import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
|
|
6
|
+
import {
|
|
7
|
+
INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE,
|
|
8
|
+
createWorkspaceEntityAndBootstrapEvents
|
|
9
|
+
} from "../common/support/realtimeServiceEvents.js";
|
|
6
10
|
import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
7
11
|
|
|
12
|
+
const WORKSPACE_MEMBER_CHANGED_EVENTS = createWorkspaceEntityAndBootstrapEvents({
|
|
13
|
+
workspaceEntity: "member",
|
|
14
|
+
workspaceOperation: "updated",
|
|
15
|
+
workspaceRealtimeEvent: "workspace.members.changed"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const WORKSPACE_INVITE_CREATED_EVENTS = createWorkspaceEntityAndBootstrapEvents({
|
|
19
|
+
workspaceEntity: "invite",
|
|
20
|
+
workspaceOperation: "created",
|
|
21
|
+
workspaceRealtimeEvent: "workspace.invites.changed",
|
|
22
|
+
bootstrapEntityId: ({ result } = {}) => result?.value?.createdInviteId,
|
|
23
|
+
bootstrapAudience: INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const WORKSPACE_INVITE_REVOKED_EVENTS = createWorkspaceEntityAndBootstrapEvents({
|
|
27
|
+
workspaceEntity: "invite",
|
|
28
|
+
workspaceOperation: "updated",
|
|
29
|
+
workspaceRealtimeEvent: "workspace.invites.changed",
|
|
30
|
+
bootstrapEntityId: ({ result } = {}) => result?.value?.revokedInviteId,
|
|
31
|
+
bootstrapAudience: INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE
|
|
32
|
+
});
|
|
33
|
+
|
|
8
34
|
const updateMemberRoleActionInput = composeSchemaDefinitions([
|
|
9
35
|
workspaceSlugParamsValidator,
|
|
10
36
|
workspaceMembersResource.operations.updateMemberRole.input
|
|
@@ -37,13 +63,13 @@ const revokeInviteActionInput = composeSchemaDefinitions([
|
|
|
37
63
|
context: "workspaceMembersActions.revokeInviteActionInput"
|
|
38
64
|
});
|
|
39
65
|
|
|
40
|
-
const
|
|
66
|
+
const workspaceMembersActionSpecifications = Object.freeze([
|
|
41
67
|
{
|
|
42
68
|
id: "workspace.roles.list",
|
|
43
69
|
version: 1,
|
|
44
70
|
kind: "query",
|
|
45
71
|
channels: ["api", "automation", "internal"],
|
|
46
|
-
|
|
72
|
+
surfaces: ["*"],
|
|
47
73
|
permission: {
|
|
48
74
|
require: "all",
|
|
49
75
|
permissions: ["workspace.roles.view"]
|
|
@@ -55,8 +81,8 @@ const workspaceMembersActions = Object.freeze([
|
|
|
55
81
|
actionName: "workspace.roles.list"
|
|
56
82
|
},
|
|
57
83
|
observability: {},
|
|
58
|
-
async
|
|
59
|
-
return returnJsonApiData(await
|
|
84
|
+
async run(workspaceMembersService, _input, context) {
|
|
85
|
+
return returnJsonApiData(await workspaceMembersService.listRoles({ context }));
|
|
60
86
|
}
|
|
61
87
|
},
|
|
62
88
|
{
|
|
@@ -64,7 +90,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
64
90
|
version: 1,
|
|
65
91
|
kind: "query",
|
|
66
92
|
channels: ["api", "automation", "internal"],
|
|
67
|
-
|
|
93
|
+
surfaces: ["*"],
|
|
68
94
|
permission: {
|
|
69
95
|
require: "all",
|
|
70
96
|
permissions: ["workspace.members.view"]
|
|
@@ -76,8 +102,8 @@ const workspaceMembersActions = Object.freeze([
|
|
|
76
102
|
actionName: "workspace.members.list"
|
|
77
103
|
},
|
|
78
104
|
observability: {},
|
|
79
|
-
async
|
|
80
|
-
return returnJsonApiData(await
|
|
105
|
+
async run(workspaceMembersService, input, context) {
|
|
106
|
+
return returnJsonApiData(await workspaceMembersService.listMembers(resolveWorkspace(context, input), {
|
|
81
107
|
context
|
|
82
108
|
}));
|
|
83
109
|
}
|
|
@@ -87,7 +113,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
87
113
|
version: 1,
|
|
88
114
|
kind: "command",
|
|
89
115
|
channels: ["api", "automation", "internal"],
|
|
90
|
-
|
|
116
|
+
surfaces: ["*"],
|
|
91
117
|
permission: {
|
|
92
118
|
require: "all",
|
|
93
119
|
permissions: ["workspace.members.manage"]
|
|
@@ -99,8 +125,9 @@ const workspaceMembersActions = Object.freeze([
|
|
|
99
125
|
actionName: "workspace.member.role.update"
|
|
100
126
|
},
|
|
101
127
|
observability: {},
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
events: WORKSPACE_MEMBER_CHANGED_EVENTS,
|
|
129
|
+
async run(workspaceMembersService, input, context) {
|
|
130
|
+
return returnJsonApiData(await workspaceMembersService.updateMemberRole(resolveWorkspace(context, input), {
|
|
104
131
|
memberUserId: input.memberUserId,
|
|
105
132
|
roleSid: input.roleSid
|
|
106
133
|
}, {
|
|
@@ -113,7 +140,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
113
140
|
version: 1,
|
|
114
141
|
kind: "command",
|
|
115
142
|
channels: ["api", "automation", "internal"],
|
|
116
|
-
|
|
143
|
+
surfaces: ["*"],
|
|
117
144
|
permission: {
|
|
118
145
|
require: "all",
|
|
119
146
|
permissions: ["workspace.members.manage"]
|
|
@@ -125,8 +152,9 @@ const workspaceMembersActions = Object.freeze([
|
|
|
125
152
|
actionName: "workspace.member.remove"
|
|
126
153
|
},
|
|
127
154
|
observability: {},
|
|
128
|
-
|
|
129
|
-
|
|
155
|
+
events: WORKSPACE_MEMBER_CHANGED_EVENTS,
|
|
156
|
+
async run(workspaceMembersService, input, context) {
|
|
157
|
+
return returnJsonApiData(await workspaceMembersService.removeMember(resolveWorkspace(context, input), {
|
|
130
158
|
memberUserId: input.memberUserId
|
|
131
159
|
}, {
|
|
132
160
|
context
|
|
@@ -138,7 +166,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
138
166
|
version: 1,
|
|
139
167
|
kind: "query",
|
|
140
168
|
channels: ["api", "automation", "internal"],
|
|
141
|
-
|
|
169
|
+
surfaces: ["*"],
|
|
142
170
|
permission: {
|
|
143
171
|
require: "all",
|
|
144
172
|
permissions: ["workspace.members.view"]
|
|
@@ -150,8 +178,8 @@ const workspaceMembersActions = Object.freeze([
|
|
|
150
178
|
actionName: "workspace.invites.list"
|
|
151
179
|
},
|
|
152
180
|
observability: {},
|
|
153
|
-
async
|
|
154
|
-
return returnJsonApiData(await
|
|
181
|
+
async run(workspaceMembersService, input, context) {
|
|
182
|
+
return returnJsonApiData(await workspaceMembersService.listInvites(resolveWorkspace(context, input), {
|
|
155
183
|
context
|
|
156
184
|
}));
|
|
157
185
|
}
|
|
@@ -161,7 +189,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
161
189
|
version: 1,
|
|
162
190
|
kind: "command",
|
|
163
191
|
channels: ["api", "assistant_tool", "automation", "internal"],
|
|
164
|
-
|
|
192
|
+
surfaces: ["*"],
|
|
165
193
|
permission: {
|
|
166
194
|
require: "all",
|
|
167
195
|
permissions: ["workspace.members.invite"]
|
|
@@ -173,13 +201,14 @@ const workspaceMembersActions = Object.freeze([
|
|
|
173
201
|
actionName: "workspace.invite.create"
|
|
174
202
|
},
|
|
175
203
|
observability: {},
|
|
204
|
+
events: WORKSPACE_INVITE_CREATED_EVENTS,
|
|
176
205
|
extensions: {
|
|
177
206
|
assistant: {
|
|
178
207
|
description: "Invite a person to the workspace."
|
|
179
208
|
}
|
|
180
209
|
},
|
|
181
|
-
async
|
|
182
|
-
return returnJsonApiData(await
|
|
210
|
+
async run(workspaceMembersService, input, context) {
|
|
211
|
+
return returnJsonApiData(await workspaceMembersService.createInvite(
|
|
183
212
|
resolveWorkspace(context, input),
|
|
184
213
|
resolveActionUser(context, input),
|
|
185
214
|
{
|
|
@@ -197,7 +226,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
197
226
|
version: 1,
|
|
198
227
|
kind: "command",
|
|
199
228
|
channels: ["api", "automation", "internal"],
|
|
200
|
-
|
|
229
|
+
surfaces: ["*"],
|
|
201
230
|
permission: {
|
|
202
231
|
require: "all",
|
|
203
232
|
permissions: ["workspace.invites.revoke"]
|
|
@@ -209,12 +238,23 @@ const workspaceMembersActions = Object.freeze([
|
|
|
209
238
|
actionName: "workspace.invite.revoke"
|
|
210
239
|
},
|
|
211
240
|
observability: {},
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
events: WORKSPACE_INVITE_REVOKED_EVENTS,
|
|
242
|
+
async run(workspaceMembersService, input, context) {
|
|
243
|
+
return returnJsonApiData(await workspaceMembersService.revokeInvite(resolveWorkspace(context, input), input.inviteId, {
|
|
214
244
|
context
|
|
215
245
|
}));
|
|
216
246
|
}
|
|
217
247
|
}
|
|
218
248
|
]);
|
|
219
249
|
|
|
220
|
-
|
|
250
|
+
function buildWorkspaceMembersActions({ workspaceMembersService } = {}) {
|
|
251
|
+
if (!workspaceMembersService) throw new TypeError("buildWorkspaceMembersActions requires workspaceMembersService.");
|
|
252
|
+
return workspaceMembersActionSpecifications.map(({ run, ...definition }) => Object.freeze({
|
|
253
|
+
...definition,
|
|
254
|
+
execute(input, context) {
|
|
255
|
+
return run(workspaceMembersService, input, context);
|
|
256
|
+
}
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { workspaceMembersActionSpecifications, buildWorkspaceMembersActions };
|
|
@@ -24,13 +24,11 @@ function resolveInviteResolutionRecordId(record = {}) {
|
|
|
24
24
|
return "workspace-invitation";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
if (!
|
|
29
|
-
throw new
|
|
27
|
+
function registerWorkspacePendingInvitationsRoutes(router) {
|
|
28
|
+
if (!router || typeof router.register !== "function") {
|
|
29
|
+
throw new TypeError("registerWorkspacePendingInvitationsRoutes requires router.register().");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const router = app.make("jskit.http.router");
|
|
33
|
-
|
|
34
32
|
router.register(
|
|
35
33
|
"GET",
|
|
36
34
|
"/api/workspace/invitations/resolve",
|
|
@@ -110,4 +108,4 @@ function bootWorkspacePendingInvitations(app) {
|
|
|
110
108
|
);
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
export {
|
|
111
|
+
export { registerWorkspacePendingInvitationsRoutes };
|
|
@@ -4,15 +4,21 @@ import {
|
|
|
4
4
|
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
5
5
|
import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
|
|
6
6
|
import { workspacePendingInvitationsResource } from "../../shared/resources/workspacePendingInvitationsResource.js";
|
|
7
|
+
import { createInviteDecisionEvents } from "../common/support/realtimeServiceEvents.js";
|
|
7
8
|
import { resolveActionUser } from "../common/support/resolveActionUser.js";
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
+
const WORKSPACE_INVITE_DECISION_EVENTS = createInviteDecisionEvents();
|
|
11
|
+
|
|
12
|
+
const workspacePendingInvitationsActionSpecifications = Object.freeze([
|
|
10
13
|
{
|
|
11
14
|
id: "workspace.invitation.resolve",
|
|
12
15
|
version: 1,
|
|
13
16
|
kind: "query",
|
|
14
17
|
channels: ["api", "automation", "internal"],
|
|
15
|
-
|
|
18
|
+
surfaces: ["*"],
|
|
19
|
+
permission: {
|
|
20
|
+
require: "none"
|
|
21
|
+
},
|
|
16
22
|
input: workspacePendingInvitationsResource.operations.resolve.query,
|
|
17
23
|
output: null,
|
|
18
24
|
idempotency: "none",
|
|
@@ -20,9 +26,9 @@ const workspacePendingInvitationsActions = Object.freeze([
|
|
|
20
26
|
actionName: "workspace.invitation.resolve"
|
|
21
27
|
},
|
|
22
28
|
observability: {},
|
|
23
|
-
async
|
|
29
|
+
async run(workspacePendingInvitationsService, input, context) {
|
|
24
30
|
return returnJsonApiData(
|
|
25
|
-
await
|
|
31
|
+
await workspacePendingInvitationsService.resolveInviteByToken(input?.token, {
|
|
26
32
|
context
|
|
27
33
|
})
|
|
28
34
|
);
|
|
@@ -33,7 +39,7 @@ const workspacePendingInvitationsActions = Object.freeze([
|
|
|
33
39
|
version: 1,
|
|
34
40
|
kind: "query",
|
|
35
41
|
channels: ["api", "automation", "internal"],
|
|
36
|
-
|
|
42
|
+
surfaces: ["*"],
|
|
37
43
|
permission: {
|
|
38
44
|
require: "authenticated"
|
|
39
45
|
},
|
|
@@ -44,9 +50,9 @@ const workspacePendingInvitationsActions = Object.freeze([
|
|
|
44
50
|
actionName: "workspace.invitations.pending.list"
|
|
45
51
|
},
|
|
46
52
|
observability: {},
|
|
47
|
-
async
|
|
53
|
+
async run(workspacePendingInvitationsService, input, context) {
|
|
48
54
|
return returnJsonApiData({
|
|
49
|
-
pendingInvites: await
|
|
55
|
+
pendingInvites: await workspacePendingInvitationsService.listPendingInvitesForUser(resolveActionUser(context, input), {
|
|
50
56
|
context
|
|
51
57
|
})
|
|
52
58
|
});
|
|
@@ -57,7 +63,7 @@ const workspacePendingInvitationsActions = Object.freeze([
|
|
|
57
63
|
version: 1,
|
|
58
64
|
kind: "command",
|
|
59
65
|
channels: ["api", "automation", "internal"],
|
|
60
|
-
|
|
66
|
+
surfaces: ["*"],
|
|
61
67
|
permission: {
|
|
62
68
|
require: "authenticated"
|
|
63
69
|
},
|
|
@@ -68,12 +74,13 @@ const workspacePendingInvitationsActions = Object.freeze([
|
|
|
68
74
|
actionName: "workspace.invite.redeem"
|
|
69
75
|
},
|
|
70
76
|
observability: {},
|
|
71
|
-
|
|
77
|
+
events: WORKSPACE_INVITE_DECISION_EVENTS,
|
|
78
|
+
async run(workspacePendingInvitationsService, input, context) {
|
|
72
79
|
const payload = input || {};
|
|
73
80
|
const user = resolveActionUser(context, input);
|
|
74
81
|
|
|
75
82
|
if (payload.decision === "accept") {
|
|
76
|
-
return returnJsonApiData(await
|
|
83
|
+
return returnJsonApiData(await workspacePendingInvitationsService.acceptInviteByToken({
|
|
77
84
|
user,
|
|
78
85
|
token: payload.token
|
|
79
86
|
}, {
|
|
@@ -81,7 +88,7 @@ const workspacePendingInvitationsActions = Object.freeze([
|
|
|
81
88
|
}));
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
return returnJsonApiData(await
|
|
91
|
+
return returnJsonApiData(await workspacePendingInvitationsService.refuseInviteByToken({
|
|
85
92
|
user,
|
|
86
93
|
token: payload.token
|
|
87
94
|
}, {
|
|
@@ -91,4 +98,16 @@ const workspacePendingInvitationsActions = Object.freeze([
|
|
|
91
98
|
}
|
|
92
99
|
]);
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
function buildWorkspacePendingInvitationsActions({ workspacePendingInvitationsService } = {}) {
|
|
102
|
+
if (!workspacePendingInvitationsService) {
|
|
103
|
+
throw new TypeError("buildWorkspacePendingInvitationsActions requires workspacePendingInvitationsService.");
|
|
104
|
+
}
|
|
105
|
+
return workspacePendingInvitationsActionSpecifications.map(({ run, ...definition }) => Object.freeze({
|
|
106
|
+
...definition,
|
|
107
|
+
execute(input, context) {
|
|
108
|
+
return run(workspacePendingInvitationsService, input, context);
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { workspacePendingInvitationsActionSpecifications, buildWorkspacePendingInvitationsActions };
|
|
@@ -19,14 +19,11 @@ function resolveWorkspaceSettingsRecordId(record = {}, context = {}) {
|
|
|
19
19
|
throw new Error("Workspace settings JSON:API response requires workspace id.");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function
|
|
23
|
-
if (!
|
|
24
|
-
throw new
|
|
22
|
+
function registerWorkspaceSettingsRoutes(router, { config = {} } = {}) {
|
|
23
|
+
if (!router || typeof router.register !== "function") {
|
|
24
|
+
throw new TypeError("registerWorkspaceSettingsRoutes requires router.register().");
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
const router = app.make("jskit.http.router");
|
|
28
|
-
const appConfig = typeof app.has === "function" && app.has("appConfig") ? app.make("appConfig") : {};
|
|
29
|
-
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
|
|
26
|
+
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(config);
|
|
30
27
|
|
|
31
28
|
router.register(
|
|
32
29
|
"GET",
|
|
@@ -92,4 +89,4 @@ function bootWorkspaceSettings(app) {
|
|
|
92
89
|
);
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
export {
|
|
92
|
+
export { registerWorkspaceSettingsRoutes };
|
|
@@ -2,8 +2,15 @@ import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
|
|
|
2
2
|
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
3
3
|
import { workspaceSettingsResource } from "../../shared/resources/workspaceSettingsResource.js";
|
|
4
4
|
import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
5
|
+
import { createWorkspaceEntityAndBootstrapEvents } from "../common/support/realtimeServiceEvents.js";
|
|
5
6
|
import { resolveWorkspace } from "../support/resolveWorkspace.js";
|
|
6
7
|
|
|
8
|
+
const WORKSPACE_SETTINGS_CHANGED_EVENTS = createWorkspaceEntityAndBootstrapEvents({
|
|
9
|
+
workspaceEntity: "settings",
|
|
10
|
+
workspaceOperation: "updated",
|
|
11
|
+
workspaceRealtimeEvent: "workspace.settings.changed"
|
|
12
|
+
});
|
|
13
|
+
|
|
7
14
|
const workspaceSettingsUpdateInputValidator = composeSchemaDefinitions([
|
|
8
15
|
workspaceSlugParamsValidator,
|
|
9
16
|
workspaceSettingsResource.operations.patch.body
|
|
@@ -12,13 +19,13 @@ const workspaceSettingsUpdateInputValidator = composeSchemaDefinitions([
|
|
|
12
19
|
context: "workspaceSettingsActions.workspaceSettingsUpdateInputValidator"
|
|
13
20
|
});
|
|
14
21
|
|
|
15
|
-
const
|
|
22
|
+
const workspaceSettingsActionSpecifications = Object.freeze([
|
|
16
23
|
{
|
|
17
24
|
id: "workspace.settings.read",
|
|
18
25
|
version: 1,
|
|
19
26
|
kind: "query",
|
|
20
27
|
channels: ["api", "automation", "internal"],
|
|
21
|
-
|
|
28
|
+
surfaces: ["*"],
|
|
22
29
|
permission: {
|
|
23
30
|
require: "any",
|
|
24
31
|
permissions: ["workspace.settings.view", "workspace.settings.update"]
|
|
@@ -30,8 +37,8 @@ const workspaceSettingsActions = Object.freeze([
|
|
|
30
37
|
actionName: "workspace.settings.read"
|
|
31
38
|
},
|
|
32
39
|
observability: {},
|
|
33
|
-
async
|
|
34
|
-
const response = await
|
|
40
|
+
async run(workspaceSettingsService, input, context) {
|
|
41
|
+
const response = await workspaceSettingsService.getWorkspaceSettings(resolveWorkspace(context, input), {
|
|
35
42
|
context
|
|
36
43
|
});
|
|
37
44
|
|
|
@@ -43,7 +50,7 @@ const workspaceSettingsActions = Object.freeze([
|
|
|
43
50
|
version: 1,
|
|
44
51
|
kind: "command",
|
|
45
52
|
channels: ["api", "assistant_tool", "automation", "internal"],
|
|
46
|
-
|
|
53
|
+
surfaces: ["*"],
|
|
47
54
|
permission: {
|
|
48
55
|
require: "all",
|
|
49
56
|
permissions: ["workspace.settings.update"]
|
|
@@ -55,14 +62,15 @@ const workspaceSettingsActions = Object.freeze([
|
|
|
55
62
|
actionName: "workspace.settings.update"
|
|
56
63
|
},
|
|
57
64
|
observability: {},
|
|
65
|
+
events: WORKSPACE_SETTINGS_CHANGED_EVENTS,
|
|
58
66
|
extensions: {
|
|
59
67
|
assistant: {
|
|
60
68
|
description: "Update workspace settings."
|
|
61
69
|
}
|
|
62
70
|
},
|
|
63
|
-
async
|
|
71
|
+
async run(workspaceSettingsService, input, context) {
|
|
64
72
|
const { workspaceSlug, ...patch } = input;
|
|
65
|
-
const response = await
|
|
73
|
+
const response = await workspaceSettingsService.updateWorkspaceSettings(
|
|
66
74
|
resolveWorkspace(context, input),
|
|
67
75
|
patch,
|
|
68
76
|
{
|
|
@@ -75,4 +83,14 @@ const workspaceSettingsActions = Object.freeze([
|
|
|
75
83
|
}
|
|
76
84
|
]);
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
function buildWorkspaceSettingsActions({ workspaceSettingsService } = {}) {
|
|
87
|
+
if (!workspaceSettingsService) throw new TypeError("buildWorkspaceSettingsActions requires workspaceSettingsService.");
|
|
88
|
+
return workspaceSettingsActionSpecifications.map(({ run, ...definition }) => Object.freeze({
|
|
89
|
+
...definition,
|
|
90
|
+
execute(input, context) {
|
|
91
|
+
return run(workspaceSettingsService, input, context);
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { workspaceSettingsActionSpecifications, buildWorkspaceSettingsActions };
|
|
@@ -35,7 +35,7 @@ function pickPatchFields(source = {}) {
|
|
|
35
35
|
return patch;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function createDefaultWorkspaceSettingsCreatePayload(workspaceId) {
|
|
38
|
+
function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled) {
|
|
39
39
|
const palettes = resolveWorkspaceThemePalettes({});
|
|
40
40
|
|
|
41
41
|
return {
|
|
@@ -48,11 +48,11 @@ function createDefaultWorkspaceSettingsCreatePayload(workspaceId) {
|
|
|
48
48
|
darkSecondaryColor: palettes.dark.secondaryColor,
|
|
49
49
|
darkSurfaceColor: palettes.dark.surfaceColor,
|
|
50
50
|
darkSurfaceVariantColor: palettes.dark.surfaceVariantColor,
|
|
51
|
-
invitesEnabled:
|
|
51
|
+
invitesEnabled: defaultInvitesEnabled
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function createRepository({ api, knex } = {}) {
|
|
55
|
+
function createRepository({ api, knex, defaultInvitesEnabled = true } = {}) {
|
|
56
56
|
if (!api?.resources?.workspaceSettings) {
|
|
57
57
|
throw new TypeError("workspaceSettingsRepository requires json-rest-api workspaceSettings resource.");
|
|
58
58
|
}
|
|
@@ -103,7 +103,7 @@ function createRepository({ api, knex } = {}) {
|
|
|
103
103
|
{
|
|
104
104
|
inputRecord: createJsonApiInputRecord(
|
|
105
105
|
RESOURCE_TYPE,
|
|
106
|
-
createDefaultWorkspaceSettingsCreatePayload(normalizedWorkspaceId),
|
|
106
|
+
createDefaultWorkspaceSettingsCreatePayload(normalizedWorkspaceId, defaultInvitesEnabled === true),
|
|
107
107
|
{
|
|
108
108
|
id: normalizedWorkspaceId
|
|
109
109
|
}
|
|
@@ -14,7 +14,8 @@ test("workspaces-core exports are explicit and aligned with production usage", (
|
|
|
14
14
|
packageDir: PACKAGE_DIR,
|
|
15
15
|
packageId: "@jskit-ai/workspaces-core",
|
|
16
16
|
requiredExports: [
|
|
17
|
-
"./server/
|
|
17
|
+
"./server/WorkspacesFeature",
|
|
18
|
+
"./server/WorkspacesIntegrationsProvider",
|
|
18
19
|
"./server/validators/routeParamsValidator",
|
|
19
20
|
"./server/support/workspaceRouteInput",
|
|
20
21
|
"./shared/resources/workspaceResource",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createAuthExtensions } from "@jskit-ai/auth-core/server/authExtensions";
|
|
4
|
+
import { createActionProvider } from "@jskit-ai/kernel/server/actions";
|
|
5
|
+
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
6
|
+
import { createBootstrapRuntime } from "@jskit-ai/kernel/server/runtime";
|
|
7
|
+
import { createUsersExtensions } from "@jskit-ai/users-core/server/usersExtensions";
|
|
8
|
+
import { WorkspacesFeature } from "../src/server/WorkspacesFeature.js";
|
|
9
|
+
import { WorkspacesIntegrationsProvider } from "../src/server/WorkspacesIntegrationsProvider.js";
|
|
10
|
+
|
|
11
|
+
function createJsonRestApiFixture() {
|
|
12
|
+
const resources = {};
|
|
13
|
+
return {
|
|
14
|
+
resources,
|
|
15
|
+
async addResource(scopeName) {
|
|
16
|
+
resources[scopeName] = Object.freeze({
|
|
17
|
+
query: async () => ({ data: [] }),
|
|
18
|
+
post: async () => null,
|
|
19
|
+
patch: async () => null
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runtimeConfig() {
|
|
26
|
+
return {
|
|
27
|
+
appPublicUrl: "http://localhost:5173",
|
|
28
|
+
tenancyMode: "workspaces",
|
|
29
|
+
tenancyPolicy: { workspace: { allowSelfCreate: true } },
|
|
30
|
+
workspaceInvitations: { enabled: true },
|
|
31
|
+
workspaceMembers: { defaults: { inviteExpiresInMs: 86400000 } },
|
|
32
|
+
workspaceSettings: { defaults: { invitesEnabled: true } },
|
|
33
|
+
surfaceDefaultId: "customer",
|
|
34
|
+
surfaceDefinitions: {
|
|
35
|
+
customer: { id: "customer", requiresWorkspace: true, accessPolicyId: "workspace_authenticated" }
|
|
36
|
+
},
|
|
37
|
+
surfaceAccessPolicies: {
|
|
38
|
+
workspace_authenticated: { requireWorkspaceMembership: false }
|
|
39
|
+
},
|
|
40
|
+
roleCatalog: {
|
|
41
|
+
workspace: { defaultInviteRole: "member" },
|
|
42
|
+
roles: {
|
|
43
|
+
owner: { permissions: ["workspace.*"] },
|
|
44
|
+
member: { assignable: true, permissions: ["workspace.settings.view"] }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("WorkspacesFeature assembles workspace behavior and explicit integrations", async () => {
|
|
51
|
+
const routes = [];
|
|
52
|
+
const visibilityResolvers = [];
|
|
53
|
+
const bootstrap = createBootstrapRuntime();
|
|
54
|
+
const authExtensions = createAuthExtensions();
|
|
55
|
+
const usersExtensions = createUsersExtensions();
|
|
56
|
+
const jsonRestApi = createJsonRestApiFixture();
|
|
57
|
+
let workspaces = null;
|
|
58
|
+
let actionCatalogue = null;
|
|
59
|
+
const probe = defineProvider({
|
|
60
|
+
id: "test.workspaces.probe",
|
|
61
|
+
requires: {
|
|
62
|
+
actions: "runtime.actions",
|
|
63
|
+
workspaceCapability: "workspaces.core"
|
|
64
|
+
},
|
|
65
|
+
setup({ actions, workspaceCapability }) {
|
|
66
|
+
actionCatalogue = actions;
|
|
67
|
+
workspaces = workspaceCapability;
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const runtime = createCapabilityRuntime({
|
|
72
|
+
inputs: {
|
|
73
|
+
"auth.extensions": authExtensions,
|
|
74
|
+
"runtime.bootstrap": bootstrap,
|
|
75
|
+
"runtime.config": runtimeConfig(),
|
|
76
|
+
"runtime.database": { knex() {} },
|
|
77
|
+
"runtime.env": {},
|
|
78
|
+
"runtime.http": {
|
|
79
|
+
router: {
|
|
80
|
+
register(method, path) {
|
|
81
|
+
routes.push(`${method} ${path}`);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
registerVisibilityResolver(resolver) {
|
|
85
|
+
visibilityResolvers.push(resolver);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"runtime.json-rest-api": jsonRestApi,
|
|
89
|
+
"users.core": {
|
|
90
|
+
repositories: {
|
|
91
|
+
userProfiles: { async findById() { return null; } }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"users.extensions": usersExtensions
|
|
95
|
+
},
|
|
96
|
+
providers: [createActionProvider(), WorkspacesFeature, WorkspacesIntegrationsProvider, probe]
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await runtime.start();
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(Object.keys(jsonRestApi.resources).sort(), [
|
|
102
|
+
"workspaceInvites",
|
|
103
|
+
"workspaceMemberships",
|
|
104
|
+
"workspaceSettings",
|
|
105
|
+
"workspaces"
|
|
106
|
+
]);
|
|
107
|
+
assert.equal(typeof workspaces.services.directory.resolveWorkspaceContextForUserBySlug, "function");
|
|
108
|
+
assert.equal(routes.includes("POST /api/workspaces"), true);
|
|
109
|
+
assert.equal(routes.includes("GET /api/workspace/invitations/resolve"), true);
|
|
110
|
+
assert.equal(actionCatalogue.listDefinitions().length, 16);
|
|
111
|
+
assert.deepEqual(bootstrap.diagnostics().contributorIds, ["workspaces.bootstrap"]);
|
|
112
|
+
assert.deepEqual(visibilityResolvers.map((entry) => entry.id), ["workspaces.visibility"]);
|
|
113
|
+
assert.deepEqual(usersExtensions.diagnostics().profileSyncLifecycleContributorIds, ["workspaces.provisioning"]);
|
|
114
|
+
assert.deepEqual(authExtensions.diagnostics().invitationContextResolverIds, ["workspaces.invitation"]);
|
|
115
|
+
assert.deepEqual(authExtensions.diagnostics().policyContextResolverIds, ["workspaces.policy"]);
|
|
116
|
+
});
|