@makolabs/ripple 1.7.5 → 1.7.10

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.
@@ -27,6 +27,7 @@ export declare const verifyToken: import("@sveltejs/kit").RemoteCommand<{
27
27
  valid: boolean;
28
28
  scopes?: string[];
29
29
  sub?: string;
30
+ client_id?: string;
30
31
  error?: string;
31
32
  token?: string;
32
33
  }>>;
@@ -1,8 +1,32 @@
1
1
  import { query, command } from '$app/server';
2
2
  import { getRequestEvent } from '$app/server';
3
3
  import { env } from '$env/dynamic/private';
4
- const CLIENT_ID = env.CLIENT_ID || 'sharkfin';
4
+ import { building } from '$app/environment';
5
+ const CLIENT_ID = env.CLIENT_ID;
5
6
  const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
7
+ if (!CLIENT_ID && !building) {
8
+ throw new Error('CLIENT_ID environment variable is required');
9
+ }
10
+ if (!ORGANIZATION_ID && !building) {
11
+ throw new Error('ALLOWED_ORG_ID environment variable is required');
12
+ }
13
+ const isTraceLevel = () => env.LOG_LEVEL?.toLowerCase() === 'trace';
14
+ const log = {
15
+ trace: (tag, ...args) => {
16
+ if (isTraceLevel())
17
+ console.debug(`[TRACE][${tag}]`, ...args);
18
+ },
19
+ info: (tag, ...args) => {
20
+ if (isTraceLevel())
21
+ console.log(`[${tag}]`, ...args);
22
+ },
23
+ warn: (tag, ...args) => {
24
+ console.warn(`[${tag}]`, ...args);
25
+ },
26
+ error: (tag, ...args) => {
27
+ console.error(`[${tag}]`, ...args);
28
+ }
29
+ };
6
30
  function handleClerkError(error, defaultMessage) {
7
31
  if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
8
32
  const enrichedError = new Error('message' in error && typeof error.message === 'string' ? error.message : 'Unknown error');
@@ -18,7 +42,20 @@ async function makeClerkRequest(endpoint, options = {}) {
18
42
  if (!CLERK_SECRET_KEY) {
19
43
  throw new Error('CLERK_SECRET_KEY environment variable is required');
20
44
  }
21
- const response = await fetch(`https://api.clerk.com/v1${endpoint}`, {
45
+ const method = options.method || 'GET';
46
+ const url = `https://api.clerk.com/v1${endpoint}`;
47
+ log.info('Clerk API', `${method} ${url}`);
48
+ if (options.body)
49
+ log.info('Clerk API', `Payload: ${options.body}`);
50
+ log.trace('Clerk API', 'Request headers:', {
51
+ Authorization: 'Bearer [REDACTED]',
52
+ 'Content-Type': 'application/json',
53
+ ...options.headers
54
+ });
55
+ if (options.body)
56
+ log.trace('Clerk API', 'Parsed payload:', JSON.parse(options.body));
57
+ const start = performance.now();
58
+ const response = await fetch(url, {
22
59
  ...options,
23
60
  headers: {
24
61
  Authorization: `Bearer ${CLERK_SECRET_KEY}`,
@@ -26,9 +63,12 @@ async function makeClerkRequest(endpoint, options = {}) {
26
63
  ...options.headers
27
64
  }
28
65
  });
66
+ const elapsed = (performance.now() - start).toFixed(2);
29
67
  if (!response.ok) {
30
68
  const errorText = await response.text();
31
- console.error(`[Clerk API] ${response.status} ${response.statusText} - ${errorText}`);
69
+ log.error('Clerk API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
70
+ log.error('Clerk API', `Body: ${errorText}`);
71
+ log.trace('Clerk API', 'Response headers:', Object.fromEntries(response.headers.entries()));
32
72
  let errorDetails;
33
73
  try {
34
74
  errorDetails = JSON.parse(errorText);
@@ -42,6 +82,10 @@ async function makeClerkRequest(endpoint, options = {}) {
42
82
  throw error;
43
83
  }
44
84
  const data = await response.json();
85
+ log.info('Clerk API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
86
+ log.info('Clerk API', `Body: ${JSON.stringify(data).slice(0, 500)}`);
87
+ log.trace('Clerk API', 'Full response body:', data);
88
+ log.trace('Clerk API', 'Response headers:', Object.fromEntries(response.headers.entries()));
45
89
  // Ensure all data is serializable by converting to plain objects
46
90
  return JSON.parse(JSON.stringify(data));
47
91
  }
@@ -56,7 +100,19 @@ async function makeAdminRequest(endpoint, options = {}) {
56
100
  missing.push('PRIVATE_BASE_AUTH_URL');
57
101
  throw new Error(`Admin API configuration missing: ${missing.join(', ')}`);
58
102
  }
103
+ const method = options.method || 'GET';
59
104
  const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
105
+ log.info('Admin API', `${method} ${url}`);
106
+ if (options.body)
107
+ log.info('Admin API', `Payload: ${options.body}`);
108
+ log.trace('Admin API', 'Request headers:', {
109
+ 'X-Admin-API-Key': '[REDACTED]',
110
+ 'Content-Type': 'application/json',
111
+ ...options.headers
112
+ });
113
+ if (options.body)
114
+ log.trace('Admin API', 'Parsed payload:', JSON.parse(options.body));
115
+ const start = performance.now();
60
116
  const response = await fetch(url, {
61
117
  ...options,
62
118
  headers: {
@@ -65,12 +121,19 @@ async function makeAdminRequest(endpoint, options = {}) {
65
121
  ...options.headers
66
122
  }
67
123
  });
124
+ const elapsed = (performance.now() - start).toFixed(2);
68
125
  if (!response.ok) {
69
126
  const errorText = await response.text();
70
- console.error(`[Admin API] ${response.status} ${response.statusText} - ${errorText}`);
127
+ log.error('Admin API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
128
+ log.error('Admin API', `Body: ${errorText}`);
129
+ log.trace('Admin API', 'Response headers:', Object.fromEntries(response.headers.entries()));
71
130
  throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
72
131
  }
73
132
  const data = await response.json();
133
+ log.info('Admin API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
134
+ log.info('Admin API', `Body: ${JSON.stringify(data).slice(0, 500)}`);
135
+ log.trace('Admin API', 'Full response body:', data);
136
+ log.trace('Admin API', 'Response headers:', Object.fromEntries(response.headers.entries()));
74
137
  // Ensure all data is serializable by converting to plain objects
75
138
  return JSON.parse(JSON.stringify(data));
76
139
  }
@@ -79,7 +142,16 @@ async function makeAuthRequest(endpoint, options = {}) {
79
142
  if (!PRIVATE_BASE_AUTH_URL) {
80
143
  throw new Error('PRIVATE_BASE_AUTH_URL environment variable is required');
81
144
  }
145
+ const method = options.method || 'GET';
82
146
  const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
147
+ log.info('Auth API', `${method} ${url}`);
148
+ if (options.body)
149
+ log.info('Auth API', `Payload: ${options.body}`);
150
+ log.trace('Auth API', 'Request headers:', {
151
+ 'Content-Type': 'application/json',
152
+ ...options.headers
153
+ });
154
+ const start = performance.now();
83
155
  const response = await fetch(url, {
84
156
  ...options,
85
157
  headers: {
@@ -87,6 +159,7 @@ async function makeAuthRequest(endpoint, options = {}) {
87
159
  ...options.headers
88
160
  }
89
161
  });
162
+ const elapsed = (performance.now() - start).toFixed(2);
90
163
  const text = await response.text();
91
164
  let data;
92
165
  try {
@@ -96,6 +169,10 @@ async function makeAuthRequest(endpoint, options = {}) {
96
169
  // Not JSON, treat as plain text error (e.g., "404 page not found")
97
170
  data = { error: text, message: text };
98
171
  }
172
+ log.info('Auth API', `Response: ${response.status} ${response.statusText} (${elapsed}ms)`);
173
+ log.info('Auth API', `Body: ${JSON.stringify(data).slice(0, 500)}`);
174
+ log.trace('Auth API', 'Full response body:', data);
175
+ log.trace('Auth API', 'Response headers:', Object.fromEntries(response.headers.entries()));
99
176
  return {
100
177
  ok: response.ok,
101
178
  status: response.status,
@@ -103,6 +180,7 @@ async function makeAuthRequest(endpoint, options = {}) {
103
180
  };
104
181
  }
105
182
  async function verifyApiKeyToken(apiKey) {
183
+ log.trace('verifyApiKeyToken', 'Called with apiKey:', apiKey.slice(0, 8) + '...[REDACTED]');
106
184
  try {
107
185
  const result = await makeAuthRequest('/auth/token', {
108
186
  method: 'POST',
@@ -110,37 +188,43 @@ async function verifyApiKeyToken(apiKey) {
110
188
  'X-API-Key': apiKey
111
189
  }
112
190
  });
191
+ log.trace('verifyApiKeyToken', 'Token exchange result ok:', result.ok);
113
192
  if (result.ok && result.data?.data?.access_token) {
114
193
  const token = result.data.data.access_token;
194
+ log.trace('verifyApiKeyToken', 'Got access_token, verifying...');
115
195
  const verifyResult = await makeAuthRequest('/auth/verify', {
116
196
  method: 'GET',
117
197
  headers: {
118
198
  Authorization: `Bearer ${token}`
119
199
  }
120
200
  });
201
+ log.trace('verifyApiKeyToken', 'Verify result ok:', verifyResult.ok, 'data:', verifyResult.data);
121
202
  if (verifyResult.ok && verifyResult.data?.data) {
122
203
  // The API returns "scope" (singular) as a space-separated string, not "scopes" array
123
204
  const scopeString = verifyResult.data.data.scope;
124
205
  const scopes = scopeString ? scopeString.split(' ').filter(Boolean) : [];
125
206
  const sub = verifyResult.data.data.sub;
207
+ const client_id = verifyResult.data.data.client_id;
208
+ log.trace('verifyApiKeyToken', 'Verified. sub:', sub, 'scopes:', scopes, 'client_id:', client_id);
126
209
  return {
127
210
  valid: true,
128
211
  scopes: scopes,
129
- sub: sub
212
+ sub: sub,
213
+ client_id: client_id
130
214
  };
131
215
  }
132
216
  }
133
217
  const errorMsg = result.data?.message ||
134
218
  result.data?.error ||
135
219
  `API key verification failed with status ${result.status}`;
136
- console.warn('[verifyApiKeyToken] Verification failed:', errorMsg);
220
+ log.warn('verifyApiKeyToken', 'Verification failed:', errorMsg);
137
221
  return {
138
222
  valid: false,
139
223
  error: errorMsg
140
224
  };
141
225
  }
142
226
  catch (error) {
143
- console.error('[verifyApiKeyToken] Exception during verification:', error);
227
+ log.error('verifyApiKeyToken', 'Exception during verification:', error);
144
228
  return {
145
229
  valid: false,
146
230
  error: error instanceof Error ? error.message : 'Unknown error during verification'
@@ -148,10 +232,12 @@ async function verifyApiKeyToken(apiKey) {
148
232
  }
149
233
  }
150
234
  async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
235
+ log.trace('createUserPermissions', 'email:', email, 'permissions:', permissions, 'clientId:', clientId);
151
236
  if (permissions.length === 0) {
237
+ log.trace('createUserPermissions', 'No permissions to create, returning null');
152
238
  return null;
153
239
  }
154
- return await makeAdminRequest('/admin/keys', {
240
+ const result = await makeAdminRequest('/admin/keys', {
155
241
  method: 'POST',
156
242
  body: JSON.stringify({
157
243
  client_id: clientId,
@@ -159,8 +245,11 @@ async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
159
245
  scopes: permissions
160
246
  })
161
247
  });
248
+ log.trace('createUserPermissions', 'Result:', result);
249
+ return result;
162
250
  }
163
251
  export const getUsers = query('unchecked', async (options) => {
252
+ log.trace('getUsers', 'Called with options:', options);
164
253
  try {
165
254
  const limit = options.pageSize;
166
255
  const offset = (options.page - 1) * options.pageSize;
@@ -179,12 +268,14 @@ export const getUsers = query('unchecked', async (options) => {
179
268
  });
180
269
  if (options.query)
181
270
  params.append('query', options.query);
271
+ log.trace('getUsers', 'Query params:', params.toString());
182
272
  const [usersData, countData] = await Promise.all([
183
273
  makeClerkRequest(`/users?${params}`),
184
274
  makeClerkRequest(`/users/count?${params}`)
185
275
  ]);
186
276
  // Extract users array - Clerk API returns { data: [...] } or array directly
187
277
  const users = Array.isArray(usersData) ? usersData : usersData?.data || [];
278
+ log.trace('getUsers', 'Fetched', users.length, 'users, total count:', countData);
188
279
  // Ensure all data is serializable by converting to plain objects
189
280
  const serializedUsers = JSON.parse(JSON.stringify(users));
190
281
  const serializedCountData = JSON.parse(JSON.stringify(countData));
@@ -195,11 +286,12 @@ export const getUsers = query('unchecked', async (options) => {
195
286
  }));
196
287
  }
197
288
  catch (error) {
198
- console.error('[getUsers] Error:', error);
289
+ log.error('getUsers', 'Error:', error);
199
290
  throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
200
291
  }
201
292
  });
202
293
  export const createUser = command('unchecked', async (userData) => {
294
+ log.trace('createUser', 'Called with userData:', { ...userData, private_metadata: '[REDACTED]' });
203
295
  const emailAddress = userData.email_addresses?.[0]?.email_address || '';
204
296
  if (!emailAddress) {
205
297
  throw new Error('Email address is required');
@@ -213,13 +305,16 @@ export const createUser = command('unchecked', async (userData) => {
213
305
  ...(userDataOnly.username && { username: userDataOnly.username }),
214
306
  ...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
215
307
  };
308
+ log.trace('createUser', 'Creating Clerk user with data:', clerkUserData);
216
309
  let result = await makeClerkRequest('/users', {
217
310
  method: 'POST',
218
311
  body: JSON.stringify(clerkUserData)
219
312
  });
220
313
  // Ensure result is serializable
221
314
  result = JSON.parse(JSON.stringify(result));
315
+ log.trace('createUser', 'Clerk user created, id:', result.id);
222
316
  if (ORGANIZATION_ID) {
317
+ log.trace('createUser', 'Adding user to org:', ORGANIZATION_ID);
223
318
  try {
224
319
  await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
225
320
  method: 'POST',
@@ -228,24 +323,29 @@ export const createUser = command('unchecked', async (userData) => {
228
323
  role: 'org:member'
229
324
  })
230
325
  });
326
+ log.trace('createUser', 'User added to org successfully');
231
327
  }
232
328
  catch (orgError) {
233
- console.error('[createUser] Failed to add user to organization:', orgError);
329
+ log.error('createUser', 'Failed to add user to organization:', orgError);
330
+ log.trace('createUser', 'Rolling back: deleting user', result.id);
234
331
  try {
235
332
  await makeClerkRequest(`/users/${result.id}`, {
236
333
  method: 'DELETE'
237
334
  });
335
+ log.trace('createUser', 'Rollback delete successful');
238
336
  }
239
337
  catch (deleteError) {
240
- console.error('[createUser] Failed to delete user after org membership failure:', deleteError);
338
+ log.error('createUser', 'Failed to delete user after org membership failure:', deleteError);
241
339
  }
242
340
  throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
243
341
  }
244
342
  }
245
343
  if (permissions && permissions.length > 0) {
344
+ log.trace('createUser', 'Assigning permissions:', permissions);
246
345
  try {
247
346
  const adminKeyResult = await createUserPermissions(emailAddress, permissions);
248
347
  const apiKey = adminKeyResult?.data?.key;
348
+ log.trace('createUser', 'Admin key created, has key:', !!apiKey);
249
349
  if (adminKeyResult && apiKey) {
250
350
  const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
251
351
  method: 'PATCH',
@@ -263,22 +363,27 @@ export const createUser = command('unchecked', async (userData) => {
263
363
  result.permissionsAssigned = permissions;
264
364
  }
265
365
  catch (permissionError) {
266
- console.error('[createUser] Failed to assign permissions:', permissionError);
366
+ log.error('createUser', 'Failed to assign permissions:', permissionError);
267
367
  result.warning = 'User created but permissions assignment failed';
268
368
  result.permissionError =
269
369
  permissionError instanceof Error ? permissionError.message : String(permissionError);
270
370
  }
271
371
  }
372
+ log.trace('createUser', 'Final result:', {
373
+ id: result.id,
374
+ permissionsAssigned: result.permissionsAssigned
375
+ });
272
376
  // Final serialization check before returning
273
377
  return JSON.parse(JSON.stringify(result));
274
378
  }
275
379
  catch (error) {
276
- console.error('[createUser] Error:', error);
380
+ log.error('createUser', 'Error:', error);
277
381
  handleClerkError(error, 'Failed to create user');
278
382
  }
279
383
  });
280
384
  export const updateUser = command('unchecked', async (options) => {
281
385
  const { userId, userData } = options;
386
+ log.trace('updateUser', 'Called for userId:', userId, 'fields:', Object.keys(userData));
282
387
  try {
283
388
  const updateData = {};
284
389
  if (userData.first_name !== undefined)
@@ -291,6 +396,7 @@ export const updateUser = command('unchecked', async (options) => {
291
396
  if (userData.private_metadata !== undefined) {
292
397
  updateData.private_metadata = userData.private_metadata;
293
398
  }
399
+ log.trace('updateUser', 'Update payload:', updateData);
294
400
  let result = await makeClerkRequest(`/users/${userId}`, {
295
401
  method: 'PATCH',
296
402
  body: JSON.stringify(updateData)
@@ -298,79 +404,95 @@ export const updateUser = command('unchecked', async (options) => {
298
404
  // Ensure result is serializable
299
405
  result = JSON.parse(JSON.stringify(result));
300
406
  if (userData.permissions !== undefined) {
407
+ log.trace('updateUser', 'Updating permissions:', userData.permissions);
301
408
  try {
302
409
  await updateUserPermissions({ userId, permissions: userData.permissions });
303
410
  }
304
411
  catch (permError) {
305
- console.error('[updateUser] Failed to update permissions:', permError);
412
+ log.error('updateUser', 'Failed to update permissions:', permError);
306
413
  }
307
414
  }
415
+ log.trace('updateUser', 'Done, result id:', result.id);
308
416
  // Final serialization check before returning
309
417
  return JSON.parse(JSON.stringify(result));
310
418
  }
311
419
  catch (error) {
312
- console.error('[updateUser] Error:', error);
420
+ log.error('updateUser', 'Error:', error);
313
421
  handleClerkError(error, 'Failed to update user');
314
422
  }
315
423
  });
316
424
  export const deleteUser = command('unchecked', async (userId) => {
425
+ log.trace('deleteUser', 'Called for userId:', userId);
317
426
  try {
318
427
  await makeClerkRequest(`/users/${userId}`, {
319
428
  method: 'DELETE'
320
429
  });
430
+ log.trace('deleteUser', 'Deleted successfully');
321
431
  }
322
432
  catch (error) {
323
- console.error('[deleteUser] Error:', error);
433
+ log.error('deleteUser', 'Error:', error);
324
434
  throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
325
435
  }
326
436
  });
327
437
  export const deleteUsers = command('unchecked', async (userIds) => {
438
+ log.trace('deleteUsers', 'Called for userIds:', userIds);
328
439
  try {
329
440
  await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
330
441
  method: 'DELETE'
331
442
  })));
443
+ log.trace('deleteUsers', 'All', userIds.length, 'users deleted successfully');
332
444
  }
333
445
  catch (error) {
334
- console.error('[deleteUsers] Error:', error);
446
+ log.error('deleteUsers', 'Error:', error);
335
447
  throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
336
448
  }
337
449
  });
338
450
  async function fetchUserPermissions(email) {
451
+ log.trace('fetchUserPermissions', 'Called for email:', email);
339
452
  try {
340
453
  const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
454
+ log.trace('fetchUserPermissions', 'Raw keys data:', userData);
341
455
  if (userData?.data?.data && Array.isArray(userData.data.data)) {
342
456
  userData.data.data = userData.data.data.filter((key) => key.status === 'active');
457
+ log.trace('fetchUserPermissions', 'Active keys count:', userData.data.data.length);
343
458
  }
344
459
  if (userData?.data?.data && Array.isArray(userData.data.data)) {
345
460
  // Deduplicate permissions by using a Set
346
461
  const allPermissions = userData.data.data.flatMap((key) => key.scopes || []);
347
462
  const dedupedPermissions = Array.from(new Set(allPermissions));
463
+ log.trace('fetchUserPermissions', 'Deduped permissions:', dedupedPermissions);
348
464
  return dedupedPermissions;
349
465
  }
350
466
  else if (userData?.scopes) {
351
467
  const permissions = Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
468
+ log.trace('fetchUserPermissions', 'Permissions from scopes field:', permissions);
352
469
  return permissions;
353
470
  }
471
+ log.trace('fetchUserPermissions', 'No permissions found, returning []');
354
472
  return [];
355
473
  }
356
474
  catch (error) {
357
- console.error('[fetchUserPermissions] Error fetching user permissions:', error);
475
+ log.error('fetchUserPermissions', 'Error fetching user permissions:', error);
476
+ log.trace('fetchUserPermissions', 'Falling back to full keys search');
358
477
  try {
359
478
  const allKeysData = await makeAdminRequest('/admin/keys');
360
479
  const userKey = allKeysData.data.data.find((key) => key.sub === email && key.client_id === CLIENT_ID && key.status === 'active');
361
480
  if (userKey) {
362
481
  const permissions = Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
482
+ log.trace('fetchUserPermissions', 'Found via fallback:', permissions);
363
483
  return permissions;
364
484
  }
485
+ log.trace('fetchUserPermissions', 'No key found in fallback search');
365
486
  return [];
366
487
  }
367
488
  catch (searchError) {
368
- console.error('[fetchUserPermissions] Error searching for user by sub:', searchError);
489
+ log.error('fetchUserPermissions', 'Error searching for user by sub:', searchError);
369
490
  throw new Error('Failed to fetch user permissions');
370
491
  }
371
492
  }
372
493
  }
373
494
  export const getUserPermissions = query('unchecked', async (userId) => {
495
+ log.trace('getUserPermissions', 'Called for userId:', userId);
374
496
  try {
375
497
  // Fetch user from Clerk to get email
376
498
  const user = await makeClerkRequest(`/users/${userId}`);
@@ -378,12 +500,14 @@ export const getUserPermissions = query('unchecked', async (userId) => {
378
500
  if (!email) {
379
501
  throw new Error('User has no email address');
380
502
  }
503
+ log.trace('getUserPermissions', 'Resolved email:', email);
381
504
  const permissions = await fetchUserPermissions(email);
505
+ log.trace('getUserPermissions', 'Returning permissions:', permissions);
382
506
  // Ensure permissions array is serializable
383
507
  return JSON.parse(JSON.stringify(permissions));
384
508
  }
385
509
  catch (error) {
386
- console.error('[getUserPermissions] Error:', error);
510
+ log.error('getUserPermissions', 'Error:', error);
387
511
  throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
388
512
  }
389
513
  });
@@ -398,6 +522,7 @@ async function refreshTokenIfSelfUpdate(userId) {
398
522
  return;
399
523
  const currentUser = getAuth();
400
524
  const isSelfUpdate = currentUser?.userId === userId;
525
+ log.trace('refreshTokenIfSelfUpdate', 'isSelfUpdate:', isSelfUpdate);
401
526
  if (isSelfUpdate) {
402
527
  try {
403
528
  // Try to dynamically import token manager - this is app-specific
@@ -410,6 +535,7 @@ async function refreshTokenIfSelfUpdate(userId) {
410
535
  tokenManager.clearTokenCookies(event.cookies);
411
536
  const newToken = await tokenManager.getAccessToken(event.cookies, event, true);
412
537
  tokenManager.setClientAccessibleToken(event.cookies, newToken);
538
+ log.trace('refreshTokenIfSelfUpdate', 'Token refreshed successfully');
413
539
  }
414
540
  }
415
541
  catch {
@@ -423,6 +549,7 @@ async function refreshTokenIfSelfUpdate(userId) {
423
549
  }
424
550
  export const updateUserPermissions = command('unchecked', async (options) => {
425
551
  const { userId, permissions } = options;
552
+ log.trace('updateUserPermissions', 'Called for userId:', userId, 'permissions:', permissions);
426
553
  try {
427
554
  // Validate permissions - require at least one permission scope
428
555
  if (permissions.length === 0) {
@@ -434,11 +561,14 @@ export const updateUserPermissions = command('unchecked', async (options) => {
434
561
  if (!email) {
435
562
  throw new Error('User has no email address');
436
563
  }
564
+ log.trace('updateUserPermissions', 'Resolved email:', email);
437
565
  // Fetch user's active keys
438
566
  const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
439
567
  const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
568
+ log.trace('updateUserPermissions', 'Active keys count:', userKeys.length);
440
569
  if (userKeys.length === 0) {
441
570
  // No active key exists, create new one
571
+ log.trace('updateUserPermissions', 'No active key, creating new one');
442
572
  const newKeyResult = await createUserPermissions(email, permissions);
443
573
  const newApiKey = newKeyResult?.data?.key;
444
574
  if (newApiKey) {
@@ -451,11 +581,13 @@ export const updateUserPermissions = command('unchecked', async (options) => {
451
581
  }
452
582
  })
453
583
  });
584
+ log.trace('updateUserPermissions', 'New key stored in Clerk metadata');
454
585
  }
455
586
  }
456
587
  else {
457
588
  // Use PUT to update existing key (per Mako Auth API spec)
458
589
  const keyId = userKeys[0].id;
590
+ log.trace('updateUserPermissions', 'Updating existing key:', keyId);
459
591
  // Get the API key string before updating
460
592
  const keyData = await makeAdminRequest(`/admin/keys/${keyId}`);
461
593
  const apiKeyString = keyData?.data?.key;
@@ -465,45 +597,52 @@ export const updateUserPermissions = command('unchecked', async (options) => {
465
597
  scopes: permissions
466
598
  })
467
599
  });
600
+ log.trace('updateUserPermissions', 'Key updated, verifying token...');
468
601
  // Verify the token has updated scopes
469
602
  if (apiKeyString) {
470
603
  try {
471
604
  const verification = await verifyApiKeyToken(apiKeyString);
472
- console.log('[updateUserPermissions] Key verification:', verification);
605
+ log.info('updateUserPermissions', 'Key verification:', verification);
473
606
  if (verification.valid) {
474
607
  // Check if the scopes match what we expect
475
608
  // Note: permissions.length > 0 is guaranteed by validation above
476
609
  const scopesMatch = permissions.length > 0 &&
477
610
  permissions.every((perm) => verification.scopes?.includes(perm));
478
611
  if (!scopesMatch) {
479
- console.warn('[updateUserPermissions] Scopes mismatch. Expected:', permissions, 'Got:', verification.scopes);
612
+ log.warn('updateUserPermissions', 'Scopes mismatch. Expected:', permissions, 'Got:', verification.scopes);
613
+ }
614
+ else {
615
+ log.trace('updateUserPermissions', 'Scopes match confirmed');
480
616
  }
481
617
  }
482
618
  else {
483
- console.warn('[updateUserPermissions] Token verification failed:', verification.error);
619
+ log.warn('updateUserPermissions', 'Token verification failed:', verification.error);
484
620
  }
485
621
  }
486
622
  catch (verifyError) {
487
- console.warn('[updateUserPermissions] Could not verify token:', verifyError);
623
+ log.warn('updateUserPermissions', 'Could not verify token:', verifyError);
488
624
  }
489
625
  }
490
626
  // Clean up any extra keys (there should only be one)
491
627
  if (userKeys.length > 1) {
628
+ log.trace('updateUserPermissions', 'Cleaning up', userKeys.length - 1, 'extra keys');
492
629
  await Promise.all(userKeys.slice(1).map((key) => makeAdminRequest(`/admin/keys/${key.id}`, {
493
630
  method: 'DELETE'
494
631
  }).catch((err) => {
495
- console.warn(`[updateUserPermissions] Failed to delete extra key ${key.id}:`, err);
632
+ log.warn('updateUserPermissions', `Failed to delete extra key ${key.id}:`, err);
496
633
  })));
497
634
  }
498
635
  }
499
636
  await refreshTokenIfSelfUpdate(userId);
637
+ log.trace('updateUserPermissions', 'Done');
500
638
  }
501
639
  catch (error) {
502
- console.error('[updateUserPermissions] Error:', error);
640
+ log.error('updateUserPermissions', 'Error:', error);
503
641
  throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
504
642
  }
505
643
  });
506
644
  export const generateApiKey = command('unchecked', async (options) => {
645
+ log.trace('generateApiKey', 'Called with userId:', options.userId, 'permissions:', options.permissions, 'revokeOld:', options.revokeOld);
507
646
  try {
508
647
  // No default permissions - require explicit permissions to be passed
509
648
  if (options.permissions.length === 0) {
@@ -515,9 +654,11 @@ export const generateApiKey = command('unchecked', async (options) => {
515
654
  if (!email) {
516
655
  throw new Error('User has no email address');
517
656
  }
657
+ log.trace('generateApiKey', 'Resolved email:', email);
518
658
  // Check if user has existing active key
519
659
  const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
520
660
  const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
661
+ log.trace('generateApiKey', 'Existing active keys:', userKeys.length);
521
662
  let newApiKey;
522
663
  let wasRotated = false;
523
664
  let oldApiKey;
@@ -526,8 +667,10 @@ export const generateApiKey = command('unchecked', async (options) => {
526
667
  if (userKeys.length > 0 && options.revokeOld) {
527
668
  // Use rotate endpoint (per Mako Auth API spec)
528
669
  const keyId = userKeys[0].id;
670
+ log.trace('generateApiKey', 'Rotating key:', keyId);
529
671
  // Get the old API key from Clerk's private_metadata
530
672
  oldApiKey = currentUser?.private_metadata?.mako_api_key;
673
+ log.trace('generateApiKey', 'Old key exists in metadata:', !!oldApiKey);
531
674
  const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
532
675
  method: 'POST',
533
676
  body: JSON.stringify({
@@ -540,43 +683,48 @@ export const generateApiKey = command('unchecked', async (options) => {
540
683
  if (!newApiKey) {
541
684
  throw new Error('Failed to rotate API key - no key in response');
542
685
  }
686
+ log.trace('generateApiKey', 'Rotation successful, new key obtained');
543
687
  // Verify old key is revoked
544
688
  if (oldApiKey) {
545
689
  try {
546
690
  const oldKeyVerification = await verifyApiKeyToken(oldApiKey);
547
- console.log('[generateApiKey] Old key verification:', oldKeyVerification.valid ? 'Still valid ⚠️' : 'Revoked ');
691
+ log.info('generateApiKey', 'Old key verification:', oldKeyVerification.valid ? 'Still valid (unexpected)' : 'Revoked (expected)');
548
692
  if (oldKeyVerification.valid) {
549
- console.warn('[generateApiKey] Old API key still valid after rotation');
693
+ log.warn('generateApiKey', 'Old API key still valid after rotation');
550
694
  }
551
695
  }
552
696
  catch (verifyError) {
553
- console.warn('[generateApiKey] Could not verify old key revocation:', verifyError);
697
+ log.warn('generateApiKey', 'Could not verify old key revocation:', verifyError);
554
698
  }
555
699
  }
556
700
  // Verify new key works with correct scopes
557
701
  try {
558
702
  const newKeyVerification = await verifyApiKeyToken(newApiKey);
559
- console.log('[generateApiKey] New key verification:', newKeyVerification);
703
+ log.info('generateApiKey', 'New key verification:', newKeyVerification);
560
704
  if (newKeyVerification.valid) {
561
705
  // Check if the scopes match what we expect
562
706
  const scopesMatch = options.permissions.every((perm) => newKeyVerification.scopes?.includes(perm));
563
707
  if (!scopesMatch) {
564
- console.warn('[generateApiKey] Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
708
+ log.warn('generateApiKey', 'Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
565
709
  verificationWarning = `New API key scopes do not match expected permissions. Expected: ${options.permissions.join(', ')}, Got: ${newKeyVerification.scopes?.join(', ') || 'none'}`;
566
710
  }
711
+ else {
712
+ log.trace('generateApiKey', 'New key scopes verified successfully');
713
+ }
567
714
  }
568
715
  else {
569
- console.warn('[generateApiKey] New key verification failed:', newKeyVerification.error);
716
+ log.warn('generateApiKey', 'New key verification failed:', newKeyVerification.error);
570
717
  verificationWarning = `New API key failed verification - ${newKeyVerification.error || 'Unknown error'}`;
571
718
  }
572
719
  }
573
720
  catch (verifyError) {
574
- console.warn('[generateApiKey] Could not verify new key:', verifyError);
721
+ log.warn('generateApiKey', 'Could not verify new key:', verifyError);
575
722
  verificationWarning = `Could not verify new API key - ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`;
576
723
  }
577
724
  }
578
725
  else {
579
726
  // Create new key if none exists or revokeOld is false
727
+ log.trace('generateApiKey', 'Creating new key (no rotation)');
580
728
  const createData = await createUserPermissions(email, options.permissions);
581
729
  if (!createData) {
582
730
  throw new Error('Failed to create admin key');
@@ -585,6 +733,7 @@ export const generateApiKey = command('unchecked', async (options) => {
585
733
  if (!newApiKey) {
586
734
  throw new Error('Failed to generate API key - no key in response');
587
735
  }
736
+ log.trace('generateApiKey', 'New key created');
588
737
  }
589
738
  // Update Clerk profile with new key
590
739
  try {
@@ -602,11 +751,12 @@ export const generateApiKey = command('unchecked', async (options) => {
602
751
  }
603
752
  })
604
753
  });
754
+ log.trace('generateApiKey', 'Clerk metadata updated with new key');
605
755
  }
606
756
  }
607
757
  catch (clerkError) {
608
- console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
609
- console.warn('[generateApiKey] Key generated but could not update Clerk profile');
758
+ log.error('generateApiKey', 'Failed to update Clerk profile:', clerkError);
759
+ log.warn('generateApiKey', 'Key generated but could not update Clerk profile');
610
760
  }
611
761
  const result = {
612
762
  success: true,
@@ -614,18 +764,20 @@ export const generateApiKey = command('unchecked', async (options) => {
614
764
  message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully',
615
765
  verificationWarning
616
766
  };
767
+ log.trace('generateApiKey', 'Done, wasRotated:', wasRotated);
617
768
  return JSON.parse(JSON.stringify(result));
618
769
  }
619
770
  catch (error) {
620
- console.error('[generateApiKey] Error:', error);
771
+ log.error('generateApiKey', 'Error:', error);
621
772
  throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
622
773
  }
623
774
  });
624
775
  export const verifyToken = command('unchecked', async (options) => {
776
+ log.trace('verifyToken', 'Called');
625
777
  try {
626
778
  const result = await verifyApiKeyToken(options.apiKey);
627
- console.log('[verifyToken] Current scopes:', result.scopes);
628
- console.log('[verifyToken] Sub:', result.sub);
779
+ log.info('verifyToken', 'Current scopes:', result.scopes);
780
+ log.info('verifyToken', 'Sub:', result.sub);
629
781
  // Also return the issued token for debugging
630
782
  if (result.valid) {
631
783
  try {
@@ -639,26 +791,30 @@ export const verifyToken = command('unchecked', async (options) => {
639
791
  valid: result.valid,
640
792
  scopes: result.scopes,
641
793
  sub: result.sub,
794
+ client_id: result.client_id,
642
795
  token: tokenResult.data?.data?.access_token
643
796
  };
797
+ log.trace('verifyToken', 'Returning valid result with token');
644
798
  // Ensure result is serializable
645
799
  return JSON.parse(JSON.stringify(finalResult));
646
800
  }
647
801
  catch (tokenError) {
648
- console.warn('[verifyToken] Could not fetch token:', tokenError);
802
+ log.warn('verifyToken', 'Could not fetch token:', tokenError);
649
803
  // Return result without token
650
804
  return JSON.parse(JSON.stringify({
651
805
  valid: result.valid,
652
806
  scopes: result.scopes,
653
- sub: result.sub
807
+ sub: result.sub,
808
+ client_id: result.client_id
654
809
  }));
655
810
  }
656
811
  }
812
+ log.trace('verifyToken', 'Returning invalid result');
657
813
  // Ensure result is serializable
658
814
  return JSON.parse(JSON.stringify(result));
659
815
  }
660
816
  catch (error) {
661
- console.error('[verifyToken] Error:', error);
817
+ log.error('verifyToken', 'Error:', error);
662
818
  const errorResult = {
663
819
  valid: false,
664
820
  error: error instanceof Error ? error.message : 'Unknown error during verification'
package/dist/index.d.ts CHANGED
@@ -989,6 +989,7 @@ export interface UserModalProps {
989
989
  user?: User | null;
990
990
  roles?: Role[];
991
991
  adapter?: UserManagementAdapter;
992
+ loadingPermissions?: boolean;
992
993
  onSave: (user: User, mode: 'create' | 'edit') => void | Promise<void>;
993
994
  onClose?: () => void;
994
995
  class?: ClassValue;
@@ -327,35 +327,41 @@
327
327
 
328
328
  <tbody class={tbodyClasses}>
329
329
  {#if loading}
330
- <tr>
331
- <td
332
- colspan={selectable ? columns.length + 1 : columns.length}
333
- class={cn(tdClasses, 'py-8 text-center')}
334
- >
335
- <div class="flex justify-center">
336
- <svg
337
- class="text-default-500 h-6 w-6 animate-spin"
338
- xmlns="http://www.w3.org/2000/svg"
339
- fill="none"
340
- viewBox="0 0 24 24"
341
- >
342
- <circle
343
- class="opacity-25"
344
- cx="12"
345
- cy="12"
346
- r="10"
347
- stroke="currentColor"
348
- stroke-width="4"
349
- ></circle>
350
- <path
351
- class="opacity-75"
352
- fill="currentColor"
353
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
354
- ></path>
355
- </svg>
356
- </div>
357
- </td>
358
- </tr>
330
+ {#each Array.from({ length: Math.min(internalPageSize, 5) }, (__, i) => i) as rowIdx (rowIdx)}
331
+ <tr class={cn(trClass, rowIdx % 2 === 1 && striped ? 'bg-default-50' : '')}>
332
+ {#if selectable}
333
+ <td class={cn(tdClasses, 'text-center')}>
334
+ <div class="bg-default-200 mx-auto h-4 w-4 animate-pulse rounded"></div>
335
+ </td>
336
+ {/if}
337
+ {#each columns as column, colIdx (column.key)}
338
+ <td class={cn(tdClasses, column.class)}>
339
+ {#if colIdx === 0}
340
+ <div class="flex items-center gap-3">
341
+ <div
342
+ class="bg-default-200 h-8 w-8 shrink-0 animate-pulse rounded-full"
343
+ ></div>
344
+ <div class="flex-1 space-y-2">
345
+ <div class="bg-default-200 h-3.5 w-28 animate-pulse rounded"></div>
346
+ <div class="bg-default-100 h-3 w-20 animate-pulse rounded"></div>
347
+ </div>
348
+ </div>
349
+ {:else if colIdx === columns.length - 1}
350
+ <div class="flex items-center justify-end gap-2">
351
+ <div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
352
+ <div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
353
+ <div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
354
+ </div>
355
+ {:else}
356
+ <div
357
+ class="bg-default-200 h-3.5 animate-pulse rounded"
358
+ style="width: {60 + ((rowIdx * 17 + colIdx * 31) % 40)}%"
359
+ ></div>
360
+ {/if}
361
+ </td>
362
+ {/each}
363
+ </tr>
364
+ {/each}
359
365
  {:else if getPaginatedData().length === 0}
360
366
  <tr>
361
367
  <td
@@ -555,35 +561,41 @@
555
561
 
556
562
  <tbody class={tbodyClasses}>
557
563
  {#if loading}
558
- <tr>
559
- <td
560
- colspan={selectable ? columns.length + 1 : columns.length}
561
- class={cn(tdClasses, 'py-8 text-center')}
562
- >
563
- <div class="flex justify-center">
564
- <svg
565
- class="text-default-500 h-6 w-6 animate-spin"
566
- xmlns="http://www.w3.org/2000/svg"
567
- fill="none"
568
- viewBox="0 0 24 24"
569
- >
570
- <circle
571
- class="opacity-25"
572
- cx="12"
573
- cy="12"
574
- r="10"
575
- stroke="currentColor"
576
- stroke-width="4"
577
- ></circle>
578
- <path
579
- class="opacity-75"
580
- fill="currentColor"
581
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
582
- ></path>
583
- </svg>
584
- </div>
585
- </td>
586
- </tr>
564
+ {#each Array.from({ length: Math.min(internalPageSize, 5) }, (__, i) => i) as rowIdx (rowIdx)}
565
+ <tr class={cn(trClass, rowIdx % 2 === 1 && striped ? 'bg-default-50' : '')}>
566
+ {#if selectable}
567
+ <td class={cn(tdClasses, 'text-center')}>
568
+ <div class="bg-default-200 mx-auto h-4 w-4 animate-pulse rounded"></div>
569
+ </td>
570
+ {/if}
571
+ {#each columns as column, colIdx (column.key)}
572
+ <td class={cn(tdClasses, column.class)}>
573
+ {#if colIdx === 0}
574
+ <div class="flex items-center gap-3">
575
+ <div
576
+ class="bg-default-200 h-8 w-8 shrink-0 animate-pulse rounded-full"
577
+ ></div>
578
+ <div class="flex-1 space-y-2">
579
+ <div class="bg-default-200 h-3.5 w-28 animate-pulse rounded"></div>
580
+ <div class="bg-default-100 h-3 w-20 animate-pulse rounded"></div>
581
+ </div>
582
+ </div>
583
+ {:else if colIdx === columns.length - 1}
584
+ <div class="flex items-center justify-end gap-2">
585
+ <div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
586
+ <div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
587
+ <div class="bg-default-200 h-5 w-5 animate-pulse rounded"></div>
588
+ </div>
589
+ {:else}
590
+ <div
591
+ class="bg-default-200 h-3.5 animate-pulse rounded"
592
+ style="width: {60 + ((rowIdx * 17 + colIdx * 31) % 40)}%"
593
+ ></div>
594
+ {/if}
595
+ </td>
596
+ {/each}
597
+ </tr>
598
+ {/each}
587
599
  {:else if getPaginatedData().length === 0}
588
600
  <tr>
589
601
  <td
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
+ import { toast } from 'svelte-sonner';
3
4
  import { PageHeader, Button, cn } from '../index.js';
4
5
  import UserTable from './UserTable.svelte';
5
6
  import UserModal from './UserModal.svelte';
@@ -17,7 +18,7 @@
17
18
  // Internal state
18
19
  let users = $state<User[]>([]);
19
20
  let totalUsers = $state(0);
20
- let loading = $state(false);
21
+ let loading = $state(true);
21
22
  let currentPage = $state(1);
22
23
  let pageSize = $state(10);
23
24
  let sortBy = $state<string | null>(null);
@@ -29,6 +30,7 @@
29
30
  let showEditCreateModal = $state(false);
30
31
  let showViewModal = $state(false);
31
32
  let selectedUser = $state<User | null>(null);
33
+ let loadingPermissions = $state(false);
32
34
 
33
35
  // Bulk action states
34
36
  let selectedUsers = new SvelteSet<string>();
@@ -52,6 +54,7 @@
52
54
  totalUsers = result.totalUsers;
53
55
  } catch (error) {
54
56
  console.error('Error loading users:', error);
57
+ toast.error('Failed to load users');
55
58
  } finally {
56
59
  loading = false;
57
60
  }
@@ -88,6 +91,7 @@
88
91
  showViewModal = true;
89
92
  // Fetch fresh permissions for the user
90
93
  if (user.id) {
94
+ loadingPermissions = true;
91
95
  try {
92
96
  const permissions = await adapter.getUserPermissions(user.id);
93
97
  selectedUser = {
@@ -96,6 +100,9 @@
96
100
  };
97
101
  } catch (error) {
98
102
  console.error('Error fetching user permissions:', error);
103
+ toast.error('Failed to load user permissions');
104
+ } finally {
105
+ loadingPermissions = false;
99
106
  }
100
107
  }
101
108
  }
@@ -105,6 +112,7 @@
105
112
  showEditCreateModal = true;
106
113
  // Fetch fresh permissions for the user
107
114
  if (user.id) {
115
+ loadingPermissions = true;
108
116
  try {
109
117
  const permissions = await adapter.getUserPermissions(user.id);
110
118
  selectedUser = {
@@ -113,6 +121,9 @@
113
121
  };
114
122
  } catch (error) {
115
123
  console.error('Error fetching user permissions:', error);
124
+ toast.error('Failed to load user permissions');
125
+ } finally {
126
+ loadingPermissions = false;
116
127
  }
117
128
  }
118
129
  }
@@ -129,6 +140,17 @@
129
140
  }
130
141
  }
131
142
 
143
+ // Sync any changes made to selectedUser (e.g. API key regeneration) back into the users array
144
+ function handleModalClose() {
145
+ if (selectedUser) {
146
+ const idx = users.findIndex((u) => u.id === selectedUser!.id);
147
+ if (idx !== -1) {
148
+ users[idx] = { ...selectedUser };
149
+ }
150
+ }
151
+ selectedUser = null;
152
+ }
153
+
132
154
  // Save handlers
133
155
  async function handleUserSave(user: User, mode: 'create' | 'edit') {
134
156
  try {
@@ -143,6 +165,7 @@
143
165
  await refreshUsersQuery();
144
166
  } catch (error) {
145
167
  console.error('Error saving user:', error);
168
+ toast.error(mode === 'create' ? 'Failed to create user' : 'Failed to update user');
146
169
  // Reload on error to restore correct state
147
170
  await loadUsers();
148
171
  throw error;
@@ -175,6 +198,7 @@
175
198
  await refreshUsersQuery();
176
199
  } catch (error) {
177
200
  console.error('Error deleting user:', error);
201
+ toast.error('Failed to delete user');
178
202
  // Reload on error to restore correct state
179
203
  await loadUsers();
180
204
  throw error;
@@ -217,6 +241,7 @@
217
241
  await refreshUsersQuery();
218
242
  } catch (error) {
219
243
  console.error('Error deleting users:', error);
244
+ toast.error('Failed to delete users');
220
245
  // Reload on error to restore correct state
221
246
  await loadUsers();
222
247
  throw error;
@@ -296,7 +321,7 @@
296
321
  bind:user={selectedUser}
297
322
  {permissions}
298
323
  onEdit={handleEditFromView}
299
- onClose={() => (selectedUser = null)}
324
+ onClose={handleModalClose}
300
325
  />
301
326
 
302
327
  <!-- User Edit/Create Modal -->
@@ -305,7 +330,8 @@
305
330
  bind:user={selectedUser}
306
331
  {roles}
307
332
  {adapter}
333
+ {loadingPermissions}
308
334
  onSave={handleUserSave}
309
- onClose={() => (selectedUser = null)}
335
+ onClose={handleModalClose}
310
336
  />
311
337
  </div>
@@ -17,6 +17,7 @@
17
17
  user = $bindable(),
18
18
  roles = [],
19
19
  adapter,
20
+ loadingPermissions = false,
20
21
  onSave,
21
22
  onClose,
22
23
  class: className
@@ -36,6 +37,7 @@
36
37
  valid?: boolean;
37
38
  scopes?: string[];
38
39
  sub?: string;
40
+ client_id?: string;
39
41
  error?: string;
40
42
  } | null>(null);
41
43
  let initialRole = $state<string>('');
@@ -490,6 +492,11 @@
490
492
  Sub: {tokenVerification.sub}
491
493
  </p>
492
494
  {/if}
495
+ {#if tokenVerification.client_id}
496
+ <p class="text-success-700 mt-1 text-xs">
497
+ Client ID: {tokenVerification.client_id}
498
+ </p>
499
+ {/if}
493
500
  {#if tokenVerification.scopes && tokenVerification.scopes.length > 0}
494
501
  <p class="text-success-700 mt-1 text-xs">
495
502
  Scopes: {tokenVerification.scopes.join(', ')}
@@ -583,7 +590,24 @@
583
590
  {/if}
584
591
 
585
592
  <!-- Permission Preview -->
586
- {#if formData.permissions && formData.permissions.length > 0}
593
+ {#if loadingPermissions && mode === 'edit'}
594
+ <div>
595
+ <div class="bg-default-200 mb-2 h-4 w-32 animate-pulse rounded"></div>
596
+ <div class="bg-default-50 rounded-lg p-3">
597
+ <div class="space-y-3">
598
+ {#each [0, 1, 2] as i (i)}
599
+ <div class="flex items-center gap-2">
600
+ <div class="bg-default-200 h-1.5 w-1.5 shrink-0 animate-pulse rounded-full"></div>
601
+ <div
602
+ class="bg-default-200 h-3.5 animate-pulse rounded"
603
+ style="width: {50 + i * 20}%"
604
+ ></div>
605
+ </div>
606
+ {/each}
607
+ </div>
608
+ </div>
609
+ </div>
610
+ {:else if formData.permissions && formData.permissions.length > 0}
587
611
  <div>
588
612
  <h4 class="text-default-700 mb-2 text-sm font-medium">
589
613
  Permissions ({formData.permissions.length})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "1.7.5",
3
+ "version": "1.7.10",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -59,6 +59,7 @@
59
59
  "svelte": "^5.0.0 || ^6.0.0"
60
60
  },
61
61
  "devDependencies": {
62
+ "@changesets/cli": "^2.29.8",
62
63
  "@eslint/compat": "^1.4.1",
63
64
  "@eslint/js": "^9.39.1",
64
65
  "@playwright/test": "^1.56.1",