@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.
@@ -1,13 +1,7 @@
1
- import { AppError } from "@jskit-ai/kernel/server/runtime";
2
1
  import { requireServiceMethod } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
3
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
2
+ import { normalizeLowerText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
3
  import {
5
4
  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
5
  resolveTenancyProfile
12
6
  } from "../shared/tenancyProfile.js";
13
7
  import { workspacePendingInvitationsResource } from "../shared/resources/workspacePendingInvitationsResource.js";
@@ -16,9 +10,6 @@ import {
16
10
  mapWorkspaceSettingsPublic,
17
11
  mapWorkspaceSummary
18
12
  } from "./common/formatters/workspaceFormatter.js";
19
- import { accountAvatarFormatter } from "./common/formatters/accountAvatarFormatter.js";
20
- import { authenticatedUserValidator } from "./common/validators/authenticatedUserValidator.js";
21
- import { userSettingsFields } from "../shared/resources/userSettingsFields.js";
22
13
 
23
14
  const REQUESTED_WORKSPACE_STATUS_RESOLVED = "resolved";
24
15
  const REQUESTED_WORKSPACE_STATUS_NOT_FOUND = "not_found";
@@ -31,47 +22,6 @@ function normalizePendingInvites(invites) {
31
22
  }).pendingInvites;
32
23
  }
33
24
 
