@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.
- package/package.descriptor.mjs +2 -2
- package/package.json +18 -3
- package/src/server/WorkspacesCoreServiceProvider.js +41 -2
- package/src/server/common/contributors/workspaceActionContextContributor.js +88 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +78 -0
- package/src/server/common/formatters/workspaceFormatter.js +53 -0
- package/src/server/common/repositories/repositoryUtils.js +59 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +208 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +190 -0
- package/src/server/common/repositories/workspacesRepository.js +202 -0
- package/src/server/common/services/workspaceContextService.js +281 -0
- package/src/server/common/support/deepFreeze.js +1 -0
- package/src/server/common/support/realtimeServiceEvents.js +91 -0
- package/src/server/common/support/resolveActionUser.js +9 -0
- package/src/server/common/support/workspaceRoutePaths.js +18 -0
- package/src/server/common/validators/authenticatedUserValidator.js +43 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/registerWorkspaceBootstrap.js +27 -0
- package/src/server/registerWorkspaceCore.js +100 -0
- package/src/server/registerWorkspaceRepositories.js +26 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +118 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +233 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +133 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +133 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +236 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +108 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +222 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +62 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +119 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +138 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +76 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +62 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +154 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +66 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/workspaceMembersResource.js +354 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +82 -0
- package/src/shared/resources/workspaceResource.js +176 -0
- package/src/shared/resources/workspaceSettingsFields.js +59 -0
- package/src/shared/resources/workspaceSettingsResource.js +169 -0
- package/src/shared/roles.js +161 -0
- package/src/shared/settings.js +119 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +2 -2
- package/test/registerServiceRealtimeEvents.test.js +116 -0
- package/test/registerWorkspaceDirectory.test.js +31 -0
- package/test/registerWorkspaceSettings.test.js +40 -0
- package/test/repositoryContracts.test.js +34 -0
- package/test/resourcesCanonical.test.js +74 -0
- package/test/roles.test.js +159 -0
- package/test/routeParamsValidator.test.js +49 -0
- package/test/settingsFieldRegistriesSingleton.test.js +14 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/usersRouteResources.test.js +97 -0
- package/test/workspaceActionContextContributor.test.js +344 -0
- package/test/workspaceActionSurfaces.test.js +85 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +169 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +398 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +546 -0
- package/test/workspaceSettingsActions.test.js +52 -0
- package/test/workspaceSettingsRepository.test.js +202 -0
- package/test/workspaceSettingsResource.test.js +169 -0
- package/test/workspaceSettingsService.test.js +140 -0
- package/test/workspacesRouteRequestInputValidator.test.js +5 -5
- 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
|
+
};
|