@jskit-ai/workspaces-core 0.2.4 → 0.2.5

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 (75) hide show
  1. package/locales/en.json +2 -2
  2. package/package.json +38 -11
  3. package/patterns/workspace-server/PATTERN.md +42 -17
  4. package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
  5. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
  6. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
  7. package/src/server/WorkspacesFeature.js +74 -183
  8. package/src/server/WorkspacesIntegrationsProvider.js +26 -10
  9. package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
  10. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
  11. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
  12. package/src/server/common/formatters/workspaceFormatter.js +12 -10
  13. package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
  14. package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
  15. package/src/server/common/repositories/workspacesRepository.js +17 -25
  16. package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
  17. package/src/server/common/support/realtimeServiceEvents.js +20 -12
  18. package/src/server/common/support/workspaceRecordId.js +21 -0
  19. package/src/server/common/validators/routeParamsValidator.js +3 -2
  20. package/src/server/i18n.js +5 -2
  21. package/src/server/support/resolveWorkspace.js +10 -7
  22. package/src/server/support/workspaceActionSurfaces.js +5 -41
  23. package/src/server/support/workspaceCrud.js +25 -0
  24. package/src/server/support/workspaceInvitationsPolicy.js +16 -27
  25. package/src/server/support/workspaceRouteInput.js +19 -2
  26. package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
  27. package/src/server/types.js +47 -0
  28. package/src/server/workspaceBootstrapContributor.js +105 -142
  29. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
  30. package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
  31. package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
  32. package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
  33. package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
  34. package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
  35. package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
  36. package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
  37. package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
  38. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
  39. package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
  40. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
  41. package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
  42. package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
  43. package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
  44. package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
  45. package/src/server/workspacesRuntime.js +185 -0
  46. package/src/shared/operationMessages.js +6 -15
  47. package/src/shared/resources/workspaceMembersResource.js +2 -2
  48. package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
  49. package/src/shared/resources/workspaceResource.js +4 -1
  50. package/src/shared/roles.js +15 -12
  51. package/src/shared/tenancyProfile.js +2 -2
  52. package/test/exportsContract.test.js +5 -0
  53. package/test/featureRuntime.test.js +5 -6
  54. package/test/packageMetadata.test.js +12 -0
  55. package/test/repositories.integration.test.js +1 -1
  56. package/test/requestedWorkspaceScope.test.js +24 -0
  57. package/test/roleLabels.test.js +29 -7
  58. package/test/support/invitationTransactions.js +35 -19
  59. package/test/workspaceActionSurfaces.test.js +0 -55
  60. package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
  61. package/test/workspaceBootstrapContributor.test.js +7 -6
  62. package/test/workspaceCrud.test.js +23 -0
  63. package/test/workspaceInvitesRepository.test.js +9 -8
  64. package/test/workspaceMembersService.test.js +131 -28
  65. package/test/workspacePendingInvitationsResource.test.js +2 -2
  66. package/test/workspacePendingInvitationsService.test.js +69 -22
  67. package/test/workspaceService.test.js +58 -47
  68. package/test/workspaceSettingsActions.test.js +14 -7
  69. package/test/workspaceSettingsRepository.test.js +11 -0
  70. package/test/workspaceSettingsService.test.js +11 -9
  71. package/test/workspacesRepository.test.js +16 -16
  72. package/test/workspacesRouteRequestInputValidator.test.js +4 -4
  73. package/src/server/common/repositories/repositoryUtils.js +0 -43
  74. package/src/server/common/support/deepFreeze.js +0 -1
  75. package/src/server/common/support/resolveActionUser.js +0 -9
package/locales/en.json CHANGED
@@ -8,9 +8,7 @@
8
8
  "invalidPayload": "Invalid authenticated user payload."
9
9
  },
10
10
  "workspace": {
11
- "slugUnavailable": "Unable to generate unique workspace slug.",
12
11
  "creationDisabled": "Workspace creation is disabled for this tenancy mode.",
13
- "nameRequired": "Workspace name is required.",
14
12
  "contextDisabled": "Workspace context is disabled.",
15
13
  "slugRequired": "workspaceSlug is required.",
16
14
  "notFound": "Workspace not found.",
@@ -18,6 +16,7 @@
18
16
  },
