@makolabs/ripple 1.6.5 → 1.6.7

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))
@@ -375,12 +431,35 @@ export const updateUserPermissions = command('unchecked', async (options) => {
375
431
  else {
376
432
  // Use PUT to update existing key (per Mako Auth API spec)
377
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;
378
437
  await makeAdminRequest(`/admin/keys/${keyId}`, {
379
438
  method: 'PUT',
380
439
  body: JSON.stringify({
381
440
  scopes: filteredPermissions
382
441
  })
383
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
+ }
384
463
  // Clean up any extra keys (there should only be one)
385
464
  if (userKeys.length > 1) {
386
465
  await Promise.all(userKeys.slice(1).map((key) => makeAdminRequest(`/admin/keys/${key.id}`, {
@@ -414,6 +493,9 @@ export const generateApiKey = command('unchecked', async (options) => {
414
493
  if (userKeys.length > 0 && options.revokeOld) {
415
494
  // Use rotate endpoint (per Mako Auth API spec)
416
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;
417
499
  const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
418
500
  method: 'POST',
419
501
  body: JSON.stringify({
@@ -426,6 +508,39 @@ export const generateApiKey = command('unchecked', async (options) => {
426
508
  if (!newApiKey) {
427
509
  throw new Error('Failed to rotate API key - no key in response');
428
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
527
+ try {
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');
535
+ }
536
+ }
537
+ else {
538
+ console.warn('[generateApiKey] New API key verification failed:', newKeyVerification.error);
539
+ }
540
+ }
541
+ catch (verifyError) {
542
+ console.warn('[generateApiKey] Could not verify new key:', verifyError);
543
+ }
429
544
  }
430
545
  else {
431
546
  // Create new key if none exists or revokeOld is false
@@ -469,3 +584,29 @@ export const generateApiKey = command('unchecked', async (options) => {
469
584
  throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
470
585
  }
471
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.5",
3
+ "version": "1.6.7",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {