@makolabs/ripple 1.2.9 → 1.2.10

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 -->
@@ -52,11 +52,12 @@ export declare const deleteUser: import("@sveltejs/kit").RemoteCommand<string, P
52
52
  */
53
53
  export declare const deleteUsers: import("@sveltejs/kit").RemoteCommand<string[], Promise<void>>;
54
54
  /**
55
- * Get permissions for a specific user
55
+ * Get permissions for specific users (batched)
56
56
  *
57
- * Uses 'sub' (userId) parameter in admin API calls
57
+ * Uses 'sub' (userId) parameter in admin API calls.
58
+ * Batches multiple permission requests to avoid n+1 problem.
58
59
  */
59
- export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, string[]>;
60
+ export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, Promise<string[]>>;
60
61
  /**
61
62
  * Update permissions for a specific user
62
63
  *
@@ -66,3 +67,15 @@ export declare const updateUserPermissions: import("@sveltejs/kit").RemoteComman
66
67
  userId: string;
67
68
  permissions: string[];
68
69
  }, Promise<void>>;
70
+ /**
71
+ * Generate new API key for a user with optional old key revocation
72
+ */
73
+ export declare const generateApiKey: import("@sveltejs/kit").RemoteCommand<{
74
+ userId: string;
75
+ permissions: string[];
76
+ revokeOld?: boolean;
77
+ }, Promise<{
78
+ success: boolean;
79
+ apiKey: string;
80
+ message: string;
81
+ }>>;
@@ -375,58 +375,84 @@ export const deleteUsers = command('unchecked', async (userIds) => {
375
375
  }
376
376
  });
377
377
  /**
378
- * Get permissions for a specific user
379
- *
380
- * Uses 'sub' (userId) parameter in admin API calls
378
+ * Helper: Fetch permissions for a single user
381
379
  */