19
17
  "member": {
20
18
  "roleNotAssignable": "Role is not assignable.",
19
+ "userIdRequired": "Member user id is required.",
21
20
  "notFound": "Member not found.",
22
21
  "ownerRoleLocked": "Cannot change workspace owner role.",
23
22
  "ownerNotRemovable": "Cannot remove workspace owner."
@@ -25,6 +24,7 @@
25
24
  "invite": {
26
25
  "disabled": "Workspace invitations are disabled.",
27
26
  "notFound": "Invite not found.",
27
+ "idRequired": "Invite id is required.",
28
28
  "notFoundOrHandled": "Invite not found or already handled.",
29
29
  "invitationNotFoundOrHandled": "Invitation not found or already handled.",
30
30
  "workspaceMissing": "Invitation workspace no longer exists.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -10,6 +10,9 @@
10
10
  "./server/WorkspacesIntegrationsProvider": "./src/server/WorkspacesIntegrationsProvider.js",
11
11
  "./server/validators/routeParamsValidator": "./src/server/common/validators/routeParamsValidator.js",
12
12
  "./server/support/workspaceRouteInput": "./src/server/support/workspaceRouteInput.js",
13
+ "./server/support/workspaceCrud": "./src/server/support/workspaceCrud.js",
14
+ "./server/support/workspaceRoutePaths": "./src/server/support/workspaceRoutePaths.js",
15
+ "./server/types": "./src/server/types.js",
13
16
  "./shared/jsonApiTransports": "./src/shared/jsonApiTransports.js",
14
17
  "./shared/settings": "./src/shared/settings.js",
15
18
  "./shared/tenancyProfile": "./src/shared/tenancyProfile.js",
@@ -19,11 +22,11 @@
19
22
  "./locales/en.json": "./locales/en.json"
20
23
  },
21
24
  "dependencies": {
22
- "@jskit-ai/database-runtime": "0.1.252",
23
- "@jskit-ai/json-rest-api-core": "0.2.3",
24
- "@jskit-ai/resource-core": "0.1.194",
25
- "@jskit-ai/resource-crud-core": "0.1.194",
26
- "@jskit-ai/users-core": "0.2.4",
25
+ "@jskit-ai/database-runtime": "0.1.253",
26
+ "@jskit-ai/json-rest-api-core": "0.2.4",
27
+ "@jskit-ai/resource-core": "0.1.195",
28
+ "@jskit-ai/resource-crud-core": "0.1.195",
29
+ "@jskit-ai/users-core": "0.2.5",
27
30
  "json-rest-schema": "^1.0.17"
28
31
  },
29
32
  "description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
@@ -86,8 +89,32 @@
86
89
  "apiSummary": {
87
90
  "surfaces": [
88
91
  {
89
- "subpath": "./server",
90
- "summary": "Exports the explicit workspace feature, integrations, services, repositories, actions, and routes."
92
+ "subpath": "./server/WorkspacesFeature",
93
+ "summary": "Exports WorkspacesFeature, which provides workspaces.core (directory, members, pending invitations and settings services, their actions and routes), and the optional tokens' defaults."
94
+ },
95
+ {
96
+ "subpath": "./server/WorkspacesIntegrationsProvider",
97
+ "summary": "Exports WorkspacesIntegrationsProvider, which adds workspace context, visibility, provisioning, realtime audiences and invitation context to auth, users and actions."
98
+ },
99
+ {
100
+ "subpath": "./server/validators/routeParamsValidator",
101
+ "summary": "Exports the workspaceSlug route params validator."
102
+ },
103
+ {
104
+ "subpath": "./server/support/workspaceRouteInput",
105
+ "summary": "Exports the action input a workspace route's params give, and requestedWorkspaceScope, the workspace a request asks for before the workspace context exists."
106
+ },
107
+ {
108
+ "subpath": "./server/support/workspaceCrud",
109
+ "summary": "Exports WORKSPACE_CRUD_SCOPE, the workspace scope of CRUD resources."
110
+ },
111
+ {
112
+ "subpath": "./server/support/workspaceRoutePaths",
113
+ "summary": "Exports the workspace route base path and resolveWorkspaceRoutePath."
114
+ },
115
+ {
116
+ "subpath": "./server/types",
117
+ "summary": "Type of the workspaces.core capability, for features that require it."
91
118
  }
92
119
  ],
93
120
  "capabilities": [
@@ -171,8 +198,8 @@
171
198
  }
172
199
  },
173
200
  "peerDependencies": {
174
- "@jskit-ai/auth-core": "0.2.3",
175
- "@jskit-ai/http-runtime": "0.2.3",
176
- "@jskit-ai/kernel": "0.2.3"
201
+ "@jskit-ai/auth-core": "0.2.4",
202
+ "@jskit-ai/http-runtime": "0.2.4",
203
+ "@jskit-ai/kernel": "0.2.4"
177
204
  }
178
205
  }
@@ -24,8 +24,8 @@ Choose workspace creation policy, roles, invitation policy, expiry, email copy,
24
24
  member permissions, personal-workspace behavior, and surface access rules.
25
25
 
26
26
  In `workspaces` mode, `tenancyPolicy.workspace.autoProvision: true` gives every
