@jskit-ai/workspaces-core 0.1.14 → 0.1.16

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 (83) hide show
  1. package/package.descriptor.mjs +2 -2
  2. package/package.json +18 -3
  3. package/src/server/WorkspacesCoreServiceProvider.js +41 -2
  4. package/src/server/common/contributors/workspaceActionContextContributor.js +88 -0
  5. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
  6. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +78 -0
  7. package/src/server/common/formatters/workspaceFormatter.js +53 -0
  8. package/src/server/common/repositories/repositoryUtils.js +59 -0
  9. package/src/server/common/repositories/workspaceInvitesRepository.js +208 -0
  10. package/src/server/common/repositories/workspaceMembershipsRepository.js +190 -0
  11. package/src/server/common/repositories/workspacesRepository.js +202 -0
  12. package/src/server/common/services/workspaceContextService.js +281 -0
  13. package/src/server/common/support/deepFreeze.js +1 -0
  14. package/src/server/common/support/realtimeServiceEvents.js +91 -0
  15. package/src/server/common/support/resolveActionUser.js +9 -0
  16. package/src/server/common/support/workspaceRoutePaths.js +18 -0
  17. package/src/server/common/validators/authenticatedUserValidator.js +43 -0
  18. package/src/server/common/validators/routeParamsValidator.js +62 -0
  19. package/src/server/registerWorkspaceBootstrap.js +27 -0
  20. package/src/server/registerWorkspaceCore.js +100 -0
  21. package/src/server/registerWorkspaceRepositories.js +26 -0
  22. package/src/server/support/resolveWorkspace.js +16 -0
  23. package/src/server/support/workspaceActionSurfaces.js +118 -0
  24. package/src/server/support/workspaceInvitationsPolicy.js +45 -0
  25. package/src/server/support/workspaceRouteInput.js +22 -0
  26. package/src/server/workspaceBootstrapContributor.js +233 -0
  27. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +133 -0
  28. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
  29. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +133 -0
  30. package/src/server/workspaceMembers/bootWorkspaceMembers.js +236 -0
  31. package/src/server/workspaceMembers/registerWorkspaceMembers.js +108 -0
  32. package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
  33. package/src/server/workspaceMembers/workspaceMembersService.js +222 -0
  34. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +62 -0
  35. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +119 -0
  36. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
  37. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +138 -0
  38. package/src/server/workspaceSettings/bootWorkspaceSettings.js +76 -0
  39. package/src/server/workspaceSettings/registerWorkspaceSettings.js +62 -0
  40. package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
  41. package/src/server/workspaceSettings/workspaceSettingsRepository.js +154 -0
  42. package/src/server/workspaceSettings/workspaceSettingsService.js +66 -0
  43. package/src/shared/operationMessages.js +16 -0
  44. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  45. package/src/shared/resources/workspaceMembersResource.js +354 -0
  46. package/src/shared/resources/workspacePendingInvitationsResource.js +82 -0
  47. package/src/shared/resources/workspaceResource.js +176 -0
  48. package/src/shared/resources/workspaceSettingsFields.js +59 -0
  49. package/src/shared/resources/workspaceSettingsResource.js +169 -0
  50. package/src/shared/roles.js +161 -0
  51. package/src/shared/settings.js +119 -0
  52. package/src/shared/support/workspacePathModel.js +145 -0
  53. package/src/shared/tenancyMode.js +35 -0
  54. package/src/shared/tenancyProfile.js +73 -0
  55. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +2 -2
  56. package/test/registerServiceRealtimeEvents.test.js +116 -0
  57. package/test/registerWorkspaceDirectory.test.js +31 -0
  58. package/test/registerWorkspaceSettings.test.js +40 -0
  59. package/test/repositoryContracts.test.js +34 -0
  60. package/test/resourcesCanonical.test.js +74 -0
  61. package/test/roles.test.js +159 -0
  62. package/test/routeParamsValidator.test.js +49 -0
  63. package/test/settingsFieldRegistriesSingleton.test.js +14 -0
  64. package/test/tenancyProfile.test.js +67 -0
  65. package/test/usersRouteResources.test.js +97 -0
  66. package/test/workspaceActionContextContributor.test.js +344 -0
  67. package/test/workspaceActionSurfaces.test.js +85 -0
  68. package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
  69. package/test/workspaceBootstrapContributor.test.js +169 -0
  70. package/test/workspaceInvitationsPolicy.test.js +71 -0
  71. package/test/workspaceInvitesRepository.test.js +111 -0
  72. package/test/workspaceMembersService.test.js +398 -0
  73. package/test/workspacePathModel.test.js +93 -0
  74. package/test/workspacePendingInvitationsResource.test.js +38 -0
  75. package/test/workspacePendingInvitationsService.test.js +151 -0
  76. package/test/workspaceRouteVisibilityResolver.test.js +83 -0
  77. package/test/workspaceService.test.js +546 -0
  78. package/test/workspaceSettingsActions.test.js +52 -0
  79. package/test/workspaceSettingsRepository.test.js +202 -0
  80. package/test/workspaceSettingsResource.test.js +169 -0
  81. package/test/workspaceSettingsService.test.js +140 -0
  82. package/test/workspacesRouteRequestInputValidator.test.js +5 -5
  83. package/test-support/registerDefaultSettingsFields.js +1 -0
