@jskit-ai/users-core 0.1.48 → 0.1.50

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 (87) hide show
  1. package/package.descriptor.mjs +7 -7
  2. package/package.json +7 -17
  3. package/src/server/common/services/authProfileSyncService.js +28 -7
  4. package/src/server/common/support/realtimeServiceEvents.js +1 -59
  5. package/src/server/profileSyncLifecycleContributorRegistry.js +56 -0
  6. package/src/server/registerUsersBootstrap.js +0 -1
  7. package/src/server/registerUsersCore.js +2 -14
  8. package/src/server/usersBootstrapContributor.js +2 -64
  9. package/src/shared/index.js +2 -99
  10. package/src/shared/settings.js +1 -119
  11. package/test/authProfileSyncService.test.js +19 -10
  12. package/test/registerServiceRealtimeEvents.test.js +0 -86
  13. package/test/registerUsersCore.test.js +6 -15
  14. package/test/repositoryContracts.test.js +1 -9
  15. package/test/resourcesCanonical.test.js +0 -16
  16. package/test/settingsFieldRegistriesSingleton.test.js +0 -5
  17. package/test/usersBootstrapContributor.test.js +2 -26
  18. package/test/usersRouteResources.test.js +0 -16
  19. package/src/server/UsersWorkspacesServiceProvider.js +0 -44
  20. package/src/server/common/contributors/workspaceActionContextContributor.js +0 -88
  21. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +0 -34
  22. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +0 -78
  23. package/src/server/common/formatters/workspaceFormatter.js +0 -53
  24. package/src/server/common/repositories/workspaceInvitesRepository.js +0 -208
  25. package/src/server/common/repositories/workspaceMembershipsRepository.js +0 -190
  26. package/src/server/common/repositories/workspacesRepository.js +0 -202
  27. package/src/server/common/services/workspaceContextService.js +0 -281
  28. package/src/server/common/support/workspaceRoutePaths.js +0 -17
  29. package/src/server/common/validators/routeParamsValidator.js +0 -62
  30. package/src/server/registerWorkspaceBootstrap.js +0 -27
  31. package/src/server/registerWorkspaceCore.js +0 -73
  32. package/src/server/registerWorkspaceRepositories.js +0 -26
  33. package/src/server/support/resolveWorkspace.js +0 -16
  34. package/src/server/support/workspaceActionSurfaces.js +0 -118
  35. package/src/server/support/workspaceInvitationsPolicy.js +0 -45
  36. package/src/server/support/workspaceRouteInput.js +0 -22
  37. package/src/server/workspaceBootstrapContributor.js +0 -212
  38. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +0 -133
  39. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
  40. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +0 -133
  41. package/src/server/workspaceMembers/bootWorkspaceMembers.js +0 -236
  42. package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -108
  43. package/src/server/workspaceMembers/workspaceMembersActions.js +0 -186
  44. package/src/server/workspaceMembers/workspaceMembersService.js +0 -222
  45. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +0 -62
  46. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -119
  47. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +0 -74
  48. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +0 -138
  49. package/src/server/workspaceSettings/bootWorkspaceSettings.js +0 -76
  50. package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -62
  51. package/src/server/workspaceSettings/workspaceSettingsActions.js +0 -72
  52. package/src/server/workspaceSettings/workspaceSettingsRepository.js +0 -154
  53. package/src/server/workspaceSettings/workspaceSettingsService.js +0 -66
  54. package/src/shared/resources/workspaceMembersResource.js +0 -354
  55. package/src/shared/resources/workspacePendingInvitationsResource.js +0 -82
  56. package/src/shared/resources/workspaceResource.js +0 -176
  57. package/src/shared/resources/workspaceSettingsFields.js +0 -59
  58. package/src/shared/resources/workspaceSettingsResource.js +0 -169
  59. package/src/shared/roles.js +0 -161
  60. package/src/shared/support/usersApiPaths.js +0 -43
  61. package/src/shared/support/usersVisibility.js +0 -42
  62. package/src/shared/support/workspacePathModel.js +0 -145
  63. package/src/shared/tenancyMode.js +0 -35
  64. package/src/shared/tenancyProfile.js +0 -73
  65. package/test/registerWorkspaceDirectory.test.js +0 -31
  66. package/test/registerWorkspaceSettings.test.js +0 -40
  67. package/test/roles.test.js +0 -159
  68. package/test/tenancyProfile.test.js +0 -67
  69. package/test/usersApiPaths.test.js +0 -49
  70. package/test/usersRouteValidators.test.js +0 -49
  71. package/test/usersVisibility.test.js +0 -27
  72. package/test/workspaceActionContextContributor.test.js +0 -344
  73. package/test/workspaceActionSurfaces.test.js +0 -85
  74. package/test/workspaceAuthPolicyContextResolver.test.js +0 -119
  75. package/test/workspaceBootstrapContributor.test.js +0 -154
  76. package/test/workspaceInvitationsPolicy.test.js +0 -71
  77. package/test/workspaceInvitesRepository.test.js +0 -111
  78. package/test/workspaceMembersService.test.js +0 -398
  79. package/test/workspacePathModel.test.js +0 -93
  80. package/test/workspacePendingInvitationsResource.test.js +0 -38
  81. package/test/workspacePendingInvitationsService.test.js +0 -151
  82. package/test/workspaceRouteVisibilityResolver.test.js +0 -83
  83. package/test/workspaceService.test.js +0 -546
  84. package/test/workspaceSettingsActions.test.js +0 -52
  85. package/test/workspaceSettingsRepository.test.js +0 -202
  86. package/test/workspaceSettingsResource.test.js +0 -169
  87. package/test/workspaceSettingsService.test.js +0 -140