27
- user a personal workspace in addition to shared ones; the server config function
28
- `personalWorkspaceName(user)` names it. `workspaceSettings.defaults` sets
27
+ user a personal workspace in addition to shared ones; the optional
28
+ `workspaces.personal-workspace-name` capability, a function of the user, names it. `workspaceSettings.defaults` sets
29
29
  `invitesEnabled` and the colours (`lightPrimaryColor`, `darkPrimaryColor` and
30
30
  the other palette fields) that new workspaces start with.
31
31
 
@@ -63,16 +63,16 @@ catalogue entries. Updating labels requires no database migration.
63
63
 
64
64
  ## Transactional invitation participation
65
65
 
66
- Through `workspaces.core`, register one callback during feature setup with
67
- `workspaces.services.pendingInvitations.registerAcceptanceParticipant(fn)`.
68
- The built-in `workspace.invite.redeem` action invokes
69
- `fn({ invite, user }, { trx, context })` after validating the recipient, state and
70
- expiry and activating membership in the uncommitted transaction. Forward `trx`
71
- to every application owner and propagate failures. JSKIT marks accepted and
72
- commits after the callback succeeds. Keep external effects out of the callback.
73
- Registration rejects non-functions, duplicates and calls after acceptance/refusal
74
- begins. Allow ordinary invitations; reject stale, revoked, conflicting or
75
- unexpectedly missing application associations.
66
+ Provide one function as the optional `workspaces.invite-acceptance-participant`
67
+ capability, from a small application feature that requires only what its writes
68
+ need (it cannot require `workspaces.core`, which receives it). The built-in
69
+ `workspace.invite.redeem` action invokes `fn({ invite, user }, { trx, context })`
70
+ after validating the recipient, state and expiry and activating membership in the
71
+ uncommitted transaction. Forward `trx` to every application owner and propagate
72
+ failures. JSKIT marks accepted and commits after the function succeeds. Keep
73
+ external effects out of it. Without the capability, acceptance activates the
74
+ membership only. Allow ordinary invitations; reject stale, revoked, conflicting
75
+ or unexpectedly missing application associations.
76
76
 
77
77
  Use `members.prepareInvite(workspace, actor,
78
78
  payload, { trx, context })` in `repositories.workspaceInvites.withTransaction`
@@ -91,8 +91,8 @@ exception, or the normalized mailer result. Persistence is unchanged by delivery
91
91
  failure. A provider may accept a message before failing; the application owns
92
92
  receipts and retries. Keep tokens/URLs out of logs and use controlled mailers in tests.
93
93
 
94
- `createInvite`, acceptance and refusal now own managed transactions and reject
95
- borrowed `options.trx`. Use preparation followed by delivery after commit for
94
+ `createInvite`, acceptance and refusal own their managed transactions and
95
+ reject a borrowed `options.trx`. Use preparation followed by delivery after commit for
96
96
  transactional creation; put related acceptance writes in the participant. Revocation may join
97
97
  a managed transaction. Raw Knex/savepoint transactions cannot own resource
98
98
  writes. Invitation mutations lock workspace then invitation rows, revalidate
@@ -115,9 +115,34 @@ authorization, identity/training linking, scope eligibility and required audit.
115
115
 
116
116
  ## Example files
117
117
 
118
- `example/config/roles.js` is a concrete role catalogue.
119
- `example/packages/main/src/server/email/workspaceInviteEmail.js` demonstrates an
120
- app-owned invitation renderer. Compose both through normal application config.
118
+ `example/config/roles.js` is a concrete role catalogue, set as the
119
+ application's `roleCatalog` config.
120
+
121
+ The invitation email comes from two optional `workspaces.core` capabilities,
122
+ which one small application feature provides:
123
+ `example/packages/main/src/server/WorkspaceInvitationsFeature.js`.
124
+
125
+ ```js
126
+ provides: {
127
+ inviteEmailTemplate: "workspaces.invite-email-template",
128
+ inviteMailer: "workspaces.invite-mailer"
129
+ },
130
+ setup({ env }) {
131
+ return {
132
+ inviteEmailTemplate: renderWorkspaceInviteEmail,
133
+ inviteMailer: createWorkspaceInviteMailer({ env })
134
+ };
135
+ }
136
+ ```
137
+
138
+ - `email/workspaceInviteEmail.js` is the renderer, a
139
+ `WorkspaceInviteEmailTemplate` returning `{ subject, text, html }`.
140
+ - `email/workspaceInviteMailer.js` is the mailer, a `WorkspaceInviteMailer`
141
+ whose `sendWorkspaceInvite` sends the rendered message over SMTP.
142
+
143
+ Without a renderer, workspaces-core uses its built-in email; without a mailer,
144
+ invitations are created and their delivery reports `mailer_unconfigured`. The
145
+ feature must not require `workspaces.core`, which receives both.
121
146
 
