@makolabs/ripple 1.6.4 → 1.6.6

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.
@@ -21,3 +21,11 @@ export declare const generateApiKey: import("@sveltejs/kit").RemoteCommand<{
21
21
  apiKey: string;
22
22
  message: string;
23
23
  }>>;
24
+ export declare const verifyToken: import("@sveltejs/kit").RemoteCommand<{
25
+ apiKey: string;
26
+ }, Promise<{
27
+ valid: boolean;
28
+ scopes?: string[];
29
+ error?: string;
30
+ token?: string;
31
+ }>>;
@@ -75,6 +75,62 @@ async function makeAdminRequest(endpoint, options = {}) {
75
75
  // Ensure all data is serializable by converting to plain objects
76
76
  return JSON.parse(JSON.stringify(data));
77
77
  }
78
+ async function makeAuthRequest(endpoint, options = {}) {
79
+ const PRIVATE_BASE_AUTH_URL = env.PRIVATE_BASE_AUTH_URL;
80
+ if (!PRIVATE_BASE_AUTH_URL) {
81
+ throw new Error('PRIVATE_BASE_AUTH_URL environment variable is required');
82
+ }
83
+ const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
84
+ const response = await fetch(url, {
85
+ ...options,
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ ...options.headers
89
+ }
90
+ });
91
+ const data = await response.json();
92
+ // Return both status and data for verification purposes
93
+ return {
94
+ ok: response.ok,
95
+ status: response.status,
96
+ data: JSON.parse(JSON.stringify(data))
97
+ };
98
+ }
99
+ async function verifyApiKeyToken(apiKey) {
100
+ try {
101
+ const result = await makeAuthRequest('/auth/issue', {
102
+ method: 'POST',
103
+ headers: {
104
+ 'X-API-Key': apiKey
105
+ }
106
+ });
107
+ if (result.ok && result.data?.data?.access_token) {
108
+ // Parse the JWT to get scopes (or use verify endpoint)
109
+ const verifyResult = await makeAuthRequest('/auth/verify', {
110
+ method: 'POST',
111
+ headers: {
112
+ 'Authorization': `Bearer ${result.data.data.access_token}`
113
+ }
114
+ });
115
+ if (verifyResult.ok && verifyResult.data?.data) {
116
+ return {
117
+ valid: true,
118
+ scopes: verifyResult.data.data.scopes || []
119
+ };
120
+ }
121
+ }
122
+ return {
123
+ valid: false,
124
+ error: result.data?.error || `API key verification failed with status ${result.status}`
125
+ };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ valid: false,
130
+ error: error instanceof Error ? error.message : 'Unknown error during verification'
131
+ };
132
+ }
133
+ }
78
134
  async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
79
135
  const filteredPermissions = PERMISSION_PREFIX
80
136
  ? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
