@jskit-ai/workspaces-core 0.2.3 → 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.
- package/locales/en.json +2 -2
- package/package.json +38 -11
- package/patterns/workspace-server/PATTERN.md +42 -17
- package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
- package/src/server/WorkspacesFeature.js +74 -183
- package/src/server/WorkspacesIntegrationsProvider.js +26 -10
- package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
- package/src/server/common/formatters/workspaceFormatter.js +12 -10
- package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
- package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
- package/src/server/common/repositories/workspacesRepository.js +17 -25
- package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
- package/src/server/common/support/realtimeServiceEvents.js +20 -12
- package/src/server/common/support/workspaceRecordId.js +21 -0
- package/src/server/common/validators/routeParamsValidator.js +3 -2
- package/src/server/i18n.js +5 -2
- package/src/server/support/resolveWorkspace.js +10 -7
- package/src/server/support/workspaceActionSurfaces.js +5 -41
- package/src/server/support/workspaceCrud.js +25 -0
- package/src/server/support/workspaceInvitationsPolicy.js +16 -27
- package/src/server/support/workspaceRouteInput.js +19 -2
- package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
- package/src/server/types.js +47 -0
- package/src/server/workspaceBootstrapContributor.js +105 -142
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
- package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
- package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
- package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
- package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
- package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
- package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
- package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
- package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
- package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
- package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
- package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
- package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
- package/src/server/workspacesRuntime.js +185 -0
- package/src/shared/operationMessages.js +6 -15
- package/src/shared/resources/workspaceMembersResource.js +2 -2
- package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
- package/src/shared/resources/workspaceResource.js +4 -1
- package/src/shared/roles.js +15 -12
- package/src/shared/tenancyProfile.js +2 -2
- package/test/exportsContract.test.js +5 -0
- package/test/featureRuntime.test.js +5 -6
- package/test/packageMetadata.test.js +12 -0
- package/test/repositories.integration.test.js +1 -1
- package/test/requestedWorkspaceScope.test.js +24 -0
- package/test/roleLabels.test.js +29 -7
- package/test/support/invitationTransactions.js +35 -19
- package/test/workspaceActionSurfaces.test.js +0 -55
- package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
- package/test/workspaceBootstrapContributor.test.js +7 -6
- package/test/workspaceCrud.test.js +23 -0
- package/test/workspaceInvitesRepository.test.js +9 -8
- package/test/workspaceMembersService.test.js +131 -28
- package/test/workspacePendingInvitationsResource.test.js +2 -2
- package/test/workspacePendingInvitationsService.test.js +69 -22
- package/test/workspaceService.test.js +58 -47
- package/test/workspaceSettingsActions.test.js +14 -7
- package/test/workspaceSettingsRepository.test.js +11 -0
- package/test/workspaceSettingsService.test.js +11 -9
- package/test/workspacesRepository.test.js +16 -16
- package/test/workspacesRouteRequestInputValidator.test.js +4 -4
- package/src/server/common/repositories/repositoryUtils.js +0 -43
- package/src/server/common/support/deepFreeze.js +0 -1
- 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.
|
|
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.
|
|
23
|
-
"@jskit-ai/json-rest-api-core": "0.2.
|
|
24
|
-
"@jskit-ai/resource-core": "0.1.
|
|
25
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
26
|
-
"@jskit-ai/users-core": "0.2.
|
|
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
|
|
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.
|
|
175
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
176
|
-
"@jskit-ai/kernel": "0.2.
|
|
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
|
|
28
|
-
`
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`fn({ invite, user }, { trx, context })`
|
|
70
|
-
expiry and activating membership in the
|
|
71
|
-
to every application owner and propagate
|
|
72
|
-
commits after the
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
`
|
|
120
|
-
|
|
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
|
|
package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js
ADDED
|
@@ -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 };
|
package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
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}.`;
|
package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js
ADDED
|
@@ -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/
|
|
21
|
-
import {
|
|
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 {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
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
|
-
|
|
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({
|
|
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
|
-
...
|
|
230
|
-
...
|
|
231
|
-
...
|
|
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
|
-
?
|
|
114
|
+
? createWorkspacePendingInvitationsActions({
|
|
234
115
|
workspacePendingInvitationsService: workspaces.services.pendingInvitations
|
|
235
116
|
})
|
|
236
117
|
: [])
|
|
237
|
-
];
|
|
118
|
+
]);
|
|
238
119
|
}
|
|
239
120
|
});
|
|
240
121
|
|
|
241
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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({
|