@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,183 @@
1
+ import { Type } from "@fastify/type-provider-typebox";
2
+ import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
3
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
4
+ import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
5
+
6
+ function bootAccountSecurityRoutes(app) {
7
+ if (!app || typeof app.make !== "function") {
8
+ throw new Error("bootAccountSecurityRoutes requires application make().");
9
+ }
10
+
11
+ const router = app.make(KERNEL_TOKENS.HttpRouter);
12
+ const authService = app.make("authService");
13
+
14
+ router.register(
15
+ "POST",
16
+ "/api/settings/security/change-password",
17
+ {
18
+ auth: "required",
19
+ meta: {
20
+ tags: ["settings"],
21
+ summary: "Set or change authenticated user's password"
22
+ },
23
+ bodyValidator: userSettingsResource.operations.passwordChange.bodyValidator,
24
+ responseValidators: withStandardErrorResponses(
25
+ {
26
+ 200: userSettingsResource.operations.passwordChange.outputValidator
27
+ },
28
+ { includeValidation400: true }
29
+ ),
30
+ rateLimit: {
31
+ max: 10,
32
+ timeWindow: "1 minute"
33
+ }
34
+ },
35
+ async function (request, reply) {
36
+ const result = await request.executeAction({
37
+ actionId: "settings.security.password.change",
38
+ input: {
39
+ payload: request.input.body
40
+ }
41
+ });
42
+
43
+ if (result?.session && typeof authService.writeSessionCookies === "function") {
44
+ authService.writeSessionCookies(reply, result.session);
45
+ }
46
+
47
+ reply.code(200).send({
48
+ ok: true,
49
+ message: result?.message || "Password updated."
50
+ });
51
+ }
52
+ );
53
+
54
+ router.register(
55
+ "PATCH",
56
+ "/api/settings/security/methods/password",
57
+ {
58
+ auth: "required",
59
+ meta: {
60
+ tags: ["settings"],
61
+ summary: "Enable or disable password sign-in method"
62
+ },
63
+ bodyValidator: userSettingsResource.operations.passwordMethodToggle.bodyValidator,
64
+ responseValidators: withStandardErrorResponses(
65
+ {
66
+ 200: userSettingsResource.operations.passwordMethodToggle.outputValidator
67
+ },
68
+ { includeValidation400: true }
69
+ ),
70
+ rateLimit: {
71
+ max: 20,
72
+ timeWindow: "1 minute"
73
+ }
74
+ },
75
+ async function (request, reply) {
76
+ const response = await request.executeAction({
77
+ actionId: "settings.security.password_method.toggle",
78
+ input: {
79
+ payload: request.input.body
80
+ }
81
+ });
82
+
83
+ reply.code(200).send(response);
84
+ }
85
+ );
86
+
87
+ router.register(
88
+ "GET",
89
+ "/api/settings/security/oauth/:provider/start",
90
+ {
91
+ auth: "required",
92
+ csrfProtection: false,
93
+ meta: {
94
+ tags: ["settings"],
95
+ summary: "Start linking an OAuth provider for authenticated user"
96
+ },
97
+ paramsValidator: userSettingsResource.operations.oauthLinkStart.paramsValidator,
98
+ queryValidator: userSettingsResource.operations.oauthLinkStart.queryValidator,
99
+ responseValidators: withStandardErrorResponses(
100
+ {
101
+ 302: { schema: Type.Unknown() }
102
+ },
103
+ { includeValidation400: true }
104
+ ),
105
+ rateLimit: {
106
+ max: 20,
107
+ timeWindow: "1 minute"
108
+ }
109
+ },
110
+ async function (request, reply) {
111
+ const result = await request.executeAction({
112
+ actionId: "settings.security.oauth.link.start",
113
+ input: {
114
+ provider: request.input.params.provider,
115
+ returnTo: request.input.query.returnTo
116
+ }
117
+ });
118
+
119
+ reply.redirect(result.url);
120
+ }
121
+ );
122
+
123
+ router.register(
124
+ "DELETE",
125
+ "/api/settings/security/oauth/:provider",
126
+ {
127
+ auth: "required",
128
+ meta: {
129
+ tags: ["settings"],
130
+ summary: "Unlink an OAuth provider from authenticated account"
131
+ },
132
+ paramsValidator: userSettingsResource.operations.oauthUnlink.paramsValidator,
133
+ responseValidators: withStandardErrorResponses(
134
+ {
135
+ 200: userSettingsResource.operations.oauthUnlink.outputValidator
136
+ },
137
+ { includeValidation400: true }
138
+ ),
139
+ rateLimit: {
140
+ max: 20,
141
+ timeWindow: "1 minute"
142
+ }
143
+ },
144
+ async function (request, reply) {
145
+ const response = await request.executeAction({
146
+ actionId: "settings.security.oauth.unlink",
147
+ input: {
148
+ provider: request.input.params.provider
149
+ }
150
+ });
151
+
152
+ reply.code(200).send(response);
153
+ }
154
+ );
155
+
156
+ router.register(
157
+ "POST",
158
+ "/api/settings/security/logout-others",
159
+ {
160
+ auth: "required",
161
+ meta: {
162
+ tags: ["settings"],
163
+ summary: "Sign out from other active sessions"
164
+ },
165
+ responseValidators: withStandardErrorResponses({
166
+ 200: userSettingsResource.operations.logoutOtherSessions.outputValidator
167
+ }),
168
+ rateLimit: {
169
+ max: 20,
170
+ timeWindow: "1 minute"
171
+ }
172
+ },
173
+ async function (request, reply) {
174
+ const response = await request.executeAction({
175
+ actionId: "settings.security.sessions.logout_others",
176
+ input: {}
177
+ });
178
+ reply.code(200).send(response);
179
+ }
180
+ );
181
+ }
182
+
183
+ export { bootAccountSecurityRoutes };
@@ -0,0 +1,31 @@
1
+ import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
2
+ import { createService as createAccountSecurityService } from "./accountSecurityService.js";
3
+ import { accountSecurityActions } from "./accountSecurityActions.js";
4
+
5
+ const USERS_ACCOUNT_SECURITY_SERVICE_TOKEN = "users.accountSecurity.service";
6
+
7
+ function registerAccountSecurity(app) {
8
+ if (!app || typeof app.singleton !== "function" || typeof app.actions !== "function") {
9
+ throw new Error("registerAccountSecurity requires application singleton()/actions().");
10
+ }
11
+
12
+ app.singleton(USERS_ACCOUNT_SECURITY_SERVICE_TOKEN, (scope) => {
13
+ const authService = scope.has("authService") ? scope.make("authService") : null;
14
+ return createAccountSecurityService({
15
+ userSettingsRepository: scope.make("userSettingsRepository"),
16
+ userProfilesRepository: scope.make("userProfilesRepository"),
17
+ authService
18
+ });
19
+ });
20
+
21
+ app.actions(
22
+ withActionDefaults(accountSecurityActions, {
23
+ domain: "settings",
24
+ dependencies: {
25
+ accountSecurityService: USERS_ACCOUNT_SECURITY_SERVICE_TOKEN
26
+ }
27
+ })
28
+ );
29
+ }
30
+
31
+ export { registerAccountSecurity };
@@ -0,0 +1,21 @@
1
+ # Server Common
2
+
3
+ This directory contains server-only runtime pieces reused by multiple slices in `users-core`.
4
+
5
+ Use these folders:
6
+ - `repositories/`: shared repositories and repository-only helpers.
7
+ - `services/`: shared domain services consumed by multiple slices.
8
+ - `contributors/`: shared action-context/bootstrap contributors.
9
+ - `validators/`: shared request/response validators used by multiple adapters.
10
+ - `formatters/`: shared payload formatters/projections for transport output.
11
+ - `routes/`: shared route schema maps used by more than one route adapter.
12
+
13
+ Keep these files here:
14
+ - `diTokens.js`: shared DI tokens used across slices.
15
+ - `registerCommonRepositories.js`: shared repository bindings.
16
+ - `registerSharedApi.js`: shared API metadata registration.
17
+
18
+ Do not put these in `common/`:
19
+ - feature-only actions/services/repositories/controllers
20
+ - one-off route payload shapes used by a single feature
21
+ - UI/client code
@@ -0,0 +1,11 @@
1
+ # `contributors/`
2
+
3
+ Put shared runtime contributors here (for action context or bootstrap payload composition).
4
+
5
+ Allowed:
6
+ - contributors reused by multiple slices/actions
7
+ - contributor logic that delegates domain work to services
8
+
9
+ Not allowed:
10
+ - feature-specific contributors used by one slice only
11
+ - direct repository access when a service already owns that domain
@@ -0,0 +1,79 @@
1
+ import {
2
+ normalizeObject,
3
+ requireServiceMethod
4
+ } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
5
+ import {
6
+ normalizeScopedRouteVisibility,
7
+ USERS_ROUTE_VISIBILITY_PUBLIC,
8
+ USERS_ROUTE_VISIBILITY_WORKSPACE,
9
+ USERS_ROUTE_VISIBILITY_WORKSPACE_USER
10
+ } from "../../../shared/support/usersVisibility.js";
11
+ import { resolveActionUser } from "../support/resolveActionUser.js";
12
+
13
+ const WORKSPACE_CONTEXT_ACTION_IDS = Object.freeze([
14
+ "workspace.roles.list",
15
+ "workspace.settings.read",
16
+ "workspace.settings.update",
17
+ "workspace.members.list",
18
+ "workspace.member.role.update",
19
+ "workspace.member.remove",
20
+ "workspace.invites.list",
21
+ "workspace.invite.create",
22
+ "workspace.invite.revoke"
23
+ ]);
24
+ const WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET = new Set([
25
+ USERS_ROUTE_VISIBILITY_WORKSPACE,
26
+ USERS_ROUTE_VISIBILITY_WORKSPACE_USER
27
+ ]);
28
+
29
+ function createWorkspaceActionContextContributor({ workspaceService } = {}) {
30
+ const contributorId = "users.workspace.context";
31
+
32
+ requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
33
+
34
+ return Object.freeze({
35
+ contributorId,
36
+ async contribute({ actionId, input, context, request } = {}) {
37
+ const payload = normalizeObject(input);
38
+ if (!Object.hasOwn(payload, "workspaceSlug")) {
39
+ return {};
40
+ }
41
+
42
+ const actionName = String(actionId || "").trim();
43
+ const hasLegacyWorkspaceActionId = WORKSPACE_CONTEXT_ACTION_IDS.includes(actionName);
44
+ const routeVisibility = normalizeScopedRouteVisibility(request?.routeOptions?.config?.visibility, {
45
+ fallback: USERS_ROUTE_VISIBILITY_PUBLIC
46
+ });
47
+ const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
48
+ if (!hasLegacyWorkspaceActionId && !hasWorkspaceRouteVisibility) {
49
+ return {};
50
+ }
51
+
52
+ const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
53
+ resolveActionUser(context, payload),
54
+ payload.workspaceSlug,
55
+ { request }
56
+ );
57
+
58
+ const contribution = {
59
+ requestMeta: {
60
+ resolvedWorkspaceContext
61
+ }
62
+ };
63
+
64
+ if (!context?.workspace) {
65
+ contribution.workspace = resolvedWorkspaceContext.workspace;
66
+ }
67
+ if (!context?.membership) {
68
+ contribution.membership = resolvedWorkspaceContext.membership;
69
+ }
70
+ if (!Array.isArray(context?.permissions) || context.permissions.length < 1) {
71
+ contribution.permissions = resolvedWorkspaceContext.permissions;
72
+ }
73
+
74
+ return contribution;
75
+ }
76
+ });
77
+ }
78
+
79
+ export { createWorkspaceActionContextContributor };
@@ -0,0 +1,34 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
4
+ if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
5
+ throw new Error(
6
+ "workspace auth policy context resolver requires workspaceService.resolveWorkspaceContextForUserBySlug()."
7
+ );
8
+ }
9
+
10
+ return async function resolveWorkspaceAuthPolicyContext({ request, actor, meta } = {}) {
11
+ const contextPolicy = normalizeText(meta?.contextPolicy || "none").toLowerCase() || "none";
12
+ const permission = normalizeText(meta?.permission);
13
+ if (contextPolicy === "none" && !permission) {
14
+ return {};
15
+ }
16
+
17
+ const workspaceSlug = normalizeText(request?.params?.workspaceSlug).toLowerCase();
18
+ if (!workspaceSlug || !actor) {
19
+ return {};
20
+ }
21
+
22
+ const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
23
+ request
24
+ });
25
+
26
+ return {
27
+ workspace: resolvedWorkspaceContext?.workspace || null,
28
+ membership: resolvedWorkspaceContext?.membership || null,
29
+ permissions: Array.isArray(resolvedWorkspaceContext?.permissions) ? resolvedWorkspaceContext.permissions : []
30
+ };
31
+ };
32
+ }
33
+
34
+ export { createWorkspaceAuthPolicyContextResolver };
@@ -0,0 +1,79 @@
1
+ import { normalizeOpaqueId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
3
+
4
+ function buildVisibilityContribution({ visibility, scopeOwnerId = 0, userOwnerId = null } = {}) {
5
+ const requiresActorScope = visibility === "workspace_user";
6
+ const contribution = {
7
+ scopeKind: requiresActorScope ? "workspace_user" : "workspace",
8
+ requiresActorScope
9
+ };
10
+
11
+ if (scopeOwnerId > 0) {
12
+ contribution.scopeOwnerId = scopeOwnerId;
13
+ }
14
+ if (requiresActorScope && userOwnerId != null) {
15
+ contribution.userOwnerId = userOwnerId;
16
+ }
17
+
18
+ return contribution;
19
+ }
20
+
21
+ function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
22
+ if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
23
+ throw new Error("workspace route visibility resolver requires workspaceService.resolveWorkspaceContextForUserBySlug().");
24
+ }
25
+
26
+ return Object.freeze({
27
+ resolverId: "users.workspace.visibility",
28
+ async resolve({ visibility, context, request, input } = {}) {
29
+ if (visibility !== "workspace" && visibility !== "workspace_user") {
30
+ return {};
31
+ }
32
+
33
+ const actor = context?.actor || request?.user || null;
34
+ const userOwnerId = normalizeOpaqueId(actor?.id);
35
+ const workspace =
36
+ context?.workspace || context?.requestMeta?.resolvedWorkspaceContext?.workspace || request?.workspace || null;
37
+ const scopeOwnerId = parsePositiveInteger(workspace?.id);
38
+ if (!scopeOwnerId) {
39
+ const workspaceSlug = normalizeText(input?.workspaceSlug).toLowerCase();
40
+
41
+ if (!workspaceSlug || !actor) {
42
+ return visibility === "workspace_user"
43
+ ? buildVisibilityContribution({
44
+ visibility,
45
+ userOwnerId
46
+ })
47
+ : {};
48
+ }
49
+
50
+ const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
51
+ request
52
+ });
53
+ const resolvedWorkspaceOwnerId = parsePositiveInteger(resolvedWorkspaceContext?.workspace?.id);
54
+ if (!resolvedWorkspaceOwnerId) {
55
+ return visibility === "workspace_user"
56
+ ? buildVisibilityContribution({
57
+ visibility,
58
+ userOwnerId
59
+ })
60
+ : {};
61
+ }
62
+
63
+ return buildVisibilityContribution({
64
+ visibility,
65
+ scopeOwnerId: resolvedWorkspaceOwnerId,
66
+ userOwnerId
67
+ });
68
+ }
69
+
70
+ return buildVisibilityContribution({
71
+ visibility,
72
+ scopeOwnerId,
73
+ userOwnerId
74
+ });
75
+ }
76
+ });
77
+ }
78
+
79
+ export { createWorkspaceRouteVisibilityResolver };
@@ -0,0 +1,21 @@
1
+ const USERS_WORKSPACE_PENDING_INVITATIONS_SERVICE_TOKEN = "users.workspace.pending-invitations.service";
2
+ const USERS_WORKSPACE_ENABLED_TOKEN = "users.workspace.enabled";
3
+ const USERS_WORKSPACE_TENANCY_ENABLED_TOKEN = "users.workspace.tenancy.enabled";
4
+ const USERS_WORKSPACE_INVITATIONS_ENABLED_TOKEN = "users.workspace.invitations.enabled";
5
+ const USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN = "users.workspace.self-create.enabled";
6
+ const USERS_TENANCY_PROFILE_TOKEN = "users.tenancy.profile";
7
+ const USERS_AVATAR_STORAGE_SERVICE_TOKEN = "users.avatar.storage.service";
8
+ const USERS_AVATAR_SERVICE_TOKEN = "users.avatar.service";
9
+ const USERS_PROFILE_SYNC_SERVICE_TOKEN = "users.profile.sync.service";
10
+
11
+ export {
12
+ USERS_WORKSPACE_PENDING_INVITATIONS_SERVICE_TOKEN,
13
+ USERS_WORKSPACE_ENABLED_TOKEN,
14
+ USERS_WORKSPACE_TENANCY_ENABLED_TOKEN,
15
+ USERS_WORKSPACE_INVITATIONS_ENABLED_TOKEN,
16
+ USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN,
17
+ USERS_TENANCY_PROFILE_TOKEN,
18
+ USERS_AVATAR_STORAGE_SERVICE_TOKEN,
19
+ USERS_AVATAR_SERVICE_TOKEN,
20
+ USERS_PROFILE_SYNC_SERVICE_TOKEN
21
+ };
@@ -0,0 +1,11 @@
1
+ # `formatters/`
2
+
3
+ Put shared transport/output projection logic here.
4
+
5
+ Allowed:
6
+ - shaping domain records into API payloads used in multiple slices
7
+
8
+ Not allowed:
9
+ - business rules
10
+ - repository reads/writes
11
+ - HTTP adapter/controller code
@@ -0,0 +1,42 @@
1
+ import { createHash } from "node:crypto";
2
+ import { normalizeLowerText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
+ import { DEFAULT_USER_SETTINGS } from "../../../shared/settings.js";
4
+
5
+ const ACCOUNT_AVATAR_FILE_PATH = "/api/settings/profile/avatar";
6
+
7
+ function createGravatarUrl(email, size = 64) {
8
+ const normalizedEmail = normalizeLowerText(email);
9
+ const hash = createHash("sha256").update(normalizedEmail).digest("hex");
10
+ return `https://www.gravatar.com/avatar/${hash}?d=mp&s=${Number(size) || 64}`;
11
+ }
12
+
13
+ function createUploadedAvatarUrl(profile = {}) {
14
+ const storageKey = String(profile?.avatarStorageKey || "").trim();
15
+ if (!storageKey) {
16
+ return null;
17
+ }
18
+
19
+ const avatarVersion = String(profile?.avatarVersion || "").trim();
20
+ if (!avatarVersion) {
21
+ return ACCOUNT_AVATAR_FILE_PATH;
22
+ }
23
+
24
+ return `${ACCOUNT_AVATAR_FILE_PATH}?v=${encodeURIComponent(avatarVersion)}`;
25
+ }
26
+
27
+ function accountAvatarFormatter(profile, settings) {
28
+ const size = Number(settings?.avatarSize || DEFAULT_USER_SETTINGS.avatarSize);
29
+ const uploadedUrl = createUploadedAvatarUrl(profile);
30
+ const gravatarUrl = createGravatarUrl(profile?.email, size);
31
+
32
+ return {
33
+ uploadedUrl,
34
+ gravatarUrl,
35
+ effectiveUrl: uploadedUrl || gravatarUrl,
36
+ hasUploadedAvatar: Boolean(uploadedUrl),
37
+ size,
38
+ version: profile?.avatarVersion || null
39
+ };
40
+ }
41
+
42
+ export { accountAvatarFormatter };
@@ -0,0 +1,71 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
2
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
3
+
4
+ function normalizeMfa(source) {
5
+ const mfaSource = isRecord(source?.mfa) ? source.mfa : {};
6
+ const methods = [];
7
+ for (const entry of Array.isArray(mfaSource.methods) ? mfaSource.methods : []) {
8
+ const normalized = normalizeText(entry);
9
+ if (!normalized) {
10
+ continue;
11
+ }
12
+ methods.push(normalized);
13
+ }
14
+
15
+ return {
16
+ status: normalizeText(mfaSource.status) || "not_enabled",
17
+ enrolled: Boolean(mfaSource.enrolled),
18
+ methods
19
+ };
20
+ }
21
+
22
+ function normalizeAuthMethods(sourceMethods) {
23
+ const methods = [];
24
+ let enabledMethodsCount = 0;
25
+
26
+ for (const method of Array.isArray(sourceMethods) ? sourceMethods : []) {
27
+ const normalizedMethod = {
28
+ id: normalizeText(method?.id),
29
+ kind: normalizeText(method?.kind),
30
+ provider: method?.provider == null ? null : normalizeText(method.provider),
31
+ label: normalizeText(method?.label || method?.id),
32
+ configured: method?.configured === true,
33
+ enabled: method?.enabled === true,
34
+ canEnable: method?.canEnable === true,
35
+ canDisable: method?.canDisable === true,
36
+ supportsSecretUpdate: method?.supportsSecretUpdate === true,
37
+ requiresCurrentPassword: method?.requiresCurrentPassword === true
38
+ };
39
+
40
+ if (normalizedMethod.enabled) {
41
+ enabledMethodsCount += 1;
42
+ }
43
+ methods.push(normalizedMethod);
44
+ }
45
+
46
+ return {
47
+ methods,
48
+ enabledMethodsCount
49
+ };
50
+ }
51
+
52
+ function accountSecurityStatusFormatter(securityStatus = {}) {
53
+ const source = isRecord(securityStatus) ? securityStatus : {};
54
+ const authPolicy = isRecord(source.authPolicy) ? source.authPolicy : {};
55
+ const { methods: authMethods, enabledMethodsCount } = normalizeAuthMethods(source.authMethods);
56
+ const minimumEnabledMethods = Number(authPolicy.minimumEnabledMethods);
57
+
58
+ return {
59
+ mfa: normalizeMfa(source),
60
+ sessions: {
61
+ canSignOutOtherDevices: true
62
+ },
63
+ authPolicy: {
64
+ minimumEnabledMethods: minimumEnabledMethods > 0 ? minimumEnabledMethods : 1,
65
+ enabledMethodsCount
66
+ },
67
+ authMethods
68
+ };
69
+ }
70
+
71
+ export { accountSecurityStatusFormatter };
@@ -0,0 +1,62 @@
1
+ import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
2
+ import {
3
+ USER_SETTINGS_SECTIONS,
4
+ userSettingsFields
5
+ } from "../../../shared/resources/userSettingsFields.js";
6
+ import { accountAvatarFormatter } from "./accountAvatarFormatter.js";
7
+ import { accountSecurityStatusFormatter } from "./accountSecurityStatusFormatter.js";
8
+
9
+ function resolveAuthProfileSettings(authService) {
10
+ if (!authService || typeof authService.getSettingsProfileAuthInfo !== "function") {
11
+ return {
12
+ emailManagedBy: "auth",
13
+ emailChangeFlow: "auth"
14
+ };
15
+ }
16
+
17
+ const authProfileSettings = authService.getSettingsProfileAuthInfo();
18
+ return {
19
+ emailManagedBy: normalizeLowerText(authProfileSettings?.emailManagedBy) || "auth",
20
+ emailChangeFlow: normalizeLowerText(authProfileSettings?.emailChangeFlow) || "auth"
21
+ };
22
+ }
23
+
24
+ function formatUserSettingsSection(section, settings = {}) {
25
+ const source = settings && typeof settings === "object" ? settings : {};
26
+ const formatted = {};
27
+
28
+ for (const field of userSettingsFields) {
29
+ if (field.section !== section) {
30
+ continue;
31
+ }
32
+ const rawValue = Object.hasOwn(source, field.key)
33
+ ? source[field.key]
34
+ : field.resolveDefault({
35
+ settings: source
36
+ });
37
+ formatted[field.key] = field.normalizeOutput(rawValue, {
38
+ settings: source
39
+ });
40
+ }
41
+
42
+ return formatted;
43
+ }
44
+
45
+ function accountSettingsResponseFormatter({ profile, settings, securityStatus, authService }) {
46
+ const authProfileSettings = resolveAuthProfileSettings(authService);
47
+
48
+ return {
49
+ profile: {
50
+ displayName: normalizeText(profile?.displayName),
51
+ email: normalizeLowerText(profile?.email),
52
+ emailManagedBy: authProfileSettings.emailManagedBy,
53
+ emailChangeFlow: authProfileSettings.emailChangeFlow,
54
+ avatar: accountAvatarFormatter(profile, settings)
55
+ },
56
+ security: accountSecurityStatusFormatter(securityStatus),
57
+ preferences: formatUserSettingsSection(USER_SETTINGS_SECTIONS.PREFERENCES, settings),
58
+ notifications: formatUserSettingsSection(USER_SETTINGS_SECTIONS.NOTIFICATIONS, settings)
59
+ };
60
+ }
61
+
62
+ export { accountSettingsResponseFormatter };