@jskit-ai/workspaces-core 0.1.136 → 0.1.138-oldline.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +352 -0
- package/package.json +9 -353
- package/src/server/WorkspacesCoreServiceProvider.js +1 -1
- package/src/server/common/contributors/workspaceActionContextContributor.js +19 -3
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +10 -3
- package/src/server/common/services/workspaceContextService.js +7 -2
- package/src/server/registerWorkspaceCore.js +2 -0
- package/src/server/support/workspaceActionSurfaces.js +17 -0
- package/test/{packageMetadata.test.js → packageDescriptor.test.js} +5 -7
- package/test/usersRouteResources.test.js +1 -1
- package/test/workspaceActionContextContributor.test.js +46 -0
- package/test/workspaceActionSurfaces.test.js +36 -1
- package/test/workspaceAuthPolicyContextResolver.test.js +31 -0
- package/test/workspaceService.test.js +64 -2
- package/test/workspaceSettingsActions.test.js +1 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@jskit-ai/workspaces-core",
|
|
4
|
+
version: "0.1.138-oldline.1",
|
|
5
|
+
kind: "runtime",
|
|
6
|
+
description: "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
|
|
7
|
+
dependsOn: [
|
|
8
|
+
"@jskit-ai/json-rest-api-core",
|
|
9
|
+
"@jskit-ai/resource-core",
|
|
10
|
+
"@jskit-ai/resource-crud-core",
|
|
11
|
+
"@jskit-ai/users-core"
|
|
12
|
+
],
|
|
13
|
+
capabilities: {
|
|
14
|
+
provides: [
|
|
15
|
+
"workspaces.core",
|
|
16
|
+
"workspaces.server-routes"
|
|
17
|
+
],
|
|
18
|
+
requires: [
|
|
19
|
+
"users.core",
|
|
20
|
+
"json-rest-api.core"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
runtime: {
|
|
24
|
+
server: {
|
|
25
|
+
providers: [
|
|
26
|
+
{
|
|
27
|
+
entrypoint: "src/server/WorkspacesCoreServiceProvider.js",
|
|
28
|
+
export: "WorkspacesCoreServiceProvider"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
client: {
|
|
33
|
+
providers: []
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
metadata: {
|
|
37
|
+
jskit: {
|
|
38
|
+
tableOwnership: {
|
|
39
|
+
tables: [
|
|
40
|
+
{
|
|
41
|
+
tableName: "workspaces",
|
|
42
|
+
provenance: "runtime-package",
|
|
43
|
+
ownerKind: "package-runtime"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
tableName: "workspace_memberships",
|
|
47
|
+
provenance: "runtime-package",
|
|
48
|
+
ownerKind: "package-runtime"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
tableName: "workspace_settings",
|
|
52
|
+
provenance: "runtime-package",
|
|
53
|
+
ownerKind: "package-runtime"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
tableName: "workspace_invites",
|
|
57
|
+
provenance: "runtime-package",
|
|
58
|
+
ownerKind: "package-runtime"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
apiSummary: {
|
|
64
|
+
surfaces: [
|
|
65
|
+
{
|
|
66
|
+
subpath: "./server",
|
|
67
|
+
summary: "Exports the workspace runtime provider and workspace route registration surface."
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
containerTokens: {
|
|
71
|
+
server: [
|
|
72
|
+
"workspaces.server.scope-support"
|
|
73
|
+
],
|
|
74
|
+
client: []
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
server: {
|
|
78
|
+
routes: [
|
|
79
|
+
{
|
|
80
|
+
method: "POST",
|
|
81
|
+
path: "/api/workspaces",
|
|
82
|
+
summary: "Create a workspace for the authenticated user."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
method: "GET",
|
|
86
|
+
path: "/api/workspaces",
|
|
87
|
+
summary: "List workspaces visible to authenticated user."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
method: "GET",
|
|
91
|
+
path: "/api/workspace/invitations/resolve",
|
|
92
|
+
summary: "Resolve safe public workspace invitation metadata."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
method: "GET",
|
|
96
|
+
path: "/api/workspace/invitations/pending",
|
|
97
|
+
summary: "List pending workspace invitations for authenticated user."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
method: "POST",
|
|
101
|
+
path: "/api/workspace/invitations/redeem",
|
|
102
|
+
summary: "Accept or refuse a workspace invitation using an invite token."
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
method: "GET",
|
|
106
|
+
path: "/api/w/:workspaceSlug/settings",
|
|
107
|
+
summary: "Get workspace settings and role catalog by workspace slug."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
method: "PATCH",
|
|
111
|
+
path: "/api/w/:workspaceSlug/settings",
|
|
112
|
+
summary: "Update workspace settings by workspace slug."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
method: "GET",
|
|
116
|
+
path: "/api/w/:workspaceSlug/roles",
|
|
117
|
+
summary: "Get role catalog by workspace slug."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
method: "GET",
|
|
121
|
+
path: "/api/w/:workspaceSlug/members",
|
|
122
|
+
summary: "List members by workspace slug."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
method: "PATCH",
|
|
126
|
+
path: "/api/w/:workspaceSlug/members/:memberUserId/role",
|
|
127
|
+
summary: "Update workspace member role by workspace slug."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
method: "GET",
|
|
131
|
+
path: "/api/w/:workspaceSlug/invites",
|
|
132
|
+
summary: "List workspace invites by workspace slug."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
method: "POST",
|
|
136
|
+
path: "/api/w/:workspaceSlug/invites",
|
|
137
|
+
summary: "Create workspace invite by workspace slug."
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
method: "DELETE",
|
|
141
|
+
path: "/api/w/:workspaceSlug/invites/:inviteId",
|
|
142
|
+
summary: "Revoke workspace invite by workspace slug."
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
mutations: {
|
|
148
|
+
dependencies: {
|
|
149
|
+
runtime: {
|
|
150
|
+
"@jskit-ai/json-rest-api-core": "0.1.92",
|
|
151
|
+
"@jskit-ai/resource-core": "0.1.90",
|
|
152
|
+
"@jskit-ai/resource-crud-core": "0.1.90",
|
|
153
|
+
"@jskit-ai/users-core": "0.1.161"
|
|
154
|
+
},
|
|
155
|
+
dev: {}
|
|
156
|
+
},
|
|
157
|
+
packageJson: {
|
|
158
|
+
scripts: {
|
|
159
|
+
"server:app": "SERVER_SURFACE=app node ./bin/server.js",
|
|
160
|
+
"server:admin": "SERVER_SURFACE=admin node ./bin/server.js"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
procfile: {},
|
|
164
|
+
files: [
|
|
165
|
+
{
|
|
166
|
+
op: "install-migration",
|
|
167
|
+
from: "templates/migrations/workspaces_core_initial.cjs",
|
|
168
|
+
toDir: "migrations",
|
|
169
|
+
extension: ".cjs",
|
|
170
|
+
reason: "Install workspace tenancy schema migration.",
|
|
171
|
+
category: "migration",
|
|
172
|
+
id: "workspaces-core-initial-schema"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
op: "install-migration",
|
|
176
|
+
from: "templates/migrations/workspaces_core_workspace_settings_single_name_source.cjs",
|
|
177
|
+
toDir: "migrations",
|
|
178
|
+
extension: ".cjs",
|
|
179
|
+
reason: "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
|
|
180
|
+
category: "migration",
|
|
181
|
+
id: "users-core-workspace-settings-single-name-source"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
op: "install-migration",
|
|
185
|
+
from: "templates/migrations/workspaces_core_workspaces_drop_color.cjs",
|
|
186
|
+
toDir: "migrations",
|
|
187
|
+
extension: ".cjs",
|
|
188
|
+
reason: "Drop workspaces.color now that workspace theme colors live in workspace_settings.",
|
|
189
|
+
category: "migration",
|
|
190
|
+
id: "users-core-workspaces-drop-color"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
from: "templates/config/roles.js",
|
|
194
|
+
to: "config/roles.js",
|
|
195
|
+
ownership: "app",
|
|
196
|
+
preserveOnRemove: true,
|
|
197
|
+
reason: "Install app-owned role catalog in a dedicated config file.",
|
|
198
|
+
category: "workspaces-core",
|
|
199
|
+
id: "users-core-app-owned-role-catalog-config"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
from: "templates/packages/main/src/server/email/workspaceInviteEmail.js",
|
|
203
|
+
to: "packages/main/src/server/email/workspaceInviteEmail.js",
|
|
204
|
+
ownership: "app",
|
|
205
|
+
preserveOnRemove: true,
|
|
206
|
+
reason: "Install app-owned editable workspace invite email template.",
|
|
207
|
+
category: "workspaces-core",
|
|
208
|
+
id: "workspaces-core-main-workspace-invite-email-template"
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
source: [
|
|
212
|
+
{
|
|
213
|
+
op: "ensure-import",
|
|
214
|
+
file: "config/server.js",
|
|
215
|
+
namedImports: ["renderWorkspaceInviteEmail"],
|
|
216
|
+
from: "../packages/main/src/server/email/workspaceInviteEmail.js",
|
|
217
|
+
reason: "Load app-owned workspace invite email renderer from packages/main.",
|
|
218
|
+
category: "workspaces-core",
|
|
219
|
+
id: "workspaces-core-server-config-workspace-invite-email-import"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
op: "ensure-assignment",
|
|
223
|
+
file: "config/server.js",
|
|
224
|
+
target: "config.workspaceInviteEmailTemplate",
|
|
225
|
+
value: "renderWorkspaceInviteEmail",
|
|
226
|
+
reason: "Bind app-owned workspace invite email renderer into server config.",
|
|
227
|
+
category: "workspaces-core",
|
|
228
|
+
id: "workspaces-core-server-config-workspace-invite-email-template"
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
text: [
|
|
232
|
+
{
|
|
233
|
+
op: "append-text",
|
|
234
|
+
file: "config/public.js",
|
|
235
|
+
position: "top",
|
|
236
|
+
skipIfContains: "import { roleCatalog } from \"./roles.js\";",
|
|
237
|
+
value: "import { roleCatalog } from \"./roles.js\";\n",
|
|
238
|
+
reason: "Load app-owned role catalog from dedicated config file.",
|
|
239
|
+
category: "workspaces-core",
|
|
240
|
+
id: "users-core-role-catalog-public-import"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
op: "append-text",
|
|
244
|
+
file: "config/public.js",
|
|
245
|
+
position: "top",
|
|
246
|
+
skipIfContains: "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";",
|
|
247
|
+
value: "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";\n",
|
|
248
|
+
reason: "Load app-owned surface access policy catalog from dedicated config file.",
|
|
249
|
+
category: "workspaces-core",
|
|
250
|
+
id: "users-core-surface-access-policies-public-import"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
op: "append-text",
|
|
254
|
+
file: "config/surfaceAccessPolicies.js",
|
|
255
|
+
position: "top",
|
|
256
|
+
skipIfContains: "export const surfaceAccessPolicies = {};",
|
|
257
|
+
value: "export const surfaceAccessPolicies = {};\n\n",
|
|
258
|
+
reason: "Initialize app-owned surface access policy config if missing.",
|
|
259
|
+
category: "workspaces-core",
|
|
260
|
+
id: "users-core-surface-access-policies-config-init"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
op: "append-text",
|
|
264
|
+
file: "config/surfaceAccessPolicies.js",
|
|
265
|
+
position: "bottom",
|
|
266
|
+
skipIfContains: "surfaceAccessPolicies.workspace_member = {",
|
|
267
|
+
value: "\nsurfaceAccessPolicies.workspace_member = {\n requireAuth: true,\n requireWorkspaceMembership: true\n};\n",
|
|
268
|
+
reason: "Register workspace-member surface access policy for workspace surfaces.",
|
|
269
|
+
category: "workspaces-core",
|
|
270
|
+
id: "users-core-surface-access-policies-workspace-member"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
op: "append-text",
|
|
274
|
+
file: "config/public.js",
|
|
275
|
+
position: "bottom",
|
|
276
|
+
skipIfContains: "config.surfaceDefinitions.app = {",
|
|
277
|
+
value:
|
|
278
|
+
"\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",
|
|
279
|
+
reason: "Append workspace surface topology when tenancy enables workspace routing.",
|
|
280
|
+
category: "workspaces-core",
|
|
281
|
+
id: "users-core-surface-config-workspace",
|
|
282
|
+
when: {
|
|
283
|
+
config: "tenancyMode",
|
|
284
|
+
in: ["personal", "workspaces"]
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
op: "append-text",
|
|
289
|
+
file: "config/public.js",
|
|
290
|
+
position: "bottom",
|
|
291
|
+
skipIfContains: "config.workspaceSwitching =",
|
|
292
|
+
value:
|
|
293
|
+
"\nconfig.workspaceSwitching = true;\nconfig.workspaceInvitations = {\n enabled: true,\n allowInPersonalMode: true\n};\n",
|
|
294
|
+
reason: "Append default workspace feature toggles into app-owned config.",
|
|
295
|
+
category: "workspaces-core",
|
|
296
|
+
id: "users-core-public-config"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
op: "append-text",
|
|
300
|
+
file: "config/public.js",
|
|
301
|
+
position: "bottom",
|
|
302
|
+
skipIfContains: "config.roleCatalog = roleCatalog;",
|
|
303
|
+
value: "\nconfig.roleCatalog = roleCatalog;\n",
|
|
304
|
+
reason: "Bind app-owned role catalog onto public config.",
|
|
305
|
+
category: "workspaces-core",
|
|
306
|
+
id: "users-core-role-catalog-public-config"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
op: "append-text",
|
|
310
|
+
file: "config/public.js",
|
|
311
|
+
position: "bottom",
|
|
312
|
+
skipIfContains: "config.surfaceAccessPolicies = surfaceAccessPolicies;",
|
|
313
|
+
value: "\nconfig.surfaceAccessPolicies = surfaceAccessPolicies;\n",
|
|
314
|
+
reason: "Bind app-owned surface access policies onto public config.",
|
|
315
|
+
category: "workspaces-core",
|
|
316
|
+
id: "users-core-surface-access-policies-public-config"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
op: "append-text",
|
|
320
|
+
file: "config/server.js",
|
|
321
|
+
position: "bottom",
|
|
322
|
+
skipIfContains: "config.workspaceColor =",
|
|
323
|
+
value: "\nconfig.workspaceColor = \"#1867C0\";\n",
|
|
324
|
+
reason: "Append default workspace server settings into app-owned config.",
|
|
325
|
+
category: "workspaces-core",
|
|
326
|
+
id: "users-core-server-config"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
op: "append-text",
|
|
330
|
+
file: "config/server.js",
|
|
331
|
+
position: "bottom",
|
|
332
|
+
skipIfContains: "config.workspaceSettings =",
|
|
333
|
+
value:
|
|
334
|
+
"\nconfig.workspaceSettings = {\n defaults: {\n invitesEnabled: true\n }\n};\n",
|
|
335
|
+
reason: "Append app-owned workspace settings defaults into the server config.",
|
|
336
|
+
category: "workspaces-core",
|
|
337
|
+
id: "users-core-workspace-settings-server-config"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
op: "append-text",
|
|
341
|
+
file: "config/server.js",
|
|
342
|
+
position: "bottom",
|
|
343
|
+
skipIfContains: "config.workspaceMembers =",
|
|
344
|
+
value:
|
|
345
|
+
"\nconfig.workspaceMembers = {\n defaults: {\n inviteExpiresInMs: 604800000\n }\n};\n",
|
|
346
|
+
reason: "Append app-owned workspace member invite policy defaults into the server config.",
|
|
347
|
+
category: "workspaces-core",
|
|
348
|
+
id: "users-core-workspace-members-server-config"
|
|
349
|
+
}
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.138-oldline.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -17,358 +17,14 @@
|
|
|
17
17
|
"./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
|
|
18
18
|
},
|
|
19
19
|
"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-
|
|
27
|
-
"@jskit-ai/users-core": "0.1.
|
|
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",
|
|
28
28
|
"json-rest-schema": "^1.0.17"
|
|
29
|
-
},
|
|
30
|
-
"description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
|
|
31
|
-
"jskit": {
|
|
32
|
-
"kind": "runtime",
|
|
33
|
-
"capabilities": {
|
|
34
|
-
"provides": [
|
|
35
|
-
"workspaces.core",
|
|
36
|
-
"workspaces.server-routes"
|
|
37
|
-
],
|
|
38
|
-
"requires": [
|
|
39
|
-
"users.core",
|
|
40
|
-
"json-rest-api.core"
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
"runtime": {
|
|
44
|
-
"server": {
|
|
45
|
-
"providers": [
|
|
46
|
-
{
|
|
47
|
-
"entrypoint": "src/server/WorkspacesCoreServiceProvider.js",
|
|
48
|
-
"export": "WorkspacesCoreServiceProvider"
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
},
|
|
52
|
-
"client": {
|
|
53
|
-
"providers": []
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"metadata": {
|
|
57
|
-
"jskit": {
|
|
58
|
-
"tableOwnership": {
|
|
59
|
-
"tables": [
|
|
60
|
-
{
|
|
61
|
-
"tableName": "workspaces",
|
|
62
|
-
"provenance": "runtime-package",
|
|
63
|
-
"ownerKind": "package-runtime"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"tableName": "workspace_memberships",
|
|
67
|
-
"provenance": "runtime-package",
|
|
68
|
-
"ownerKind": "package-runtime"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"tableName": "workspace_settings",
|
|
72
|
-
"provenance": "runtime-package",
|
|
73
|
-
"ownerKind": "package-runtime"
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"tableName": "workspace_invites",
|
|
77
|
-
"provenance": "runtime-package",
|
|
78
|
-
"ownerKind": "package-runtime"
|
|
79
|
-
}
|
|
80
|
-
]
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
"apiSummary": {
|
|
84
|
-
"surfaces": [
|
|
85
|
-
{
|
|
86
|
-
"subpath": "./server",
|
|
87
|
-
"summary": "Exports the workspace runtime provider and workspace route registration surface."
|
|
88
|
-
}
|
|
89
|
-
],
|
|
90
|
-
"containerTokens": {
|
|
91
|
-
"server": [
|
|
92
|
-
"workspaces.server.scope-support"
|
|
93
|
-
],
|
|
94
|
-
"client": []
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
"server": {
|
|
98
|
-
"routes": [
|
|
99
|
-
{
|
|
100
|
-
"method": "POST",
|
|
101
|
-
"path": "/api/workspaces",
|
|
102
|
-
"summary": "Create a workspace for the authenticated user."
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"method": "GET",
|
|
106
|
-
"path": "/api/workspaces",
|
|
107
|
-
"summary": "List workspaces visible to authenticated user."
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"method": "GET",
|
|
111
|
-
"path": "/api/workspace/invitations/resolve",
|
|
112
|
-
"summary": "Resolve safe public workspace invitation metadata."
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
"method": "GET",
|
|
116
|
-
"path": "/api/workspace/invitations/pending",
|
|
117
|
-
"summary": "List pending workspace invitations for authenticated user."
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"method": "POST",
|
|
121
|
-
"path": "/api/workspace/invitations/redeem",
|
|
122
|
-
"summary": "Accept or refuse a workspace invitation using an invite token."
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
"method": "GET",
|
|
126
|
-
"path": "/api/w/:workspaceSlug/settings",
|
|
127
|
-
"summary": "Get workspace settings and role catalog by workspace slug."
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"method": "PATCH",
|
|
131
|
-
"path": "/api/w/:workspaceSlug/settings",
|
|
132
|
-
"summary": "Update workspace settings by workspace slug."
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
"method": "GET",
|
|
136
|
-
"path": "/api/w/:workspaceSlug/roles",
|
|
137
|
-
"summary": "Get role catalog by workspace slug."
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
"method": "GET",
|
|
141
|
-
"path": "/api/w/:workspaceSlug/members",
|
|
142
|
-
"summary": "List members by workspace slug."
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
"method": "PATCH",
|
|
146
|
-
"path": "/api/w/:workspaceSlug/members/:memberUserId/role",
|
|
147
|
-
"summary": "Update workspace member role by workspace slug."
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
"method": "GET",
|
|
151
|
-
"path": "/api/w/:workspaceSlug/invites",
|
|
152
|
-
"summary": "List workspace invites by workspace slug."
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
"method": "POST",
|
|
156
|
-
"path": "/api/w/:workspaceSlug/invites",
|
|
157
|
-
"summary": "Create workspace invite by workspace slug."
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
"method": "DELETE",
|
|
161
|
-
"path": "/api/w/:workspaceSlug/invites/:inviteId",
|
|
162
|
-
"summary": "Revoke workspace invite by workspace slug."
|
|
163
|
-
}
|
|
164
|
-
]
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
"mutations": {
|
|
168
|
-
"dependencies": {
|
|
169
|
-
"runtime": {
|
|
170
|
-
"@jskit-ai/json-rest-api-core": "0.1.102",
|
|
171
|
-
"@jskit-ai/resource-core": "0.1.100",
|
|
172
|
-
"@jskit-ai/resource-crud-core": "0.1.100",
|
|
173
|
-
"@jskit-ai/users-core": "0.1.171"
|
|
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
|
-
}
|
|
371
|
-
]
|
|
372
|
-
}
|
|
373
29
|
}
|
|
374
30
|
}
|
|
@@ -25,7 +25,7 @@ import { workspaceSettingsResource } from "../shared/resources/workspaceSettings
|
|
|
25
25
|
class WorkspacesCoreServiceProvider {
|
|
26
26
|
static id = "workspaces.core";
|
|
27
27
|
|
|
28
|
-
static
|
|
28
|
+
static dependsOn = ["users.core", "json-rest-api.core"];
|
|
29
29
|
|
|
30
30
|
async register(app) {
|
|
31
31
|
registerWorkspaceRepositories(app);
|
|
@@ -30,15 +30,22 @@ function normalizeWorkspaceSurfaceIds(surfaceIds = []) {
|
|
|
30
30
|
return normalized;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function createWorkspaceActionContextContributor({
|
|
33
|
+
function createWorkspaceActionContextContributor({
|
|
34
|
+
workspaceService,
|
|
35
|
+
workspaceMembershipOptionalSurfaceIds = [],
|
|
36
|
+
workspaceSurfaceIds = []
|
|
37
|
+
} = {}) {
|
|
34
38
|
const contributorId = "workspaces.context";
|
|
39
|
+
const workspaceMembershipOptionalSurfaceIdSet = normalizeWorkspaceSurfaceIds(
|
|
40
|
+
workspaceMembershipOptionalSurfaceIds
|
|
41
|
+
);
|
|
35
42
|
const workspaceSurfaceIdSet = normalizeWorkspaceSurfaceIds(workspaceSurfaceIds);
|
|
36
43
|
|
|
37
44
|
requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
|
|
38
45
|
|
|
39
46
|
return Object.freeze({
|
|
40
47
|
contributorId,
|
|
41
|
-
async contribute({ definition = null, input, context, request } = {}) {
|
|
48
|
+
async contribute({ definition = null, input, context, request, surface = "" } = {}) {
|
|
42
49
|
const payload = normalizeObject(input);
|
|
43
50
|
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
44
51
|
return {};
|
|
@@ -47,6 +54,7 @@ function createWorkspaceActionContextContributor({ workspaceService, workspaceSu
|
|
|
47
54
|
const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
48
55
|
const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
|
|
49
56
|
const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
57
|
+
const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(surface || context?.surface);
|
|
50
58
|
const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
|
|
51
59
|
const routeVisibilityInput =
|
|
52
60
|
request && request.routeOptions && request.routeOptions.config
|
|
@@ -58,10 +66,18 @@ function createWorkspaceActionContextContributor({ workspaceService, workspaceSu
|
|
|
58
66
|
return {};
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
const resolveOptions = { request };
|
|
70
|
+
if (
|
|
71
|
+
!hasWorkspaceRouteVisibility &&
|
|
72
|
+
workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)
|
|
73
|
+
) {
|
|
74
|
+
resolveOptions.requireMembership = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
62
78
|
resolveActionUser(context, payload),
|
|
63
79
|
payload.workspaceSlug,
|
|
64
|
-
|
|
80
|
+
resolveOptions
|
|
65
81
|
);
|
|
66
82
|
|
|
67
83
|
const contribution = {
|
|
@@ -19,9 +19,16 @@ function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
|
|
|
19
19
|
return {};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const resolveOptions = { request };
|
|
23
|
+
if (contextPolicy === "optional" && !permission) {
|
|
24
|
+
resolveOptions.requireMembership = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
28
|
+
actor,
|
|
29
|
+
workspaceSlug,
|
|
30
|
+
resolveOptions
|
|
31
|
+
);
|
|
25
32
|
|
|
26
33
|
return {
|
|
27
34
|
workspace: resolvedWorkspaceContext?.workspace || null,
|
|
@@ -215,6 +215,7 @@ function createService({
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
|
|
218
|
+
const requireMembership = options?.requireMembership !== false;
|
|
218
219
|
const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
|
|
219
220
|
if (!normalizedUserId) {
|
|
220
221
|
throw new AppError(401, "Authentication required.");
|
|
@@ -249,12 +250,16 @@ function createService({
|
|
|
249
250
|
membership = await workspaceMembershipsRepository.ensureOwnerMembership(workspace.id, normalizedUserId, options);
|
|
250
251
|
}
|
|
251
252
|
|
|
252
|
-
if (!membership || normalizeLowerText(membership.status) !== "active") {
|
|
253
|
+
if ((!membership || normalizeLowerText(membership.status) !== "active") && requireMembership) {
|
|
253
254
|
throw new AppError(403, "You do not have access to this workspace.");
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
if (!membership || normalizeLowerText(membership.status) !== "active") {
|
|
258
|
+
membership = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
256
261
|
const workspaceSettings = await ensureWorkspaceSettingsForWorkspace(workspace, options);
|
|
257
|
-
const permissions = buildPermissionsFromMembership(membership, appConfig);
|
|
262
|
+
const permissions = membership ? buildPermissionsFromMembership(membership, appConfig) : [];
|
|
258
263
|
|
|
259
264
|
return {
|
|
260
265
|
workspace,
|
|
@@ -13,6 +13,7 @@ import { TENANCY_MODE_WORKSPACES, resolveTenancyProfile } from "../shared/tenanc
|
|
|
13
13
|
import { resolveWorkspaceInvitationsPolicy } from "./support/workspaceInvitationsPolicy.js";
|
|
14
14
|
import {
|
|
15
15
|
registerWorkspaceActionSurfaceSources,
|
|
16
|
+
resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig,
|
|
16
17
|
resolveWorkspaceSurfaceIdsFromAppConfig
|
|
17
18
|
} from "./support/workspaceActionSurfaces.js";
|
|
18
19
|
import { createWorkspaceServerScopeSupport } from "./support/workspaceServerScopeSupport.js";
|
|
@@ -81,6 +82,7 @@ function registerWorkspaceCore(app) {
|
|
|
81
82
|
const appConfig = resolveAppConfig(scope);
|
|
82
83
|
return createWorkspaceActionContextContributor({
|
|
83
84
|
workspaceService: scope.make("workspaces.service"),
|
|
85
|
+
workspaceMembershipOptionalSurfaceIds: resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(appConfig),
|
|
84
86
|
workspaceSurfaceIds: resolveWorkspaceSurfaceIdsFromAppConfig(appConfig)
|
|
85
87
|
});
|
|
86
88
|
});
|
|
@@ -23,6 +23,22 @@ function resolveWorkspaceSurfaceIdsFromAppConfig(appConfig = {}) {
|
|
|
23
23
|
return resolveSurfaceIdsFromAppConfig(appConfig, (definition) => definition.requiresWorkspace === true);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(appConfig = {}) {
|
|
27
|
+
const policies = isRecord(appConfig?.surfaceAccessPolicies) ? appConfig.surfaceAccessPolicies : {};
|
|
28
|
+
|
|
29
|
+
return resolveSurfaceIdsFromAppConfig(appConfig, (definition) => {
|
|
30
|
+
if (definition.requiresWorkspace !== true) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const policyId = String(definition.accessPolicyId || "")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase();
|
|
37
|
+
const policy = policyId && isRecord(policies[policyId]) ? policies[policyId] : {};
|
|
38
|
+
return Object.hasOwn(policy, "requireWorkspaceMembership") && policy.requireWorkspaceMembership === false;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
function resolveSurfaceIdsFromAppConfig(appConfig = {}, predicate) {
|
|
27
43
|
const source = isRecord(appConfig?.surfaceDefinitions) ? appConfig.surfaceDefinitions : {};
|
|
28
44
|
const resolved = [];
|
|
@@ -111,6 +127,7 @@ function resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig = {}) {
|
|
|
111
127
|
|
|
112
128
|
export {
|
|
113
129
|
resolveWorkspaceSurfaceIdsFromAppConfig,
|
|
130
|
+
resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig,
|
|
114
131
|
resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig,
|
|
115
132
|
materializeWorkspaceActionSurfaces,
|
|
116
133
|
materializeWorkspaceActionSurfacesFromAppConfig,
|
|
@@ -3,35 +3,33 @@ import path from "node:path";
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
const packageMetadata = packageJson.jskit;
|
|
6
|
+
import descriptor from "../package.descriptor.mjs";
|
|
9
7
|
|
|
10
8
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
11
9
|
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
12
10
|
|
|
13
11
|
function findRoute(method, routePath) {
|
|
14
|
-
const routes =
|
|
12
|
+
const routes = descriptor?.metadata?.server?.routes;
|
|
15
13
|
return Array.isArray(routes)
|
|
16
14
|
? routes.find((entry) => entry?.method === method && entry?.path === routePath) || null
|
|
17
15
|
: null;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
function findFileMutation(id) {
|
|
21
|
-
const fileMutations =
|
|
19
|
+
const fileMutations = descriptor?.mutations?.files;
|
|
22
20
|
return Array.isArray(fileMutations)
|
|
23
21
|
? fileMutations.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
24
22
|
: null;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
function findSourceMutation(id) {
|
|
28
|
-
const sourceMutations =
|
|
26
|
+
const sourceMutations = descriptor?.mutations?.source;
|
|
29
27
|
return Array.isArray(sourceMutations)
|
|
30
28
|
? sourceMutations.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
31
29
|
: null;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
test("workspaces-core
|
|
32
|
+
test("workspaces-core descriptor advertises public invite resolution route metadata", () => {
|
|
35
33
|
assert.deepEqual(findRoute("GET", "/api/workspace/invitations/resolve"), {
|
|
36
34
|
method: "GET",
|
|
37
35
|
path: "/api/workspace/invitations/resolve",
|
|
@@ -95,7 +95,7 @@ test("workspace settings and invite operations expose canonical validators", ()
|
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
test("workspaces-core
|
|
98
|
+
test("workspaces-core no longer uses a workspace schema helper that exposes raw schema leaves", () => {
|
|
99
99
|
const testFilePath = fileURLToPath(import.meta.url);
|
|
100
100
|
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
101
101
|
const workspaceRoutesFilePath = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
|
|
@@ -342,3 +342,49 @@ test("workspace action context contributor resolves context for workspace surfac
|
|
|
342
342
|
permissions: ["crud.breeds.list"]
|
|
343
343
|
});
|
|
344
344
|
});
|
|
345
|
+
|
|
346
|
+
test("workspace action context contributor permits missing membership only for an explicitly optional active surface", async () => {
|
|
347
|
+
const calls = [];
|
|
348
|
+
const contributor = createWorkspaceActionContextContributor({
|
|
349
|
+
workspaceService: {
|
|
350
|
+
async resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options) {
|
|
351
|
+
calls.push({ user, workspaceSlug, options });
|
|
352
|
+
return {
|
|
353
|
+
workspace: { id: 77, slug: "acme" },
|
|
354
|
+
membership: null,
|
|
355
|
+
permissions: []
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
workspaceMembershipOptionalSurfaceIds: ["app"],
|
|
360
|
+
workspaceSurfaceIds: ["admin", "app"]
|
|
361
|
+
});
|
|
362
|
+
const request = {
|
|
363
|
+
user: { id: 42 },
|
|
364
|
+
routeOptions: {
|
|
365
|
+
config: {
|
|
366
|
+
surface: "app",
|
|
367
|
+
visibility: "public"
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const contribution = await contributor.contribute({
|
|
373
|
+
definition: {
|
|
374
|
+
id: "customer.pets.list",
|
|
375
|
+
surfaces: ["app"]
|
|
376
|
+
},
|
|
377
|
+
input: { workspaceSlug: "acme" },
|
|
378
|
+
context: {
|
|
379
|
+
requestMeta: { request }
|
|
380
|
+
},
|
|
381
|
+
request,
|
|
382
|
+
surface: "app"
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
assert.equal(calls.length, 1);
|
|
386
|
+
assert.equal(calls[0].options.requireMembership, false);
|
|
387
|
+
assert.deepEqual(contribution.workspace, { id: 77, slug: "acme" });
|
|
388
|
+
assert.equal(contribution.membership, null);
|
|
389
|
+
assert.deepEqual(contribution.permissions, []);
|
|
390
|
+
});
|
|
@@ -2,9 +2,44 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
materializeWorkspaceActionSurfacesFromAppConfig,
|
|
5
|
-
resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig
|
|
5
|
+
resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig,
|
|
6
|
+
resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig
|
|
6
7
|
} from "../src/server/support/workspaceActionSurfaces.js";
|
|
7
8
|
|
|
9
|
+
test("resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig honors explicit surface policy overrides", () => {
|
|
10
|
+
const surfaceIds = resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig({
|
|
11
|
+
surfaceAccessPolicies: {
|
|
12
|
+
workspace_authenticated: {
|
|
13
|
+
requireWorkspaceMembership: false
|
|
14
|
+
},
|
|
15
|
+
workspace_member: {
|
|
16
|
+
requireWorkspaceMembership: true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
surfaceDefinitions: {
|
|
20
|
+
app: {
|
|
21
|
+
id: "app",
|
|
22
|
+
enabled: true,
|
|
23
|
+
requiresWorkspace: true,
|
|
24
|
+
accessPolicyId: "workspace_authenticated"
|
|
25
|
+
},
|
|
26
|
+
admin: {
|
|
27
|
+
id: "admin",
|
|
28
|
+
enabled: true,
|
|
29
|
+
requiresWorkspace: true,
|
|
30
|
+
accessPolicyId: "workspace_member"
|
|
31
|
+
},
|
|
32
|
+
implicit: {
|
|
33
|
+
id: "implicit",
|
|
34
|
+
enabled: true,
|
|
35
|
+
requiresWorkspace: true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(surfaceIds, ["app"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
8
43
|
test("materializeWorkspaceActionSurfacesFromAppConfig resolves workspace surfaces from appConfig", () => {
|
|
9
44
|
const actionDefinitions = [
|
|
10
45
|
{
|
|
@@ -117,3 +117,34 @@ test("workspace auth policy context resolver skips workspace lookup when workspa
|
|
|
117
117
|
assert.equal(called, false);
|
|
118
118
|
assert.deepEqual(resolved, {});
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
test("workspace auth policy context resolver treats optional context as membership-optional", async () => {
|
|
122
|
+
const calls = [];
|
|
123
|
+
const request = {
|
|
124
|
+
params: { workspaceSlug: "acme" }
|
|
125
|
+
};
|
|
126
|
+
const actor = { id: 7 };
|
|
127
|
+
const resolver = createWorkspaceAuthPolicyContextResolver({
|
|
128
|
+
workspaceService: {
|
|
129
|
+
async resolveWorkspaceContextForUserBySlug(resolvedActor, workspaceSlug, options) {
|
|
130
|
+
calls.push({ resolvedActor, workspaceSlug, options });
|
|
131
|
+
return {
|
|
132
|
+
workspace: { id: 11, slug: workspaceSlug },
|
|
133
|
+
membership: null,
|
|
134
|
+
permissions: []
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const resolved = await resolver({
|
|
141
|
+
request,
|
|
142
|
+
actor,
|
|
143
|
+
meta: { contextPolicy: "optional" }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
assert.equal(calls.length, 1);
|
|
147
|
+
assert.equal(calls[0].options.requireMembership, false);
|
|
148
|
+
assert.equal(resolved.membership, null);
|
|
149
|
+
assert.deepEqual(resolved.permissions, []);
|
|
150
|
+
});
|
|
@@ -179,7 +179,7 @@ function createWorkspaceServiceFixture({
|
|
|
179
179
|
return { service, calls, insertedPayloads };
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
test("workspaceService
|
|
182
|
+
test("workspaceService no longer exposes bootstrap payload assembly", () => {
|
|
183
183
|
const { service } = createWorkspaceServiceFixture();
|
|
184
184
|
assert.equal(service.buildBootstrapPayload, undefined);
|
|
185
185
|
});
|
|
@@ -199,7 +199,7 @@ test("workspaceService.listWorkspacesForUser returns only accessible workspaces"
|
|
|
199
199
|
assert.equal(calls.insert, 0);
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
test("workspaceService.listWorkspacesForUser
|
|
202
|
+
test("workspaceService.listWorkspacesForUser no longer provisions personal workspace in workspace mode", async () => {
|
|
203
203
|
const { service, calls } = createWorkspaceServiceFixture({
|
|
204
204
|
tenancyMode: "workspaces",
|
|
205
205
|
personalWorkspace: null
|
|
@@ -522,6 +522,68 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
|
|
|
522
522
|
assert.deepEqual(context.permissions, ["*"]);
|
|
523
523
|
});
|
|
524
524
|
|
|
525
|
+
test("workspaceService.resolveWorkspaceContextForUserBySlug can resolve an existing workspace without membership", async () => {
|
|
526
|
+
const service = createService({
|
|
527
|
+
appConfig: {
|
|
528
|
+
tenancyMode: "personal",
|
|
529
|
+
roleCatalog: createRoleCatalog()
|
|
530
|
+
},
|
|
531
|
+
workspacesRepository: {
|
|
532
|
+
async findBySlug() {
|
|
533
|
+
return {
|
|
534
|
+
id: "42",
|
|
535
|
+
slug: "dog-and-groom",
|
|
536
|
+
name: "Dog And Groom",
|
|
537
|
+
ownerUserId: "99",
|
|
538
|
+
isPersonal: false,
|
|
539
|
+
avatarUrl: ""
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
async findPersonalByOwnerUserId() {
|
|
543
|
+
return null;
|
|
544
|
+
},
|
|
545
|
+
async listForUserId() {
|
|
546
|
+
return [];
|
|
547
|
+
},
|
|
548
|
+
async insert() {
|
|
549
|
+
throw new Error("not implemented");
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
workspaceMembershipsRepository: {
|
|
553
|
+
async findByWorkspaceIdAndUserId() {
|
|
554
|
+
return null;
|
|
555
|
+
},
|
|
556
|
+
async ensureOwnerMembership() {
|
|
557
|
+
throw new Error("must not create membership for a non-owner");
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
workspaceSettingsRepository: {
|
|
561
|
+
async ensureForWorkspaceId() {
|
|
562
|
+
return { invitesEnabled: true };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await assert.rejects(
|
|
568
|
+
() =>
|
|
569
|
+
service.resolveWorkspaceContextForUserBySlug(
|
|
570
|
+
{ id: "7", email: "customer@example.com" },
|
|
571
|
+
"dog-and-groom"
|
|
572
|
+
),
|
|
573
|
+
/You do not have access to this workspace/
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
const context = await service.resolveWorkspaceContextForUserBySlug(
|
|
577
|
+
{ id: "7", email: "customer@example.com" },
|
|
578
|
+
"dog-and-groom",
|
|
579
|
+
{ requireMembership: false }
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
assert.equal(context.workspace.slug, "dog-and-groom");
|
|
583
|
+
assert.equal(context.membership, null);
|
|
584
|
+
assert.deepEqual(context.permissions, []);
|
|
585
|
+
});
|
|
586
|
+
|
|
525
587
|
test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions from appConfig.roleCatalog", async () => {
|
|
526
588
|
const { service } = createWorkspaceServiceFixture({
|
|
527
589
|
roleCatalog: {
|
|
@@ -16,7 +16,7 @@ test("workspace settings actions live in their own action array", () => {
|
|
|
16
16
|
assert.equal(workspaceSettingsActions[1].extensions?.assistant?.description, "Update workspace settings.");
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test("workspace actions array
|
|
19
|
+
test("workspace actions array no longer owns workspace settings actions", () => {
|
|
20
20
|
const otherWorkspaceActionIds = [
|
|
21
21
|
...workspaceDirectoryActions,
|
|
22
22
|
...workspacePendingInvitationsActions,
|