@jskit-ai/users-core 0.1.47 → 0.1.49

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 (104) hide show
  1. package/package.descriptor.mjs +9 -46
  2. package/package.json +8 -19
  3. package/src/server/UsersCoreServiceProvider.js +0 -4
  4. package/src/server/common/registerCommonRepositories.js +0 -5
  5. package/src/server/common/services/authProfileSyncService.js +28 -7
  6. package/src/server/common/support/realtimeServiceEvents.js +1 -59
  7. package/src/server/profileSyncLifecycleContributorRegistry.js +56 -0
  8. package/src/server/registerUsersBootstrap.js +1 -3
  9. package/src/server/registerUsersCore.js +2 -14
  10. package/src/server/usersBootstrapContributor.js +10 -85
  11. package/src/shared/index.js +2 -99
  12. package/src/shared/settings.js +1 -119
  13. package/templates/migrations/users_core_generic_initial.cjs +0 -16
  14. package/test/authProfileSyncService.test.js +19 -10
  15. package/test/registerServiceRealtimeEvents.test.js +0 -94
  16. package/test/registerUsersCore.test.js +6 -19
  17. package/test/repositoryContracts.test.js +1 -11
  18. package/test/resourcesCanonical.test.js +1 -19
  19. package/test/settingsFieldRegistriesSingleton.test.js +0 -10
  20. package/test/usersBootstrapContributor.test.js +20 -38
  21. package/test/usersRouteRequestInputValidator.test.js +2 -43
  22. package/test/usersRouteResources.test.js +2 -20
  23. package/test-support/registerDefaultSettingsFields.js +0 -1
  24. package/src/server/UsersWorkspacesServiceProvider.js +0 -44
  25. package/src/server/common/contributors/workspaceActionContextContributor.js +0 -88
  26. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +0 -34
  27. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +0 -78
  28. package/src/server/common/formatters/workspaceFormatter.js +0 -53
  29. package/src/server/common/repositories/workspaceInvitesRepository.js +0 -208
  30. package/src/server/common/repositories/workspaceMembershipsRepository.js +0 -190
  31. package/src/server/common/repositories/workspacesRepository.js +0 -202
  32. package/src/server/common/services/workspaceContextService.js +0 -281
  33. package/src/server/common/support/workspaceRoutePaths.js +0 -17
  34. package/src/server/common/validators/routeParamsValidator.js +0 -62
  35. package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +0 -63
  36. package/src/server/consoleSettings/consoleService.js +0 -36
  37. package/src/server/consoleSettings/consoleSettingsActions.js +0 -55
  38. package/src/server/consoleSettings/consoleSettingsRepository.js +0 -115
  39. package/src/server/consoleSettings/consoleSettingsService.js +0 -40
  40. package/src/server/consoleSettings/registerConsoleSettings.js +0 -56
  41. package/src/server/registerWorkspaceBootstrap.js +0 -27
  42. package/src/server/registerWorkspaceCore.js +0 -73
  43. package/src/server/registerWorkspaceRepositories.js +0 -26
  44. package/src/server/support/resolveWorkspace.js +0 -16
  45. package/src/server/support/workspaceActionSurfaces.js +0 -135
  46. package/src/server/support/workspaceInvitationsPolicy.js +0 -45
  47. package/src/server/support/workspaceRouteInput.js +0 -22
  48. package/src/server/workspaceBootstrapContributor.js +0 -211
  49. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +0 -133
  50. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
  51. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +0 -133
  52. package/src/server/workspaceMembers/bootWorkspaceMembers.js +0 -236
  53. package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -108
  54. package/src/server/workspaceMembers/workspaceMembersActions.js +0 -186
  55. package/src/server/workspaceMembers/workspaceMembersService.js +0 -222
  56. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +0 -62
  57. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -119
  58. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +0 -74
  59. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +0 -138
  60. package/src/server/workspaceSettings/bootWorkspaceSettings.js +0 -76
  61. package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -62
  62. package/src/server/workspaceSettings/workspaceSettingsActions.js +0 -72
  63. package/src/server/workspaceSettings/workspaceSettingsRepository.js +0 -154
  64. package/src/server/workspaceSettings/workspaceSettingsService.js +0 -66
  65. package/src/shared/resources/consoleSettingsFields.js +0 -54
  66. package/src/shared/resources/consoleSettingsResource.js +0 -119
  67. package/src/shared/resources/workspaceMembersResource.js +0 -354
  68. package/src/shared/resources/workspacePendingInvitationsResource.js +0 -82
  69. package/src/shared/resources/workspaceResource.js +0 -176
  70. package/src/shared/resources/workspaceSettingsFields.js +0 -59
  71. package/src/shared/resources/workspaceSettingsResource.js +0 -169
  72. package/src/shared/roles.js +0 -161
  73. package/src/shared/support/usersApiPaths.js +0 -43
  74. package/src/shared/support/usersVisibility.js +0 -42
  75. package/src/shared/support/workspacePathModel.js +0 -145
  76. package/src/shared/tenancyMode.js +0 -35
  77. package/src/shared/tenancyProfile.js +0 -73
  78. package/templates/migrations/users_core_console_owner.cjs +0 -37
  79. package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +0 -11
  80. package/test/consoleService.test.js +0 -57
  81. package/test/consoleSettingsService.test.js +0 -86
  82. package/test/registerWorkspaceDirectory.test.js +0 -31
  83. package/test/registerWorkspaceSettings.test.js +0 -40
  84. package/test/roles.test.js +0 -159
  85. package/test/tenancyProfile.test.js +0 -67
  86. package/test/usersApiPaths.test.js +0 -49
  87. package/test/usersRouteValidators.test.js +0 -49
  88. package/test/usersVisibility.test.js +0 -27
  89. package/test/workspaceActionContextContributor.test.js +0 -344
  90. package/test/workspaceActionSurfaces.test.js +0 -105
  91. package/test/workspaceAuthPolicyContextResolver.test.js +0 -119
  92. package/test/workspaceBootstrapContributor.test.js +0 -154
  93. package/test/workspaceInvitationsPolicy.test.js +0 -71
  94. package/test/workspaceInvitesRepository.test.js +0 -111
  95. package/test/workspaceMembersService.test.js +0 -398
  96. package/test/workspacePathModel.test.js +0 -93
  97. package/test/workspacePendingInvitationsResource.test.js +0 -38
  98. package/test/workspacePendingInvitationsService.test.js +0 -151
  99. package/test/workspaceRouteVisibilityResolver.test.js +0 -83
  100. package/test/workspaceService.test.js +0 -546
  101. package/test/workspaceSettingsActions.test.js +0 -52
  102. package/test/workspaceSettingsRepository.test.js +0 -202
  103. package/test/workspaceSettingsResource.test.js +0 -169
  104. package/test/workspaceSettingsService.test.js +0 -140
