@makolabs/ripple 1.2.9 → 1.2.11

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.
@@ -263,6 +263,7 @@
263
263
  bind:open={showEditCreateModal}
264
264
  bind:user={selectedUser}
265
265
  {roles}
266
+ {adapter}
266
267
  onSave={handleUserSave}
267
268
  onClose={() => (selectedUser = null)}
268
269
  />
@@ -8,6 +8,7 @@
8
8
  open = $bindable(),
9
9
  user = $bindable(),
10
10
  roles = [],
11
+ adapter,
11
12
  onSave,
12
13
  onClose,
13
14
  class: className
@@ -20,6 +21,8 @@
20
21
  let formErrors = $state<FormErrors>({});
21
22
  let saving = $state(false);
22
23
  let formElement = $state<HTMLFormElement | null>(null);
24
+ let showApiKey = $state(false);
25
+ let regeneratingApiKey = $state(false);
23
26
 
24
27
  // Form data
25
28
  let formData = $state<Partial<User>>({
@@ -165,10 +168,51 @@
165
168
  }
166
169
  }
167
170
 
171
+ async function handleRegenerateApiKey() {
172
+ if (!user?.id || !adapter?.generateApiKey || !formData.permissions) return;
173
+
174
+ try {
175
+ regeneratingApiKey = true;
176
+ const result = await adapter.generateApiKey({
177
+ userId: user.id,
178
+ permissions: formData.permissions,
179
+ revokeOld: true
180
+ });
181
+
182
+ // Update user's private_metadata with new API key
183
+ if (user && result.apiKey) {
184
+ user = {
185
+ ...user,
186
+ private_metadata: {
187
+ ...(user.private_metadata || {}),
188
+ mako_api_key: result.apiKey
189
+ }
190
+ };
191
+ }
192
+ } catch (error) {
193
+ console.error('Error regenerating API key:', error);
194
+ formErrors.apiKey = error instanceof Error ? error.message : 'Failed to regenerate API key';
195
+ } finally {
196
+ regeneratingApiKey = false;
197
+ }
198
+ }
199
+
168
200
  function getModalTitle() {
169
201
  if (mode === 'create') return 'Create New User';
170
- return `Edit ${getUserDisplayName(user)}`;
202
+ return `Edit ${getUserDisplayName(user ?? null)}`;
171
203
  }
204
+
205
+ // Get API key from user's private_metadata
206
+ const apiKey = $derived(
207
+ user?.private_metadata &&
208
+ typeof user.private_metadata === 'object' &&
209
+ 'mako_api_key' in user.private_metadata
210
+ ? (user.private_metadata.mako_api_key as string) || ''
211
+ : ''
212
+ );
213
+
214
+ // Mask API key for display
215
+ const maskedApiKey = $derived(apiKey ? '•'.repeat(Math.min(apiKey.length, 40)) : '');
172
216
  </script>
173
217
 
174
218
  <Modal
@@ -244,6 +288,87 @@
244
288
  <p class="mt-1 text-xs text-red-500">{formErrors.email}</p>
245
289
  {/if}
246
290
  </div>
