@jskit-ai/workspaces-core 0.1.137 → 0.1.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +38 -238
- 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 +18 -6
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +25 -4
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
- package/src/server/common/services/workspaceContextService.js +7 -2
- package/src/server/common/support/realtimeServiceEvents.js +102 -61
- package/src/server/support/workspaceActionSurfaces.js +18 -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 +20 -54
- package/test/workspaceActionContextContributor.test.js +49 -8
- package/test/workspaceActionSurfaces.test.js +36 -1
- package/test/workspaceAuthPolicyContextResolver.test.js +61 -0
- package/test/workspaceRouteVisibilityResolver.test.js +31 -0
- package/test/workspaceService.test.js +62 -0
- package/test/workspaceSettingsActions.test.js +102 -16
- package/test/workspacesRouteRequestInputValidator.test.js +14 -65
- package/src/server/WorkspacesCoreServiceProvider.js +0 -64
- package/src/server/registerWorkspaceBootstrap.js +0 -27
- package/src/server/registerWorkspaceCore.js +0 -101
- 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/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
|
@@ -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 };
|
|
@@ -30,38 +30,50 @@ function normalizeWorkspaceSurfaceIds(surfaceIds = []) {
|
|
|
30
30
|
return normalized;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function createWorkspaceActionContextContributor({
|
|
33
|
+
function createWorkspaceActionContextContributor({
|
|
34
|
+
workspaceService,
|
|
35
|
+
workspaceMembershipOptionalSurfaceIds = [],
|
|
36
|
+
workspaceSurfaceIds = []
|
|
37
|
+
} = {}) {
|
|
34
38
|
const contributorId = "workspaces.context";
|
|
39
|
+
const workspaceMembershipOptionalSurfaceIdSet = normalizeWorkspaceSurfaceIds(
|
|
40
|
+
workspaceMembershipOptionalSurfaceIds
|
|
41
|
+
);
|
|
35
42
|
const workspaceSurfaceIdSet = normalizeWorkspaceSurfaceIds(workspaceSurfaceIds);
|
|
36
43
|
|
|
37
44
|
requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
|
|
38
45
|
|
|
39
46
|
return Object.freeze({
|
|
40
47
|
contributorId,
|
|
41
|
-
async contribute({ definition = null, input, context
|
|
48
|
+
async contribute({ definition = null, input, context } = {}) {
|
|
42
49
|
const payload = normalizeObject(input);
|
|
43
50
|
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
44
51
|
return {};
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
const request = context?.requestMeta?.request || null;
|
|
47
55
|
const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
48
56
|
const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
|
|
49
57
|
const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
58
|
+
const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(context?.surface);
|
|
50
59
|
const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
|
|
51
60
|
const routeVisibilityInput =
|
|
52
|
-
|
|
53
|
-
? request.routeOptions.config.visibility
|
|
54
|
-
: ROUTE_VISIBILITY_PUBLIC;
|
|
61
|
+
context?.routeVisibility ?? request?.routeOptions?.config?.visibility ?? ROUTE_VISIBILITY_PUBLIC;
|
|
55
62
|
const routeVisibility = checkRouteVisibility(routeVisibilityInput);
|
|
56
63
|
const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
|
|
57
64
|
if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
|
|
58
65
|
return {};
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
const resolveOptions = { request };
|
|
69
|
+
if (workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)) {
|
|
70
|
+
resolveOptions.requireMembership = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
62
74
|
resolveActionUser(context, payload),
|
|
63
75
|
payload.workspaceSlug,
|
|
64
|
-
|
|
76
|
+
resolveOptions
|
|
65
77
|
);
|
|
66
78
|
|
|
67
79
|
const contribution = {
|
|
@@ -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);
|
|
@@ -19,9 +29,20 @@ function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
|
|
|
19
29
|
return {};
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
const resolveOptions = { request };
|
|
33
|
+
const activeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
34
|
+
if (
|
|
35
|
+
(contextPolicy === "optional" && !permission) ||
|
|
36
|
+
membershipOptionalSurfaceIds.has(activeSurfaceId)
|
|
37
|
+
) {
|
|
38
|
+
resolveOptions.requireMembership = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
42
|
+
actor,
|
|
43
|
+
workspaceSlug,
|
|
44
|
+
resolveOptions
|
|
45
|
+
);
|
|
25
46
|
|
|
26
47
|
return {
|
|
27
48
|
workspace: resolvedWorkspaceContext?.workspace || null,
|
|
@@ -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"
|
|
@@ -215,6 +215,7 @@ function createService({
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
|
|
218
|
+
const requireMembership = options?.requireMembership !== false;
|
|
218
219
|
const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
|
|
219
220
|
if (!normalizedUserId) {
|
|
220
221
|
throw new AppError(401, "Authentication required.");
|
|
@@ -249,12 +250,16 @@ function createService({
|
|
|
249
250
|
membership = await workspaceMembershipsRepository.ensureOwnerMembership(workspace.id, normalizedUserId, options);
|
|
250
251
|
}
|
|
251
252
|
|
|
252
|
-
if (!membership || normalizeLowerText(membership.status) !== "active") {
|
|
253
|
+
if ((!membership || normalizeLowerText(membership.status) !== "active") && requireMembership) {
|
|
253
254
|
throw new AppError(403, "You do not have access to this workspace.");
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
if (!membership || normalizeLowerText(membership.status) !== "active") {
|
|
258
|
+
membership = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
256
261
|
const workspaceSettings = await ensureWorkspaceSettingsForWorkspace(workspace, options);
|
|
257
|
-
const permissions = buildPermissionsFromMembership(membership, appConfig);
|
|
262
|
+
const permissions = membership ? buildPermissionsFromMembership(membership, appConfig) : [];
|
|
258
263
|
|
|
259
264
|
return {
|
|
260
265
|
workspace,
|
|
@@ -1,91 +1,132 @@
|
|
|
1
|
+
import { normalizeDbRecordId } from "@jskit-ai/database-runtime/shared";
|
|
2
|
+
import { createEntityChangedActionEvent } from "@jskit-ai/kernel/server/actions";
|
|
1
3
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
-
import {
|
|
4
|
+
import { resolveWorkspace } from "../../support/resolveWorkspace.js";
|
|
3
5
|
|
|
4
|
-
function
|
|
5
|
-
return
|
|
6
|
+
function resultValue({ result } = {}) {
|
|
7
|
+
return result?.value ?? result ?? {};
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
function
|
|
10
|
+
function actorId({ context } = {}) {
|
|
11
|
+
return context?.actor?.id;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function workspaceId(execution = {}) {
|
|
15
|
+
return resolveWorkspace(execution.context, execution.input)?.id || resultValue(execution)?.workspaceId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function workspaceSlugPayload(execution = {}) {
|
|
9
19
|
return {
|
|
10
|
-
workspaceSlug: String(
|
|
20
|
+
workspaceSlug: String(
|
|
21
|
+
resolveWorkspace(execution.context, execution.input)?.slug || execution.input?.workspaceSlug || ""
|
|
22
|
+
).trim()
|
|
11
23
|
};
|
|
12
24
|
}
|
|
13
25
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
type: "entity.changed",
|
|
28
|
-
source: "users",
|
|
29
|
-
entity: "bootstrap",
|
|
30
|
-
operation: "updated",
|
|
31
|
-
entityId: resolveActorScopedEntityId,
|
|
32
|
-
realtime: {
|
|
33
|
-
event: "users.bootstrap.changed",
|
|
34
|
-
audience: "actor_user"
|
|
35
|
-
}
|
|
26
|
+
const INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE = Object.freeze({
|
|
27
|
+
preset: "event_scope",
|
|
28
|
+
async userQuery({ knex, event } = {}) {
|
|
29
|
+
if (typeof knex !== "function") return [];
|
|
30
|
+
const inviteId = normalizeRecordId(event?.entityId, { fallback: null });
|
|
31
|
+
if (!inviteId) return [];
|
|
32
|
+
const row = await knex("workspace_invites as wi")
|
|
33
|
+
.join("users as up", "up.email", "wi.email")
|
|
34
|
+
.where("wi.id", inviteId)
|
|
35
|
+
.first("up.id as user_id");
|
|
36
|
+
const userId = normalizeDbRecordId(row?.user_id, { fallback: null });
|
|
37
|
+
return userId ? [{ userId }] : [];
|
|
36
38
|
}
|
|
37
|
-
|
|
39
|
+
});
|
|
38
40
|
|
|
39
41
|
function createWorkspaceEntityAndBootstrapEvents({
|
|
40
42
|
workspaceEntity,
|
|
41
43
|
workspaceOperation,
|
|
42
44
|
workspaceRealtimeEvent,
|
|
43
|
-
|
|
44
|
-
bootstrapEntityId = ({ args }) => args?.[0]?.id,
|
|
45
|
+
bootstrapEntityId = workspaceId,
|
|
45
46
|
bootstrapAudience = "event_scope"
|
|
46
47
|
} = {}) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.trim()
|
|
50
|
-
.toLowerCase();
|
|
51
|
-
const normalizedWorkspaceRealtimeEvent = String(workspaceRealtimeEvent || "").trim();
|
|
52
|
-
if (!normalizedWorkspaceEntity || !normalizedWorkspaceOperation || !normalizedWorkspaceRealtimeEvent) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
"createWorkspaceEntityAndBootstrapEvents requires workspaceEntity, workspaceOperation, and workspaceRealtimeEvent."
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
if (typeof workspaceEntityId !== "function") {
|
|
58
|
-
throw new Error("createWorkspaceEntityAndBootstrapEvents requires workspaceEntityId to be a function.");
|
|
59
|
-
}
|
|
60
|
-
if (typeof bootstrapEntityId !== "function") {
|
|
61
|
-
throw new Error("createWorkspaceEntityAndBootstrapEvents requires bootstrapEntityId to be a function.");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return deepFreeze([
|
|
65
|
-
{
|
|
66
|
-
type: "entity.changed",
|
|
48
|
+
return Object.freeze([
|
|
49
|
+
createEntityChangedActionEvent({
|
|
67
50
|
source: "workspace",
|
|
68
|
-
entity:
|
|
69
|
-
operation:
|
|
70
|
-
entityId:
|
|
51
|
+
entity: workspaceEntity,
|
|
52
|
+
operation: workspaceOperation,
|
|
53
|
+
entityId: workspaceId,
|
|
71
54
|
realtime: {
|
|
72
|
-
event:
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
event: workspaceRealtimeEvent,
|
|
56
|
+
audience: "event_scope",
|
|
57
|
+
payload: workspaceSlugPayload
|
|
75
58
|
}
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
type: "entity.changed",
|
|
59
|
+
}),
|
|
60
|
+
createEntityChangedActionEvent({
|
|
79
61
|
source: "users",
|
|
80
62
|
entity: "bootstrap",
|
|
81
63
|
operation: "updated",
|
|
82
|
-
entityId:
|
|
64
|
+
entityId: bootstrapEntityId,
|
|
83
65
|
realtime: {
|
|
84
66
|
event: "users.bootstrap.changed",
|
|
85
67
|
audience: bootstrapAudience
|
|
86
68
|
}
|
|
69
|
+
})
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createActorEvent({ source, entity, realtimeEvent }) {
|
|
74
|
+
return createEntityChangedActionEvent({
|
|
75
|
+
source,
|
|
76
|
+
entity,
|
|
77
|
+
operation: "updated",
|
|
78
|
+
entityId: actorId,
|
|
79
|
+
realtime: { event: realtimeEvent, audience: "actor_user" }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createWorkspaceAudienceEvent({ entity, realtimeEvent }) {
|
|
84
|
+
return createEntityChangedActionEvent({
|
|
85
|
+
source: "workspace",
|
|
86
|
+
entity,
|
|
87
|
+
operation: "updated",
|
|
88
|
+
entityId: ({ result }) => result?.value?.workspaceId ?? result?.workspaceId,
|
|
89
|
+
realtime: {
|
|
90
|
+
event: realtimeEvent,
|
|
91
|
+
audience: ({ result }) => ({ workspaceId: result?.value?.workspaceId ?? result?.workspaceId })
|
|
87
92
|
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function onlyAccepted(builder) {
|
|
97
|
+
return (execution) => execution.input?.decision === "accept" ? builder(execution) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createInviteDecisionEvents() {
|
|
101
|
+
return Object.freeze([
|
|
102
|
+
createActorEvent({
|
|
103
|
+
source: "workspace",
|
|
104
|
+
entity: "invitation",
|
|
105
|
+
realtimeEvent: "workspace.invitations.pending.changed"
|
|
106
|
+
}),
|
|
107
|
+
createActorEvent({
|
|
108
|
+
source: "users",
|
|
109
|
+
entity: "bootstrap",
|
|
110
|
+
realtimeEvent: "users.bootstrap.changed"
|
|
111
|
+
}),
|
|
112
|
+
onlyAccepted(createActorEvent({
|
|
113
|
+
source: "workspace",
|
|
114
|
+
entity: "directory",
|
|
115
|
+
realtimeEvent: "workspaces.changed"
|
|
116
|
+
})),
|
|
117
|
+
onlyAccepted(createWorkspaceAudienceEvent({
|
|
118
|
+
entity: "member",
|
|
119
|
+
realtimeEvent: "workspace.members.changed"
|
|
120
|
+
})),
|
|
121
|
+
createWorkspaceAudienceEvent({
|
|
122
|
+
entity: "invite",
|
|
123
|
+
realtimeEvent: "workspace.invites.changed"
|
|
124
|
+
})
|
|
88
125
|
]);
|
|
89
126
|
}
|
|
90
127
|
|
|
91
|
-
export {
|
|
128
|
+
export {
|
|
129
|
+
INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE,
|
|
130
|
+
createInviteDecisionEvents,
|
|
131
|
+
createWorkspaceEntityAndBootstrapEvents
|
|
132
|
+
};
|
|
@@ -23,6 +23,22 @@ function resolveWorkspaceSurfaceIdsFromAppConfig(appConfig = {}) {
|
|
|
23
23
|
return resolveSurfaceIdsFromAppConfig(appConfig, (definition) => definition.requiresWorkspace === true);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(appConfig = {}) {
|
|
27
|
+
const policies = isRecord(appConfig?.surfaceAccessPolicies) ? appConfig.surfaceAccessPolicies : {};
|
|
28
|
+
|
|
29
|
+
return resolveSurfaceIdsFromAppConfig(appConfig, (definition) => {
|
|
30
|
+
if (definition.requiresWorkspace !== true) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const policyId = String(definition.accessPolicyId || "")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase();
|
|
37
|
+
const policy = policyId && isRecord(policies[policyId]) ? policies[policyId] : {};
|
|
38
|
+
return Object.hasOwn(policy, "requireWorkspaceMembership") && policy.requireWorkspaceMembership === false;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
function resolveSurfaceIdsFromAppConfig(appConfig = {}, predicate) {
|
|
27
43
|
const source = isRecord(appConfig?.surfaceDefinitions) ? appConfig.surfaceDefinitions : {};
|
|
28
44
|
const resolved = [];
|
|
@@ -73,17 +89,6 @@ function materializeWorkspaceActionSurfaces(actions = [], { workspaceSurfaceIds
|
|
|
73
89
|
return Object.freeze(materialized.map((entry) => Object.freeze({ ...entry })));
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
function registerWorkspaceActionSurfaceSources(app) {
|
|
77
|
-
if (!app || typeof app.actionSurfaceSource !== "function") {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
app.actionSurfaceSource("workspace", ({ scope }) => {
|
|
82
|
-
const appConfig = scope?.has?.("appConfig") ? scope.make("appConfig") : {};
|
|
83
|
-
return resolveWorkspaceSurfaceIdsFromAppConfig(appConfig);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
92
|
function materializeWorkspaceActionSurfacesFromAppConfig(actions = [], { appConfig = {} } = {}) {
|
|
88
93
|
const workspaceSurfaceIds = resolveWorkspaceSurfaceIdsFromAppConfig(appConfig);
|
|
89
94
|
return materializeWorkspaceActionSurfaces(actions, { workspaceSurfaceIds });
|
|
@@ -111,8 +116,8 @@ function resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig = {}) {
|
|
|
111
116
|
|
|
112
117
|
export {
|
|
113
118
|
resolveWorkspaceSurfaceIdsFromAppConfig,
|
|
119
|
+
resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig,
|
|
114
120
|
resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig,
|
|
115
121
|
materializeWorkspaceActionSurfaces,
|
|
116
|
-
materializeWorkspaceActionSurfacesFromAppConfig
|
|
117
|
-
registerWorkspaceActionSurfaceSources
|
|
122
|
+
materializeWorkspaceActionSurfacesFromAppConfig
|
|
118
123
|
};
|
|
@@ -25,17 +25,11 @@ function resolveWorkspaceRecordId(record = {}, context = {}) {
|
|
|
25
25
|
throw new Error("Workspace JSON:API response requires workspace id.");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function
|
|
29
|
-
if (!
|
|
30
|
-
throw new
|
|
28
|
+
function registerWorkspaceDirectoryRoutes(router, { config = {}, workspaceSelfCreateEnabled = false } = {}) {
|
|
29
|
+
if (!router || typeof router.register !== "function") {
|
|
30
|
+
throw new TypeError("registerWorkspaceDirectoryRoutes requires router.register().");
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
const router = app.make("jskit.http.router");
|
|
34
|
-
const appConfig = app.has("appConfig") ? app.make("appConfig") : {};
|
|
35
|
-
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
|
|
36
|
-
const workspaceSelfCreateEnabled = app.has("workspaces.self-create.enabled")
|
|
37
|
-
? app.make("workspaces.self-create.enabled") === true
|
|
38
|
-
: false;
|
|
32
|
+
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(config);
|
|
39
33
|
|
|
40
34
|
if (workspaceSelfCreateEnabled) {
|
|
41
35
|
router.register(
|
|
@@ -157,4 +151,4 @@ function bootWorkspaceDirectoryRoutes(app) {
|
|
|
157
151
|
);
|
|
158
152
|
}
|
|
159
153
|
|
|
160
|
-
export {
|
|
154
|
+
export { registerWorkspaceDirectoryRoutes };
|
|
@@ -16,13 +16,13 @@ const workspaceUpdateInputValidator = composeSchemaDefinitions([
|
|
|
16
16
|
context: "workspaceDirectoryActions.workspaceUpdateInputValidator"
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const workspaceDirectoryActionSpecifications = Object.freeze([
|
|
20
20
|
{
|
|
21
21
|
id: "workspace.workspaces.create",
|
|
22
22
|
version: 1,
|
|
23
23
|
kind: "command",
|
|
24
24
|
channels: ["api", "assistant_tool", "automation", "internal"],
|
|
25
|
-
|
|
25
|
+
surfaces: ["*"],
|
|
26
26
|
permission: {
|
|
27
27
|
require: "authenticated"
|
|
28
28
|
},
|
|
@@ -38,8 +38,8 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
38
38
|
description: "Create a workspace for the authenticated user."
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
|
-
async
|
|
42
|
-
return returnJsonApiData(await
|
|
41
|
+
async run(workspaceService, input, context) {
|
|
42
|
+
return returnJsonApiData(await workspaceService.createWorkspaceForAuthenticatedUser(resolveActionUser(context, input), input, {
|
|
43
43
|
request: resolveRequest(context),
|
|
44
44
|
context
|
|
45
45
|
}));
|
|
@@ -50,7 +50,7 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
50
50
|
version: 1,
|
|
51
51
|
kind: "query",
|
|
52
52
|
channels: ["api", "automation", "internal"],
|
|
53
|
-
|
|
53
|
+
surfaces: ["*"],
|
|
54
54
|
permission: {
|
|
55
55
|
require: "authenticated"
|
|
56
56
|
},
|
|
@@ -61,9 +61,9 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
61
61
|
actionName: "workspace.workspaces.list"
|
|
62
62
|
},
|
|
63
63
|
observability: {},
|
|
64
|
-
async
|
|
64
|
+
async run(workspaceService, input, context) {
|
|
65
65
|
return returnJsonApiData({
|
|
66
|
-
items: await
|
|
66
|
+
items: await workspaceService.listWorkspacesForAuthenticatedUser(resolveActionUser(context, input), {
|
|
67
67
|
request: resolveRequest(context),
|
|
68
68
|
context
|
|
69
69
|
}),
|
|
@@ -76,7 +76,7 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
76
76
|
version: 1,
|
|
77
77
|
kind: "query",
|
|
78
78
|
channels: ["api", "automation", "internal"],
|
|
79
|
-
|
|
79
|
+
surfaces: ["*"],
|
|
80
80
|
permission: {
|
|
81
81
|
require: "any",
|
|
82
82
|
permissions: ["workspace.settings.view", "workspace.settings.update"]
|
|
@@ -88,8 +88,8 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
88
88
|
actionName: "workspace.workspaces.read"
|
|
89
89
|
},
|
|
90
90
|
observability: {},
|
|
91
|
-
async
|
|
92
|
-
return returnJsonApiData(await
|
|
91
|
+
async run(workspaceService, input, context) {
|
|
92
|
+
return returnJsonApiData(await workspaceService.getWorkspaceForAuthenticatedUser(
|
|
93
93
|
resolveActionUser(context, input),
|
|
94
94
|
input.workspaceSlug,
|
|
95
95
|
{
|
|
@@ -104,7 +104,7 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
104
104
|
version: 1,
|
|
105
105
|
kind: "command",
|
|
106
106
|
channels: ["api", "assistant_tool", "automation", "internal"],
|
|
107
|
-
|
|
107
|
+
surfaces: ["*"],
|
|
108
108
|
permission: {
|
|
109
109
|
require: "all",
|
|
110
110
|
permissions: ["workspace.settings.update"]
|
|
@@ -121,9 +121,9 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
121
121
|
description: "Update workspace profile fields."
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
|
-
async
|
|
124
|
+
async run(workspaceService, input, context) {
|
|
125
125
|
const { workspaceSlug, ...patch } = input;
|
|
126
|
-
return returnJsonApiData(await
|
|
126
|
+
return returnJsonApiData(await workspaceService.updateWorkspaceForAuthenticatedUser(
|
|
127
127
|
resolveActionUser(context, input),
|
|
128
128
|
workspaceSlug,
|
|
129
129
|
patch,
|
|
@@ -136,4 +136,14 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
136
136
|
}
|
|
137
137
|
]);
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
function buildWorkspaceDirectoryActions({ workspaceService } = {}) {
|
|
140
|
+
if (!workspaceService) throw new TypeError("buildWorkspaceDirectoryActions requires workspaceService.");
|
|
141
|
+
return workspaceDirectoryActionSpecifications.map(({ run, ...definition }) => Object.freeze({
|
|
142
|
+
...definition,
|
|
143
|
+
execute(input, context) {
|
|
144
|
+
return run(workspaceService, input, context);
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { workspaceDirectoryActionSpecifications, buildWorkspaceDirectoryActions };
|
|
@@ -28,18 +28,11 @@ function resolveWorkspaceAggregateRecordId(record = {}, context = {}) {
|
|
|
28
28
|
throw new Error("Workspace JSON:API response requires workspace id.");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function
|
|
32
|
-
if (!
|
|
33
|
-
throw new
|
|
31
|
+
function registerWorkspaceMembersRoutes(router, { config = {}, workspaceInvitationsEnabled = false } = {}) {
|
|
32
|
+
if (!router || typeof router.register !== "function") {
|
|
33
|
+
throw new TypeError("registerWorkspaceMembersRoutes requires router.register().");
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
const router = app.make("jskit.http.router");
|
|
37
|
-
const appConfig = typeof app.has === "function" && app.has("appConfig") ? app.make("appConfig") : {};
|
|
38
|
-
const workspaceInvitationsEnabled =
|
|
39
|
-
typeof app.has === "function" && app.has("workspaces.invitations.enabled")
|
|
40
|
-
? app.make("workspaces.invitations.enabled") === true
|
|
41
|
-
: false;
|
|
42
|
-
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
|
|
35
|
+
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(config);
|
|
43
36
|
|
|
44
37
|
router.register(
|
|
45
38
|
"GET",
|
|
@@ -271,4 +264,4 @@ function bootWorkspaceMembers(app) {
|
|
|
271
264
|
}
|
|
272
265
|
}
|
|
273
266
|
|
|
274
|
-
export {
|
|
267
|
+
export { registerWorkspaceMembersRoutes };
|