@jskit-ai/users-core 0.1.33 → 0.1.36

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.
@@ -3,12 +3,11 @@ import {
3
3
  } from "@jskit-ai/kernel/server/actions";
4
4
  import { registerRouteVisibilityResolver } from "@jskit-ai/kernel/server/http";
5
5
  import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
6
- import { TENANCY_MODE_WORKSPACES, resolveTenancyProfile } from "../shared/tenancyProfile.js";
7
6
  import { createService as createWorkspaceService } from "./common/services/workspaceContextService.js";
8
- import { createService as createAuthProfileSyncService } from "./common/services/authProfileSyncService.js";
9
7
  import { createWorkspaceActionContextContributor } from "./common/contributors/workspaceActionContextContributor.js";
10
8
  import { createWorkspaceRouteVisibilityResolver } from "./common/contributors/workspaceRouteVisibilityResolver.js";
11
9
  import { createWorkspaceAuthPolicyContextResolver } from "./common/contributors/workspaceAuthPolicyContextResolver.js";
10
+ import { TENANCY_MODE_WORKSPACES } from "../shared/tenancyProfile.js";
12
11
  import { resolveWorkspaceInvitationsPolicy } from "./support/workspaceInvitationsPolicy.js";
13
12
  import { resolveWorkspaceSurfaceIdsFromAppConfig } from "./support/workspaceActionSurfaces.js";
14
13
 
@@ -27,20 +26,6 @@ function registerWorkspaceCore(app) {
27
26
  workspaceSettingsRepository: scope.make("workspaceSettingsRepository")
28
27
  });
29
28
  });
30
-
31
- app.singleton("users.profile.sync.service", (scope) => {
32
- return createAuthProfileSyncService({
33
- usersRepository: scope.make("usersRepository"),
34
- userSettingsRepository: scope.make("userSettingsRepository"),
35
- workspaceProvisioningService: scope.make("users.workspace.service")
36
- });
37
- });
38
-
39
- app.singleton("users.tenancy.profile", (scope) => {
40
- const appConfig = resolveAppConfig(scope);
41
- return resolveTenancyProfile(appConfig);
42
- });
43
-
44
29
  app.singleton("users.workspace.enabled", (scope) => {
45
30
  return scope.make("users.tenancy.profile").workspace.enabled === true;
46
31
  });