122
147
  ## Variation points
123
148
 
@@ -0,0 +1,27 @@
1
+ import { defineFeature } from "@jskit-ai/kernel/server/features";
2
+ import { renderWorkspaceInviteEmail } from "./email/workspaceInviteEmail.js";
3
+ import { createWorkspaceInviteMailer } from "./email/workspaceInviteMailer.js";
4
+
5
+ /**
6
+ * The application's invitation email: the renderer and the mailer that sends
7
+ * it, each filling one of workspaces-core's optional tokens.
8
+ */
9
+ const WorkspaceInvitationsFeature = defineFeature({
10
+ id: "main.workspace-invitations",
11
+ requires: {
12
+ env: "runtime.env"
13
+ },
14
+ provides: {
15
+ inviteEmailTemplate: "workspaces.invite-email-template",
16
+ inviteMailer: "workspaces.invite-mailer"
17
+ },
18
+ /** @param {{ env: import("@jskit-ai/kernel/server/types").RuntimeEnv }} capabilities */
19
+ setup({ env }) {
20
+ return {
21
+ inviteEmailTemplate: renderWorkspaceInviteEmail,
22
+ inviteMailer: createWorkspaceInviteMailer({ env })
23
+ };
24
+ }
25
+ });
26
+
27
+ export { WorkspaceInvitationsFeature };
@@ -1,13 +1,14 @@
1
- function renderWorkspaceInviteEmail({
2
- inviteUrl = "",
3
- workspace = {},
4
- inviter = null,
5
- roleSid = "member",
6
- expiresAt = ""
7
- } = {}) {
8
- const workspaceName = String(workspace?.name || workspace?.slug || "the workspace").trim() || "the workspace";
9
- const inviterName = String(inviter?.displayName || inviter?.email || "").trim();
10
- const roleName = String(roleSid || "member").trim() || "member";
1
+ /**
2
+ * The application's invitation email, provided as
3
+ * `workspaces.invite-email-template`. workspaces-core passes every field; the
4
+ * inviter is null when no one is signed in, and `roleLabel` is present only
5
+ * when the role catalogue names the role.
6
+ * @type {import("@jskit-ai/workspaces-core/server/types").WorkspaceInviteEmailTemplate}
7
+ */
8
+ function renderWorkspaceInviteEmail({ inviteUrl, workspace, inviter, roleSid, roleLabel, expiresAt }) {
9
+ const workspaceName = workspace.name || workspace.slug;
10
+ const inviterName = inviter?.displayName || inviter?.email || "";
11
+ const roleName = roleLabel || roleSid;
11
12
  const intro = inviterName
12
13
  ? `${inviterName} invited you to join ${workspaceName} as ${roleName}.`
13
14
  : `You have been invited to join ${workspaceName} as ${roleName}.`;
@@ -0,0 +1,27 @@
1
+ import nodemailer from "nodemailer";
2
+
3
+ /**
4
+ * The application's `workspaces.invite-mailer`: sends each rendered
5
+ * invitation through the SMTP server `SMTP_URL` names, from `MAIL_FROM`.
6
+ * @param {object} dependencies
7
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} dependencies.env
8
+ * @returns {import("@jskit-ai/workspaces-core/server/types").WorkspaceInviteMailer}
9
+ */
10
+ function createWorkspaceInviteMailer({ env }) {
11
+ const transport = nodemailer.createTransport(env.SMTP_URL);
12
+
13
+ async function sendWorkspaceInvite({ email, message }) {
14
+ const sent = await transport.sendMail({
15
+ from: env.MAIL_FROM,
16
+ to: email,
17
+ subject: message.subject,
18
+ text: message.text,
19
+ ...(message.html ? { html: message.html } : {})
20
+ });
21
+ return { status: "sent", providerMessageId: sent.messageId };
22
+ }
23
+
24
+ return Object.freeze({ sendWorkspaceInvite });
25
+ }
26
+
27
+ export { createWorkspaceInviteMailer };
@@ -1,178 +1,18 @@
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
1
  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 { WORKSPACE_SETTINGS_COLOR_FIELDS, normalizeWorkspaceHexColor } from "../shared/settings.js";
13
- import { resolveTenancyProfile } from "../shared/tenancyProfile.js";
14
- import { createRepository as createWorkspaceInvitesRepository } from "./common/repositories/workspaceInvitesRepository.js";
15
- import { createRepository as createWorkspaceMembershipsRepository } from "./common/repositories/workspaceMembershipsRepository.js";
16
- import { createRepository as createWorkspacesRepository } from "./common/repositories/workspacesRepository.js";
17
- import { createService as createWorkspaceService } from "./common/services/workspaceContextService.js";
18
- import { createWorkspacesServerI18n } from "./i18n.js";
19
2
  import { createWorkspaceBootstrapContributor } from "./workspaceBootstrapContributor.js";
20
- import { registerWorkspaceDirectoryRoutes } from "./workspaceDirectory/bootWorkspaceDirectoryRoutes.js";
21
- import { buildWorkspaceDirectoryActions } from "./workspaceDirectory/workspaceDirectoryActions.js";
3
+ import { registerWorkspaceDirectoryRoutes } from "./workspaceDirectory/workspaceDirectoryRoutes.js";
4
+ import { createWorkspaceDirectoryActions } from "./workspaceDirectory/workspaceDirectoryActions.js";
5
+ import { registerWorkspaceMembersRoutes } from "./workspaceMembers/workspaceMembersRoutes.js";
6
+ import { createWorkspaceMembersActions } from "./workspaceMembers/workspaceMembersActions.js";
7
+ import { registerWorkspacePendingInvitationsRoutes } from "./workspacePendingInvitations/workspacePendingInvitationsRoutes.js";
8
+ import { createWorkspacePendingInvitationsActions } from "./workspacePendingInvitations/workspacePendingInvitationsActions.js";
9
+ import { registerWorkspaceSettingsRoutes } from "./workspaceSettings/workspaceSettingsRoutes.js";
10
+ import { unconfiguredWorkspaceInviteMailer } from "./workspaceMembers/unconfiguredWorkspaceInviteMailer.js";
22
11
  import { renderDefaultWorkspaceInviteEmail } from "./workspaceMembers/defaultWorkspaceInviteEmail.js";
23
- import { registerWorkspaceMembersRoutes } from "./workspaceMembers/bootWorkspaceMembers.js";
24
- import { buildWorkspaceMembersActions } from "./workspaceMembers/workspaceMembersActions.js";
25
- import { createWorkspaceInviteUrlBuilder } from "./workspaceMembers/workspaceInviteUrls.js";
26
- import { createService as createWorkspaceMembersService } from "./workspaceMembers/workspaceMembersService.js";
27
- import { registerWorkspacePendingInvitationsRoutes } from "./workspacePendingInvitations/bootWorkspacePendingInvitations.js";
28
- import { buildWorkspacePendingInvitationsActions } from "./workspacePendingInvitations/workspacePendingInvitationsActions.js";
29
- import { createService as createWorkspacePendingInvitationsService } from "./workspacePendingInvitations/workspacePendingInvitationsService.js";
30
- import { registerWorkspaceSettingsRoutes } from "./workspaceSettings/bootWorkspaceSettings.js";
31
- import { createRepository as createWorkspaceSettingsRepository } from "./workspaceSettings/workspaceSettingsRepository.js";
32
- import { buildWorkspaceSettingsActions } from "./workspaceSettings/workspaceSettingsActions.js";
33
- import { createService as createWorkspaceSettingsService } from "./workspaceSettings/workspaceSettingsService.js";
34
- import { resolveWorkspaceInvitationsPolicy } from "./support/workspaceInvitationsPolicy.js";
35
- import { workspaceSlugParamsValidator } from "./common/validators/routeParamsValidator.js";
36
- import { buildWorkspaceInputFromRouteParams } from "./support/workspaceRouteInput.js";
37
- import { resolveWorkspace } from "./support/resolveWorkspace.js";
38
-
39
- function requirePositiveInteger(value, label) {
40
- const normalized = Number(value);
41
- if (!Number.isInteger(normalized) || normalized < 1) {
42
- throw new TypeError(`${label} must be a positive integer.`);
43
- }
44
- return normalized;
45
- }
46
-
47
- function requireBoolean(value, label) {
48
- if (typeof value !== "boolean") {
49
- throw new TypeError(`${label} must be boolean.`);
50
- }
51
- return value;
52
- }
53
-
54
- function requireWorkspaceColorDefaults(defaults) {
55
- const colors = {};
56
- for (const fieldName of WORKSPACE_SETTINGS_COLOR_FIELDS) {
57
- if (defaults?.[fieldName] === undefined) {
58
- continue;
59
- }
60
- const color = normalizeWorkspaceHexColor(defaults[fieldName]);
61
- if (!color) {
62
- throw new TypeError(`runtime.config.workspaceSettings.defaults.${fieldName} must be a hex color like #1867C0.`);
63
- }
64
- colors[fieldName] = color;
65
- }
66
- return colors;
67
- }
68
-
69
- function resolveInviteEmailTemplate(config) {
70
- const template =
71
- config.workspaceInviteEmailTemplate ||
72
- config.workspaceInviteEmailRenderer ||
73
- config.workspaceInvitations?.emailTemplate ||
74
- config.workspaceInvitations?.emailRenderer;
75
- return typeof template === "function" ? template : renderDefaultWorkspaceInviteEmail;
76
- }
77
-
78
- async function installWorkspaceResources(jsonRestApi) {
79
- const scopeOptions = { writeSerializers: { "datetime-utc": toDatabaseDateTimeUtc } };
80
- await addResourceIfMissing(
81
- jsonRestApi,
82
- "workspaces",
83
- createJsonRestResourceScopeOptions(workspaceResource, scopeOptions)
84
- );
85
- await addResourceIfMissing(
86
- jsonRestApi,
87
- "workspaceMemberships",
88
- createJsonRestResourceScopeOptions(workspaceMembershipsResource, scopeOptions)
89
- );
90
- await addResourceIfMissing(
91
- jsonRestApi,
92
- "workspaceInvites",
93
- createJsonRestResourceScopeOptions(workspaceInvitesResource, scopeOptions)
94
- );
95
- await addResourceIfMissing(
96
- jsonRestApi,
97
- "workspaceSettings",
98
- createJsonRestResourceScopeOptions(workspaceSettingsResource, scopeOptions)
99
- );
100
- }
101
-
102
- function createWorkspacesRuntime({ appRoot = "", config, env, jsonRestApi, users = null } = {}) {
103
- const tenancyProfile = resolveTenancyProfile(config);
104
- const invitationsPolicy = resolveWorkspaceInvitationsPolicy({ config, appConfig: config, tenancyProfile });
105
- const invitationsEnabled = invitationsPolicy.enabled === true;
106
- const roleCatalog = createWorkspaceRoleCatalog(config);
107
- const workspaceSettingsRepository = createWorkspaceSettingsRepository({
108
- api: jsonRestApi,
109
- defaultInvitesEnabled: requireBoolean(
110
- config?.workspaceSettings?.defaults?.invitesEnabled,
111
- "runtime.config.workspaceSettings.defaults.invitesEnabled"
112
- ),
113
- defaultColorSettings: requireWorkspaceColorDefaults(config?.workspaceSettings?.defaults)
114
- });
115
- const workspacesRepository = createWorkspacesRepository({ api: jsonRestApi });
116
- const workspaceMembershipsRepository = createWorkspaceMembershipsRepository({ api: jsonRestApi });
117
- const workspaceInvitesRepository = createWorkspaceInvitesRepository({ api: jsonRestApi });
118
- const workspaceService = createWorkspaceService({
119
- appConfig: config,
120
- workspacesRepository,
121
- workspaceMembershipsRepository,
122
- workspaceSettingsRepository
123
- });
124
- const workspacePendingInvitationsService = createWorkspacePendingInvitationsService({
125
- workspaceInvitesRepository,
126
- workspaceMembershipsRepository,
127
- roleCatalog
128
- });
129
- const workspaceMembersService = createWorkspaceMembersService({
130
- workspaceMembershipsRepository,
131
- workspaceInvitesRepository,
132
- inviteExpiresInMs: requirePositiveInteger(
133
- config?.workspaceMembers?.defaults?.inviteExpiresInMs,
134
- "runtime.config.workspaceMembers.defaults.inviteExpiresInMs"
135
- ),
136
- roleCatalog,
137
- workspaceInvitationsEnabled: invitationsEnabled,
138
- inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: config, env }),
139
- workspaceInviteMailer: config?.workspaceInviteMailer || null,
140
- workspaceInviteEmailTemplate: resolveInviteEmailTemplate(config),
141
- i18n: createWorkspacesServerI18n({ appRoot }),
142
- userProfilesRepository: users?.repositories?.userProfiles || null,
143
- userSettingsRepository: users?.repositories?.userSettings || null
144
- });
145
- const workspaceSettingsService = createWorkspaceSettingsService({
146
- workspaceSettingsRepository,
147
- workspaceInvitationsEnabled: invitationsEnabled,
148
- roleCatalog
149
- });
150
-
151
- return Object.freeze({
152
- config,
153
- enabled: tenancyProfile.workspace.enabled === true,
154
- invitationsEnabled,
155
- selfCreateEnabled: tenancyProfile.workspace.allowSelfCreate === true,
156
- scope: Object.freeze({
157
- params: workspaceSlugParamsValidator,
158
- buildInputFromRouteParams: buildWorkspaceInputFromRouteParams,
159
- resolveWorkspace
160
- }),
161
- tenancyProfile,
162
- repositories: Object.freeze({
163
- workspaceInvites: workspaceInvitesRepository,
164
- workspaceMemberships: workspaceMembershipsRepository,
165
- workspaceSettings: workspaceSettingsRepository,
166
- workspaces: workspacesRepository
167
- }),
168
- services: Object.freeze({
169
- directory: workspaceService,
170
- members: workspaceMembersService,
171
- pendingInvitations: workspacePendingInvitationsService,
172
- settings: workspaceSettingsService
173
- })
174
- });
175
- }
12
+ import { noInviteAcceptanceParticipant } from "./workspacePendingInvitations/noInviteAcceptanceParticipant.js";
13
+ import { buildWorkspaceName } from "./common/services/workspaceDirectoryService.js";
14
+ import { createWorkspaceSettingsActions } from "./workspaceSettings/workspaceSettingsActions.js";
15
+ import { createWorkspacesRuntime, installWorkspaceResources } from "./workspacesRuntime.js";
176
16
 