291
+
292
+ <!-- Mako API Key (Edit mode only) -->
293
+ {#if mode === 'edit' && (apiKey || adapter?.generateApiKey)}
294
+ <div>
295
+ <label for="api-key" class="mb-1 block text-sm font-medium text-gray-700">
296
+ Mako API Key
297
+ </label>
298
+ <div class="flex gap-2">
299
+ <div class="relative flex-1">
300
+ <input
301
+ id="api-key"
302
+ type={showApiKey ? 'text' : 'password'}
303
+ value={showApiKey ? apiKey : maskedApiKey}
304
+ readonly
305
+ class="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 pr-10 font-mono text-sm"
306
+ placeholder="No API key generated"
307
+ />
308
+ <button
309
+ type="button"
310
+ onclick={() => (showApiKey = !showApiKey)}
311
+ class="absolute top-1/2 right-2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
312
+ aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
313
+ >
314
+ {#if showApiKey}
315
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
316
+ <path
317
+ stroke-linecap="round"
318
+ stroke-linejoin="round"
319
+ stroke-width="2"
320
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.29 3.29m0 0A9.966 9.966 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
321
+ ></path>
322
+ </svg>
323
+ {:else}
324
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
325
+ <path
326
+ stroke-linecap="round"
327
+ stroke-linejoin="round"
328
+ stroke-width="2"
329
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
330
+ ></path>
331
+ <path
332
+ stroke-linecap="round"
333
+ stroke-linejoin="round"
334
+ stroke-width="2"
335
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
336
+ ></path>
337
+ </svg>
338
+ {/if}
339
+ </button>
340
+ </div>
341
+ {#if adapter?.generateApiKey}
342
+ <Button
343
+ type="button"
344
+ variant="outline"
345
+ onclick={handleRegenerateApiKey}
346
+ disabled={regeneratingApiKey ||
347
+ !formData.permissions ||
348
+ formData.permissions.length === 0}
349
+ isLoading={regeneratingApiKey}
350
+ >
351
+ <svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
352
+ <path
353
+ stroke-linecap="round"
354
+ stroke-linejoin="round"
355
+ stroke-width="2"
356
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
357
+ ></path>
358
+ </svg>
359
+ Regenerate Key
360
+ </Button>
361
+ {/if}
362
+ </div>
363
+ {#if formErrors.apiKey}
364
+ <p class="mt-1 text-xs text-red-500">{formErrors.apiKey}</p>
365
+ {:else}
366
+ <p class="mt-1 text-xs text-gray-500">
367
+ API keys are system-managed and cannot be manually edited
368
+ </p>
369
+ {/if}
370
+ </div>
371
+ {/if}
247
372
  </div>
248
373
 
249
374
  <!-- Right Column: Permissions & Role -->
@@ -1,68 +1,24 @@
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 a specific user
56
- *
57
- * Uses 'sub' (userId) parameter in admin API calls
58
- */
59
- export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, string[]>;
60
- /**
61
- * Update permissions for a specific user
62
- *
63
- * Uses 'sub' (userId) parameter in admin API calls
64
- */
11
+ export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, Promise<string[]>>;
65
12
  export declare const updateUserPermissions: import("@sveltejs/kit").RemoteCommand<{
66
13
  userId: string;
67
14
  permissions: string[];
68
15
  }, Promise<void>>;
16
+ export declare const generateApiKey: import("@sveltejs/kit").RemoteCommand<{
17
+ userId: string;
18
+ permissions: string[];
19
+ revokeOld?: boolean;
20
+ }, Promise<{
21
+ success: boolean;
22
+ apiKey: string;
23
+ message: string;
24
+ }>>;
@@ -1,46 +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 { query, command } from '$app/server';
26
2
  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
3
+ const CLIENT_ID = env.CLIENT_ID || 'sharkfin';
38
4
  const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
39
- // Organization ID for adding users to organizations
40
5
  const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
41
- /**
42
- * Helper: Make authenticated request to Clerk API
43
- */
6
+ function handleClerkError(error, defaultMessage) {
7
+ if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
8
+ const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
9
+ enrichedError.status = error.status;
10
+ enrichedError.details = error.details;
11
+ enrichedError.clerkError = true;
12
+ throw enrichedError;
13
+ }
14
+ throw new Error(`${defaultMessage}: ${error instanceof Error ? error.message : 'Unknown error'}`);
15
+ }
44
16
  async function makeClerkRequest(endpoint, options = {}) {
45
17
  const CLERK_SECRET_KEY = env.CLERK_SECRET_KEY;
46
18
  if (!CLERK_SECRET_KEY) {
@@ -56,8 +28,7 @@ async function makeClerkRequest(endpoint, options = {}) {
56
28
  });
57
29
  if (!response.ok) {
58
30
  const errorText = await response.text();
59
- console.error(`❌ [Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
60
- // Try to parse error details
31
+ console.error(`[Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
61
32
  let errorDetails;
62
33
  try {
63
34
  errorDetails = JSON.parse(errorText);
@@ -65,7 +36,6 @@ async function makeClerkRequest(endpoint, options = {}) {
65
36
  catch {
66
37
  errorDetails = { message: errorText || `${response.status} ${response.statusText}` };
67
38
  }
68
- // Throw error with status and details for better frontend handling
69
39
  const error = new Error(errorDetails.message || `Clerk API request failed: ${response.status} ${response.statusText}`);
70
40
  error.status = response.status;
71
41
  error.details = errorDetails;
@@ -73,9 +43,6 @@ async function makeClerkRequest(endpoint, options = {}) {
73
43
  }
74
44
  return response.json();
75
45
  }
76
- /**
77
- * Helper: Make authenticated request to Admin API
78
- */
79
46
  async function makeAdminRequest(endpoint, options = {}) {
80
47
  const ADMIN_API_KEY = env.ADMIN_API_KEY;
81
48
  const PRIVATE_BASE_AUTH_URL = env.PRIVATE_BASE_AUTH_URL;
@@ -98,60 +65,39 @@ async function makeAdminRequest(endpoint, options = {}) {
98
65
  });
99
66
  if (!response.ok) {
100
67
  const errorText = await response.text();
101
- console.error(`❌ [Admin API] ${response.status} ${response.statusText} - ${errorText}`);
68
+ console.error(`[Admin API] ${response.status} ${response.statusText} - ${errorText}`);
102
69
  throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
103
70
  }
104
71
  return response.json();
105
72
  }
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
73
  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
74
  const filteredPermissions = PERMISSION_PREFIX
117
75
  ? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
118
76
  : permissions;
119
77
  if (filteredPermissions.length === 0) {
120
- console.log(`⚠️ [createUserPermissions] No ${PERMISSION_PREFIX || ''} permissions provided, skipping`);
121
78
  return null;
122
79
  }
123
- const createData = await makeAdminRequest('/admin/keys', {
80
+ return await makeAdminRequest('/admin/keys', {
124
81
  method: 'POST',
125
82
  body: JSON.stringify({
126
83
  client_id: clientId,
127
- sub: userId, // userId is used as 'sub' parameter
84
+ sub: userId,
128
85
  scopes: filteredPermissions
129
86
  })
130
87
  });
131
- console.log(`✅ [createUserPermissions] Admin key created successfully for user ${userId}`);
132
- return createData;
133
88
  }
134
- /**
135
- * Get users with pagination and sorting
136
- *
137
- * Transforms adapter options to Clerk API format
138
- */
139
89
  export const getUsers = query('unchecked', async (options) => {
140
- console.log('🔍 [getUsers] Fetching users with options:', options);
141
90
  try {
142
- // Transform adapter options to Clerk API format
143
91
  const limit = options.pageSize;
144
92
  const offset = (options.page - 1) * options.pageSize;
145
- // Build orderBy string
146
93
  let orderBy = '';
147
94
  if (options.sortBy) {
148
95
  const prefix = options.sortOrder === 'desc' ? '-' : '';
149
96
  orderBy = `${prefix}${options.sortBy}`;
150
97
  }
151
98
  else {
152
- orderBy = '-created_at'; // Default
99
+ orderBy = '-created_at';
153
100
  }
154
- // Build Clerk API parameters
155
101
  const params = new URLSearchParams({
156
102
  limit: limit.toString(),
157
103
  offset: offset.toString(),
@@ -159,91 +105,66 @@ export const getUsers = query('unchecked', async (options) => {
159
105
  });
160
106
  if (options.query)
161
107
  params.append('query', options.query);
162
- // Fetch users and count in parallel
163
108
  const [usersData, countData] = await Promise.all([
164
109
  makeClerkRequest(`/users?${params}`),
165
110
  makeClerkRequest(`/users/count?${params}`)
166
111
  ]);
167
- console.log(`✅ [getUsers] Fetched ${usersData.length} users, total: ${countData.total_count}`);
168
112
  return {
169
113
  users: usersData,
170
114
  totalUsers: countData.total_count || 0
171
115
  };
172
116
  }
173
117
  catch (error) {
174
- console.error('[getUsers] Error:', error);
118
+ console.error('[getUsers] Error:', error);
175
119
  throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
176
120
  }
177
121
  });
178
- /**
179
- * Create a new user
180
- *
181
- * Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
182
- */
183
122
  export const createUser = command('unchecked', async (userData) => {
184
- // Transform User to Clerk API format
185
123
  const emailAddress = userData.email_addresses?.[0]?.email_address || '';
186
124
  if (!emailAddress) {
187
125
  throw new Error('Email address is required');
188
126
  }
189
- console.log('📝 [createUser] Creating new user:', emailAddress);
190
127
  try {
191
- // Extract permissions from userData
192
128
  const { permissions, ...userDataOnly } = userData;
193
- // Transform userData to match Clerk Backend API format (snake_case)
194
129
  const clerkUserData = {
195
130
  first_name: userDataOnly.first_name || '',
196
131
  last_name: userDataOnly.last_name || '',
197
- email_address: [emailAddress], // Clerk expects an array
132
+ email_address: [emailAddress],
198
133
  ...(userDataOnly.username && { username: userDataOnly.username }),
199
134
  ...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
200
135
  };
201
- // Step 1: Create user in Clerk
202
- console.log('📝 [createUser] Creating user in Clerk...');
203
136
  let result = await makeClerkRequest('/users', {
204
137
  method: 'POST',
205
138
  body: JSON.stringify(clerkUserData)
206
139
  });
207
- // Step 1.5: Add user to organization if configured
208
140
  if (ORGANIZATION_ID) {
209
141
  try {
210
- console.log(`🏢 [createUser] Adding user ${result.id} to organization ${ORGANIZATION_ID}...`);
211
142
  await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
212
143
  method: 'POST',
213
144
  body: JSON.stringify({
214
145
  user_id: result.id,
215
- role: 'org:member' // Standard Clerk organization role
146
+ role: 'org:member'
216
147
  })
217
148
  });
218
- console.log(`✅ [createUser] User added to organization successfully`);
219
149
  }
220
150
  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
151
+ console.error('[createUser] Failed to add user to organization:', orgError);
223
152
  try {
224
153
  await makeClerkRequest(`/users/${result.id}`, {
225
154
  method: 'DELETE'
226
155
  });
227
- console.log(`🗑️ [createUser] User ${result.id} deleted due to org membership failure`);
228
156
  }
229
157
  catch (deleteError) {
230
- console.error(`❌ [createUser] Failed to delete user after org membership failure:`, deleteError);
158
+ console.error('[createUser] Failed to delete user after org membership failure:', deleteError);
231
159
  }
232
160
  throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
233
161
  }
234
162
  }
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
163
  if (permissions && permissions.length > 0) {
240
164
  try {
241
- console.log(`🔑 [createUser] Creating permissions for new user: ${result.id}`);
242
165
  const adminKeyResult = await createUserPermissions(result.id, permissions);
243
- // Extract the API key from the admin service response
244
166
  const apiKey = adminKeyResult?.data?.key;
245
167
  if (adminKeyResult && apiKey) {
246
- // Update user's private metadata with the API key
247
168
  const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
248
169
  method: 'PATCH',
249
170
  body: JSON.stringify({
@@ -253,60 +174,34 @@ export const createUser = command('unchecked', async (userData) => {
253
174
  }
254
175
  })
255
176
  });
256
- // Update the result with the updated user data
257
177
  result = updatedUser;
258
- console.log(`✅ [createUser] API key stored in user's private metadata`);
259
178
  }
260
- else {
261
- console.warn(`⚠️ [createUser] No API key found in admin key result`);
262
- }
263
- // Add permission info to result
264
179
  result.adminKey = adminKeyResult;
265
180
  result.permissionsAssigned = permissions;
266
- console.log(`✅ [createUser] User ${result.id} created with permissions successfully`);
267
181
  }
268
182
  catch (permissionError) {
269
- console.error(`❌ [createUser] Failed to assign permissions:`, permissionError);
270
- // Don't fail the entire operation, but warn
183
+ console.error('[createUser] Failed to assign permissions:', permissionError);
271
184
  result.warning = 'User created but permissions assignment failed';
272
185
  result.permissionError =
273
186
  permissionError instanceof Error ? permissionError.message : String(permissionError);
274
187
  }
275
188
  }
276
- else {
277
- console.log(`⚠️ [createUser] User ${result.id} created without permissions`);
278
- }
279
189
  return result;
280
190
  }
281
191
  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'}`);
192
+ console.error('[createUser] Error:', error);
193
+ handleClerkError(error, 'Failed to create user');
292
194
  }
293
195
  });
294
- /**
295
- * Update an existing user
296
- */
297
196
  export const updateUser = command('unchecked', async (options) => {
298
197
  const { userId, userData } = options;
299
- console.log(`📝 [updateUser] Updating user ${userId}`);
300
198
  try {
301
- // Transform User to Clerk API format
302
- // Only include fields that Clerk accepts
303
199
  const updateData = {};
304
200
  if (userData.first_name !== undefined)
305
201
  updateData.first_name = userData.first_name;
306
202
  if (userData.last_name !== undefined)
307
203
  updateData.last_name = userData.last_name;
308
204
  if (userData.username !== undefined && userData.username !== '') {
309
- // Only include username if it's not empty (prevents "username already exists" error)
310
205
  updateData.username = userData.username;
311
206
  }
312
207
  if (userData.private_metadata !== undefined) {
@@ -316,172 +211,202 @@ export const updateUser = command('unchecked', async (options) => {
316
211
  method: 'PATCH',
317
212
  body: JSON.stringify(updateData)
318
213
  });
319
- // If permissions changed, update them separately
320
214
  if (userData.permissions !== undefined) {
321
215
  try {
322
216
  await updateUserPermissions({ userId, permissions: userData.permissions });
323
217
  }
324
218
  catch (permError) {
325
- console.error(`❌ [updateUser] Failed to update permissions:`, permError);
326
- // Don't fail the entire operation
219
+ console.error('[updateUser] Failed to update permissions:', permError);
327
220
  }
328
221
  }
329
- console.log(`✅ [updateUser] User ${userId} updated successfully`);
330
222
  return result;
331
223
  }
332
224
  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'}`);
225
+ console.error('[updateUser] Error:', error);
226
+ handleClerkError(error, 'Failed to update user');
343
227
  }
344
228
  });
345
- /**
346
- * Delete a single user
347
- */
348
229
  export const deleteUser = command('unchecked', async (userId) => {
349
- console.log(`🗑️ [deleteUser] Deleting user ${userId}`);
350
230
  try {
351
231
  await makeClerkRequest(`/users/${userId}`, {
352
232
  method: 'DELETE'
353
233
  });
354
- console.log(`✅ [deleteUser] User ${userId} deleted successfully`);
355
234
  }
356
235
  catch (error) {
357
- console.error('[deleteUser] Error:', error);
236
+ console.error('[deleteUser] Error:', error);
358
237
  throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
359
238
  }
360
239
  });
361
- /**
362
- * Delete multiple users
363
- */
364
240
  export const deleteUsers = command('unchecked', async (userIds) => {
365
- console.log(`🗑️ [deleteUsers] Deleting ${userIds.length} users`);
366
241
  try {
367
242
  await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
368
243
  method: 'DELETE'
369
244
  })));
370
- console.log(`✅ [deleteUsers] ${userIds.length} users deleted successfully`);
371
245
  }
372
246
  catch (error) {
373
- console.error('[deleteUsers] Error:', error);
247
+ console.error('[deleteUsers] Error:', error);
374
248
  throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
375
249
  }
376
250
  });
377
- /**
378
- * Get permissions for a specific user
379
- *
380
- * Uses 'sub' (userId) parameter in admin API calls
381
- */
382
- export const getUserPermissions = query('unchecked', async (userId) => {
383
- console.log(`🔍 [getUserPermissions] Fetching permissions for user: ${userId}`);
251
+ async function fetchUserPermissions(userId) {
384
252
  try {
385
- // Try direct lookup first using client_id and sub (userId)
253
+ const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
254
+ if (userData?.data?.data && Array.isArray(userData.data.data)) {
255
+ userData.data.data = userData.data.data.filter((key) => key.status === 'active');
256
+ }
257
+ if (userData?.data?.data && Array.isArray(userData.data.data)) {
258
+ return userData.data.data.flatMap((key) => key.scopes || []);
259
+ }
260
+ else if (userData?.scopes) {
261
+ return Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
262
+ }
263
+ return [];
264
+ }
265
+ catch {
386
266
  try {
387
- const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
388
- console.log(`✅ [getUserPermissions] Direct lookup successful for user ${userId}`);
389
- // Filter the response to only include active keys
390
- if (userData?.data?.data && Array.isArray(userData.data.data)) {
391
- userData.data.data = userData.data.data.filter((key) => key.status === 'active');
392
- console.log(`🔍 [getUserPermissions] Filtered to ${userData.data.data.length} active key(s)`);
393
- }
394
- // Extract scopes from the response
395
- if (userData?.data?.data && Array.isArray(userData.data.data)) {
396
- return userData.data.data.flatMap((key) => key.scopes || []);
397
- }
398
- else if (userData?.scopes) {
399
- return Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
267
+ const allKeysData = await makeAdminRequest('/admin/keys');
268
+ const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
269
+ if (userKey) {
270
+ return Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
400
271
  }
401
272
  return [];
402
273
  }
403
- catch {
404
- console.log(`❌ [getUserPermissions] Direct lookup failed, trying search by sub field`);
405
- // If direct lookup fails with 404, search all keys by sub field
406
- try {
407
- const allKeysData = await makeAdminRequest('/admin/keys');
408
- console.log(`🔍 [getUserPermissions] Searching through ${allKeysData.data?.data?.length || 0} keys`);
409
- // Find the ACTIVE key for this user (ignore revoked keys)
410
- // Match by sub (userId) and client_id
411
- const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
412
- if (userKey) {
413
- console.log(`✅ [getUserPermissions] Found active user key by sub field`);
414
- return Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
415
- }
416
- else {
417
- console.log(`❌ [getUserPermissions] No user found, returning empty permissions`);
418
- return [];
419
- }
420
- }
421
- catch (searchError) {
422
- console.error('❌ [getUserPermissions] Error searching for user by sub:', searchError);
423
- throw new Error('Failed to fetch user permissions');
424
- }
274
+ catch (searchError) {
275
+ console.error('[fetchUserPermissions] Error searching for user by sub:', searchError);
276
+ throw new Error('Failed to fetch user permissions');
425
277
  }
426
278
  }
279
+ }
280
+ export const getUserPermissions = query.batch('unchecked', async (userIds) => {
281
+ try {
282
+ const permissionPromises = userIds.map((userId) => fetchUserPermissions(userId));
283
+ const permissionsResults = await Promise.all(permissionPromises);
284
+ const lookup = new Map();
285
+ userIds.forEach((userId, index) => {
286
+ lookup.set(userId, permissionsResults[index]);
287
+ });
288
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
289
+ return (userId, _index) => {
290
+ const permissions = lookup.get(userId) || [];
291
+ return Promise.resolve(permissions);
292
+ };
293
+ }
427
294
  catch (error) {
428
- console.error('[getUserPermissions] Error:', error);
429
- throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
295
+ console.error('[getUserPermissions] Batch error:', error);
296
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
297
+ return (_userId, _index) => {
298
+ throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
299
+ };
430
300
  }
431
301
  });
432
- /**
433
- * Update permissions for a specific user
434
- *
435
- * Uses 'sub' (userId) parameter in admin API calls
436
- */
437
302
  export const updateUserPermissions = command('unchecked', async (options) => {
438
303
  const { userId, permissions } = options;
439
- console.log(`🔑 [updateUserPermissions] Updating permissions for user ${userId}`);
440
304
  try {
441
- let adminKeyId = userId; // Use userId as default key ID
442
- // Try direct update first
305
+ let adminKeyId = userId;
443
306
  try {
444
307
  await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
445
308
  method: 'PUT',
446
309
  body: JSON.stringify({ scopes: permissions })
447
310
  });
448
- console.log(`✅ [updateUserPermissions] Permissions updated successfully`);
449
311
  return;
450
312
  }
451
313
  catch {
452
- // If direct update fails, try to find the correct adminKeyId
453
- console.log(`⚠️ [updateUserPermissions] Direct update failed, searching for key ID...`);
454
314
  try {
455
315
  const allKeysData = await makeAdminRequest('/admin/keys');
456
- // Find the ACTIVE key for this user (ignore revoked keys)
457
- // Match by sub (userId) and client_id
458
316
  const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
459
317
  if (userKey) {
460
- // Use the found key ID for update
461
318
  adminKeyId = userKey.id;
462
319
  await makeAdminRequest(`/admin/keys/${adminKeyId}`, {
463
320
  method: 'PUT',
464
321
  body: JSON.stringify({ scopes: permissions })
465
322
  });
466
- console.log(`✅ [updateUserPermissions] Permissions updated successfully (after search)`);
467
323
  return;
468
324
  }
469
325
  else {
470
- // User doesn't exist, create new admin key
471
- console.log(`📝 [updateUserPermissions] Creating new admin key...`);
472
326
  await createUserPermissions(userId, permissions);
473
- console.log(`✅ [updateUserPermissions] New admin key created successfully`);
474
327
  return;
475
328
  }
476
329
  }
477
330
  catch (searchError) {
478
- console.error('[updateUserPermissions] Error during permission update:', searchError);
331
+ console.error('[updateUserPermissions] Error during permission update:', searchError);
479
332
  throw new Error('Failed to update permissions');
480
333
  }
481
334
  }
482
335
  }
483
336
  catch (error) {
484
- console.error('[updateUserPermissions] Error:', error);
337
+ console.error('[updateUserPermissions] Error:', error);
485
338
  throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
486
339
  }
487
340
  });
341
+ export const generateApiKey = command('unchecked', async (options) => {
342
+ try {
343
+ const filteredPermissions = PERMISSION_PREFIX
344
+ ? options.permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
345
+ : options.permissions;
346
+ if (filteredPermissions.length === 0) {
347
+ throw new Error(`At least one ${PERMISSION_PREFIX || ''} permission is required to generate an API key`);
348
+ }
349
+ let oldKeyId = null;
350
+ if (options.revokeOld) {
351
+ try {
352
+ const allKeysData = await makeAdminRequest('/admin/keys');
353
+ if (allKeysData?.data?.data && Array.isArray(allKeysData.data.data)) {
354
+ const userKey = allKeysData.data.data.find((key) => key.sub === options.userId && key.client_id === CLIENT_ID && key.status === 'active');
355
+ if (userKey) {
356
+ oldKeyId = userKey.id;
357
+ }
358
+ }
359
+ }
360
+ catch (e) {
361
+ console.warn('[generateApiKey] Could not fetch existing key for revocation:', e);
362
+ }
363
+ }
364
+ const createData = await createUserPermissions(options.userId, filteredPermissions);
365
+ if (!createData) {
366
+ throw new Error('Failed to create admin key');
367
+ }
368
+ const newApiKey = createData?.data?.key;
369
+ if (!newApiKey) {
370
+ console.error('[generateApiKey] No API key in response:', createData);
371
+ throw new Error('Failed to generate API key - no key in response');
372
+ }
373
+ try {
374
+ const currentUser = await makeClerkRequest(`/users/${options.userId}`);
375
+ await makeClerkRequest(`/users/${options.userId}`, {
376
+ method: 'PATCH',
377
+ body: JSON.stringify({
378
+ private_metadata: {
379
+ ...(currentUser.private_metadata || {}),
380
+ mako_api_key: newApiKey
381
+ }
382
+ })
383
+ });
384
+ }
385
+ catch (clerkError) {
386
+ console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
387
+ console.warn('[generateApiKey] Key generated but could not update Clerk profile');
388
+ }
389
+ if (oldKeyId) {
390
+ try {
391
+ await makeAdminRequest(`/admin/keys/${oldKeyId}`, {
392
+ method: 'DELETE'
393
+ });
394
+ }
395
+ catch (revokeError) {
396
+ console.error('[generateApiKey] Failed to revoke old key:', revokeError);
397
+ console.warn('[generateApiKey] New key generated but could not revoke old key');
398
+ }
399
+ }
400
+ return {
401
+ success: true,
402
+ apiKey: newApiKey,
403
+ message: oldKeyId
404
+ ? 'New API key generated and old key revoked successfully'
405
+ : 'API key generated successfully'
406
+ };
407
+ }
408
+ catch (error) {
409
+ console.error('[generateApiKey] Error:', error);
410
+ throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
411
+ }
412
+ });
@@ -56,11 +56,12 @@ export interface UserTableProps {
56
56
  class?: ClassValue;
57
57
  }
58
58
  export interface UserModalProps {
59
- open: boolean;
60
- user: User | null;
59
+ open?: boolean;
60
+ user?: User | null;
61
61
  roles?: Role[];
62
- onSave: (user: User, mode: 'create' | 'edit') => Promise<void>;
63
- onClose: () => void;
62
+ adapter?: UserManagementAdapter;
63
+ onSave: (user: User, mode: 'create' | 'edit') => void | Promise<void>;
64
+ onClose?: () => void;
64
65
  class?: ClassValue;
65
66
  }
66
67
  export interface UserViewModalProps {
@@ -95,6 +96,15 @@ export interface UserManagementAdapter {
95
96
  userId: string;
96
97
  permissions: string[];
97
98
  }) => PromiseLike<void>;
99
+ generateApiKey?: (options: {
100
+ userId: string;
101
+ permissions: string[];
102
+ revokeOld?: boolean;
103
+ }) => PromiseLike<{
104
+ success: boolean;
105
+ apiKey: string;
106
+ message: string;
107
+ }>;
98
108
  }
99
109
  export interface UserManagementProps {
100
110
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -59,7 +59,6 @@
59
59
  "@storybook/addon-docs": "^9.0.18",
60
60
  "@storybook/addon-svelte-csf": "^5.0.7",
61
61
  "@storybook/sveltekit": "^9.0.18",
62
- "@sveltejs/kit": "^2.16.0",
63
62
  "@sveltejs/package": "^2.0.0",
64
63
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
65
64
  "@tailwindcss/vite": "^4.0.14",
@@ -117,8 +116,9 @@
117
116
  "dependencies": {
118
117
  "@friendofsvelte/mermaid": "^0.0.4",
119
118
  "@friendofsvelte/state": "^0.0.6-ts",
120
- "@makolabs/ripple": "^1.2.4",
119
+ "@makolabs/ripple": "^1.2.9",
121
120
  "@sveltejs/adapter-static": "^3.0.9",
121
+ "@sveltejs/kit": "^2.48.4",
122
122
  "compromise": "^14.14.4",
123
123
  "dayjs": "^1.11.13",
124
124
  "echarts": "^5.6.0",