@jskit-ai/workspaces-core 0.1.137 → 0.1.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +38 -238
  2. package/patterns/workspace-server/PATTERN.md +60 -0
  3. package/src/server/WorkspacesFeature.js +226 -0
  4. package/src/server/WorkspacesIntegrationsProvider.js +74 -0
  5. package/src/server/common/contributors/workspaceActionContextContributor.js +18 -6
  6. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +25 -4
  7. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
  8. package/src/server/common/services/workspaceContextService.js +7 -2
  9. package/src/server/common/support/realtimeServiceEvents.js +102 -61
  10. package/src/server/support/workspaceActionSurfaces.js +18 -13
  11. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
  12. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
  13. package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
  14. package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
  15. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
  16. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
  17. package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
  18. package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
  19. package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
  20. package/test/exportsContract.test.js +2 -1
  21. package/test/featureRuntime.test.js +116 -0
  22. package/test/packageMetadata.test.js +20 -54
  23. package/test/workspaceActionContextContributor.test.js +49 -8
  24. package/test/workspaceActionSurfaces.test.js +36 -1
  25. package/test/workspaceAuthPolicyContextResolver.test.js +61 -0
  26. package/test/workspaceRouteVisibilityResolver.test.js +31 -0
  27. package/test/workspaceService.test.js +62 -0
  28. package/test/workspaceSettingsActions.test.js +102 -16
  29. package/test/workspacesRouteRequestInputValidator.test.js +14 -65
  30. package/src/server/WorkspacesCoreServiceProvider.js +0 -64
  31. package/src/server/registerWorkspaceBootstrap.js +0 -27
  32. package/src/server/registerWorkspaceCore.js +0 -101
  33. package/src/server/registerWorkspaceRepositories.js +0 -30
  34. package/src/server/support/workspaceServerScopeSupport.js +0 -18
  35. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
  36. package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -137
  37. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -130
  38. package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -66
  39. package/test/registerServiceRealtimeEvents.test.js +0 -116
  40. package/test/registerWorkspaceBootstrap.test.js +0 -86
  41. package/test/registerWorkspaceDirectory.test.js +0 -31
  42. package/test/registerWorkspaceMembers.test.js +0 -140
  43. package/test/registerWorkspaceSettings.test.js +0 -40
  44. package/test/workspaceServerScopeSupport.test.js +0 -131
  45. /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
  46. /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
  47. /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
  48. /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
  49. /package/{templates → patterns/workspace-server/example}/packages/main/src/server/email/workspaceInviteEmail.js +0 -0
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.1.137",
3
+ "version": "0.1.138",
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,14 @@
17
18
  "./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
18
19
  },
19
20
  "dependencies": {
20
- "@jskit-ai/auth-core": "0.1.157",
21
- "@jskit-ai/database-runtime": "0.1.159",
22
- "@jskit-ai/http-runtime": "0.1.157",
23
- "@jskit-ai/json-rest-api-core": "0.1.103",
24
- "@jskit-ai/kernel": "0.1.159",
25
- "@jskit-ai/resource-core": "0.1.101",
26
- "@jskit-ai/resource-crud-core": "0.1.101",
27
- "@jskit-ai/users-core": "0.1.172",
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
- "json-rest-api.core"
48
+ "users.extensions"
41
49
  ]
42
50
  },
43
51
  "runtime": {
44
52
  "server": {
45
53
  "providers": [
46
54
  {
47
- "entrypoint": "src/server/WorkspacesCoreServiceProvider.js",
48
- "export": "WorkspacesCoreServiceProvider"
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 runtime provider and workspace route registration surface."
91
+ "summary": "Exports the explicit workspace feature, integrations, services, repositories, actions, and routes."
88
92
  }
89
93
  ],
90
- "containerTokens": {
91
- "server": [
92
- "workspaces.server.scope-support"
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
- "mutations": {
168
- "dependencies": {
169
- "runtime": {
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 };