@jskit-ai/users-core 0.1.4

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 (148) hide show
  1. package/package.descriptor.mjs +464 -0
  2. package/package.json +35 -0
  3. package/src/server/UsersCoreServiceProvider.js +74 -0
  4. package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
  5. package/src/server/accountNotifications/accountNotificationsService.js +41 -0
  6. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
  7. package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
  8. package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
  9. package/src/server/accountPreferences/accountPreferencesService.js +41 -0
  10. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
  11. package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
  12. package/src/server/accountProfile/accountProfileActions.js +137 -0
  13. package/src/server/accountProfile/accountProfileService.js +124 -0
  14. package/src/server/accountProfile/avatarService.js +141 -0
  15. package/src/server/accountProfile/avatarStorageService.js +132 -0
  16. package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
  17. package/src/server/accountProfile/registerAccountProfile.js +62 -0
  18. package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
  19. package/src/server/accountSecurity/accountSecurityActions.js +144 -0
  20. package/src/server/accountSecurity/accountSecurityService.js +103 -0
  21. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
  22. package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
  23. package/src/server/common/README.md +21 -0
  24. package/src/server/common/contributors/README.md +11 -0
  25. package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
  26. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
  27. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
  28. package/src/server/common/diTokens.js +21 -0
  29. package/src/server/common/formatters/README.md +11 -0
  30. package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
  31. package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
  32. package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
  33. package/src/server/common/formatters/workspaceFormatter.js +46 -0
  34. package/src/server/common/registerCommonRepositories.js +45 -0
  35. package/src/server/common/registerSharedApi.js +9 -0
  36. package/src/server/common/repositories/README.md +24 -0
  37. package/src/server/common/repositories/repositoryUtils.js +50 -0
  38. package/src/server/common/repositories/userProfilesRepository.js +251 -0
  39. package/src/server/common/repositories/userSettingsRepository.js +179 -0
  40. package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
  41. package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
  42. package/src/server/common/repositories/workspacesRepository.js +183 -0
  43. package/src/server/common/routes/README.md +11 -0
  44. package/src/server/common/services/README.md +12 -0
  45. package/src/server/common/services/accountContextService.js +31 -0
  46. package/src/server/common/services/authProfileSyncService.js +128 -0
  47. package/src/server/common/services/workspaceContextService.js +270 -0
  48. package/src/server/common/support/deepFreeze.js +17 -0
  49. package/src/server/common/support/realtimeServiceEvents.js +94 -0
  50. package/src/server/common/support/resolveActionUser.js +11 -0
  51. package/src/server/common/support/workspaceRoutePaths.js +17 -0
  52. package/src/server/common/validators/README.md +11 -0
  53. package/src/server/common/validators/authenticatedUserValidator.js +42 -0
  54. package/src/server/common/validators/routeParamsValidator.js +62 -0
  55. package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
  56. package/src/server/consoleSettings/consoleService.js +36 -0
  57. package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
  58. package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
  59. package/src/server/consoleSettings/consoleSettingsService.js +40 -0
  60. package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
  61. package/src/server/registerWorkspaceBootstrap.js +36 -0
  62. package/src/server/registerWorkspaceCore.js +95 -0
  63. package/src/server/support/resolveWorkspace.js +16 -0
  64. package/src/server/support/workspaceActionSurfaces.js +135 -0
  65. package/src/server/support/workspaceInvitationsPolicy.js +45 -0
  66. package/src/server/support/workspaceRouteInput.js +22 -0
  67. package/src/server/workspaceBootstrapContributor.js +401 -0
  68. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
  69. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
  70. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
  71. package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
  72. package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
  73. package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
  74. package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
  75. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
  76. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
  77. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
  78. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
  79. package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
  80. package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
  81. package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
  82. package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
  83. package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
  84. package/src/shared/events/usersEvents.js +19 -0
  85. package/src/shared/index.js +91 -0
  86. package/src/shared/operationMessages.js +16 -0
  87. package/src/shared/resources/consoleSettingsFields.js +55 -0
  88. package/src/shared/resources/consoleSettingsResource.js +139 -0
  89. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  90. package/src/shared/resources/userProfileResource.js +148 -0
  91. package/src/shared/resources/userSettingsFields.js +71 -0
  92. package/src/shared/resources/userSettingsResource.js +416 -0
  93. package/src/shared/resources/workspaceMembersResource.js +352 -0
  94. package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
  95. package/src/shared/resources/workspaceResource.js +149 -0
  96. package/src/shared/resources/workspaceSettingsFields.js +60 -0
  97. package/src/shared/resources/workspaceSettingsResource.js +178 -0
  98. package/src/shared/roles.js +136 -0
  99. package/src/shared/settings.js +31 -0
  100. package/src/shared/support/usersApiPaths.js +34 -0
  101. package/src/shared/support/usersVisibility.js +45 -0
  102. package/src/shared/support/workspacePathModel.js +145 -0
  103. package/src/shared/tenancyMode.js +35 -0
  104. package/src/shared/tenancyProfile.js +73 -0
  105. package/templates/config/workspaceRoles.js +30 -0
  106. package/templates/migrations/users_core_console_owner.cjs +39 -0
  107. package/templates/migrations/users_core_initial.cjs +118 -0
  108. package/templates/migrations/users_core_profile_username.cjs +98 -0
  109. package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
  110. package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
  111. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
  112. package/test/authProfileSyncService.test.js +119 -0
  113. package/test/avatarService.test.js +114 -0
  114. package/test/avatarStorageService.test.js +61 -0
  115. package/test/consoleService.test.js +57 -0
  116. package/test/consoleSettingsService.test.js +86 -0
  117. package/test/exportsContract.test.js +38 -0
  118. package/test/registerAvatarMultipartSupport.test.js +64 -0
  119. package/test/registerServiceRealtimeEvents.test.js +160 -0
  120. package/test/registerWorkspaceDirectory.test.js +26 -0
  121. package/test/registerWorkspaceSettings.test.js +44 -0
  122. package/test/resourcesCanonical.test.js +90 -0
  123. package/test/roles.test.js +74 -0
  124. package/test/settingsFieldRegistriesSingleton.test.js +24 -0
  125. package/test/tenancyProfile.test.js +67 -0
  126. package/test/userSettingsResource.test.js +31 -0
  127. package/test/usersApiPaths.test.js +31 -0
  128. package/test/usersRouteRequestInputValidator.test.js +556 -0
  129. package/test/usersRouteResources.test.js +113 -0
  130. package/test/usersRouteValidators.test.js +49 -0
  131. package/test/usersVisibility.test.js +22 -0
  132. package/test/workspaceActionContextContributor.test.js +251 -0
  133. package/test/workspaceActionSurfaces.test.js +105 -0
  134. package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
  135. package/test/workspaceBootstrapContributor.test.js +466 -0
  136. package/test/workspaceInvitationsPolicy.test.js +71 -0
  137. package/test/workspaceInvitesRepository.test.js +111 -0
  138. package/test/workspaceMembersService.test.js +400 -0
  139. package/test/workspacePathModel.test.js +93 -0
  140. package/test/workspacePendingInvitationsResource.test.js +38 -0
  141. package/test/workspacePendingInvitationsService.test.js +151 -0
  142. package/test/workspaceRouteVisibilityResolver.test.js +83 -0
  143. package/test/workspaceService.test.js +480 -0
  144. package/test/workspaceSettingsActions.test.js +42 -0
  145. package/test/workspaceSettingsRepository.test.js +156 -0
  146. package/test/workspaceSettingsResource.test.js +156 -0
  147. package/test/workspaceSettingsService.test.js +120 -0
  148. package/test-support/registerDefaultSettingsFields.js +3 -0