177
17
  const WorkspacesFeature = defineFeature({
178
18
  id: "workspaces.core",
@@ -186,18 +26,59 @@ const WorkspacesFeature = defineFeature({
186
26
  jsonRestApi: "runtime.json-rest-api",
187
27
  users: "users.core"
188
28
  },
29
+ optional: {
30
+ inviteAcceptanceParticipant: "workspaces.invite-acceptance-participant",
31
+ inviteEmailTemplate: "workspaces.invite-email-template",
32
+ inviteMailer: "workspaces.invite-mailer",
33
+ personalWorkspaceName: "workspaces.personal-workspace-name"
34
+ },
189
35
  provides: {
190
36
  workspaces: "workspaces.core"
191
37
  },
192
- async setup({ appRoot, bootstrap, config, env, http, jsonRestApi, users }) {
38
+ /**
39
+ * @param {object} capabilities
40
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeAppRoot} capabilities.appRoot
41
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeBootstrap} capabilities.bootstrap
42
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeConfig} capabilities.config
43
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} capabilities.env
44
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeHttp} capabilities.http
45
+ * @param {import("./types.js").WorkspaceInviteAcceptanceParticipant | null} capabilities.inviteAcceptanceParticipant
46
+ * @param {import("./types.js").WorkspaceInviteEmailTemplate | null} capabilities.inviteEmailTemplate
47
+ * @param {import("./types.js").WorkspaceInviteMailer | null} capabilities.inviteMailer
48
+ * @param {import("@jskit-ai/json-rest-api-core/server/types").RuntimeJsonRestApi} capabilities.jsonRestApi
49
+ * @param {import("./types.js").PersonalWorkspaceName | null} capabilities.personalWorkspaceName
50
+ * @param {import("@jskit-ai/users-core/server/types").UsersCore} capabilities.users
51
+ */
52
+ async setup({
53
+ appRoot,
54
+ bootstrap,
55
+ config,
56
+ env,
57
+ http,
58
+ inviteAcceptanceParticipant,
59
+ inviteEmailTemplate,
60
+ inviteMailer,
61
+ jsonRestApi,
62
+ personalWorkspaceName,
63
+ users
64
+ }) {
193
65
  await installWorkspaceResources(jsonRestApi);
194
- const workspaces = createWorkspacesRuntime({ appRoot, config, env, jsonRestApi, users });
66
+ const workspaces = createWorkspacesRuntime({
67
+ appRoot,
68
+ config,
69
+ env,
70
+ inviteAcceptanceParticipant: inviteAcceptanceParticipant ?? noInviteAcceptanceParticipant,
71
+ inviteEmailTemplate: inviteEmailTemplate ?? renderDefaultWorkspaceInviteEmail,
72
+ inviteMailer: inviteMailer ?? unconfiguredWorkspaceInviteMailer,
73
+ jsonRestApi,
74
+ personalWorkspaceName: personalWorkspaceName ?? buildWorkspaceName,
75
+ users
76
+ });
195
77
 
196
78
  bootstrap.register({
197
79
  id: "workspaces.bootstrap",
198
80
  order: 200,
199
81
  contribute: createWorkspaceBootstrapContributor({
200
- appConfig: config,
201
82
  tenancyProfile: workspaces.tenancyProfile,
202
83
  userProfilesRepository: users.repositories.userProfiles,
203
84
  workspaceInvitationsEnabled: workspaces.invitationsEnabled,
@@ -224,18 +105,28 @@ const WorkspacesFeature = defineFeature({
224
105
  return { workspaces };
225
106
  },
226
107
  actions({ workspaces }) {
227
- if (!workspaces.enabled) return [];
228
- return [
229
- ...buildWorkspaceDirectoryActions({ workspaceService: workspaces.services.directory }),
230
- ...buildWorkspaceSettingsActions({ workspaceSettingsService: workspaces.services.settings }),
231
- ...buildWorkspaceMembersActions({ workspaceMembersService: workspaces.services.members }),
108
+ if (!workspaces.enabled) return Object.freeze([]);
109
+ return Object.freeze([
110
+ ...createWorkspaceDirectoryActions({ workspaceService: workspaces.services.directory }),
111
+ ...createWorkspaceSettingsActions({ workspaceSettingsService: workspaces.services.settings }),
112
+ ...createWorkspaceMembersActions({ workspaceMembersService: workspaces.services.members }),
232
113
  ...(workspaces.invitationsEnabled
233
- ? buildWorkspacePendingInvitationsActions({
114
+ ? createWorkspacePendingInvitationsActions({
234
115
  workspacePendingInvitationsService: workspaces.services.pendingInvitations
235
116
  })
236
117
  : [])
237
- ];
118
+ ]);
238
119
  }
239
120
  });
240
121
 
241
- export { WorkspacesFeature, createWorkspacesRuntime, installWorkspaceResources };
122
+ // Applications' tests build the runtime and install its resources directly, from
123
+ // this package's public feature path, with the optional tokens' defaults.
124
+ export {
125
+ WorkspacesFeature,
126
+ buildWorkspaceName,
127
+ createWorkspacesRuntime,
128
+ installWorkspaceResources,
129
+ noInviteAcceptanceParticipant,
130
+ renderDefaultWorkspaceInviteEmail,
131
+ unconfiguredWorkspaceInviteMailer
132
+ };
@@ -7,8 +7,12 @@ import {
7
7
  resolveWorkspaceSurfaceIdsFromAppConfig
8
8
  } from "./support/workspaceActionSurfaces.js";
9
9
 
10
- // Every active membership puts the actor in the workspace audience and in its
11
- // own workspace-user audience.
10
+ /**
11
+ * Every active membership puts the actor in the workspace audience and in its
12
+ * own workspace-user audience.
13
+ * @param {import("./types.js").WorkspaceMembershipsRepository} workspaceMemberships
14
+ * @param {string} actorId
15
+ */
12
16
  async function resolveWorkspaceRealtimeScopes(workspaceMemberships, actorId) {
13
17
  const workspaceIds = await workspaceMemberships.listActiveWorkspaceIdsByUserId(actorId);
14
18
  return workspaceIds.flatMap((workspaceId) => [
@@ -30,6 +34,16 @@ const WorkspacesIntegrationsProvider = defineProvider({
30
34
  optional: {
31
35
  realtime: "runtime.realtime"
32
36
  },
37
+ /**
38
+ * @param {object} capabilities
39
+ * @param {import("@jskit-ai/kernel/server/types").ActionCatalogue} capabilities.actions
40
+ * @param {import("@jskit-ai/auth-core/server/types").AuthExtensions} capabilities.authExtensions
41
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeConfig} capabilities.config
42
+ * @param {import("@jskit-ai/kernel/server/types").RuntimeHttp} capabilities.http
43
+ * @param {import("@jskit-ai/realtime/server/types").RuntimeRealtime | null} capabilities.realtime The installed realtime runtime, when there is one.
44
+ * @param {import("@jskit-ai/users-core/server/types").UsersExtensions} capabilities.usersExtensions
45
+ * @param {import("./types.js").WorkspacesCore} capabilities.workspaces
46
+ */
33
47
  setup({ actions, authExtensions, config, http, realtime, usersExtensions, workspaces }) {
34
48
  const workspaceService = workspaces.services.directory;
35
49
  const membershipOptionalSurfaceIds = resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(config);
@@ -66,18 +80,20 @@ const WorkspacesIntegrationsProvider = defineProvider({
66
80
  usersExtensions.registerProfileSyncLifecycleContributor({
67
81
  contributorId: "workspaces.provisioning",
68
82
  order: 100,
69
- async afterIdentityProfileSynced({ profile, options } = {}) {
83
+ async afterIdentityProfileSynced({ profile, options }) {
70
84
  await workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser(profile, options);
71
85
  }
72
86
  });
73
87
 
74
- realtime?.registerAudienceContributor({
75
- id: "workspaces.memberships",
76
- resolveActorScopes: (actorId) => resolveWorkspaceRealtimeScopes(
77
- workspaces.repositories.workspaceMemberships,
78
- actorId
79
- )
80
- });
88
+ if (realtime) {
89
+ realtime.registerAudienceContributor({
90
+ id: "workspaces.memberships",
91
+ resolveActorScopes: (actorId) => resolveWorkspaceRealtimeScopes(
92
+ workspaces.repositories.workspaceMemberships,
93
+ actorId
94
+ )
95
+ });
96
+ }
81
97
 
82
98
  if (workspaces.invitationsEnabled) {
83
99
  authExtensions.registerInvitationContextResolver({