@jskit-ai/users-core 0.1.30 → 0.1.32

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.
Files changed (37) hide show
  1. package/package.descriptor.mjs +8 -6
  2. package/package.json +6 -6
  3. package/src/server/UsersCoreServiceProvider.js +1 -3
  4. package/src/server/accountProfile/avatarService.js +18 -59
  5. package/src/server/accountProfile/avatarStorageService.js +14 -95
  6. package/src/server/accountProfile/bootAccountProfileRoutes.js +10 -14
  7. package/src/server/common/formatters/workspaceFormatter.js +2 -2
  8. package/src/server/common/repositories/userProfilesRepository.js +6 -6
  9. package/src/server/common/repositories/workspaceInvitesRepository.js +2 -2
  10. package/src/server/common/repositories/workspaceMembershipsRepository.js +9 -9
  11. package/src/server/common/repositories/workspacesRepository.js +2 -2
  12. package/src/server/common/services/authProfileSyncService.js +5 -5
  13. package/src/server/common/services/workspaceContextService.js +3 -3
  14. package/src/server/common/validators/authenticatedUserValidator.js +2 -2
  15. package/src/server/workspaceBootstrapContributor.js +2 -2
  16. package/src/server/workspaceMembers/bootWorkspaceMembers.js +2 -2
  17. package/src/server/workspaceMembers/workspaceMembersActions.js +2 -2
  18. package/src/server/workspaceMembers/workspaceMembersService.js +11 -11
  19. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +1 -1
  20. package/src/shared/resources/workspaceMembersResource.js +11 -11
  21. package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
  22. package/src/shared/resources/workspaceResource.js +2 -2
  23. package/src/shared/roles.js +8 -8
  24. package/templates/migrations/users_core_initial.cjs +5 -5
  25. package/test/authProfileSyncService.test.js +5 -5
  26. package/test/avatarService.test.js +4 -4
  27. package/test/usersRouteRequestInputValidator.test.js +4 -4
  28. package/test/workspaceActionContextContributor.test.js +9 -9
  29. package/test/workspaceAuthPolicyContextResolver.test.js +2 -2
  30. package/test/workspaceBootstrapContributor.test.js +1 -1
  31. package/test/workspaceInvitesRepository.test.js +3 -3
  32. package/test/workspaceMembersService.test.js +10 -10
  33. package/test/workspacePendingInvitationsResource.test.js +2 -2
  34. package/test/workspacePendingInvitationsService.test.js +3 -3
  35. package/test/workspaceService.test.js +10 -10
  36. package/src/server/accountProfile/registerAvatarMultipartSupport.js +0 -40
  37. package/test/registerAvatarMultipartSupport.test.js +0 -63
@@ -1,13 +1,14 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-core",
4
- version: "0.1.30",
4
+ version: "0.1.32",
5
5
  kind: "runtime",
6
6
  description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
7
7
  dependsOn: [
8
8
  "@jskit-ai/auth-core",
9
9
  "@jskit-ai/database-runtime",
10
10
  "@jskit-ai/http-runtime",
11
+ "@jskit-ai/uploads-runtime",
11
12
  "@jskit-ai/storage-runtime"
12
13
  ],