@@ -349,19 +405,69 @@ async function refreshTokenIfSelfUpdate(userId) {
349
405
  export const updateUserPermissions = command('unchecked', async (options) => {
350
406
  const { userId, permissions } = options;
351
407
  try {
352
- // Fetch all keys for this specific user (not paginated, gets all keys)
408
+ // Fetch user's active keys
353
409
  const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
354
- // Find ALL active keys for this user (not just one)
355
410
  const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
356
- // Create new key first, then delete old ones (safer order)
357
- const newKey = await createUserPermissions(userId, permissions);
358
- // Only delete old keys after new key is successfully created
359
- if (userKeys.length > 0 && newKey) {
360
- await Promise.all(userKeys.map((key) => makeAdminRequest(`/admin/keys/${key.id}`, {
361
- method: 'DELETE'
362
- }).catch((err) => {
363
- console.warn(`[updateUserPermissions] Failed to delete key ${key.id}:`, err);
364
- })));
411
+ const filteredPermissions = PERMISSION_PREFIX
412
+ ? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
413
+ : permissions;
414
+ if (userKeys.length === 0) {
415
+ // No active key exists, create new one
416
+ const newKeyResult = await createUserPermissions(userId, permissions);
417
+ const newApiKey = newKeyResult?.data?.key;
418
+ if (newApiKey) {
419
+ const currentUser = await makeClerkRequest(`/users/${userId}`);
420
+ await makeClerkRequest(`/users/${userId}`, {
421
+ method: 'PATCH',
422
+ body: JSON.stringify({
423
+ private_metadata: {
424
+ ...(currentUser.private_metadata || {}),
425
+ mako_api_key: newApiKey
426
+ }
427
+ })
428
+ });
429
+ }
430
+ }
431
+ else {
432
+ // Use PUT to update existing key (per Mako Auth API spec)
433
+ const keyId = userKeys[0].id;
434
+ // Get the API key string before updating
435
+ const keyData = await makeAdminRequest(`/admin/keys/${keyId}`);
436
+ const apiKeyString = keyData?.data?.key;
437
+ await makeAdminRequest(`/admin/keys/${keyId}`, {
438
+ method: 'PUT',
439
+ body: JSON.stringify({
440
+ scopes: filteredPermissions
441
+ })
442
+ });
443
+ // Verify the token has updated scopes
444
+ if (apiKeyString) {
445
+ try {
446
+ const verification = await verifyApiKeyToken(apiKeyString);
447
+ if (verification.valid) {
448
+ console.log('[updateUserPermissions] Token verification successful. Scopes:', verification.scopes);
449
+ // Check if the scopes match what we expect
450
+ const scopesMatch = filteredPermissions.every(perm => verification.scopes?.includes(perm));
451
+ if (!scopesMatch) {
452
+ console.warn('[updateUserPermissions] Token scopes do not match expected permissions');
453
+ }
454
+ }
455
+ else {
456
+ console.warn('[updateUserPermissions] Token verification failed:', verification.error);
457
+ }
458
+ }
459
+ catch (verifyError) {
460
+ console.warn('[updateUserPermissions] Could not verify token:', verifyError);
461
+ }
462
+ }
463
+ // Clean up any extra keys (there should only be one)
464
+ if (userKeys.length > 1) {
465
+ await Promise.all(userKeys.slice(1).map((key) => makeAdminRequest(`/admin/keys/${key.id}`, {
466
+ method: 'DELETE'
467
+ }).catch((err) => {
468
+ console.warn(`[updateUserPermissions] Failed to delete extra key ${key.id}:`, err);
469
+ })));
470
+ }
365
471
  }
366
472
  await refreshTokenIfSelfUpdate(userId);
367
473
  }
@@ -379,30 +485,75 @@ export const generateApiKey = command('unchecked', async (options) => {
379
485
  if (filteredPermissions.length === 0 && PERMISSION_PREFIX) {
380
486
  filteredPermissions = [`${PERMISSION_PREFIX}readonly`];
381
487
  }
382
- let oldKeyId = null;
383
- if (options.revokeOld) {
488
+ // Check if user has existing active key
489
+ const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${options.userId}`);
490
+ const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
491
+ let newApiKey;
492
+ let wasRotated = false;
493
+ if (userKeys.length > 0 && options.revokeOld) {
494
+ // Use rotate endpoint (per Mako Auth API spec)
495
+ const keyId = userKeys[0].id;
496
+ // Get the old API key string before rotating
497
+ const oldKeyData = await makeAdminRequest(`/admin/keys/${keyId}`);
498
+ const oldApiKey = oldKeyData?.data?.key;
499
+ const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
500
+ method: 'POST',
501
+ body: JSON.stringify({
502
+ scopes: filteredPermissions
503
+ })
504
+ });
505
+ // Rotate endpoint returns key in data.key field
506
+ newApiKey = rotateResult?.data?.key;
507
+ wasRotated = true;
508
+ if (!newApiKey) {
509
+ throw new Error('Failed to rotate API key - no key in response');
510
+ }
511
+ // Verify old key is revoked
512
+ if (oldApiKey) {
513
+ try {
514
+ const oldKeyVerification = await verifyApiKeyToken(oldApiKey);
515
+ if (oldKeyVerification.valid) {
516
+ console.warn('[generateApiKey] Old API key is still valid after rotation');
517
+ }
518
+ else {
519
+ console.log('[generateApiKey] Old API key successfully revoked');
520
+ }
521
+ }
522
+ catch (verifyError) {
523
+ console.warn('[generateApiKey] Could not verify old key revocation:', verifyError);
524
+ }
525
+ }
526
+ // Verify new key works with correct scopes
384
527
  try {
385
- const allKeysData = await makeAdminRequest('/admin/keys');
386
- if (allKeysData?.data?.data && Array.isArray(allKeysData.data.data)) {
387
- const userKey = allKeysData.data.data.find((key) => key.sub === options.userId && key.client_id === CLIENT_ID && key.status === 'active');
388
- if (userKey) {
389
- oldKeyId = userKey.id;
528
+ const newKeyVerification = await verifyApiKeyToken(newApiKey);
529
+ if (newKeyVerification.valid) {
530
+ console.log('[generateApiKey] New API key verification successful. Scopes:', newKeyVerification.scopes);
531
+ // Check if the scopes match what we expect
532
+ const scopesMatch = filteredPermissions.every(perm => newKeyVerification.scopes?.includes(perm));
533
+ if (!scopesMatch) {
534
+ console.warn('[generateApiKey] New key scopes do not match expected permissions');
390
535
  }
391
536
  }
537
+ else {
538
+ console.warn('[generateApiKey] New API key verification failed:', newKeyVerification.error);
539
+ }
392
540
  }
393
- catch (e) {
394
- console.warn('[generateApiKey] Could not fetch existing key for revocation:', e);
541
+ catch (verifyError) {
542
+ console.warn('[generateApiKey] Could not verify new key:', verifyError);
395
543
  }
396
544
  }
397
- const createData = await createUserPermissions(options.userId, filteredPermissions);
398
- if (!createData) {
399
- throw new Error('Failed to create admin key');
400
- }
401
- const newApiKey = createData?.data?.key;
402
- if (!newApiKey) {
403
- console.error('[generateApiKey] No API key in response:', createData);
404
- throw new Error('Failed to generate API key - no key in response');
545
+ else {
546
+ // Create new key if none exists or revokeOld is false
547
+ const createData = await createUserPermissions(options.userId, filteredPermissions);
548
+ if (!createData) {
549
+ throw new Error('Failed to create admin key');
550
+ }
551
+ newApiKey = createData?.data?.key;
552
+ if (!newApiKey) {
553
+ throw new Error('Failed to generate API key - no key in response');
554
+ }
405
555
  }
556
+ // Update Clerk profile with new key
406
557
  try {
407
558
  const currentUser = await makeClerkRequest(`/users/${options.userId}`);
408
559
  await makeClerkRequest(`/users/${options.userId}`, {
@@ -419,22 +570,11 @@ export const generateApiKey = command('unchecked', async (options) => {
419
570
  console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
420
571
  console.warn('[generateApiKey] Key generated but could not update Clerk profile');
421
572
  }
422
- if (oldKeyId) {
423
- try {
424
- await makeAdminRequest(`/admin/keys/${oldKeyId}`, {
425
- method: 'DELETE'
426
- });
427
- }
428
- catch (revokeError) {
429
- console.error('[generateApiKey] Failed to revoke old key:', revokeError);
430
- console.warn('[generateApiKey] New key generated but could not revoke old key');
431
- }
432
- }
433
573
  const result = {
434
574
  success: true,
435
575
  apiKey: newApiKey,
436
- message: oldKeyId
437
- ? 'New API key generated and old key revoked successfully'
576
+ message: wasRotated
577
+ ? 'API key rotated successfully'
438
578
  : 'API key generated successfully'
439
579
  };
440
580
  return JSON.parse(JSON.stringify(result));
@@ -444,3 +584,29 @@ export const generateApiKey = command('unchecked', async (options) => {
444
584
  throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
445
585
  }
446
586
  });
587
+ export const verifyToken = command('unchecked', async (options) => {
588
+ try {
589
+ const result = await verifyApiKeyToken(options.apiKey);
590
+ // Also return the issued token for debugging
591
+ if (result.valid) {
592
+ const tokenResult = await makeAuthRequest('/auth/issue', {
593
+ method: 'POST',
594
+ headers: {
595
+ 'X-API-Key': options.apiKey
596
+ }
597
+ });
598
+ return {
599
+ ...result,
600
+ token: tokenResult.data?.data?.access_token
601
+ };
602
+ }
603
+ return result;
604
+ }
605
+ catch (error) {
606
+ console.error('[verifyToken] Error:', error);
607
+ return {
608
+ valid: false,
609
+ error: error instanceof Error ? error.message : 'Unknown error during verification'
610
+ };
611
+ }
612
+ });
package/dist/index.d.ts CHANGED
@@ -1031,6 +1031,14 @@ export interface UserManagementAdapter {
1031
1031
  apiKey: string;
1032
1032
  message: string;
1033
1033
  }>;
1034
+ verifyToken?: (options: {
1035
+ apiKey: string;
1036
+ }) => PromiseLike<{
1037
+ valid: boolean;
1038
+ scopes?: string[];
1039
+ error?: string;
1040
+ token?: string;
1041
+ }>;
1034
1042
  }
1035
1043
  export interface UserManagementProps {
1036
1044
  /**
@@ -30,6 +30,8 @@
30
30
  let formElement = $state<HTMLFormElement | null>(null);
31
31
  let showApiKey = $state(false);
32
32
  let regeneratingApiKey = $state(false);
33
+ let verifyingToken = $state(false);
34
+ let tokenVerification = $state<{ valid?: boolean; scopes?: string[]; error?: string } | null>(null);
33
35
  let initialRole = $state<string>('');
34
36
 
35
37
  // Form data
@@ -131,6 +133,7 @@
131
133
  open = false;
132
134
  formErrors = {};
133
135
  saving = false;
136
+ tokenVerification = null;
134
137
  if (onClose) onClose();
135
138
  }
136
139
 
@@ -184,6 +187,9 @@
184
187
  role: roleValue,
185
188
  permissions: role ? [...role.permissions] : []
186
189
  };
190
+
191
+ // Clear token verification when permissions change
192
+ tokenVerification = null;
187
193
  }
188
194
 
189
195
  async function handleRegenerateApiKey() {
@@ -197,6 +203,7 @@
197
203
 
198
204
  try {
199
205
  regeneratingApiKey = true;
206
+ tokenVerification = null; // Clear previous verification
200
207
  const result = await adapter.generateApiKey({
201
208
  userId: user.id,
202
209
  permissions: formData.permissions,
@@ -221,6 +228,27 @@
221
228
  }
222
229
  }
223
230
 
231
+ async function handleVerifyToken() {
232
+ if (!adapter?.verifyToken || !apiKey) return;
233
+
234
+ try {
235
+ verifyingToken = true;
236
+ formErrors.apiKey = undefined;
237
+ const result = await adapter.verifyToken({ apiKey });
238
+ tokenVerification = result;
239
+
240
+ if (!result.valid) {
241
+ formErrors.apiKey = result.error || 'Token verification failed';
242
+ }
243
+ } catch (error) {
244
+ console.error('Error verifying token:', error);
245
+ formErrors.apiKey = error instanceof Error ? error.message : 'Failed to verify token';
246
+ tokenVerification = { valid: false, error: 'Verification failed' };
247
+ } finally {
248
+ verifyingToken = false;
249
+ }
250
+ }
251
+
224
252
  function getModalTitle() {
225
253
  if (mode === 'create') return 'Create New User';
226
254
  return `Edit ${getUserDisplayName(user ?? null)}`;
@@ -320,32 +348,58 @@
320
348
  <label for="api-key" class="text-default-700 block text-sm font-medium">
321
349
  Mako API Key
322
350
  </label>
323
- {#if adapter?.generateApiKey}
324
- <button
325
- type="button"
326
- onclick={handleRegenerateApiKey}
327
- disabled={regeneratingApiKey ||
328
- !formData.permissions ||
329
- formData.permissions.length === 0}
330
- 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"
331
- aria-label="Regenerate API key"
332
- >
333
- <svg
334
- class="h-4 w-4 {regeneratingApiKey ? 'animate-spin' : ''}"
335
- fill="none"
336
- stroke="currentColor"
337
- viewBox="0 0 24 24"
351
+ <div class="flex items-center gap-3">
352
+ {#if adapter?.verifyToken && apiKey}
353
+ <button
354
+ type="button"
355
+ onclick={handleVerifyToken}
356
+ disabled={verifyingToken}
357
+ 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
+ aria-label="Verify token"
338
359
  >
339
- <path
340
- stroke-linecap="round"
341
- stroke-linejoin="round"
342
- stroke-width="2"
343
- 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"
344
- ></path>
345
- </svg>
346
- Regenerate
347
- </button>
348
- {/if}
360
+ <svg
361
+ class="h-4 w-4 {verifyingToken ? 'animate-spin' : ''}"
362
+ fill="none"
363
+ stroke="currentColor"
364
+ viewBox="0 0 24 24"
365
+ >
366
+ <path
367
+ stroke-linecap="round"
368
+ stroke-linejoin="round"
369
+ stroke-width="2"
370
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
371
+ ></path>
372
+ </svg>
373
+ Verify
374
+ </button>
375
+ {/if}
376
+ {#if adapter?.generateApiKey}
377
+ <button
378
+ type="button"
379
+ onclick={handleRegenerateApiKey}
380
+ disabled={regeneratingApiKey ||
381
+ !formData.permissions ||
382
+ formData.permissions.length === 0}
383
+ 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
+ aria-label="Regenerate API key"
385
+ >
386
+ <svg
387
+ class="h-4 w-4 {regeneratingApiKey ? 'animate-spin' : ''}"
388
+ fill="none"
389
+ stroke="currentColor"
390
+ viewBox="0 0 24 24"
391
+ >
392
+ <path
393
+ stroke-linecap="round"
394
+ stroke-linejoin="round"
395
+ stroke-width="2"
396
+ 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"
397
+ ></path>
398
+ </svg>
399
+ Regenerate
400
+ </button>
401
+ {/if}
402
+ </div>
349
403
  </div>
350
404
  <div class="relative flex-1">
351
405
  <input
@@ -391,6 +445,38 @@
391
445
  </div>
392
446
  {#if formErrors.apiKey}
393
447
  <p class="text-danger-500 mt-1 text-xs">{formErrors.apiKey}</p>
448
+ {:else if tokenVerification}
449
+ {#if tokenVerification.valid}
450
+ <div class="bg-success-50 border-success-200 mt-2 rounded-lg border p-3">
451
+ <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>
454
+ </svg>
455
+ <div class="min-w-0 flex-1">
456
+ <p class="text-success-800 text-xs font-medium">Token verified successfully</p>
457
+ {#if tokenVerification.scopes && tokenVerification.scopes.length > 0}
458
+ <p class="text-success-700 mt-1 text-xs">
459
+ Scopes: {tokenVerification.scopes.join(', ')}
460
+ </p>
461
+ {/if}
462
+ </div>
463
+ </div>
464
+ </div>
465
+ {:else}
466
+ <div class="bg-danger-50 border-danger-200 mt-2 rounded-lg border p-3">
467
+ <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>
470
+ </svg>
471
+ <div class="min-w-0 flex-1">
472
+ <p class="text-danger-800 text-xs font-medium">Token verification failed</p>
473
+ {#if tokenVerification.error}
474
+ <p class="text-danger-700 mt-1 text-xs">{tokenVerification.error}</p>
475
+ {/if}
476
+ </div>
477
+ </div>
478
+ </div>
479
+ {/if}
394
480
  {:else}
395
481
  <p class="text-default-500 mt-1 text-xs">
396
482
  API keys are system-managed and cannot be manually edited
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {