@makolabs/ripple 1.6.8 → 1.7.1

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.
@@ -2,7 +2,6 @@ import { query, command } from '$app/server';
2
2
  import { getRequestEvent } from '$app/server';
3
3
  import { env } from '$env/dynamic/private';
4
4
  const CLIENT_ID = env.CLIENT_ID || 'sharkfin';
5
- const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
6
5
  const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
7
6
  function handleClerkError(error, defaultMessage) {
8
7
  if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
@@ -93,7 +92,7 @@ async function makeAuthRequest(endpoint, options = {}) {
93
92
  try {
94
93
  data = JSON.parse(text);
95
94
  }
96
- catch (parseError) {
95
+ catch {
97
96
  // Not JSON, treat as plain text error (e.g., "404 page not found")
98
97
  data = { error: text, message: text };
99
98
  }
@@ -116,7 +115,7 @@ async function verifyApiKeyToken(apiKey) {
116
115
  const verifyResult = await makeAuthRequest('/auth/verify', {
117
116
  method: 'GET',
118
117
  headers: {
119
- 'Authorization': `Bearer ${token}`
118
+ Authorization: `Bearer ${token}`
120
119
  }
121
120
  });
122
121
  if (verifyResult.ok && verifyResult.data?.data) {
@@ -129,7 +128,9 @@ async function verifyApiKeyToken(apiKey) {
129
128
  };
130
129
  }
131
130
  }
132
- const errorMsg = result.data?.message || result.data?.error || `API key verification failed with status ${result.status}`;
131
+ const errorMsg = result.data?.message ||
132
+ result.data?.error ||
133
+ `API key verification failed with status ${result.status}`;
133
134
  console.warn('[verifyApiKeyToken] Verification failed:', errorMsg);
134
135
  return {
135
136
  valid: false,
@@ -145,10 +146,7 @@ async function verifyApiKeyToken(apiKey) {
145
146
  }
146
147
  }
147
148
  async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
148
- const filteredPermissions = PERMISSION_PREFIX
149
- ? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
150
- : permissions;
151
- if (filteredPermissions.length === 0) {
149
+ if (permissions.length === 0) {
152
150
  return null;
153
151
  }
154
152
  return await makeAdminRequest('/admin/keys', {
@@ -156,7 +154,7 @@ async function createUserPermissions(userId, permissions, clientId = CLIENT_ID)
156
154
  body: JSON.stringify({
157
155
  client_id: clientId,
158
156
  sub: userId,
159
- scopes: filteredPermissions
157
+ scopes: permissions
160
158
  })
161
159
  });
162
160
  }
@@ -421,9 +419,6 @@ export const updateUserPermissions = command('unchecked', async (options) => {
421
419
  // Fetch user's active keys
422
420
  const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
423
421
  const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
424
- const filteredPermissions = PERMISSION_PREFIX
425
- ? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
426
- : permissions;
427
422
  if (userKeys.length === 0) {
428
423
  // No active key exists, create new one
429
424
  const newKeyResult = await createUserPermissions(userId, permissions);
@@ -450,7 +445,7 @@ export const updateUserPermissions = command('unchecked', async (options) => {
450
445
  await makeAdminRequest(`/admin/keys/${keyId}`, {
451
446
  method: 'PUT',
452
447
  body: JSON.stringify({
453
- scopes: filteredPermissions
448
+ scopes: permissions
454
449
  })
455
450
  });
456
451
  // Verify the token has updated scopes
@@ -460,9 +455,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
460
455
  console.log('[updateUserPermissions] Key verification:', verification);
461
456
  if (verification.valid) {
462
457
  // Check if the scopes match what we expect
463
- const scopesMatch = filteredPermissions.every(perm => verification.scopes?.includes(perm));
458
+ const scopesMatch = permissions.every((perm) => verification.scopes?.includes(perm));
464
459
  if (!scopesMatch) {
465
- console.warn('[updateUserPermissions] Scopes mismatch. Expected:', filteredPermissions, 'Got:', verification.scopes);
460
+ console.warn('[updateUserPermissions] Scopes mismatch. Expected:', permissions, 'Got:', verification.scopes);
466
461
  }
467
462
  }
468
463
  else {
@@ -491,12 +486,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
491
486
  });
492
487
  export const generateApiKey = command('unchecked', async (options) => {
493
488
  try {
494
- let filteredPermissions = PERMISSION_PREFIX
495
- ? options.permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
496
- : options.permissions;
497
- // Default to readonly permission if none provided (first-time key generation)
498
- if (filteredPermissions.length === 0 && PERMISSION_PREFIX) {
499
- filteredPermissions = [`${PERMISSION_PREFIX}readonly`];
489
+ // No default permissions - require explicit permissions to be passed
490
+ if (options.permissions.length === 0) {
491
+ throw new Error('At least one permission scope is required');
500
492
  }
501
493
  // Check if user has existing active key
502
494
  const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${options.userId}`);
@@ -505,16 +497,17 @@ export const generateApiKey = command('unchecked', async (options) => {
505
497
  let wasRotated = false;
506
498
  let oldApiKey;
507
499
  let currentUser = null;
500
+ let verificationWarning;
508
501
  if (userKeys.length > 0 && options.revokeOld) {
509
502
  // Use rotate endpoint (per Mako Auth API spec)
510
503
  const keyId = userKeys[0].id;
511
504
  // Get the old API key from Clerk's private_metadata
512
505
  currentUser = await makeClerkRequest(`/users/${options.userId}`);
513
- oldApiKey = currentUser.private_metadata?.mako_api_key;
506
+ oldApiKey = currentUser?.private_metadata?.mako_api_key;
514
507
  const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
515
508
  method: 'POST',
516
509
  body: JSON.stringify({
517
- scopes: filteredPermissions
510
+ scopes: options.permissions
518
511
  })
519
512
  });
520
513
  // Rotate endpoint returns key in data.key field
@@ -542,22 +535,25 @@ export const generateApiKey = command('unchecked', async (options) => {
542
535
  console.log('[generateApiKey] New key verification:', newKeyVerification);
543
536
  if (newKeyVerification.valid) {
544
537
  // Check if the scopes match what we expect
545
- const scopesMatch = filteredPermissions.every(perm => newKeyVerification.scopes?.includes(perm));
538
+ const scopesMatch = options.permissions.every((perm) => newKeyVerification.scopes?.includes(perm));
546
539
  if (!scopesMatch) {
547
- console.warn('[generateApiKey] Scopes mismatch. Expected:', filteredPermissions, 'Got:', newKeyVerification.scopes);
540
+ console.warn('[generateApiKey] Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
541
+ verificationWarning = `New API key scopes do not match expected permissions. Expected: ${options.permissions.join(', ')}, Got: ${newKeyVerification.scopes?.join(', ') || 'none'}`;
548
542
  }
549
543
  }
550
544
  else {
551
545
  console.warn('[generateApiKey] New key verification failed:', newKeyVerification.error);
546
+ verificationWarning = `New API key failed verification - ${newKeyVerification.error || 'Unknown error'}`;
552
547
  }
553
548
  }
554
549
  catch (verifyError) {
555
550
  console.warn('[generateApiKey] Could not verify new key:', verifyError);
551
+ verificationWarning = `Could not verify new API key - ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`;
556
552
  }
557
553
  }
558
554
  else {
559
555
  // Create new key if none exists or revokeOld is false
560
- const createData = await createUserPermissions(options.userId, filteredPermissions);
556
+ const createData = await createUserPermissions(options.userId, options.permissions);
561
557
  if (!createData) {
562
558
  throw new Error('Failed to create admin key');
563
559
  }
@@ -572,15 +568,17 @@ export const generateApiKey = command('unchecked', async (options) => {
572
568
  if (!currentUser) {
573
569
  currentUser = await makeClerkRequest(`/users/${options.userId}`);
574
570
  }
575
- await makeClerkRequest(`/users/${options.userId}`, {
576
- method: 'PATCH',
577
- body: JSON.stringify({
578
- private_metadata: {
579
- ...(currentUser.private_metadata || {}),
580
- mako_api_key: newApiKey
581
- }
582
- })
583
- });
571
+ if (currentUser) {
572
+ await makeClerkRequest(`/users/${options.userId}`, {
573
+ method: 'PATCH',
574
+ body: JSON.stringify({
575
+ private_metadata: {
576
+ ...(currentUser.private_metadata || {}),
577
+ mako_api_key: newApiKey
578
+ }
579
+ })
580
+ });
581
+ }
584
582
  }
585
583
  catch (clerkError) {
586
584
  console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
@@ -589,9 +587,8 @@ export const generateApiKey = command('unchecked', async (options) => {
589
587
  const result = {
590
588
  success: true,
591
589
  apiKey: newApiKey,
592
- message: wasRotated
593
- ? 'API key rotated successfully'
594
- : 'API key generated successfully'
590
+ message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully',
591
+ verificationWarning
595
592
  };
596
593
  return JSON.parse(JSON.stringify(result));
597
594
  }
package/dist/index.d.ts CHANGED
@@ -1030,6 +1030,7 @@ export interface UserManagementAdapter {
1030
1030
  success: boolean;
1031
1031
  apiKey: string;
1032
1032
  message: string;
1033
+ verificationWarning?: string;
1033
1034
  }>;
1034
1035
  verifyToken?: (options: {
1035
1036
  apiKey: string;
@@ -4,9 +4,8 @@
4
4
  import UserTable from './UserTable.svelte';
5
5
  import UserModal from './UserModal.svelte';
6
6
  import UserViewModal from './UserViewModal.svelte';
7
- import type { User, UserManagementProps, Role, Permission, GetUsersResult } from '../index.js';
7
+ import type { User, UserManagementProps, Role, Permission } from '../index.js';
8
8
  import { SvelteSet } from 'svelte/reactivity';
9
- import type { RemoteQuery } from '@sveltejs/kit';
10
9
 
11
10
  let {
12
11
  adapter,
@@ -60,13 +59,7 @@
60
59
 
61
60
  // Refresh the query cache to get fresh data
62
61
  async function refreshUsersQuery() {
63
- const query = adapter.getUsers({
64
- page: currentPage,
65
- pageSize,
66
- sortBy: sortBy || undefined,
67
- sortOrder: sortOrder || 'desc'
68
- }) as RemoteQuery<GetUsersResult>;
69
- await query.refresh();
62
+ await loadUsers();
70
63
  }
71
64
 
72
65
  // Handlers
@@ -146,10 +139,8 @@
146
139
  } else {
147
140
  await adapter.updateUser({ userId: user.id, userData: user });
148
141
  }
149
- // Invalidate the query cache by calling refresh() before loading
142
+ // Refresh the users list
150
143
  await refreshUsersQuery();
151
- // Force refresh by reassigning state to trigger reactivity
152
- await loadUsers();
153
144
  } catch (error) {
154
145
  console.error('Error saving user:', error);
155
146
  // Reload on error to restore correct state
@@ -180,11 +171,8 @@
180
171
  // Perform the actual delete operation
181
172
  await adapter.deleteUser(userId);
182
173
 
183
- // Invalidate the query cache by calling refresh() before loading
184
- await refreshUsersQuery();
185
-
186
174
  // Refresh to ensure we have the latest data
187
- await loadUsers();
175
+ await refreshUsersQuery();
188
176
  } catch (error) {
189
177
  console.error('Error deleting user:', error);
190
178
  // Reload on error to restore correct state
@@ -225,11 +213,8 @@
225
213
  // Perform the actual delete operation
226
214
  await adapter.deleteUsers(userIds);
227
215
 
228
- // Invalidate the query cache by calling refresh() before loading
229
- await refreshUsersQuery();
230
-
231
216
  // Refresh to ensure we have the latest data
232
- await loadUsers();
217
+ await refreshUsersQuery();
233
218
  } catch (error) {
234
219
  console.error('Error deleting users:', error);
235
220
  // Reload on error to restore correct state
@@ -9,6 +9,7 @@
9
9
  type Role,
10
10
  getUserDisplayName
11
11
  } from '../index.js';
12
+ import { toast } from 'svelte-sonner';
12
13
 
13
14
  // Icons as simple SVGs
14
15
  let {
@@ -31,7 +32,9 @@
31
32
  let showApiKey = $state(false);
32
33
  let regeneratingApiKey = $state(false);
33
34
  let verifyingToken = $state(false);
34
- let tokenVerification = $state<{ valid?: boolean; scopes?: string[]; error?: string } | null>(null);
35
+ let tokenVerification = $state<{ valid?: boolean; scopes?: string[]; error?: string } | null>(
36
+ null
37
+ );
35
38
  let initialRole = $state<string>('');
36
39
 
37
40
  // Form data
@@ -187,7 +190,7 @@
187
190
  role: roleValue,
188
191
  permissions: role ? [...role.permissions] : []
189
192
  };
190
-
193
+
191
194
  // Clear token verification when permissions change
192
195
  tokenVerification = null;
193
196
  }
@@ -220,6 +223,14 @@
220
223
  }
221
224
  };
222
225
  }
226
+
227
+ // Show warning toast if new key verification failed
228
+ if (result.verificationWarning) {
229
+ toast.warning('API Key Verification Warning', {
230
+ description: result.verificationWarning,
231
+ duration: 8000 // 8 seconds for important warnings
232
+ });
233
+ }
223
234
  } catch (error) {
224
235
  console.error('Error regenerating API key:', error);
225
236
  formErrors.apiKey = error instanceof Error ? error.message : 'Failed to regenerate API key';
@@ -233,7 +244,7 @@
233
244
 
234
245
  try {
235
246
  verifyingToken = true;
236
- formErrors.apiKey = undefined;
247
+ delete formErrors.apiKey;
237
248
  const result = await adapter.verifyToken({ apiKey });
238
249
  tokenVerification = result;
239
250
 
@@ -274,7 +285,7 @@
274
285
  contentclass="max-w-4xl"
275
286
  class={cn(className)}
276
287
  >
277
- <form bind:this={formElement} onsubmit={handleSubmit} class="flex gap-6">
288
+ <form bind:this={formElement} onsubmit={handleSubmit} class="flex gap-6" data-testid="user-form">
278
289
  <!-- Left Column: Profile Information -->
279
290
  <div class="min-w-0 flex-1 space-y-4">
280
291
  <div class="border-default-200 border-b pb-3">
@@ -294,6 +305,7 @@
294
305
  ? 'border-danger-300'
295
306
  : 'border-default-300'}"
296
307
  placeholder="First name"
308
+ data-testid="first-name-input"
297
309
  required
298
310
  />
299
311
  {#if formErrors.first_name}
@@ -314,6 +326,7 @@
314
326
  ? 'border-danger-300'
315
327
  : 'border-default-300'}"
316
328
  placeholder="Last name"
329
+ data-testid="last-name-input"
317
330
  required
318
331
  />
319
332
  {#if formErrors.last_name}
@@ -334,6 +347,7 @@
334
347
  ? 'border-danger-300'
335
348
  : 'border-default-300'}"
336
349
  placeholder="user@example.com"
350
+ data-testid="email-input"
337
351
  required
338
352
  />
339
353
  {#if formErrors.email}
@@ -356,6 +370,7 @@
356
370
  disabled={verifyingToken}
357
371
  class="disabled:text-default-400 inline-flex items-center gap-1 text-sm text-green-600 transition-colors hover:text-green-700 hover:underline disabled:cursor-not-allowed"
358
372
  aria-label="Verify token"
373
+ data-testid="verify-token-button"
359
374
  >
360
375
  <svg
361
376
  class="h-4 w-4 {verifyingToken ? 'animate-spin' : ''}"
@@ -382,6 +397,7 @@
382
397
  formData.permissions.length === 0}
383
398
  class="disabled:text-default-400 inline-flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-700 hover:underline disabled:cursor-not-allowed"
384
399
  aria-label="Regenerate API key"
400
+ data-testid="regenerate-api-key-button"
385
401
  >
386
402
  <svg
387
403
  class="h-4 w-4 {regeneratingApiKey ? 'animate-spin' : ''}"
@@ -409,12 +425,14 @@
409
425
  readonly
410
426
  class="border-default-300 bg-default-50 w-full rounded-lg border px-3 py-2 pr-10 font-mono text-sm"
411
427
  placeholder="No API key generated"
428
+ data-testid="api-key-input"
412
429
  />
413
430
  <button
414
431
  type="button"
415
432
  onclick={() => (showApiKey = !showApiKey)}
416
433
  class="text-default-500 hover:text-default-700 absolute top-1/2 right-2 -translate-y-1/2"
417
434
  aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
435
+ data-testid="toggle-api-key-visibility"
418
436
  >
419
437
  {#if showApiKey}
420
438
  <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -449,8 +467,18 @@
449
467
  {#if tokenVerification.valid}
450
468
  <div class="bg-success-50 border-success-200 mt-2 rounded-lg border p-3">
451
469
  <div class="flex items-start gap-2">
452
- <svg class="text-success-600 mt-0.5 h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
453
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
470
+ <svg
471
+ class="text-success-600 mt-0.5 h-4 w-4 shrink-0"
472
+ fill="none"
473
+ stroke="currentColor"
474
+ viewBox="0 0 24 24"
475
+ >
476
+ <path
477
+ stroke-linecap="round"
478
+ stroke-linejoin="round"
479
+ stroke-width="2"
480
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
481
+ ></path>
454
482
  </svg>
455
483
  <div class="min-w-0 flex-1">
456
484
  <p class="text-success-800 text-xs font-medium">Token verified successfully</p>
@@ -465,8 +493,18 @@
465
493
  {:else}
466
494
  <div class="bg-danger-50 border-danger-200 mt-2 rounded-lg border p-3">
467
495
  <div class="flex items-start gap-2">
468
- <svg class="text-danger-600 mt-0.5 h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
469
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
496
+ <svg
497
+ class="text-danger-600 mt-0.5 h-4 w-4 shrink-0"
498
+ fill="none"
499
+ stroke="currentColor"
500
+ viewBox="0 0 24 24"
501
+ >
502
+ <path
503
+ stroke-linecap="round"
504
+ stroke-linejoin="round"
505
+ stroke-width="2"
506
+ d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
507
+ ></path>
470
508
  </svg>
471
509
  <div class="min-w-0 flex-1">
472
510
  <p class="text-danger-800 text-xs font-medium">Token verification failed</p>
@@ -511,6 +549,7 @@
511
549
  ? 'border-blue-500 bg-blue-50 opacity-75'
512
550
  : 'border-blue-500 bg-blue-50'
513
551
  : 'border-default-200 hover:border-default-300 bg-white'}"
552
+ data-testid="role-{role.value}"
514
553
  >
515
554
  <div class="flex items-center justify-between gap-2">
516
555
  <div class="min-w-0 flex-1">
@@ -565,7 +604,13 @@
565
604
  <div></div>
566
605
  {/if}
567
606
  <div class="flex gap-3">
568
- <Button variant="outline" onclick={handleClose} disabled={saving} type="button">
607
+ <Button
608
+ variant="outline"
609
+ onclick={handleClose}
610
+ disabled={saving}
611
+ type="button"
612
+ data-testid="cancel-button"
613
+ >
569
614
  Cancel
570
615
  </Button>
571
616
  <Button
@@ -574,6 +619,7 @@
574
619
  onclick={() => formElement?.requestSubmit()}
575
620
  disabled={saving}
576
621
  isLoading={saving}
622
+ data-testid="save-user-button"
577
623
  >
578
624
  {mode === 'create' ? 'Create User' : 'Save Changes'}
579
625
  </Button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "1.6.8",
3
+ "version": "1.7.1",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -18,6 +18,10 @@
18
18
  "format": "prettier --write .",
19
19
  "lint": "prettier --check . && eslint .",
20
20
  "test:unit": "vitest",
21
+ "test:e2e": "playwright test",
22
+ "test:e2e:ui": "playwright test --ui",
23
+ "test:e2e:debug": "playwright test --debug",
24
+ "test:e2e:report": "playwright show-report",
21
25
  "test": "npm run test:unit -- --run",
22
26
  "pub:minor": "git add . && git commit -m \"chore: prepare for publish minor\" && npm version minor && git push --follow-tags && npm publish",
23
27
  "pub:patch": "git add . && git commit -m \"chore: prepare for publish patch\" && npm version patch && git push --follow-tags && npm publish",