13
14
  capabilities: {
@@ -19,6 +20,7 @@ export default Object.freeze({
19
20
  "runtime.actions",
20
21
  "runtime.database",
21
22
  "runtime.storage",
23
+ "runtime.uploads",
22
24
  "auth.provider",
23
25
  "auth.policy"
24
26
  ]
@@ -196,11 +198,11 @@ export default Object.freeze({
196
198
  mutations: {
197
199
  dependencies: {
198
200
  runtime: {
199
- "@jskit-ai/auth-core": "0.1.20",
200
- "@jskit-ai/database-runtime": "0.1.21",
201
- "@jskit-ai/http-runtime": "0.1.20",
202
- "@jskit-ai/kernel": "0.1.21",
203
- "@fastify/multipart": "^9.4.0",
201
+ "@jskit-ai/auth-core": "0.1.22",
202
+ "@jskit-ai/database-runtime": "0.1.23",
203
+ "@jskit-ai/http-runtime": "0.1.22",
204
+ "@jskit-ai/kernel": "0.1.23",
205
+ "@jskit-ai/uploads-runtime": "0.1.1",
204
206
  "@fastify/type-provider-typebox": "^6.1.0",
205
207
  "typebox": "^1.0.81"
206
208
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-core",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -24,11 +24,11 @@
24
24
  "./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
25
25
  },
26
26
  "dependencies": {
27
- "@jskit-ai/auth-core": "0.1.20",
28
- "@jskit-ai/database-runtime": "0.1.21",
29
- "@jskit-ai/http-runtime": "0.1.20",
30
- "@jskit-ai/kernel": "0.1.21",
31
- "@fastify/multipart": "^9.4.0",
27
+ "@jskit-ai/auth-core": "0.1.22",
28
+ "@jskit-ai/database-runtime": "0.1.23",
29
+ "@jskit-ai/http-runtime": "0.1.22",
30
+ "@jskit-ai/kernel": "0.1.23",
31
+ "@jskit-ai/uploads-runtime": "0.1.1",
32
32
  "@fastify/type-provider-typebox": "^6.1.0",
33
33
  "typebox": "^1.0.81"
34
34
  }
@@ -23,13 +23,12 @@ import { registerAccountNotifications } from "./accountNotifications/registerAcc
23
23
  import { registerAccountProfile } from "./accountProfile/registerAccountProfile.js";
24
24
  import { registerAccountSecurity } from "./accountSecurity/registerAccountSecurity.js";
25
25
  import { registerConsoleSettings } from "./consoleSettings/registerConsoleSettings.js";
26
- import { registerAvatarMultipartSupport } from "./accountProfile/registerAvatarMultipartSupport.js";
27
26
  import { registerUsersCoreActionSurfaceSources } from "./support/workspaceActionSurfaces.js";
28
27
 
29
28
  class UsersCoreServiceProvider {
30
29
  static id = "users.core";
31
30
 
32
- static dependsOn = ["runtime.server", "runtime.actions", "runtime.database", "runtime.storage", "auth.provider"];
31
+ static dependsOn = ["runtime.server", "runtime.actions", "runtime.database", "runtime.storage", "auth.provider", "runtime.uploads"];
33
32
 
34
33
  register(app) {
35
34
  registerUsersCoreActionSurfaceSources(app);
@@ -58,7 +57,6 @@ class UsersCoreServiceProvider {
58
57
  bootWorkspaceSettings(app);
59
58
  bootWorkspaceMembers(app);
60
59
  }
61
- await registerAvatarMultipartSupport(app);
62
60
  bootAccountProfileRoutes(app);
63
61
  bootAccountPreferencesRoutes(app);
64
62
  bootAccountNotificationsRoutes(app);
@@ -1,63 +1,24 @@
1
- import { AppError, createValidationError } from "@jskit-ai/kernel/server/runtime/errors";
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { DEFAULT_IMAGE_UPLOAD_POLICY } from "@jskit-ai/uploads-runtime/shared";
3
+ import {
4
+ normalizeUploadPolicy,
5
+ readUploadBuffer,
6
+ validateUploadMimeType
7
+ } from "@jskit-ai/uploads-runtime/server/policy/uploadPolicy";
2
8
  import { resolveUserProfile } from "../common/services/accountContextService.js";
3
9
 
4
- const DEFAULT_AVATAR_POLICY = Object.freeze({
5
- allowedMimeTypes: Object.freeze(["image/jpeg", "image/png", "image/webp"]),
6
- maxUploadBytes: 5 * 1024 * 1024
7
- });
10
+ const DEFAULT_AVATAR_POLICY = DEFAULT_IMAGE_UPLOAD_POLICY;
8
11
 
9
12
  function resolveAvatarPolicy(policy = {}) {
10
- const source = policy && typeof policy === "object" ? policy : {};
11
- const allowedMimeTypes =
12
- Array.isArray(source.allowedMimeTypes) && source.allowedMimeTypes.length > 0
13
- ? source.allowedMimeTypes
14
- .map((value) => String(value || "").trim().toLowerCase())
15
- .filter((value) => value.length > 0)
16
- : [...DEFAULT_AVATAR_POLICY.allowedMimeTypes];
17
- const normalizedMaxUploadBytes = Number(source.maxUploadBytes);
18
- const maxUploadBytes =
19
- Number.isInteger(normalizedMaxUploadBytes) && normalizedMaxUploadBytes > 0
20
- ? normalizedMaxUploadBytes
21
- : DEFAULT_AVATAR_POLICY.maxUploadBytes;
22
-
23
- return Object.freeze({
24
- allowedMimeTypes: Object.freeze(allowedMimeTypes),
25
- maxUploadBytes
26
- });
13
+ return normalizeUploadPolicy(policy, DEFAULT_AVATAR_POLICY);
27
14
  }
28
15
 
29
16
  async function readAvatarBuffer(stream, { maxBytes = DEFAULT_AVATAR_POLICY.maxUploadBytes } = {}) {
30
- if (!stream || typeof stream.on !== "function") {
31
- throw new TypeError("Avatar upload stream is required.");
32
- }
33
-
34
- const chunks = [];
35
- let total = 0;
36
-
37
- for await (const chunk of stream) {
38
- const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
39
- total += bufferChunk.length;
40
-
41
- if (total > maxBytes) {
42
- throw createValidationError({
43
- avatar: `Avatar file is too large. Maximum allowed size is ${Math.floor(maxBytes / (1024 * 1024))}MB.`
44
- });
45
- }
46
-
47
- chunks.push(bufferChunk);
48
- }
49
-
50
- if (chunks.length === 0) {
51
- throw createValidationError({
52
- avatar: "Avatar file is empty."
53
- });
54
- }
55
-
56
- return Buffer.concat(chunks);
57
- }
58
-
59
- function normalizeMimeType(value) {
60
- return String(value || "").trim().toLowerCase();
17
+ return readUploadBuffer(stream, {
18
+ maxBytes,
19
+ fieldName: "avatar",
20
+ label: "Avatar"
21
+ });
61
22
  }
62
23
 
63
24
  function createService({ userProfilesRepository, avatarStorageService, avatarPolicy } = {}) {
@@ -80,12 +41,10 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
80
41
 
81
42
  async function uploadForUser(user, payload = {}) {
82
43
  const profile = await resolveProfile(user);
83
- const mimeType = normalizeMimeType(payload?.mimeType);
84
- if (!resolvedAvatarPolicy.allowedMimeTypes.includes(mimeType)) {
85
- throw createValidationError({
86
- avatar: `Avatar must be one of: ${resolvedAvatarPolicy.allowedMimeTypes.join(", ")}.`
87
- });
88
- }
44
+ validateUploadMimeType(payload?.mimeType, resolvedAvatarPolicy, {
45
+ fieldName: "avatar",
46
+ label: "Avatar"
47
+ });
89
48
 
90
49
  const buffer = await readAvatarBuffer(payload.stream, {
91
50
  maxBytes: resolvedAvatarPolicy.maxUploadBytes
@@ -1,10 +1,10 @@
1
1
  import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
+ import {
3
+ createUploadStorageService,
4
+ detectCommonMimeTypeFromBuffer
5
+ } from "@jskit-ai/uploads-runtime/server/storage/createUploadStorageService";
2
6
 
3
7
  const AVATAR_STORAGE_PREFIX = "users/avatars";
4
- const AVATAR_MIME_TYPE_JPEG = "image/jpeg";
5
- const AVATAR_MIME_TYPE_PNG = "image/png";
6
- const AVATAR_MIME_TYPE_WEBP = "image/webp";
7
- const AVATAR_MIME_TYPE_FALLBACK = "application/octet-stream";
8
8
 
9
9
  function buildAvatarStorageKey(userId) {
10
10
  const normalizedUserId = parsePositiveInteger(userId);
@@ -15,106 +15,25 @@ function buildAvatarStorageKey(userId) {
15
15
  return `${AVATAR_STORAGE_PREFIX}/${normalizedUserId}/avatar`;
16
16
  }
17
17
 
18
- function normalizeStorageKey(value) {
19
- const normalized = String(value || "").trim();
20
- if (!normalized) {
21
- return "";
22
- }
23
- if (normalized.startsWith("/") || normalized.includes("..")) {
24
- return "";
25
- }
26
- return normalized;
27
- }
28
-
29
- function detectAvatarMimeTypeFromBuffer(buffer) {
30
- if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
31
- return AVATAR_MIME_TYPE_FALLBACK;
32
- }
33
-
34
- if (
35
- buffer.length >= 3 &&
36
- buffer[0] === 0xff &&
37
- buffer[1] === 0xd8 &&
38
- buffer[2] === 0xff
39
- ) {
40
- return AVATAR_MIME_TYPE_JPEG;
41
- }
42
-
43
- if (
44
- buffer.length >= 8 &&
45
- buffer[0] === 0x89 &&
46
- buffer[1] === 0x50 &&
47
- buffer[2] === 0x4e &&
48
- buffer[3] === 0x47 &&
49
- buffer[4] === 0x0d &&
50
- buffer[5] === 0x0a &&
51
- buffer[6] === 0x1a &&
52
- buffer[7] === 0x0a
53
- ) {
54
- return AVATAR_MIME_TYPE_PNG;
55
- }
56
-
57
- if (
58
- buffer.length >= 12 &&
59
- buffer[0] === 0x52 &&
60
- buffer[1] === 0x49 &&
61
- buffer[2] === 0x46 &&
62
- buffer[3] === 0x46 &&
63
- buffer[8] === 0x57 &&
64
- buffer[9] === 0x45 &&
65
- buffer[10] === 0x42 &&
66
- buffer[11] === 0x50
67
- ) {
68
- return AVATAR_MIME_TYPE_WEBP;
69
- }
70
-
71
- return AVATAR_MIME_TYPE_FALLBACK;
72
- }
73
-
74
18
  function createService({ storage } = {}) {
75
- if (!storage || typeof storage.getItemRaw !== "function" || typeof storage.setItemRaw !== "function") {
76
- throw new TypeError("avatarStorageService requires a storage binding with getItemRaw()/setItemRaw().");
77
- }
19
+ const uploadStorageService = createUploadStorageService({
20
+ storage,
21
+ mimeTypeDetector: detectCommonMimeTypeFromBuffer
22
+ });
78
23
 
79
24
  async function saveAvatar({ userId, buffer }) {
80
- if (!Buffer.isBuffer(buffer)) {
81
- throw new TypeError("Avatar buffer must be a Buffer instance.");
82
- }
83
-
84
- const storageKey = buildAvatarStorageKey(userId);
85
- await storage.setItemRaw(storageKey, buffer);
86
-
87
- return Object.freeze({
88
- storageKey
25
+ return uploadStorageService.saveFile({
26
+ storageKey: buildAvatarStorageKey(userId),
27
+ buffer
89
28
  });
90
29
  }
91
30
 
92
31
  async function readAvatar(storageKey) {
93
- const normalizedStorageKey = normalizeStorageKey(storageKey);
94
- if (!normalizedStorageKey) {
95
- return null;
96
- }
97
-
98
- const value = await storage.getItemRaw(normalizedStorageKey);
99
- if (value == null) {
100
- return null;
101
- }
102
-
103
- const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
104
- return Object.freeze({
105
- storageKey: normalizedStorageKey,
106
- buffer,
107
- mimeType: detectAvatarMimeTypeFromBuffer(buffer)
108
- });
32
+ return uploadStorageService.readFile(storageKey);
109
33
  }
110
34
 
111
35
  async function deleteAvatar(storageKey) {
112
- const normalizedStorageKey = normalizeStorageKey(storageKey);
113
- if (!normalizedStorageKey || typeof storage.removeItem !== "function") {
114
- return;
115
- }
116
-
117
- await storage.removeItem(normalizedStorageKey);
36
+ await uploadStorageService.deleteFile(storageKey);
118
37
  }
119
38
 
120
39
  return Object.freeze({
@@ -126,7 +45,7 @@ function createService({ storage } = {}) {
126
45
 
127
46
  const __testables = Object.freeze({
128
47
  buildAvatarStorageKey,
129
- detectAvatarMimeTypeFromBuffer
48
+ detectAvatarMimeTypeFromBuffer: detectCommonMimeTypeFromBuffer
130
49
  });
131
50
 
132
51
  export { createService, __testables };
@@ -1,5 +1,5 @@
1
- import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
1
  import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
+ import { readSingleMultipartFile } from "@jskit-ai/uploads-runtime/server/multipart/readSingleMultipartFile";
3
3
  import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
4
4
  import { userProfileResource } from "../../shared/resources/userProfileResource.js";
5
5
 
@@ -113,24 +113,20 @@ function bootAccountProfileRoutes(app) {
113
113
  )
114
114
  },
115
115
  async function (request, reply) {
116
- const filePart = await request.file();
117
- if (!filePart) {
118
- throw new AppError(400, "Validation failed.", {
119
- details: {
120
- fieldErrors: {
121
- avatar: "Avatar file is required."
122
- }
123
- }
124
- });
125
- }
116
+ const filePart = await readSingleMultipartFile(request, {
117
+ fieldNames: ["avatar"],
118
+ required: true,
119
+ fieldErrorKey: "avatar",
120
+ label: "Avatar"
121
+ });
126
122
 
127
123
  const uploadDimension = filePart.fields?.uploadDimension?.value;
128
124
  const response = await request.executeAction({
129
125
  actionId: "settings.profile.avatar.upload",
130
126
  input: {
131
- stream: filePart.file,
132
- mimeType: filePart.mimetype,
133
- fileName: filePart.filename,
127
+ stream: filePart.stream,
128
+ mimeType: filePart.mimeType,
129
+ fileName: filePart.fileName,
134
130
  uploadDimension
135
131
  }
136
132
  });
@@ -7,7 +7,7 @@ function mapWorkspaceSummary(workspace, membership) {
7
7
  slug: normalizeText(workspace.slug),
8
8
  name: normalizeText(workspace.name),
9
9
  avatarUrl: normalizeText(workspace.avatarUrl),
10
- roleId: normalizeLowerText(membership?.roleId || "member") || "member",
10
+ roleSid: normalizeLowerText(membership?.roleSid || "member") || "member",
11
11
  isAccessible: normalizeLowerText(membership?.status || "active") === "active"
12
12
  };
13
13
  }
@@ -40,7 +40,7 @@ function mapMembershipSummary(membership, workspace) {
40
40
 
41
41
  return {
42
42
  workspaceId: Number(workspace?.id || membership.workspaceId),
43
- roleId: normalizeLowerText(membership.roleId || "member") || "member",
43
+ roleSid: normalizeLowerText(membership.roleSid || "member") || "member",
44
44
  status: normalizeLowerText(membership.status || "active") || "active"
45
45
  };
46
46
  }
@@ -13,7 +13,7 @@ const USERNAME_MAX_LENGTH = 120;
13
13
  function normalizeIdentity(identityLike) {
14
14
  const source = identityLike && typeof identityLike === "object" ? identityLike : {};
15
15
  const provider = normalizeLowerText(source.provider || source.authProvider);
16
- const providerUserId = normalizeText(source.providerUserId || source.authProviderUserId);
16
+ const providerUserId = normalizeText(source.providerUserId || source.authProviderUserSid);
17
17
  if (!provider || !providerUserId) {
18
18
  return null;
19
19
  }
@@ -57,7 +57,7 @@ function mapProfileRow(row) {
57
57
  return {
58
58
  id: Number(row.id),
59
59
  authProvider: normalizeLowerText(row.auth_provider),
60
- authProviderUserId: normalizeText(row.auth_provider_user_id),
60
+ authProviderUserSid: normalizeText(row.auth_provider_user_sid),
61
61
  email: normalizeLowerText(row.email),
62
62
  username: normalizeLowerText(row.username),
63
63
  displayName: normalizeText(row.display_name),
@@ -123,7 +123,7 @@ function createRepository(knex) {
123
123
  const row = await client("users")
124
124
  .where({
125
125
  auth_provider: identity.provider,
126
- auth_provider_user_id: identity.providerUserId
126
+ auth_provider_user_sid: identity.providerUserId
127
127
  })
128
128
  .first();
129
129
  return mapProfileRow(row);
@@ -168,7 +168,7 @@ function createRepository(knex) {
168
168
  const client = options?.trx || knex;
169
169
  const identity = normalizeIdentity(profileLike);
170
170
  if (!identity) {
171
- throw new TypeError("upsert requires provider/authProvider and providerUserId/authProviderUserId.");
171
+ throw new TypeError("upsert requires provider/authProvider and providerUserId/authProviderUserSid.");
172
172
  }
173
173
 
174
174
  const email = normalizeLowerText(profileLike.email);
@@ -181,7 +181,7 @@ function createRepository(knex) {
181
181
  const executeUpsert = async (trx) => {
182
182
  const where = {
183
183
  auth_provider: identity.provider,
184
- auth_provider_user_id: identity.providerUserId
184
+ auth_provider_user_sid: identity.providerUserId
185
185
  };
186
186
  const existing = await trx("users").where(where).first();
187
187
 
@@ -200,7 +200,7 @@ function createRepository(knex) {
200
200
  const username = await resolveUniqueUsername(trx, requestedUsername || usernameBaseFromEmail(email));
201
201
  await trx("users").insert({
202
202
  auth_provider: identity.provider,
203
- auth_provider_user_id: identity.providerUserId,
203
+ auth_provider_user_sid: identity.providerUserId,
204
204
  email,
205
205
  display_name: displayName,
206
206
  username
@@ -17,7 +17,7 @@ function mapRow(row) {
17
17
  id: Number(row.id),
18
18
  workspaceId: Number(row.workspace_id),
19
19
  email: normalizeLowerText(row.email),
20
- roleId: normalizeLowerText(row.role_id || "member") || "member",
20
+ roleSid: normalizeLowerText(row.role_sid || "member") || "member",
21
21
  status: normalizeLowerText(row.status || "pending") || "pending",
22
22
  tokenHash: normalizeText(row.token_hash),
23
23
  invitedByUserId: row.invited_by_user_id == null ? null : Number(row.invited_by_user_id),
@@ -86,7 +86,7 @@ function createRepository(knex) {
86
86
  const insertPayload = {
87
87
  workspace_id: Number(source.workspaceId),
88
88
  email: normalizeLowerText(source.email),
89
- role_id: normalizeLowerText(source.roleId || "member") || "member",
89
+ role_sid: normalizeLowerText(source.roleSid || "member") || "member",
90
90
  status: normalizeLowerText(source.status || "pending") || "pending",
91
91
  token_hash: normalizeText(source.tokenHash),
92
92
  invited_by_user_id: source.invitedByUserId == null ? null : Number(source.invitedByUserId),
@@ -16,7 +16,7 @@ function mapRow(row) {
16
16
  id: Number(row.id),
17
17
  workspaceId: Number(row.workspace_id),
18
18
  userId: Number(row.user_id),
19
- roleId: normalizeLowerText(row.role_id || "member") || "member",
19
+ roleSid: normalizeLowerText(row.role_sid || "member") || "member",
20
20
  status: normalizeLowerText(row.status || "active") || "active",
21
21
  createdAt: toIsoString(row.created_at),
22
22
  updatedAt: toIsoString(row.updated_at)
@@ -30,7 +30,7 @@ function mapMemberSummaryRow(row) {
30
30
 
31
31
  return {
32
32
  userId: Number(row.user_id),
33
- roleId: normalizeLowerText(row.role_id || "member") || "member",
33
+ roleSid: normalizeLowerText(row.role_sid || "member") || "member",
34
34
  status: normalizeLowerText(row.status || "active") || "active",
35
35
  displayName: normalizeText(row.display_name),
36
36
  email: normalizeLowerText(row.email)
@@ -54,11 +54,11 @@ function createRepository(knex) {
54
54
  const client = options?.trx || knex;
55
55
  const existing = await findByWorkspaceIdAndUserId(workspaceId, userId, { trx: client });
56
56
  if (existing) {
57
- if (existing.roleId !== OWNER_ROLE_ID || existing.status !== "active") {
57
+ if (existing.roleSid !== OWNER_ROLE_ID || existing.status !== "active") {
58
58
  await client("workspace_memberships")
59
59
  .where({ workspace_id: Number(workspaceId), user_id: Number(userId) })
60
60
  .update({
61
- role_id: OWNER_ROLE_ID,
61
+ role_sid: OWNER_ROLE_ID,
62
62
  status: "active",
63
63
  updated_at: nowDb()
64
64
  });
@@ -70,7 +70,7 @@ function createRepository(knex) {
70
70
  await client("workspace_memberships").insert({
71
71
  workspace_id: Number(workspaceId),
72
72
  user_id: Number(userId),
73
- role_id: OWNER_ROLE_ID,
73
+ role_sid: OWNER_ROLE_ID,
74
74
  status: "active",
75
75
  created_at: nowDb(),
76
76
  updated_at: nowDb()
@@ -87,14 +87,14 @@ function createRepository(knex) {
87
87
  async function upsertMembership(workspaceId, userId, patch = {}, options = {}) {
88
88
  const client = options?.trx || knex;
89
89
  const existing = await findByWorkspaceIdAndUserId(workspaceId, userId, { trx: client });
90
- const roleId = normalizeLowerText(patch.roleId || existing?.roleId || "member") || "member";
90
+ const roleSid = normalizeLowerText(patch.roleSid || existing?.roleSid || "member") || "member";
91
91
  const status = normalizeLowerText(patch.status || existing?.status || "active") || "active";
92
92
 
93
93
  if (!existing) {
94
94
  await client("workspace_memberships").insert({
95
95
  workspace_id: Number(workspaceId),
96
96
  user_id: Number(userId),
97
- role_id: roleId,
97
+ role_sid: roleSid,
98
98
  status,
99
99
  created_at: nowDb(),
100
100
  updated_at: nowDb()
@@ -105,7 +105,7 @@ function createRepository(knex) {
105
105
  await client("workspace_memberships")
106
106
  .where({ workspace_id: Number(workspaceId), user_id: Number(userId) })
107
107
  .update({
108
- role_id: roleId,
108
+ role_sid: roleSid,
109
109
  status,
110
110
  updated_at: nowDb()
111
111
  });
@@ -121,7 +121,7 @@ function createRepository(knex) {
121
121
  .orderBy("up.display_name", "asc")
122
122
  .select([
123
123
  "wm.user_id",
124
- "wm.role_id",
124
+ "wm.role_sid",
125
125
  "wm.status",
126
126
  "up.display_name",
127
127
  "up.email"
@@ -32,7 +32,7 @@ function mapMembershipWorkspaceRow(row) {
32
32
 
33
33
  return {
34
34
  ...mapRow(row),
35
- roleId: normalizeLowerText(row.role_id || "member"),
35
+ roleSid: normalizeLowerText(row.role_sid || "member"),
36
36
  membershipStatus: normalizeLowerText(row.membership_status || "active") || "active"
37
37
  };
38
38
  }
@@ -55,7 +55,7 @@ function createRepository(knex) {
55
55
  "w.deleted_at"
56
56
  ];
57
57
  if (includeMembership) {
58
- columns.push("wm.role_id", "wm.status as membership_status");
58
+ columns.push("wm.role_sid", "wm.status as membership_status");
59
59
  }
60
60
  return columns;
61
61
  }
@@ -9,7 +9,7 @@ function buildNormalizedIdentityKey(identityLike) {
9
9
 
10
10
  return {
11
11
  authProvider: identity.provider,
12
- authProviderUserId: identity.providerUserId
12
+ authProviderUserSid: identity.providerUserId
13
13
  };
14
14
  }
15
15
 
@@ -25,7 +25,7 @@ function buildNormalizedIdentityProfile(profileLike) {
25
25
 
26
26
  return {
27
27
  authProvider: identity.authProvider,
28
- authProviderUserId: identity.authProviderUserId,
28
+ authProviderUserSid: identity.authProviderUserSid,
29
29
  email,
30
30
  displayName,
31
31
  username: normalizeLowerText(source.username)
@@ -41,7 +41,7 @@ function profileNeedsUpdate(existing, nextProfile) {
41
41
  existing.email !== nextProfile.email ||
42
42
  existing.displayName !== nextProfile.displayName ||
43
43
  existing.authProvider !== nextProfile.authProvider ||
44
- existing.authProviderUserId !== nextProfile.authProviderUserId
44
+ existing.authProviderUserSid !== nextProfile.authProviderUserSid
45
45
  );
46
46
  }
47
47
 
@@ -69,7 +69,7 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
69
69
  return userProfilesRepository.findByIdentity(
70
70
  {
71
71
  provider: normalized.authProvider,
72
- providerUserId: normalized.authProviderUserId
72
+ providerUserId: normalized.authProviderUserSid
73
73
  },
74
74
  options
75
75
  );
@@ -80,7 +80,7 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
80
80
  return userProfilesRepository.upsert(
81
81
  {
82
82
  authProvider: normalized.authProvider,
83
- authProviderUserId: normalized.authProviderUserId,
83
+ authProviderUserSid: normalized.authProviderUserSid,
84
84
  email: normalized.email,
85
85
  displayName: normalized.displayName,
86
86
  username: normalized.username
@@ -50,8 +50,8 @@ function buildWorkspaceName(user = {}) {
50
50
  }
51
51
 
52
52
  function buildPermissionsFromMembership(membership, appConfig = {}) {
53
- const roleId = normalizeLowerText(membership?.roleId || "member");
54
- return resolveRolePermissions(roleId, appConfig);
53
+ const roleSid = normalizeLowerText(membership?.roleSid || "member");
54
+ return resolveRolePermissions(roleSid, appConfig);
55
55
  }
56
56
 
57
57
  function hashInviteToken(token) {
@@ -145,7 +145,7 @@ function createService({
145
145
 
146
146
  const list = await workspacesRepository.listForUserId(normalizedUser.id, options);
147
147
  const accessible = list
148
- .map((entry) => mapWorkspaceSummary(entry, { roleId: entry.roleId, status: entry.membershipStatus }))
148
+ .map((entry) => mapWorkspaceSummary(entry, { roleSid: entry.roleSid, status: entry.membershipStatus }))
149
149
  .filter((entry) => entry.isAccessible);
150
150
 
151
151
  return accessible;
@@ -16,7 +16,7 @@ function normalizeAuthenticatedUser(input = {}) {
16
16
  username: normalizeLowerText(source.username),
17
17
  displayName: normalizeText(source.displayName) || email || `User ${id}`,
18
18
  authProvider: normalizeLowerText(source.authProvider),
19
- authProviderUserId: normalizeText(source.authProviderUserId),
19
+ authProviderUserSid: normalizeText(source.authProviderUserSid),
20
20
  avatarStorageKey: source.avatarStorageKey ? normalizeText(source.avatarStorageKey) : null,
21
21
  avatarVersion: source.avatarVersion == null ? null : String(source.avatarVersion)
22
22
  };
@@ -30,7 +30,7 @@ const authenticatedUserValidator = Object.freeze({
30
30
  username: Type.Optional(Type.String()),
31
31
  displayName: Type.Optional(Type.String()),
32
32
  authProvider: Type.Optional(Type.String()),
33
- authProviderUserId: Type.Optional(Type.String()),
33
+ authProviderUserSid: Type.Optional(Type.String()),
34
34
  avatarStorageKey: Type.Optional(Type.Union([Type.String(), Type.Null()])),
35
35
  avatarVersion: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Null()]))
36
36
  },
@@ -319,7 +319,7 @@ function createWorkspaceBootstrapContributor({
319
319
  const latestProfile =
320
320
  (await userProfilesRepository.findByIdentity({
321
321
  provider: normalizedUser.authProvider,
322
- providerUserId: normalizedUser.authProviderUserId
322
+ providerUserId: normalizedUser.authProviderUserSid
323
323
  })) || normalizedUser;
324
324
 
325
325
  const workspaces = await workspaceService.listWorkspacesForUser(latestProfile, { request });
@@ -362,7 +362,7 @@ function createWorkspaceBootstrapContributor({
362
362
  pendingInvites,
363
363
  activeWorkspace: workspaceContext
364
364
  ? mapWorkspaceSummary(workspaceContext.workspace, {
365
- roleId: workspaceContext.membership?.roleId,
365
+ roleSid: workspaceContext.membership?.roleSid,
366
366
  status: workspaceContext.membership?.status
367
367
  })
368
368
  : null,