@makolabs/ripple 1.6.9 → 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) {
|
|
@@ -147,10 +146,7 @@ async function verifyApiKeyToken(apiKey) {
|
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
|
|
150
|
-
|
|
151
|
-
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
152
|
-
: permissions;
|
|
153
|
-
if (filteredPermissions.length === 0) {
|
|
149
|
+
if (permissions.length === 0) {
|
|
154
150
|
return null;
|
|
155
151
|
}
|
|
156
152
|
return await makeAdminRequest('/admin/keys', {
|
|
@@ -158,7 +154,7 @@ async function createUserPermissions(userId, permissions, clientId = CLIENT_ID)
|
|
|
158
154
|
body: JSON.stringify({
|
|
159
155
|
client_id: clientId,
|
|
160
156
|
sub: userId,
|
|
161
|
-
scopes:
|
|
157
|
+
scopes: permissions
|
|
162
158
|
})
|
|
163
159
|
});
|
|
164
160
|
}
|
|
@@ -423,9 +419,6 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
423
419
|
// Fetch user's active keys
|
|
424
420
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
|
|
425
421
|
const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
|
|
426
|
-
const filteredPermissions = PERMISSION_PREFIX
|
|
427
|
-
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
428
|
-
: permissions;
|
|
429
422
|
if (userKeys.length === 0) {
|
|
430
423
|
// No active key exists, create new one
|
|
431
424
|
const newKeyResult = await createUserPermissions(userId, permissions);
|
|
@@ -452,7 +445,7 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
452
445
|
await makeAdminRequest(`/admin/keys/${keyId}`, {
|
|
453
446
|
method: 'PUT',
|
|
454
447
|
body: JSON.stringify({
|
|
455
|
-
scopes:
|
|
448
|
+
scopes: permissions
|
|
456
449
|
})
|
|
457
450
|
});
|
|
458
451
|
// Verify the token has updated scopes
|
|
@@ -462,9 +455,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
462
455
|
console.log('[updateUserPermissions] Key verification:', verification);
|
|
463
456
|
if (verification.valid) {
|
|
464
457
|
// Check if the scopes match what we expect
|
|
465
|
-
const scopesMatch =
|
|
458
|
+
const scopesMatch = permissions.every((perm) => verification.scopes?.includes(perm));
|
|
466
459
|
if (!scopesMatch) {
|
|
467
|
-
console.warn('[updateUserPermissions] Scopes mismatch. Expected:',
|
|
460
|
+
console.warn('[updateUserPermissions] Scopes mismatch. Expected:', permissions, 'Got:', verification.scopes);
|
|
468
461
|
}
|
|
469
462
|
}
|
|
470
463
|
else {
|
|
@@ -493,12 +486,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
493
486
|
});
|
|
494
487
|
export const generateApiKey = command('unchecked', async (options) => {
|
|
495
488
|
try {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// Default to readonly permission if none provided (first-time key generation)
|
|
500
|
-
if (filteredPermissions.length === 0 && PERMISSION_PREFIX) {
|
|
501
|
-
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');
|
|
502
492
|
}
|
|
503
493
|
// Check if user has existing active key
|
|
504
494
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${options.userId}`);
|
|
@@ -507,6 +497,7 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
507
497
|
let wasRotated = false;
|
|
508
498
|
let oldApiKey;
|
|
509
499
|
let currentUser = null;
|
|
500
|
+
let verificationWarning;
|
|
510
501
|
if (userKeys.length > 0 && options.revokeOld) {
|
|
511
502
|
// Use rotate endpoint (per Mako Auth API spec)
|
|
512
503
|
const keyId = userKeys[0].id;
|
|
@@ -516,7 +507,7 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
516
507
|
const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
|
|
517
508
|
method: 'POST',
|
|
518
509
|
body: JSON.stringify({
|
|
519
|
-
scopes:
|
|
510
|
+
scopes: options.permissions
|
|
520
511
|
})
|
|
521
512
|
});
|
|
522
513
|
// Rotate endpoint returns key in data.key field
|
|
@@ -544,22 +535,25 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
544
535
|
console.log('[generateApiKey] New key verification:', newKeyVerification);
|
|
545
536
|
if (newKeyVerification.valid) {
|
|
546
537
|
// Check if the scopes match what we expect
|
|
547
|
-
const scopesMatch =
|
|
538
|
+
const scopesMatch = options.permissions.every((perm) => newKeyVerification.scopes?.includes(perm));
|
|
548
539
|
if (!scopesMatch) {
|
|
549
|
-
console.warn('[generateApiKey] Scopes mismatch. Expected:',
|
|
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'}`;
|
|
550
542
|
}
|
|
551
543
|
}
|
|
552
544
|
else {
|
|
553
545
|
console.warn('[generateApiKey] New key verification failed:', newKeyVerification.error);
|
|
546
|
+
verificationWarning = `New API key failed verification - ${newKeyVerification.error || 'Unknown error'}`;
|
|
554
547
|
}
|
|
555
548
|
}
|
|
556
549
|
catch (verifyError) {
|
|
557
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'}`;
|
|
558
552
|
}
|
|
559
553
|
}
|
|
560
554
|
else {
|
|
561
555
|
// Create new key if none exists or revokeOld is false
|
|
562
|
-
const createData = await createUserPermissions(options.userId,
|
|
556
|
+
const createData = await createUserPermissions(options.userId, options.permissions);
|
|
563
557
|
if (!createData) {
|
|
564
558
|
throw new Error('Failed to create admin key');
|
|
565
559
|
}
|
|
@@ -593,7 +587,8 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
593
587
|
const result = {
|
|
594
588
|
success: true,
|
|
595
589
|
apiKey: newApiKey,
|
|
596
|
-
message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully'
|
|
590
|
+
message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully',
|
|
591
|
+
verificationWarning
|
|
597
592
|
};
|
|
598
593
|
return JSON.parse(JSON.stringify(result));
|
|
599
594
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 {
|
|
@@ -222,6 +223,14 @@
|
|
|
222
223
|
}
|
|
223
224
|
};
|
|
224
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
|
+
}
|
|
225
234
|
} catch (error) {
|
|
226
235
|
console.error('Error regenerating API key:', error);
|
|
227
236
|
formErrors.apiKey = error instanceof Error ? error.message : 'Failed to regenerate API key';
|
|
@@ -276,7 +285,7 @@
|
|
|
276
285
|
contentclass="max-w-4xl"
|
|
277
286
|
class={cn(className)}
|
|
278
287
|
>
|
|
279
|
-
<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">
|
|
280
289
|
<!-- Left Column: Profile Information -->
|
|
281
290
|
<div class="min-w-0 flex-1 space-y-4">
|
|
282
291
|
<div class="border-default-200 border-b pb-3">
|
|
@@ -296,6 +305,7 @@
|
|
|
296
305
|
? 'border-danger-300'
|
|
297
306
|
: 'border-default-300'}"
|
|
298
307
|
placeholder="First name"
|
|
308
|
+
data-testid="first-name-input"
|
|
299
309
|
required
|
|
300
310
|
/>
|
|
301
311
|
{#if formErrors.first_name}
|
|
@@ -316,6 +326,7 @@
|
|
|
316
326
|
? 'border-danger-300'
|
|
317
327
|
: 'border-default-300'}"
|
|
318
328
|
placeholder="Last name"
|
|
329
|
+
data-testid="last-name-input"
|
|
319
330
|
required
|
|
320
331
|
/>
|
|
321
332
|
{#if formErrors.last_name}
|
|
@@ -336,6 +347,7 @@
|
|
|
336
347
|
? 'border-danger-300'
|
|
337
348
|
: 'border-default-300'}"
|
|
338
349
|
placeholder="user@example.com"
|
|
350
|
+
data-testid="email-input"
|
|
339
351
|
required
|
|
340
352
|
/>
|
|
341
353
|
{#if formErrors.email}
|
|
@@ -358,6 +370,7 @@
|
|
|
358
370
|
disabled={verifyingToken}
|
|
359
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"
|
|
360
372
|
aria-label="Verify token"
|
|
373
|
+
data-testid="verify-token-button"
|
|
361
374
|
>
|
|
362
375
|
<svg
|
|
363
376
|
class="h-4 w-4 {verifyingToken ? 'animate-spin' : ''}"
|
|
@@ -384,6 +397,7 @@
|
|
|
384
397
|
formData.permissions.length === 0}
|
|
385
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"
|
|
386
399
|
aria-label="Regenerate API key"
|
|
400
|
+
data-testid="regenerate-api-key-button"
|
|
387
401
|
>
|
|
388
402
|
<svg
|
|
389
403
|
class="h-4 w-4 {regeneratingApiKey ? 'animate-spin' : ''}"
|
|
@@ -411,12 +425,14 @@
|
|
|
411
425
|
readonly
|
|
412
426
|
class="border-default-300 bg-default-50 w-full rounded-lg border px-3 py-2 pr-10 font-mono text-sm"
|
|
413
427
|
placeholder="No API key generated"
|
|
428
|
+
data-testid="api-key-input"
|
|
414
429
|
/>
|
|
415
430
|
<button
|
|
416
431
|
type="button"
|
|
417
432
|
onclick={() => (showApiKey = !showApiKey)}
|
|
418
433
|
class="text-default-500 hover:text-default-700 absolute top-1/2 right-2 -translate-y-1/2"
|
|
419
434
|
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
|
|
435
|
+
data-testid="toggle-api-key-visibility"
|
|
420
436
|
>
|
|
421
437
|
{#if showApiKey}
|
|
422
438
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -533,6 +549,7 @@
|
|
|
533
549
|
? 'border-blue-500 bg-blue-50 opacity-75'
|
|
534
550
|
: 'border-blue-500 bg-blue-50'
|
|
535
551
|
: 'border-default-200 hover:border-default-300 bg-white'}"
|
|
552
|
+
data-testid="role-{role.value}"
|
|
536
553
|
>
|
|
537
554
|
<div class="flex items-center justify-between gap-2">
|
|
538
555
|
<div class="min-w-0 flex-1">
|
|
@@ -587,7 +604,13 @@
|
|
|
587
604
|
<div></div>
|
|
588
605
|
{/if}
|
|
589
606
|
<div class="flex gap-3">
|
|
590
|
-
<Button
|
|
607
|
+
<Button
|
|
608
|
+
variant="outline"
|
|
609
|
+
onclick={handleClose}
|
|
610
|
+
disabled={saving}
|
|
611
|
+
type="button"
|
|
612
|
+
data-testid="cancel-button"
|
|
613
|
+
>
|
|
591
614
|
Cancel
|
|
592
615
|
</Button>
|
|
593
616
|
<Button
|
|
@@ -596,6 +619,7 @@
|
|
|
596
619
|
onclick={() => formElement?.requestSubmit()}
|
|
597
620
|
disabled={saving}
|
|
598
621
|
isLoading={saving}
|
|
622
|
+
data-testid="save-user-button"
|
|
599
623
|
>
|
|
600
624
|
{mode === 'create' ? 'Create User' : 'Save Changes'}
|
|
601
625
|
</Button>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makolabs/ripple",
|
|
3
|
-
"version": "1.
|
|
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",
|