382
- export const getUserPermissions = query('unchecked', async (userId) => {
383
- console.log(`🔍 [getUserPermissions] Fetching permissions for user: ${userId}`);
380
+ async function fetchUserPermissions(userId) {
381
+ console.log(`🔍 [fetchUserPermissions] Fetching permissions for user: ${userId}`);
382
+ // Try direct lookup first using client_id and sub (userId)
384
383
  try {
385
- // Try direct lookup first using client_id and sub (userId)
384
+ 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
+ if (userData?.data?.data && Array.isArray(userData.data.data)) {
388
+ 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
+ }
391
+ // Extract scopes from the response
392
+ if (userData?.data?.data && Array.isArray(userData.data.data)) {
393
+ return userData.data.data.flatMap((key) => key.scopes || []);
394
+ }
395
+ else if (userData?.scopes) {
396
+ return Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
397
+ }
398
+ return [];
399
+ }
400
+ 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
386
403
  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)`);
404
+ 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
+ const userKey = allKeysData.data.data.find((key) => key.sub === userId && key.client_id === CLIENT_ID && key.status === 'active');
409
+ if (userKey) {
410
+ console.log(`✅ [fetchUserPermissions] Found active user key by sub field`);
411
+ return Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
393
412
  }
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 || []);
413
+ else {
414
+ console.log(`❌ [fetchUserPermissions] No user found, returning empty permissions`);
415
+ return [];
397
416
  }
398
- else if (userData?.scopes) {
399
- return Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
400
- }
401
- return [];
402
417
  }
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
- }
418
+ catch (searchError) {
419
+ console.error('❌ [fetchUserPermissions] Error searching for user by sub:', searchError);
420
+ throw new Error('Failed to fetch user permissions');
425
421
  }
426
422
  }
423
+ }
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
+ export const getUserPermissions = query.batch('unchecked', async (userIds) => {
431
+ console.log(`🔍 [getUserPermissions] Batch fetching permissions for ${userIds.length} users`);
432
+ try {
433
+ // Fetch all permissions in parallel
434
+ const permissionPromises = userIds.map((userId) => fetchUserPermissions(userId));
435
+ const permissionsResults = await Promise.all(permissionPromises);
436
+ // Create a lookup map for O(1) access
437
+ const lookup = new Map();
438
+ userIds.forEach((userId, index) => {
439
+ lookup.set(userId, permissionsResults[index]);
440
+ });
441
+ console.log(`✅ [getUserPermissions] Batch fetch completed for ${userIds.length} users`);
442
+ // Return a function that SvelteKit will call for each individual request
443
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
444
+ return (userId, _index) => {
445
+ const permissions = lookup.get(userId) || [];
446
+ return Promise.resolve(permissions);
447
+ };
448
+ }
427
449
  catch (error) {
428
- console.error('❌ [getUserPermissions] Error:', error);
429
- throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
450
+ console.error('❌ [getUserPermissions] Batch error:', error);
451
+ // Return a function that throws for all requests
452
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
453
+ return (_userId, _index) => {
454
+ throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
455
+ };
430
456
  }
431
457
  });
432
458
  /**
@@ -485,3 +511,96 @@ export const updateUserPermissions = command('unchecked', async (options) => {
485
511
  throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
486
512
  }
487
513
  });
514
+ /**
515
+ * Generate new API key for a user with optional old key revocation
516
+ */
517
+ export const generateApiKey = command('unchecked', async (options) => {
518
+ console.log(`🔑 [generateApiKey] Generating new API key for user ${options.userId}`);
519
+ try {
520
+ // Filter permissions by prefix if configured
521
+ const filteredPermissions = PERMISSION_PREFIX
522
+ ? options.permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
523
+ : options.permissions;
524
+ if (filteredPermissions.length === 0) {
525
+ throw new Error(`At least one ${PERMISSION_PREFIX || ''} permission is required to generate an API key`);
526
+ }
527
+ // If revokeOld is true, find and revoke the old key first
528
+ let oldKeyId = null;
529
+ if (options.revokeOld) {
530
+ try {
531
+ console.log(`🔍 [generateApiKey] Looking for existing active key to revoke`);
532
+ 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)
538
+ const userKey = allKeysData.data.data.find((key) => key.sub === options.userId && key.client_id === CLIENT_ID && key.status === 'active');
539
+ if (userKey) {
540
+ oldKeyId = userKey.id;
541
+ console.log(`📌 [generateApiKey] Found existing active key: ${oldKeyId}`);
542
+ }
543
+ }
544
+ }
545
+ catch (e) {
546
+ console.warn('⚠️ [generateApiKey] Could not fetch existing key for revocation:', e);
547
+ // Continue anyway - not critical
548
+ }
549
+ }
550
+ // Create new admin key
551
+ const createData = await createUserPermissions(options.userId, filteredPermissions);
552
+ if (!createData) {
553
+ throw new Error('Failed to create admin key');
554
+ }
555
+ const newApiKey = createData?.data?.key;
556
+ if (!newApiKey) {
557
+ console.error('❌ [generateApiKey] No API key in response:', createData);
558
+ throw new Error('Failed to generate API key - no key in response');
559
+ }
560
+ console.log(`✅ [generateApiKey] New API key generated successfully`);
561
+ // Update user's Clerk profile with the new API key
562
+ try {
563
+ // First, get the current user data to preserve existing private_metadata
564
+ const currentUser = await makeClerkRequest(`/users/${options.userId}`);
565
+ await makeClerkRequest(`/users/${options.userId}`, {
566
+ method: 'PATCH',
567
+ body: JSON.stringify({
568
+ private_metadata: {
569
+ ...(currentUser.private_metadata || {}),
570
+ mako_api_key: newApiKey
571
+ }
572
+ })
573
+ });
574
+ console.log(`✅ [generateApiKey] API key stored in user's Clerk profile`);
575
+ }
576
+ catch (clerkError) {
577
+ console.error('❌ [generateApiKey] Failed to update Clerk profile:', clerkError);
578
+ console.warn('⚠️ [generateApiKey] Key generated but could not update Clerk profile');
579
+ }
580
+ // Revoke old key if it exists
581
+ if (oldKeyId) {
582
+ try {
583
+ console.log(`🗑️ [generateApiKey] Revoking old key: ${oldKeyId}`);
584
+ await makeAdminRequest(`/admin/keys/${oldKeyId}`, {
585
+ method: 'DELETE'
586
+ });
587
+ console.log(`✅ [generateApiKey] Old key revoked successfully`);
588
+ }
589
+ 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');
592
+ }
593
+ }
594
+ return {
595
+ success: true,
596
+ apiKey: newApiKey,
597
+ message: oldKeyId
598
+ ? 'New API key generated and old key revoked successfully'
599
+ : 'API key generated successfully'
600
+ };
601
+ }
602
+ catch (error) {
603
+ console.error('❌ [generateApiKey] Error:', error);
604
+ throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
605
+ }
606
+ });
@@ -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.10",
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",