@jskit-ai/users-core 0.1.23 → 0.1.25

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,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-core",
4
- version: "0.1.23",
4
+ version: "0.1.25",
5
5
  description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
6
6
  dependsOn: [
7
7
  "@jskit-ai/auth-core",
@@ -261,6 +261,15 @@ export default Object.freeze({
261
261
  category: "migration",
262
262
  id: "users-core-workspace-settings-single-name-source"
263
263
  },
264
+ {
265
+ op: "install-migration",
266
+ from: "templates/migrations/users_core_workspaces_drop_color.cjs",
267
+ toDir: "migrations",
268
+ extension: ".cjs",
269
+ reason: "Drop legacy workspaces.color now that workspace theme colors live in workspace_settings.",
270
+ category: "migration",
271
+ id: "users-core-workspaces-drop-color"
272
+ },
264
273
  {
265
274
  from: "templates/packages/main/src/shared/resources/workspaceSettingsFields.js",
266
275
  to: "packages/main/src/shared/resources/workspaceSettingsFields.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-core",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -1,7 +1,4 @@
1
- import {
2
- coerceWorkspaceColor,
3
- resolveWorkspaceThemePalettes
4
- } from "../../../shared/settings.js";
1
+ import { resolveWorkspaceThemePalettes } from "../../../shared/settings.js";
5
2
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
6
3
 
7
4
  function mapWorkspaceSummary(workspace, membership) {
@@ -9,7 +6,6 @@ function mapWorkspaceSummary(workspace, membership) {
9
6
  id: Number(workspace.id),
10
7
  slug: normalizeText(workspace.slug),
11
8
  name: normalizeText(workspace.name),
12
- color: coerceWorkspaceColor(workspace.color),
13
9
  avatarUrl: normalizeText(workspace.avatarUrl),
14
10
  roleId: normalizeLowerText(membership?.roleId || "member") || "member",
15
11
  isAccessible: normalizeLowerText(membership?.status || "active") === "active"
@@ -6,13 +6,11 @@ import {
6
6
  nowDb,
7
7
  isDuplicateEntryError
8
8
  } from "./repositoryUtils.js";
9
- import { coerceWorkspaceColor } from "../../../shared/settings.js";
10
9
 
11
10
  function mapRow(row) {
12
11
  if (!row) {
13
12
  return null;
14
13
  }
15
- const color = coerceWorkspaceColor(row.color);
16
14
 
17
15
  return {
18
16
  id: Number(row.id),
@@ -21,7 +19,6 @@ function mapRow(row) {
21
19
  ownerUserId: Number(row.owner_user_id),
22
20
  isPersonal: Boolean(row.is_personal),
23
21
  avatarUrl: row.avatar_url ? normalizeText(row.avatar_url) : "",
24
- color,
25
22
  createdAt: toIsoString(row.created_at),
26
23
  updatedAt: toIsoString(row.updated_at),
27
24
  deletedAt: toNullableIso(row.deleted_at)
@@ -53,7 +50,6 @@ function createRepository(knex) {
53
50
  "w.owner_user_id",
54
51
  "w.is_personal",
55
52
  "w.avatar_url",
56
- "w.color",
57
53
  "w.created_at",
58
54
  "w.updated_at",
59
55
  "w.deleted_at"
@@ -107,7 +103,6 @@ function createRepository(knex) {
107
103
  owner_user_id: Number(source.ownerUserId),
108
104
  is_personal: source.isPersonal ? 1 : 0,
109
105
  avatar_url: normalizeText(source.avatarUrl),
110
- color: coerceWorkspaceColor(source.color),
111
106
  created_at: nowDb(),
112
107
  updated_at: nowDb(),
113
108
  deleted_at: null
@@ -146,9 +141,6 @@ function createRepository(knex) {
146
141
  if (Object.hasOwn(source, "avatarUrl")) {
147
142
  dbPatch.avatar_url = normalizeText(source.avatarUrl);
148
143
  }
149
- if (Object.hasOwn(source, "color")) {
150
- dbPatch.color = coerceWorkspaceColor(source.color);
151
- }
152
144
 
153
145
  await client("workspaces").where({ id: Number(workspaceId) }).update(dbPatch);
154
146
  return findById(workspaceId, { trx: client });
@@ -4,7 +4,6 @@ import {
4
4
  TENANCY_MODE_NONE,
5
5
  resolveTenancyProfile
6
6
  } from "../../../shared/tenancyProfile.js";
7
- import { coerceWorkspaceColor } from "../../../shared/settings.js";
8
7
  import {
9
8
  resolveRolePermissions
10
9
  } from "../../../shared/roles.js";
@@ -84,7 +83,6 @@ function createService({
84
83
  const resolvedTenancyProfile = resolveTenancyProfile(appConfig);
85
84
  const resolvedTenancyMode = resolvedTenancyProfile.mode;
86
85
  const workspacePolicy = resolvedTenancyProfile.workspace;
87
- const resolvedWorkspaceColor = coerceWorkspaceColor(appConfig.workspaceColor);
88
86
  async function ensureUniqueWorkspaceSlug(baseSlug, options = {}) {
89
87
  let suffix = 0;
90
88
  while (suffix < 1000) {
@@ -125,8 +123,7 @@ function createService({
125
123
  name: buildWorkspaceName(normalizedUser),
126
124
  ownerUserId: normalizedUser.id,
127
125
  isPersonal: true,
128
- avatarUrl: "",
129
- color: resolvedWorkspaceColor
126
+ avatarUrl: ""
130
127
  },
131
128
  options
132
129
  );
@@ -194,8 +191,7 @@ function createService({
194
191
  name: createInput.name,
195
192
  ownerUserId: normalizedUser.id,
196
193
  isPersonal: false,
197
- avatarUrl: "",
198
- color: resolvedWorkspaceColor
194
+ avatarUrl: ""
199
195
  },
200
196
  options
201
197
  );
@@ -11,8 +11,7 @@ const workspaceSummaryOutputSchema = Type.Object(
11
11
  slug: Type.String({ minLength: 1 }),
12
12
  name: Type.String({ minLength: 1 }),
13
13
  ownerUserId: Type.Integer({ minimum: 1 }),
14
- avatarUrl: Type.String(),
15
- color: Type.String({ minLength: 1 })
14
+ avatarUrl: Type.String()
16
15
  },
17
16
  { additionalProperties: false }
18
17
  );
@@ -49,8 +48,7 @@ function normalizeWorkspaceAdminSummary(workspace) {
49
48
  slug: normalizeText(source.slug),
50
49
  name: normalizeText(source.name),
51
50
  ownerUserId: Number(source.ownerUserId),
52
- avatarUrl: normalizeText(source.avatarUrl),
53
- color: normalizeText(source.color)
51
+ avatarUrl: normalizeText(source.avatarUrl)
54
52
  };
55
53
  }
56
54
 
@@ -36,10 +36,6 @@ function normalizeWorkspaceInput(payload = {}) {
36
36
  if (Object.hasOwn(source, "avatarUrl")) {
37
37
  normalized.avatarUrl = normalizeWorkspaceAvatarUrl(source.avatarUrl);
38
38
  }
39
- if (Object.hasOwn(source, "color")) {
40
- const color = normalizeText(source.color);
41
- normalized.color = /^#[0-9A-Fa-f]{6}$/.test(color) ? color.toUpperCase() : null;
42
- }
43
39
  if (Object.hasOwn(source, "isPersonal")) {
44
40
  normalized.isPersonal = source.isPersonal === true;
45
41
  }
@@ -55,8 +51,7 @@ function normalizeWorkspaceOutput(payload = {}) {
55
51
  slug: normalizeLowerText(source.slug),
56
52
  name: normalizeText(source.name),
57
53
  ownerUserId: Number(source.ownerUserId),
58
- avatarUrl: normalizeText(source.avatarUrl),
59
- color: normalizeText(source.color).toUpperCase()
54
+ avatarUrl: normalizeText(source.avatarUrl)
60
55
  };
61
56
  }
62
57
 
@@ -67,7 +62,6 @@ function normalizeWorkspaceListItemOutput(payload = {}) {
67
62
  id: Number(source.id),
68
63
  slug: normalizeLowerText(source.slug),
69
64
  name: normalizeText(source.name),
70
- color: normalizeText(source.color).toUpperCase(),
71
65
  avatarUrl: normalizeText(source.avatarUrl),
72
66
  roleId: normalizeLowerText(source.roleId || "member") || "member",
73
67
  isAccessible: source.isAccessible !== false
@@ -80,8 +74,7 @@ const responseRecordSchema = Type.Object(
80
74
  slug: Type.String({ minLength: 1 }),
81
75
  name: Type.String({ minLength: 1, maxLength: 160 }),
82
76
  ownerUserId: Type.Integer({ minimum: 1 }),
83
- avatarUrl: Type.String(),
84
- color: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" })
77
+ avatarUrl: Type.String()
85
78
  },
86
79
  { additionalProperties: false }
87
80
  );
@@ -91,7 +84,6 @@ const listItemSchema = Type.Object(
91
84
  id: Type.Integer({ minimum: 1 }),
92
85
  slug: Type.String({ minLength: 1 }),
93
86
  name: Type.String({ minLength: 1, maxLength: 160 }),
94
- color: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
95
87
  avatarUrl: Type.String(),
96
88
  roleId: Type.String({ minLength: 1 }),
97
89
  isAccessible: Type.Boolean()
@@ -118,17 +110,6 @@ const patchRequestBodySchema = Type.Object(
118
110
  default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
119
111
  }
120
112
  })
121
- ),
122
- color: Type.Optional(
123
- Type.String({
124
- minLength: 7,
125
- maxLength: 7,
126
- pattern: "^#[0-9A-Fa-f]{6}$",
127
- messages: {
128
- pattern: "Workspace color must be a hex color like #1867C0.",
129
- default: "Workspace color must be a hex color like #1867C0."
130
- }
131
- })
132
113
  )
133
114
  },
134
115
  { additionalProperties: false }
@@ -0,0 +1,85 @@
1
+ const WORKSPACES_TABLE = "workspaces";
2
+ const WORKSPACE_SETTINGS_TABLE = "workspace_settings";
3
+ const LEGACY_COLOR_COLUMN = "color";
4
+ const SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN = "light_primary_color";
5
+ const DEFAULT_WORKSPACE_COLOR = "#1867C0";
6
+
7
+ async function hasTable(knex, tableName) {
8
+ return knex.schema.hasTable(tableName);
9
+ }
10
+
11
+ async function hasColumn(knex, tableName, columnName) {
12
+ return knex.schema.hasColumn(tableName, columnName);
13
+ }
14
+
15
+ function normalizeHexColor(value) {
16
+ const normalized = String(value || "").trim().toUpperCase();
17
+ return /^#[0-9A-F]{6}$/.test(normalized) ? normalized : "";
18
+ }
19
+
20
+ exports.up = async function up(knex) {
21
+ const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
22
+ if (!hasWorkspaces) {
23
+ return;
24
+ }
25
+
26
+ const hasLegacyColor = await hasColumn(knex, WORKSPACES_TABLE, LEGACY_COLOR_COLUMN);
27
+ if (!hasLegacyColor) {
28
+ return;
29
+ }
30
+
31
+ await knex.schema.alterTable(WORKSPACES_TABLE, (table) => {
32
+ table.dropColumn(LEGACY_COLOR_COLUMN);
33
+ });
34
+ };
35
+
36
+ exports.down = async function down(knex) {
37
+ const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
38
+ if (!hasWorkspaces) {
39
+ return;
40
+ }
41
+
42
+ const hasLegacyColor = await hasColumn(knex, WORKSPACES_TABLE, LEGACY_COLOR_COLUMN);
43
+ if (!hasLegacyColor) {
44
+ await knex.schema.alterTable(WORKSPACES_TABLE, (table) => {
45
+ table.string(LEGACY_COLOR_COLUMN, 7).notNullable().defaultTo(DEFAULT_WORKSPACE_COLOR);
46
+ });
47
+ }
48
+
49
+ const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
50
+ if (!hasWorkspaceSettings) {
51
+ return;
52
+ }
53
+
54
+ const hasLightPrimaryColor = await hasColumn(
55
+ knex,
56
+ WORKSPACE_SETTINGS_TABLE,
57
+ SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN
58
+ );
59
+ if (!hasLightPrimaryColor) {
60
+ return;
61
+ }
62
+
63
+ const workspaceSettingsRows = await knex(WORKSPACE_SETTINGS_TABLE).select(
64
+ "workspace_id",
65
+ SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN
66
+ );
67
+
68
+ for (const row of workspaceSettingsRows) {
69
+ const workspaceId = Number(row?.workspace_id || 0);
70
+ if (!Number.isInteger(workspaceId) || workspaceId < 1) {
71
+ continue;
72
+ }
73
+
74
+ const restoredColor = normalizeHexColor(row?.[SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN]);
75
+ if (!restoredColor) {
76
+ continue;
77
+ }
78
+
79
+ await knex(WORKSPACES_TABLE)
80
+ .where({ id: workspaceId })
81
+ .update({
82
+ color: restoredColor
83
+ });
84
+ }
85
+ };
@@ -471,7 +471,7 @@ test("workspace route handlers build action input from request.input", async ()
471
471
  createActionRequest({
472
472
  input: {
473
473
  params: { workspaceSlug: "acme" },
474
- body: { name: "Acme", avatarUrl: "https://example.com/acme.png", color: "#0F6B54" }
474
+ body: { name: "Acme", avatarUrl: "https://example.com/acme.png" }
475
475
  },
476
476
  executeAction
477
477
  }),
@@ -482,7 +482,7 @@ test("workspace route handlers build action input from request.input", async ()
482
482
  actionId: "workspace.workspaces.update",
483
483
  input: {
484
484
  workspaceSlug: "acme",
485
- patch: { name: "Acme", avatarUrl: "https://example.com/acme.png", color: "#0F6B54" }
485
+ patch: { name: "Acme", avatarUrl: "https://example.com/acme.png" }
486
486
  }
487
487
  });
488
488
  });
@@ -42,8 +42,7 @@ function createFixture() {
42
42
  slug: "tonymobily3",
43
43
  name: "TonyMobily3",
44
44
  ownerUserId: 9,
45
- avatarUrl: "",
46
- color: "#0F6B54"
45
+ avatarUrl: ""
47
46
  };
48
47
 
49
48
  const service = createService({
@@ -248,8 +247,7 @@ test("workspaceMembersService.listMembers uses the resolved workspace directly",
248
247
  slug: "tonymobily3",
249
248
  name: "TonyMobily3",
250
249
  ownerUserId: 9,
251
- avatarUrl: "",
252
- color: "#0F6B54"
250
+ avatarUrl: ""
253
251
  });
254
252
  assert.equal(response.members.length, 1);
255
253
  assert.equal(response.members[0].displayName, "Alice");
@@ -278,8 +276,7 @@ test("workspaceMembersService.removeMember marks membership revoked and returns
278
276
  slug: "tonymobily3",
279
277
  name: "TonyMobily3",
280
278
  ownerUserId: 9,
281
- avatarUrl: "",
282
- color: "#0F6B54"
279
+ avatarUrl: ""
283
280
  };
284
281
  const service = createService({
285
282
  workspaceMembershipsRepository: {
@@ -349,8 +346,7 @@ test("workspaceMembersService.removeMember rejects removing the owner", async ()
349
346
  slug: "tonymobily3",
350
347
  name: "TonyMobily3",
351
348
  ownerUserId: 9,
352
- avatarUrl: "",
353
- color: "#0F6B54"
349
+ avatarUrl: ""
354
350
  };
355
351
  const service = createService({
356
352
  workspaceMembershipsRepository: {
@@ -31,8 +31,7 @@ function createWorkspaceServiceFixture({
31
31
  name: "TonyMobily3",
32
32
  ownerUserId: 7,
33
33
  isPersonal: true,
34
- avatarUrl: "",
35
- color: "#0F6B54"
34
+ avatarUrl: ""
36
35
  }
37
36
  } = {}) {
38
37
  const calls = {
@@ -96,7 +95,6 @@ function createWorkspaceServiceFixture({
96
95
  slug: "tonymobily3",
97
96
  name: "TonyMobily3",
98
97
  avatarUrl: "",
99
- color: "#0F6B54",
100
98
  roleId: "owner",
101
99
  membershipStatus: "active"
102
100
  },
@@ -105,7 +103,6 @@ function createWorkspaceServiceFixture({
105
103
  slug: "pending-workspace",
106
104
  name: "Pending Workspace",
107
105
  avatarUrl: "",
108
- color: "#0F6B54",
109
106
  roleId: "member",
110
107
  membershipStatus: "pending"
111
108
  }
@@ -121,8 +118,7 @@ function createWorkspaceServiceFixture({
121
118
  name: String(payload.name || ""),
122
119
  ownerUserId: Number(payload.ownerUserId),
123
120
  isPersonal: payload.isPersonal === true,
124
- avatarUrl: String(payload.avatarUrl || ""),
125
- color: String(payload.color || "#0F6B54")
121
+ avatarUrl: String(payload.avatarUrl || "")
126
122
  };
127
123
  workspaceBySlug.set(String(inserted.slug).trim().toLowerCase(), inserted);
128
124
  return inserted;
@@ -143,9 +139,6 @@ function createWorkspaceServiceFixture({
143
139
  if (Object.hasOwn(patch, "avatarUrl")) {
144
140
  updated.avatarUrl = String(patch.avatarUrl || "");
145
141
  }
146
- if (Object.hasOwn(patch, "color")) {
147
- updated.color = String(patch.color || "#0F6B54");
148
- }
149
142
  workspaceBySlug.set(slug, updated);
150
143
  return {
151
144
  ...updated
@@ -227,7 +220,6 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
227
220
  slug: "chiaramobily",
228
221
  name: "Chiara Personal",
229
222
  avatarUrl: "",
230
- color: "#0F6B54",
231
223
  roleId: "owner",
232
224
  membershipStatus: "active"
233
225
  },
@@ -236,7 +228,6 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
236
228
  slug: "tonymobily",
237
229
  name: "Tony Workspace",
238
230
  avatarUrl: "",
239
- color: "#0F6B54",
240
231
  roleId: "member",
241
232
  membershipStatus: "active"
242
233
  },
@@ -245,7 +236,6 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
245
236
  slug: "pending-workspace",
246
237
  name: "Pending Workspace",
247
238
  avatarUrl: "",
248
- color: "#0F6B54",
249
239
  roleId: "member",
250
240
  membershipStatus: "pending"
251
241
  }
@@ -379,8 +369,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug allows personal tena
379
369
  name: "My Personal",
380
370
  ownerUserId: 7,
381
371
  isPersonal: true,
382
- avatarUrl: "",
383
- color: "#0F6B54"
372
+ avatarUrl: ""
384
373
  },
385
374
  additionalWorkspaces: [
386
375
  {
@@ -389,8 +378,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug allows personal tena
389
378
  name: "Team Alpha",
390
379
  ownerUserId: 99,
391
380
  isPersonal: false,
392
- avatarUrl: "",
393
- color: "#0F6B54"
381
+ avatarUrl: ""
394
382
  }
395
383
  ]
396
384
  });
@@ -429,8 +417,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
429
417
  name: "TonyMobily",
430
418
  ownerUserId: 7,
431
419
  isPersonal: true,
432
- avatarUrl: "",
433
- color: "#0F6B54"
420
+ avatarUrl: ""
434
421
  };
435
422
  },
436
423
  async findPersonalByOwnerUserId() {
@@ -515,8 +502,7 @@ test("workspaceService.getWorkspaceForAuthenticatedUser resolves workspace from
515
502
  name: "Team Alpha",
516
503
  ownerUserId: 99,
517
504
  isPersonal: false,
518
- avatarUrl: "",
519
- color: "#0F6B54"
505
+ avatarUrl: ""
520
506
  }
521
507
  ]
522
508
  });
@@ -546,13 +532,11 @@ test("workspaceService.updateWorkspaceForAuthenticatedUser updates workspace pro
546
532
  "tonymobily3",
547
533
  {
548
534
  name: "Updated Workspace",
549
- avatarUrl: "https://example.com/acme.png",
550
- color: "#123ABC"
535
+ avatarUrl: "https://example.com/acme.png"
551
536
  }
552
537
  );
553
538
 
554
539
  assert.equal(calls.updateById, 1);
555
540
  assert.equal(workspace.name, "Updated Workspace");
556
541
  assert.equal(workspace.avatarUrl, "https://example.com/acme.png");
557
- assert.equal(workspace.color, "#123ABC");
558
542
  });
@@ -25,8 +25,7 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
25
25
  id: 7,
26
26
  slug: "tonymobily3",
27
27
  name: "TonyMobily3",
28
- ownerUserId: 9,
29
- color: defaultTheme.light.color
28
+ ownerUserId: 9
30
29
  },
31
30
  settings: {
32
31
  lightPrimaryColor: defaultTheme.light.color,