@@ -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,37 +0,0 @@
1
- /**
2
- * @param {import('knex').Knex} knex
3
- */
4
- exports.up = async function up(knex) {
5
- const hasConsoleSettingsTable = await knex.schema.hasTable("console_settings");
6
- if (!hasConsoleSettingsTable) {
7
- return;
8
- }
9
-
10
- const hasOwnerUserId = await knex.schema.hasColumn("console_settings", "owner_user_id");
11
- if (hasOwnerUserId) {
12
- return;
13
- }
14
-
15
- await knex.schema.alterTable("console_settings", (table) => {
16
- table.bigInteger("owner_user_id").unsigned().nullable();
17
- });
18
- };
19
-
20
- /**
21
- * @param {import('knex').Knex} knex
22
- */
23
- exports.down = async function down(knex) {
24
- const hasConsoleSettingsTable = await knex.schema.hasTable("console_settings");
25
- if (!hasConsoleSettingsTable) {
26
- return;
27
- }
28
-
29
- const hasOwnerUserId = await knex.schema.hasColumn("console_settings", "owner_user_id");
30
- if (!hasOwnerUserId) {
31
- return;
32
- }
33
-
34
- await knex.schema.alterTable("console_settings", (table) => {
35
- table.dropColumn("owner_user_id");
36
- });
37
- };
@@ -1,11 +0,0 @@
1
- // @jskit-contract users.settings-fields.console.v1
2
- // Append-only settings field registrations for console settings.
3
-
4
- import {
5
- defineField,
6
- resetConsoleSettingsFields
7
- } from "@jskit-ai/users-core/shared/resources/consoleSettingsFields";
8
-
9
- resetConsoleSettingsFields();
10
-
11
- void defineField;
@@ -1,57 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { createService } from "../src/server/consoleSettings/consoleService.js";
4
-
5
- function createFixture(initialOwnerUserId = null) {
6
- const state = {
7
- ownerUserId: initialOwnerUserId
8
- };
9
-
10
- const service = createService({
11
- consoleSettingsRepository: {
12
- async ensureOwnerUserId(userId) {
13
- const normalizedUserId = String(userId || "");
14
- if (!state.ownerUserId) {
15
- state.ownerUserId = normalizedUserId;
16
- }
17
- return state.ownerUserId;
18
- }
19
- }
20
- });
21
-
22
- return { service, state };
23
- }
24
-
25
- test("consoleService seeds the first authenticated user as console owner", async () => {
26
- const { service, state } = createFixture();
27
-
28
- const firstOwner = await service.ensureInitialConsoleMember("7");
29
- const secondAttempt = await service.ensureInitialConsoleMember("9");
30
-
31
- assert.equal(firstOwner, "7");
32
- assert.equal(secondAttempt, "7");
33
- assert.equal(state.ownerUserId, "7");
34
- });
35
-
36
- test("consoleService.requireConsoleOwner denies authenticated non-owners", async () => {
37
- const { service } = createFixture("7");
38
-
39
- await assert.rejects(
40
- () =>
41
- service.requireConsoleOwner({
42
- actor: {
43
- id: "9"
44
- }
45
- }),
46
- (error) => error?.status === 403
47
- );
48
- });
49
-
50
- test("consoleService.requireConsoleOwner requires authentication", async () => {
51
- const { service } = createFixture("7");
52
-
53
- await assert.rejects(
54
- () => service.requireConsoleOwner({}),
55
- (error) => error?.status === 401
56
- );
57
- });
@@ -1,86 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { createService } from "../src/server/consoleSettings/consoleSettingsService.js";
4
-
5
- function createFixture({ deny = false } = {}) {
6
- const calls = {
7
- requireConsoleOwner: [],
8
- updateSingleton: []
9
- };
10
-
11
- const service = createService({
12
- consoleService: {
13
- async requireConsoleOwner(context) {
14
- calls.requireConsoleOwner.push(context || null);
15
- if (deny) {
16
- const error = new Error("Forbidden.");
17
- error.status = 403;
18
- throw error;
19
- }
20
- }
21
- },
22
- consoleSettingsRepository: {
23
- async getSingleton() {
24
- return {};
25
- },
26
- async updateSingleton(patch = {}) {
27
- calls.updateSingleton.push({ ...patch });
28
- return {};
29
- }
30
- }
31
- });
32
-
33
- return { service, calls };
34
- }
35
-
36
- test("consoleSettingsService.getSettings requires owner access and returns normalized payload", async () => {
37
- const { service, calls } = createFixture();
38
- const context = {
39
- actor: {
40
- id: 7
41
- }
42
- };
43
-
44
- const response = await service.getSettings({ context });
45
-
46
- assert.deepEqual(calls.requireConsoleOwner, [context]);
47
- assert.deepEqual(response, {
48
- settings: {}
49
- });
50
- });
51
-
52
- test("consoleSettingsService.updateSettings requires owner access before writing", async () => {
53
- const { service, calls } = createFixture();
54
- const context = {
55
- actor: {
56
- id: 7
57
- }
58
- };
59
-
60
- const response = await service.updateSettings(
61
- {},
62
- { context }
63
- );
64
-
65
- assert.deepEqual(calls.requireConsoleOwner, [context]);
66
- assert.deepEqual(calls.updateSingleton, [{}]);
67
- assert.deepEqual(response, {
68
- settings: {}
69
- });
70
- });
71
-
72
- test("consoleSettingsService denies access when owner validation fails", async () => {
73
- const { service } = createFixture({ deny: true });
74
-
75
- await assert.rejects(
76
- () =>
77
- service.getSettings({
78
- context: {
79
- actor: {
80
- id: 9
81
- }
82
- }
83
- }),
84
- (error) => error?.status === 403
85
- );
86
- });
@@ -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
- });