@makolabs/ripple 1.2.10 → 1.2.12

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,75 +1,18 @@
1
- /**
2
- * User Management Remote Functions
3
- *
4
- * Complete implementation template for user management remote functions.
5
- * This follows the same pattern as users.remote.ts and is designed to be
6
- * used as-is in SvelteKit applications like sharkfin-frontend.
7
- *
8
- * Configuration:
9
- * - Set environment variables: CLERK_SECRET_KEY, ADMIN_API_KEY, PRIVATE_BASE_AUTH_URL, ALLOWED_ORG_ID
10
- * - Configure CLIENT_ID via ADMIN_CONFIG or env variable (CLIENT_ID)
11
- * - Adjust permission prefix filter via PERMISSION_PREFIX env variable
12
- *
13
- * @see https://svelte.dev/docs/kit/remote-functions
14
- *
15
- * Usage in your app:
16
- * ```svelte
17
- * <script>
18
- * import { UserManagement } from '@makolabs/ripple';
19
- * import * as adapter from '../UserManagement.remote';
20
- * </script>
21
- *
22
- * <UserManagement adapter={adapter} />
23
- * ```
24
- */
25
1
  import type { User } from '../user-management.js';
26
2
  import type { GetUsersOptions, GetUsersResult } from './types.js';
27
- /**
28
- * Get users with pagination and sorting
29
- *
30
- * Transforms adapter options to Clerk API format
31
- */
32
3
  export declare const getUsers: import("@sveltejs/kit").RemoteQueryFunction<GetUsersOptions, GetUsersResult>;
33
- /**
34
- * Create a new user
35
- *
36
- * Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
37
- */
38
4
  export declare const createUser: import("@sveltejs/kit").RemoteCommand<Partial<User>, Promise<User>>;
39
- /**
40
- * Update an existing user
41
- */
42
5
  export declare const updateUser: import("@sveltejs/kit").RemoteCommand<{
43
6
  userId: string;
44
7
  userData: Partial<User>;
45
8
  }, Promise<User>>;
46
- /**
47
- * Delete a single user
48
- */
49
9
  export declare const deleteUser: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
50
- /**
51
- * Delete multiple users
52
- */
53
10
  export declare const deleteUsers: import("@sveltejs/kit").RemoteCommand<string[], Promise<void>>;
54
- /**
55
- * Get permissions for specific users (batched)
56
- *
57
- * Uses 'sub' (userId) parameter in admin API calls.
58
- * Batches multiple permission requests to avoid n+1 problem.
59
- */
60
11
  export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, Promise<string[]>>;
61
- /**
62
- * Update permissions for a specific user
63
- *
64
- * Uses 'sub' (userId) parameter in admin API calls
65
- */
66
12
  export declare const updateUserPermissions: import("@sveltejs/kit").RemoteCommand<{
67
13
  userId: string;
68
14
  permissions: string[];
69
15
  }, Promise<void>>;