34
- function getOAuthProviderCatalogPayload(authService) {
35
- if (!authService || typeof authService.getOAuthProviderCatalog !== "function") {
36
- return {
37
- oauthProviders: [],
38
- oauthDefaultProvider: null
39
- };
40
- }
41
-
42
- const catalog = authService.getOAuthProviderCatalog();
43
- const providers = Array.isArray(catalog?.providers)
44
- ? catalog.providers
45
- .map((provider) => ({
46
- id: normalizeLowerText(provider?.id),
47
- label: normalizeText(provider?.label)
48
- }))
49
- .filter((provider) => provider.id && provider.label)
50
- : [];
51
- const defaultProvider = normalizeLowerText(catalog?.defaultProvider);
52
-
53
- return {
54
- oauthProviders: providers,
55
- oauthDefaultProvider: providers.some((provider) => provider.id === defaultProvider) ? defaultProvider : null
56
- };
57
- }
58
-
59
- function normalizeBoolean(value, fallback) {
60
- if (typeof value === "boolean") {
61
- return value;
62
- }
63
- if (typeof value === "string") {
64
- const normalized = normalizeLowerText(value);
65
- if (normalized === "true") {
66
- return true;
67
- }
68
- if (normalized === "false") {
69
- return false;
70
- }
71
- }
72
- return fallback;
73
- }
74
-
75
25
  function normalizeQueryPayload(value = {}) {
76
26
  if (!value || typeof value !== "object" || Array.isArray(value)) {
77
27
  return {};
@@ -137,111 +87,28 @@ function resolveRequestedWorkspaceStatusFromError(error) {
137
87
  return "";
138
88
  }
139
89
 
140
- function resolveAppState(appConfig = {}, { workspaceInvitationsEnabled = true } = {}) {
141
- const features = {
142
- workspaceSwitching: normalizeBoolean(appConfig.workspaceSwitching, true),
143
- workspaceInvites: workspaceInvitationsEnabled === true,
144
- assistantEnabled: normalizeBoolean(appConfig.assistantEnabled, false),
145
- assistantRequiredPermission: normalizeText(appConfig.assistantRequiredPermission),
146
- socialEnabled: normalizeBoolean(appConfig.socialEnabled, false),
147
- socialFederationEnabled: normalizeBoolean(appConfig.socialFederationEnabled, false)
148
- };
149
-
150
- return {
151
- features
152
- };
153
- }
154
-
155
- function normalizeSlugPolicy(value = "") {
156
- const normalizedValue = normalizeLowerText(value);
157
- if (
158
- normalizedValue === WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME ||
159
- normalizedValue === WORKSPACE_SLUG_POLICY_USER_SELECTED
160
- ) {
161
- return normalizedValue;
162
- }
163
- return WORKSPACE_SLUG_POLICY_NONE;
164
- }
165
-
166
- function isSupportedTenancyMode(value = "") {
167
- return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value === TENANCY_MODE_WORKSPACES;
168
- }
169
-
170
90
  function resolveBootstrapTenancyProfile(tenancyProfile = null, appConfig = {}) {
171
91
  const fallback = resolveTenancyProfile(appConfig);
172
- const source = tenancyProfile && typeof tenancyProfile === "object" ? tenancyProfile : fallback;
173
- const mode = isSupportedTenancyMode(source?.mode) ? source.mode : fallback.mode;
174
- const workspace = source?.workspace && typeof source.workspace === "object" ? source.workspace : fallback.workspace;
175
-
176
92
  return Object.freeze({
177
- mode,
93
+ mode: fallback.mode,
178
94
  workspace: Object.freeze({
179
- enabled: workspace.enabled === true,
180
- autoProvision: workspace.autoProvision === true,
181
- allowSelfCreate: workspace.allowSelfCreate === true,
182
- slugPolicy: normalizeSlugPolicy(workspace.slugPolicy)
95
+ enabled: fallback.workspace.enabled === true,
96
+ autoProvision: fallback.workspace.autoProvision === true,
97
+ allowSelfCreate: fallback.workspace.allowSelfCreate === true,
98
+ slugPolicy: fallback.workspace.slugPolicy
183
99
  })
184
100
  });
185
101
  }
186
102
 
187
- function createAnonymousBootstrapPayload({ appState, tenancyProfile }) {
188
- return {
189
- session: {
190
- authenticated: false
191
- },
192
- profile: null,
193
- tenancy: tenancyProfile,
194
- app: appState,
195
- workspaces: [],
196
- pendingInvites: [],
197
- activeWorkspace: null,
198
- membership: null,
199
- requestedWorkspace: null,
200
- permissions: [],
201
- surfaceAccess: {
202
- consoleowner: false
203
- },
204
- workspaceSettings: null,
205
- userSettings: null
206
- };
207
- }
208
-
209
- function mapUserSettingsBootstrap(settings = {}) {
210
- const source = settings && typeof settings === "object" ? settings : {};
211
- const mapped = {};
212
-
213
- for (const field of userSettingsFields) {
214
- if (field.includeInBootstrap === false) {
215
- continue;
216
- }
217
- const rawValue = Object.hasOwn(source, field.key)
218
- ? source[field.key]
219
- : field.resolveDefault({
220
- settings: source
221
- });
222
- mapped[field.key] = field.normalizeOutput(rawValue, {
223
- settings: source
224
- });
225
- }
226
-
227
- return mapped;
228
- }
229
-
230
103
  function createWorkspaceBootstrapContributor({
231
104
  workspaceService,
232
105
  workspacePendingInvitationsService,
233
106
  usersRepository,
234
- userSettingsRepository,
235
107
  workspaceInvitationsEnabled = false,
236
108
  appConfig = {},
237
- tenancyProfile = null,
238
- authService,
239
- consoleService = null
109
+ tenancyProfile = null
240
110
  } = {}) {
241
- const contributorId = "users.bootstrap";
242
- const appState = resolveAppState(appConfig, {
243
- workspaceInvitationsEnabled
244
- });
111
+ const contributorId = "users.workspace.bootstrap";
245
112
  const resolvedTenancyProfile = resolveBootstrapTenancyProfile(tenancyProfile, appConfig);
246
113
 
247
114
  requireServiceMethod(workspaceService, "listWorkspacesForUser", contributorId, {
@@ -255,144 +122,83 @@ function createWorkspaceBootstrapContributor({
255
122
  serviceLabel: "workspacePendingInvitationsService"
256
123
  });
257
124
  }
258
- requireServiceMethod(usersRepository, "findByIdentity", contributorId, {
125
+ requireServiceMethod(usersRepository, "findById", contributorId, {
259
126
  serviceLabel: "usersRepository"
260
127
  });
261
- requireServiceMethod(userSettingsRepository, "ensureForUserId", contributorId, {
262
- serviceLabel: "userSettingsRepository"
263
- });
264
128
 
265
129
  return Object.freeze({
266
130
  contributorId,
267
- async contribute({ request = null, reply = null, query = {} } = {}) {
268
- const authResult = await request.executeAction({
269
- actionId: "auth.session.read"
270
- });
131
+ async contribute({ request = null, query = {}, payload = {} } = {}) {
132
+ const normalizedUserId = Number(payload?.session?.authenticated === true ? payload?.session?.userId : 0);
133
+ const normalizedWorkspaceSlug = resolveBootstrapWorkspaceSlug({ query, request });
134
+ if (!normalizedUserId) {
135
+ if (!normalizedWorkspaceSlug || resolvedTenancyProfile.mode === TENANCY_MODE_NONE) {
136
+ return {};
137
+ }
271
138
 
272
- if (authResult?.clearSession === true && typeof authService?.clearSessionCookies === "function") {
273
- authService.clearSessionCookies(reply);
274
- }
275
- if (authResult?.session && typeof authService?.writeSessionCookies === "function") {
276
- authService.writeSessionCookies(reply, authResult.session);
277
- }
278
- if (authResult?.transientFailure === true) {
279
- throw new AppError(503, "Authentication service temporarily unavailable. Please retry.");
139
+ return {
140
+ requestedWorkspace: createRequestedWorkspacePayload(
141
+ normalizedWorkspaceSlug,
142
+ REQUESTED_WORKSPACE_STATUS_UNAUTHENTICATED
143
+ )
144
+ };
280
145
  }
281
- let seededConsoleOwnerUserId = 0;
282
- if (
283
- authResult?.authenticated &&
284
- authResult?.profile?.id != null &&
285
- consoleService &&
286
- typeof consoleService.ensureInitialConsoleMember === "function"
287
- ) {
288
- seededConsoleOwnerUserId = Number(await consoleService.ensureInitialConsoleMember(authResult.profile.id));
146
+
147
+ const latestProfile = await usersRepository.findById(normalizedUserId);
148
+ if (!latestProfile) {
149
+ return {};
289
150
  }
290
151
 
291
- const user = authResult?.authenticated ? authResult.profile : null;
292
- const normalizedUser = authenticatedUserValidator.normalize(user);
293
152
  const pendingInvites =
294
- workspaceInvitationsEnabled && normalizedUser
153
+ workspaceInvitationsEnabled
295
154
  ? normalizePendingInvites(
296
- await workspacePendingInvitationsService.listPendingInvitesForUser(normalizedUser, {
155
+ await workspacePendingInvitationsService.listPendingInvitesForUser(latestProfile, {
297
156
  context: {
298
- actor: normalizedUser
157
+ actor: latestProfile
299
158
  }
300
159
  })
301
160
  )
302
161
  : [];
303
- const normalizedWorkspaceSlug = resolveBootstrapWorkspaceSlug({ query, request });
304
- let payload = createAnonymousBootstrapPayload({
305
- appState,
306
- tenancyProfile: resolvedTenancyProfile
307
- });
308
- if (normalizedWorkspaceSlug && !normalizedUser) {
309
- payload = {
310
- ...payload,
311
- requestedWorkspace: createRequestedWorkspacePayload(
162
+ const workspaces = await workspaceService.listWorkspacesForUser(latestProfile, { request });
163
+ let workspaceContext = null;
164
+ let requestedWorkspace = null;
165
+ if (normalizedWorkspaceSlug && resolvedTenancyProfile.mode !== TENANCY_MODE_NONE) {
166
+ try {
167
+ workspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
168
+ latestProfile,
312
169
  normalizedWorkspaceSlug,
313
- REQUESTED_WORKSPACE_STATUS_UNAUTHENTICATED
314
- )
315
- };
316
- }
317
-
318
- if (normalizedUser) {
319
- const latestProfile =
320
- (await usersRepository.findByIdentity({
321
- provider: normalizedUser.authProvider,
322
- providerUserId: normalizedUser.authProviderUserSid
323
- })) || normalizedUser;
324
-
325
- const workspaces = await workspaceService.listWorkspacesForUser(latestProfile, { request });
326
- let workspaceContext = null;
327
- let requestedWorkspace = null;
328
- if (normalizedWorkspaceSlug && resolvedTenancyProfile.mode !== TENANCY_MODE_NONE) {
329
- try {
330
- workspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
331
- latestProfile,
332
- normalizedWorkspaceSlug,
333
- { request }
334
- );
335
- requestedWorkspace = createRequestedWorkspacePayload(
336
- normalizedWorkspaceSlug,
337
- REQUESTED_WORKSPACE_STATUS_RESOLVED
338
- );
339
- } catch (error) {
340
- const requestedWorkspaceStatus = resolveRequestedWorkspaceStatusFromError(error);
341
- if (!requestedWorkspaceStatus) {
342
- throw error;
343
- }
344
- requestedWorkspace = createRequestedWorkspacePayload(normalizedWorkspaceSlug, requestedWorkspaceStatus);
170
+ { request }
171
+ );
172
+ requestedWorkspace = createRequestedWorkspacePayload(
173
+ normalizedWorkspaceSlug,
174
+ REQUESTED_WORKSPACE_STATUS_RESOLVED
175
+ );
176
+ } catch (error) {
177
+ const requestedWorkspaceStatus = resolveRequestedWorkspaceStatusFromError(error);
178
+ if (!requestedWorkspaceStatus) {
179
+ throw error;
345
180
  }
181
+ requestedWorkspace = createRequestedWorkspacePayload(normalizedWorkspaceSlug, requestedWorkspaceStatus);
346
182
  }
347
-
348
- const userSettings = await userSettingsRepository.ensureForUserId(latestProfile.id);
349
- payload = {
350
- session: {
351
- authenticated: true,
352
- userId: latestProfile.id
353
- },
354
- profile: {
355
- displayName: latestProfile.displayName,
356
- email: latestProfile.email,
357
- avatar: accountAvatarFormatter(latestProfile, userSettings)
358
- },
359
- tenancy: resolvedTenancyProfile,
360
- app: appState,
361
- workspaces: [...workspaces],
362
- pendingInvites,
363
- activeWorkspace: workspaceContext
364
- ? mapWorkspaceSummary(workspaceContext.workspace, {
365
- roleSid: workspaceContext.membership?.roleSid,
366
- status: workspaceContext.membership?.status
367
- })
368
- : null,
369
- membership: mapMembershipSummary(workspaceContext?.membership, workspaceContext?.workspace),
370
- requestedWorkspace,
371
- permissions: workspaceContext ? [...workspaceContext.permissions] : [],
372
- surfaceAccess: {
373
- consoleowner: seededConsoleOwnerUserId > 0 && seededConsoleOwnerUserId === Number(latestProfile.id)
374
- },
375
- workspaceSettings: workspaceContext
376
- ? mapWorkspaceSettingsPublic(workspaceContext.workspaceSettings, {
377
- workspaceInvitationsEnabled
378
- })
379
- : null,
380
- userSettings: mapUserSettingsBootstrap(userSettings),
381
- requestMeta: {
382
- hasRequest: Boolean(request)
383
- }
384
- };
385
183
  }
386
184
 
387
- const oauthCatalogPayload = getOAuthProviderCatalogPayload(authService);
388
- const session = payload?.session && typeof payload.session === "object" ? payload.session : { authenticated: false };
389
-
390
185
  return {
391
- ...payload,
392
- session: {
393
- ...session,
394
- ...oauthCatalogPayload
395
- }
186
+ workspaces: [...workspaces],
187
+ pendingInvites,
188
+ activeWorkspace: workspaceContext
189
+ ? mapWorkspaceSummary(workspaceContext.workspace, {
190
+ roleSid: workspaceContext.membership?.roleSid,
191
+ status: workspaceContext.membership?.status
192
+ })
193
+ : null,
194
+ membership: mapMembershipSummary(workspaceContext?.membership, workspaceContext?.workspace),
195
+ requestedWorkspace,
196
+ permissions: workspaceContext ? [...workspaceContext.permissions] : [],
197
+ workspaceSettings: workspaceContext
198
+ ? mapWorkspaceSettingsPublic(workspaceContext.workspaceSettings, {
199
+ workspaceInvitationsEnabled
200
+ })
201
+ : null
396
202
  };
397
203
  }
398
204
  });
@@ -30,8 +30,7 @@ const DEFAULT_USER_SETTINGS = Object.freeze({
30
30
  accountActivity: true,
31
31
  securityAlerts: true,
32
32
  passwordSignInEnabled: true,
33
- passwordSetupRequired: false,
34
- lastActiveWorkspaceId: null
33
+ passwordSetupRequired: false
35
34
  });
36
35
 
37
36
  function normalizeWorkspaceHexColor(value) {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @param {import('knex').Knex} knex
3
+ */
4
+ exports.up = async function up(knex) {
5
+ const hasUsersTable = await knex.schema.hasTable("users");
6
+ if (!hasUsersTable) {
7
+ await knex.schema.createTable("users", (table) => {
8
+ table.increments("id").primary();
9
+ table.string("auth_provider", 64).notNullable();
10
+ table.string("auth_provider_user_sid", 191).notNullable();
11
+ table.string("email", 255).notNullable();
12
+ table.string("username", 120).notNullable();
13
+ table.string("display_name", 160).notNullable();
14
+ table.string("avatar_storage_key", 512).nullable();
15
+ table.string("avatar_version", 64).nullable();
16
+ table.timestamp("avatar_updated_at", { useTz: false }).nullable();
17
+ table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
18
+ table.unique(["auth_provider", "auth_provider_user_sid"], "uq_users_identity");
19
+ table.unique(["email"], "uq_users_email");
20
+ table.unique(["username"], "uq_users_username");
21
+ });
22
+ }
23
+
24
+ const hasUserSettingsTable = await knex.schema.hasTable("user_settings");
25
+ if (!hasUserSettingsTable) {
26
+ await knex.schema.createTable("user_settings", (table) => {
27
+ table.integer("user_id").unsigned().primary().references("id").inTable("users").onDelete("CASCADE");
28
+ table.string("theme", 32).notNullable().defaultTo("system");
29
+ table.string("locale", 24).notNullable().defaultTo("en");
30
+ table.string("time_zone", 64).notNullable().defaultTo("UTC");
31
+ table.string("date_format", 32).notNullable().defaultTo("yyyy-mm-dd");
32
+ table.string("number_format", 32).notNullable().defaultTo("1,234.56");
33
+ table.string("currency_code", 3).notNullable().defaultTo("USD");
34
+ table.integer("avatar_size").notNullable().defaultTo(64);
35
+ table.boolean("password_sign_in_enabled").notNullable().defaultTo(true);
36
+ table.boolean("password_setup_required").notNullable().defaultTo(false);
37
+ table.boolean("notify_product_updates").notNullable().defaultTo(true);
38
+ table.boolean("notify_account_activity").notNullable().defaultTo(true);
39
+ table.boolean("notify_security_alerts").notNullable().defaultTo(true);
40
+ table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
41
+ table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
42
+ });
43
+ }
44
+
45
+ const hasConsoleSettingsTable = await knex.schema.hasTable("console_settings");
46
+ if (!hasConsoleSettingsTable) {
47
+ await knex.schema.createTable("console_settings", (table) => {
48
+ table.integer("id").primary();
49
+ table.integer("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
50
+ table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
51
+ table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
52
+ });
53
+
54
+ await knex("console_settings").insert({
55
+ id: 1,
56
+ created_at: knex.fn.now(),
57
+ updated_at: knex.fn.now()
58
+ });
59
+ }
60
+ };
61
+
62
+ /**
63
+ * @param {import('knex').Knex} knex
64
+ */
65
+ exports.down = async function down(knex) {
66
+ await knex.schema.dropTableIfExists("console_settings");
67
+ await knex.schema.dropTableIfExists("user_settings");
68
+ await knex.schema.dropTableIfExists("users");
69
+ };
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { registerUsersCore } from "../src/server/registerUsersCore.js";
4
+
5
+ test("registerUsersCore registers console and workspace action surface aliases when action runtime is available", () => {
6
+ const calls = [];
7
+ const app = {
8
+ singleton() {
9
+ return this;
10
+ },
11
+ actionSurfaceSource(sourceName, resolver) {
12
+ calls.push({
13
+ sourceName: String(sourceName || ""),
14
+ resolverType: typeof resolver
15
+ });
16
+ return this;
17
+ }
18
+ };
19
+
20
+ registerUsersCore(app);
21
+
22
+ assert.deepEqual(calls, [
23
+ {
24
+ sourceName: "workspace",
25
+ resolverType: "function"
26
+ },
27
+ {
28
+ sourceName: "console",
29
+ resolverType: "function"
30
+ }
31
+ ]);
32
+ });
33
+
34
+ test("registerUsersCore still works when action runtime has not installed actionSurfaceSource yet", () => {
35
+ const app = {
36
+ singleton() {
37
+ return this;
38
+ }
39
+ };
40
+
41
+ assert.doesNotThrow(() => registerUsersCore(app));
42
+ });
@@ -0,0 +1,172 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createUsersBootstrapContributor } from "../src/server/usersBootstrapContributor.js";
4
+ import {
5
+ TENANCY_MODE_PERSONAL,
6
+ WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
7
+ } from "../src/shared/tenancyProfile.js";
8
+
9
+ function createAuthenticatedProfile(overrides = {}) {
10
+ return {
11
+ id: 7,
12
+ authProvider: "local",
13
+ authProviderUserSid: "user-7",
14
+ username: "tester",
15
+ displayName: "Test User",
16
+ email: "test@example.com",
17
+ ...overrides
18
+ };
19
+ }
20
+
21
+ function createUserSettings() {
22
+ return {
23
+ theme: "system",
24
+ locale: "en",
25
+ timeZone: "UTC",
26
+ dateFormat: "YYYY-MM-DD",
27
+ numberFormat: "1,234.56",
28
+ currencyCode: "USD",
29
+ avatarSize: 64,
30
+ productUpdates: true,
31
+ accountActivity: true,
32
+ securityAlerts: true
33
+ };
34
+ }
35
+
36
+ test("users bootstrap contributor seeds the initial console owner and exposes generic app payload", async () => {
37
+ const profile = createAuthenticatedProfile({ id: 12 });
38
+ const consoleOwnerSeeds = [];
39
+ const writtenSessions = [];
40
+ const contributor = createUsersBootstrapContributor({
41
+ usersRepository: {
42
+ async findById() {
43
+ return profile;
44
+ }
45
+ },
46
+ userSettingsRepository: {
47
+ async ensureForUserId() {
48
+ return createUserSettings();
49
+ }
50
+ },
51
+ authService: {
52
+ writeSessionCookies(reply, session) {
53
+ writtenSessions.push({ reply, session });
54
+ },
55
+ getOAuthProviderCatalog() {
56
+ return {
57
+ providers: [
58
+ { id: "google", label: "Google" }
59
+ ],
60
+ defaultProvider: "google"
61
+ };
62
+ }
63
+ },
64
+ consoleService: {
65
+ async ensureInitialConsoleMember(userId) {
66
+ consoleOwnerSeeds.push(Number(userId));
67
+ return Number(userId);
68
+ }
69
+ }
70
+ });
71
+
72
+ const reply = {};
73
+ const payload = await contributor.contribute({
74
+ request: {
75
+ async executeAction() {
76
+ return {
77
+ authenticated: true,
78
+ profile,
79
+ session: {
80
+ csrfToken: "csrf-1"
81
+ }
82
+ };
83
+ }
84
+ },
85
+ reply
86
+ });
87
+
88
+ assert.deepEqual(consoleOwnerSeeds, [12]);
89
+ assert.equal(writtenSessions.length, 1);
90
+ assert.equal(writtenSessions[0].reply, reply);
91
+ assert.deepEqual(writtenSessions[0].session, {
92
+ csrfToken: "csrf-1"
93
+ });
94
+ assert.equal(payload.session.authenticated, true);
95
+ assert.equal(payload.session.userId, 12);
96
+ assert.equal(payload.surfaceAccess.consoleowner, true);
97
+ assert.equal(payload.app.features.workspaceSwitching, false);
98
+ assert.deepEqual(payload.session.oauthProviders, [
99
+ {
100
+ id: "google",
101
+ label: "Google"
102
+ }
103
+ ]);
104
+ assert.equal(payload.session.oauthDefaultProvider, "google");
105
+ assert.deepEqual(payload.workspaces, []);
106
+ assert.deepEqual(payload.userSettings, {});
107
+ assert.equal(payload.requestMeta.hasRequest, true);
108
+ });
109
+
110
+ test("users bootstrap contributor emits canonical tenancy profile for anonymous bootstrap", async () => {
111
+ const contributor = createUsersBootstrapContributor({
112
+ usersRepository: {
113
+ async findById() {
114
+ return null;
115
+ }
116
+ },
117
+ userSettingsRepository: {
118
+ async ensureForUserId() {
119
+ return createUserSettings();
120
+ }
121
+ },
122
+ tenancyProfile: {
123
+ mode: TENANCY_MODE_PERSONAL,
124
+ workspace: {
125
+ enabled: true,
126
+ autoProvision: true,
127
+ allowSelfCreate: false,
128
+ slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
129
+ }
130
+ },
131
+ appConfig: {
132
+ tenancyMode: "none"
133
+ },
134
+ authService: {
135
+ getOAuthProviderCatalog() {
136
+ return {
137
+ providers: [],
138
+ defaultProvider: null
139
+ };
140
+ }
141
+ }
142
+ });
143
+
144
+ const payload = await contributor.contribute({
145
+ request: {
146
+ async executeAction() {
147
+ return {
148
+ authenticated: false
149
+ };
150
+ }
151
+ },
152
+ reply: {}
153
+ });
154
+
155
+ assert.deepEqual(payload.tenancy, {
156
+ mode: TENANCY_MODE_PERSONAL,
157
+ workspace: {
158
+ enabled: true,
159
+ autoProvision: true,
160
+ allowSelfCreate: false,
161
+ slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
162
+ }
163
+ });
164
+ assert.deepEqual(payload.session, {
165
+ authenticated: false,
166
+ oauthProviders: [],
167
+ oauthDefaultProvider: null
168
+ });
169
+ assert.deepEqual(payload.workspaces, []);
170
+ assert.equal(payload.surfaceAccess.consoleowner, false);
171
+ assert.equal(payload.userSettings, null);
172
+ });