@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,114 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { Readable } from "node:stream";
4
+ import { createService } from "../src/server/accountProfile/avatarService.js";
5
+
6
+ function createRepositoryDouble(initialProfile) {
7
+ const state = {
8
+ profile: { ...initialProfile }
9
+ };
10
+
11
+ return {
12
+ state,
13
+ async findByIdentity() {
14
+ return { ...state.profile };
15
+ },
16
+ async findById() {
17
+ return { ...state.profile };
18
+ },
19
+ async updateAvatarById(userId, avatar = {}) {
20
+ state.profile = {
21
+ ...state.profile,
22
+ id: Number(userId),
23
+ avatarStorageKey: avatar.avatarStorageKey || null,
24
+ avatarVersion: avatar.avatarVersion == null ? null : String(avatar.avatarVersion)
25
+ };
26
+ return { ...state.profile };
27
+ },
28
+ async clearAvatarById(userId) {
29
+ state.profile = {
30
+ ...state.profile,
31
+ id: Number(userId),
32
+ avatarStorageKey: null,
33
+ avatarVersion: null
34
+ };
35
+ return { ...state.profile };
36
+ }
37
+ };
38
+ }
39
+
40
+ test("avatarService uploadForUser stores bytes and updates profile avatar fields", async () => {
41
+ const repository = createRepositoryDouble({
42
+ id: 7,
43
+ authProvider: "local",
44
+ authProviderUserId: "u-7",
45
+ email: "test@example.com",
46
+ displayName: "Tester",
47
+ avatarStorageKey: null,
48
+ avatarVersion: null
49
+ });
50
+
51
+ const savedPayloads = [];
52
+ const avatarStorageService = {
53
+ async saveAvatar(payload) {
54
+ savedPayloads.push(payload);
55
+ return {
56
+ storageKey: "users/avatars/7/avatar"
57
+ };
58
+ }
59
+ };
60
+
61
+ const avatarService = createService({
62
+ userProfilesRepository: repository,
63
+ avatarStorageService
64
+ });
65
+
66
+ const user = {
67
+ authProvider: "local",
68
+ authProviderUserId: "u-7"
69
+ };
70
+
71
+ const result = await avatarService.uploadForUser(user, {
72
+ mimeType: "image/png",
73
+ stream: Readable.from([Buffer.from([0x89, 0x50, 0x4e, 0x47])])
74
+ });
75
+
76
+ assert.equal(savedPayloads.length, 1);
77
+ assert.equal(savedPayloads[0].userId, 7);
78
+ assert.ok(Buffer.isBuffer(savedPayloads[0].buffer));
79
+ assert.equal(result.profile.avatarStorageKey, "users/avatars/7/avatar");
80
+ assert.equal(typeof result.profile.avatarVersion, "string");
81
+ });
82
+
83
+ test("avatarService clearForUser removes stored avatar and clears profile fields", async () => {
84
+ const repository = createRepositoryDouble({
85
+ id: 7,
86
+ authProvider: "local",
87
+ authProviderUserId: "u-7",
88
+ email: "test@example.com",
89
+ displayName: "Tester",
90
+ avatarStorageKey: "users/avatars/7/avatar",
91
+ avatarVersion: "123"
92
+ });
93
+
94
+ const deletedKeys = [];
95
+ const avatarStorageService = {
96
+ async deleteAvatar(storageKey) {
97
+ deletedKeys.push(storageKey);
98
+ }
99
+ };
100
+
101
+ const avatarService = createService({
102
+ userProfilesRepository: repository,
103
+ avatarStorageService
104
+ });
105
+
106
+ const profile = await avatarService.clearForUser({
107
+ authProvider: "local",
108
+ authProviderUserId: "u-7"
109
+ });
110
+
111
+ assert.deepEqual(deletedKeys, ["users/avatars/7/avatar"]);
112
+ assert.equal(profile.avatarStorageKey, null);
113
+ assert.equal(profile.avatarVersion, null);
114
+ });
@@ -0,0 +1,61 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createService, __testables } from "../src/server/accountProfile/avatarStorageService.js";
4
+
5
+ function createStorageDouble() {
6
+ const values = new Map();
7
+ return {
8
+ async getItemRaw(key) {
9
+ return values.has(key) ? values.get(key) : null;
10
+ },
11
+ async setItemRaw(key, value) {
12
+ values.set(key, value);
13
+ },
14
+ async removeItem(key) {
15
+ values.delete(key);
16
+ }
17
+ };
18
+ }
19
+
20
+ test("avatarStorageService builds stable storage key by user id", () => {
21
+ assert.equal(__testables.buildAvatarStorageKey(7), "users/avatars/7/avatar");
22
+ assert.throws(() => __testables.buildAvatarStorageKey(0), /positive integer user id/);
23
+ });
24
+
25
+ test("avatarStorageService detects common avatar mime types", () => {
26
+ assert.equal(
27
+ __testables.detectAvatarMimeTypeFromBuffer(Buffer.from([0xff, 0xd8, 0xff, 0xdb])),
28
+ "image/jpeg"
29
+ );
30
+ assert.equal(
31
+ __testables.detectAvatarMimeTypeFromBuffer(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
32
+ "image/png"
33
+ );
34
+ assert.equal(
35
+ __testables.detectAvatarMimeTypeFromBuffer(
36
+ Buffer.from([0x52, 0x49, 0x46, 0x46, 0x11, 0x22, 0x33, 0x44, 0x57, 0x45, 0x42, 0x50])
37
+ ),
38
+ "image/webp"
39
+ );
40
+ });
41
+
42
+ test("avatarStorageService saves, reads, and deletes avatar bytes", async () => {
43
+ const storage = createStorageDouble();
44
+ const avatarStorageService = createService({ storage });
45
+ const payload = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
46
+
47
+ const saved = await avatarStorageService.saveAvatar({
48
+ userId: 42,
49
+ buffer: payload
50
+ });
51
+ assert.equal(saved.storageKey, "users/avatars/42/avatar");
52
+
53
+ const loaded = await avatarStorageService.readAvatar(saved.storageKey);
54
+ assert.equal(loaded?.mimeType, "image/png");
55
+ assert.ok(Buffer.isBuffer(loaded?.buffer));
56
+ assert.equal(loaded?.buffer?.toString("hex"), payload.toString("hex"));
57
+
58
+ await avatarStorageService.deleteAvatar(saved.storageKey);
59
+ const missing = await avatarStorageService.readAvatar(saved.storageKey);
60
+ assert.equal(missing, null);
61
+ });
@@ -0,0 +1,57 @@
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 = Number(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
+ });
@@ -0,0 +1,86 @@
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
+ });
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { evaluatePackageExportsContract } from "../../../tooling/test-support/exportsContract.mjs";
6
+
7
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
+ const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
9
+ const PACKAGE_DIR = path.join(REPO_ROOT, "packages", "users-core");
10
+
11
+ test("users-core exports are explicit and aligned with production/template usage", () => {
12
+ const result = evaluatePackageExportsContract({
13
+ repoRoot: REPO_ROOT,
14
+ packageDir: PACKAGE_DIR,
15
+ packageId: "@jskit-ai/users-core"
16
+ });
17
+
18
+ assert.deepEqual(
19
+ result.wildcardExports,
20
+ [],
21
+ `users-core exports must be explicit. Remove wildcard keys: ${result.wildcardExports.join(", ")}`
22
+ );
23
+ assert.deepEqual(
24
+ result.missingRequiredExports,
25
+ [],
26
+ `users-core required exports missing: ${result.missingRequiredExports.join(", ")}`
27
+ );
28
+ assert.deepEqual(
29
+ result.missingExports,
30
+ [],
31
+ `users-core imports missing from package exports:\n${result.missingExports.join("\n")}`
32
+ );
33
+ assert.deepEqual(
34
+ result.staleExports,
35
+ [],
36
+ `Stale users-core exports found. Remove stale keys: ${result.staleExports.join(", ")}`
37
+ );
38
+ });
@@ -0,0 +1,64 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
4
+ import { registerAvatarMultipartSupport } from "../src/server/accountProfile/registerAvatarMultipartSupport.js";
5
+
6
+ function createAppStub({ hasFastify = true, fastify = null } = {}) {
7
+ const resolvedFastify =
8
+ fastify ||
9
+ {
10
+ register: async () => {},
11
+ hasContentTypeParser: () => false
12
+ };
13
+
14
+ return {
15
+ has(token) {
16
+ if (token === KERNEL_TOKENS.Fastify) {
17
+ return hasFastify;
18
+ }
19
+ return false;
20
+ },
21
+ make(token) {
22
+ if (token === KERNEL_TOKENS.Fastify) {
23
+ return resolvedFastify;
24
+ }
25
+ return null;
26
+ }
27
+ };
28
+ }
29
+
30
+ test("registerAvatarMultipartSupport returns early when Fastify is not available", async () => {
31
+ const app = createAppStub({ hasFastify: false });
32
+ await assert.doesNotReject(async () => registerAvatarMultipartSupport(app));
33
+ });
34
+
35
+ test("registerAvatarMultipartSupport registers multipart parser only once", async () => {
36
+ let registerCount = 0;
37
+ const fastify = {
38
+ register: async () => {
39
+ registerCount += 1;
40
+ },
41
+ hasContentTypeParser: () => false
42
+ };
43
+ const app = createAppStub({ fastify });
44
+
45
+ await registerAvatarMultipartSupport(app);
46
+ await registerAvatarMultipartSupport(app);
47
+
48
+ assert.equal(registerCount, 1);
49
+ });
50
+
51
+ test("registerAvatarMultipartSupport skips registration when parser already exists", async () => {
52
+ let registerCount = 0;
53
+ const fastify = {
54
+ register: async () => {
55
+ registerCount += 1;
56
+ },
57
+ hasContentTypeParser: (contentType) => String(contentType || "").trim().toLowerCase() === "multipart"
58
+ };
59
+ const app = createAppStub({ fastify });
60
+
61
+ await registerAvatarMultipartSupport(app);
62
+
63
+ assert.equal(registerCount, 0);
64
+ });
@@ -0,0 +1,160 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { registerAccountProfile } from "../src/server/accountProfile/registerAccountProfile.js";
4
+ import { registerAccountPreferences } from "../src/server/accountPreferences/registerAccountPreferences.js";
5
+ import { registerAccountNotifications } from "../src/server/accountNotifications/registerAccountNotifications.js";
6
+ import { registerConsoleSettings } from "../src/server/consoleSettings/registerConsoleSettings.js";
7
+ import { registerWorkspaceMembers } from "../src/server/workspaceMembers/registerWorkspaceMembers.js";
8
+ import { registerWorkspacePendingInvitations } from "../src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js";
9
+ import {
10
+ ACCOUNT_SETTINGS_CHANGED_EVENT,
11
+ CONSOLE_SETTINGS_CHANGED_EVENT,
12
+ USERS_BOOTSTRAP_CHANGED_EVENT,
13
+ WORKSPACE_MEMBERS_CHANGED_EVENT,
14
+ WORKSPACE_INVITES_CHANGED_EVENT,
15
+ WORKSPACES_CHANGED_EVENT,
16
+ WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT
17
+ } from "../src/shared/events/usersEvents.js";
18
+
19
+ function createAppDouble() {
20
+ const serviceCalls = [];
21
+
22
+ return {
23
+ serviceCalls,
24
+ app: {
25
+ singleton() {
26
+ return this;
27
+ },
28
+ service(token, factory, metadata) {
29
+ serviceCalls.push({
30
+ token,
31
+ factory,
32
+ metadata
33
+ });
34
+ return this;
35
+ },
36
+ actions() {
37
+ return this;
38
+ }
39
+ }
40
+ };
41
+ }
42
+
43
+ function findServiceCall(serviceCalls, token) {
44
+ return serviceCalls.find((entry) => entry.token === token) || null;
45
+ }
46
+
47
+ test("account register functions publish account.settings.changed for update operations", () => {
48
+ const profileApp = createAppDouble();
49
+ registerAccountProfile(profileApp.app);
50
+ const profile = findServiceCall(profileApp.serviceCalls, "users.accountProfile.service");
51
+ assert.equal(profile?.metadata?.events?.updateProfile?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
52
+ assert.equal(profile?.metadata?.events?.updateProfile?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
53
+ assert.equal(profile?.metadata?.events?.uploadAvatar?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
54
+ assert.equal(profile?.metadata?.events?.uploadAvatar?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
55
+ assert.equal(profile?.metadata?.events?.deleteAvatar?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
56
+ assert.equal(profile?.metadata?.events?.deleteAvatar?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
57
+
58
+ const preferencesApp = createAppDouble();
59
+ registerAccountPreferences(preferencesApp.app);
60
+ const preferences = findServiceCall(preferencesApp.serviceCalls, "users.accountPreferences.service");
61
+ assert.equal(preferences?.metadata?.events?.updatePreferences?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
62
+ assert.equal(preferences?.metadata?.events?.updatePreferences?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
63
+
64
+ const notificationsApp = createAppDouble();
65
+ registerAccountNotifications(notificationsApp.app);
66
+ const notifications = findServiceCall(notificationsApp.serviceCalls, "users.accountNotifications.service");
67
+ assert.equal(notifications?.metadata?.events?.updateNotifications?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
68
+ assert.equal(notifications?.metadata?.events?.updateNotifications?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
69
+ });
70
+
71
+ test("console settings register publishes console.settings.changed", () => {
72
+ const payload = createAppDouble();
73
+ registerConsoleSettings(payload.app);
74
+ const consoleSettings = findServiceCall(payload.serviceCalls, "users.console.settings.service");
75
+ assert.equal(consoleSettings?.metadata?.events?.updateSettings?.[0]?.realtime?.event, CONSOLE_SETTINGS_CHANGED_EVENT);
76
+ });
77
+
78
+ test("workspace register functions publish members/invites/workspace-list realtime events", async () => {
79
+ const membersApp = createAppDouble();
80
+ registerWorkspaceMembers(membersApp.app);
81
+ const members = findServiceCall(membersApp.serviceCalls, "users.workspace.members.service");
82
+ assert.equal(members?.metadata?.events?.updateMemberRole?.[0]?.realtime?.event, WORKSPACE_MEMBERS_CHANGED_EVENT);
83
+ assert.equal(members?.metadata?.events?.updateMemberRole?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
84
+ assert.equal(members?.metadata?.events?.removeMember?.[0]?.realtime?.event, WORKSPACE_MEMBERS_CHANGED_EVENT);
85
+ assert.equal(members?.metadata?.events?.removeMember?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
86
+ assert.equal(members?.metadata?.events?.createInvite?.[0]?.realtime?.event, WORKSPACE_INVITES_CHANGED_EVENT);
87
+ assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
88
+ assert.equal(members?.metadata?.events?.createInvite?.[1]?.entityId?.({ result: { createdInviteId: 91 } }), 91);
89
+ assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.preset, "event_scope");
90
+ assert.equal(typeof members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery, "function");
91
+ const createInviteAudienceQueryResult = await members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery({
92
+ knex() {
93
+ return {
94
+ join() {
95
+ return this;
96
+ },
97
+ where(field, value) {
98
+ assert.equal(field, "wi.id");
99
+ assert.equal(value, 91);
100
+ return this;
101
+ },
102
+ async first() {
103
+ return {
104
+ user_id: 55
105
+ };
106
+ }
107
+ };
108
+ },
109
+ event: {
110
+ entityId: 91
111
+ }
112
+ });
113
+ assert.deepEqual(createInviteAudienceQueryResult, [{ userId: 55 }]);
114
+ assert.equal(members?.metadata?.events?.revokeInvite?.[0]?.realtime?.event, WORKSPACE_INVITES_CHANGED_EVENT);
115
+ assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
116
+ assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.entityId?.({ result: { revokedInviteId: 19 } }), 19);
117
+ assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.preset, "event_scope");
118
+ assert.equal(typeof members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.userQuery, "function");
119
+
120
+ const pendingApp = createAppDouble();
121
+ registerWorkspacePendingInvitations(pendingApp.app);
122
+ const pending = findServiceCall(pendingApp.serviceCalls, "users.workspace.pending-invitations.service");
123
+ const acceptInviteEvents = Array.isArray(pending?.metadata?.events?.acceptInviteByToken)
124
+ ? pending.metadata.events.acceptInviteByToken
125
+ : [];
126
+ const acceptInviteRealtimeEvents = acceptInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
127
+ assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT));
128
+ assert.ok(acceptInviteRealtimeEvents.includes(USERS_BOOTSTRAP_CHANGED_EVENT));
129
+ assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACES_CHANGED_EVENT));
130
+ assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACE_MEMBERS_CHANGED_EVENT));
131
+ assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACE_INVITES_CHANGED_EVENT));
132
+
133
+ const acceptedMembersChange = acceptInviteEvents.find(
134
+ (entry) => entry?.realtime?.event === WORKSPACE_MEMBERS_CHANGED_EVENT
135
+ );
136
+ assert.equal(acceptedMembersChange?.entityId?.({ result: { workspaceId: 9 } }), 9);
137
+ assert.deepEqual(
138
+ acceptedMembersChange?.realtime?.audience?.({
139
+ event: {
140
+ entityId: 9
141
+ }
142
+ }),
143
+ {
144
+ workspaceId: 9
145
+ }
146
+ );
147
+
148
+ const acceptedInvitesChange = acceptInviteEvents.find(
149
+ (entry) => entry?.realtime?.event === WORKSPACE_INVITES_CHANGED_EVENT
150
+ );
151
+ assert.equal(acceptedInvitesChange?.entityId?.({ result: { workspaceId: 9 } }), 9);
152
+
153
+ const refuseInviteEvents = Array.isArray(pending?.metadata?.events?.refuseInviteByToken)
154
+ ? pending.metadata.events.refuseInviteByToken
155
+ : [];
156
+ const refuseInviteRealtimeEvents = refuseInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
157
+ assert.ok(refuseInviteRealtimeEvents.includes(WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT));
158
+ assert.ok(refuseInviteRealtimeEvents.includes(USERS_BOOTSTRAP_CHANGED_EVENT));
159
+ assert.ok(refuseInviteRealtimeEvents.includes(WORKSPACE_INVITES_CHANGED_EVENT));
160
+ });
@@ -0,0 +1,26 @@
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), ["workspace.workspaces.create", "workspace.workspaces.list"]);
26
+ });
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { registerWorkspaceSettings } from "../src/server/workspaceSettings/registerWorkspaceSettings.js";
4
+ import {
5
+ USERS_BOOTSTRAP_CHANGED_EVENT,
6
+ WORKSPACE_SETTINGS_CHANGED_EVENT
7
+ } from "../src/shared/events/usersEvents.js";
8
+
9
+ test("registerWorkspaceSettings registers workspace settings service realtime event metadata", () => {
10
+ const singletonBindings = new Map();
11
+ const actionCalls = [];
12
+ const serviceCalls = [];
13
+
14
+ const app = {
15
+ singleton(token, factory) {
16
+ singletonBindings.set(token, factory);
17
+ return this;
18
+ },
19
+ service(token, factory, metadata) {
20
+ serviceCalls.push({
21
+ token,
22
+ factory,
23
+ metadata
24
+ });
25
+ return this;
26
+ },
27
+ actions(definitions) {
28
+ actionCalls.push(definitions);
29
+ return this;
30
+ }
31
+ };
32
+
33
+ registerWorkspaceSettings(app);
34
+
35
+ assert.equal(singletonBindings.has("workspaceSettingsRepository"), true);
36
+ assert.equal(serviceCalls.length, 1);
37
+ assert.equal(serviceCalls[0].token, "users.workspace.settings.service");
38
+ assert.equal(typeof serviceCalls[0].factory, "function");
39
+ assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.event, WORKSPACE_SETTINGS_CHANGED_EVENT);
40
+ assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.audience, "event_scope");
41
+ assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
42
+ assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.audience, "event_scope");
43
+ assert.equal(actionCalls.length, 1);
44
+ });