@@ -0,0 +1,178 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
+ import {
4
+ normalizeObjectInput,
5
+ createCursorListValidator
6
+ } from "@jskit-ai/kernel/shared/validators";
7
+ import { workspaceSettingsFields } from "./workspaceSettingsFields.js";
8
+ import { createWorkspaceRoleCatalog } from "../roles.js";
9
+
10
+ function buildCreateBodySchema() {
11
+ const properties = {};
12
+ for (const field of workspaceSettingsFields) {
13
+ properties[field.key] = field.required === false ? Type.Optional(field.inputSchema) : field.inputSchema;
14
+ }
15
+
16
+ return Type.Object(properties, {
17
+ additionalProperties: false,
18
+ messages: {
19
+ additionalProperties: "Unexpected field.",
20
+ default: "Invalid value."
21
+ }
22
+ });
23
+ }
24
+
25
+ function buildSettingsOutputSchema() {
26
+ const properties = {};
27
+ for (const field of workspaceSettingsFields) {
28
+ properties[field.key] = field.outputSchema;
29
+ }
30
+ properties.invitesAvailable = Type.Boolean();
31
+ properties.invitesEffective = Type.Boolean();
32
+
33
+ return Type.Object(properties, { additionalProperties: false });
34
+ }
35
+
36
+ function buildResponseRecordSchema() {
37
+ return Type.Object(
38
+ {
39
+ workspace: Type.Object(
40
+ {
41
+ id: Type.Integer({ minimum: 1 }),
42
+ slug: Type.String({ minLength: 1 }),
43
+ ownerUserId: Type.Integer({ minimum: 1 })
44
+ },
45
+ { additionalProperties: false }
46
+ ),
47
+ settings: buildSettingsOutputSchema(),
48
+ roleCatalog: Type.Object(
49
+ {
50
+ collaborationEnabled: Type.Boolean(),
51
+ defaultInviteRole: Type.String(),
52
+ roles: Type.Array(Type.Object({}, { additionalProperties: true })),
53
+ assignableRoleIds: Type.Array(Type.String({ minLength: 1 }))
54
+ },
55
+ { additionalProperties: true }
56
+ )
57
+ },
58
+ { additionalProperties: false }
59
+ );
60
+ }
61
+
62
+ function normalizeInput(payload = {}) {
63
+ const source = normalizeObjectInput(payload);
64
+ const normalized = {};
65
+
66
+ for (const field of workspaceSettingsFields) {
67
+ if (!Object.hasOwn(source, field.key)) {
68
+ continue;
69
+ }
70
+ normalized[field.key] = field.normalizeInput(source[field.key], {
71
+ payload: source
72
+ });
73
+ }
74
+
75
+ return normalized;
76
+ }
77
+
78
+ function normalizeOutput(payload = {}) {
79
+ const source = normalizeObjectInput(payload);
80
+ const workspace = normalizeObjectInput(source.workspace);
81
+ const settings = normalizeObjectInput(source.settings);
82
+ const normalizedSettings = {};
83
+
84
+ for (const field of workspaceSettingsFields) {
85
+ const rawValue = Object.hasOwn(settings, field.key)
86
+ ? settings[field.key]
87
+ : field.resolveDefault({
88
+ workspace,
89
+ settings
90
+ });
91
+ normalizedSettings[field.key] = field.normalizeOutput(rawValue, {
92
+ workspace,
93
+ settings
94
+ });
95
+ }
96
+
97
+ const invitesEnabled = normalizedSettings.invitesEnabled !== false;
98
+ const invitesAvailable = settings.invitesAvailable !== false;
99
+ const invitesEffective =
100
+ typeof settings.invitesEffective === "boolean" ? settings.invitesEffective : invitesEnabled;
101
+ normalizedSettings.invitesEnabled = invitesEnabled;
102
+ normalizedSettings.invitesAvailable = invitesAvailable;
103
+ normalizedSettings.invitesEffective = invitesEffective;
104
+ const roleCatalog = normalizeObjectInput(source.roleCatalog);
105
+ const hasRoleCatalog =
106
+ Array.isArray(roleCatalog.roles) &&
107
+ roleCatalog.roles.length > 0 &&
108
+ Array.isArray(roleCatalog.assignableRoleIds);
109
+
110
+ return {
111
+ workspace: {
112
+ id: Number(workspace.id),
113
+ slug: normalizeText(workspace.slug),
114
+ ownerUserId: Number(workspace.ownerUserId)
115
+ },
116
+ settings: normalizedSettings,
117
+ roleCatalog: hasRoleCatalog ? roleCatalog : createWorkspaceRoleCatalog()
118
+ };
119
+ }
120
+
121
+ const responseRecordValidator = Object.freeze({
122
+ get schema() {
123
+ return buildResponseRecordSchema();
124
+ },
125
+ normalize: normalizeOutput
126
+ });
127
+
128
+ const resource = {
129
+ resource: "workspaceSettings",
130
+ messages: {
131
+ validation: "Fix invalid workspace settings values and try again.",
132
+ saveSuccess: "Workspace settings updated.",
133
+ saveError: "Unable to update workspace settings.",
134
+ apiValidation: "Validation failed."
135
+ },
136
+ operations: {
137
+ view: {
138
+ method: "GET",
139
+ outputValidator: responseRecordValidator
140
+ },
141
+ list: {
142
+ method: "GET",
143
+ outputValidator: createCursorListValidator(responseRecordValidator)
144
+ },
145
+ create: {
146
+ method: "POST",
147
+ bodyValidator: {
148
+ get schema() {
149
+ return buildCreateBodySchema();
150
+ },
151
+ normalize: normalizeInput
152
+ },
153
+ outputValidator: responseRecordValidator
154
+ },
155
+ replace: {
156
+ method: "PUT",
157
+ bodyValidator: {
158
+ get schema() {
159
+ return buildCreateBodySchema();
160
+ },
161
+ normalize: normalizeInput
162
+ },
163
+ outputValidator: responseRecordValidator
164
+ },
165
+ patch: {
166
+ method: "PATCH",
167
+ bodyValidator: {
168
+ get schema() {
169
+ return Type.Partial(buildCreateBodySchema(), { additionalProperties: false });
170
+ },
171
+ normalize: normalizeInput
172
+ },
173
+ outputValidator: responseRecordValidator
174
+ }
175
+ }
176
+ };
177
+
178
+ export { resource as workspaceSettingsResource };
@@ -0,0 +1,136 @@
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 createRoleDescriptor(roleId, configuredDefinition) {
24
+ const source = asRecord(configuredDefinition);
25
+ const assignable = roleId === OWNER_ROLE_ID ? false : source.assignable === true;
26
+ const permissions = normalizePermissionList(source.permissions);
27
+
28
+ return Object.freeze({
29
+ id: roleId,
30
+ assignable,
31
+ permissions: Object.freeze([...permissions])
32
+ });
33
+ }
34
+
35
+ function listConfiguredRoleIds(appConfig = {}) {
36
+ const configuredRoles = normalizeConfiguredRoles(appConfig);
37
+ return Object.freeze(Object.keys(configuredRoles));
38
+ }
39
+
40
+ function resolveConfiguredDefaultInviteRole(appConfig = {}) {
41
+ return normalizeRoleId(appConfig?.workspaceRoles?.defaultInviteRole);
42
+ }
43
+
44
+ function normalizeConfiguredRoles(appConfig = {}) {
45
+ const workspaceRoles = asRecord(appConfig?.workspaceRoles);
46
+ const configuredRoles = asRecord(workspaceRoles.roles);
47
+ const normalizedRoles = {};
48
+
49
+ for (const [roleId, roleDefinition] of Object.entries(configuredRoles)) {
50
+ const normalizedRoleId = normalizeRoleId(roleId);
51
+ if (!normalizedRoleId) {
52
+ continue;
53
+ }
54
+ normalizedRoles[normalizedRoleId] = roleDefinition;
55
+ }
56
+
57
+ return normalizedRoles;
58
+ }
59
+
60
+ function createWorkspaceRoleCatalog(appConfig = {}) {
61
+ const configuredRoles = normalizeConfiguredRoles(appConfig);
62
+ const roleIds = listConfiguredRoleIds(appConfig);
63
+ const roles = roleIds.map((roleId) => createRoleDescriptor(roleId, configuredRoles[roleId]));
64
+ const assignableRoleIds = roles.filter((role) => role.assignable).map((role) => role.id);
65
+ const configuredDefaultInviteRole = resolveConfiguredDefaultInviteRole(appConfig);
66
+ const defaultInviteRole = assignableRoleIds.includes(configuredDefaultInviteRole)
67
+ ? configuredDefaultInviteRole
68
+ : assignableRoleIds[0] || "";
69
+
70
+ return Object.freeze({
71
+ collaborationEnabled: assignableRoleIds.length > 0 && Boolean(defaultInviteRole),
72
+ defaultInviteRole,
73
+ roles: Object.freeze(
74
+ roles.map((role) =>
75
+ Object.freeze({
76
+ id: role.id,
77
+ assignable: role.assignable,
78
+ permissions: Object.freeze([...role.permissions])
79
+ })
80
+ )
81
+ ),
82
+ assignableRoleIds: Object.freeze([...assignableRoleIds])
83
+ });
84
+ }
85
+
86
+ function cloneWorkspaceRoleCatalog(roleCatalog = null) {
87
+ const source = asRecord(roleCatalog);
88
+
89
+ return {
90
+ collaborationEnabled: source.collaborationEnabled === true,
91
+ defaultInviteRole: String(source.defaultInviteRole || ""),
92
+ roles: Array.isArray(source.roles)
93
+ ? source.roles.map((role) => ({
94
+ id: normalizeRoleId(role?.id),
95
+ assignable: role?.assignable === true,
96
+ permissions: Array.isArray(role?.permissions) ? [...role.permissions] : []
97
+ }))
98
+ : [],
99
+ assignableRoleIds: Array.isArray(source.assignableRoleIds) ? [...source.assignableRoleIds] : []
100
+ };
101
+ }
102
+
103
+ function listRoleDescriptors(appConfig = {}) {
104
+ const roleCatalog = createWorkspaceRoleCatalog(appConfig);
105
+ return roleCatalog.roles.map((role) => ({
106
+ id: role.id,
107
+ assignable: role.assignable,
108
+ permissions: [...role.permissions]
109
+ }));
110
+ }
111
+
112
+ function resolveRolePermissions(roleId, appConfig = {}) {
113
+ const normalizedRoleId = normalizeRoleId(roleId);
114
+ if (!normalizedRoleId) {
115
+ return [];
116
+ }
117
+
118
+ const roleCatalog = createWorkspaceRoleCatalog(appConfig);
119
+ const role = roleCatalog.roles.find((entry) => entry.id === normalizedRoleId);
120
+ if (!role) {
121
+ return [];
122
+ }
123
+
124
+ return [...role.permissions];
125
+ }
126
+
127
+ export {
128
+ OWNER_ROLE_ID,
129
+ ADMIN_ROLE_ID,
130
+ MEMBER_ROLE_ID,
131
+ resolveRolePermissions,
132
+ listRoleDescriptors,
133
+ createWorkspaceRoleCatalog,
134
+ cloneWorkspaceRoleCatalog,
135
+ hasPermission
136
+ };
@@ -0,0 +1,31 @@
1
+ const DEFAULT_WORKSPACE_COLOR = "#0F6B54";
2
+
3
+ const DEFAULT_USER_SETTINGS = Object.freeze({
4
+ theme: "system",
5
+ locale: "en",
6
+ timeZone: "UTC",
7
+ dateFormat: "yyyy-mm-dd",
8
+ numberFormat: "1,234.56",
9
+ currencyCode: "USD",
10
+ avatarSize: 64,
11
+ productUpdates: true,
12
+ accountActivity: true,
13
+ securityAlerts: true,
14
+ passwordSignInEnabled: true,
15
+ passwordSetupRequired: false,
16
+ lastActiveWorkspaceId: null
17
+ });
18
+
19
+ function coerceWorkspaceColor(value) {
20
+ const normalized = String(value || "").trim();
21
+ if (/^#[0-9A-Fa-f]{6}$/.test(normalized)) {
22
+ return normalized.toUpperCase();
23
+ }
24
+ return DEFAULT_WORKSPACE_COLOR;
25
+ }
26
+
27
+ export {
28
+ DEFAULT_WORKSPACE_COLOR,
29
+ DEFAULT_USER_SETTINGS,
30
+ coerceWorkspaceColor
31
+ };
@@ -0,0 +1,34 @@
1
+ import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
2
+
3
+ const USERS_PUBLIC_API_BASE_PATH = "/api";
4
+ const USERS_WORKSPACE_API_BASE_PATH = "/api/w/:workspaceSlug/workspace";
5
+
6
+ function normalizeApiRelativePath(relativePath = "/") {
7
+ const normalizedPath = normalizePathname(relativePath);
8
+ return normalizedPath || "/";
9
+ }
10
+
11
+ function normalizeSurfaceWorkspaceRequirement(value = false) {
12
+ return value === true;
13
+ }
14
+
15
+ function resolveApiBasePath({ surfaceRequiresWorkspace = false, relativePath = "/" } = {}) {
16
+ const basePath = normalizeSurfaceWorkspaceRequirement(surfaceRequiresWorkspace)
17
+ ? USERS_WORKSPACE_API_BASE_PATH
18
+ : USERS_PUBLIC_API_BASE_PATH;
19
+ const normalizedRelativePath = normalizeApiRelativePath(relativePath);
20
+
21
+ if (normalizedRelativePath === "/") {
22
+ return basePath;
23
+ }
24
+
25
+ return `${basePath}${normalizedRelativePath}`;
26
+ }
27
+
28
+ export {
29
+ USERS_PUBLIC_API_BASE_PATH,
30
+ USERS_WORKSPACE_API_BASE_PATH,
31
+ normalizeApiRelativePath,
32
+ normalizeSurfaceWorkspaceRequirement,
33
+ resolveApiBasePath
34
+ };
@@ -0,0 +1,45 @@
1
+ import { normalizeRouteVisibilityToken } from "@jskit-ai/kernel/shared/support/visibility";
2
+
3
+ const USERS_ROUTE_VISIBILITY_PUBLIC = "public";
4
+ const USERS_ROUTE_VISIBILITY_USER = "user";
5
+ const USERS_ROUTE_VISIBILITY_WORKSPACE = "workspace";
6
+ const USERS_ROUTE_VISIBILITY_WORKSPACE_USER = "workspace_user";
7
+
8
+ const USERS_ROUTE_VISIBILITY_LEVELS = Object.freeze([
9
+ USERS_ROUTE_VISIBILITY_PUBLIC,
10
+ USERS_ROUTE_VISIBILITY_USER,
11
+ USERS_ROUTE_VISIBILITY_WORKSPACE,
12
+ USERS_ROUTE_VISIBILITY_WORKSPACE_USER
13
+ ]);
14
+ const USERS_ROUTE_VISIBILITY_LEVEL_SET = new Set(USERS_ROUTE_VISIBILITY_LEVELS);
15
+
16
+ function normalizeScopedRouteVisibility(value, { fallback = USERS_ROUTE_VISIBILITY_PUBLIC } = {}) {
17
+ const normalized = normalizeRouteVisibilityToken(value, { fallback: USERS_ROUTE_VISIBILITY_PUBLIC });
18
+ if (USERS_ROUTE_VISIBILITY_LEVEL_SET.has(normalized)) {
19
+ return normalized;
20
+ }
21
+
22
+ const normalizedFallback = normalizeRouteVisibilityToken(fallback, { fallback: USERS_ROUTE_VISIBILITY_PUBLIC });
23
+ if (USERS_ROUTE_VISIBILITY_LEVEL_SET.has(normalizedFallback)) {
24
+ return normalizedFallback;
25
+ }
26
+
27
+ return USERS_ROUTE_VISIBILITY_PUBLIC;
28
+ }
29
+
30
+ function isWorkspaceVisibility(visibility = "") {
31
+ const normalized = normalizeScopedRouteVisibility(visibility, {
32
+ fallback: USERS_ROUTE_VISIBILITY_PUBLIC
33
+ });
34
+ return normalized === USERS_ROUTE_VISIBILITY_WORKSPACE || normalized === USERS_ROUTE_VISIBILITY_WORKSPACE_USER;
35
+ }
36
+
37
+ export {
38
+ USERS_ROUTE_VISIBILITY_PUBLIC,
39
+ USERS_ROUTE_VISIBILITY_USER,
40
+ USERS_ROUTE_VISIBILITY_WORKSPACE,
41
+ USERS_ROUTE_VISIBILITY_WORKSPACE_USER,
42
+ USERS_ROUTE_VISIBILITY_LEVELS,
43
+ normalizeScopedRouteVisibility,
44
+ isWorkspaceVisibility
45
+ };
@@ -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_WORKSPACE = "workspace";
4
+
5
+ const TENANCY_MODES = Object.freeze([
6
+ TENANCY_MODE_NONE,
7
+ TENANCY_MODE_PERSONAL,
8
+ TENANCY_MODE_WORKSPACE
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_WORKSPACE,
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_WORKSPACE,
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 isWorkspaceTenancyMode(value = "") {
60
+ return normalizeTenancyMode(value) === TENANCY_MODE_WORKSPACE;
61
+ }
62
+
63
+ export {
64
+ TENANCY_MODE_NONE,
65
+ TENANCY_MODE_PERSONAL,
66
+ TENANCY_MODE_WORKSPACE,
67
+ normalizeTenancyMode,
68
+ WORKSPACE_SLUG_POLICY_NONE,
69
+ WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
70
+ WORKSPACE_SLUG_POLICY_USER_SELECTED,
71
+ resolveTenancyProfile,
72
+ isWorkspaceTenancyMode
73
+ };