@@ -0,0 +1,26 @@
1
+ import { createRepository as createWorkspacesRepository } from "./common/repositories/workspacesRepository.js";
2
+ import { createRepository as createWorkspaceMembershipsRepository } from "./common/repositories/workspaceMembershipsRepository.js";
3
+ import { createRepository as createWorkspaceInvitesRepository } from "./common/repositories/workspaceInvitesRepository.js";
4
+
5
+ function registerWorkspaceRepositories(app) {
6
+ if (!app || typeof app.singleton !== "function") {
7
+ throw new Error("registerWorkspaceRepositories requires application singleton().");
8
+ }
9
+
10
+ app.singleton("workspacesRepository", (scope) => {
11
+ const knex = scope.make("jskit.database.knex");
12
+ return createWorkspacesRepository(knex);
13
+ });
14
+
15
+ app.singleton("workspaceMembershipsRepository", (scope) => {
16
+ const knex = scope.make("jskit.database.knex");
17
+ return createWorkspaceMembershipsRepository(knex);
18
+ });
19
+
20
+ app.singleton("workspaceInvitesRepository", (scope) => {
21
+ const knex = scope.make("jskit.database.knex");
22
+ return createWorkspaceInvitesRepository(knex);
23
+ });
24
+ }
25
+
26
+ export { registerWorkspaceRepositories };
@@ -0,0 +1,248 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime";
2
+ import { requireServiceMethod } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
3
+ import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
+ import {
5
+ TENANCY_MODE_NONE,
6
+ TENANCY_MODE_PERSONAL,
7
+ TENANCY_MODE_WORKSPACES,
8
+ WORKSPACE_SLUG_POLICY_NONE,
9
+ WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
10
+ WORKSPACE_SLUG_POLICY_USER_SELECTED,
11
+ resolveTenancyProfile
12
+ } from "../shared/tenancyProfile.js";
13
+ import { accountAvatarFormatter } from "./common/formatters/accountAvatarFormatter.js";
14
+ import { authenticatedUserValidator } from "./common/validators/authenticatedUserValidator.js";
15
+ import { userSettingsFields } from "../shared/resources/userSettingsFields.js";
16
+
17
+ function getOAuthProviderCatalogPayload(authService) {
18
+ if (!authService || typeof authService.getOAuthProviderCatalog !== "function") {
19
+ return {
20
+ oauthProviders: [],
21
+ oauthDefaultProvider: null
22
+ };
23
+ }
24
+
25
+ const catalog = authService.getOAuthProviderCatalog();
26
+ const providers = Array.isArray(catalog?.providers)
27
+ ? catalog.providers
28
+ .map((provider) => ({
29
+ id: normalizeLowerText(provider?.id),
30
+ label: normalizeText(provider?.label)
31
+ }))
32
+ .filter((provider) => provider.id && provider.label)
33
+ : [];
34
+ const defaultProvider = normalizeLowerText(catalog?.defaultProvider);
35
+
36
+ return {
37
+ oauthProviders: providers,
38
+ oauthDefaultProvider: providers.some((provider) => provider.id === defaultProvider) ? defaultProvider : null
39
+ };
40
+ }
41
+
42
+ function normalizeBoolean(value, fallback) {
43
+ if (typeof value === "boolean") {
44
+ return value;
45
+ }
46
+ if (typeof value === "string") {
47
+ const normalized = normalizeLowerText(value);
48
+ if (normalized === "true") {
49
+ return true;
50
+ }
51
+ if (normalized === "false") {
52
+ return false;
53
+ }
54
+ }
55
+ return fallback;
56
+ }
57
+
58
+ function resolveAppState(appConfig = {}, { workspaceInvitationsEnabled = false } = {}) {
59
+ const features = {
60
+ workspaceSwitching: normalizeBoolean(appConfig.workspaceSwitching, false),
61
+ workspaceInvites: workspaceInvitationsEnabled === true,
62
+ assistantEnabled: normalizeBoolean(appConfig.assistantEnabled, false),
63
+ assistantRequiredPermission: normalizeText(appConfig.assistantRequiredPermission),
64
+ socialEnabled: normalizeBoolean(appConfig.socialEnabled, false),
65
+ socialFederationEnabled: normalizeBoolean(appConfig.socialFederationEnabled, false)
66
+ };
67
+
68
+ return {
69
+ features
70
+ };
71
+ }
72
+
73
+ function normalizeSlugPolicy(value = "") {
74
+ const normalizedValue = normalizeLowerText(value);
75
+ if (
76
+ normalizedValue === WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME ||
77
+ normalizedValue === WORKSPACE_SLUG_POLICY_USER_SELECTED
78
+ ) {
79
+ return normalizedValue;
80
+ }
81
+ return WORKSPACE_SLUG_POLICY_NONE;
82
+ }
83
+
84
+ function isSupportedTenancyMode(value = "") {
85
+ return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value === TENANCY_MODE_WORKSPACES;
86
+ }
87
+
88
+ function resolveBootstrapTenancyProfile(tenancyProfile = null, appConfig = {}) {
89
+ const fallback = resolveTenancyProfile(appConfig);
90
+ const source = tenancyProfile && typeof tenancyProfile === "object" ? tenancyProfile : fallback;
91
+ const mode = isSupportedTenancyMode(source?.mode) ? source.mode : fallback.mode;
92
+ const workspace = source?.workspace && typeof source.workspace === "object" ? source.workspace : fallback.workspace;
93
+
94
+ return Object.freeze({
95
+ mode,
96
+ workspace: Object.freeze({
97
+ enabled: workspace.enabled === true,
98
+ autoProvision: workspace.autoProvision === true,
99
+ allowSelfCreate: workspace.allowSelfCreate === true,
100
+ slugPolicy: normalizeSlugPolicy(workspace.slugPolicy)
101
+ })
102
+ });
103
+ }
104
+
105
+ function createAnonymousBootstrapPayload({ appState, tenancyProfile }) {
106
+ return {
107
+ session: {
108
+ authenticated: false
109
+ },
110
+ profile: null,
111
+ tenancy: tenancyProfile,
112
+ app: appState,
113
+ workspaces: [],
114
+ pendingInvites: [],
115
+ activeWorkspace: null,
116
+ membership: null,
117
+ requestedWorkspace: null,
118
+ permissions: [],
119
+ surfaceAccess: {
120
+ consoleowner: false
121
+ },
122
+ workspaceSettings: null,
123
+ userSettings: null,
124
+ requestMeta: {
125
+ hasRequest: false
126
+ }
127
+ };
128
+ }
129
+
130
+ function mapUserSettingsBootstrap(settings = {}) {
131
+ const source = settings && typeof settings === "object" ? settings : {};
132
+ const mapped = {};
133
+
134
+ for (const field of userSettingsFields) {
135
+ if (field.includeInBootstrap === false) {
136
+ continue;
137
+ }
138
+ const rawValue = Object.hasOwn(source, field.key)
139
+ ? source[field.key]
140
+ : field.resolveDefault({
141
+ settings: source
142
+ });
143
+ mapped[field.key] = field.normalizeOutput(rawValue, {
144
+ settings: source
145
+ });
146
+ }
147
+
148
+ return mapped;
149
+ }
150
+
151
+ function createUsersBootstrapContributor({
152
+ usersRepository,
153
+ userSettingsRepository,
154
+ appConfig = {},
155
+ tenancyProfile = null,
156
+ authService,
157
+ consoleService = null
158
+ } = {}) {
159
+ const contributorId = "users.bootstrap";
160
+ const appState = resolveAppState(appConfig);
161
+ const resolvedTenancyProfile = resolveBootstrapTenancyProfile(tenancyProfile, appConfig);
162
+
163
+ requireServiceMethod(usersRepository, "findById", contributorId, {
164
+ serviceLabel: "usersRepository"
165
+ });
166
+ requireServiceMethod(userSettingsRepository, "ensureForUserId", contributorId, {
167
+ serviceLabel: "userSettingsRepository"
168
+ });
169
+
170
+ return Object.freeze({
171
+ contributorId,
172
+ async contribute({ request = null, reply = null } = {}) {
173
+ const authResult = await request.executeAction({
174
+ actionId: "auth.session.read"
175
+ });
176
+
177
+ if (authResult?.clearSession === true && typeof authService?.clearSessionCookies === "function") {
178
+ authService.clearSessionCookies(reply);
179
+ }
180
+ if (authResult?.session && typeof authService?.writeSessionCookies === "function") {
181
+ authService.writeSessionCookies(reply, authResult.session);
182
+ }
183
+ if (authResult?.transientFailure === true) {
184
+ throw new AppError(503, "Authentication service temporarily unavailable. Please retry.");
185
+ }
186
+
187
+ const normalizedUser = authenticatedUserValidator.normalize(authResult?.authenticated ? authResult?.profile : null);
188
+ let payload = createAnonymousBootstrapPayload({
189
+ appState,
190
+ tenancyProfile: resolvedTenancyProfile
191
+ });
192
+
193
+ if (normalizedUser) {
194
+ const latestProfile = (await usersRepository.findById(normalizedUser.id)) || normalizedUser;
195
+ const userSettings = await userSettingsRepository.ensureForUserId(latestProfile.id);
196
+
197
+ let seededConsoleOwnerUserId = 0;
198
+ if (
199
+ consoleService &&
200
+ typeof consoleService.ensureInitialConsoleMember === "function"
201
+ ) {
202
+ seededConsoleOwnerUserId = Number(await consoleService.ensureInitialConsoleMember(latestProfile.id));
203
+ }
204
+
205
+ payload = {
206
+ session: {
207
+ authenticated: true,
208
+ userId: latestProfile.id
209
+ },
210
+ profile: {
211
+ displayName: latestProfile.displayName,
212
+ email: latestProfile.email,
213
+ avatar: accountAvatarFormatter(latestProfile, userSettings)
214
+ },
215
+ tenancy: resolvedTenancyProfile,
216
+ app: appState,
217
+ workspaces: [],
218
+ pendingInvites: [],
219
+ activeWorkspace: null,
220
+ membership: null,
221
+ requestedWorkspace: null,
222
+ permissions: [],
223
+ surfaceAccess: {
224
+ consoleowner: seededConsoleOwnerUserId > 0 && seededConsoleOwnerUserId === Number(latestProfile.id)
225
+ },
226
+ workspaceSettings: null,
227
+ userSettings: mapUserSettingsBootstrap(userSettings),
228
+ requestMeta: {
229
+ hasRequest: Boolean(request)
230
+ }
231
+ };
232
+ }
233
+
234
+ const oauthCatalogPayload = getOAuthProviderCatalogPayload(authService);
235
+ const session = payload?.session && typeof payload.session === "object" ? payload.session : { authenticated: false };
236
+
237
+ return {
238
+ ...payload,
239
+ session: {
240
+ ...session,
241
+ ...oauthCatalogPayload
242
+ }
243
+ };
244
+ }
245
+ });
246
+ }
247
+
248
+ export { createUsersBootstrapContributor };