@@ -0,0 +1,169 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
+ import {
4
+ normalizeObjectInput,
5
+ createCursorListValidator,
6
+ normalizeSettingsFieldInput,
7
+ recordIdSchema
8
+ } from "@jskit-ai/kernel/shared/validators";
9
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
10
+ import { workspaceSettingsFields } from "./workspaceSettingsFields.js";
11
+ import { createWorkspaceRoleCatalog } from "../roles.js";
12
+
13
+ function buildCreateBodySchema() {
14
+ const properties = {};
15
+ for (const field of workspaceSettingsFields) {
16
+ properties[field.key] = field.required === false ? Type.Optional(field.inputSchema) : field.inputSchema;
17
+ }
18
+
19
+ return Type.Object(properties, {
20
+ additionalProperties: false,
21
+ messages: {
22
+ additionalProperties: "Unexpected field.",
23
+ default: "Invalid value."
24
+ }
25
+ });
26
+ }
27
+
28
+ function buildSettingsOutputSchema() {
29
+ const properties = {};
30
+ for (const field of workspaceSettingsFields) {
31
+ properties[field.key] = field.outputSchema;
32
+ }
33
+ properties.invitesAvailable = Type.Boolean();
34
+ properties.invitesEffective = Type.Boolean();
35
+
36
+ return Type.Object(properties, { additionalProperties: false });
37
+ }
38
+
39
+ function buildResponseRecordSchema() {
40
+ return Type.Object(
41
+ {
42
+ workspace: Type.Object(
43
+ {
44
+ id: recordIdSchema,
45
+ slug: Type.String({ minLength: 1 }),
46
+ ownerUserId: recordIdSchema
47
+ },
48
+ { additionalProperties: false }
49
+ ),
50
+ settings: buildSettingsOutputSchema(),
51
+ roleCatalog: Type.Object(
52
+ {
53
+ collaborationEnabled: Type.Boolean(),
54
+ defaultInviteRole: Type.String(),
55
+ roles: Type.Array(Type.Object({}, { additionalProperties: true })),
56
+ assignableRoleIds: Type.Array(Type.String({ minLength: 1 }))
57
+ },
58
+ { additionalProperties: true }
59
+ )
60
+ },
61
+ { additionalProperties: false }
62
+ );
63
+ }
64
+
65
+ function normalizeInput(payload = {}) {
66
+ return normalizeSettingsFieldInput(payload, workspaceSettingsFields);
67
+ }
68
+
69
+ function normalizeOutput(payload = {}) {
70
+ const source = normalizeObjectInput(payload);
71
+ const workspace = normalizeObjectInput(source.workspace);
72
+ const settings = normalizeObjectInput(source.settings);
73
+ const normalizedSettings = {};
74
+
75
+ for (const field of workspaceSettingsFields) {
76
+ const rawValue = Object.hasOwn(settings, field.key)
77
+ ? settings[field.key]
78
+ : field.resolveDefault({
79
+ workspace,
80
+ settings
81
+ });
82
+ normalizedSettings[field.key] = field.normalizeOutput(rawValue, {
83
+ workspace,
84
+ settings
85
+ });
86
+ }
87
+
88
+ const invitesEnabled = normalizedSettings.invitesEnabled !== false;
89
+ const invitesAvailable = settings.invitesAvailable !== false;
90
+ const invitesEffective =
91
+ typeof settings.invitesEffective === "boolean" ? settings.invitesEffective : invitesEnabled;
92
+ normalizedSettings.invitesEnabled = invitesEnabled;
93
+ normalizedSettings.invitesAvailable = invitesAvailable;
94
+ normalizedSettings.invitesEffective = invitesEffective;
95
+ const roleCatalog = normalizeObjectInput(source.roleCatalog);
96
+ const hasRoleCatalog =
97
+ Array.isArray(roleCatalog.roles) &&
98
+ roleCatalog.roles.length > 0 &&
99
+ Array.isArray(roleCatalog.assignableRoleIds);
100
+
101
+ return {
102
+ workspace: {
103
+ id: normalizeRecordId(workspace.id, { fallback: "" }),
104
+ slug: normalizeText(workspace.slug),
105
+ ownerUserId: normalizeRecordId(workspace.ownerUserId, { fallback: "" })
106
+ },
107
+ settings: normalizedSettings,
108
+ roleCatalog: hasRoleCatalog ? roleCatalog : createWorkspaceRoleCatalog()
109
+ };
110
+ }
111
+
112
+ const responseRecordValidator = Object.freeze({
113
+ get schema() {
114
+ return buildResponseRecordSchema();
115
+ },
116
+ normalize: normalizeOutput
117
+ });
118
+
119
+ const resource = {
120
+ resource: "workspaceSettings",
121
+ messages: {
122
+ validation: "Fix invalid workspace settings values and try again.",
123
+ saveSuccess: "Workspace settings updated.",
124
+ saveError: "Unable to update workspace settings.",
125
+ apiValidation: "Validation failed."
126
+ },
127
+ operations: {
128
+ view: {
129
+ method: "GET",
130
+ outputValidator: responseRecordValidator
131
+ },
132
+ list: {
133
+ method: "GET",
134
+ outputValidator: createCursorListValidator(responseRecordValidator)
135
+ },
136
+ create: {
137
+ method: "POST",
138
+ bodyValidator: {
139
+ get schema() {
140
+ return buildCreateBodySchema();
141
+ },
142
+ normalize: normalizeInput
143
+ },
144
+ outputValidator: responseRecordValidator
145
+ },
146
+ replace: {
147
+ method: "PUT",
148
+ bodyValidator: {
149
+ get schema() {
150
+ return buildCreateBodySchema();
151
+ },
152
+ normalize: normalizeInput
153
+ },
154
+ outputValidator: responseRecordValidator
155
+ },
156
+ patch: {
157
+ method: "PATCH",
158
+ bodyValidator: {
159
+ get schema() {
160
+ return Type.Partial(buildCreateBodySchema(), { additionalProperties: false });
161
+ },
162
+ normalize: normalizeInput
163
+ },
164
+ outputValidator: responseRecordValidator
165
+ }
166
+ }
167
+ };
168
+
169
+ export { resource as workspaceSettingsResource };
@@ -0,0 +1,161 @@
1
+ import {
2
+ hasPermission,
3
+ normalizePermissionList
4
+ } from "@jskit-ai/kernel/shared/support";
5
+
6
+ const OWNER_ROLE_ID = "owner";
7
+ const ADMIN_ROLE_ID = "admin";
8
+ const MEMBER_ROLE_ID = "member";
9
+
10
+ function asRecord(value) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
12
+ return {};
13
+ }
14
+ return value;
15
+ }
16
+
17
+ function normalizeRoleId(value) {
18
+ return String(value || "")
19
+ .trim()
20
+ .toLowerCase();
21
+ }
22
+
23
+ function resolveInheritedRolePermissions(roleSid, configuredRoles = {}, seenRoleIds = new Set()) {
24
+ if (seenRoleIds.has(roleSid)) {
25
+ throw new TypeError(`roleCatalog role "${roleSid}" has circular inheritance.`);
26
+ }
27
+
28
+ const source = asRecord(configuredRoles[roleSid]);
29
+ const inheritedRoleId = normalizeRoleId(source.inherits);
30
+ const directPermissions = normalizePermissionList(source.permissions);
31
+ if (!inheritedRoleId) {
32
+ return directPermissions;
33
+ }
34
+
35
+ if (!Object.hasOwn(configuredRoles, inheritedRoleId)) {
36
+ throw new TypeError(`roleCatalog role "${roleSid}" inherits unknown role "${inheritedRoleId}".`);
37
+ }
38
+
39
+ const nextSeenRoleIds = new Set(seenRoleIds);
40
+ nextSeenRoleIds.add(roleSid);
41
+
42
+ return normalizePermissionList([
43
+ ...resolveInheritedRolePermissions(inheritedRoleId, configuredRoles, nextSeenRoleIds),
44
+ ...directPermissions
45
+ ]);
46
+ }
47
+
48
+ function createRoleDescriptor(roleSid, configuredDefinition, configuredRoles = {}) {
49
+ const source = asRecord(configuredDefinition);
50
+ const assignable = roleSid === OWNER_ROLE_ID ? false : source.assignable === true;
51
+ const permissions = resolveInheritedRolePermissions(roleSid, configuredRoles);
52
+
53
+ return Object.freeze({
54
+ id: roleSid,
55
+ assignable,
56
+ permissions: Object.freeze([...permissions])
57
+ });
58
+ }
59
+
60
+ function listConfiguredRoleIds(appConfig = {}) {
61
+ const configuredRoles = normalizeConfiguredRoles(appConfig);
62
+ return Object.freeze(Object.keys(configuredRoles));
63
+ }
64
+
65
+ function resolveConfiguredDefaultInviteRole(appConfig = {}) {
66
+ return normalizeRoleId(appConfig?.roleCatalog?.workspace?.defaultInviteRole);
67
+ }
68
+
69
+ function normalizeConfiguredRoles(appConfig = {}) {
70
+ const roleCatalog = asRecord(appConfig?.roleCatalog);
71
+ const configuredRoles = asRecord(roleCatalog.roles);
72
+ const normalizedRoles = {};
73
+
74
+ for (const [roleSid, roleDefinition] of Object.entries(configuredRoles)) {
75
+ const normalizedRoleId = normalizeRoleId(roleSid);
76
+ if (!normalizedRoleId) {
77
+ continue;
78
+ }
79
+ normalizedRoles[normalizedRoleId] = roleDefinition;
80
+ }
81
+
82
+ return normalizedRoles;
83
+ }
84
+
85
+ function createWorkspaceRoleCatalog(appConfig = {}) {
86
+ const configuredRoles = normalizeConfiguredRoles(appConfig);
87
+ const roleIds = listConfiguredRoleIds(appConfig);
88
+ const roles = roleIds.map((roleSid) => createRoleDescriptor(roleSid, configuredRoles[roleSid], configuredRoles));
89
+ const assignableRoleIds = roles.filter((role) => role.assignable).map((role) => role.id);
90
+ const configuredDefaultInviteRole = resolveConfiguredDefaultInviteRole(appConfig);
91
+ const defaultInviteRole = assignableRoleIds.includes(configuredDefaultInviteRole)
92
+ ? configuredDefaultInviteRole
93
+ : assignableRoleIds[0] || "";
94
+
95
+ return Object.freeze({
96
+ collaborationEnabled: assignableRoleIds.length > 0 && Boolean(defaultInviteRole),
97
+ defaultInviteRole,
98
+ roles: Object.freeze(
99
+ roles.map((role) =>
100
+ Object.freeze({
101
+ id: role.id,
102
+ assignable: role.assignable,
103
+ permissions: Object.freeze([...role.permissions])
104
+ })
105
+ )
106
+ ),
107
+ assignableRoleIds: Object.freeze([...assignableRoleIds])
108
+ });
109
+ }
110
+
111
+ function cloneWorkspaceRoleCatalog(roleCatalog = null) {
112
+ const source = asRecord(roleCatalog);
113
+
114
+ return {
115
+ collaborationEnabled: source.collaborationEnabled === true,
116
+ defaultInviteRole: String(source.defaultInviteRole || ""),
117
+ roles: Array.isArray(source.roles)
118
+ ? source.roles.map((role) => ({
119
+ id: normalizeRoleId(role?.id),
120
+ assignable: role?.assignable === true,
121
+ permissions: Array.isArray(role?.permissions) ? [...role.permissions] : []
122
+ }))
123
+ : [],
124
+ assignableRoleIds: Array.isArray(source.assignableRoleIds) ? [...source.assignableRoleIds] : []
125
+ };
126
+ }
127
+
128
+ function listRoleDescriptors(appConfig = {}) {
129
+ const roleCatalog = createWorkspaceRoleCatalog(appConfig);
130
+ return roleCatalog.roles.map((role) => ({
131
+ id: role.id,
132
+ assignable: role.assignable,
133
+ permissions: [...role.permissions]
134
+ }));
135
+ }
136
+
137
+ function resolveRolePermissions(roleSid, appConfig = {}) {
138
+ const normalizedRoleId = normalizeRoleId(roleSid);
139
+ if (!normalizedRoleId) {
140
+ return [];
141
+ }
142
+
143
+ const roleCatalog = createWorkspaceRoleCatalog(appConfig);
144
+ const role = roleCatalog.roles.find((entry) => entry.id === normalizedRoleId);
145
+ if (!role) {
146
+ return [];
147
+ }
148
+
149
+ return [...role.permissions];
150
+ }
151
+
152
+ export {
153
+ OWNER_ROLE_ID,
154
+ ADMIN_ROLE_ID,
155
+ MEMBER_ROLE_ID,
156
+ resolveRolePermissions,
157
+ listRoleDescriptors,
158
+ createWorkspaceRoleCatalog,
159
+ cloneWorkspaceRoleCatalog,
160
+ hasPermission
161
+ };
@@ -0,0 +1,119 @@
1
+ const WORKSPACE_THEME_MODE_LIGHT = "light";
2
+ const WORKSPACE_THEME_MODE_DARK = "dark";
3
+ const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
4
+
5
+ const DEFAULT_WORKSPACE_LIGHT_PALETTE = Object.freeze({
6
+ color: "#1867C0",
7
+ secondaryColor: "#48A9A6",
8
+ surfaceColor: "#FFFFFF",
9
+ surfaceVariantColor: "#424242"
10
+ });
11
+
12
+ const DEFAULT_WORKSPACE_DARK_PALETTE = Object.freeze({
13
+ color: "#2196F3",
14
+ secondaryColor: "#54B6B2",
15
+ surfaceColor: "#212121",
16
+ surfaceVariantColor: "#C8C8C8"
17
+ });
18
+
19
+ const DEFAULT_WORKSPACE_COLOR = DEFAULT_WORKSPACE_LIGHT_PALETTE.color;
20
+
21
+ function normalizeWorkspaceHexColor(value) {
22
+ const normalized = String(value || "").trim();
23
+ if (!HEX_COLOR_PATTERN.test(normalized)) {
24
+ return "";
25
+ }
26
+ return normalized.toUpperCase();
27
+ }
28
+
29
+ function coerceWorkspaceColor(value) {
30
+ return normalizeWorkspaceHexColor(value) || DEFAULT_WORKSPACE_COLOR;
31
+ }
32
+
33
+ function normalizeWorkspaceThemeMode(value = "") {
34
+ const normalized = String(value || "").trim().toLowerCase();
35
+ if (normalized === WORKSPACE_THEME_MODE_DARK) {
36
+ return WORKSPACE_THEME_MODE_DARK;
37
+ }
38
+ return WORKSPACE_THEME_MODE_LIGHT;
39
+ }
40
+
41
+ function resolveWorkspaceThemeDefaultPalette(mode = WORKSPACE_THEME_MODE_LIGHT) {
42
+ const normalizedMode = normalizeWorkspaceThemeMode(mode);
43
+ return normalizedMode === WORKSPACE_THEME_MODE_DARK
44
+ ? DEFAULT_WORKSPACE_DARK_PALETTE
45
+ : DEFAULT_WORKSPACE_LIGHT_PALETTE;
46
+ }
47
+
48
+ function coerceWorkspaceThemeColor(value, fallbackColor = DEFAULT_WORKSPACE_COLOR) {
49
+ return normalizeWorkspaceHexColor(value) || normalizeWorkspaceHexColor(fallbackColor) || DEFAULT_WORKSPACE_COLOR;
50
+ }
51
+
52
+ function coerceWorkspaceSecondaryColor(value, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
53
+ return coerceWorkspaceThemeColor(value, resolveWorkspaceThemeDefaultPalette(mode).secondaryColor);
54
+ }
55
+
56
+ function coerceWorkspaceSurfaceColor(value, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
57
+ return coerceWorkspaceThemeColor(value, resolveWorkspaceThemeDefaultPalette(mode).surfaceColor);
58
+ }
59
+
60
+ function coerceWorkspaceSurfaceVariantColor(value, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
61
+ return coerceWorkspaceThemeColor(value, resolveWorkspaceThemeDefaultPalette(mode).surfaceVariantColor);
62
+ }
63
+
64
+ function resolveWorkspaceThemePalette(input = {}, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
65
+ const source = input && typeof input === "object" ? input : {};
66
+ const normalizedMode = normalizeWorkspaceThemeMode(mode);
67
+ const paletteDefaults = resolveWorkspaceThemeDefaultPalette(normalizedMode);
68
+
69
+ if (normalizedMode === WORKSPACE_THEME_MODE_DARK) {
70
+ return Object.freeze({
71
+ color: coerceWorkspaceThemeColor(source.darkPrimaryColor, paletteDefaults.color),
72
+ secondaryColor: coerceWorkspaceThemeColor(source.darkSecondaryColor, paletteDefaults.secondaryColor),
73
+ surfaceColor: coerceWorkspaceThemeColor(source.darkSurfaceColor, paletteDefaults.surfaceColor),
74
+ surfaceVariantColor: coerceWorkspaceThemeColor(
75
+ source.darkSurfaceVariantColor,
76
+ paletteDefaults.surfaceVariantColor
77
+ )
78
+ });
79
+ }
80
+
81
+ return Object.freeze({
82
+ color: coerceWorkspaceThemeColor(source.lightPrimaryColor, paletteDefaults.color),
83
+ secondaryColor: coerceWorkspaceThemeColor(source.lightSecondaryColor, paletteDefaults.secondaryColor),
84
+ surfaceColor: coerceWorkspaceThemeColor(source.lightSurfaceColor, paletteDefaults.surfaceColor),
85
+ surfaceVariantColor: coerceWorkspaceThemeColor(
86
+ source.lightSurfaceVariantColor,
87
+ paletteDefaults.surfaceVariantColor
88
+ )
89
+ });
90
+ }
91
+
92
+ function resolveWorkspaceThemePalettes(input = {}) {
93
+ return Object.freeze({
94
+ light: resolveWorkspaceThemePalette(input, {
95
+ mode: WORKSPACE_THEME_MODE_LIGHT
96
+ }),
97
+ dark: resolveWorkspaceThemePalette(input, {
98
+ mode: WORKSPACE_THEME_MODE_DARK
99
+ })
100
+ });
101
+ }
102
+
103
+ export {
104
+ DEFAULT_WORKSPACE_DARK_PALETTE,
105
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
106
+ DEFAULT_WORKSPACE_COLOR,
107
+ coerceWorkspaceColor,
108
+ coerceWorkspaceThemeColor,
109
+ coerceWorkspaceSecondaryColor,
110
+ coerceWorkspaceSurfaceColor,
111
+ coerceWorkspaceSurfaceVariantColor,
112
+ normalizeWorkspaceHexColor,
113
+ normalizeWorkspaceThemeMode,
114
+ resolveWorkspaceThemeDefaultPalette,
115
+ resolveWorkspaceThemePalettes,
116
+ WORKSPACE_THEME_MODE_DARK,
117
+ WORKSPACE_THEME_MODE_LIGHT,
118
+ resolveWorkspaceThemePalette
119
+ };
@@ -0,0 +1,145 @@
1
+ import {
2
+ deriveSurfaceRouteBaseFromPagesRoot,
3
+ normalizeSurfaceId
4
+ } from "@jskit-ai/kernel/shared/surface/registry";
5
+ import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
6
+
7
+ function normalizeWorkspaceBasePath(workspaceBasePath = "/w") {
8
+ return normalizePathname(workspaceBasePath || "/w");
9
+ }
10
+
11
+ function normalizeSurfaceSegment(segmentLike = "") {
12
+ const normalizedPath = normalizePathname(segmentLike || "/");
13
+ if (normalizedPath === "/") {
14
+ return "";
15
+ }
16
+ return normalizedPath.replace(/^\/+/, "");
17
+ }
18
+
19
+ function normalizeSurfaceRouteBase(routeBaseLike = "") {
20
+ const rawRouteBase = String(routeBaseLike || "").trim();
21
+ if (!rawRouteBase || rawRouteBase === "/") {
22
+ return "/";
23
+ }
24
+ const withoutLeadingSlash = rawRouteBase.startsWith("/") ? rawRouteBase.slice(1) : rawRouteBase;
25
+ return deriveSurfaceRouteBaseFromPagesRoot(withoutLeadingSlash || "");
26
+ }
27
+
28
+ function normalizeSurfaceSegmentFromRouteBase(routeBase, { workspaceBasePath = "/w" } = {}) {
29
+ const normalizedRouteBase = normalizeSurfaceRouteBase(routeBase);
30
+ if (normalizedRouteBase === "/") {
31
+ return "";
32
+ }
33
+
34
+ const normalizedWorkspaceBasePath = normalizeWorkspaceBasePath(workspaceBasePath);
35
+ const workspacePlaceholderRoot = `${normalizedWorkspaceBasePath}/:workspaceSlug`;
36
+ if (normalizedRouteBase === workspacePlaceholderRoot) {
37
+ return "";
38
+ }
39
+ if (normalizedRouteBase.startsWith(`${workspacePlaceholderRoot}/`)) {
40
+ const remainder = normalizedRouteBase.slice(`${workspacePlaceholderRoot}/`.length);
41
+ const firstSegment = remainder.split("/").filter(Boolean)[0] || "";
42
+ return firstSegment.startsWith(":") ? "" : firstSegment;
43
+ }
44
+
45
+ const firstSegment = normalizedRouteBase.replace(/^\/+/, "").split("/").filter(Boolean)[0] || "";
46
+ if (!firstSegment || firstSegment.startsWith(":")) {
47
+ return "";
48
+ }
49
+ return firstSegment;
50
+ }
51
+
52
+ function parseWorkspacePathname(pathname = "", { workspaceBasePath = "/w" } = {}) {
53
+ const normalizedPathname = normalizePathname(pathname);
54
+ const normalizedWorkspaceBasePath = normalizeWorkspaceBasePath(workspaceBasePath);
55
+ if (!normalizedPathname.startsWith(`${normalizedWorkspaceBasePath}/`)) {
56
+ return null;
57
+ }
58
+
59
+ const trailingPath = normalizedPathname.slice(`${normalizedWorkspaceBasePath}/`.length);
60
+ const segments = trailingPath.split("/").filter(Boolean);
61
+ if (segments.length < 1) {
62
+ return null;
63
+ }
64
+
65
+ const [workspaceSlug, ...suffixSegments] = segments;
66
+ return {
67
+ workspaceSlug: String(workspaceSlug || "").trim(),
68
+ suffixSegments
69
+ };
70
+ }
71
+
72
+ function resolveDefaultWorkspaceSurfaceId({
73
+ defaultSurfaceId = "",
74
+ workspaceSurfaceIds = [],
75
+ surfaceRequiresWorkspace = null
76
+ } = {}) {
77
+ const normalizedDefaultSurfaceId = normalizeSurfaceId(defaultSurfaceId);
78
+ if (
79
+ normalizedDefaultSurfaceId &&
80
+ typeof surfaceRequiresWorkspace === "function" &&
81
+ surfaceRequiresWorkspace(normalizedDefaultSurfaceId)
82
+ ) {
83
+ return normalizedDefaultSurfaceId;
84
+ }
85
+
86
+ for (const workspaceSurfaceId of Array.isArray(workspaceSurfaceIds) ? workspaceSurfaceIds : []) {
87
+ const normalizedWorkspaceSurfaceId = normalizeSurfaceId(workspaceSurfaceId);
88
+ if (normalizedWorkspaceSurfaceId) {
89
+ return normalizedWorkspaceSurfaceId;
90
+ }
91
+ }
92
+
93
+ return normalizedDefaultSurfaceId;
94
+ }
95
+
96
+ function resolveWorkspaceSurfaceIdFromSuffixSegments({
97
+ suffixSegments = [],
98
+ defaultWorkspaceSurfaceId = "",
99
+ workspaceSurfaces = []
100
+ } = {}) {
101
+ const normalizedDefaultWorkspaceSurfaceId = normalizeSurfaceId(defaultWorkspaceSurfaceId);
102
+ if (!Array.isArray(suffixSegments) || suffixSegments.length < 1) {
103
+ return normalizedDefaultWorkspaceSurfaceId;
104
+ }
105
+
106
+ const suffixPath = suffixSegments.join("/");
107
+ const candidates = (Array.isArray(workspaceSurfaces) ? workspaceSurfaces : [])
108
+ .map((entry) => {
109
+ const surfaceId = normalizeSurfaceId(entry?.surfaceId || entry?.id);
110
+ if (!surfaceId || surfaceId === normalizedDefaultWorkspaceSurfaceId) {
111
+ return null;
112
+ }
113
+
114
+ const segment =
115
+ normalizeSurfaceSegment(entry?.segment) ||
116
+ normalizeSurfaceSegmentFromRouteBase(entry?.routeBase || entry?.pagesRoot) ||
117
+ surfaceId;
118
+ if (!segment) {
119
+ return null;
120
+ }
121
+
122
+ return {
123
+ surfaceId,
124
+ segment
125
+ };
126
+ })
127
+ .filter(Boolean)
128
+ .sort((left, right) => right.segment.length - left.segment.length);
129
+
130
+ for (const candidate of candidates) {
131
+ if (suffixPath === candidate.segment || suffixPath.startsWith(`${candidate.segment}/`)) {
132
+ return candidate.surfaceId;
133
+ }
134
+ }
135
+
136
+ return normalizedDefaultWorkspaceSurfaceId;
137
+ }
138
+
139
+ export {
140
+ normalizePathname,
141
+ normalizeSurfaceSegmentFromRouteBase,
142
+ parseWorkspacePathname,
143
+ resolveDefaultWorkspaceSurfaceId,
144
+ resolveWorkspaceSurfaceIdFromSuffixSegments
145
+ };
@@ -0,0 +1,35 @@
1
+ const TENANCY_MODE_NONE = "none";
2
+ const TENANCY_MODE_PERSONAL = "personal";
3
+ const TENANCY_MODE_WORKSPACES = "workspaces";
4
+
5
+ const TENANCY_MODES = Object.freeze([
6
+ TENANCY_MODE_NONE,
7
+ TENANCY_MODE_PERSONAL,
8
+ TENANCY_MODE_WORKSPACES
9
+ ]);
10
+
11
+ function normalizeTenancyMode(value = "") {
12
+ const normalized = String(value || "")
13
+ .trim()
14
+ .toLowerCase();
15
+ if (!TENANCY_MODES.includes(normalized)) {
16
+ return TENANCY_MODE_NONE;
17
+ }
18
+ return normalized;
19
+ }
20
+
21
+ function isTenancyMode(value = "") {
22
+ const normalized = String(value || "")
23
+ .trim()
24
+ .toLowerCase();
25
+ return TENANCY_MODES.includes(normalized);
26
+ }
27
+
28
+ export {
29
+ TENANCY_MODE_NONE,
30
+ TENANCY_MODE_PERSONAL,
31
+ TENANCY_MODE_WORKSPACES,
32
+ TENANCY_MODES,
33
+ normalizeTenancyMode,
34
+ isTenancyMode
35
+ };
@@ -0,0 +1,73 @@
1
+ import {
2
+ TENANCY_MODE_NONE,
3
+ TENANCY_MODE_PERSONAL,
4
+ TENANCY_MODE_WORKSPACES,
5
+ normalizeTenancyMode
6
+ } from "./tenancyMode.js";
7
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
8
+
9
+ const WORKSPACE_SLUG_POLICY_NONE = "none";
10
+ const WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME = "immutable_username";
11
+ const WORKSPACE_SLUG_POLICY_USER_SELECTED = "user_selected";
12
+
13
+ function resolveWorkspacePolicyOverrides(appConfig = {}) {
14
+ const tenancyPolicy = isRecord(appConfig?.tenancyPolicy) ? appConfig.tenancyPolicy : {};
15
+ const workspacePolicy = isRecord(tenancyPolicy.workspace) ? tenancyPolicy.workspace : {};
16
+
17
+ return Object.freeze({
18
+ allowSelfCreate: typeof workspacePolicy.allowSelfCreate === "boolean" ? workspacePolicy.allowSelfCreate : null
19
+ });
20
+ }
21
+
22
+ function resolveWorkspacePolicy(mode, overrides = {}) {
23
+ if (mode === TENANCY_MODE_NONE) {
24
+ return Object.freeze({
25
+ enabled: false,
26
+ autoProvision: false,
27
+ allowSelfCreate: false,
28
+ slugPolicy: WORKSPACE_SLUG_POLICY_NONE
29
+ });
30
+ }
31
+
32
+ if (mode === TENANCY_MODE_PERSONAL) {
33
+ return Object.freeze({
34
+ enabled: true,
35
+ autoProvision: true,
36
+ allowSelfCreate: false,
37
+ slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
38
+ });
39
+ }
40
+
41
+ return Object.freeze({
42
+ enabled: true,
43
+ autoProvision: false,
44
+ allowSelfCreate: overrides.allowSelfCreate === true,
45
+ slugPolicy: WORKSPACE_SLUG_POLICY_USER_SELECTED
46
+ });
47
+ }
48
+
49
+ function resolveTenancyProfile(appConfig = {}) {
50
+ const mode = normalizeTenancyMode(appConfig?.tenancyMode);
51
+ const workspacePolicyOverrides = resolveWorkspacePolicyOverrides(appConfig);
52
+
53
+ return Object.freeze({
54
+ mode,
55
+ workspace: resolveWorkspacePolicy(mode, workspacePolicyOverrides)
56
+ });
57
+ }
58
+
59
+ function isWorkspacesTenancyMode(value = "") {
60
+ return normalizeTenancyMode(value) === TENANCY_MODE_WORKSPACES;
61
+ }
62
+
63
+ export {
64
+ TENANCY_MODE_NONE,
65
+ TENANCY_MODE_PERSONAL,
66
+ TENANCY_MODE_WORKSPACES,
67
+ normalizeTenancyMode,
68
+ WORKSPACE_SLUG_POLICY_NONE,
69
+ WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
70
+ WORKSPACE_SLUG_POLICY_USER_SELECTED,
71
+ resolveTenancyProfile,
72
+ isWorkspacesTenancyMode
73
+ };