@jskit-ai/workspaces-core 0.1.138-oldline.1 → 0.1.139

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 (50) hide show
  1. package/package.json +154 -10
  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 +5 -9
  6. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +16 -2
  7. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
  8. package/src/server/common/support/realtimeServiceEvents.js +102 -61
  9. package/src/server/support/workspaceActionSurfaces.js +1 -13
  10. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
  11. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
  12. package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
  13. package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
  14. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
  15. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
  16. package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
  17. package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
  18. package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
  19. package/test/exportsContract.test.js +2 -1
  20. package/test/featureRuntime.test.js +116 -0
  21. package/test/packageMetadata.test.js +59 -0
  22. package/test/usersRouteResources.test.js +1 -1
  23. package/test/workspaceActionContextContributor.test.js +7 -12
  24. package/test/workspaceAuthPolicyContextResolver.test.js +30 -0
  25. package/test/workspaceRouteVisibilityResolver.test.js +31 -0
  26. package/test/workspaceService.test.js +2 -2
  27. package/test/workspaceSettingsActions.test.js +103 -17
  28. package/test/workspacesRouteRequestInputValidator.test.js +14 -65
  29. package/package.descriptor.mjs +0 -352
  30. package/src/server/WorkspacesCoreServiceProvider.js +0 -64
  31. package/src/server/registerWorkspaceBootstrap.js +0 -27
  32. package/src/server/registerWorkspaceCore.js +0 -103
  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/packageDescriptor.test.js +0 -91
  40. package/test/registerServiceRealtimeEvents.test.js +0 -116
  41. package/test/registerWorkspaceBootstrap.test.js +0 -86
  42. package/test/registerWorkspaceDirectory.test.js +0 -31
  43. package/test/registerWorkspaceMembers.test.js +0 -140
  44. package/test/registerWorkspaceSettings.test.js +0 -40
  45. package/test/workspaceServerScopeSupport.test.js +0 -131
  46. /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
  47. /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
  48. /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
  49. /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
  50. /package/{templates → patterns/workspace-server/example}/packages/main/src/server/email/workspaceInviteEmail.js +0 -0
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.1.138-oldline.1",
3
+ "version": "0.1.139",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "exports": {
9
- "./server/WorkspacesCoreServiceProvider": "./src/server/WorkspacesCoreServiceProvider.js",
9
+ "./server/WorkspacesFeature": "./src/server/WorkspacesFeature.js",
10
+ "./server/WorkspacesIntegrationsProvider": "./src/server/WorkspacesIntegrationsProvider.js",
10
11
  "./server/validators/routeParamsValidator": "./src/server/common/validators/routeParamsValidator.js",
11
12
  "./server/support/workspaceRouteInput": "./src/server/support/workspaceRouteInput.js",
12
13
  "./shared/jsonApiTransports": "./src/shared/jsonApiTransports.js",
@@ -17,14 +18,157 @@
17
18
  "./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
18
19
  },
19
20
  "dependencies": {
20
- "@jskit-ai/auth-core": "0.1.146",
21
- "@jskit-ai/database-runtime": "0.1.147",
22
- "@jskit-ai/http-runtime": "0.1.146",
23
- "@jskit-ai/json-rest-api-core": "0.1.92",
24
- "@jskit-ai/kernel": "0.1.147",
25
- "@jskit-ai/resource-crud-core": "0.1.90",
26
- "@jskit-ai/resource-core": "0.1.90",
27
- "@jskit-ai/users-core": "0.1.161",
21
+ "@jskit-ai/auth-core": "0.1.159",
22
+ "@jskit-ai/database-runtime": "0.1.161",
23
+ "@jskit-ai/http-runtime": "0.1.159",
24
+ "@jskit-ai/json-rest-api-core": "0.1.105",
25
+ "@jskit-ai/kernel": "0.1.161",
26
+ "@jskit-ai/resource-core": "0.1.103",
27
+ "@jskit-ai/resource-crud-core": "0.1.103",
28
+ "@jskit-ai/users-core": "0.1.174",
28
29
  "json-rest-schema": "^1.0.17"
30
+ },
31
+ "description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
32
+ "jskit": {
33
+ "kind": "runtime",
34
+ "capabilities": {
35
+ "provides": [
36
+ "workspaces.core"
37
+ ],
38
+ "requires": [
39
+ "auth.extensions",
40
+ "runtime.actions",
41
+ "runtime.bootstrap",
42
+ "runtime.config",
43
+ "runtime.database",
44
+ "runtime.env",
45
+ "runtime.http",
46
+ "runtime.json-rest-api",
47
+ "users.core",
48
+ "users.extensions"
49
+ ]
50
+ },
51
+ "runtime": {
52
+ "server": {
53
+ "providers": [
54
+ {
55
+ "entrypoint": "src/server/WorkspacesFeature.js",
56
+ "export": "WorkspacesFeature"
57
+ },
58
+ {
59
+ "entrypoint": "src/server/WorkspacesIntegrationsProvider.js",
60
+ "export": "WorkspacesIntegrationsProvider"
61
+ }
62
+ ]
63
+ },
64
+ "client": {
65
+ "providers": []
66
+ }
67
+ },
68
+ "metadata": {
69
+ "jskit": {
70
+ "tableOwnership": {
71
+ "tables": [
72
+ {
73
+ "tableName": "workspaces"
74
+ },
75
+ {
76
+ "tableName": "workspace_memberships"
77
+ },
78
+ {
79
+ "tableName": "workspace_settings"
80
+ },
81
+ {
82
+ "tableName": "workspace_invites"
83
+ }
84
+ ]
85
+ }
86
+ },
87
+ "apiSummary": {
88
+ "surfaces": [
89
+ {
90
+ "subpath": "./server",
91
+ "summary": "Exports the explicit workspace feature, integrations, services, repositories, actions, and routes."
92
+ }
93
+ ],
94
+ "capabilities": [
95
+ "workspaces.core"
96
+ ]
97
+ },
98
+ "server": {
99
+ "routes": [
100
+ {
101
+ "method": "POST",
102
+ "path": "/api/workspaces",
103
+ "summary": "Create a workspace for the authenticated user."
104
+ },
105
+ {
106
+ "method": "GET",
107
+ "path": "/api/workspaces",
108
+ "summary": "List workspaces visible to authenticated user."
109
+ },
110
+ {
111
+ "method": "GET",
112
+ "path": "/api/workspace/invitations/resolve",
113
+ "summary": "Resolve safe public workspace invitation metadata."
114
+ },
115
+ {
116
+ "method": "GET",
117
+ "path": "/api/workspace/invitations/pending",
118
+ "summary": "List pending workspace invitations for authenticated user."
119
+ },
120
+ {
121
+ "method": "POST",
122
+ "path": "/api/workspace/invitations/redeem",
123
+ "summary": "Accept or refuse a workspace invitation using an invite token."
124
+ },
125
+ {
126
+ "method": "GET",
127
+ "path": "/api/w/:workspaceSlug/settings",
128
+ "summary": "Get workspace settings and role catalog by workspace slug."
129
+ },
130
+ {
131
+ "method": "PATCH",
132
+ "path": "/api/w/:workspaceSlug/settings",
133
+ "summary": "Update workspace settings by workspace slug."
134
+ },
135
+ {
136
+ "method": "GET",
137
+ "path": "/api/w/:workspaceSlug/roles",
138
+ "summary": "Get role catalog by workspace slug."
139
+ },
140
+ {
141
+ "method": "GET",
142
+ "path": "/api/w/:workspaceSlug/members",
143
+ "summary": "List members by workspace slug."
144
+ },
145
+ {
146
+ "method": "PATCH",
147
+ "path": "/api/w/:workspaceSlug/members/:memberUserId/role",
148
+ "summary": "Update workspace member role by workspace slug."
149
+ },
150
+ {
151
+ "method": "GET",
152
+ "path": "/api/w/:workspaceSlug/invites",
153
+ "summary": "List workspace invites by workspace slug."
154
+ },
155
+ {
156
+ "method": "POST",
157
+ "path": "/api/w/:workspaceSlug/invites",
158
+ "summary": "Create workspace invite by workspace slug."
159
+ },
160
+ {
161
+ "method": "DELETE",
162
+ "path": "/api/w/:workspaceSlug/invites/:inviteId",
163
+ "summary": "Revoke workspace invite by workspace slug."
164
+ }
165
+ ]
166
+ }
167
+ },
168
+ "migrations": {
169
+ "directories": [
170
+ "migrations"
171
+ ]
172
+ }
29
173
  }
