@jskit-ai/users-core 0.1.19 → 0.1.21

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.19",
4
+ version: "0.1.21",
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.14",
207
- "@jskit-ai/database-runtime": "0.1.15",
208
- "@jskit-ai/http-runtime": "0.1.14",
209
- "@jskit-ai/kernel": "0.1.14",
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",
210
210
  "@fastify/multipart": "^9.4.0",
211
211
  "@fastify/type-provider-typebox": "^6.1.0",
212
212
  "typebox": "^1.0.81"
@@ -397,7 +397,7 @@ export default Object.freeze({
397
397
  id: "users-core-surface-config-workspace",
398
398
  when: {
399
399
  config: "tenancyMode",
400
- in: ["personal", "workspace"]
400
+ in: ["personal", "workspaces"]
401
401
  }
402
402
  },
403
403
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-core",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -24,10 +24,10 @@
24
24
  "./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
25
25
  },
26
26
  "dependencies": {
27
- "@jskit-ai/auth-core": "0.1.14",
28
- "@jskit-ai/database-runtime": "0.1.15",
29
- "@jskit-ai/http-runtime": "0.1.14",
30
- "@jskit-ai/kernel": "0.1.14",
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",
31
31
  "@fastify/multipart": "^9.4.0",
32
32
  "@fastify/type-provider-typebox": "^6.1.0",
33
33
  "typebox": "^1.0.81"
@@ -93,7 +93,7 @@ function createDuplicateEmailConflictError() {
93
93
  async function resolveUniqueUsername(client, baseUsername, { excludeUserId = 0 } = {}) {
94
94
  for (let suffix = 0; suffix < 1000; suffix += 1) {
95
95
  const candidate = buildUsernameCandidate(baseUsername, suffix);
96
- const existing = await client("user_profiles").where({ username: candidate }).first();
96
+ const existing = await client("users").where({ username: candidate }).first();
97
97
  if (!existing || Number(existing.id) === Number(excludeUserId || 0)) {
98
98
  return candidate;
99
99
  }
@@ -109,7 +109,7 @@ function createRepository(knex) {
109
109
 
110
110
  async function findById(userId, options = {}) {
111
111
  const client = options?.trx || knex;
112
- const row = await client("user_profiles").where({ id: userId }).first();
112
+ const row = await client("users").where({ id: userId }).first();
113
113
  return mapProfileRow(row);
114
114
  }
115
115
 
@@ -120,7 +120,7 @@ function createRepository(knex) {
120
120
  return null;
121
121
  }
122
122
 
123
- const row = await client("user_profiles")
123
+ const row = await client("users")
124
124
  .where({
125
125
  auth_provider: identity.provider,
126
126
  auth_provider_user_id: identity.providerUserId
@@ -131,7 +131,7 @@ function createRepository(knex) {
131
131
 
132
132
  async function updateDisplayNameById(userId, displayName, options = {}) {
133
133
  const client = options?.trx || knex;
134
- await client("user_profiles")
134
+ await client("users")
135
135
  .where({ id: userId })
136
136
  .update({
137
137
  display_name: normalizeText(displayName)
@@ -141,7 +141,7 @@ function createRepository(knex) {
141
141
 
142
142
  async function updateAvatarById(userId, avatar = {}, options = {}) {
143
143
  const client = options?.trx || knex;
144
- await client("user_profiles")
144
+ await client("users")
145
145
  .where({ id: userId })
146
146
  .update({
147
147
  avatar_storage_key: avatar.avatarStorageKey || null,
@@ -154,7 +154,7 @@ function createRepository(knex) {
154
154
 
155
155
  async function clearAvatarById(userId, options = {}) {
156
156
  const client = options?.trx || knex;
157
- await client("user_profiles")
157
+ await client("users")
158
158
  .where({ id: userId })
159
159
  .update({
160
160
  avatar_storage_key: null,
@@ -183,7 +183,7 @@ function createRepository(knex) {
183
183
  auth_provider: identity.provider,
184
184
  auth_provider_user_id: identity.providerUserId
185
185
  };
186
- const existing = await trx("user_profiles").where(where).first();
186
+ const existing = await trx("users").where(where).first();
187
187
 
188
188
  try {
189
189
  if (existing) {
@@ -191,14 +191,14 @@ function createRepository(knex) {
191
191
  const username = existingUsername || (await resolveUniqueUsername(trx, requestedUsername || usernameBaseFromEmail(email), {
192
192
  excludeUserId: existing.id
193
193
  }));
194
- await trx("user_profiles").where({ id: existing.id }).update({
194
+ await trx("users").where({ id: existing.id }).update({
195
195
  email,
196
196
  display_name: displayName,
197
197
  username
198
198
  });
199
199
  } else {
200
200
  const username = await resolveUniqueUsername(trx, requestedUsername || usernameBaseFromEmail(email));
201
- await trx("user_profiles").insert({
201
+ await trx("users").insert({
202
202
  auth_provider: identity.provider,
203
203
  auth_provider_user_id: identity.providerUserId,
204
204
  email,
@@ -218,7 +218,7 @@ function createRepository(knex) {
218
218
  }
219
219
  }
220
220
 
221
- const reloaded = await trx("user_profiles").where(where).first();
221
+ const reloaded = await trx("users").where(where).first();
222
222
  return mapProfileRow(reloaded);
223
223
  };
224
224
 
@@ -116,7 +116,7 @@ function createRepository(knex) {
116
116
  async function listActiveByWorkspaceId(workspaceId, options = {}) {
117
117
  const client = options?.trx || knex;
118
118
  const rows = await client("workspace_memberships as wm")
119
- .join("user_profiles as up", "up.id", "wm.user_id")
119
+ .join("users as up", "up.id", "wm.user_id")
120
120
  .where({ "wm.workspace_id": Number(workspaceId), "wm.status": "active" })
121
121
  .orderBy("up.display_name", "asc")
122
122
  .select([
@@ -4,7 +4,7 @@ import {
4
4
  } from "@jskit-ai/kernel/server/actions";
5
5
  import { registerRouteVisibilityResolver } from "@jskit-ai/kernel/server/http";
6
6
  import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
7
- import { TENANCY_MODE_WORKSPACE, resolveTenancyProfile } from "../shared/tenancyProfile.js";
7
+ import { TENANCY_MODE_WORKSPACES, resolveTenancyProfile } from "../shared/tenancyProfile.js";
8
8
  import { createService as createWorkspaceService } from "./common/services/workspaceContextService.js";
9
9
  import { createService as createAuthProfileSyncService } from "./common/services/authProfileSyncService.js";
10
10
  import { createWorkspaceActionContextContributor } from "./common/contributors/workspaceActionContextContributor.js";
@@ -59,7 +59,7 @@ function registerWorkspaceCore(app) {
59
59
  });
60
60
 
61
61
  app.singleton(USERS_WORKSPACE_TENANCY_ENABLED_TOKEN, (scope) => {
62
- return scope.make(USERS_TENANCY_PROFILE_TOKEN).mode === TENANCY_MODE_WORKSPACE;
62
+ return scope.make(USERS_TENANCY_PROFILE_TOKEN).mode === TENANCY_MODE_WORKSPACES;
63
63
  });
64
64
 
65
65
  app.singleton(USERS_WORKSPACE_INVITATIONS_ENABLED_TOKEN, (scope) => {
@@ -4,7 +4,7 @@ import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actio
4
4
  import {
5
5
  TENANCY_MODE_NONE,
6
6
  TENANCY_MODE_PERSONAL,
7
- TENANCY_MODE_WORKSPACE,
7
+ TENANCY_MODE_WORKSPACES,
8
8
  WORKSPACE_SLUG_POLICY_NONE,
9
9
  WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
10
10
  WORKSPACE_SLUG_POLICY_USER_SELECTED,
@@ -164,7 +164,7 @@ function normalizeSlugPolicy(value = "") {
164
164
  }
165
165
 
166
166
  function isSupportedTenancyMode(value = "") {
167
- return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value === TENANCY_MODE_WORKSPACE;
167
+ return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value === TENANCY_MODE_WORKSPACES;
168
168
  }
169
169
 
170
170
  function resolveBootstrapTenancyProfile(tenancyProfile = null, appConfig = {}) {
@@ -35,7 +35,7 @@ const INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE = Object.freeze({
35
35
  }
36
36
 
37
37
  const row = await knex("workspace_invites as wi")
38
- .join("user_profiles as up", "up.email", "wi.email")
38
+ .join("users as up", "up.email", "wi.email")
39
39
  .where("wi.id", inviteId)
40
40
  .first("up.id as user_id");
41
41
 
@@ -28,13 +28,13 @@ import {
28
28
  import {
29
29
  TENANCY_MODE_NONE,
30
30
  TENANCY_MODE_PERSONAL,
31
- TENANCY_MODE_WORKSPACE,
31
+ TENANCY_MODE_WORKSPACES,
32
32
  normalizeTenancyMode,
33
33
  WORKSPACE_SLUG_POLICY_NONE,
34
34
  WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
35
35
  WORKSPACE_SLUG_POLICY_USER_SELECTED,
36
36
  resolveTenancyProfile,
37
- isWorkspaceTenancyMode
37
+ isWorkspacesTenancyMode
38
38
  } from "./tenancyProfile.js";
39
39
  import {
40
40
  ACCOUNT_SETTINGS_CHANGED_EVENT,
@@ -71,13 +71,13 @@ const USERS_SHARED_API = Object.freeze({
71
71
  resolveWorkspaceThemePalette,
72
72
  TENANCY_MODE_NONE,
73
73
  TENANCY_MODE_PERSONAL,
74
- TENANCY_MODE_WORKSPACE,
74
+ TENANCY_MODE_WORKSPACES,
75
75
  normalizeTenancyMode,
76
76
  WORKSPACE_SLUG_POLICY_NONE,
77
77
  WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
78
78
  WORKSPACE_SLUG_POLICY_USER_SELECTED,
79
79
  resolveTenancyProfile,
80
- isWorkspaceTenancyMode,
80
+ isWorkspacesTenancyMode,
81
81
  ACCOUNT_SETTINGS_CHANGED_EVENT,
82
82
  CONSOLE_SETTINGS_CHANGED_EVENT,
83
83
  WORKSPACE_SETTINGS_CHANGED_EVENT,
@@ -112,13 +112,13 @@ export {
112
112
  resolveWorkspaceThemePalette,
113
113
  TENANCY_MODE_NONE,
114
114
  TENANCY_MODE_PERSONAL,
115
- TENANCY_MODE_WORKSPACE,
115
+ TENANCY_MODE_WORKSPACES,
116
116
  normalizeTenancyMode,
117
117
  WORKSPACE_SLUG_POLICY_NONE,
118
118
  WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
119
119
  WORKSPACE_SLUG_POLICY_USER_SELECTED,
120
120
  resolveTenancyProfile,
121
- isWorkspaceTenancyMode,
121
+ isWorkspacesTenancyMode,
122
122
  ACCOUNT_SETTINGS_CHANGED_EVENT,
123
123
  CONSOLE_SETTINGS_CHANGED_EVENT,
124
124
  WORKSPACE_SETTINGS_CHANGED_EVENT,
@@ -1,11 +1,16 @@
1
1
  import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
2
+ import { splitPathQueryAndHash } from "@jskit-ai/kernel/shared/support";
2
3
 
3
4
  const USERS_PUBLIC_API_BASE_PATH = "/api";
4
5
  const USERS_WORKSPACE_API_BASE_PATH = "/api/w/:workspaceSlug/workspace";
5
6
 
6
7
  function normalizeApiRelativePath(relativePath = "/") {
7
- const normalizedPath = normalizePathname(relativePath);
8
- return normalizedPath || "/";
8
+ const { pathname, queryString, hash } = splitPathQueryAndHash(relativePath);
9
+ const normalizedPath = normalizePathname(pathname || "/") || "/";
10
+ const normalizedQueryString = String(queryString || "").trim().replace(/^\?+/, "");
11
+ const normalizedHash = String(hash || "").trim();
12
+ const querySuffix = normalizedQueryString ? `?${normalizedQueryString}` : "";
13
+ return `${normalizedPath}${querySuffix}${normalizedHash}`;
9
14
  }
10
15
 
11
16
  function normalizeSurfaceWorkspaceRequirement(value = false) {
@@ -22,6 +27,10 @@ function resolveApiBasePath({ surfaceRequiresWorkspace = false, relativePath = "
22
27
  return basePath;
23
28
  }
24
29
 
30
+ if (normalizedRelativePath.startsWith("/?") || normalizedRelativePath.startsWith("/#")) {
31
+ return `${basePath}${normalizedRelativePath.slice(1)}`;
32
+ }
33
+
25
34
  return `${basePath}${normalizedRelativePath}`;
26
35
  }
27
36
 
@@ -1,11 +1,11 @@
1
1
  const TENANCY_MODE_NONE = "none";
2
2
  const TENANCY_MODE_PERSONAL = "personal";
3
- const TENANCY_MODE_WORKSPACE = "workspace";
3
+ const TENANCY_MODE_WORKSPACES = "workspaces";
4
4
 
5
5
  const TENANCY_MODES = Object.freeze([
6
6
  TENANCY_MODE_NONE,
7
7
  TENANCY_MODE_PERSONAL,
8
- TENANCY_MODE_WORKSPACE
8
+ TENANCY_MODE_WORKSPACES
9
9
  ]);
10
10
 
11
11
  function normalizeTenancyMode(value = "") {
@@ -28,7 +28,7 @@ function isTenancyMode(value = "") {
28
28
  export {
29
29
  TENANCY_MODE_NONE,
30
30
  TENANCY_MODE_PERSONAL,
31
- TENANCY_MODE_WORKSPACE,
31
+ TENANCY_MODE_WORKSPACES,
32
32
  TENANCY_MODES,
33
33
  normalizeTenancyMode,
34
34
  isTenancyMode
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  TENANCY_MODE_NONE,
3
3
  TENANCY_MODE_PERSONAL,
4
- TENANCY_MODE_WORKSPACE,
4
+ TENANCY_MODE_WORKSPACES,
5
5
  normalizeTenancyMode
6
6
  } from "./tenancyMode.js";
7
7
  import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
@@ -56,18 +56,18 @@ function resolveTenancyProfile(appConfig = {}) {
56
56
  });
57
57
  }
58
58
 
59
- function isWorkspaceTenancyMode(value = "") {
60
- return normalizeTenancyMode(value) === TENANCY_MODE_WORKSPACE;
59
+ function isWorkspacesTenancyMode(value = "") {
60
+ return normalizeTenancyMode(value) === TENANCY_MODE_WORKSPACES;
61
61
  }
62
62
 
63
63
  export {
64
64
  TENANCY_MODE_NONE,
65
65
  TENANCY_MODE_PERSONAL,
66
- TENANCY_MODE_WORKSPACE,
66
+ TENANCY_MODE_WORKSPACES,
67
67
  normalizeTenancyMode,
68
68
  WORKSPACE_SLUG_POLICY_NONE,
69
69
  WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
70
70
  WORKSPACE_SLUG_POLICY_USER_SELECTED,
71
71
  resolveTenancyProfile,
72
- isWorkspaceTenancyMode
72
+ isWorkspacesTenancyMode
73
73
  };
@@ -2,7 +2,7 @@
2
2
  * @param {import('knex').Knex} knex
3
3
  */
4
4
  exports.up = async function up(knex) {
5
- await knex.schema.createTable("user_profiles", (table) => {
5
+ await knex.schema.createTable("users", (table) => {
6
6
  table.increments("id").primary();
7
7
  table.string("auth_provider", 64).notNullable();
8
8
  table.string("auth_provider_user_id", 191).notNullable();
@@ -13,16 +13,16 @@ exports.up = async function up(knex) {
13
13
  table.string("avatar_version", 64).nullable();
14
14
  table.timestamp("avatar_updated_at", { useTz: false }).nullable();
15
15
  table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
16
- table.unique(["auth_provider", "auth_provider_user_id"], "uq_user_profiles_identity");
17
- table.unique(["email"], "uq_user_profiles_email");
18
- table.unique(["username"], "uq_user_profiles_username");
16
+ table.unique(["auth_provider", "auth_provider_user_id"], "uq_users_identity");
17
+ table.unique(["email"], "uq_users_email");
18
+ table.unique(["username"], "uq_users_username");
19
19
  });
20
20
 
21
21
  await knex.schema.createTable("workspaces", (table) => {
22
22
  table.increments("id").primary();
23
23
  table.string("slug", 120).notNullable().unique();
24
24
  table.string("name", 160).notNullable();
25
- table.integer("owner_user_id").unsigned().notNullable().references("id").inTable("user_profiles").onDelete("CASCADE");
25
+ table.integer("owner_user_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
26
26
  table.boolean("is_personal").notNullable().defaultTo(true);
27
27
  table.string("avatar_url", 512).notNullable().defaultTo("");
28
28
  table.string("color", 7).notNullable().defaultTo("#1867C0");
@@ -34,7 +34,7 @@ exports.up = async function up(knex) {
34
34
  await knex.schema.createTable("workspace_memberships", (table) => {
35
35
  table.increments("id").primary();
36
36
  table.integer("workspace_id").unsigned().notNullable().references("id").inTable("workspaces").onDelete("CASCADE");
37
- table.integer("user_id").unsigned().notNullable().references("id").inTable("user_profiles").onDelete("CASCADE");
37
+ table.integer("user_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
38
38
  table.string("role_id", 64).notNullable().defaultTo("member");
39
39
  table.string("status", 32).notNullable().defaultTo("active");
40
40
  table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
@@ -66,7 +66,7 @@ exports.up = async function up(knex) {
66
66
  table.string("role_id", 64).notNullable().defaultTo("member");
67
67
  table.string("status", 32).notNullable().defaultTo("pending");
68
68
  table.string("token_hash", 191).notNullable();
69
- table.integer("invited_by_user_id").unsigned().nullable().references("id").inTable("user_profiles").onDelete("SET NULL");
69
+ table.integer("invited_by_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
70
70
  table.timestamp("expires_at", { useTz: false }).nullable();
71
71
  table.timestamp("accepted_at", { useTz: false }).nullable();
72
72
  table.timestamp("revoked_at", { useTz: false }).nullable();
@@ -77,7 +77,7 @@ exports.up = async function up(knex) {
77
77
  });
78
78
 
79
79
  await knex.schema.createTable("user_settings", (table) => {
80
- table.integer("user_id").unsigned().primary().references("id").inTable("user_profiles").onDelete("CASCADE");
80
+ table.integer("user_id").unsigned().primary().references("id").inTable("users").onDelete("CASCADE");
81
81
  table.integer("last_active_workspace_id").unsigned().nullable().references("id").inTable("workspaces").onDelete("SET NULL");
82
82
  table.string("theme", 32).notNullable().defaultTo("system");
83
83
  table.string("locale", 24).notNullable().defaultTo("en");
@@ -119,5 +119,5 @@ exports.down = async function down(knex) {
119
119
  await knex.schema.dropTableIfExists("workspace_settings");
120
120
  await knex.schema.dropTableIfExists("workspace_memberships");
121
121
  await knex.schema.dropTableIfExists("workspaces");
122
- await knex.schema.dropTableIfExists("user_profiles");
122
+ await knex.schema.dropTableIfExists("users");
123
123
  };
@@ -42,37 +42,37 @@ function resolveUniqueUsername(baseUsername, usedUsernames) {
42
42
  * @param {import('knex').Knex} knex
43
43
  */
44
44
  exports.up = async function up(knex) {
45
- const hasUserProfilesTable = await knex.schema.hasTable("user_profiles");
46
- if (!hasUserProfilesTable) {
45
+ const hasUsersTable = await knex.schema.hasTable("users");
46
+ if (!hasUsersTable) {
47
47
  return;
48
48
  }
49
49
 
50
- const hasUsername = await knex.schema.hasColumn("user_profiles", "username");
50
+ const hasUsername = await knex.schema.hasColumn("users", "username");
51
51
  if (hasUsername) {
52
52
  return;
53
53
  }
54
54
 
55
- await knex.schema.alterTable("user_profiles", (table) => {
55
+ await knex.schema.alterTable("users", (table) => {
56
56
  table.string("username", USERNAME_MAX_LENGTH).nullable();
57
57
  });
58
58
 
59
- const profiles = await knex("user_profiles").select(["id", "email"]).orderBy("id", "asc");
59
+ const profiles = await knex("users").select(["id", "email"]).orderBy("id", "asc");
60
60
  const usedUsernames = new Set();
61
61
 
62
62
  for (const profile of profiles) {
63
63
  const nextUsername = resolveUniqueUsername(usernameBaseFromEmail(profile.email), usedUsernames);
64
64
  usedUsernames.add(nextUsername);
65
- await knex("user_profiles").where({ id: Number(profile.id) }).update({
65
+ await knex("users").where({ id: Number(profile.id) }).update({
66
66
  username: nextUsername
67
67
  });
68
68
  }
69
69
 
70
- await knex.schema.alterTable("user_profiles", (table) => {
70
+ await knex.schema.alterTable("users", (table) => {
71
71
  table.string("username", USERNAME_MAX_LENGTH).notNullable().alter();
72
72
  });
73
73
 
74
- await knex.schema.alterTable("user_profiles", (table) => {
75
- table.unique(["username"], "uq_user_profiles_username");
74
+ await knex.schema.alterTable("users", (table) => {
75
+ table.unique(["username"], "uq_users_username");
76
76
  });
77
77
  };
78
78
 
@@ -80,17 +80,17 @@ exports.up = async function up(knex) {
80
80
  * @param {import('knex').Knex} knex
81
81
  */
82
82
  exports.down = async function down(knex) {
83
- const hasUserProfilesTable = await knex.schema.hasTable("user_profiles");
84
- if (!hasUserProfilesTable) {
83
+ const hasUsersTable = await knex.schema.hasTable("users");
84
+ if (!hasUsersTable) {
85
85
  return;
86
86
  }
87
87
 
88
- const hasUsername = await knex.schema.hasColumn("user_profiles", "username");
88
+ const hasUsername = await knex.schema.hasColumn("users", "username");
89
89
  if (!hasUsername) {
90
90
  return;
91
91
  }
92
92
 
93
- await knex.schema.alterTable("user_profiles", (table) => {
93
+ await knex.schema.alterTable("users", (table) => {
94
94
  table.dropColumn("username");
95
95
  });
96
96
  };
@@ -3,12 +3,12 @@ import test from "node:test";
3
3
  import {
4
4
  TENANCY_MODE_NONE,
5
5
  TENANCY_MODE_PERSONAL,
6
- TENANCY_MODE_WORKSPACE,
6
+ TENANCY_MODE_WORKSPACES,
7
7
  WORKSPACE_SLUG_POLICY_NONE,
8
8
  WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
9
9
  WORKSPACE_SLUG_POLICY_USER_SELECTED,
10
10
  resolveTenancyProfile,
11
- isWorkspaceTenancyMode
11
+ isWorkspacesTenancyMode
12
12
  } from "../src/shared/tenancyProfile.js";
13
13
 
14
14
  test("resolveTenancyProfile returns mode-specific workspace policy matrix", () => {
@@ -34,9 +34,9 @@ test("resolveTenancyProfile returns mode-specific workspace policy matrix", () =
34
34
  }
35
35
  });
36
36
 
37
- const workspaceProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACE });
37
+ const workspaceProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACES });
38
38
  assert.deepEqual(workspaceProfile, {
39
- mode: TENANCY_MODE_WORKSPACE,
39
+ mode: TENANCY_MODE_WORKSPACES,
40
40
  workspace: {
41
41
  enabled: true,
42
42
  autoProvision: false,
@@ -46,15 +46,15 @@ test("resolveTenancyProfile returns mode-specific workspace policy matrix", () =
46
46
  });
47
47
  });
48
48
 
49
- test("isWorkspaceTenancyMode is true only for workspace mode", () => {
50
- assert.equal(isWorkspaceTenancyMode(TENANCY_MODE_WORKSPACE), true);
51
- assert.equal(isWorkspaceTenancyMode(TENANCY_MODE_PERSONAL), false);
52
- assert.equal(isWorkspaceTenancyMode(TENANCY_MODE_NONE), false);
49
+ test("isWorkspacesTenancyMode is true only for workspace mode", () => {
50
+ assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_WORKSPACES), true);
51
+ assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_PERSONAL), false);
52
+ assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_NONE), false);
53
53
  });
54
54
 
55
55
  test("resolveTenancyProfile allows explicit workspace self-create policy override", () => {
56
56
  const workspaceProfile = resolveTenancyProfile({
57
- tenancyMode: TENANCY_MODE_WORKSPACE,
57
+ tenancyMode: TENANCY_MODE_WORKSPACES,
58
58
  tenancyPolicy: {
59
59
  workspace: {
60
60
  allowSelfCreate: true
@@ -62,6 +62,6 @@ test("resolveTenancyProfile allows explicit workspace self-create policy overrid
62
62
  }
63
63
  });
64
64
 
65
- assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACE);
65
+ assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACES);
66
66
  assert.equal(workspaceProfile.workspace.allowSelfCreate, true);
67
67
  });
@@ -29,3 +29,21 @@ test("resolveApiBasePath resolves workspace and non-workspace API base paths", (
29
29
  "/api/customers"
30
30
  );
31
31
  });
32
+
33
+ test("resolveApiBasePath preserves query strings and hash fragments", () => {
34
+ assert.equal(
35
+ resolveApiBasePath({
36
+ surfaceRequiresWorkspace: true,
37
+ relativePath: "/customers?search=buddy#top"
38
+ }),
39
+ "/api/w/:workspaceSlug/workspace/customers?search=buddy#top"
40
+ );
41
+
42
+ assert.equal(
43
+ resolveApiBasePath({
44
+ surfaceRequiresWorkspace: false,
45
+ relativePath: "/?cursor=2"
46
+ }),
47
+ "/api?cursor=2"
48
+ );
49
+ });
@@ -229,10 +229,10 @@ test("users-core route registration follows tenancy mode matrix", async () => {
229
229
  tenancyMode: "personal"
230
230
  });
231
231
  const workspaceRoutes = await registerRoutesForMode({
232
- tenancyMode: "workspace"
232
+ tenancyMode: "workspaces"
233
233
  });
234
234
  const workspaceSelfCreateRoutes = await registerRoutesForMode({
235
- tenancyMode: "workspace",
235
+ tenancyMode: "workspaces",
236
236
  tenancyPolicy: {
237
237
  workspace: {
238
238
  allowSelfCreate: true
@@ -62,7 +62,7 @@ test("workspace bootstrap contributor passes actor context to pending invites se
62
62
  },
63
63
  workspaceInvitationsEnabled: true,
64
64
  appConfig: {
65
- tenancyMode: "workspace"
65
+ tenancyMode: "workspaces"
66
66
  }
67
67
  });
68
68
 
@@ -251,7 +251,7 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
251
251
  },
252
252
  workspaceInvitationsEnabled: true,
253
253
  appConfig: {
254
- tenancyMode: "workspace"
254
+ tenancyMode: "workspaces"
255
255
  }
256
256
  });
257
257
 
@@ -314,7 +314,7 @@ test("workspace bootstrap contributor returns global payload with requestedWorks
314
314
  },
315
315
  workspaceInvitationsEnabled: true,
316
316
  appConfig: {
317
- tenancyMode: "workspace"
317
+ tenancyMode: "workspaces"
318
318
  }
319
319
  });
320
320
 
@@ -386,7 +386,7 @@ test("workspace bootstrap contributor returns requestedWorkspace=not_found when
386
386
  },
387
387
  workspaceInvitationsEnabled: false,
388
388
  appConfig: {
389
- tenancyMode: "workspace"
389
+ tenancyMode: "workspaces"
390
390
  }
391
391
  });
392
392
 
@@ -439,7 +439,7 @@ test("workspace bootstrap contributor returns requestedWorkspace=unauthenticated
439
439
  },
440
440
  workspaceInvitationsEnabled: false,
441
441
  appConfig: {
442
- tenancyMode: "workspace"
442
+ tenancyMode: "workspaces"
443
443
  }
444
444
  });
445
445
 
@@ -19,7 +19,7 @@ function createWorkspaceRoles() {
19
19
  }
20
20
 
21
21
  function createWorkspaceServiceFixture({
22
- tenancyMode = "workspace",
22
+ tenancyMode = "workspaces",
23
23
  tenancyPolicy = {},
24
24
  workspaceRoles = createWorkspaceRoles(),
25
25
  additionalWorkspaces = [],
@@ -177,7 +177,7 @@ test("workspaceService.listWorkspacesForUser returns only accessible workspaces"
177
177
 
178
178
  test("workspaceService.listWorkspacesForUser no longer provisions personal workspace in workspace mode", async () => {
179
179
  const { service, calls } = createWorkspaceServiceFixture({
180
- tenancyMode: "workspace",
180
+ tenancyMode: "workspaces",
181
181
  personalWorkspace: null
182
182
  });
183
183
 
@@ -260,7 +260,7 @@ test("workspaceService.provisionWorkspaceForNewUser provisions personal workspac
260
260
 
261
261
  test("workspaceService.provisionWorkspaceForNewUser is a no-op outside personal tenancy", async () => {
262
262
  const { service, calls } = createWorkspaceServiceFixture({
263
- tenancyMode: "workspace"
263
+ tenancyMode: "workspaces"
264
264
  });
265
265
 
266
266
  const result = await service.provisionWorkspaceForNewUser({
@@ -275,7 +275,7 @@ test("workspaceService.provisionWorkspaceForNewUser is a no-op outside personal
275
275
 
276
276
  test("workspaceService.createWorkspaceForAuthenticatedUser creates non-personal workspace in workspace tenancy", async () => {
277
277
  const { service, calls, insertedPayloads } = createWorkspaceServiceFixture({
278
- tenancyMode: "workspace",
278
+ tenancyMode: "workspaces",
279
279
  tenancyPolicy: {
280
280
  workspace: {
281
281
  allowSelfCreate: true
@@ -304,7 +304,7 @@ test("workspaceService.createWorkspaceForAuthenticatedUser creates non-personal
304
304
 
305
305
  test("workspaceService.createWorkspaceForAuthenticatedUser rejects creation when self-create policy is disabled", async () => {
306
306
  const { service } = createWorkspaceServiceFixture({
307
- tenancyMode: "workspace"
307
+ tenancyMode: "workspaces"
308
308
  });
309
309
 
310
310
  await assert.rejects(