@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.
Files changed (49) hide show
  1. package/package.json +38 -238
  2. package/patterns/workspace-server/PATTERN.md +60 -0
  3. package/src/server/WorkspacesFeature.js +226 -0
  4. package/src/server/WorkspacesIntegrationsProvider.js +74 -0
  5. package/src/server/common/contributors/workspaceActionContextContributor.js +18 -6
  6. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +25 -4
  7. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
  8. package/src/server/common/services/workspaceContextService.js +7 -2
  9. package/src/server/common/support/realtimeServiceEvents.js +102 -61
  10. package/src/server/support/workspaceActionSurfaces.js +18 -13
  11. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
  12. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
  13. package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
  14. package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
  15. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
  16. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
  17. package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
  18. package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
  19. package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
  20. package/test/exportsContract.test.js +2 -1
  21. package/test/featureRuntime.test.js +116 -0
  22. package/test/packageMetadata.test.js +20 -54
  23. package/test/workspaceActionContextContributor.test.js +49 -8
  24. package/test/workspaceActionSurfaces.test.js +36 -1
  25. package/test/workspaceAuthPolicyContextResolver.test.js +61 -0
  26. package/test/workspaceRouteVisibilityResolver.test.js +31 -0
  27. package/test/workspaceService.test.js +62 -0
  28. package/test/workspaceSettingsActions.test.js +102 -16
  29. package/test/workspacesRouteRequestInputValidator.test.js +14 -65
  30. package/src/server/WorkspacesCoreServiceProvider.js +0 -64
  31. package/src/server/registerWorkspaceBootstrap.js +0 -27
  32. package/src/server/registerWorkspaceCore.js +0 -101
  33. package/src/server/registerWorkspaceRepositories.js +0 -30
  34. package/src/server/support/workspaceServerScopeSupport.js +0 -18
  35. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
  36. package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -137
  37. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -130
  38. package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -66
  39. package/test/registerServiceRealtimeEvents.test.js +0 -116
  40. package/test/registerWorkspaceBootstrap.test.js +0 -86
  41. package/test/registerWorkspaceDirectory.test.js +0 -31
  42. package/test/registerWorkspaceMembers.test.js +0 -140
  43. package/test/registerWorkspaceSettings.test.js +0 -40
  44. package/test/workspaceServerScopeSupport.test.js +0 -131
  45. /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
  46. /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
  47. /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
  48. /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
  49. /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 workspaceMembersActions = Object.freeze([
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
- surfacesFrom: "workspace",
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 execute(_input, context, deps) {
59
- return returnJsonApiData(await deps.workspaceMembersService.listRoles({ context }));
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
- surfacesFrom: "workspace",
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 execute(input, context, deps) {
80
- return returnJsonApiData(await deps.workspaceMembersService.listMembers(resolveWorkspace(context, input), {
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
- surfacesFrom: "workspace",
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
- async execute(input, context, deps) {
103
- return returnJsonApiData(await deps.workspaceMembersService.updateMemberRole(resolveWorkspace(context, input), {
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
- surfacesFrom: "workspace",
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
- async execute(input, context, deps) {
129
- return returnJsonApiData(await deps.workspaceMembersService.removeMember(resolveWorkspace(context, input), {
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
- surfacesFrom: "workspace",
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 execute(input, context, deps) {
154
- return returnJsonApiData(await deps.workspaceMembersService.listInvites(resolveWorkspace(context, input), {
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
- surfacesFrom: "workspace",
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 execute(input, context, deps) {
182
- return returnJsonApiData(await deps.workspaceMembersService.createInvite(
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
- surfacesFrom: "workspace",
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
- async execute(input, context, deps) {
213
- return returnJsonApiData(await deps.workspaceMembersService.revokeInvite(resolveWorkspace(context, input), input.inviteId, {
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
- export { workspaceMembersActions };
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 bootWorkspacePendingInvitations(app) {
28
- if (!app || typeof app.make !== "function") {
29
- throw new Error("bootWorkspacePendingInvitations requires application make().");
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 { bootWorkspacePendingInvitations };
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 workspacePendingInvitationsActions = Object.freeze([
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
- surfacesFrom: "enabled",
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 execute(input, context, deps) {
29
+ async run(workspacePendingInvitationsService, input, context) {
24
30
  return returnJsonApiData(
25
- await deps.workspacePendingInvitationsService.resolveInviteByToken(input?.token, {
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
- surfacesFrom: "enabled",
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 execute(input, context, deps) {
53
+ async run(workspacePendingInvitationsService, input, context) {
48
54
  return returnJsonApiData({
49
- pendingInvites: await deps.workspacePendingInvitationsService.listPendingInvitesForUser(resolveActionUser(context, input), {
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
- surfacesFrom: "enabled",
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
- async execute(input, context, deps) {
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 deps.workspacePendingInvitationsService.acceptInviteByToken({
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 deps.workspacePendingInvitationsService.refuseInviteByToken({
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
- export { workspacePendingInvitationsActions };
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 bootWorkspaceSettings(app) {
23
- if (!app || typeof app.make !== "function") {
24
- throw new Error("bootWorkspaceSettings requires application make().");
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 { bootWorkspaceSettings };
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 workspaceSettingsActions = Object.freeze([
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
- surfacesFrom: "workspace",
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 execute(input, context, deps) {
34
- const response = await deps.workspaceSettingsService.getWorkspaceSettings(resolveWorkspace(context, input), {
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
- surfacesFrom: "workspace",
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 execute(input, context, deps) {
71
+ async run(workspaceSettingsService, input, context) {
64
72
  const { workspaceSlug, ...patch } = input;
65
- const response = await deps.workspaceSettingsService.updateWorkspaceSettings(
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
- export { workspaceSettingsActions };
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: true
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/WorkspacesCoreServiceProvider",
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
+ });