@@ -1,42 +0,0 @@
1
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
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 checkRouteVisibility(value, { context = "checkRouteVisibility" } = {}) {
17
- const normalized = normalizeText(value).toLowerCase();
18
- if (USERS_ROUTE_VISIBILITY_LEVEL_SET.has(normalized)) {
19
- return normalized;
20
- }
21
-
22
- throw new TypeError(
23
- `${context} must be one of: ${USERS_ROUTE_VISIBILITY_LEVELS.join(", ")}.`
24
- );
25
- }
26
-
27
- function isWorkspaceVisibility(visibility = "") {
28
- const normalized = checkRouteVisibility(visibility, {
29
- context: "isWorkspaceVisibility visibility"
30
- });
31
- return normalized === USERS_ROUTE_VISIBILITY_WORKSPACE || normalized === USERS_ROUTE_VISIBILITY_WORKSPACE_USER;
32
- }
33
-
34
- export {
35
- USERS_ROUTE_VISIBILITY_PUBLIC,
36
- USERS_ROUTE_VISIBILITY_USER,
37
- USERS_ROUTE_VISIBILITY_WORKSPACE,
38
- USERS_ROUTE_VISIBILITY_WORKSPACE_USER,
39
- USERS_ROUTE_VISIBILITY_LEVELS,
40
- checkRouteVisibility,
41
- isWorkspaceVisibility
42
- };
@@ -1,145 +0,0 @@
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
- };
@@ -1,35 +0,0 @@
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
- };
@@ -1,73 +0,0 @@
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
- };
@@ -1,31 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerWorkspaceDirectory } from "../src/server/workspaceDirectory/registerWorkspaceDirectory.js";
4
-
5
- function createAppDouble() {
6
- const actionBatches = [];
7
-
8
- return {
9
- actionBatches,
10
- singleton() {},
11
- actions(entries) {
12
- actionBatches.push(Array.isArray(entries) ? entries : [entries]);
13
- }
14
- };
15
- }
16
-
17
- function listActionIds(app) {
18
- return app.actionBatches.flat().map((entry) => String(entry?.id || ""));
19
- }
20
-
21
- test("registerWorkspaceDirectory registers workspace directory actions without resolving runtime tenancy tokens", () => {
22
- const app = createAppDouble();
23
-
24
- registerWorkspaceDirectory(app);
25
- assert.deepEqual(listActionIds(app), [
26
- "workspace.workspaces.create",
27
- "workspace.workspaces.list",
28
- "workspace.workspaces.read",
29
- "workspace.workspaces.update"
30
- ]);
31
- });
@@ -1,40 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerWorkspaceSettings } from "../src/server/workspaceSettings/registerWorkspaceSettings.js";
4
-
5
- test("registerWorkspaceSettings registers workspace settings service realtime event metadata", () => {
6
- const singletonBindings = new Map();
7
- const actionCalls = [];
8
- const serviceCalls = [];
9
-
10
- const app = {
11
- singleton(token, factory) {
12
- singletonBindings.set(token, factory);
13
- return this;
14
- },
15
- service(token, factory, metadata) {
16
- serviceCalls.push({
17
- token,
18
- factory,
19
- metadata
20
- });
21
- return this;
22
- },
23
- actions(definitions) {
24
- actionCalls.push(definitions);
25
- return this;
26
- }
27
- };
28
-
29
- registerWorkspaceSettings(app);
30
-
31
- assert.equal(singletonBindings.has("workspaceSettingsRepository"), true);
32
- assert.equal(serviceCalls.length, 1);
33
- assert.equal(serviceCalls[0].token, "users.workspace.settings.service");
34
- assert.equal(typeof serviceCalls[0].factory, "function");
35
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.event, "workspace.settings.changed");
36
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.audience, "event_scope");
37
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.event, "users.bootstrap.changed");
38
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.audience, "event_scope");
39
- assert.equal(actionCalls.length, 1);
40
- });
@@ -1,159 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- createWorkspaceRoleCatalog,
5
- cloneWorkspaceRoleCatalog,
6
- resolveRolePermissions,
7
- hasPermission
8
- } from "../src/shared/roles.js";
9
-
10
- test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.roleCatalog", () => {
11
- const emptyCatalog = createWorkspaceRoleCatalog();
12
- assert.deepEqual(emptyCatalog.roles, []);
13
- assert.deepEqual(emptyCatalog.assignableRoleIds, []);
14
- assert.equal(emptyCatalog.defaultInviteRole, "");
15
- assert.equal(emptyCatalog.collaborationEnabled, false);
16
-
17
- const appConfig = {
18
- roleCatalog: {
19
- workspace: {
20
- defaultInviteRole: "editor"
21
- },
22
- roles: {
23
- owner: {
24
- assignable: false,
25
- permissions: ["workspace.settings.update"]
26
- },
27
- editor: {
28
- assignable: true,
29
- permissions: ["crud.contacts.*"]
30
- }
31
- }
32
- }
33
- };
34
- const roleCatalog = createWorkspaceRoleCatalog(appConfig);
35
- const editorRole = roleCatalog.roles.find((role) => role.id === "editor");
36
-
37
- assert.equal(roleCatalog.defaultInviteRole, "editor");
38
- assert.equal(roleCatalog.assignableRoleIds.includes("editor"), true);
39
- assert.deepEqual(resolveRolePermissions("owner", appConfig), ["workspace.settings.update"]);
40
- assert.equal(hasPermission(editorRole?.permissions, "crud.contacts.update"), true);
41
- });
42
-
43
- test("createWorkspaceRoleCatalog resolves inherited role permissions with parent permissions first", () => {
44
- const appConfig = {
45
- roleCatalog: {
46
- workspace: {
47
- defaultInviteRole: "member"
48
- },
49
- roles: {
50
- member: {
51
- assignable: true,
52
- permissions: [
53
- "workspace.settings.view",
54
- "crud.contacts.list"
55
- ]
56
- },
57
- admin: {
58
- assignable: true,
59
- inherits: "member",
60
- permissions: [
61
- "workspace.settings.update",
62
- "workspace.members.manage",
63
- "workspace.settings.view"
64
- ]
65
- }
66
- }
67
- }
68
- };
69
-
70
- const roleCatalog = createWorkspaceRoleCatalog(appConfig);
71
- const adminRole = roleCatalog.roles.find((role) => role.id === "admin");
72
-
73
- assert.deepEqual(adminRole, {
74
- id: "admin",
75
- assignable: true,
76
- permissions: [
77
- "workspace.settings.view",
78
- "crud.contacts.list",
79
- "workspace.settings.update",
80
- "workspace.members.manage"
81
- ]
82
- });
83
- });
84
-
85
- test("createWorkspaceRoleCatalog rejects unknown inherited roles", () => {
86
- assert.throws(
87
- () =>
88
- createWorkspaceRoleCatalog({
89
- roleCatalog: {
90
- roles: {
91
- admin: {
92
- assignable: true,
93
- inherits: "member",
94
- permissions: []
95
- }
96
- }
97
- }
98
- }),
99
- /inherits unknown role "member"/
100
- );
101
- });
102
-
103
- test("createWorkspaceRoleCatalog rejects circular inherited roles", () => {
104
- assert.throws(
105
- () =>
106
- createWorkspaceRoleCatalog({
107
- roleCatalog: {
108
- roles: {
109
- member: {
110
- assignable: true,
111
- inherits: "admin",
112
- permissions: []
113
- },
114
- admin: {
115
- assignable: true,
116
- inherits: "member",
117
- permissions: []
118
- }
119
- }
120
- }
121
- }),
122
- /circular inheritance/
123
- );
124
- });
125
-
126
- test("cloneWorkspaceRoleCatalog normalizes role ids and returns detached arrays", () => {
127
- const source = {
128
- collaborationEnabled: true,
129
- defaultInviteRole: "member",
130
- roles: [
131
- {
132
- id: " MEMBER ",
133
- assignable: true,
134
- permissions: ["workspace.members.view"]
135
- }
136
- ],
137
- assignableRoleIds: ["member"]
138
- };
139
-
140
- const cloned = cloneWorkspaceRoleCatalog(source);
141
- assert.deepEqual(cloned, {
142
- collaborationEnabled: true,
143
- defaultInviteRole: "member",
144
- roles: [
145
- {
146
- id: "member",
147
- assignable: true,
148
- permissions: ["workspace.members.view"]
149
- }
150
- ],
151
- assignableRoleIds: ["member"]
152
- });
153
-
154
- cloned.roles[0].permissions.push("workspace.members.manage");
155
- cloned.assignableRoleIds.push("admin");
156
-
157
- assert.deepEqual(source.roles[0].permissions, ["workspace.members.view"]);
158
- assert.deepEqual(source.assignableRoleIds, ["member"]);
159
- });
@@ -1,67 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- TENANCY_MODE_NONE,
5
- TENANCY_MODE_PERSONAL,
6
- TENANCY_MODE_WORKSPACES,
7
- WORKSPACE_SLUG_POLICY_NONE,
8
- WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
9
- WORKSPACE_SLUG_POLICY_USER_SELECTED,
10
- resolveTenancyProfile,
11
- isWorkspacesTenancyMode
12
- } from "../src/shared/tenancyProfile.js";
13
-
14
- test("resolveTenancyProfile returns mode-specific workspace policy matrix", () => {
15
- const noneProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_NONE });
16
- assert.deepEqual(noneProfile, {
17
- mode: TENANCY_MODE_NONE,
18
- workspace: {
19
- enabled: false,
20
- autoProvision: false,
21
- allowSelfCreate: false,
22
- slugPolicy: WORKSPACE_SLUG_POLICY_NONE
23
- }
24
- });
25
-
26
- const personalProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_PERSONAL });
27
- assert.deepEqual(personalProfile, {
28
- mode: TENANCY_MODE_PERSONAL,
29
- workspace: {
30
- enabled: true,
31
- autoProvision: true,
32
- allowSelfCreate: false,
33
- slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
34
- }
35
- });
36
-
37
- const workspaceProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACES });
38
- assert.deepEqual(workspaceProfile, {
39
- mode: TENANCY_MODE_WORKSPACES,
40
- workspace: {
41
- enabled: true,
42
- autoProvision: false,
43
- allowSelfCreate: false,
44
- slugPolicy: WORKSPACE_SLUG_POLICY_USER_SELECTED
45
- }
46
- });
47
- });
48
-
49
- test("isWorkspacesTenancyMode is true only for workspace mode", () => {
50
- assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_WORKSPACES), true);
51
- assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_PERSONAL), false);
52
- assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_NONE), false);
53
- });
54
-
55
- test("resolveTenancyProfile allows explicit workspace self-create policy override", () => {
56
- const workspaceProfile = resolveTenancyProfile({
57
- tenancyMode: TENANCY_MODE_WORKSPACES,
58
- tenancyPolicy: {
59
- workspace: {
60
- allowSelfCreate: true
61
- }
62
- }
63
- });
64
-
65
- assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACES);
66
- assert.equal(workspaceProfile.workspace.allowSelfCreate, true);
67
- });
@@ -1,49 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- normalizeSurfaceWorkspaceRequirement,
5
- resolveApiBasePath
6
- } from "../src/shared/support/usersApiPaths.js";
7
-
8
- test("normalizeSurfaceWorkspaceRequirement only accepts explicit true", () => {
9
- assert.equal(normalizeSurfaceWorkspaceRequirement(true), true);
10
- assert.equal(normalizeSurfaceWorkspaceRequirement(false), false);
11
- assert.equal(normalizeSurfaceWorkspaceRequirement("true"), false);
12
- assert.equal(normalizeSurfaceWorkspaceRequirement(1), false);
13
- });
14
-
15
- test("resolveApiBasePath resolves workspace and non-workspace API base paths", () => {
16
- assert.equal(
17
- resolveApiBasePath({
18
- surfaceRequiresWorkspace: true,
19
- relativePath: "/customers"
20
- }),
21
- "/api/w/:workspaceSlug/customers"
22
- );
23
-
24
- assert.equal(
25
- resolveApiBasePath({
26
- surfaceRequiresWorkspace: false,
27
- relativePath: "/customers"
28
- }),
29
- "/api/customers"
30
- );
31
- });
32
-
33
- test("resolveApiBasePath preserves query strings and hash fragments", () => {
34
- assert.equal(
35
- resolveApiBasePath({
36
- surfaceRequiresWorkspace: true,
37
- relativePath: "/customers?search=buddy#top"
38
- }),
39
- "/api/w/:workspaceSlug/customers?search=buddy#top"
40
- );
41
-
42
- assert.equal(
43
- resolveApiBasePath({
44
- surfaceRequiresWorkspace: false,
45
- relativePath: "/?cursor=2"
46
- }),
47
- "/api?cursor=2"
48
- );
49
- });
@@ -1,49 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { Type } from "@fastify/type-provider-typebox";
4
- import { compileRouteValidator } from "@jskit-ai/kernel/_testable";
5
- import { routeParamsValidator } from "../src/server/common/validators/routeParamsValidator.js";
6
-
7
- test("routeParamsValidator exposes a shared route params validator", () => {
8
- assert.equal(typeof routeParamsValidator.schema, "object");
9
- assert.equal(typeof routeParamsValidator.normalize, "function");
10
- });
11
-
12
- test("route validator pipeline uses the shared params validator and merges query arrays automatically", () => {
13
- const paginationQueryValidator = Object.freeze({
14
- schema: Type.Object(
15
- {
16
- cursor: Type.Optional(Type.String({ minLength: 1 })),
17
- limit: Type.Optional(Type.String({ pattern: "^[0-9]+$" }))
18
- },
19
- { additionalProperties: false }
20
- )
21
- });
22
- const searchQueryValidator = Object.freeze({
23
- schema: Type.Object(
24
- {
25
- search: Type.Optional(Type.String({ minLength: 1 }))
26
- },
27
- { additionalProperties: false }
28
- )
29
- });
30
-
31
- const compiled = compileRouteValidator({
32
- paramsValidator: routeParamsValidator,
33
- queryValidator: [paginationQueryValidator, searchQueryValidator]
34
- });
35
-
36
- assert.equal(compiled.schema.params.type, "object");
37
- assert.equal(compiled.schema.params.additionalProperties, false);
38
- assert.equal(typeof compiled.schema.params.properties.workspaceSlug, "object");
39
- assert.equal(typeof compiled.schema.params.properties.memberUserId, "object");
40
- assert.equal(typeof compiled.schema.params.properties.inviteId, "object");
41
- assert.equal(typeof compiled.schema.params.properties.provider, "object");
42
- assert.equal(compiled.input.params({ workspaceSlug: " ACME " }).workspaceSlug, "acme");
43
-
44
- assert.equal(compiled.schema.querystring.type, "object");
45
- assert.equal(compiled.schema.querystring.additionalProperties, false);
46
- assert.equal(typeof compiled.schema.querystring.properties.cursor, "object");
47
- assert.equal(typeof compiled.schema.querystring.properties.limit, "object");
48
- assert.equal(typeof compiled.schema.querystring.properties.search, "object");
49
- });