70
- /**
71
- * Generate new API key for a user with optional old key revocation
72
- */
73
16
  export declare const generateApiKey: import("@sveltejs/kit").RemoteCommand<{
74
17
  userId: string;
75
18
  permissions: string[];
@@ -1,46 +1,19 @@
1
- /**
2
- * User Management Remote Functions
3
- *
4
- * Complete implementation template for user management remote functions.
5
- * This follows the same pattern as users.remote.ts and is designed to be
6
- * used as-is in SvelteKit applications like sharkfin-frontend.
7
- *
8
- * Configuration:
9
- * - Set environment variables: CLERK_SECRET_KEY, ADMIN_API_KEY, PRIVATE_BASE_AUTH_URL, ALLOWED_ORG_ID
10
- * - Configure CLIENT_ID via ADMIN_CONFIG or env variable (CLIENT_ID)
11
- * - Adjust permission prefix filter via PERMISSION_PREFIX env variable
12
- *
13
- * @see https://svelte.dev/docs/kit/remote-functions
14
- *
15
- * Usage in your app:
16
- * ```svelte
17
- * <script>
18
- * import { UserManagement } from '@makolabs/ripple';
19
- * import * as adapter from '../UserManagement.remote';
20
- * </script>
21
- *
22
- * <UserManagement adapter={adapter} />
23
- * ```
24
- */
25
1
  import { query, command } from '$app/server';
2
+ import { getRequestEvent } from '$app/server';
26
3
  import { env } from '$env/dynamic/private';
27
- /**
28
- * Configuration
29
- * Override these values or import from your config file
30
- */
31
- // Option 1: Import from your config (recommended)
32
- // import { ADMIN_CONFIG } from '../../constants/permissions';
33
- // const CLIENT_ID = ADMIN_CONFIG.CLIENT_ID;
34
- // Option 2: Use environment variable
35
- const CLIENT_ID = env.CLIENT_ID || 'sharkfin'; // Default fallback
36
- // Permission prefix filter - adjust based on your permission structure
37
- // Example: 'sharkfin:' filters to only sharkfin permissions
4
+ const CLIENT_ID = env.CLIENT_ID || 'sharkfin';
38
5
  const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
39
- // Organization ID for adding users to organizations
40
6
  const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
41
- /**
42
- * Helper: Make authenticated request to Clerk API
43
- */
7
+ function handleClerkError(error, defaultMessage) {
8
+ if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
9
+ const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
10
+ enrichedError.status = error.status;
11
+ enrichedError.details = error.details;
12
+ enrichedError.clerkError = true;
13
+ throw enrichedError;
14
+ }
15
+ throw new Error(`${defaultMessage}: ${error instanceof Error ? error.message : 'Unknown error'}`);
16
+ }
44
17
  async function makeClerkRequest(endpoint, options = {}) {
45
18
  const CLERK_SECRET_KEY = env.CLERK_SECRET_KEY;
46
19
  if (!CLERK_SECRET_KEY) {
@@ -56,8 +29,7 @@ async function makeClerkRequest(endpoint, options = {}) {
56
29
  });
57
30
  if (!response.ok) {
58
31
  const errorText = await response.text();
59
- console.error(`❌ [Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
60
- // Try to parse error details
32
+ console.error(`[Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
61
33
  let errorDetails;
62
34
  try {
63
35
  errorDetails = JSON.parse(errorText);
@@ -65,7 +37,6 @@ async function makeClerkRequest(endpoint, options = {}) {
65
37
  catch {
66
38
  errorDetails = { message: errorText || `${response.status} ${response.statusText}` };
67
39
  }
68
- // Throw error with status and details for better frontend handling
69
40
  const error = new Error(errorDetails.message || `Clerk API request failed: ${response.status} ${response.statusText}`);
70
41
  error.status = response.status;
71
42
  error.details = errorDetails;
@@ -73,9 +44,6 @@ async function makeClerkRequest(endpoint, options = {}) {
73
44
  }
74
45
  return response.json();
75
46
  }
76
- /**
77
- * Helper: Make authenticated request to Admin API
78
- */
79
47
  async function makeAdminRequest(endpoint, options = {}) {
80
48
  const ADMIN_API_KEY = env.ADMIN_API_KEY;
81
49
  const PRIVATE_BASE_AUTH_URL = env.PRIVATE_BASE_AUTH_URL;
@@ -98,60 +66,39 @@ async function makeAdminRequest(endpoint, options = {}) {
98
66
  });
99
67
  if (!response.ok) {
100
68
  const errorText = await response.text();
101
- console.error(`❌ [Admin API] ${response.status} ${response.statusText} - ${errorText}`);
69
+ console.error(`[Admin API] ${response.status} ${response.statusText} - ${errorText}`);
102
70
  throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
103
71
  }
104
72
  return response.json();
105
73
  }
106
- /**
107
- * Helper: Create admin key with permissions for a user
108
- *
109
- * @param userId - The user ID (used as 'sub' in admin API)
110
- * @param permissions - Array of permission strings
111
- * @param clientId - Client ID (defaults to CLIENT_ID config)
112
- */
113
74
  async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
114
- console.log(`🔑 [createUserPermissions] Creating admin key for user ${userId}`);
115
- // Filter permissions by prefix if configured
116
75
  const filteredPermissions = PERMISSION_PREFIX
117
76
  ? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
118
77
  : permissions;
119
78
  if (filteredPermissions.length === 0) {
120
- console.log(`⚠️ [createUserPermissions] No ${PERMISSION_PREFIX || ''} permissions provided, skipping`);
121
79
  return null;
122
80
  }
123
- const createData = await makeAdminRequest('/admin/keys', {
81
+ return await makeAdminRequest('/admin/keys', {
124
82
  method: 'POST',
125
83
  body: JSON.stringify({
126
84
  client_id: clientId,
127
- sub: userId, // userId is used as 'sub' parameter
85
+ sub: userId,
128
86
  scopes: filteredPermissions
129
87
  })
130
88
  });
131
- console.log(`✅ [createUserPermissions] Admin key created successfully for user ${userId}`);
132
- return createData;
133
89
  }
134
- /**
135
- * Get users with pagination and sorting
136
- *
137
- * Transforms adapter options to Clerk API format
138
- */
139
90
  export const getUsers = query('unchecked', async (options) => {
140
- console.log('🔍 [getUsers] Fetching users with options:', options);
141
91
  try {
142
- // Transform adapter options to Clerk API format
143
92
  const limit = options.pageSize;
144
93
  const offset = (options.page - 1) * options.pageSize;
145
- // Build orderBy string
146
94
  let orderBy = '';
147
95
  if (options.sortBy) {
148
96
  const prefix = options.sortOrder === 'desc' ? '-' : '';
149
97
  orderBy = `${prefix}${options.sortBy}`;
150
98
  }
151
99
  else {
152
- orderBy = '-created_at'; // Default
100
+ orderBy = '-created_at';
153
101
  }
154
- // Build Clerk API parameters
155
102
  const params = new URLSearchParams({
156
103
  limit: limit.toString(),
157
104
  offset: offset.toString(),
@@ -159,91 +106,66 @@ export const getUsers = query('unchecked', async (options) => {
159
106
  });
160
107
  if (options.query)
161
108
  params.append('query', options.query);
162
- // Fetch users and count in parallel
163
109
  const [usersData, countData] = await Promise.all([
164
110
  makeClerkRequest(`/users?${params}`),
165
111
  makeClerkRequest(`/users/count?${params}`)
166
112
  ]);
167
- console.log(`✅ [getUsers] Fetched ${usersData.length} users, total: ${countData.total_count}`);
168
113
  return {
169
114
  users: usersData,
170
115
  totalUsers: countData.total_count || 0
171
116
  };
172
117
  }
173
118
  catch (error) {
174
- console.error('[getUsers] Error:', error);
119
+ console.error('[getUsers] Error:', error);
175
120
  throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
176
121
  }
177
122
  });
178
- /**
179
- * Create a new user
180
- *
181
- * Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
182
- */
183
123
  export const createUser = command('unchecked', async (userData) => {
184
- // Transform User to Clerk API format
185
124
  const emailAddress = userData.email_addresses?.[0]?.email_address || '';
186
125
  if (!emailAddress) {
187
126
  throw new Error('Email address is required');
188
127
  }
189
- console.log('📝 [createUser] Creating new user:', emailAddress);
190
128
  try {
191
- // Extract permissions from userData
192
129
  const { permissions, ...userDataOnly } = userData;
193
- // Transform userData to match Clerk Backend API format (snake_case)
194
130
  const clerkUserData = {
195
131
  first_name: userDataOnly.first_name || '',
196
132
  last_name: userDataOnly.last_name || '',
197
- email_address: [emailAddress], // Clerk expects an array
133
+ email_address: [emailAddress],
198
134
  ...(userDataOnly.username && { username: userDataOnly.username }),
199
135
  ...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
200
136
  };
201
- // Step 1: Create user in Clerk
202
- console.log('📝 [createUser] Creating user in Clerk...');
203
137
  let result = await makeClerkRequest('/users', {
204
138
  method: 'POST',
205
139
  body: JSON.stringify(clerkUserData)
206
140
  });
207
- // Step 1.5: Add user to organization if configured
208
141
  if (ORGANIZATION_ID) {
209
142
  try {
210
- console.log(`🏢 [createUser] Adding user ${result.id} to organization ${ORGANIZATION_ID}...`);
211
143
  await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
212
144
  method: 'POST',
213
145
  body: JSON.stringify({
214
146
  user_id: result.id,
215
- role: 'org:member' // Standard Clerk organization role
147
+ role: 'org:member'
216
148
  })
217
149
  });
218
- console.log(`✅ [createUser] User added to organization successfully`);
219
150
  }
220
151
  catch (orgError) {
221
- console.error(`❌ [createUser] Failed to add user to organization:`, orgError);
222
- // Clean up: delete the user since they can't be added to the org
152
+ console.error('[createUser] Failed to add user to organization:', orgError);
223
153
  try {
224
154
  await makeClerkRequest(`/users/${result.id}`, {
225
155
  method: 'DELETE'
226
156
  });
227
- console.log(`🗑️ [createUser] User ${result.id} deleted due to org membership failure`);
228
157
  }
229
158
  catch (deleteError) {
230
- console.error(`❌ [createUser] Failed to delete user after org membership failure:`, deleteError);
159
+ console.error('[createUser] Failed to delete user after org membership failure:', deleteError);
231
160
  }
232
161
  throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
233
162
  }
234
163
  }
235
- else {
236
- console.warn('⚠️ [createUser] ALLOWED_ORG_ID not configured, skipping organization membership');
237
- }
238
- // Step 2: Create admin key with permissions if permissions are provided
239
164
  if (permissions && permissions.length > 0) {
240
165
  try {
241
- console.log(`🔑 [createUser] Creating permissions for new user: ${result.id}`);
242
166
  const adminKeyResult = await createUserPermissions(result.id, permissions);
243
- // Extract the API key from the admin service response
244
167
  const apiKey = adminKeyResult?.data?.key;
245
168
  if (adminKeyResult && apiKey) {
246
- // Update user's private metadata with the API key
247
169
  const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
248
170
  method: 'PATCH',
249
171
  body: JSON.stringify({
@@ -253,60 +175,34 @@ export const createUser = command('unchecked', async (userData) => {
253
175
  }
254
176
  })
255
177
  });
256
- // Update the result with the updated user data
257
178
  result = updatedUser;
258
- console.log(`✅ [createUser] API key stored in user's private metadata`);
259
179
  }
260
- else {
261
- console.warn(`⚠️ [createUser] No API key found in admin key result`);
262
- }
263
- // Add permission info to result
264
180
  result.adminKey = adminKeyResult;
265
181
  result.permissionsAssigned = permissions;
266
- console.log(`✅ [createUser] User ${result.id} created with permissions successfully`);
267
182
  }
268
183
  catch (permissionError) {
269
- console.error(`❌ [createUser] Failed to assign permissions:`, permissionError);
270
- // Don't fail the entire operation, but warn
184
+ console.error('[createUser] Failed to assign permissions:', permissionError);
271
185
  result.warning = 'User created but permissions assignment failed';
272
186
  result.permissionError =
273
187
  permissionError instanceof Error ? permissionError.message : String(permissionError);
274
188
  }
275
189
  }
276
- else {
277
- console.log(`⚠️ [createUser] User ${result.id} created without permissions`);
278
- }
279
190
  return result;
280
191
  }
281
192
  catch (error) {
282
- console.error('[createUser] Error:', error);
283
- // Handle Clerk API errors with detailed information
284
- if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
285
- const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
286
- enrichedError.status = error.status;
287
- enrichedError.details = error.details;
288
- enrichedError.clerkError = true;
289
- throw enrichedError;
290
- }
291
- throw new Error(`Failed to create user: ${error instanceof Error ? error.message : 'Unknown error'}`);
193
+ console.error('[createUser] Error:', error);
194
+ handleClerkError(error, 'Failed to create user');
292
195
  }
293
196
  });
294
- /**
295
- * Update an existing user
296
- */
297
197
  export const updateUser = command('unchecked', async (options) => {
298
198
  const { userId, userData } = options;
299
- console.log(`📝 [updateUser] Updating user ${userId}`);
300
199
  try {
301
- // Transform User to Clerk API format
302
- // Only include fields that Clerk accepts
303
200
  const updateData = {};
304
201
  if (userData.first_name !== undefined)
305
202
  updateData.first_name = userData.first_name;
306
203
  if (userData.last_name !== undefined)
307
204
  updateData.last_name = userData.last_name;
308
205
  if (userData.username !== undefined && userData.username !== '') {
309
- // Only include username if it's not empty (prevents "username already exists" error)
310
206
  updateData.username = userData.username;
311
207
  }
312
208
  if (userData.private_metadata !== undefined) {
@@ -316,79 +212,49 @@ export const updateUser = command('unchecked', async (options) => {
316
212
  method: 'PATCH',
317
213
  body: JSON.stringify(updateData)
318
214
  });
319
- // If permissions changed, update them separately
320
215
  if (userData.permissions !== undefined) {
321
216
  try {
322
217
  await updateUserPermissions({ userId, permissions: userData.permissions });
323
218
  }
324
219
  catch (permError) {
325
- console.error(`❌ [updateUser] Failed to update permissions:`, permError);
326
- // Don't fail the entire operation
220
+ console.error('[updateUser] Failed to update permissions:', permError);
327
221
  }
328
222
  }
329
- console.log(`✅ [updateUser] User ${userId} updated successfully`);
330
223
  return result;
331
224
  }
332
225
  catch (error) {
333
- console.error('[updateUser] Error:', error);
334
- // Handle Clerk API errors with detailed information
335
- if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
336
- const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
337
- enrichedError.status = error.status;
338
- enrichedError.details = error.details;
339
- enrichedError.clerkError = true;
340
- throw enrichedError;
341
- }
342
- throw new Error(`Failed to update user: ${error instanceof Error ? error.message : 'Unknown error'}`);
226
+ console.error('[updateUser] Error:', error);
227
+ handleClerkError(error, 'Failed to update user');
343
228
  }
344
229
  });
345
- /**
346
- * Delete a single user
347
- */
348
230
  export const deleteUser = command('unchecked', async (userId) => {
349
- console.log(`🗑️ [deleteUser] Deleting user ${userId}`);
350
231
  try {
351
232
  await makeClerkRequest(`/users/${userId}`, {
352
233
  method: 'DELETE'
353
234
  });
354
- console.log(`✅ [deleteUser] User ${userId} deleted successfully`);
355
235
  }
356
236
  catch (error) {
357
- console.error('[deleteUser] Error:', error);
237
+ console.error('[deleteUser] Error:', error);
358
238
  throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
359
239
  }
360
240
  });
361
- /**
362
- * Delete multiple users
363
- */
364
241
  export const deleteUsers = command('unchecked', async (userIds) => {
365
- console.log(`🗑️ [deleteUsers] Deleting ${userIds.length} users`);
366
242
  try {
367
243
  await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
368
244
  method: 'DELETE'
369
245
  })));
370
- console.log(`✅ [deleteUsers] ${userIds.length} users deleted successfully`);
371
246
  }
372
247
  catch (error) {
373
- console.error('[deleteUsers] Error:', error);
248
+ console.error('[deleteUsers] Error:', error);
374
249
  throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
375
250
  }
376
251
  });
377
- /**
378
- * Helper: Fetch permissions for a single user
379
- */
380
252
  async function fetchUserPermissions(userId) {
381
- console.log(`🔍 [fetchUserPermissions] Fetching permissions for user: ${userId}`);
382
- // Try direct lookup first using client_id and sub (userId)
383
253
  try {
384
254
  const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
385
- console.log(`✅ [fetchUserPermissions] Direct lookup successful for user ${userId}`);
386
- // Filter the response to only include active keys
387
255
  if (userData?.data?.data && Array.isArray(userData.data.data)) {
388
256
  userData.data.data = userData.data.data.filter((key) => key.status === 'active');
389
- console.log(`🔍 [fetchUserPermissions] Filtered to ${userData.data.data.length} active key(s)`);
390
257
  }
391
- // Extract scopes from the response
392
258
  if (userData?.data?.data && Array.isArray(userData.data.data)) {
393
259
  return userData.data.data.flatMap((key) => key.scopes || []);
394
260
  }
@@ -398,48 +264,28 @@ async function fetchUserPermissions(userId) {
398
264
  return [];
399
265
  }
400
266
  catch {
401
- console.log(`❌ [fetchUserPermissions] Direct lookup failed, trying search by sub field`);
402
- // If direct lookup fails with 404, search all keys by sub field
403
267
  try {
404
268
  const allKeysData = await makeAdminRequest('/admin/keys');
405
- console.log(`🔍 [fetchUserPermissions] Searching through ${allKeysData.data?.data?.length || 0} keys`);
406
- // Find the ACTIVE key for this user (ignore revoked keys)
407
- // Match by sub (userId) and client_id
408
269
  const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
409
270
  if (userKey) {
410
- console.log(`✅ [fetchUserPermissions] Found active user key by sub field`);
411
271
  return Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
412
272
  }
413
- else {
414
- console.log(`❌ [fetchUserPermissions] No user found, returning empty permissions`);
415
- return [];
416
- }
273
+ return [];
417
274
  }
418
275
  catch (searchError) {
419
- console.error('[fetchUserPermissions] Error searching for user by sub:', searchError);
276
+ console.error('[fetchUserPermissions] Error searching for user by sub:', searchError);
420
277
  throw new Error('Failed to fetch user permissions');
421
278
  }
422
279
  }
423
280
  }
424
- /**
425
- * Get permissions for specific users (batched)
426
- *
427
- * Uses 'sub' (userId) parameter in admin API calls.
428
- * Batches multiple permission requests to avoid n+1 problem.
429
- */
430
281
  export const getUserPermissions = query.batch('unchecked', async (userIds) => {
431
- console.log(`🔍 [getUserPermissions] Batch fetching permissions for ${userIds.length} users`);
432
282
  try {
433
- // Fetch all permissions in parallel
434
283
  const permissionPromises = userIds.map((userId) => fetchUserPermissions(userId));
435
284
  const permissionsResults = await Promise.all(permissionPromises);
436
- // Create a lookup map for O(1) access
437
285
  const lookup = new Map();
438
286
  userIds.forEach((userId, index) => {
439
287
  lookup.set(userId, permissionsResults[index]);
440
288
  });
441
- console.log(`✅ [getUserPermissions] Batch fetch completed for ${userIds.length} users`);
442
- // Return a function that SvelteKit will call for each individual request
443
289
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
444
290
  return (userId, _index) => {
445
291
  const permissions = lookup.get(userId) || [];
@@ -447,120 +293,122 @@ export const getUserPermissions = query.batch('unchecked', async (userIds) => {
447
293
  };
448
294
  }
449
295
  catch (error) {
450
- console.error('[getUserPermissions] Batch error:', error);
451
- // Return a function that throws for all requests
296
+ console.error('[getUserPermissions] Batch error:', error);
452
297
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
453
298
  return (_userId, _index) => {
454
299
  throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
455
300
  };
456
301
  }
457
302
  });
458
- /**
459
- * Update permissions for a specific user
460
- *
461
- * Uses 'sub' (userId) parameter in admin API calls
462
- */
303
+ async function refreshTokenIfSelfUpdate(userId) {
304
+ try {
305
+ const event = getRequestEvent();
306
+ if (!event?.locals)
307
+ return;
308
+ // Try to get current user - this is app-specific, so we check if the method exists
309
+ const getAuth = event.locals.auth;
310
+ if (!getAuth)
311
+ return;
312
+ const currentUser = getAuth();
313
+ const isSelfUpdate = currentUser?.userId === userId;
314
+ if (isSelfUpdate) {
315
+ try {
316
+ // Try to dynamically import token manager - this is app-specific
317
+ // Use Function constructor to avoid TypeScript checking the import path
318
+ const importPath = '$lib/server/auth-hooks/token-manager.server';
319
+ const tokenManager = await new Function('path', 'return import(path)')(importPath).catch(() => null);
320
+ if (tokenManager?.clearTokenCookies &&
321
+ tokenManager?.getAccessToken &&
322
+ tokenManager?.setClientAccessibleToken) {
323
+ tokenManager.clearTokenCookies(event.cookies);
324
+ const newToken = await tokenManager.getAccessToken(event.cookies, event, true);
325
+ tokenManager.setClientAccessibleToken(event.cookies, newToken);
326
+ }
327
+ }
328
+ catch {
329
+ // Token refresh not available in this app, skip silently
330
+ }
331
+ }
332
+ }
333
+ catch {
334
+ // Token refresh not available in this app, skip silently
335
+ }
336
+ }
463
337
  export const updateUserPermissions = command('unchecked', async (options) => {
464
338
  const { userId, permissions } = options;
465
- console.log(`🔑 [updateUserPermissions] Updating permissions for user ${userId}`);
466
339
  try {
467
- let adminKeyId = userId; // Use userId as default key ID
468
- // Try direct update first
340
+ let adminKeyId = userId;
469
341
  try {
470
342
  await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
471
343
  method: 'PUT',
472
344
  body: JSON.stringify({ scopes: permissions })
473
345
  });
474
- console.log(`✅ [updateUserPermissions] Permissions updated successfully`);
346
+ await refreshTokenIfSelfUpdate(userId);
475
347
  return;
476
348
  }
477
349
  catch {
478
- // If direct update fails, try to find the correct adminKeyId
479
- console.log(`⚠️ [updateUserPermissions] Direct update failed, searching for key ID...`);
480
350
  try {
481
351
  const allKeysData = await makeAdminRequest('/admin/keys');
482
- // Find the ACTIVE key for this user (ignore revoked keys)
483
- // Match by sub (userId) and client_id
484
352
  const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
485
353
  if (userKey) {
486
- // Use the found key ID for update
487
354
  adminKeyId = userKey.id;
488
355
  await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
489
356
  method: 'PUT',
490
357
  body: JSON.stringify({ scopes: permissions })
491
358
  });
492
- console.log(`✅ [updateUserPermissions] Permissions updated successfully (after search)`);
359
+ await refreshTokenIfSelfUpdate(userId);
493
360
  return;
494
361
  }
495
362
  else {
496
- // User doesn't exist, create new admin key
497
- console.log(`📝 [updateUserPermissions] Creating new admin key...`);
498
363
  await createUserPermissions(userId, permissions);
499
- console.log(`✅ [updateUserPermissions] New admin key created successfully`);
364
+ await refreshTokenIfSelfUpdate(userId);
500
365
  return;
501
366
  }
502
367
  }
503
368
  catch (searchError) {
504
- console.error('[updateUserPermissions] Error during permission update:', searchError);
369
+ console.error('[updateUserPermissions] Error during permission update:', searchError);
505
370
  throw new Error('Failed to update permissions');
506
371
  }
507
372
  }
508
373
  }
509
374
  catch (error) {
510
- console.error('[updateUserPermissions] Error:', error);
375
+ console.error('[updateUserPermissions] Error:', error);
511
376
  throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
512
377
  }
513
378
  });
