@makolabs/ripple 1.6.5 → 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))
|
|
@@ -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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
stroke
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|