@jskit-ai/workspaces-core 0.1.138-oldline.1 → 0.1.139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +154 -10
- package/patterns/workspace-server/PATTERN.md +60 -0
- package/src/server/WorkspacesFeature.js +226 -0
- package/src/server/WorkspacesIntegrationsProvider.js +74 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +5 -9
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +16 -2
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
- package/src/server/common/support/realtimeServiceEvents.js +102 -61
- package/src/server/support/workspaceActionSurfaces.js +1 -13
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
- package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
- package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
- package/test/exportsContract.test.js +2 -1
- package/test/featureRuntime.test.js +116 -0
- package/test/packageMetadata.test.js +59 -0
- package/test/usersRouteResources.test.js +1 -1
- package/test/workspaceActionContextContributor.test.js +7 -12
- package/test/workspaceAuthPolicyContextResolver.test.js +30 -0
- package/test/workspaceRouteVisibilityResolver.test.js +31 -0
- package/test/workspaceService.test.js +2 -2
- package/test/workspaceSettingsActions.test.js +103 -17
- package/test/workspacesRouteRequestInputValidator.test.js +14 -65
- package/package.descriptor.mjs +0 -352
- package/src/server/WorkspacesCoreServiceProvider.js +0 -64
- package/src/server/registerWorkspaceBootstrap.js +0 -27
- package/src/server/registerWorkspaceCore.js +0 -103
- package/src/server/registerWorkspaceRepositories.js +0 -30
- package/src/server/support/workspaceServerScopeSupport.js +0 -18
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -137
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -130
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -66
- package/test/packageDescriptor.test.js +0 -91
- package/test/registerServiceRealtimeEvents.test.js +0 -116
- package/test/registerWorkspaceBootstrap.test.js +0 -86
- package/test/registerWorkspaceDirectory.test.js +0 -31
- package/test/registerWorkspaceMembers.test.js +0 -140
- package/test/registerWorkspaceSettings.test.js +0 -40
- package/test/workspaceServerScopeSupport.test.js +0 -131
- /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
- /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
- /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
- /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
- /package/{templates → patterns/workspace-server/example}/packages/main/src/server/email/workspaceInviteEmail.js +0 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.139",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
|
-
"./server/
|
|
9
|
+
"./server/WorkspacesFeature": "./src/server/WorkspacesFeature.js",
|
|
10
|
+
"./server/WorkspacesIntegrationsProvider": "./src/server/WorkspacesIntegrationsProvider.js",
|
|
10
11
|
"./server/validators/routeParamsValidator": "./src/server/common/validators/routeParamsValidator.js",
|
|
11
12
|
"./server/support/workspaceRouteInput": "./src/server/support/workspaceRouteInput.js",
|
|
12
13
|
"./shared/jsonApiTransports": "./src/shared/jsonApiTransports.js",
|
|
@@ -17,14 +18,157 @@
|
|
|
17
18
|
"./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@jskit-ai/auth-core": "0.1.
|
|
21
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
22
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
23
|
-
"@jskit-ai/json-rest-api-core": "0.1.
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
25
|
-
"@jskit-ai/resource-
|
|
26
|
-
"@jskit-ai/resource-core": "0.1.
|
|
27
|
-
"@jskit-ai/users-core": "0.1.
|
|
21
|
+
"@jskit-ai/auth-core": "0.1.159",
|
|
22
|
+
"@jskit-ai/database-runtime": "0.1.161",
|
|
23
|
+
"@jskit-ai/http-runtime": "0.1.159",
|
|
24
|
+
"@jskit-ai/json-rest-api-core": "0.1.105",
|
|
25
|
+
"@jskit-ai/kernel": "0.1.161",
|
|
26
|
+
"@jskit-ai/resource-core": "0.1.103",
|
|
27
|
+
"@jskit-ai/resource-crud-core": "0.1.103",
|
|
28
|
+
"@jskit-ai/users-core": "0.1.174",
|
|
28
29
|
"json-rest-schema": "^1.0.17"
|
|
30
|
+
},
|
|
31
|
+
"description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
|
|
32
|
+
"jskit": {
|
|
33
|
+
"kind": "runtime",
|
|
34
|
+
"capabilities": {
|
|
35
|
+
"provides": [
|
|
36
|
+
"workspaces.core"
|
|
37
|
+
],
|
|
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",
|
|
47
|
+
"users.core",
|
|
48
|
+
"users.extensions"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"runtime": {
|
|
52
|
+
"server": {
|
|
53
|
+
"providers": [
|
|
54
|
+
{
|
|
55
|
+
"entrypoint": "src/server/WorkspacesFeature.js",
|
|
56
|
+
"export": "WorkspacesFeature"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"entrypoint": "src/server/WorkspacesIntegrationsProvider.js",
|
|
60
|
+
"export": "WorkspacesIntegrationsProvider"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"client": {
|
|
65
|
+
"providers": []
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"metadata": {
|
|
69
|
+
"jskit": {
|
|
70
|
+
"tableOwnership": {
|
|
71
|
+
"tables": [
|
|
72
|
+
{
|
|
73
|
+
"tableName": "workspaces"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"tableName": "workspace_memberships"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"tableName": "workspace_settings"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"tableName": "workspace_invites"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"apiSummary": {
|
|
88
|
+
"surfaces": [
|
|
89
|
+
{
|
|
90
|
+
"subpath": "./server",
|
|
91
|
+
"summary": "Exports the explicit workspace feature, integrations, services, repositories, actions, and routes."
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"capabilities": [
|
|
95
|
+
"workspaces.core"
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"server": {
|
|
99
|
+
"routes": [
|
|
100
|
+
{
|
|
101
|
+
"method": "POST",
|
|
102
|
+
"path": "/api/workspaces",
|
|
103
|
+
"summary": "Create a workspace for the authenticated user."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"method": "GET",
|
|
107
|
+
"path": "/api/workspaces",
|
|
108
|
+
"summary": "List workspaces visible to authenticated user."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"method": "GET",
|
|
112
|
+
"path": "/api/workspace/invitations/resolve",
|
|
113
|
+
"summary": "Resolve safe public workspace invitation metadata."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"method": "GET",
|
|
117
|
+
"path": "/api/workspace/invitations/pending",
|
|
118
|
+
"summary": "List pending workspace invitations for authenticated user."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"method": "POST",
|
|
122
|
+
"path": "/api/workspace/invitations/redeem",
|
|
123
|
+
"summary": "Accept or refuse a workspace invitation using an invite token."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"method": "GET",
|
|
127
|
+
"path": "/api/w/:workspaceSlug/settings",
|
|
128
|
+
"summary": "Get workspace settings and role catalog by workspace slug."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"method": "PATCH",
|
|
132
|
+
"path": "/api/w/:workspaceSlug/settings",
|
|
133
|
+
"summary": "Update workspace settings by workspace slug."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"method": "GET",
|
|
137
|
+
"path": "/api/w/:workspaceSlug/roles",
|
|
138
|
+
"summary": "Get role catalog by workspace slug."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"method": "GET",
|
|
142
|
+
"path": "/api/w/:workspaceSlug/members",
|
|
143
|
+
"summary": "List members by workspace slug."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"method": "PATCH",
|
|
147
|
+
"path": "/api/w/:workspaceSlug/members/:memberUserId/role",
|
|
148
|
+
"summary": "Update workspace member role by workspace slug."
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"method": "GET",
|
|
152
|
+
"path": "/api/w/:workspaceSlug/invites",
|
|
153
|
+
"summary": "List workspace invites by workspace slug."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"method": "POST",
|
|
157
|
+
"path": "/api/w/:workspaceSlug/invites",
|
|
158
|
+
"summary": "Create workspace invite by workspace slug."
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"method": "DELETE",
|
|
162
|
+
"path": "/api/w/:workspaceSlug/invites/:inviteId",
|
|
163
|
+
"summary": "Revoke workspace invite by workspace slug."
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"migrations": {
|
|
169
|
+
"directories": [
|
|
170
|
+
"migrations"
|
|
171
|
+
]
|
|
172
|
+
}
|
|
29
173
|
}
|
|
30
174
|
}
|
|
@@ -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 };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
2
|
+
import { createWorkspaceActionContextContributor } from "./common/contributors/workspaceActionContextContributor.js";
|
|
3
|
+
import { createWorkspaceAuthPolicyContextResolver } from "./common/contributors/workspaceAuthPolicyContextResolver.js";
|
|
4
|
+
import { createWorkspaceRouteVisibilityResolver } from "./common/contributors/workspaceRouteVisibilityResolver.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig,
|
|
7
|
+
resolveWorkspaceSurfaceIdsFromAppConfig
|
|
8
|
+
} from "./support/workspaceActionSurfaces.js";
|
|
9
|
+
|
|
10
|
+
const WorkspacesIntegrationsProvider = defineProvider({
|
|
11
|
+
id: "workspaces.integrations",
|
|
12
|
+
requires: {
|
|
13
|
+
actions: "runtime.actions",
|
|
14
|
+
authExtensions: "auth.extensions",
|
|
15
|
+
config: "runtime.config",
|
|
16
|
+
http: "runtime.http",
|
|
17
|
+
usersExtensions: "users.extensions",
|
|
18
|
+
workspaces: "workspaces.core"
|
|
19
|
+
},
|
|
20
|
+
setup({ actions, authExtensions, config, http, usersExtensions, workspaces }) {
|
|
21
|
+
const workspaceService = workspaces.services.directory;
|
|
22
|
+
const membershipOptionalSurfaceIds = resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(config);
|
|
23
|
+
const workspaceSurfaceIds = resolveWorkspaceSurfaceIdsFromAppConfig(config);
|
|
24
|
+
|
|
25
|
+
const actionContext = createWorkspaceActionContextContributor({
|
|
26
|
+
workspaceMembershipOptionalSurfaceIds: membershipOptionalSurfaceIds,
|
|
27
|
+
workspaceService,
|
|
28
|
+
workspaceSurfaceIds
|
|
29
|
+
});
|
|
30
|
+
actions.registerContextContributor({
|
|
31
|
+
id: actionContext.contributorId,
|
|
32
|
+
contribute: actionContext.contribute
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const authPolicyResolver = createWorkspaceAuthPolicyContextResolver({
|
|
36
|
+
workspaceMembershipOptionalSurfaceIds: membershipOptionalSurfaceIds,
|
|
37
|
+
workspaceService
|
|
38
|
+
});
|
|
39
|
+
authExtensions.registerPolicyContextResolver({
|
|
40
|
+
resolverId: "workspaces.policy",
|
|
41
|
+
resolveAuthPolicyContext: authPolicyResolver
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const visibility = createWorkspaceRouteVisibilityResolver({
|
|
45
|
+
workspaceMembershipOptionalSurfaceIds: membershipOptionalSurfaceIds,
|
|
46
|
+
workspaceService
|
|
47
|
+
});
|
|
48
|
+
http.registerVisibilityResolver({
|
|
49
|
+
id: visibility.resolverId,
|
|
50
|
+
resolve: visibility.resolve
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
usersExtensions.registerProfileSyncLifecycleContributor({
|
|
54
|
+
contributorId: "workspaces.provisioning",
|
|
55
|
+
order: 100,
|
|
56
|
+
async afterIdentityProfileSynced({ profile, options } = {}) {
|
|
57
|
+
await workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser(profile, options);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (workspaces.invitationsEnabled) {
|
|
62
|
+
authExtensions.registerInvitationContextResolver({
|
|
63
|
+
resolverId: "workspaces.invitation",
|
|
64
|
+
resolveInvitationContext(invitation, options = {}) {
|
|
65
|
+
return workspaces.services.pendingInvitations.resolveInviteContextForAuth(invitation, options);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export { WorkspacesIntegrationsProvider };
|
|
@@ -45,21 +45,20 @@ function createWorkspaceActionContextContributor({
|
|
|
45
45
|
|
|
46
46
|
return Object.freeze({
|
|
47
47
|
contributorId,
|
|
48
|
-
async contribute({ definition = null, input, context
|
|
48
|
+
async contribute({ definition = null, input, context } = {}) {
|
|
49
49
|
const payload = normalizeObject(input);
|
|
50
50
|
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
51
51
|
return {};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const request = context?.requestMeta?.request || null;
|
|
54
55
|
const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
55
56
|
const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
|
|
56
57
|
const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
57
|
-
const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(
|
|
58
|
+
const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(context?.surface);
|
|
58
59
|
const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
|
|
59
60
|
const routeVisibilityInput =
|
|
60
|
-
|
|
61
|
-
? request.routeOptions.config.visibility
|
|
62
|
-
: ROUTE_VISIBILITY_PUBLIC;
|
|
61
|
+
context?.routeVisibility ?? request?.routeOptions?.config?.visibility ?? ROUTE_VISIBILITY_PUBLIC;
|
|
63
62
|
const routeVisibility = checkRouteVisibility(routeVisibilityInput);
|
|
64
63
|
const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
|
|
65
64
|
if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
|
|
@@ -67,10 +66,7 @@ function createWorkspaceActionContextContributor({
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
const resolveOptions = { request };
|
|
70
|
-
if (
|
|
71
|
-
!hasWorkspaceRouteVisibility &&
|
|
72
|
-
workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)
|
|
73
|
-
) {
|
|
69
|
+
if (workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)) {
|
|
74
70
|
resolveOptions.requireMembership = false;
|
|
75
71
|
}
|
|
76
72
|
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
2
3
|
|
|
3
|
-
function createWorkspaceAuthPolicyContextResolver({
|
|
4
|
+
function createWorkspaceAuthPolicyContextResolver({
|
|
5
|
+
workspaceService,
|
|
6
|
+
workspaceMembershipOptionalSurfaceIds = []
|
|
7
|
+
} = {}) {
|
|
4
8
|
if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
|
|
5
9
|
throw new Error(
|
|
6
10
|
"workspace auth policy context resolver requires workspaceService.resolveWorkspaceContextForUserBySlug()."
|
|
7
11
|
);
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
const membershipOptionalSurfaceIds = new Set(
|
|
15
|
+
(Array.isArray(workspaceMembershipOptionalSurfaceIds) ? workspaceMembershipOptionalSurfaceIds : [])
|
|
16
|
+
.map((entry) => normalizeSurfaceId(entry))
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
);
|
|
19
|
+
|
|
10
20
|
return async function resolveWorkspaceAuthPolicyContext({ request, actor, meta } = {}) {
|
|
11
21
|
const contextPolicy = normalizeText(meta?.contextPolicy || "none").toLowerCase() || "none";
|
|
12
22
|
const permission = normalizeText(meta?.permission);
|
|
@@ -20,7 +30,11 @@ function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
|
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
const resolveOptions = { request };
|
|
23
|
-
|
|
33
|
+
const activeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
34
|
+
if (
|
|
35
|
+
(contextPolicy === "optional" && !permission) ||
|
|
36
|
+
membershipOptionalSurfaceIds.has(activeSurfaceId)
|
|
37
|
+
) {
|
|
24
38
|
resolveOptions.requireMembership = false;
|
|
25
39
|
}
|
|
26
40
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeOpaqueId, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
2
3
|
|
|
3
4
|
function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId = null } = {}) {
|
|
4
5
|
const requiresActorScope = visibility === "workspace_user";
|
|
@@ -17,11 +18,20 @@ function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId =
|
|
|
17
18
|
return contribution;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function createWorkspaceRouteVisibilityResolver({
|
|
21
|
+
function createWorkspaceRouteVisibilityResolver({
|
|
22
|
+
workspaceService,
|
|
23
|
+
workspaceMembershipOptionalSurfaceIds = []
|
|
24
|
+
} = {}) {
|
|
21
25
|
if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
|
|
22
26
|
throw new Error("workspace route visibility resolver requires workspaceService.resolveWorkspaceContextForUserBySlug().");
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
const membershipOptionalSurfaceIds = new Set(
|
|
30
|
+
(Array.isArray(workspaceMembershipOptionalSurfaceIds) ? workspaceMembershipOptionalSurfaceIds : [])
|
|
31
|
+
.map((entry) => normalizeSurfaceId(entry))
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
);
|
|
34
|
+
|
|
25
35
|
return Object.freeze({
|
|
26
36
|
resolverId: "workspaces.visibility",
|
|
27
37
|
async resolve({ visibility, context, request, input } = {}) {
|
|
@@ -46,9 +56,17 @@ function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
|
|
|
46
56
|
: {};
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
const
|
|
50
|
-
request
|
|
51
|
-
|
|
59
|
+
const activeSurfaceId = normalizeSurfaceId(
|
|
60
|
+
request?.routeOptions?.config?.surface || context?.surface
|
|
61
|
+
);
|
|
62
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
63
|
+
actor,
|
|
64
|
+
workspaceSlug,
|
|
65
|
+
{
|
|
66
|
+
request,
|
|
67
|
+
...(membershipOptionalSurfaceIds.has(activeSurfaceId) ? { requireMembership: false } : {})
|
|
68
|
+
}
|
|
69
|
+
);
|
|
52
70
|
const resolvedWorkspaceOwnerId = normalizeRecordId(resolvedWorkspaceContext?.workspace?.id, { fallback: null });
|
|
53
71
|
if (!resolvedWorkspaceOwnerId) {
|
|
54
72
|
return visibility === "workspace_user"
|