@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.
Files changed (75) hide show
  1. package/locales/en.json +2 -2
  2. package/package.json +38 -11
  3. package/patterns/workspace-server/PATTERN.md +42 -17
  4. package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
  5. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
  6. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
  7. package/src/server/WorkspacesFeature.js +74 -183
  8. package/src/server/WorkspacesIntegrationsProvider.js +26 -10
  9. package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
  10. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
  11. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
  12. package/src/server/common/formatters/workspaceFormatter.js +12 -10
  13. package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
  14. package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
  15. package/src/server/common/repositories/workspacesRepository.js +17 -25
  16. package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
  17. package/src/server/common/support/realtimeServiceEvents.js +20 -12
  18. package/src/server/common/support/workspaceRecordId.js +21 -0
  19. package/src/server/common/validators/routeParamsValidator.js +3 -2
  20. package/src/server/i18n.js +5 -2
  21. package/src/server/support/resolveWorkspace.js +10 -7
  22. package/src/server/support/workspaceActionSurfaces.js +5 -41
  23. package/src/server/support/workspaceCrud.js +25 -0
  24. package/src/server/support/workspaceInvitationsPolicy.js +16 -27
  25. package/src/server/support/workspaceRouteInput.js +19 -2
  26. package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
  27. package/src/server/types.js +47 -0
  28. package/src/server/workspaceBootstrapContributor.js +105 -142
  29. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
  30. package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
  31. package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
  32. package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
  33. package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
  34. package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
  35. package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
  36. package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
  37. package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
  38. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
  39. package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
  40. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
  41. package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
  42. package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
  43. package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
  44. package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
  45. package/src/server/workspacesRuntime.js +185 -0
  46. package/src/shared/operationMessages.js +6 -15
  47. package/src/shared/resources/workspaceMembersResource.js +2 -2
  48. package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
  49. package/src/shared/resources/workspaceResource.js +4 -1
  50. package/src/shared/roles.js +15 -12
  51. package/src/shared/tenancyProfile.js +2 -2
  52. package/test/exportsContract.test.js +5 -0
  53. package/test/featureRuntime.test.js +5 -6
  54. package/test/packageMetadata.test.js +12 -0
  55. package/test/repositories.integration.test.js +1 -1
  56. package/test/requestedWorkspaceScope.test.js +24 -0
  57. package/test/roleLabels.test.js +29 -7
  58. package/test/support/invitationTransactions.js +35 -19
  59. package/test/workspaceActionSurfaces.test.js +0 -55
  60. package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
  61. package/test/workspaceBootstrapContributor.test.js +7 -6
  62. package/test/workspaceCrud.test.js +23 -0
  63. package/test/workspaceInvitesRepository.test.js +9 -8
  64. package/test/workspaceMembersService.test.js +131 -28
  65. package/test/workspacePendingInvitationsResource.test.js +2 -2
  66. package/test/workspacePendingInvitationsService.test.js +69 -22
  67. package/test/workspaceService.test.js +58 -47
  68. package/test/workspaceSettingsActions.test.js +14 -7
  69. package/test/workspaceSettingsRepository.test.js +11 -0
  70. package/test/workspaceSettingsService.test.js +11 -9
  71. package/test/workspacesRepository.test.js +16 -16
  72. package/test/workspacesRouteRequestInputValidator.test.js +4 -4
  73. package/src/server/common/repositories/repositoryUtils.js +0 -43
  74. package/src/server/common/support/deepFreeze.js +0 -1
  75. 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?.surfaceAccessPolicies) ? appConfig.surfaceAccessPolicies : {};
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?.surfaceDefinitions) ? appConfig.surfaceDefinitions : {};
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?.surfaceDefaultId,
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?.surfaceDefaultId) ||
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
- TENANCY_MODE_NONE,
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
- function normalizeWorkspaceInvitationsConfig(appConfig = {}) {
8
- const source = appConfig && typeof appConfig === "object" && !Array.isArray(appConfig)
9
- ? appConfig.workspaceInvitations
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: normalizedSource.enabled !== false,
17
- allowInPersonalMode: normalizedSource.allowInPersonalMode !== false
9
+ enabled: source.enabled !== false,
10
+ allowInPersonalMode: source.allowInPersonalMode !== false
18
11
  });
19
12
  }
20
13
 
21
- function resolveWorkspaceInvitationsPolicy({
22
- appConfig = {},
23
- tenancyProfile = null
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 normalizedTenancyProfile = tenancyProfile && typeof tenancyProfile === "object"
27
- ? tenancyProfile
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?.workspaceSlug).toLowerCase();
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
- function resolveWorkspaceRoutePath(relativePath = "/") {
10
- const normalizedRelativePath = normalizePathname(relativePath || "/");
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 { requireServiceMethod } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
2
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { normalizeLowerText } from "@jskit-ai/kernel/shared/support/normalize";
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 normalizeQueryPayload(value = {}) {
20
- if (!value || typeof value !== "object" || Array.isArray(value)) {
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 = normalizeQueryPayload(request?.input?.query);
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 = normalizeQueryPayload(request?.query);
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 createRequestedWorkspacePayload(workspaceSlug = "", status = "") {
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
- function resolveBootstrapTenancyProfile(tenancyProfile = null, appConfig = {}) {
85
- const fallback = resolveTenancyProfile(appConfig);
86
- return Object.freeze({
87
- mode: fallback.mode,
88
- workspace: Object.freeze({
89
- enabled: fallback.workspace.enabled === true,
90
- autoProvision: fallback.workspace.autoProvision === true,
91
- allowSelfCreate: fallback.workspace.allowSelfCreate === true,
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 = false,
102
- appConfig = {},
103
- tenancyProfile = null
104
- } = {}) {
105
- const contributorId = "users.workspace.bootstrap";
106
- const resolvedTenancyProfile = resolveBootstrapTenancyProfile(tenancyProfile, appConfig);
107
-
108
- requireServiceMethod(workspaceService, "listWorkspacesForUser", contributorId, {
109
- serviceLabel: "workspaceService"
110
- });
111
- requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId, {
112
- serviceLabel: "workspaceService"
113
- });
114
- if (workspaceInvitationsEnabled) {
115
- requireServiceMethod(workspacePendingInvitationsService, "listPendingInvitesForUser", contributorId, {
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: resolvedTenancyProfile,
104
+ tenancy: tenancyProfile,
195
105
  app: {
196
106
  ...inheritedAppState,
197
107
  features: {
198
108
  ...inheritedFeatures,
199
- workspaceSwitching: normalizeLowerText(resolvedTenancyProfile.mode) !== TENANCY_MODE_NONE,
200
- workspaceInvites: workspaceInvitationsEnabled === true
109
+ workspaceSwitching: tenancyProfile.mode !== TENANCY_MODE_NONE,
110
+ workspaceInvites: workspaceInvitationsEnabled
201
111
  }
202
112
  },
203
- workspaces: [...workspaces],
204
- pendingInvites,
205
- activeWorkspace: workspaceContext
206
- ? mapWorkspaceSummary(workspaceContext.workspace, {
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 };