@jskit-ai/workspaces-core 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/locales/en.json +2 -2
- package/package.json +38 -11
- package/patterns/workspace-server/PATTERN.md +42 -17
- package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
- package/src/server/WorkspacesFeature.js +74 -183
- package/src/server/WorkspacesIntegrationsProvider.js +26 -10
- package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
- package/src/server/common/formatters/workspaceFormatter.js +12 -10
- package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
- package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
- package/src/server/common/repositories/workspacesRepository.js +17 -25
- package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
- package/src/server/common/support/realtimeServiceEvents.js +20 -12
- package/src/server/common/support/workspaceRecordId.js +21 -0
- package/src/server/common/validators/routeParamsValidator.js +3 -2
- package/src/server/i18n.js +5 -2
- package/src/server/support/resolveWorkspace.js +10 -7
- package/src/server/support/workspaceActionSurfaces.js +5 -41
- package/src/server/support/workspaceCrud.js +25 -0
- package/src/server/support/workspaceInvitationsPolicy.js +16 -27
- package/src/server/support/workspaceRouteInput.js +19 -2
- package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
- package/src/server/types.js +47 -0
- package/src/server/workspaceBootstrapContributor.js +105 -142
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
- package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
- package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
- package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
- package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
- package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
- package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
- package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
- package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
- package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
- package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
- package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
- package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
- package/src/server/workspacesRuntime.js +185 -0
- package/src/shared/operationMessages.js +6 -15
- package/src/shared/resources/workspaceMembersResource.js +2 -2
- package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
- package/src/shared/resources/workspaceResource.js +4 -1
- package/src/shared/roles.js +15 -12
- package/src/shared/tenancyProfile.js +2 -2
- package/test/exportsContract.test.js +5 -0
- package/test/featureRuntime.test.js +5 -6
- package/test/packageMetadata.test.js +12 -0
- package/test/repositories.integration.test.js +1 -1
- package/test/requestedWorkspaceScope.test.js +24 -0
- package/test/roleLabels.test.js +29 -7
- package/test/support/invitationTransactions.js +35 -19
- package/test/workspaceActionSurfaces.test.js +0 -55
- package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
- package/test/workspaceBootstrapContributor.test.js +7 -6
- package/test/workspaceCrud.test.js +23 -0
- package/test/workspaceInvitesRepository.test.js +9 -8
- package/test/workspaceMembersService.test.js +131 -28
- package/test/workspacePendingInvitationsResource.test.js +2 -2
- package/test/workspacePendingInvitationsService.test.js +69 -22
- package/test/workspaceService.test.js +58 -47
- package/test/workspaceSettingsActions.test.js +14 -7
- package/test/workspaceSettingsRepository.test.js +11 -0
- package/test/workspaceSettingsService.test.js +11 -9
- package/test/workspacesRepository.test.js +16 -16
- package/test/workspacesRouteRequestInputValidator.test.js +4 -4
- package/src/server/common/repositories/repositoryUtils.js +0 -43
- package/src/server/common/support/deepFreeze.js +0 -1
- package/src/server/common/support/resolveActionUser.js +0 -9
|
@@ -24,7 +24,7 @@ function resolveWorkspaceSurfaceIdsFromAppConfig(appConfig = {}) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(appConfig = {}) {
|
|
27
|
-
const policies = isRecord(appConfig
|
|
27
|
+
const policies = isRecord(appConfig.surfaceAccessPolicies) ? appConfig.surfaceAccessPolicies : {};
|
|
28
28
|
|
|
29
29
|
return resolveSurfaceIdsFromAppConfig(appConfig, (definition) => {
|
|
30
30
|
if (definition.requiresWorkspace !== true) {
|
|
@@ -40,7 +40,7 @@ function resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(appConfig = {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function resolveSurfaceIdsFromAppConfig(appConfig = {}, predicate) {
|
|
43
|
-
const source = isRecord(appConfig
|
|
43
|
+
const source = isRecord(appConfig.surfaceDefinitions) ? appConfig.surfaceDefinitions : {};
|
|
44
44
|
const resolved = [];
|
|
45
45
|
|
|
46
46
|
for (const [key, value] of Object.entries(source)) {
|
|
@@ -60,46 +60,12 @@ function resolveSurfaceIdsFromAppConfig(appConfig = {}, predicate) {
|
|
|
60
60
|
return normalizeSurfaceIds(resolved);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function materializeWorkspaceActionSurfaces(actions = [], { workspaceSurfaceIds = [] } = {}) {
|
|
64
|
-
const sourceActions = Array.isArray(actions) ? actions : [];
|
|
65
|
-
const resolvedWorkspaceSurfaceIds = normalizeSurfaceIds(workspaceSurfaceIds);
|
|
66
|
-
const materialized = [];
|
|
67
|
-
|
|
68
|
-
for (const entry of sourceActions) {
|
|
69
|
-
const action = isRecord(entry) ? entry : {};
|
|
70
|
-
const surfacesFrom = String(action.surfacesFrom || "")
|
|
71
|
-
.trim()
|
|
72
|
-
.toLowerCase();
|
|
73
|
-
if (surfacesFrom !== "workspace") {
|
|
74
|
-
materialized.push(action);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (resolvedWorkspaceSurfaceIds.length < 1) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const { surfacesFrom: _ignored, ...rest } = action;
|
|
83
|
-
materialized.push({
|
|
84
|
-
...rest,
|
|
85
|
-
surfaces: [...resolvedWorkspaceSurfaceIds]
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return Object.freeze(materialized.map((entry) => Object.freeze({ ...entry })));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function materializeWorkspaceActionSurfacesFromAppConfig(actions = [], { appConfig = {} } = {}) {
|
|
93
|
-
const workspaceSurfaceIds = resolveWorkspaceSurfaceIdsFromAppConfig(appConfig);
|
|
94
|
-
return materializeWorkspaceActionSurfaces(actions, { workspaceSurfaceIds });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
63
|
function resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig = {}) {
|
|
98
64
|
const workspaceSurfaceIds = resolveWorkspaceSurfaceIdsFromAppConfig(appConfig);
|
|
99
65
|
const workspaceSurfaceSet = new Set(workspaceSurfaceIds);
|
|
100
66
|
|
|
101
67
|
const resolvedSurfaceId = resolveDefaultWorkspaceSurfaceId({
|
|
102
|
-
defaultSurfaceId: appConfig
|
|
68
|
+
defaultSurfaceId: appConfig.surfaceDefaultId,
|
|
103
69
|
workspaceSurfaceIds,
|
|
104
70
|
surfaceRequiresWorkspace(surfaceId) {
|
|
105
71
|
return workspaceSurfaceSet.has(surfaceId);
|
|
@@ -109,7 +75,7 @@ function resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig = {}) {
|
|
|
109
75
|
return (
|
|
110
76
|
normalizeSurfaceId(resolvedSurfaceId) ||
|
|
111
77
|
normalizeSurfaceId(workspaceSurfaceIds[0]) ||
|
|
112
|
-
normalizeSurfaceId(appConfig
|
|
78
|
+
normalizeSurfaceId(appConfig.surfaceDefaultId) ||
|
|
113
79
|
""
|
|
114
80
|
);
|
|
115
81
|
}
|
|
@@ -117,7 +83,5 @@ function resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig = {}) {
|
|
|
117
83
|
export {
|
|
118
84
|
resolveWorkspaceSurfaceIdsFromAppConfig,
|
|
119
85
|
resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig,
|
|
120
|
-
resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig
|
|
121
|
-
materializeWorkspaceActionSurfaces,
|
|
122
|
-
materializeWorkspaceActionSurfacesFromAppConfig
|
|
86
|
+
resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig
|
|
123
87
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { routeParamsValidator, workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
2
|
+
import { buildWorkspaceInputFromRouteParams } from "./workspaceRouteInput.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The workspace scope's input: the workspace slug in the request's route params.
|
|
6
|
+
* @param {{ input: { params: Record<string, unknown> } }} request
|
|
7
|
+
*/
|
|
8
|
+
function workspaceCrudScopeInput(request) {
|
|
9
|
+
return buildWorkspaceInputFromRouteParams(request.input.params);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The scope a workspace CRUD feature hands defineCrudJsonApiFeature: its routes
|
|
14
|
+
* live under /w/:workspaceSlug, and the workspace slug is the scope's only input.
|
|
15
|
+
* @type {import("@jskit-ai/crud-core/server/types").CrudScope}
|
|
16
|
+
*/
|
|
17
|
+
const WORKSPACE_CRUD_SCOPE = Object.freeze({
|
|
18
|
+
routeBase: "/w/:workspaceSlug",
|
|
19
|
+
actionInputValidator: workspaceSlugParamsValidator,
|
|
20
|
+
routeParamsValidator,
|
|
21
|
+
inputKeys: Object.freeze(["workspaceSlug"]),
|
|
22
|
+
input: workspaceCrudScopeInput
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export { WORKSPACE_CRUD_SCOPE };
|
|
@@ -1,33 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
TENANCY_MODE_PERSONAL,
|
|
4
|
-
normalizeTenancyMode
|
|
5
|
-
} from "../../shared/tenancyMode.js";
|
|
1
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { TENANCY_MODE_PERSONAL } from "../../shared/tenancyMode.js";
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
: null;
|
|
11
|
-
const normalizedSource = source && typeof source === "object" && !Array.isArray(source)
|
|
12
|
-
? source
|
|
13
|
-
: {};
|
|
4
|
+
/** @param {import("@jskit-ai/kernel/server/types").RuntimeConfig} appConfig */
|
|
5
|
+
function normalizeWorkspaceInvitationsConfig(appConfig) {
|
|
6
|
+
const source = isRecord(appConfig.workspaceInvitations) ? appConfig.workspaceInvitations : {};
|
|
14
7
|
|
|
15
8
|
return Object.freeze({
|
|
16
|
-
enabled:
|
|
17
|
-
allowInPersonalMode:
|
|
9
|
+
enabled: source.enabled !== false,
|
|
10
|
+
allowInPersonalMode: source.allowInPersonalMode !== false
|
|
18
11
|
});
|
|
19
12
|
}
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeConfig} options.appConfig
|
|
17
|
+
* @param {import("../types.js").WorkspaceTenancyProfile} options.tenancyProfile
|
|
18
|
+
*/
|
|
19
|
+
function resolveWorkspaceInvitationsPolicy({ appConfig, tenancyProfile }) {
|
|
25
20
|
const config = normalizeWorkspaceInvitationsConfig(appConfig);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
: {};
|
|
29
|
-
const tenancyMode = normalizeTenancyMode(normalizedTenancyProfile.mode || appConfig?.tenancyMode);
|
|
30
|
-
const workspaceEnabled = normalizedTenancyProfile?.workspace?.enabled === true || tenancyMode !== TENANCY_MODE_NONE;
|
|
21
|
+
const tenancyMode = tenancyProfile.mode;
|
|
22
|
+
const workspaceEnabled = tenancyProfile.workspace.enabled === true;
|
|
31
23
|
const enabledForTenancyMode = tenancyMode !== TENANCY_MODE_PERSONAL || config.allowInPersonalMode === true;
|
|
32
24
|
const enabled = config.enabled === true && workspaceEnabled && enabledForTenancyMode;
|
|
33
25
|
|
|
@@ -39,7 +31,4 @@ function resolveWorkspaceInvitationsPolicy({
|
|
|
39
31
|
});
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
export {
|
|
43
|
-
normalizeWorkspaceInvitationsConfig,
|
|
44
|
-
resolveWorkspaceInvitationsPolicy
|
|
45
|
-
};
|
|
34
|
+
export { resolveWorkspaceInvitationsPolicy };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { BOOTSTRAP_PATH } from "@jskit-ai/kernel/shared/surface/paths";
|
|
2
3
|
|
|
3
4
|
function readWorkspaceSlugFromRouteParams(params = {}) {
|
|
4
|
-
const workspaceSlug = normalizeText(params
|
|
5
|
+
const workspaceSlug = normalizeText(params.workspaceSlug).toLowerCase();
|
|
5
6
|
return workspaceSlug || "";
|
|
6
7
|
}
|
|
7
8
|
|
|
@@ -16,7 +17,23 @@ function buildWorkspaceInputFromRouteParams(params = {}) {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The workspace a request asks for, for code that runs before the workspace action context contributor, such as an
|
|
22
|
+
* auth service decorator: the route's `workspaceSlug` param, else the bootstrap request's `workspaceSlug` query.
|
|
23
|
+
* It reads what the router matched and parsed, never the URL. `{}` when the request names no workspace.
|
|
24
|
+
* @param {{ params?: Record<string, unknown>, query?: Record<string, unknown>, routeOptions?: { url?: string } }} request
|
|
25
|
+
* @returns {{ workspaceSlug?: string }}
|
|
26
|
+
*/
|
|
27
|
+
function requestedWorkspaceScope(request) {
|
|
28
|
+
const routeInput = buildWorkspaceInputFromRouteParams(request.params || {});
|
|
29
|
+
if (routeInput.workspaceSlug || request.routeOptions?.url !== BOOTSTRAP_PATH) {
|
|
30
|
+
return routeInput;
|
|
31
|
+
}
|
|
32
|
+
return buildWorkspaceInputFromRouteParams({ workspaceSlug: request.query?.workspaceSlug });
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export {
|
|
20
36
|
readWorkspaceSlugFromRouteParams,
|
|
21
|
-
buildWorkspaceInputFromRouteParams
|
|
37
|
+
buildWorkspaceInputFromRouteParams,
|
|
38
|
+
requestedWorkspaceScope
|
|
22
39
|
};
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
|
|
2
2
|
import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
3
3
|
|
|
4
|
+
/** The API base of the workspace routes, /api/w/:workspaceSlug. */
|
|
4
5
|
const WORKSPACE_ROUTE_BASE_PATH = resolveScopedApiBasePath({
|
|
5
6
|
routeBase: "/w/:workspaceSlug",
|
|
6
7
|
strictParams: false
|
|
7
8
|
});
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A workspace route's path: relativePath under /api/w/:workspaceSlug; the
|
|
12
|
+
* base itself when relativePath is left out or empty.
|
|
13
|
+
* @param {string} [relativePath]
|
|
14
|
+
*/
|
|
15
|
+
function resolveWorkspaceRoutePath(relativePath) {
|
|
16
|
+
const normalizedRelativePath = normalizePathname(relativePath);
|
|
11
17
|
if (normalizedRelativePath === "/") {
|
|
12
18
|
return WORKSPACE_ROUTE_BASE_PATH;
|
|
13
19
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The type of the `workspaces.core` capability, for features that require it.
|
|
2
|
+
|
|
3
|
+
/** @typedef {ReturnType<typeof import("./workspacesRuntime.js").createWorkspacesRuntime>} WorkspacesCore */
|
|
4
|
+
|
|
5
|
+
/** @typedef {ReturnType<typeof import("./common/services/workspaceDirectoryService.js").createService>} WorkspaceDirectoryService The workspace directory service: `services.directory` of `workspaces.core`. */
|
|
6
|
+
/** @typedef {ReturnType<typeof import("./workspaceMembers/workspaceMembersService.js").createService>} WorkspaceMembersService The workspace members service: `services.members` of `workspaces.core`. */
|
|
7
|
+
/** @typedef {ReturnType<typeof import("./workspacePendingInvitations/workspacePendingInvitationsService.js").createService>} WorkspacePendingInvitationsService The pending invitations service: `services.pendingInvitations` of `workspaces.core`. */
|
|
8
|
+
/** @typedef {ReturnType<typeof import("./workspaceSettings/workspaceSettingsService.js").createService>} WorkspaceSettingsService The workspace settings service: `services.settings` of `workspaces.core`. */
|
|
9
|
+
|
|
10
|
+
/** @typedef {ReturnType<typeof import("./common/repositories/workspacesRepository.js").createRepository>} WorkspacesRepository */
|
|
11
|
+
/** @typedef {ReturnType<typeof import("./common/repositories/workspaceMembershipsRepository.js").createRepository>} WorkspaceMembershipsRepository */
|
|
12
|
+
/** @typedef {ReturnType<typeof import("./common/repositories/workspaceInvitesRepository.js").createRepository>} WorkspaceInvitesRepository */
|
|
13
|
+
/** @typedef {ReturnType<typeof import("./workspaceSettings/workspaceSettingsRepository.js").createRepository>} WorkspaceSettingsRepository */
|
|
14
|
+
/** @typedef {ReturnType<typeof import("../shared/roles.js").createWorkspaceRoleCatalog>} WorkspaceRoleCatalog */
|
|
15
|
+
/** @typedef {ReturnType<typeof import("../shared/tenancyProfile.js").resolveTenancyProfile>} WorkspaceTenancyProfile */
|
|
16
|
+
/** @typedef {ReturnType<typeof import("./workspaceMembers/workspaceInviteUrls.js").createWorkspaceInviteUrlBuilder>} WorkspaceInviteUrlBuilder The invitation URL a token opens, from the app's public URL and invite path. */
|
|
17
|
+
/** @typedef {ReturnType<typeof import("./i18n.js").createWorkspacesServerI18n>} WorkspacesServerI18n The workspace texts rendered on the server, in a recipient's language. */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The `workspaces.invite-mailer` capability: a feature that provides it sends
|
|
21
|
+
* workspace invitation email. Its result is the delivery outcome; the status
|
|
22
|
+
* defaults to "sent".
|
|
23
|
+
* @typedef {object} WorkspaceInviteMailer
|
|
24
|
+
* @property {(message: { email: string, inviteUrl: string, workspace: object, inviter: object | null, roleSid: string, expiresAt: string, locale: string, message: object }, options?: object) => Promise<{ status?: string, message?: string, messageCode?: string, messageParams?: object, providerMessageId?: string } | void>} sendWorkspaceInvite
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The `workspaces.invite-email-template` capability: renders an invitation
|
|
29
|
+
* email's `{ subject, text, html }`.
|
|
30
|
+
* @typedef {(input: { email: string, inviteUrl: string, workspace: object, inviter: object | null, roleSid: string, roleLabel?: string, expiresAt: string, locale: string, t: (key: string, params?: object) => string }) => object | Promise<object>} WorkspaceInviteEmailTemplate
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The `workspaces.invite-acceptance-participant` capability: the application's
|
|
35
|
+
* writes for an accepted invitation. It runs after the membership is activated,
|
|
36
|
+
* inside the uncommitted acceptance transaction (`trx`), and a failure rolls the
|
|
37
|
+
* acceptance back.
|
|
38
|
+
* @typedef {(acceptance: { invite: object, user: object }, options: { trx: unknown, context?: object }) => Promise<void>} WorkspaceInviteAcceptanceParticipant
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The `workspaces.personal-workspace-name` capability: names the personal
|
|
43
|
+
* workspace provisioned for a user. An empty name falls back to the default.
|
|
44
|
+
* @typedef {(user: object) => string} PersonalWorkspaceName
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export {};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
TENANCY_MODE_NONE,
|
|
6
|
-
resolveTenancyProfile
|
|
7
|
-
} from "../shared/tenancyProfile.js";
|
|
1
|
+
import { normalizeLowerText, normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
3
|
+
import { TENANCY_MODE_NONE } from "../shared/tenancyProfile.js";
|
|
8
4
|
import {
|
|
9
5
|
mapMembershipSummary,
|
|
10
6
|
mapWorkspaceSettingsPublic,
|
|
@@ -16,25 +12,18 @@ const REQUESTED_WORKSPACE_STATUS_NOT_FOUND = "not_found";
|
|
|
16
12
|
const REQUESTED_WORKSPACE_STATUS_FORBIDDEN = "forbidden";
|
|
17
13
|
const REQUESTED_WORKSPACE_STATUS_UNAUTHENTICATED = "unauthenticated";
|
|
18
14
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function resolveBootstrapWorkspaceSlug({ query = {}, request = null } = {}) {
|
|
27
|
-
const normalizedQuery = normalizeQueryPayload(query);
|
|
15
|
+
function resolveBootstrapWorkspaceSlug({ query, request }) {
|
|
16
|
+
const normalizedQuery = normalizeObjectInput(query);
|
|
28
17
|
if (Object.hasOwn(normalizedQuery, "workspaceSlug")) {
|
|
29
18
|
return normalizeLowerText(normalizedQuery.workspaceSlug);
|
|
30
19
|
}
|
|
31
20
|
|
|
32
|
-
const normalizedInputQuery =
|
|
21
|
+
const normalizedInputQuery = normalizeObjectInput(request?.input?.query);
|
|
33
22
|
if (Object.hasOwn(normalizedInputQuery, "workspaceSlug")) {
|
|
34
23
|
return normalizeLowerText(normalizedInputQuery.workspaceSlug);
|
|
35
24
|
}
|
|
36
25
|
|
|
37
|
-
const normalizedRequestQuery =
|
|
26
|
+
const normalizedRequestQuery = normalizeObjectInput(request?.query);
|
|
38
27
|
if (Object.hasOwn(normalizedRequestQuery, "workspaceSlug")) {
|
|
39
28
|
return normalizeLowerText(normalizedRequestQuery.workspaceSlug);
|
|
40
29
|
}
|
|
@@ -42,7 +31,7 @@ function resolveBootstrapWorkspaceSlug({ query = {}, request = null } = {}) {
|
|
|
42
31
|
return "";
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
function normalizeRequestedWorkspaceStatus(value
|
|
34
|
+
function normalizeRequestedWorkspaceStatus(value) {
|
|
46
35
|
const normalizedValue = normalizeLowerText(value);
|
|
47
36
|
if (
|
|
48
37
|
normalizedValue === REQUESTED_WORKSPACE_STATUS_RESOLVED ||
|
|
@@ -55,7 +44,7 @@ function normalizeRequestedWorkspaceStatus(value = "") {
|
|
|
55
44
|
return "";
|
|
56
45
|
}
|
|
57
46
|
|
|
58
|
-
function
|
|
47
|
+
function buildRequestedWorkspacePayload(workspaceSlug, status) {
|
|
59
48
|
const normalizedWorkspaceSlug = normalizeLowerText(workspaceSlug);
|
|
60
49
|
const normalizedStatus = normalizeRequestedWorkspaceStatus(status);
|
|
61
50
|
if (!normalizedWorkspaceSlug || !normalizedStatus) {
|
|
@@ -81,144 +70,118 @@ function resolveRequestedWorkspaceStatusFromError(error) {
|
|
|
81
70
|
return "";
|
|
82
71
|
}
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
slugPolicy: fallback.workspace.slugPolicy
|
|
93
|
-
})
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
73
|
+
/**
|
|
74
|
+
* @param {object} dependencies
|
|
75
|
+
* @param {import("./types.js").WorkspaceDirectoryService} dependencies.workspaceService
|
|
76
|
+
* @param {import("./types.js").WorkspacePendingInvitationsService} dependencies.workspacePendingInvitationsService
|
|
77
|
+
* @param {import("@jskit-ai/users-core/server/types").UserProfilesRepository} dependencies.userProfilesRepository
|
|
78
|
+
* @param {boolean} dependencies.workspaceInvitationsEnabled
|
|
79
|
+
* @param {import("./types.js").WorkspaceTenancyProfile} dependencies.tenancyProfile
|
|
80
|
+
*/
|
|
97
81
|
function createWorkspaceBootstrapContributor({
|
|
98
82
|
workspaceService,
|
|
99
83
|
workspacePendingInvitationsService,
|
|
100
84
|
userProfilesRepository,
|
|
101
|
-
workspaceInvitationsEnabled
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
serviceLabel: "workspacePendingInvitationsService"
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
requireServiceMethod(userProfilesRepository, "findById", contributorId, {
|
|
120
|
-
serviceLabel: "internal.repository.user-profiles"
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return Object.freeze({
|
|
124
|
-
contributorId,
|
|
125
|
-
order: 200,
|
|
126
|
-
async contribute({ request = null, query = {}, payload = {} } = {}) {
|
|
127
|
-
const inheritedAppState = payload?.app && typeof payload.app === "object" ? payload.app : {};
|
|
128
|
-
const inheritedFeatures =
|
|
129
|
-
inheritedAppState.features && typeof inheritedAppState.features === "object" ? inheritedAppState.features : {};
|
|
130
|
-
const normalizedUserId = normalizeRecordId(
|
|
131
|
-
payload?.session?.authenticated === true ? payload?.session?.userId : null,
|
|
132
|
-
{ fallback: null }
|
|
133
|
-
);
|
|
134
|
-
const normalizedWorkspaceSlug = resolveBootstrapWorkspaceSlug({ query, request });
|
|
135
|
-
if (!normalizedUserId) {
|
|
136
|
-
if (!normalizedWorkspaceSlug || resolvedTenancyProfile.mode === TENANCY_MODE_NONE) {
|
|
137
|
-
return {};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
tenancy: resolvedTenancyProfile,
|
|
142
|
-
app: {
|
|
143
|
-
...inheritedAppState,
|
|
144
|
-
features: {
|
|
145
|
-
...inheritedFeatures,
|
|
146
|
-
workspaceSwitching: normalizeLowerText(resolvedTenancyProfile.mode) !== TENANCY_MODE_NONE,
|
|
147
|
-
workspaceInvites: workspaceInvitationsEnabled === true
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
requestedWorkspace: createRequestedWorkspacePayload(
|
|
151
|
-
normalizedWorkspaceSlug,
|
|
152
|
-
REQUESTED_WORKSPACE_STATUS_UNAUTHENTICATED
|
|
153
|
-
)
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const latestProfile = await userProfilesRepository.findById(normalizedUserId);
|
|
158
|
-
if (!latestProfile) {
|
|
85
|
+
workspaceInvitationsEnabled,
|
|
86
|
+
tenancyProfile
|
|
87
|
+
}) {
|
|
88
|
+
/** @param {{ request?: object, query?: object, payload: object }} bootstrap The kernel bootstrap's request, query and payload so far. */
|
|
89
|
+
async function contribute({ request, query, payload }) {
|
|
90
|
+
const inheritedAppState = payload.app && typeof payload.app === "object" ? payload.app : {};
|
|
91
|
+
const inheritedFeatures =
|
|
92
|
+
inheritedAppState.features && typeof inheritedAppState.features === "object" ? inheritedAppState.features : {};
|
|
93
|
+
const normalizedUserId = normalizeRecordId(
|
|
94
|
+
payload.session?.authenticated === true ? payload.session?.userId : null,
|
|
95
|
+
{ fallback: null }
|
|
96
|
+
);
|
|
97
|
+
const normalizedWorkspaceSlug = resolveBootstrapWorkspaceSlug({ query, request });
|
|
98
|
+
if (!normalizedUserId) {
|
|
99
|
+
if (!normalizedWorkspaceSlug || tenancyProfile.mode === TENANCY_MODE_NONE) {
|
|
159
100
|
return {};
|
|
160
101
|
}
|
|
161
102
|
|
|
162
|
-
const pendingInvites =
|
|
163
|
-
workspaceInvitationsEnabled
|
|
164
|
-
? await workspacePendingInvitationsService.listPendingInvitesForUser(latestProfile, {
|
|
165
|
-
context: {
|
|
166
|
-
actor: latestProfile
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
|
-
: [];
|
|
170
|
-
const workspaces = await workspaceService.listWorkspacesForUser(latestProfile, { request });
|
|
171
|
-
let workspaceContext = null;
|
|
172
|
-
let requestedWorkspace = null;
|
|
173
|
-
if (normalizedWorkspaceSlug && resolvedTenancyProfile.mode !== TENANCY_MODE_NONE) {
|
|
174
|
-
try {
|
|
175
|
-
workspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
176
|
-
latestProfile,
|
|
177
|
-
normalizedWorkspaceSlug,
|
|
178
|
-
{ request }
|
|
179
|
-
);
|
|
180
|
-
requestedWorkspace = createRequestedWorkspacePayload(
|
|
181
|
-
normalizedWorkspaceSlug,
|
|
182
|
-
REQUESTED_WORKSPACE_STATUS_RESOLVED
|
|
183
|
-
);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
const requestedWorkspaceStatus = resolveRequestedWorkspaceStatusFromError(error);
|
|
186
|
-
if (!requestedWorkspaceStatus) {
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
requestedWorkspace = createRequestedWorkspacePayload(normalizedWorkspaceSlug, requestedWorkspaceStatus);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
103
|
return {
|
|
194
|
-
tenancy:
|
|
104
|
+
tenancy: tenancyProfile,
|
|
195
105
|
app: {
|
|
196
106
|
...inheritedAppState,
|
|
197
107
|
features: {
|
|
198
108
|
...inheritedFeatures,
|
|
199
|
-
workspaceSwitching:
|
|
200
|
-
workspaceInvites: workspaceInvitationsEnabled
|
|
109
|
+
workspaceSwitching: tenancyProfile.mode !== TENANCY_MODE_NONE,
|
|
110
|
+
workspaceInvites: workspaceInvitationsEnabled
|
|
201
111
|
}
|
|
202
112
|
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
roleSid: workspaceContext.membership?.roleSid,
|
|
208
|
-
status: workspaceContext.membership?.status
|
|
209
|
-
})
|
|
210
|
-
: null,
|
|
211
|
-
membership: mapMembershipSummary(workspaceContext?.membership, workspaceContext?.workspace),
|
|
212
|
-
requestedWorkspace,
|
|
213
|
-
permissions: workspaceContext ? [...workspaceContext.permissions] : [],
|
|
214
|
-
workspaceSettings: workspaceContext
|
|
215
|
-
? mapWorkspaceSettingsPublic(workspaceContext.workspaceSettings, {
|
|
216
|
-
workspaceInvitationsEnabled
|
|
217
|
-
})
|
|
218
|
-
: null
|
|
113
|
+
requestedWorkspace: buildRequestedWorkspacePayload(
|
|
114
|
+
normalizedWorkspaceSlug,
|
|
115
|
+
REQUESTED_WORKSPACE_STATUS_UNAUTHENTICATED
|
|
116
|
+
)
|
|
219
117
|
};
|
|
220
118
|
}
|
|
221
|
-
|
|
119
|
+
|
|
120
|
+
const latestProfile = await userProfilesRepository.findById(normalizedUserId);
|
|
121
|
+
if (!latestProfile) {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const pendingInvites =
|
|
126
|
+
workspaceInvitationsEnabled
|
|
127
|
+
? await workspacePendingInvitationsService.listPendingInvitesForUser(latestProfile, {
|
|
128
|
+
context: {
|
|
129
|
+
actor: latestProfile
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
: [];
|
|
133
|
+
const workspaces = await workspaceService.listWorkspacesForUser(latestProfile);
|
|
134
|
+
let workspaceContext = null;
|
|
135
|
+
let requestedWorkspace = null;
|
|
136
|
+
if (normalizedWorkspaceSlug && tenancyProfile.mode !== TENANCY_MODE_NONE) {
|
|
137
|
+
try {
|
|
138
|
+
workspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
139
|
+
latestProfile,
|
|
140
|
+
normalizedWorkspaceSlug
|
|
141
|
+
);
|
|
142
|
+
requestedWorkspace = buildRequestedWorkspacePayload(
|
|
143
|
+
normalizedWorkspaceSlug,
|
|
144
|
+
REQUESTED_WORKSPACE_STATUS_RESOLVED
|
|
145
|
+
);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const requestedWorkspaceStatus = resolveRequestedWorkspaceStatusFromError(error);
|
|
148
|
+
if (!requestedWorkspaceStatus) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
requestedWorkspace = buildRequestedWorkspacePayload(normalizedWorkspaceSlug, requestedWorkspaceStatus);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
tenancy: tenancyProfile,
|
|
157
|
+
app: {
|
|
158
|
+
...inheritedAppState,
|
|
159
|
+
features: {
|
|
160
|
+
...inheritedFeatures,
|
|
161
|
+
workspaceSwitching: tenancyProfile.mode !== TENANCY_MODE_NONE,
|
|
162
|
+
workspaceInvites: workspaceInvitationsEnabled
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
workspaces: [...workspaces],
|
|
166
|
+
pendingInvites,
|
|
167
|
+
activeWorkspace: workspaceContext
|
|
168
|
+
? mapWorkspaceSummary(workspaceContext.workspace, {
|
|
169
|
+
roleSid: workspaceContext.membership?.roleSid,
|
|
170
|
+
status: workspaceContext.membership?.status
|
|
171
|
+
})
|
|
172
|
+
: null,
|
|
173
|
+
membership: mapMembershipSummary(workspaceContext?.membership, workspaceContext?.workspace),
|
|
174
|
+
requestedWorkspace,
|
|
175
|
+
permissions: workspaceContext ? [...workspaceContext.permissions] : [],
|
|
176
|
+
workspaceSettings: workspaceContext
|
|
177
|
+
? mapWorkspaceSettingsPublic(workspaceContext.workspaceSettings, {
|
|
178
|
+
workspaceInvitationsEnabled
|
|
179
|
+
})
|
|
180
|
+
: null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Object.freeze({ contribute });
|
|
222
185
|
}
|
|
223
186
|
|
|
224
187
|
export { createWorkspaceBootstrapContributor };
|