514
- /**
515
- * Generate new API key for a user with optional old key revocation
516
- */
517
379
  export const generateApiKey = command('unchecked', async (options) => {
518
- console.log(`🔑 [generateApiKey] Generating new API key for user ${options.userId}`);
519
380
  try {
520
- // Filter permissions by prefix if configured
521
381
  const filteredPermissions = PERMISSION_PREFIX
522
382
  ? options.permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
523
383
  : options.permissions;
524
384
  if (filteredPermissions.length === 0) {
525
385
  throw new Error(`At least one ${PERMISSION_PREFIX || ''} permission is required to generate an API key`);
526
386
  }
527
- // If revokeOld is true, find and revoke the old key first
528
387
  let oldKeyId = null;
529
388
  if (options.revokeOld) {
530
389
  try {
531
- console.log(`🔍 [generateApiKey] Looking for existing active key to revoke`);
532
390
  const allKeysData = await makeAdminRequest('/admin/keys');
533
- if (!allKeysData?.data?.data || !Array.isArray(allKeysData.data.data)) {
534
- console.warn('⚠️ [generateApiKey] Unexpected response structure from /admin/keys');
535
- }
536
- else {
537
- // Find the ACTIVE key for this user (ignore already revoked keys)
391
+ if (allKeysData?.data?.data && Array.isArray(allKeysData.data.data)) {
538
392
  const userKey = allKeysData.data.data.find((key) => key.sub === options.userId && key.client_id === CLIENT_ID && key.status === 'active');
539
393
  if (userKey) {
540
394
  oldKeyId = userKey.id;
541
- console.log(`📌 [generateApiKey] Found existing active key: ${oldKeyId}`);
542
395
  }
543
396
  }
544
397
  }
545
398
  catch (e) {
546
- console.warn('⚠️ [generateApiKey] Could not fetch existing key for revocation:', e);
547
- // Continue anyway - not critical
399
+ console.warn('[generateApiKey] Could not fetch existing key for revocation:', e);
548
400
  }
549
401
  }
550
- // Create new admin key
551
402
  const createData = await createUserPermissions(options.userId, filteredPermissions);
552
403
  if (!createData) {
553
404
  throw new Error('Failed to create admin key');
554
405
  }
555
406
  const newApiKey = createData?.data?.key;
556
407
  if (!newApiKey) {
557
- console.error('[generateApiKey] No API key in response:', createData);
408
+ console.error('[generateApiKey] No API key in response:', createData);
558
409
  throw new Error('Failed to generate API key - no key in response');
559
410
  }
560
- console.log(`✅ [generateApiKey] New API key generated successfully`);
561
- // Update user's Clerk profile with the new API key
562
411
  try {
563
- // First, get the current user data to preserve existing private_metadata
564
412
  const currentUser = await makeClerkRequest(`/users/${options.userId}`);
565
413
  await makeClerkRequest(`/users/${options.userId}`, {
566
414
  method: 'PATCH',
@@ -571,24 +419,20 @@ export const generateApiKey = command('unchecked', async (options) => {
571
419
  }
572
420
  })
573
421
  });
574
- console.log(`✅ [generateApiKey] API key stored in user's Clerk profile`);
575
422
  }
576
423
  catch (clerkError) {
577
- console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
578
- console.warn('⚠️ [generateApiKey] Key generated but could not update Clerk profile');
424
+ console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
425
+ console.warn('[generateApiKey] Key generated but could not update Clerk profile');
579
426
  }
580
- // Revoke old key if it exists
581
427
  if (oldKeyId) {
582
428
  try {
583
- console.log(`🗑️ [generateApiKey] Revoking old key: ${oldKeyId}`);
584
429
  await makeAdminRequest(`/admin/keys/${oldKeyId}`, {
585
430
  method: 'DELETE'
586
431
  });
587
- console.log(`✅ [generateApiKey] Old key revoked successfully`);
588
432
  }
589
433
  catch (revokeError) {
590
- console.error('[generateApiKey] Failed to revoke old key:', revokeError);
591
- console.warn('⚠️ [generateApiKey] New key generated but could not revoke old key');
434
+ console.error('[generateApiKey] Failed to revoke old key:', revokeError);
435
+ console.warn('[generateApiKey] New key generated but could not revoke old key');
592
436
  }
593
437
  }
594
438
  return {
@@ -600,7 +444,7 @@ export const generateApiKey = command('unchecked', async (options) => {
600
444
  };
601
445
  }
602
446
  catch (error) {
603
- console.error('[generateApiKey] Error:', error);
447
+ console.error('[generateApiKey] Error:', error);
604
448
  throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
605
449
  }
606
450
  });
@@ -7,4 +7,5 @@
7
7
  * @see https://svelte.dev/docs/kit/remote-functions
8
8
  */
9
9
  export type { GetUsersOptions, GetUsersResult } from './types.js';
10
- export * from './mockUserManagement.js';
10
+ export * from './UserManagement.remote.js';
11
+ export * as MockUserManagement from './mockUserManagement.js';
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @see https://svelte.dev/docs/kit/remote-functions
8
8
  */
9
- // Export mock adapter functions for testing/storybook
10
- export * from './mockUserManagement.js';
11
- // Note: UserManagement.remote.ts is a template file showing how to implement
12
- // remote functions in your SvelteKit app. It is not exported from this library.
9
+ // Export default remote functions implementation
10
+ export * from './UserManagement.remote.js';
11
+ // Export mock adapter functions for testing/storybook (with different names to avoid conflicts)
12
+ export * as MockUserManagement from './mockUserManagement.js';
@@ -110,6 +110,7 @@ export interface UserManagementProps {
110
110
  /**
111
111
  * Adapter module containing remote functions or async functions
112
112
  * Should be imported from a .remote.ts file
113
+ * If not provided, uses the default adapter from @makolabs/ripple
113
114
  *
114
115
  * Example:
115
116
  * ```ts
@@ -117,7 +118,7 @@ export interface UserManagementProps {
117
118
  * <UserManagement adapter={adapter} roles={roles} />
118
119
  * ```
119
120
  */
120
- adapter: UserManagementAdapter;
121
+ adapter?: UserManagementAdapter;
121
122
  /**
122
123
  * Available roles for user assignment
123
124
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {