@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
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.138",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
|
-
"./server/
|
|
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,14 @@
|
|
|
17
18
|
"./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@jskit-ai/auth-core": "0.1.
|
|
21
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
22
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
23
|
-
"@jskit-ai/json-rest-api-core": "0.1.
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
25
|
-
"@jskit-ai/resource-core": "0.1.
|
|
26
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
27
|
-
"@jskit-ai/users-core": "0.1.
|
|
21
|
+
"@jskit-ai/auth-core": "0.1.158",
|
|
22
|
+
"@jskit-ai/database-runtime": "0.1.160",
|
|
23
|
+
"@jskit-ai/http-runtime": "0.1.158",
|
|
24
|
+
"@jskit-ai/json-rest-api-core": "0.1.104",
|
|
25
|
+
"@jskit-ai/kernel": "0.1.160",
|
|
26
|
+
"@jskit-ai/resource-core": "0.1.102",
|
|
27
|
+
"@jskit-ai/resource-crud-core": "0.1.102",
|
|
28
|
+
"@jskit-ai/users-core": "0.1.173",
|
|
28
29
|
"json-rest-schema": "^1.0.17"
|
|
29
30
|
},
|
|
30
31
|
"description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
|
|
@@ -32,20 +33,31 @@
|
|
|
32
33
|
"kind": "runtime",
|
|
33
34
|
"capabilities": {
|
|
34
35
|
"provides": [
|
|
35
|
-
"workspaces.core"
|
|
36
|
-
"workspaces.server-routes"
|
|
36
|
+
"workspaces.core"
|
|
37
37
|
],
|
|
38
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",
|
|
39
47
|
"users.core",
|
|
40
|
-
"
|
|
48
|
+
"users.extensions"
|
|
41
49
|
]
|
|
42
50
|
},
|
|
43
51
|
"runtime": {
|
|
44
52
|
"server": {
|
|
45
53
|
"providers": [
|
|
46
54
|
{
|
|
47
|
-
"entrypoint": "src/server/
|
|
48
|
-
"export": "
|
|
55
|
+
"entrypoint": "src/server/WorkspacesFeature.js",
|
|
56
|
+
"export": "WorkspacesFeature"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"entrypoint": "src/server/WorkspacesIntegrationsProvider.js",
|
|
60
|
+
"export": "WorkspacesIntegrationsProvider"
|
|
49
61
|
}
|
|
50
62
|
]
|
|
51
63
|
},
|
|
@@ -58,24 +70,16 @@
|
|
|
58
70
|
"tableOwnership": {
|
|
59
71
|
"tables": [
|
|
60
72
|
{
|
|
61
|
-
"tableName": "workspaces"
|
|
62
|
-
"provenance": "runtime-package",
|
|
63
|
-
"ownerKind": "package-runtime"
|
|
73
|
+
"tableName": "workspaces"
|
|
64
74
|
},
|
|
65
75
|
{
|
|
66
|
-
"tableName": "workspace_memberships"
|
|
67
|
-
"provenance": "runtime-package",
|
|
68
|
-
"ownerKind": "package-runtime"
|
|
76
|
+
"tableName": "workspace_memberships"
|
|
69
77
|
},
|
|
70
78
|
{
|
|
71
|
-
"tableName": "workspace_settings"
|
|
72
|
-
"provenance": "runtime-package",
|
|
73
|
-
"ownerKind": "package-runtime"
|
|
79
|
+
"tableName": "workspace_settings"
|
|
74
80
|
},
|
|
75
81
|
{
|
|
76
|
-
"tableName": "workspace_invites"
|
|
77
|
-
"provenance": "runtime-package",
|
|
78
|
-
"ownerKind": "package-runtime"
|
|
82
|
+
"tableName": "workspace_invites"
|
|
79
83
|
}
|
|
80
84
|
]
|
|
81
85
|
}
|
|
@@ -84,15 +88,12 @@
|
|
|
84
88
|
"surfaces": [
|
|
85
89
|
{
|
|
86
90
|
"subpath": "./server",
|
|
87
|
-
"summary": "Exports the workspace
|
|
91
|
+
"summary": "Exports the explicit workspace feature, integrations, services, repositories, actions, and routes."
|
|
88
92
|
}
|
|
89
93
|
],
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
],
|
|
94
|
-
"client": []
|
|
95
|
-
}
|
|
94
|
+
"capabilities": [
|
|
95
|
+
"workspaces.core"
|
|
96
|
+
]
|
|
96
97
|
},
|
|
97
98
|
"server": {
|
|
98
99
|
"routes": [
|
|
@@ -164,210 +165,9 @@
|
|
|
164
165
|
]
|
|
165
166
|
}
|
|
166
167
|
},
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"@jskit-ai/json-rest-api-core": "0.1.103",
|
|
171
|
-
"@jskit-ai/resource-core": "0.1.101",
|
|
172
|
-
"@jskit-ai/resource-crud-core": "0.1.101",
|
|
173
|
-
"@jskit-ai/users-core": "0.1.172"
|
|
174
|
-
},
|
|
175
|
-
"dev": {}
|
|
176
|
-
},
|
|
177
|
-
"packageJson": {
|
|
178
|
-
"scripts": {
|
|
179
|
-
"server:app": "SERVER_SURFACE=app node ./bin/server.js",
|
|
180
|
-
"server:admin": "SERVER_SURFACE=admin node ./bin/server.js"
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
"procfile": {},
|
|
184
|
-
"files": [
|
|
185
|
-
{
|
|
186
|
-
"op": "install-migration",
|
|
187
|
-
"from": "templates/migrations/workspaces_core_initial.cjs",
|
|
188
|
-
"toDir": "migrations",
|
|
189
|
-
"extension": ".cjs",
|
|
190
|
-
"reason": "Install workspace tenancy schema migration.",
|
|
191
|
-
"category": "migration",
|
|
192
|
-
"id": "workspaces-core-initial-schema"
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
"op": "install-migration",
|
|
196
|
-
"from": "templates/migrations/workspaces_core_workspace_settings_single_name_source.cjs",
|
|
197
|
-
"toDir": "migrations",
|
|
198
|
-
"extension": ".cjs",
|
|
199
|
-
"reason": "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
|
|
200
|
-
"category": "migration",
|
|
201
|
-
"id": "users-core-workspace-settings-single-name-source"
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
"op": "install-migration",
|
|
205
|
-
"from": "templates/migrations/workspaces_core_workspaces_drop_color.cjs",
|
|
206
|
-
"toDir": "migrations",
|
|
207
|
-
"extension": ".cjs",
|
|
208
|
-
"reason": "Drop workspaces.color now that workspace theme colors live in workspace_settings.",
|
|
209
|
-
"category": "migration",
|
|
210
|
-
"id": "users-core-workspaces-drop-color"
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
"from": "templates/config/roles.js",
|
|
214
|
-
"to": "config/roles.js",
|
|
215
|
-
"ownership": "app",
|
|
216
|
-
"preserveOnRemove": true,
|
|
217
|
-
"reason": "Install app-owned role catalog in a dedicated config file.",
|
|
218
|
-
"category": "workspaces-core",
|
|
219
|
-
"id": "users-core-app-owned-role-catalog-config"
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
"from": "templates/packages/main/src/server/email/workspaceInviteEmail.js",
|
|
223
|
-
"to": "packages/main/src/server/email/workspaceInviteEmail.js",
|
|
224
|
-
"ownership": "app",
|
|
225
|
-
"preserveOnRemove": true,
|
|
226
|
-
"reason": "Install app-owned editable workspace invite email template.",
|
|
227
|
-
"category": "workspaces-core",
|
|
228
|
-
"id": "workspaces-core-main-workspace-invite-email-template"
|
|
229
|
-
}
|
|
230
|
-
],
|
|
231
|
-
"source": [
|
|
232
|
-
{
|
|
233
|
-
"op": "ensure-import",
|
|
234
|
-
"file": "config/server.js",
|
|
235
|
-
"namedImports": [
|
|
236
|
-
"renderWorkspaceInviteEmail"
|
|
237
|
-
],
|
|
238
|
-
"from": "../packages/main/src/server/email/workspaceInviteEmail.js",
|
|
239
|
-
"reason": "Load app-owned workspace invite email renderer from packages/main.",
|
|
240
|
-
"category": "workspaces-core",
|
|
241
|
-
"id": "workspaces-core-server-config-workspace-invite-email-import"
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
"op": "ensure-assignment",
|
|
245
|
-
"file": "config/server.js",
|
|
246
|
-
"target": "config.workspaceInviteEmailTemplate",
|
|
247
|
-
"value": "renderWorkspaceInviteEmail",
|
|
248
|
-
"reason": "Bind app-owned workspace invite email renderer into server config.",
|
|
249
|
-
"category": "workspaces-core",
|
|
250
|
-
"id": "workspaces-core-server-config-workspace-invite-email-template"
|
|
251
|
-
}
|
|
252
|
-
],
|
|
253
|
-
"text": [
|
|
254
|
-
{
|
|
255
|
-
"op": "append-text",
|
|
256
|
-
"file": "config/public.js",
|
|
257
|
-
"position": "top",
|
|
258
|
-
"skipIfContains": "import { roleCatalog } from \"./roles.js\";",
|
|
259
|
-
"value": "import { roleCatalog } from \"./roles.js\";\n",
|
|
260
|
-
"reason": "Load app-owned role catalog from dedicated config file.",
|
|
261
|
-
"category": "workspaces-core",
|
|
262
|
-
"id": "users-core-role-catalog-public-import"
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
"op": "append-text",
|
|
266
|
-
"file": "config/public.js",
|
|
267
|
-
"position": "top",
|
|
268
|
-
"skipIfContains": "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";",
|
|
269
|
-
"value": "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";\n",
|
|
270
|
-
"reason": "Load app-owned surface access policy catalog from dedicated config file.",
|
|
271
|
-
"category": "workspaces-core",
|
|
272
|
-
"id": "users-core-surface-access-policies-public-import"
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
"op": "append-text",
|
|
276
|
-
"file": "config/surfaceAccessPolicies.js",
|
|
277
|
-
"position": "top",
|
|
278
|
-
"skipIfContains": "export const surfaceAccessPolicies = {};",
|
|
279
|
-
"value": "export const surfaceAccessPolicies = {};\n\n",
|
|
280
|
-
"reason": "Initialize app-owned surface access policy config if missing.",
|
|
281
|
-
"category": "workspaces-core",
|
|
282
|
-
"id": "users-core-surface-access-policies-config-init"
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
"op": "append-text",
|
|
286
|
-
"file": "config/surfaceAccessPolicies.js",
|
|
287
|
-
"position": "bottom",
|
|
288
|
-
"skipIfContains": "surfaceAccessPolicies.workspace_member = {",
|
|
289
|
-
"value": "\nsurfaceAccessPolicies.workspace_member = {\n requireAuth: true,\n requireWorkspaceMembership: true\n};\n",
|
|
290
|
-
"reason": "Register workspace-member surface access policy for workspace surfaces.",
|
|
291
|
-
"category": "workspaces-core",
|
|
292
|
-
"id": "users-core-surface-access-policies-workspace-member"
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
"op": "append-text",
|
|
296
|
-
"file": "config/public.js",
|
|
297
|
-
"position": "bottom",
|
|
298
|
-
"skipIfContains": "config.surfaceDefinitions.app = {",
|
|
299
|
-
"value": "\nconfig.surfaceDefinitions.app = {\n id: \"app\",\n label: \"App\",\n pagesRoot: \"w/[workspaceSlug]\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n\nconfig.surfaceDefinitions.admin = {\n id: \"admin\",\n label: \"Admin\",\n pagesRoot: \"w/[workspaceSlug]/admin\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n",
|
|
300
|
-
"reason": "Append workspace surface topology when tenancy enables workspace routing.",
|
|
301
|
-
"category": "workspaces-core",
|
|
302
|
-
"id": "users-core-surface-config-workspace",
|
|
303
|
-
"when": {
|
|
304
|
-
"config": "tenancyMode",
|
|
305
|
-
"in": [
|
|
306
|
-
"personal",
|
|
307
|
-
"workspaces"
|
|
308
|
-
]
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
"op": "append-text",
|
|
313
|
-
"file": "config/public.js",
|
|
314
|
-
"position": "bottom",
|
|
315
|
-
"skipIfContains": "config.workspaceSwitching =",
|
|
316
|
-
"value": "\nconfig.workspaceSwitching = true;\nconfig.workspaceInvitations = {\n enabled: true,\n allowInPersonalMode: true\n};\n",
|
|
317
|
-
"reason": "Append default workspace feature toggles into app-owned config.",
|
|
318
|
-
"category": "workspaces-core",
|
|
319
|
-
"id": "users-core-public-config"
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
"op": "append-text",
|
|
323
|
-
"file": "config/public.js",
|
|
324
|
-
"position": "bottom",
|
|
325
|
-
"skipIfContains": "config.roleCatalog = roleCatalog;",
|
|
326
|
-
"value": "\nconfig.roleCatalog = roleCatalog;\n",
|
|
327
|
-
"reason": "Bind app-owned role catalog onto public config.",
|
|
328
|
-
"category": "workspaces-core",
|
|
329
|
-
"id": "users-core-role-catalog-public-config"
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
"op": "append-text",
|
|
333
|
-
"file": "config/public.js",
|
|
334
|
-
"position": "bottom",
|
|
335
|
-
"skipIfContains": "config.surfaceAccessPolicies = surfaceAccessPolicies;",
|
|
336
|
-
"value": "\nconfig.surfaceAccessPolicies = surfaceAccessPolicies;\n",
|
|
337
|
-
"reason": "Bind app-owned surface access policies onto public config.",
|
|
338
|
-
"category": "workspaces-core",
|
|
339
|
-
"id": "users-core-surface-access-policies-public-config"
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
"op": "append-text",
|
|
343
|
-
"file": "config/server.js",
|
|
344
|
-
"position": "bottom",
|
|
345
|
-
"skipIfContains": "config.workspaceColor =",
|
|
346
|
-
"value": "\nconfig.workspaceColor = \"#1867C0\";\n",
|
|
347
|
-
"reason": "Append default workspace server settings into app-owned config.",
|
|
348
|
-
"category": "workspaces-core",
|
|
349
|
-
"id": "users-core-server-config"
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
"op": "append-text",
|
|
353
|
-
"file": "config/server.js",
|
|
354
|
-
"position": "bottom",
|
|
355
|
-
"skipIfContains": "config.workspaceSettings =",
|
|
356
|
-
"value": "\nconfig.workspaceSettings = {\n defaults: {\n invitesEnabled: true\n }\n};\n",
|
|
357
|
-
"reason": "Append app-owned workspace settings defaults into the server config.",
|
|
358
|
-
"category": "workspaces-core",
|
|
359
|
-
"id": "users-core-workspace-settings-server-config"
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
"op": "append-text",
|
|
363
|
-
"file": "config/server.js",
|
|
364
|
-
"position": "bottom",
|
|
365
|
-
"skipIfContains": "config.workspaceMembers =",
|
|
366
|
-
"value": "\nconfig.workspaceMembers = {\n defaults: {\n inviteExpiresInMs: 604800000\n }\n};\n",
|
|
367
|
-
"reason": "Append app-owned workspace member invite policy defaults into the server config.",
|
|
368
|
-
"category": "workspaces-core",
|
|
369
|
-
"id": "users-core-workspace-members-server-config"
|
|
370
|
-
}
|
|
168
|
+
"migrations": {
|
|
169
|
+
"directories": [
|
|
170
|
+
"migrations"
|
|
371
171
|
]
|
|
372
172
|
}
|
|
373
173
|
}
|
|
@@ -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 };
|