@jskit-ai/workspaces-core 0.1.137 → 0.1.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +38 -238
  2. package/patterns/workspace-server/PATTERN.md +60 -0
  3. package/src/server/WorkspacesFeature.js +226 -0
  4. package/src/server/WorkspacesIntegrationsProvider.js +74 -0
  5. package/src/server/common/contributors/workspaceActionContextContributor.js +18 -6
  6. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +25 -4
  7. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
  8. package/src/server/common/services/workspaceContextService.js +7 -2
  9. package/src/server/common/support/realtimeServiceEvents.js +102 -61
  10. package/src/server/support/workspaceActionSurfaces.js +18 -13
  11. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
  12. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
  13. package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
  14. package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
  15. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
  16. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
  17. package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
  18. package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
  19. package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
  20. package/test/exportsContract.test.js +2 -1
  21. package/test/featureRuntime.test.js +116 -0
  22. package/test/packageMetadata.test.js +20 -54
  23. package/test/workspaceActionContextContributor.test.js +49 -8
  24. package/test/workspaceActionSurfaces.test.js +36 -1
  25. package/test/workspaceAuthPolicyContextResolver.test.js +61 -0
  26. package/test/workspaceRouteVisibilityResolver.test.js +31 -0
  27. package/test/workspaceService.test.js +62 -0
  28. package/test/workspaceSettingsActions.test.js +102 -16
  29. package/test/workspacesRouteRequestInputValidator.test.js +14 -65
  30. package/src/server/WorkspacesCoreServiceProvider.js +0 -64
  31. package/src/server/registerWorkspaceBootstrap.js +0 -27
  32. package/src/server/registerWorkspaceCore.js +0 -101
  33. package/src/server/registerWorkspaceRepositories.js +0 -30
  34. package/src/server/support/workspaceServerScopeSupport.js +0 -18
  35. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
  36. package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -137
  37. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -130
  38. package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -66
  39. package/test/registerServiceRealtimeEvents.test.js +0 -116
  40. package/test/registerWorkspaceBootstrap.test.js +0 -86
  41. package/test/registerWorkspaceDirectory.test.js +0 -31
  42. package/test/registerWorkspaceMembers.test.js +0 -140
  43. package/test/registerWorkspaceSettings.test.js +0 -40
  44. package/test/workspaceServerScopeSupport.test.js +0 -131
  45. /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
  46. /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
  47. /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
  48. /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
  49. /package/{templates → patterns/workspace-server/example}/packages/main/src/server/email/workspaceInviteEmail.js +0 -0
@@ -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({ workspaceService, workspaceSurfaceIds = [] } = {}) {
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, request } = {}) {
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
- request && request.routeOptions && request.routeOptions.config
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
- { request }
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({ workspaceService } = {}) {
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 resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
23
- request
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({ workspaceService } = {}) {
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 resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
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 { deepFreeze } from "./deepFreeze.js";
4
+ import { resolveWorkspace } from "../../support/resolveWorkspace.js";
3
5
 
4
- function resolveActorScopedEntityId({ options } = {}) {
5
- return normalizeRecordId(options?.context?.actor?.id, { fallback: "" });
6
+ function resultValue({ result } = {}) {
7
+ return result?.value ?? result ?? {};
6
8
  }
7
9
 
8
- function resolveWorkspaceSlugPayload({ args } = {}) {
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(args?.[0]?.slug || "").trim()
20
+ workspaceSlug: String(
21
+ resolveWorkspace(execution.context, execution.input)?.slug || execution.input?.workspaceSlug || ""
22
+ ).trim()
11
23
  };
12
24
  }
13
25
 
14
- const ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS = deepFreeze([
15
- {
16
- type: "entity.changed",
17
- source: "account",
18
- entity: "settings",
19
- operation: "updated",
20
- entityId: resolveActorScopedEntityId,
21
- realtime: {
22
- event: "account.settings.changed",
23
- audience: "actor_user"
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
- workspaceEntityId = ({ args }) => args?.[0]?.id,
44
- bootstrapEntityId = ({ args }) => args?.[0]?.id,
45
+ bootstrapEntityId = workspaceId,
45
46
  bootstrapAudience = "event_scope"
46
47
  } = {}) {
47
- const normalizedWorkspaceEntity = String(workspaceEntity || "").trim();
48
- const normalizedWorkspaceOperation = String(workspaceOperation || "")
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: normalizedWorkspaceEntity,
69
- operation: normalizedWorkspaceOperation,
70
- entityId: (payload = {}) => normalizeRecordId(workspaceEntityId(payload), { fallback: "" }),
51
+ entity: workspaceEntity,
52
+ operation: workspaceOperation,
53
+ entityId: workspaceId,
71
54
  realtime: {
72
- event: normalizedWorkspaceRealtimeEvent,
73
- payload: resolveWorkspaceSlugPayload,
74
- audience: "event_scope"
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: (payload = {}) => normalizeRecordId(bootstrapEntityId(payload), { fallback: "" }),
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 { ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS, createWorkspaceEntityAndBootstrapEvents };
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 bootWorkspaceDirectoryRoutes(app) {
29
- if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
30
- throw new Error("bootWorkspaceDirectoryRoutes requires application make()/has().");
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 { bootWorkspaceDirectoryRoutes };
154
+ export { registerWorkspaceDirectoryRoutes };
@@ -16,13 +16,13 @@ const workspaceUpdateInputValidator = composeSchemaDefinitions([
16
16
  context: "workspaceDirectoryActions.workspaceUpdateInputValidator"
17
17
  });
18
18
 
19
- const workspaceDirectoryActions = Object.freeze([
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
- surfacesFrom: "enabled",
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 execute(input, context, deps) {
42
- return returnJsonApiData(await deps.workspaceService.createWorkspaceForAuthenticatedUser(resolveActionUser(context, input), input, {
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
- surfacesFrom: "enabled",
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 execute(input, context, deps) {
64
+ async run(workspaceService, input, context) {
65
65
  return returnJsonApiData({
66
- items: await deps.workspaceService.listWorkspacesForAuthenticatedUser(resolveActionUser(context, input), {
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
- surfacesFrom: "workspace",
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 execute(input, context, deps) {
92
- return returnJsonApiData(await deps.workspaceService.getWorkspaceForAuthenticatedUser(
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
- surfacesFrom: "workspace",
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 execute(input, context, deps) {
124
+ async run(workspaceService, input, context) {
125
125
  const { workspaceSlug, ...patch } = input;
126
- return returnJsonApiData(await deps.workspaceService.updateWorkspaceForAuthenticatedUser(
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
- export { workspaceDirectoryActions };
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 bootWorkspaceMembers(app) {
32
- if (!app || typeof app.make !== "function") {
33
- throw new Error("bootWorkspaceMembers requires application make().");
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 { bootWorkspaceMembers };
267
+ export { registerWorkspaceMembersRoutes };