30
174
  }
@@ -0,0 +1,60 @@
1
+ ---
2
+ id: workspaces/workspace-server
3
+ title: Workspace tenancy server composition
4
+ summary: Configure roles, workspace access policy, invitations, and app-owned invitation email rendering around JSKIT workspace runtime APIs.
5
+ keywords: access, invite, membership, multitenancy, policy, role, tenancy, workspace
6
+ requires: @jskit-ai/workspaces-core, @jskit-ai/users-core
7
+ ---
8
+
9
+ # Workspace tenancy server composition
10
+
11
+ ## Use when
12
+
13
+ Use this pattern when records, routes, or navigation belong to a selected
14
+ workspace and membership or role affects access.
15
+
16
+ ## Do not use when
17
+
18
+ Do not use workspace tenancy for a genuinely single-owner product or as a
19
+ substitute for a simpler domain ownership column.
20
+
21
+ ## Product decisions
22
+
23
+ Choose workspace creation policy, roles, invitation policy, expiry, email copy,
24
+ member permissions, personal-workspace behavior, and surface access rules.
25
+
26
+ ## Framework APIs
27
+
28
+ Use workspace routes, resources, scope helpers, tenancy profiles, and settings
29
+ contracts exported by `@jskit-ai/workspaces-core`.
30
+
31
+ ## Invariants
32
+
33
+ - Workspace membership and role are checked before scoped application actions.
34
+ - Role ids are stable product vocabulary shared by server and client config.
35
+ - Invitation tokens remain secret; emails receive only the intended link.
36
+ - Workspace scope reaches repository queries, not just route parsing.
37
+ - Package-owned migrations run directly from the installed package graph.
38
+
39
+ ## Example files
40
+
41
+ `example/config/roles.js` is a concrete role catalogue.
42
+ `example/packages/main/src/server/email/workspaceInviteEmail.js` demonstrates an
43
+ app-owned invitation renderer. Compose both through normal application config.
44
+
45
+ ## Variation points
46
+
47
+ Change roles, invitation policy and copy, defaults, access policies, surface
48
+ ids, and product-specific workspace settings.
49
+
50
+ ## Verification
51
+
52
+ Test create/list/select, member and non-member access, each role boundary,
53
+ invitation expiry/redemption, cross-workspace isolation, and email output.
54
+
55
+ ## Avoid
56
+
57
+ - synthetic membership workarounds
58
+ - authorizing from client navigation state
59
+ - embedding invitation templates or secrets in package metadata
60
+ - source-appending install commands
@@ -0,0 +1,226 @@
1
+ import { toDatabaseDateTimeUtc } from "@jskit-ai/database-runtime/shared";
2
+ import {
3
+ addResourceIfMissing,
4
+ createJsonRestResourceScopeOptions
5
+ } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
6
+ import { defineFeature } from "@jskit-ai/kernel/server/features";
7
+ import { workspaceInvitesResource } from "../shared/resources/workspaceInvitesResource.js";
8
+ import { workspaceMembershipsResource } from "../shared/resources/workspaceMembershipsResource.js";
9
+ import { workspaceResource } from "../shared/resources/workspaceResource.js";
10
+ import { workspaceSettingsResource } from "../shared/resources/workspaceSettingsResource.js";
11
+ import { createWorkspaceRoleCatalog } from "../shared/roles.js";
12
+ import { resolveTenancyProfile } from "../shared/tenancyProfile.js";
13
+ import { createRepository as createWorkspaceInvitesRepository } from "./common/repositories/workspaceInvitesRepository.js";
14
+ import { createRepository as createWorkspaceMembershipsRepository } from "./common/repositories/workspaceMembershipsRepository.js";
15
+ import { createRepository as createWorkspacesRepository } from "./common/repositories/workspacesRepository.js";
16
+ import { createService as createWorkspaceService } from "./common/services/workspaceContextService.js";
17
+ import { createWorkspaceBootstrapContributor } from "./workspaceBootstrapContributor.js";
18
+ import { registerWorkspaceDirectoryRoutes } from "./workspaceDirectory/bootWorkspaceDirectoryRoutes.js";
19
+ import { buildWorkspaceDirectoryActions } from "./workspaceDirectory/workspaceDirectoryActions.js";
20
+ import { renderDefaultWorkspaceInviteEmail } from "./workspaceMembers/defaultWorkspaceInviteEmail.js";
21
+ import { registerWorkspaceMembersRoutes } from "./workspaceMembers/bootWorkspaceMembers.js";
22
+ import { buildWorkspaceMembersActions } from "./workspaceMembers/workspaceMembersActions.js";
23
+ import { createWorkspaceInviteUrlBuilder } from "./workspaceMembers/workspaceInviteUrls.js";
24
+ import { createService as createWorkspaceMembersService } from "./workspaceMembers/workspaceMembersService.js";
25
+ import { registerWorkspacePendingInvitationsRoutes } from "./workspacePendingInvitations/bootWorkspacePendingInvitations.js";
26
+ import { buildWorkspacePendingInvitationsActions } from "./workspacePendingInvitations/workspacePendingInvitationsActions.js";
27
+ import { createService as createWorkspacePendingInvitationsService } from "./workspacePendingInvitations/workspacePendingInvitationsService.js";
28
+ import { registerWorkspaceSettingsRoutes } from "./workspaceSettings/bootWorkspaceSettings.js";
29
+ import { createRepository as createWorkspaceSettingsRepository } from "./workspaceSettings/workspaceSettingsRepository.js";
30
+ import { buildWorkspaceSettingsActions } from "./workspaceSettings/workspaceSettingsActions.js";
31
+ import { createService as createWorkspaceSettingsService } from "./workspaceSettings/workspaceSettingsService.js";
32
+ import { resolveWorkspaceInvitationsPolicy } from "./support/workspaceInvitationsPolicy.js";
33
+ import { workspaceSlugParamsValidator } from "./common/validators/routeParamsValidator.js";
34
+ import { buildWorkspaceInputFromRouteParams } from "./support/workspaceRouteInput.js";
35
+ import { resolveWorkspace } from "./support/resolveWorkspace.js";
36
+
37
+ function requirePositiveInteger(value, label) {
38
+ const normalized = Number(value);
39
+ if (!Number.isInteger(normalized) || normalized < 1) {
40
+ throw new TypeError(`${label} must be a positive integer.`);
41
+ }
42
+ return normalized;
43
+ }
44
+
45
+ function requireBoolean(value, label) {
46
+ if (typeof value !== "boolean") {
47
+ throw new TypeError(`${label} must be boolean.`);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function resolveInviteEmailTemplate(config) {
53
+ const template =
54
+ config.workspaceInviteEmailTemplate ||
55
+ config.workspaceInviteEmailRenderer ||
56
+ config.workspaceInvitations?.emailTemplate ||
57
+ config.workspaceInvitations?.emailRenderer;
58
+ return typeof template === "function" ? template : renderDefaultWorkspaceInviteEmail;
59
+ }
60
+
61
+ async function installWorkspaceResources(jsonRestApi) {
62
+ const scopeOptions = { writeSerializers: { "datetime-utc": toDatabaseDateTimeUtc } };
63
+ await addResourceIfMissing(
64
+ jsonRestApi,
65
+ "workspaces",
66
+ createJsonRestResourceScopeOptions(workspaceResource, scopeOptions)
67
+ );
68
+ await addResourceIfMissing(
69
+ jsonRestApi,
70
+ "workspaceMemberships",
71
+ createJsonRestResourceScopeOptions(workspaceMembershipsResource, scopeOptions)
72
+ );
73
+ await addResourceIfMissing(
74
+ jsonRestApi,
75
+ "workspaceInvites",
76
+ createJsonRestResourceScopeOptions(workspaceInvitesResource, scopeOptions)
77
+ );
78
+ await addResourceIfMissing(
79
+ jsonRestApi,
80
+ "workspaceSettings",
81
+ createJsonRestResourceScopeOptions(workspaceSettingsResource, scopeOptions)
82
+ );
83
+ }
84
+
85
+ function createWorkspacesRuntime({ config, database, env, jsonRestApi } = {}) {
86
+ const tenancyProfile = resolveTenancyProfile(config);
87
+ const invitationsPolicy = resolveWorkspaceInvitationsPolicy({ config, appConfig: config, tenancyProfile });
88
+ const invitationsEnabled = invitationsPolicy.enabled === true;
89
+ const roleCatalog = createWorkspaceRoleCatalog(config);
90
+ const workspaceSettingsRepository = createWorkspaceSettingsRepository({
91
+ api: jsonRestApi,
92
+ knex: database.knex,
93
+ defaultInvitesEnabled: requireBoolean(
94
+ config?.workspaceSettings?.defaults?.invitesEnabled,
95
+ "runtime.config.workspaceSettings.defaults.invitesEnabled"
96
+ )
97
+ });
98
+ const workspacesRepository = createWorkspacesRepository({ api: jsonRestApi, knex: database.knex });
99
+ const workspaceMembershipsRepository = createWorkspaceMembershipsRepository({
100
+ api: jsonRestApi,
101
+ knex: database.knex
102
+ });
103
+ const workspaceInvitesRepository = createWorkspaceInvitesRepository({
104
+ api: jsonRestApi,
105
+ knex: database.knex
106
+ });
107
+ const workspaceService = createWorkspaceService({
108
+ appConfig: config,
109
+ workspacesRepository,
110
+ workspaceMembershipsRepository,
111
+ workspaceSettingsRepository
112
+ });
113
+ const workspacePendingInvitationsService = createWorkspacePendingInvitationsService({
114
+ workspaceInvitesRepository,
115
+ workspaceMembershipsRepository
116
+ });
117
+ const workspaceMembersService = createWorkspaceMembersService({
118
+ workspaceMembershipsRepository,
119
+ workspaceInvitesRepository,
120
+ inviteExpiresInMs: requirePositiveInteger(
121
+ config?.workspaceMembers?.defaults?.inviteExpiresInMs,
122
+ "runtime.config.workspaceMembers.defaults.inviteExpiresInMs"
123
+ ),
124
+ roleCatalog,
125
+ workspaceInvitationsEnabled: invitationsEnabled,
126
+ inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: config, env }),
127
+ workspaceInviteMailer: config?.workspaceInviteMailer || null,
128
+ workspaceInviteEmailTemplate: resolveInviteEmailTemplate(config)
129
+ });
130
+ const workspaceSettingsService = createWorkspaceSettingsService({
131
+ workspaceSettingsRepository,
132
+ workspaceInvitationsEnabled: invitationsEnabled,
133
+ roleCatalog
134
+ });
135
+
136
+ return Object.freeze({
137
+ config,
138
+ enabled: tenancyProfile.workspace.enabled === true,
139
+ invitationsEnabled,
140
+ selfCreateEnabled: tenancyProfile.workspace.allowSelfCreate === true,
141
+ scope: Object.freeze({
142
+ params: workspaceSlugParamsValidator,
143
+ buildInputFromRouteParams: buildWorkspaceInputFromRouteParams,
144
+ resolveWorkspace
145
+ }),
146
+ tenancyProfile,
147
+ repositories: Object.freeze({
148
+ workspaceInvites: workspaceInvitesRepository,
149
+ workspaceMemberships: workspaceMembershipsRepository,
150
+ workspaceSettings: workspaceSettingsRepository,
151
+ workspaces: workspacesRepository
152
+ }),
153
+ services: Object.freeze({
154
+ directory: workspaceService,
155
+ members: workspaceMembersService,
156
+ pendingInvitations: workspacePendingInvitationsService,
157
+ settings: workspaceSettingsService
158
+ })
159
+ });
160
+ }
161
+
162
+ const WorkspacesFeature = defineFeature({
163
+ id: "workspaces.core",
164
+ domain: "workspace",
165
+ requires: {
166
+ bootstrap: "runtime.bootstrap",
167
+ config: "runtime.config",
168
+ database: "runtime.database",
169
+ env: "runtime.env",
170
+ http: "runtime.http",
171
+ jsonRestApi: "runtime.json-rest-api",
172
+ users: "users.core"
173
+ },
174
+ provides: {
175
+ workspaces: "workspaces.core"
176
+ },
177
+ async setup({ bootstrap, config, database, env, http, jsonRestApi, users }) {
178
+ await installWorkspaceResources(jsonRestApi);
179
+ const workspaces = createWorkspacesRuntime({ config, database, env, jsonRestApi });
180
+
181
+ bootstrap.register({
182
+ id: "workspaces.bootstrap",
183
+ order: 200,
184
+ contribute: createWorkspaceBootstrapContributor({
185
+ appConfig: config,
186
+ tenancyProfile: workspaces.tenancyProfile,
187
+ userProfilesRepository: users.repositories.userProfiles,
188
+ workspaceInvitationsEnabled: workspaces.invitationsEnabled,
189
+ workspacePendingInvitationsService: workspaces.services.pendingInvitations,
190
+ workspaceService: workspaces.services.directory
191
+ }).contribute
192
+ });
193
+
194
+ if (workspaces.enabled) {
195
+ registerWorkspaceDirectoryRoutes(http.router, {
196
+ config,
197
+ workspaceSelfCreateEnabled: workspaces.selfCreateEnabled
198
+ });
199
+ registerWorkspaceSettingsRoutes(http.router, { config });
200
+ registerWorkspaceMembersRoutes(http.router, {
201
+ config,
202
+ workspaceInvitationsEnabled: workspaces.invitationsEnabled
203
+ });
204
+ if (workspaces.invitationsEnabled) {
205
+ registerWorkspacePendingInvitationsRoutes(http.router);
206
+ }
207
+ }
208
+
209
+ return { workspaces };
210
+ },
211
+ actions({ workspaces }) {
212
+ if (!workspaces.enabled) return [];
213
+ return [
214
+ ...buildWorkspaceDirectoryActions({ workspaceService: workspaces.services.directory }),
215
+ ...buildWorkspaceSettingsActions({ workspaceSettingsService: workspaces.services.settings }),
216
+ ...buildWorkspaceMembersActions({ workspaceMembersService: workspaces.services.members }),
217
+ ...(workspaces.invitationsEnabled
218
+ ? buildWorkspacePendingInvitationsActions({
219
+ workspacePendingInvitationsService: workspaces.services.pendingInvitations
220
+ })
221
+ : [])
222
+ ];
223
+ }
224
+ });
225
+
226
+ export { WorkspacesFeature, createWorkspacesRuntime, installWorkspaceResources };
@@ -0,0 +1,74 @@
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
2
+ import { createWorkspaceActionContextContributor } from "./common/contributors/workspaceActionContextContributor.js";
3
+ import { createWorkspaceAuthPolicyContextResolver } from "./common/contributors/workspaceAuthPolicyContextResolver.js";
4
+ import { createWorkspaceRouteVisibilityResolver } from "./common/contributors/workspaceRouteVisibilityResolver.js";
5
+ import {
6
+ resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig,
7
+ resolveWorkspaceSurfaceIdsFromAppConfig
8
+ } from "./support/workspaceActionSurfaces.js";
9
+
10
+ const WorkspacesIntegrationsProvider = defineProvider({
11
+ id: "workspaces.integrations",
12
+ requires: {
13
+ actions: "runtime.actions",
14
+ authExtensions: "auth.extensions",
15
+ config: "runtime.config",
16
+ http: "runtime.http",
17
+ usersExtensions: "users.extensions",
18
+ workspaces: "workspaces.core"
19
+ },
20
+ setup({ actions, authExtensions, config, http, usersExtensions, workspaces }) {
21
+ const workspaceService = workspaces.services.directory;
22
+ const membershipOptionalSurfaceIds = resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(config);
23
+ const workspaceSurfaceIds = resolveWorkspaceSurfaceIdsFromAppConfig(config);
24
+
25
+ const actionContext = createWorkspaceActionContextContributor({
26
+ workspaceMembershipOptionalSurfaceIds: membershipOptionalSurfaceIds,
27
+ workspaceService,
28
+ workspaceSurfaceIds
29
+ });
30
+ actions.registerContextContributor({
31
+ id: actionContext.contributorId,
32
+ contribute: actionContext.contribute
33
+ });
34
+
35
+ const authPolicyResolver = createWorkspaceAuthPolicyContextResolver({
36
+ workspaceMembershipOptionalSurfaceIds: membershipOptionalSurfaceIds,
37
+ workspaceService
38
+ });
39
+ authExtensions.registerPolicyContextResolver({
40
+ resolverId: "workspaces.policy",
41
+ resolveAuthPolicyContext: authPolicyResolver
42
+ });
43
+
44
+ const visibility = createWorkspaceRouteVisibilityResolver({
45
+ workspaceMembershipOptionalSurfaceIds: membershipOptionalSurfaceIds,
46
+ workspaceService
47
+ });
48
+ http.registerVisibilityResolver({
49
+ id: visibility.resolverId,
50
+ resolve: visibility.resolve
51
+ });
52
+
53
+ usersExtensions.registerProfileSyncLifecycleContributor({
54
+ contributorId: "workspaces.provisioning",
55
+ order: 100,
56
+ async afterIdentityProfileSynced({ profile, options } = {}) {
57
+ await workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser(profile, options);
58
+ }
59
+ });
60
+
61
+ if (workspaces.invitationsEnabled) {
62
+ authExtensions.registerInvitationContextResolver({
63
+ resolverId: "workspaces.invitation",
64
+ resolveInvitationContext(invitation, options = {}) {
65
+ return workspaces.services.pendingInvitations.resolveInviteContextForAuth(invitation, options);
66
+ }
67
+ });
68
+ }
69
+
70
+ return {};
71
+ }
72
+ });
73
+
74
+ export { WorkspacesIntegrationsProvider };
@@ -45,21 +45,20 @@ function createWorkspaceActionContextContributor({
45
45
 
46
46
  return Object.freeze({
47
47
  contributorId,
48
- async contribute({ definition = null, input, context, request, surface = "" } = {}) {
48
+ async contribute({ definition = null, input, context } = {}) {
49
49
  const payload = normalizeObject(input);
50
50
  if (!Object.hasOwn(payload, "workspaceSlug")) {
51
51
  return {};
52
52
  }
53
53
 
54
+ const request = context?.requestMeta?.request || null;
54
55
  const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
55
56
  const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
56
57
  const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
57
- const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(surface || context?.surface);
58
+ const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(context?.surface);
58
59
  const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
59
60
  const routeVisibilityInput =
60
- request && request.routeOptions && request.routeOptions.config
61
- ? request.routeOptions.config.visibility
62
- : ROUTE_VISIBILITY_PUBLIC;
61
+ context?.routeVisibility ?? request?.routeOptions?.config?.visibility ?? ROUTE_VISIBILITY_PUBLIC;
63
62
  const routeVisibility = checkRouteVisibility(routeVisibilityInput);
64
63
  const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
65
64
  if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
@@ -67,10 +66,7 @@ function createWorkspaceActionContextContributor({
67
66
  }
68
67
 
69
68
  const resolveOptions = { request };
70
- if (
71
- !hasWorkspaceRouteVisibility &&
72
- workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)
73
- ) {
69
+ if (workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)) {
74
70
  resolveOptions.requireMembership = false;
75
71
  }
76
72
 
@@ -1,12 +1,22 @@
1
1
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
2
3
 
3
- function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
4
+ function createWorkspaceAuthPolicyContextResolver({
5
+ workspaceService,
6
+ workspaceMembershipOptionalSurfaceIds = []
7
+ } = {}) {
4
8
  if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
5
9
  throw new Error(
6
10
  "workspace auth policy context resolver requires workspaceService.resolveWorkspaceContextForUserBySlug()."
7
11
  );
8
12
  }
9
13
 
14
+ const membershipOptionalSurfaceIds = new Set(
15
+ (Array.isArray(workspaceMembershipOptionalSurfaceIds) ? workspaceMembershipOptionalSurfaceIds : [])
16
+ .map((entry) => normalizeSurfaceId(entry))
17
+ .filter(Boolean)
18
+ );
19
+
10
20
  return async function resolveWorkspaceAuthPolicyContext({ request, actor, meta } = {}) {
11
21
  const contextPolicy = normalizeText(meta?.contextPolicy || "none").toLowerCase() || "none";
12
22
  const permission = normalizeText(meta?.permission);
@@ -20,7 +30,11 @@ function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
20
30
  }
21
31
 
22
32
  const resolveOptions = { request };
23
- if (contextPolicy === "optional" && !permission) {
33
+ const activeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
34
+ if (
35
+ (contextPolicy === "optional" && !permission) ||
36
+ membershipOptionalSurfaceIds.has(activeSurfaceId)
37
+ ) {
24
38
  resolveOptions.requireMembership = false;
25
39
  }
26
40
 
@@ -1,4 +1,5 @@
1
1
  import { normalizeOpaqueId, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
2
3
 
3
4
  function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId = null } = {}) {
4
5
  const requiresActorScope = visibility === "workspace_user";
@@ -17,11 +18,20 @@ function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId =
17
18
  return contribution;
18
19
  }
19
20
 
20
- function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
21
+ function createWorkspaceRouteVisibilityResolver({
22
+ workspaceService,
23
+ workspaceMembershipOptionalSurfaceIds = []
24
+ } = {}) {
21
25
  if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
22
26
  throw new Error("workspace route visibility resolver requires workspaceService.resolveWorkspaceContextForUserBySlug().");
23
27
  }
24
28
 
29
+ const membershipOptionalSurfaceIds = new Set(
30
+ (Array.isArray(workspaceMembershipOptionalSurfaceIds) ? workspaceMembershipOptionalSurfaceIds : [])
31
+ .map((entry) => normalizeSurfaceId(entry))
32
+ .filter(Boolean)
33
+ );
34
+
25
35
  return Object.freeze({
26
36
  resolverId: "workspaces.visibility",
27
37
  async resolve({ visibility, context, request, input } = {}) {
@@ -46,9 +56,17 @@ function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
46
56
  : {};
47
57
  }
48
58
 
49
- const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
50
- request
51
- });
59
+ const activeSurfaceId = normalizeSurfaceId(
60
+ request?.routeOptions?.config?.surface || context?.surface
61
+ );
62
+ const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
63
+ actor,
64
+ workspaceSlug,
65
+ {
66
+ request,
67
+ ...(membershipOptionalSurfaceIds.has(activeSurfaceId) ? { requireMembership: false } : {})
68
+ }
69
+ );
52
70
  const resolvedWorkspaceOwnerId = normalizeRecordId(resolvedWorkspaceContext?.workspace?.id, { fallback: null });
53
71
  if (!resolvedWorkspaceOwnerId) {
54
72
  return visibility === "workspace_user"