@jskit-ai/users-core 0.1.21 → 0.1.23

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.21",
4
+ version: "0.1.23",
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",
@@ -203,10 +203,10 @@ export default Object.freeze({
203
203
  mutations: {
204
204
  dependencies: {
205
205
  runtime: {
206
- "@jskit-ai/auth-core": "0.1.15",
207
- "@jskit-ai/database-runtime": "0.1.16",
208
- "@jskit-ai/http-runtime": "0.1.15",
209
- "@jskit-ai/kernel": "0.1.16",
206
+ "@jskit-ai/auth-core": "0.1.16",
207
+ "@jskit-ai/database-runtime": "0.1.17",
208
+ "@jskit-ai/http-runtime": "0.1.16",
209
+ "@jskit-ai/kernel": "0.1.17",
210
210
  "@fastify/multipart": "^9.4.0",
211
211
  "@fastify/type-provider-typebox": "^6.1.0",
212
212
  "typebox": "^1.0.81"
@@ -252,6 +252,15 @@ export default Object.freeze({
252
252
  category: "migration",
253
253
  id: "users-core-console-owner-schema"
254
254
  },
255
+ {
256
+ op: "install-migration",
257
+ from: "templates/migrations/users_core_workspace_settings_single_name_source.cjs",
258
+ toDir: "migrations",
259
+ extension: ".cjs",
260
+ reason: "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
261
+ category: "migration",
262
+ id: "users-core-workspace-settings-single-name-source"
263
+ },
255
264
  {
256
265
  from: "templates/packages/main/src/shared/resources/workspaceSettingsFields.js",
257
266
  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.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -16,6 +16,7 @@
16
16
  "./shared/support/usersVisibility": "./src/shared/support/usersVisibility.js",
17
17
  "./shared/support/workspacePathModel": "./src/shared/support/workspacePathModel.js",
18
18
  "./shared/support/usersApiPaths": "./src/shared/support/usersApiPaths.js",
19
+ "./shared/resources/workspaceResource": "./src/shared/resources/workspaceResource.js",
19
20
  "./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js",
20
21
  "./shared/resources/workspaceSettingsFields": "./src/shared/resources/workspaceSettingsFields.js",
21
22
  "./shared/resources/userProfileResource": "./src/shared/resources/userProfileResource.js",
@@ -24,10 +25,10 @@
24
25
  "./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
25
26
  },
26
27
  "dependencies": {
27
- "@jskit-ai/auth-core": "0.1.15",
28
- "@jskit-ai/database-runtime": "0.1.16",
29
- "@jskit-ai/http-runtime": "0.1.15",
30
- "@jskit-ai/kernel": "0.1.16",
28
+ "@jskit-ai/auth-core": "0.1.16",
29
+ "@jskit-ai/database-runtime": "0.1.17",
30
+ "@jskit-ai/http-runtime": "0.1.16",
31
+ "@jskit-ai/kernel": "0.1.17",
31
32
  "@fastify/multipart": "^9.4.0",
32
33
  "@fastify/type-provider-typebox": "^6.1.0",
33
34
  "typebox": "^1.0.81"
@@ -23,7 +23,6 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
23
23
  const themePalettes = resolveWorkspaceThemePalettes(source);
24
24
 
25
25
  return {
26
- name: normalizeText(source.name),
27
26
  lightPrimaryColor: themePalettes.light.color,
28
27
  lightSecondaryColor: themePalettes.light.secondaryColor,
29
28
  lightSurfaceColor: themePalettes.light.surfaceColor,
@@ -32,7 +31,6 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
32
31
  darkSecondaryColor: themePalettes.dark.secondaryColor,
33
32
  darkSurfaceColor: themePalettes.dark.surfaceColor,
34
33
  darkSurfaceVariantColor: themePalettes.dark.surfaceVariantColor,
35
- avatarUrl: normalizeText(source.avatarUrl),
36
34
  invitesEnabled,
37
35
  invitesAvailable,
38
36
  invitesEffective: invitesAvailable && invitesEnabled
@@ -45,14 +45,14 @@ function createRepository(knex) {
45
45
  throw new TypeError("workspacesRepository requires knex.");
46
46
  }
47
47
 
48
- function workspaceSelectColumns(client, { includeMembership = false } = {}) {
48
+ function workspaceSelectColumns({ includeMembership = false } = {}) {
49
49
  const columns = [
50
50
  "w.id",
51
51
  "w.slug",
52
- client.raw("COALESCE(ws.name, w.name) as name"),
52
+ "w.name",
53
53
  "w.owner_user_id",
54
54
  "w.is_personal",
55
- client.raw("COALESCE(ws.avatar_url, w.avatar_url) as avatar_url"),
55
+ "w.avatar_url",
56
56
  "w.color",
57
57
  "w.created_at",
58
58
  "w.updated_at",
@@ -67,9 +67,8 @@ function createRepository(knex) {
67
67
  async function findById(workspaceId, options = {}) {
68
68
  const client = options?.trx || knex;
69
69
  const row = await client("workspaces as w")
70
- .leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
71
70
  .where({ "w.id": Number(workspaceId) })
72
- .select(workspaceSelectColumns(client))
71
+ .select(workspaceSelectColumns())
73
72
  .first();
74
73
  return mapRow(row);
75
74
  }
@@ -82,9 +81,8 @@ function createRepository(knex) {
82
81
  }
83
82
 
84
83
  const row = await client("workspaces as w")
85
- .leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
86
84
  .where({ "w.slug": normalizedSlug })
87
- .select(workspaceSelectColumns(client))
85
+ .select(workspaceSelectColumns())
88
86
  .first();
89
87
  return mapRow(row);
90
88
  }
@@ -92,10 +90,9 @@ function createRepository(knex) {
92
90
  async function findPersonalByOwnerUserId(userId, options = {}) {
93
91
  const client = options?.trx || knex;
94
92
  const row = await client("workspaces as w")
95
- .leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
96
93
  .where({ "w.owner_user_id": Number(userId), "w.is_personal": 1 })
97
94
  .orderBy("w.id", "asc")
98
- .select(workspaceSelectColumns(client))
95
+ .select(workspaceSelectColumns())
99
96
  .first();
100
97
  return mapRow(row);
101
98
  }
@@ -161,12 +158,11 @@ function createRepository(knex) {
161
158
  const client = options?.trx || knex;
162
159
  const rows = await client("workspace_memberships as wm")
163
160
  .join("workspaces as w", "w.id", "wm.workspace_id")
164
- .leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
165
161
  .where({ "wm.user_id": Number(userId) })
166
162
  .whereNull("w.deleted_at")
167
163
  .orderBy("w.is_personal", "desc")
168
164
  .orderBy("w.id", "asc")
169
- .select(workspaceSelectColumns(client, { includeMembership: true }));
165
+ .select(workspaceSelectColumns({ includeMembership: true }));
170
166
 
171
167
  return rows.map(mapMembershipWorkspaceRow).filter(Boolean);
172
168
  }
@@ -205,6 +205,16 @@ function createService({
205
205
  return inserted;
206
206
  }
207
207
 
208
+ async function getWorkspaceForAuthenticatedUser(user, workspaceSlug, options = {}) {
209
+ const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
210
+ return workspaceContext.workspace;
211
+ }
212
+
213
+ async function updateWorkspaceForAuthenticatedUser(user, workspaceSlug, patch = {}, options = {}) {
214
+ const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
215
+ return workspacesRepository.updateById(workspaceContext.workspace.id, patch, options);
216
+ }
217
+
208
218
  async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
209
219
  const normalizedUser = authenticatedUserValidator.normalize(user);
210
220
  if (!normalizedUser) {
@@ -261,6 +271,8 @@ function createService({
261
271
  ensurePersonalWorkspaceForUser,
262
272
  provisionWorkspaceForNewUser,
263
273
  createWorkspaceForAuthenticatedUser,
274
+ getWorkspaceForAuthenticatedUser,
275
+ updateWorkspaceForAuthenticatedUser,
264
276
  listWorkspacesForUser,
265
277
  listWorkspacesForAuthenticatedUser,
266
278
  resolveWorkspaceContextForUserBySlug
@@ -4,6 +4,9 @@ import { workspaceResource } from "../../shared/resources/workspaceResource.js";
4
4
  import {
5
5
  USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN
6
6
  } from "../common/diTokens.js";
7
+ import { resolveWorkspaceRoutePath } from "../common/support/workspaceRoutePaths.js";
8
+ import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
9
+ import { resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig } from "../support/workspaceActionSurfaces.js";
7
10
 
8
11
  function bootWorkspaceDirectoryRoutes(app) {
9
12
  if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
@@ -11,6 +14,8 @@ function bootWorkspaceDirectoryRoutes(app) {
11
14
  }
12
15
 
13
16
  const router = app.make(KERNEL_TOKENS.HttpRouter);
17
+ const appConfig = app.has("appConfig") ? app.make("appConfig") : {};
18
+ const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
14
19
  const workspaceSelfCreateEnabled = app.has(USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN)
15
20
  ? app.make(USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN) === true
16
21
  : false;
@@ -68,6 +73,65 @@ function bootWorkspaceDirectoryRoutes(app) {
68
73
  reply.code(200).send(response);
69
74
  }
70
75
  );
76
+
77
+ router.register(
78
+ "GET",
79
+ resolveWorkspaceRoutePath("/"),
80
+ {
81
+ auth: "required",
82
+ surface: workspaceRouteSurfaceId,
83
+ visibility: "workspace",
84
+ meta: {
85
+ tags: ["workspace"],
86
+ summary: "Get workspace profile by workspace slug"
87
+ },
88
+ paramsValidator: workspaceSlugParamsValidator,
89
+ responseValidators: withStandardErrorResponses({
90
+ 200: workspaceResource.operations.view.outputValidator
91
+ })
92
+ },
93
+ async function (request, reply) {
94
+ const response = await request.executeAction({
95
+ actionId: "workspace.workspaces.read",
96
+ input: {
97
+ workspaceSlug: request.input.params.workspaceSlug
98
+ }
99
+ });
100
+ reply.code(200).send(response);
101
+ }
102
+ );
103
+
104
+ router.register(
105
+ "PATCH",
106
+ resolveWorkspaceRoutePath("/"),
107
+ {
108
+ auth: "required",
109
+ surface: workspaceRouteSurfaceId,
110
+ visibility: "workspace",
111
+ meta: {
112
+ tags: ["workspace"],
113
+ summary: "Update workspace profile by workspace slug"
114
+ },
115
+ paramsValidator: workspaceSlugParamsValidator,
116
+ bodyValidator: workspaceResource.operations.patch.bodyValidator,
117
+ responseValidators: withStandardErrorResponses(
118
+ {
119
+ 200: workspaceResource.operations.patch.outputValidator
120
+ },
121
+ { includeValidation400: true }
122
+ )
123
+ },
124
+ async function (request, reply) {
125
+ const response = await request.executeAction({
126
+ actionId: "workspace.workspaces.update",
127
+ input: {
128
+ workspaceSlug: request.input.params.workspaceSlug,
129
+ patch: request.input.body
130
+ }
131
+ });
132
+ reply.code(200).send(response);
133
+ }
134
+ );
71
135
  }
72
136
 
73
137
  export { bootWorkspaceDirectoryRoutes };
@@ -3,6 +3,7 @@ import {
3
3
  resolveRequest
4
4
  } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
5
5
  import { workspaceResource } from "../../shared/resources/workspaceResource.js";
6
+ import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
6
7
  import { resolveActionUser } from "../common/support/resolveActionUser.js";
7
8
 
8
9
  const workspaceDirectoryActions = Object.freeze([
@@ -59,6 +60,73 @@ const workspaceDirectoryActions = Object.freeze([
59
60
  nextCursor: null
60
61
  };
61
62
  }
63
+ },
64
+ {
65
+ id: "workspace.workspaces.read",
66
+ version: 1,
67
+ kind: "query",
68
+ channels: ["api", "automation", "internal"],
69
+ surfacesFrom: "workspace",
70
+ permission: {
71
+ require: "any",
72
+ permissions: ["workspace.settings.view", "workspace.settings.update"]
73
+ },
74
+ inputValidator: workspaceSlugParamsValidator,
75
+ outputValidator: workspaceResource.operations.view.outputValidator,
76
+ idempotency: "none",
77
+ audit: {
78
+ actionName: "workspace.workspaces.read"
79
+ },
80
+ observability: {},
81
+ async execute(input, context, deps) {
82
+ return deps.workspaceService.getWorkspaceForAuthenticatedUser(
83
+ resolveActionUser(context, input),
84
+ input.workspaceSlug,
85
+ {
86
+ request: resolveRequest(context),
87
+ context
88
+ }
89
+ );
90
+ }
91
+ },
92
+ {
93
+ id: "workspace.workspaces.update",
94
+ version: 1,
95
+ kind: "command",
96
+ channels: ["api", "assistant_tool", "automation", "internal"],
97
+ surfacesFrom: "workspace",
98
+ permission: {
99
+ require: "all",
100
+ permissions: ["workspace.settings.update"]
101
+ },
102
+ inputValidator: [
103
+ workspaceSlugParamsValidator,
104
+ {
105
+ patch: workspaceResource.operations.patch.bodyValidator
106
+ }
107
+ ],
108
+ outputValidator: workspaceResource.operations.patch.outputValidator,
109
+ idempotency: "optional",
110
+ audit: {
111
+ actionName: "workspace.workspaces.update"
112
+ },
113
+ observability: {},
114
+ extensions: {
115
+ assistant: {
116
+ description: "Update workspace profile fields."
117
+ }
118
+ },
119
+ async execute(input, context, deps) {
120
+ return deps.workspaceService.updateWorkspaceForAuthenticatedUser(
121
+ resolveActionUser(context, input),
122
+ input.workspaceSlug,
123
+ input.patch,
124
+ {
125
+ request: resolveRequest(context),
126
+ context
127
+ }
128
+ );
129
+ }
62
130
  }
63
131
  ]);
64
132
 
@@ -5,6 +5,21 @@ import {
5
5
  createCursorListValidator
6
6
  } from "@jskit-ai/kernel/shared/validators";
7
7
 
8
+ function normalizeWorkspaceAvatarUrl(value) {
9
+ const avatarUrl = normalizeText(value);
10
+ if (!avatarUrl) {
11
+ return "";
12
+ }
13
+ if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
14
+ return null;
15
+ }
16
+ try {
17
+ return new URL(avatarUrl).toString();
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
8
23
  function normalizeWorkspaceInput(payload = {}) {
9
24
  const source = normalizeObjectInput(payload);
10
25
  const normalized = {};
@@ -19,7 +34,7 @@ function normalizeWorkspaceInput(payload = {}) {
19
34
  normalized.ownerUserId = Number(source.ownerUserId);
20
35
  }
21
36
  if (Object.hasOwn(source, "avatarUrl")) {
22
- normalized.avatarUrl = normalizeText(source.avatarUrl);
37
+ normalized.avatarUrl = normalizeWorkspaceAvatarUrl(source.avatarUrl);
23
38
  }
24
39
  if (Object.hasOwn(source, "color")) {
25
40
  const color = normalizeText(source.color);
@@ -92,6 +107,33 @@ const createRequestBodySchema = Type.Object(
92
107
  { additionalProperties: false }
93
108
  );
94
109
 
110
+ const patchRequestBodySchema = Type.Object(
111
+ {
112
+ name: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
113
+ avatarUrl: Type.Optional(
114
+ Type.String({
115
+ pattern: "^(https?://.+)?$",
116
+ messages: {
117
+ pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
118
+ default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
119
+ }
120
+ })
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
+ )
133
+ },
134
+ { additionalProperties: false }
135
+ );
136
+
95
137
  const responseRecordValidator = Object.freeze({
96
138
  schema: responseRecordSchema,
97
139
  normalize: normalizeWorkspaceOutput
@@ -138,7 +180,7 @@ const resource = {
138
180
  patch: {
139
181
  method: "PATCH",
140
182
  bodyValidator: {
141
- schema: Type.Partial(createRequestBodySchema, { additionalProperties: false }),
183
+ schema: patchRequestBodySchema,
142
184
  normalize: normalizeWorkspaceInput
143
185
  },
144
186
  outputValidator: responseRecordValidator
@@ -0,0 +1,71 @@
1
+ const WORKSPACE_SETTINGS_TABLE = "workspace_settings";
2
+ const WORKSPACES_TABLE = "workspaces";
3
+ const LEGACY_NAME_COLUMN = "name";
4
+ const LEGACY_AVATAR_COLUMN = "avatar_url";
5
+
6
+ async function hasTable(knex, tableName) {
7
+ return knex.schema.hasTable(tableName);
8
+ }
9
+
10
+ async function hasColumn(knex, tableName, columnName) {
11
+ return knex.schema.hasColumn(tableName, columnName);
12
+ }
13
+
14
+ exports.up = async function up(knex) {
15
+ const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
16
+ if (!hasWorkspaceSettings) {
17
+ return;
18
+ }
19
+
20
+ const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
21
+ const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
22
+ if (!hasLegacyName && !hasLegacyAvatarUrl) {
23
+ return;
24
+ }
25
+
26
+ await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
27
+ if (hasLegacyName) {
28
+ table.dropColumn(LEGACY_NAME_COLUMN);
29
+ }
30
+ if (hasLegacyAvatarUrl) {
31
+ table.dropColumn(LEGACY_AVATAR_COLUMN);
32
+ }
33
+ });
34
+ };
35
+
36
+ exports.down = async function down(knex) {
37
+ const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
38
+ if (!hasWorkspaceSettings) {
39
+ return;
40
+ }
41
+
42
+ const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
43
+ const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
44
+ if (!hasLegacyName || !hasLegacyAvatarUrl) {
45
+ await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
46
+ if (!hasLegacyName) {
47
+ table.string(LEGACY_NAME_COLUMN, 160).notNullable().defaultTo("Workspace");
48
+ }
49
+ if (!hasLegacyAvatarUrl) {
50
+ table.string(LEGACY_AVATAR_COLUMN, 512).notNullable().defaultTo("");
51
+ }
52
+ });
53
+ }
54
+
55
+ const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
56
+ if (!hasWorkspaces) {
57
+ return;
58
+ }
59
+
60
+ const workspaceRows = await knex(WORKSPACES_TABLE).select("id", "name", "avatar_url");
61
+ for (const workspaceRow of workspaceRows) {
62
+ const normalizedName = String(workspaceRow?.name || "").trim() || "Workspace";
63
+ const normalizedAvatarUrl = String(workspaceRow?.avatar_url || "").trim();
64
+ await knex(WORKSPACE_SETTINGS_TABLE)
65
+ .where({ workspace_id: Number(workspaceRow.id) })
66
+ .update({
67
+ name: normalizedName,
68
+ avatar_url: normalizedAvatarUrl
69
+ });
70
+ }
71
+ };
@@ -13,21 +13,6 @@ import {
13
13
  resetWorkspaceSettingsFields
14
14
  } from "@jskit-ai/users-core/shared/resources/workspaceSettingsFields";
15
15
 
16
- function normalizeAvatarUrl(value) {
17
- const avatarUrl = normalizeText(value);
18
- if (!avatarUrl) {
19
- return "";
20
- }
21
- if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
22
- return null;
23
- }
24
- try {
25
- return new URL(avatarUrl).toString();
26
- } catch {
27
- return null;
28
- }
29
- }
30
-
31
16
  function normalizeHexColor(value) {
32
17
  const color = normalizeText(value);
33
18
  return /^#[0-9A-Fa-f]{6}$/.test(color) ? color.toUpperCase() : null;
@@ -35,43 +20,6 @@ function normalizeHexColor(value) {
35
20
 
36
21
  resetWorkspaceSettingsFields();
37
22
 
38
- defineField({
39
- key: "name",
40
- dbColumn: "name",
41
- required: true,
42
- inputSchema: Type.String({
43
- minLength: 1,
44
- maxLength: 160,
45
- messages: {
46
- required: "Workspace name is required.",
47
- minLength: "Workspace name is required.",
48
- maxLength: "Workspace name must be at most 160 characters.",
49
- default: "Workspace name is required."
50
- }
51
- }),
52
- outputSchema: Type.String({ minLength: 1, maxLength: 160 }),
53
- normalizeInput: (value) => normalizeText(value),
54
- normalizeOutput: (value) => normalizeText(value),
55
- resolveDefault: ({ workspace = {} } = {}) => normalizeText(workspace.name) || "Workspace"
56
- });
57
-
58
- defineField({
59
- key: "avatarUrl",
60
- dbColumn: "avatar_url",
61
- required: false,
62
- inputSchema: Type.String({
63
- pattern: "^(https?://.+)?$",
64
- messages: {
65
- pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
66
- default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
67
- }
68
- }),
69
- outputSchema: Type.String(),
70
- normalizeInput: normalizeAvatarUrl,
71
- normalizeOutput: (value) => normalizeText(value),
72
- resolveDefault: ({ workspace = {} } = {}) => normalizeText(workspace.avatarUrl)
73
- });
74
-
75
23
  defineField({
76
24
  key: "lightPrimaryColor",
77
25
  dbColumn: "light_primary_color",
@@ -22,5 +22,10 @@ test("registerWorkspaceDirectory registers workspace directory actions without r
22
22
  const app = createAppDouble();
23
23
 
24
24
  registerWorkspaceDirectory(app);
25
- assert.deepEqual(listActionIds(app), ["workspace.workspaces.create", "workspace.workspaces.list"]);
25
+ assert.deepEqual(listActionIds(app), [
26
+ "workspace.workspaces.create",
27
+ "workspace.workspaces.list",
28
+ "workspace.workspaces.read",
29
+ "workspace.workspaces.update"
30
+ ]);
26
31
  });
@@ -126,6 +126,10 @@ test("workspace and settings routes attach only the shared transport normalizers
126
126
  method: "GET",
127
127
  path: "/api/w/:workspaceSlug/workspace/settings"
128
128
  });
129
+ const workspacePatch = findRoute(routes, {
130
+ method: "PATCH",
131
+ path: "/api/w/:workspaceSlug/workspace"
132
+ });
129
133
  const workspaceSettingsPatch = findRoute(routes, {
130
134
  method: "PATCH",
131
135
  path: "/api/w/:workspaceSlug/workspace/settings"
@@ -156,6 +160,7 @@ test("workspace and settings routes attach only the shared transport normalizers
156
160
  });
157
161
 
158
162
  assert.equal(typeof workspaceSettings?.paramsValidator?.normalize, "function");
163
+ assert.equal(typeof workspacePatch?.bodyValidator?.normalize, "function");
159
164
  assert.equal(typeof workspaceSettingsPatch?.bodyValidator?.normalize, "function");
160
165
  assert.equal(typeof workspaceMemberRole?.paramsValidator?.normalize, "function");
161
166
  assert.equal(typeof workspaceMemberRole?.bodyValidator?.normalize, "function");
@@ -167,8 +172,16 @@ test("workspace and settings routes attach only the shared transport normalizers
167
172
  assert.equal(typeof consoleSettingsPatch?.bodyValidator?.normalize, "function");
168
173
  });
169
174
 
170
- test("workspace settings routes mount one canonical workspace endpoint", async () => {
175
+ test("workspace core/settings routes mount one canonical workspace endpoint", async () => {
171
176
  const routes = await registerRoutes();
177
+ const workspace = findRoute(routes, {
178
+ method: "GET",
179
+ path: "/api/w/:workspaceSlug/workspace"
180
+ });
181
+ const workspacePatch = findRoute(routes, {
182
+ method: "PATCH",
183
+ path: "/api/w/:workspaceSlug/workspace"
184
+ });
172
185
  const workspaceSettings = findRoute(routes, {
173
186
  method: "GET",
174
187
  path: "/api/w/:workspaceSlug/workspace/settings"
@@ -186,6 +199,11 @@ test("workspace settings routes mount one canonical workspace endpoint", async (
186
199
  path: "/api/console/w/:workspaceSlug/workspace/settings"
187
200
  });
188
201
 
202
+ assert.ok(workspace);
203
+ assert.equal(workspace?.visibility, "workspace");
204
+ assert.equal(workspacePatch?.visibility, "workspace");
205
+ assert.equal(workspace?.surface, "");
206
+ assert.equal(workspacePatch?.surface, "");
189
207
  assert.ok(workspaceSettings);
190
208
  assert.equal(workspaceSettings?.visibility, "workspace");
191
209
  assert.equal(workspaceSettingsPatch?.visibility, "workspace");
@@ -205,6 +223,8 @@ test("users-core boot skips workspace routes when workspace policy is disabled",
205
223
 
206
224
  assert.equal(findRoute(routes, { method: "GET", path: "/api/workspaces" }), null);
207
225
  assert.equal(findRoute(routes, { method: "POST", path: "/api/workspaces" }), null);
226
+ assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" }), null);
227
+ assert.equal(findRoute(routes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" }), null);
208
228
  assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" }), null);
209
229
  assert.equal(findRoute(routes, { method: "GET", path: "/api/settings" })?.path, "/api/settings");
210
230
  });
@@ -242,11 +262,21 @@ test("users-core route registration follows tenancy mode matrix", async () => {
242
262
 
243
263
  assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/workspaces" }), null);
244
264
  assert.equal(findRoute(noneRoutes, { method: "POST", path: "/api/workspaces" }), null);
265
+ assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" }), null);
266
+ assert.equal(findRoute(noneRoutes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" }), null);
245
267
  assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" }), null);
246
268
  assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/workspace/invitations/pending" }), null);
247
269
 
248
270
  assert.equal(findRoute(personalRoutes, { method: "GET", path: "/api/workspaces" })?.path, "/api/workspaces");
249
271
  assert.equal(findRoute(personalRoutes, { method: "POST", path: "/api/workspaces" }), null);
272
+ assert.equal(
273
+ findRoute(personalRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" })?.path,
274
+ "/api/w/:workspaceSlug/workspace"
275
+ );
276
+ assert.equal(
277
+ findRoute(personalRoutes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" })?.path,
278
+ "/api/w/:workspaceSlug/workspace"
279
+ );
250
280
  assert.equal(
251
281
  findRoute(personalRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" })?.path,
252
282
  "/api/w/:workspaceSlug/workspace/settings"
@@ -258,6 +288,14 @@ test("users-core route registration follows tenancy mode matrix", async () => {
258
288
 
259
289
  assert.equal(findRoute(workspaceRoutes, { method: "GET", path: "/api/workspaces" })?.path, "/api/workspaces");
260
290
  assert.equal(findRoute(workspaceRoutes, { method: "POST", path: "/api/workspaces" }), null);
291
+ assert.equal(
292
+ findRoute(workspaceRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" })?.path,
293
+ "/api/w/:workspaceSlug/workspace"
294
+ );
295
+ assert.equal(
296
+ findRoute(workspaceRoutes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" })?.path,
297
+ "/api/w/:workspaceSlug/workspace"
298
+ );
261
299
  assert.equal(
262
300
  findRoute(workspaceRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" })?.path,
263
301
  "/api/w/:workspaceSlug/workspace/settings"
@@ -404,7 +442,7 @@ test("workspace settings route handlers build action input from request.input",
404
442
  createActionRequest({
405
443
  input: {
406
444
  params: { workspaceSlug: "acme" },
407
- body: { name: "Acme Workspace" }
445
+ body: { lightPrimaryColor: "#0F6B54" }
408
446
  },
409
447
  executeAction
410
448
  }),
@@ -413,7 +451,39 @@ test("workspace settings route handlers build action input from request.input",
413
451
 
414
452
  assert.deepEqual(calls[0], {
415
453
  actionId: "workspace.settings.update",
416
- input: { workspaceSlug: "acme", patch: { name: "Acme Workspace" } }
454
+ input: { workspaceSlug: "acme", patch: { lightPrimaryColor: "#0F6B54" } }
455
+ });
456
+ });
457
+
458
+ test("workspace route handlers build action input from request.input", async () => {
459
+ const routes = await registerRoutes();
460
+ const workspacePatch = findRoute(routes, {
461
+ method: "PATCH",
462
+ path: "/api/w/:workspaceSlug/workspace"
463
+ });
464
+ const calls = [];
465
+ const executeAction = async (payload) => {
466
+ calls.push(payload);
467
+ return {};
468
+ };
469
+
470
+ await workspacePatch.handler(
471
+ createActionRequest({
472
+ input: {
473
+ params: { workspaceSlug: "acme" },
474
+ body: { name: "Acme", avatarUrl: "https://example.com/acme.png", color: "#0F6B54" }
475
+ },
476
+ executeAction
477
+ }),
478
+ createReplyDouble()
479
+ );
480
+
481
+ assert.deepEqual(calls[0], {
482
+ actionId: "workspace.workspaces.update",
483
+ input: {
484
+ workspaceSlug: "acme",
485
+ patch: { name: "Acme", avatarUrl: "https://example.com/acme.png", color: "#0F6B54" }
486
+ }
417
487
  });
418
488
  });
419
489
 
@@ -39,6 +39,7 @@ function createWorkspaceServiceFixture({
39
39
  findPersonalByOwnerUserId: 0,
40
40
  listForUserId: 0,
41
41
  insert: 0,
42
+ updateById: 0,
42
43
  ensureOwnerMembership: 0
43
44
  };
44
45
  let nextWorkspaceId = 10;
@@ -125,6 +126,32 @@ function createWorkspaceServiceFixture({
125
126
  };
126
127
  workspaceBySlug.set(String(inserted.slug).trim().toLowerCase(), inserted);
127
128
  return inserted;
129
+ },
130
+ async updateById(workspaceId, patch) {
131
+ calls.updateById += 1;
132
+ const targetId = Number(workspaceId);
133
+ for (const [slug, workspace] of workspaceBySlug.entries()) {
134
+ if (Number(workspace.id) !== targetId) {
135
+ continue;
136
+ }
137
+ const updated = {
138
+ ...workspace
139
+ };
140
+ if (Object.hasOwn(patch, "name")) {
141
+ updated.name = String(patch.name || "");
142
+ }
143
+ if (Object.hasOwn(patch, "avatarUrl")) {
144
+ updated.avatarUrl = String(patch.avatarUrl || "");
145
+ }
146
+ if (Object.hasOwn(patch, "color")) {
147
+ updated.color = String(patch.color || "#0F6B54");
148
+ }
149
+ workspaceBySlug.set(slug, updated);
150
+ return {
151
+ ...updated
152
+ };
153
+ }
154
+ return null;
128
155
  }
129
156
  },
130
157
  workspaceMembershipsRepository: {
@@ -478,3 +505,54 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions
478
505
 
479
506
  assert.deepEqual(context.permissions, ["workspace.settings.update"]);
480
507
  });
508
+
509
+ test("workspaceService.getWorkspaceForAuthenticatedUser resolves workspace from slug context", async () => {
510
+ const { service } = createWorkspaceServiceFixture({
511
+ additionalWorkspaces: [
512
+ {
513
+ id: 42,
514
+ slug: "team-alpha",
515
+ name: "Team Alpha",
516
+ ownerUserId: 99,
517
+ isPersonal: false,
518
+ avatarUrl: "",
519
+ color: "#0F6B54"
520
+ }
521
+ ]
522
+ });
523
+
524
+ const workspace = await service.getWorkspaceForAuthenticatedUser(
525
+ {
526
+ id: 7,
527
+ email: "chiaramobily@gmail.com",
528
+ displayName: "Chiara"
529
+ },
530
+ "team-alpha"
531
+ );
532
+
533
+ assert.equal(workspace.slug, "team-alpha");
534
+ assert.equal(workspace.name, "Team Alpha");
535
+ });
536
+
537
+ test("workspaceService.updateWorkspaceForAuthenticatedUser updates workspace profile fields", async () => {
538
+ const { service, calls } = createWorkspaceServiceFixture();
539
+
540
+ const workspace = await service.updateWorkspaceForAuthenticatedUser(
541
+ {
542
+ id: 7,
543
+ email: "chiaramobily@gmail.com",
544
+ displayName: "Chiara"
545
+ },
546
+ "tonymobily3",
547
+ {
548
+ name: "Updated Workspace",
549
+ avatarUrl: "https://example.com/acme.png",
550
+ color: "#123ABC"
551
+ }
552
+ );
553
+
554
+ assert.equal(calls.updateById, 1);
555
+ assert.equal(workspace.name, "Updated Workspace");
556
+ assert.equal(workspace.avatarUrl, "https://example.com/acme.png");
557
+ assert.equal(workspace.color, "#123ABC");
558
+ });
@@ -40,3 +40,13 @@ test("workspace directory actions use the canonical workspace list resource outp
40
40
  assert.ok(listAction);
41
41
  assert.equal(listAction.outputValidator, workspaceResource.operations.list.outputValidator);
42
42
  });
43
+
44
+ test("workspace directory read/update actions use canonical workspace resource validators", () => {
45
+ const readAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.read");
46
+ const updateAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.update");
47
+
48
+ assert.ok(readAction);
49
+ assert.ok(updateAction);
50
+ assert.equal(readAction.outputValidator, workspaceResource.operations.view.outputValidator);
51
+ assert.equal(updateAction.outputValidator, workspaceResource.operations.patch.outputValidator);
52
+ });
@@ -16,8 +16,6 @@ function createKnexStub(rowOverrides = {}) {
16
16
  updatePayload: null,
17
17
  row: {
18
18
  workspace_id: 1,
19
- name: "Workspace",
20
- avatar_url: "",
21
19
  light_primary_color: DEFAULT_WORKSPACE_THEME.light.color,
22
20
  light_secondary_color: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
23
21
  light_surface_color: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
@@ -41,8 +39,6 @@ function createKnexStub(rowOverrides = {}) {
41
39
  state.insertedRow = { ...payload };
42
40
  state.row = {
43
41
  workspace_id: payload.workspace_id,
44
- name: payload.name,
45
- avatar_url: payload.avatar_url,
46
42
  light_primary_color: payload.light_primary_color,
47
43
  light_secondary_color: payload.light_secondary_color,
48
44
  light_surface_color: payload.light_surface_color,
@@ -69,12 +65,6 @@ function createKnexStub(rowOverrides = {}) {
69
65
  if (Object.hasOwn(payload, "invites_enabled")) {
70
66
  state.row.invites_enabled = payload.invites_enabled;
71
67
  }
72
- if (Object.hasOwn(payload, "name")) {
73
- state.row.name = payload.name;
74
- }
75
- if (Object.hasOwn(payload, "avatar_url")) {
76
- state.row.avatar_url = payload.avatar_url;
77
- }
78
68
  if (Object.hasOwn(payload, "light_primary_color")) {
79
69
  state.row.light_primary_color = payload.light_primary_color;
80
70
  }
@@ -122,8 +112,6 @@ test("workspaceSettingsRepository.findByWorkspaceId maps the stored row", async
122
112
 
123
113
  assert.deepEqual(record, {
124
114
  workspaceId: 1,
125
- name: "Workspace",
126
- avatarUrl: "",
127
115
  lightPrimaryColor: DEFAULT_WORKSPACE_THEME.light.color,
128
116
  lightSecondaryColor: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
129
117
  lightSurfaceColor: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
@@ -162,8 +150,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
162
150
  const record = await repository.ensureForWorkspaceId(5);
163
151
 
164
152
  assert.equal(state.insertedRow.workspace_id, 5);
165
- assert.equal(state.insertedRow.name, "Workspace");
166
- assert.equal(state.insertedRow.avatar_url, "");
167
153
  assert.equal(state.insertedRow.light_primary_color, DEFAULT_WORKSPACE_THEME.light.color);
168
154
  assert.equal(state.insertedRow.light_secondary_color, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
169
155
  assert.equal(state.insertedRow.light_surface_color, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
@@ -179,8 +165,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
179
165
  DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor
180
166
  );
181
167
  assert.equal(state.insertedRow.invites_enabled, false);
182
- assert.equal(record.name, "Workspace");
183
- assert.equal(record.avatarUrl, "");
184
168
  assert.equal(record.lightPrimaryColor, DEFAULT_WORKSPACE_THEME.light.color);
185
169
  assert.equal(record.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
186
170
  assert.equal(record.lightSurfaceColor, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
@@ -192,23 +176,17 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
192
176
  assert.equal(record.invitesEnabled, false);
193
177
  });
194
178
 
195
- test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates name/avatar/color columns", async () => {
179
+ test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings columns", async () => {
196
180
  const { knexStub, state } = createKnexStub();
197
181
  const repository = createRepository(knexStub, {
198
182
  defaultInvitesEnabled: true
199
183
  });
200
184
 
201
185
  const updated = await repository.updateSettingsByWorkspaceId(1, {
202
- name: "New name",
203
- avatarUrl: "https://example.com/avatar.png",
204
186
  lightPrimaryColor: "#123abc"
205
187
  });
206
188
 
207
- assert.equal(state.updatePayload.name, "New name");
208
- assert.equal(state.updatePayload.avatar_url, "https://example.com/avatar.png");
209
189
  assert.equal(state.updatePayload.light_primary_color, "#123ABC");
210
- assert.equal(updated.name, "New name");
211
- assert.equal(updated.avatarUrl, "https://example.com/avatar.png");
212
190
  assert.equal(updated.lightPrimaryColor, "#123ABC");
213
191
  });
214
192
 
@@ -46,8 +46,6 @@ function parseBody(operation, payload = {}) {
46
46
 
47
47
  test("workspace settings patch body normalizes valid payload before validation", () => {
48
48
  const parsed = parseBody(workspaceSettingsResource.operations.patch, {
49
- name: " Team Mercury ",
50
- avatarUrl: "https://example.com/avatar.png",
51
49
  lightPrimaryColor: "#0f6b54",
52
50
  lightSecondaryColor: "#0b4d3c",
53
51
  lightSurfaceColor: "#eef5f3",
@@ -62,8 +60,6 @@ test("workspace settings patch body normalizes valid payload before validation",
62
60
  assert.equal(parsed.ok, true);
63
61
  assert.deepEqual(parsed.fieldErrors, {});
64
62
  assert.deepEqual(parsed.value, {
65
- name: "Team Mercury",
66
- avatarUrl: "https://example.com/avatar.png",
67
63
  lightPrimaryColor: "#0F6B54",
68
64
  lightSecondaryColor: "#0B4D3C",
69
65
  lightSurfaceColor: "#EEF5F3",
@@ -76,31 +72,18 @@ test("workspace settings patch body normalizes valid payload before validation",
76
72
  });
77
73
  });
78
74
 
79
- test("workspace settings patch body validates avatar URL protocol", () => {
75
+ test("workspace settings patch body ignores unknown fields after normalization", () => {
80
76
  const parsed = parseBody(workspaceSettingsResource.operations.patch, {
81
- avatarUrl: "ftp://example.com/avatar.png"
77
+ avatarUrl: "https://example.com/avatar.png"
82
78
  });
83
79
 
84
- assert.equal(parsed.ok, false);
85
- assert.equal(
86
- parsed.fieldErrors.avatarUrl,
87
- "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
88
- );
89
- });
90
-
91
- test("workspace settings patch body keeps max-length name rule", () => {
92
- const parsed = parseBody(workspaceSettingsResource.operations.patch, {
93
- name: "x".repeat(161)
94
- });
95
-
96
- assert.equal(parsed.ok, false);
97
- assert.equal(parsed.fieldErrors.name, "Workspace name must be at most 160 characters.");
80
+ assert.equal(parsed.ok, true);
81
+ assert.deepEqual(parsed.fieldErrors, {});
82
+ assert.deepEqual(parsed.value, {});
98
83
  });
99
84
 
100
85
  test("workspace settings create body requires full-write fields", () => {
101
- const parsed = parseBody(workspaceSettingsResource.operations.create, {
102
- name: "Mercury Workspace"
103
- });
86
+ const parsed = parseBody(workspaceSettingsResource.operations.create, {});
104
87
 
105
88
  assert.equal(parsed.ok, false);
106
89
  assert.equal(parsed.fieldErrors.lightPrimaryColor, "Light primary color is required.");
@@ -125,8 +108,6 @@ test("workspace settings output normalizes raw service payloads", () => {
125
108
  ownerUserId: "9"
126
109
  },
127
110
  settings: {
128
- name: " Mercury Workspace ",
129
- avatarUrl: " https://example.com/avatar.png ",
130
111
  lightPrimaryColor: "#0f6b54",
131
112
  invitesEnabled: false
132
113
  },
@@ -140,8 +121,6 @@ test("workspace settings output normalizes raw service payloads", () => {
140
121
  ownerUserId: 9
141
122
  },
142
123
  settings: {
143
- name: "Mercury Workspace",
144
- avatarUrl: "https://example.com/avatar.png",
145
124
  lightPrimaryColor: "#0F6B54",
146
125
  lightSecondaryColor: expectedTheme.light.secondaryColor,
147
126
  lightSurfaceColor: expectedTheme.light.surfaceColor,
@@ -26,12 +26,9 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
26
26
  slug: "tonymobily3",
27
27
  name: "TonyMobily3",
28
28
  ownerUserId: 9,
29
- avatarUrl: "",
30
29
  color: defaultTheme.light.color
31
30
  },
32
31
  settings: {
33
- name: "TonyMobily3",
34
- avatarUrl: "",
35
32
  lightPrimaryColor: defaultTheme.light.color,
36
33
  lightSecondaryColor: defaultTheme.light.secondaryColor,
37
34
  lightSurfaceColor: defaultTheme.light.surfaceColor,
@@ -75,8 +72,6 @@ test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEn
75
72
  );
76
73
 
77
74
  assert.deepEqual(response.settings, {
78
- name: "TonyMobily3",
79
- avatarUrl: "",
80
75
  lightPrimaryColor: "#0F6B54",
81
76
  lightSecondaryColor: "#48A9A6",
82
77
  lightSurfaceColor: "#FFFFFF",
@@ -97,19 +92,15 @@ test("workspaceSettingsService.updateWorkspaceSettings writes editable fields th
97
92
  const response = await service.updateWorkspaceSettings(
98
93
  state.workspace,
99
94
  {
100
- name: "New Name",
101
95
  invitesEnabled: false
102
96
  },
103
97
  authorizedOptions(["workspace.settings.update"])
104
98
  );
105
99
 
106
100
  assert.deepEqual(state.settingsPatch, {
107
- name: "New Name",
108
101
  invitesEnabled: false
109
102
  });
110
103
  assert.deepEqual(response.settings, {
111
- name: "New Name",
112
- avatarUrl: "",
113
104
  lightPrimaryColor: "#0F6B54",
114
105
  lightSecondaryColor: "#48A9A6",
115
106
  lightSurfaceColor: "#FFFFFF",
@@ -135,8 +126,6 @@ test("workspaceSettingsService disables invite settings in output when app polic
135
126
  );
136
127
 
137
128
  assert.deepEqual(response.settings, {
138
- name: "TonyMobily3",
139
- avatarUrl: "",
140
129
  lightPrimaryColor: "#0F6B54",
141
130
  lightSecondaryColor: "#48A9A6",
142
131
  lightSurfaceColor: "#FFFFFF",