@makolabs/ripple 1.7.6 → 1.7.11

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');
@@ -20,9 +44,17 @@ async function makeClerkRequest(endpoint, options = {}) {
20
44
  }
21
45
  const method = options.method || 'GET';
22
46
  const url = `https://api.clerk.com/v1${endpoint}`;
23
- console.log(`[Clerk API] ${method} ${url}`);
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
+ });
24
55
  if (options.body)
25
- console.log(`[Clerk API] Payload: ${options.body}`);
56
+ log.trace('Clerk API', 'Parsed payload:', JSON.parse(options.body));
57
+ const start = performance.now();
26
58
  const response = await fetch(url, {
27
59
  ...options,
28
60
  headers: {
@@ -31,10 +63,12 @@ async function makeClerkRequest(endpoint, options = {}) {
31
63
  ...options.headers
32
64
  }
33
65
  });
66
+ const elapsed = (performance.now() - start).toFixed(2);
34
67
  if (!response.ok) {
35
68
  const errorText = await response.text();
36
- console.error(`[Clerk API] Response: ${response.status} ${response.statusText}`);
37
- console.error(`[Clerk API] Body: ${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()));
38
72
  let errorDetails;
39
73
  try {
40
74
  errorDetails = JSON.parse(errorText);
@@ -48,8 +82,10 @@ async function makeClerkRequest(endpoint, options = {}) {
48
82
  throw error;
49
83
  }
50
84
  const data = await response.json();
51
- console.log(`[Clerk API] Response: ${response.status} ${response.statusText}`);
52
- console.log(`[Clerk API] Body: ${JSON.stringify(data).slice(0, 500)}`);
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()));
53
89
  // Ensure all data is serializable by converting to plain objects
54
90
  return JSON.parse(JSON.stringify(data));
55
91
  }
@@ -66,9 +102,17 @@ async function makeAdminRequest(endpoint, options = {}) {
66
102
  }
67
103
  const method = options.method || 'GET';
68
104
  const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
69
- console.log(`[Admin API] ${method} ${url}`);
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
+ });
70
113
  if (options.body)
71
- console.log(`[Admin API] Payload: ${options.body}`);
114
+ log.trace('Admin API', 'Parsed payload:', JSON.parse(options.body));
115
+ const start = performance.now();
72
116
  const response = await fetch(url, {
73
117
  ...options,
74
118
  headers: {
@@ -77,15 +121,19 @@ async function makeAdminRequest(endpoint, options = {}) {
77
121
  ...options.headers
78
122
  }
79
123
  });
124
+ const elapsed = (performance.now() - start).toFixed(2);
80
125
  if (!response.ok) {
81
126
  const errorText = await response.text();
82
- console.error(`[Admin API] Response: ${response.status} ${response.statusText}`);
83
- console.error(`[Admin API] Body: ${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()));
84
130
  throw new Error(`Admin API request failed: ${response.status} ${response.statusText} - ${errorText}`);
85
131
  }
86
132
  const data = await response.json();
87
- console.log(`[Admin API] Response: ${response.status} ${response.statusText}`);
88
- console.log(`[Admin API] Body: ${JSON.stringify(data).slice(0, 500)}`);
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()));
89
137
  // Ensure all data is serializable by converting to plain objects
90
138
  return JSON.parse(JSON.stringify(data));
91
139
  }
@@ -96,9 +144,14 @@ async function makeAuthRequest(endpoint, options = {}) {
96
144
  }
97
145
  const method = options.method || 'GET';
98
146
  const url = `${PRIVATE_BASE_AUTH_URL}${endpoint}`;
99
- console.log(`[Auth API] ${method} ${url}`);
147
+ log.info('Auth API', `${method} ${url}`);
100
148
  if (options.body)
101
- console.log(`[Auth API] Payload: ${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();
102
155
  const response = await fetch(url, {
103
156
  ...options,
104
157
  headers: {
@@ -106,6 +159,7 @@ async function makeAuthRequest(endpoint, options = {}) {
106
159
  ...options.headers
107
160
  }
108
161
  });
162
+ const elapsed = (performance.now() - start).toFixed(2);
109
163
  const text = await response.text();
110
164
  let data;
111
165
  try {
@@ -115,8 +169,10 @@ async function makeAuthRequest(endpoint, options = {}) {
115
169
  // Not JSON, treat as plain text error (e.g., "404 page not found")
116
170
  data = { error: text, message: text };
117
171
  }
118
- console.log(`[Auth API] Response: ${response.status} ${response.statusText}`);
119
- console.log(`[Auth API] Body: ${JSON.stringify(data).slice(0, 500)}`);
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()));
120
176
  return {
121
177
  ok: response.ok,
122
178
  status: response.status,
@@ -124,6 +180,7 @@ async function makeAuthRequest(endpoint, options = {}) {
124
180
  };
125
181
  }
126
182
  async function verifyApiKeyToken(apiKey) {
183
+ log.trace('verifyApiKeyToken', 'Called with apiKey:', apiKey.slice(0, 8) + '...[REDACTED]');
127
184
  try {
128
185
  const result = await makeAuthRequest('/auth/token', {
129
186
  method: 'POST',
@@ -131,37 +188,43 @@ async function verifyApiKeyToken(apiKey) {
131
188
  'X-API-Key': apiKey
132
189
  }
133
190
  });
191
+ log.trace('verifyApiKeyToken', 'Token exchange result ok:', result.ok);
134
192
  if (result.ok && result.data?.data?.access_token) {
135
193
  const token = result.data.data.access_token;
194
+ log.trace('verifyApiKeyToken', 'Got access_token, verifying...');
136
195
  const verifyResult = await makeAuthRequest('/auth/verify', {
137
196
  method: 'GET',
138
197
  headers: {
139
198
  Authorization: `Bearer ${token}`
140
199
  }
141
200
  });
201
+ log.trace('verifyApiKeyToken', 'Verify result ok:', verifyResult.ok, 'data:', verifyResult.data);
142
202
  if (verifyResult.ok && verifyResult.data?.data) {
143
203
  // The API returns "scope" (singular) as a space-separated string, not "scopes" array
144
204
  const scopeString = verifyResult.data.data.scope;
145
205
  const scopes = scopeString ? scopeString.split(' ').filter(Boolean) : [];
146
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);
147
209
  return {
148
210
  valid: true,
149
211
  scopes: scopes,
150
- sub: sub
212
+ sub: sub,
213
+ client_id: client_id
151
214
  };
152
215
  }
153
216
  }
154
217
  const errorMsg = result.data?.message ||
155
218
  result.data?.error ||
156
219
  `API key verification failed with status ${result.status}`;
157
- console.warn('[verifyApiKeyToken] Verification failed:', errorMsg);
220
+ log.warn('verifyApiKeyToken', 'Verification failed:', errorMsg);
158
221
  return {
159
222
  valid: false,
160
223
  error: errorMsg
161
224
  };
162
225
  }
163
226
  catch (error) {
164
- console.error('[verifyApiKeyToken] Exception during verification:', error);
227
+ log.error('verifyApiKeyToken', 'Exception during verification:', error);
165
228
  return {
166
229
  valid: false,
167
230
  error: error instanceof Error ? error.message : 'Unknown error during verification'
@@ -169,10 +232,12 @@ async function verifyApiKeyToken(apiKey) {
169
232
  }
170
233
  }
171
234
  async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
235
+ log.trace('createUserPermissions', 'email:', email, 'permissions:', permissions, 'clientId:', clientId);
172
236
  if (permissions.length === 0) {
237
+ log.trace('createUserPermissions', 'No permissions to create, returning null');
173
238
  return null;
174
239
  }
175
- return await makeAdminRequest('/admin/keys', {
240
+ const result = await makeAdminRequest('/admin/keys', {
176
241
  method: 'POST',
177
242
  body: JSON.stringify({
178
243
  client_id: clientId,
@@ -180,8 +245,11 @@ async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
180
245
  scopes: permissions
181
246
  })
182
247
  });
248
+ log.trace('createUserPermissions', 'Result:', result);
249
+ return result;
183
250
  }
184
251
  export const getUsers = query('unchecked', async (options) => {
252
+ log.trace('getUsers', 'Called with options:', options);
185
253
  try {
186
254
  const limit = options.pageSize;
187
255
  const offset = (options.page - 1) * options.pageSize;
@@ -200,12 +268,14 @@ export const getUsers = query('unchecked', async (options) => {
200
268
  });
201
269
  if (options.query)
202
270
  params.append('query', options.query);
271
+ log.trace('getUsers', 'Query params:', params.toString());
203
272
  const [usersData, countData] = await Promise.all([
204
273
  makeClerkRequest(`/users?${params}`),
205
274
  makeClerkRequest(`/users/count?${params}`)
206
275
  ]);
207
276
  // Extract users array - Clerk API returns { data: [...] } or array directly
208
277
  const users = Array.isArray(usersData) ? usersData : usersData?.data || [];
278
+ log.trace('getUsers', 'Fetched', users.length, 'users, total count:', countData);
209
279
  // Ensure all data is serializable by converting to plain objects
210
280
  const serializedUsers = JSON.parse(JSON.stringify(users));
211
281
  const serializedCountData = JSON.parse(JSON.stringify(countData));
@@ -216,11 +286,12 @@ export const getUsers = query('unchecked', async (options) => {
216
286
  }));
217
287
  }
218
288
  catch (error) {
219
- console.error('[getUsers] Error:', error);
289
+ log.error('getUsers', 'Error:', error);
220
290
  throw new Error(`Failed to fetch users: ${error instanceof Error ? error.message : 'Unknown error'}`);
221
291
  }
222
292
  });
223
293
  export const createUser = command('unchecked', async (userData) => {
294
+ log.trace('createUser', 'Called with userData:', { ...userData, private_metadata: '[REDACTED]' });
224
295
  const emailAddress = userData.email_addresses?.[0]?.email_address || '';
225
296
  if (!emailAddress) {
226
297
  throw new Error('Email address is required');
@@ -234,13 +305,16 @@ export const createUser = command('unchecked', async (userData) => {
234
305
  ...(userDataOnly.username && { username: userDataOnly.username }),
235
306
  ...(userDataOnly.private_metadata && { private_metadata: userDataOnly.private_metadata })
236
307
  };
308
+ log.trace('createUser', 'Creating Clerk user with data:', clerkUserData);
237
309
  let result = await makeClerkRequest('/users', {
238
310
  method: 'POST',
239
311
  body: JSON.stringify(clerkUserData)
240
312
  });
241
313
  // Ensure result is serializable
242
314
  result = JSON.parse(JSON.stringify(result));
315
+ log.trace('createUser', 'Clerk user created, id:', result.id);
243
316
  if (ORGANIZATION_ID) {
317
+ log.trace('createUser', 'Adding user to org:', ORGANIZATION_ID);
244
318
  try {
245
319
  await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
246
320
  method: 'POST',
@@ -249,24 +323,29 @@ export const createUser = command('unchecked', async (userData) => {
249
323
  role: 'org:member'
250
324
  })
251
325
  });
326
+ log.trace('createUser', 'User added to org successfully');
252
327
  }
253
328
  catch (orgError) {
254
- 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);
255
331
  try {
256
332
  await makeClerkRequest(`/users/${result.id}`, {
257
333
  method: 'DELETE'
258
334
  });
335
+ log.trace('createUser', 'Rollback delete successful');
259
336
  }
260
337
  catch (deleteError) {
261
- 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);
262
339
  }
263
340
  throw new Error(`Failed to add user to organization. User creation rolled back. Error: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
264
341
  }
265
342
  }
266
343
  if (permissions && permissions.length > 0) {
344
+ log.trace('createUser', 'Assigning permissions:', permissions);
267
345
  try {
268
346
  const adminKeyResult = await createUserPermissions(emailAddress, permissions);
269
347
  const apiKey = adminKeyResult?.data?.key;
348
+ log.trace('createUser', 'Admin key created, has key:', !!apiKey);
270
349
  if (adminKeyResult && apiKey) {
271
350
  const updatedUser = await makeClerkRequest(`/users/${result.id}`, {
272
351
  method: 'PATCH',
@@ -284,22 +363,27 @@ export const createUser = command('unchecked', async (userData) => {
284
363
  result.permissionsAssigned = permissions;
285
364
  }
286
365
  catch (permissionError) {
287
- console.error('[createUser] Failed to assign permissions:', permissionError);
366
+ log.error('createUser', 'Failed to assign permissions:', permissionError);
288
367
  result.warning = 'User created but permissions assignment failed';
289
368
  result.permissionError =
290
369
  permissionError instanceof Error ? permissionError.message : String(permissionError);
291
370
  }
292
371
  }
372
+ log.trace('createUser', 'Final result:', {
373
+ id: result.id,
374
+ permissionsAssigned: result.permissionsAssigned
375
+ });
293
376
  // Final serialization check before returning
294
377
  return JSON.parse(JSON.stringify(result));
295
378
  }
296
379
  catch (error) {
297
- console.error('[createUser] Error:', error);
380
+ log.error('createUser', 'Error:', error);
298
381
  handleClerkError(error, 'Failed to create user');
299
382
  }
300
383
  });
301
384
  export const updateUser = command('unchecked', async (options) => {
302
385
  const { userId, userData } = options;
386
+ log.trace('updateUser', 'Called for userId:', userId, 'fields:', Object.keys(userData));
303
387
  try {
304
388
  const updateData = {};
305
389
  if (userData.first_name !== undefined)
@@ -312,6 +396,7 @@ export const updateUser = command('unchecked', async (options) => {
312
396
  if (userData.private_metadata !== undefined) {
313
397
  updateData.private_metadata = userData.private_metadata;
314
398
  }
399
+ log.trace('updateUser', 'Update payload:', updateData);
315
400
  let result = await makeClerkRequest(`/users/${userId}`, {
316
401
  method: 'PATCH',
317
402
  body: JSON.stringify(updateData)
@@ -319,79 +404,95 @@ export const updateUser = command('unchecked', async (options) => {
319
404
  // Ensure result is serializable
320
405
  result = JSON.parse(JSON.stringify(result));
321
406
  if (userData.permissions !== undefined) {
407
+ log.trace('updateUser', 'Updating permissions:', userData.permissions);
322
408
  try {
323
409
  await updateUserPermissions({ userId, permissions: userData.permissions });
324
410
  }
325
411
  catch (permError) {
326
- console.error('[updateUser] Failed to update permissions:', permError);
412
+ log.error('updateUser', 'Failed to update permissions:', permError);
327
413
  }
328
414
  }
415
+ log.trace('updateUser', 'Done, result id:', result.id);
329
416
  // Final serialization check before returning
330
417
  return JSON.parse(JSON.stringify(result));
331
418
  }
332
419
  catch (error) {
333
- console.error('[updateUser] Error:', error);
420
+ log.error('updateUser', 'Error:', error);
334
421
  handleClerkError(error, 'Failed to update user');
335
422
  }
336
423
  });
337
424
  export const deleteUser = command('unchecked', async (userId) => {
425
+ log.trace('deleteUser', 'Called for userId:', userId);
338
426
  try {
339
427
  await makeClerkRequest(`/users/${userId}`, {
340
428
  method: 'DELETE'
341
429
  });
430
+ log.trace('deleteUser', 'Deleted successfully');
342
431
  }
343
432
  catch (error) {
344
- console.error('[deleteUser] Error:', error);
433
+ log.error('deleteUser', 'Error:', error);
345
434
  throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`);
346
435
  }
347
436
  });
348
437
  export const deleteUsers = command('unchecked', async (userIds) => {
438
+ log.trace('deleteUsers', 'Called for userIds:', userIds);
349
439
  try {
350
440
  await Promise.all(userIds.map((userId) => makeClerkRequest(`/users/${userId}`, {
351
441
  method: 'DELETE'
352
442
  })));
443
+ log.trace('deleteUsers', 'All', userIds.length, 'users deleted successfully');
353
444
  }
354
445
  catch (error) {
355
- console.error('[deleteUsers] Error:', error);
446
+ log.error('deleteUsers', 'Error:', error);
356
447
  throw new Error(`Failed to delete users: ${error instanceof Error ? error.message : 'Unknown error'}`);
357
448
  }
358
449
  });
359
450
  async function fetchUserPermissions(email) {
451
+ log.trace('fetchUserPermissions', 'Called for email:', email);
360
452
  try {
361
453
  const userData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
454
+ log.trace('fetchUserPermissions', 'Raw keys data:', userData);
362
455
  if (userData?.data?.data && Array.isArray(userData.data.data)) {
363
456
  userData.data.data = userData.data.data.filter((key) => key.status === 'active');
457
+ log.trace('fetchUserPermissions', 'Active keys count:', userData.data.data.length);
364
458
  }
365
459
  if (userData?.data?.data && Array.isArray(userData.data.data)) {
366
460
  // Deduplicate permissions by using a Set
367
461
  const allPermissions = userData.data.data.flatMap((key) => key.scopes || []);
368
462
  const dedupedPermissions = Array.from(new Set(allPermissions));
463
+ log.trace('fetchUserPermissions', 'Deduped permissions:', dedupedPermissions);
369
464
  return dedupedPermissions;
370
465
  }
371
466
  else if (userData?.scopes) {
372
467
  const permissions = Array.isArray(userData.scopes) ? userData.scopes : [userData.scopes];
468
+ log.trace('fetchUserPermissions', 'Permissions from scopes field:', permissions);
373
469
  return permissions;
374
470
  }
471
+ log.trace('fetchUserPermissions', 'No permissions found, returning []');
375
472
  return [];
376
473
  }
377
474
  catch (error) {
378
- 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');
379
477
  try {
380
478
  const allKeysData = await makeAdminRequest('/admin/keys');
381
479
  const userKey = allKeysData.data.data.find((key) => key.sub === email && key.client_id === CLIENT_ID && key.status === 'active');
382
480
  if (userKey) {
383
481
  const permissions = Array.isArray(userKey.scopes) ? userKey.scopes : [userKey.scopes];
482
+ log.trace('fetchUserPermissions', 'Found via fallback:', permissions);
384
483
  return permissions;
385
484
  }
485
+ log.trace('fetchUserPermissions', 'No key found in fallback search');
386
486
  return [];
387
487
  }
388
488
  catch (searchError) {
389
- console.error('[fetchUserPermissions] Error searching for user by sub:', searchError);
489
+ log.error('fetchUserPermissions', 'Error searching for user by sub:', searchError);
390
490
  throw new Error('Failed to fetch user permissions');
391
491
  }
392
492
  }
393
493
  }
394
494
  export const getUserPermissions = query('unchecked', async (userId) => {
495
+ log.trace('getUserPermissions', 'Called for userId:', userId);
395
496
  try {
396
497
  // Fetch user from Clerk to get email
397
498
  const user = await makeClerkRequest(`/users/${userId}`);
@@ -399,12 +500,14 @@ export const getUserPermissions = query('unchecked', async (userId) => {
399
500
  if (!email) {
400
501
  throw new Error('User has no email address');
401
502
  }
503
+ log.trace('getUserPermissions', 'Resolved email:', email);
402
504
  const permissions = await fetchUserPermissions(email);
505
+ log.trace('getUserPermissions', 'Returning permissions:', permissions);
403
506
  // Ensure permissions array is serializable
404
507
  return JSON.parse(JSON.stringify(permissions));
405
508
  }
406
509
  catch (error) {
407
- console.error('[getUserPermissions] Error:', error);
510
+ log.error('getUserPermissions', 'Error:', error);
408
511
  throw new Error(`Failed to fetch user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
409
512
  }
410
513
  });
@@ -419,6 +522,7 @@ async function refreshTokenIfSelfUpdate(userId) {
419
522
  return;
420
523
  const currentUser = getAuth();
421
524
  const isSelfUpdate = currentUser?.userId === userId;
525
+ log.trace('refreshTokenIfSelfUpdate', 'isSelfUpdate:', isSelfUpdate);
422
526
  if (isSelfUpdate) {
423
527
  try {
424
528
  // Try to dynamically import token manager - this is app-specific
@@ -431,6 +535,7 @@ async function refreshTokenIfSelfUpdate(userId) {
431
535
  tokenManager.clearTokenCookies(event.cookies);
432
536
  const newToken = await tokenManager.getAccessToken(event.cookies, event, true);
433
537
  tokenManager.setClientAccessibleToken(event.cookies, newToken);
538
+ log.trace('refreshTokenIfSelfUpdate', 'Token refreshed successfully');
434
539
  }
435
540
  }
436
541
  catch {
@@ -444,6 +549,7 @@ async function refreshTokenIfSelfUpdate(userId) {
444
549
  }
445
550
  export const updateUserPermissions = command('unchecked', async (options) => {
446
551
  const { userId, permissions } = options;
552
+ log.trace('updateUserPermissions', 'Called for userId:', userId, 'permissions:', permissions);
447
553
  try {
448
554
  // Validate permissions - require at least one permission scope
449
555
  if (permissions.length === 0) {
@@ -455,11 +561,14 @@ export const updateUserPermissions = command('unchecked', async (options) => {
455
561
  if (!email) {
456
562
  throw new Error('User has no email address');
457
563
  }
564
+ log.trace('updateUserPermissions', 'Resolved email:', email);
458
565
  // Fetch user's active keys
459
566
  const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
460
567
  const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
568
+ log.trace('updateUserPermissions', 'Active keys count:', userKeys.length);
461
569
  if (userKeys.length === 0) {
462
570
  // No active key exists, create new one
571
+ log.trace('updateUserPermissions', 'No active key, creating new one');
463
572
  const newKeyResult = await createUserPermissions(email, permissions);
464
573
  const newApiKey = newKeyResult?.data?.key;
465
574
  if (newApiKey) {
@@ -472,11 +581,13 @@ export const updateUserPermissions = command('unchecked', async (options) => {
472
581
  }
473
582
  })
474
583
  });
584
+ log.trace('updateUserPermissions', 'New key stored in Clerk metadata');
475
585
  }
476
586
  }
477
587
  else {
478
588
  // Use PUT to update existing key (per Mako Auth API spec)
479
589
  const keyId = userKeys[0].id;
590
+ log.trace('updateUserPermissions', 'Updating existing key:', keyId);
480
591
  // Get the API key string before updating
481
592
  const keyData = await makeAdminRequest(`/admin/keys/${keyId}`);
482
593
  const apiKeyString = keyData?.data?.key;
@@ -486,45 +597,52 @@ export const updateUserPermissions = command('unchecked', async (options) => {
486
597
  scopes: permissions
487
598
  })
488
599
  });
600
+ log.trace('updateUserPermissions', 'Key updated, verifying token...');
489
601
  // Verify the token has updated scopes
490
602
  if (apiKeyString) {
491
603
  try {
492
604
  const verification = await verifyApiKeyToken(apiKeyString);
493
- console.log('[updateUserPermissions] Key verification:', verification);
605
+ log.info('updateUserPermissions', 'Key verification:', verification);
494
606
  if (verification.valid) {
495
607
  // Check if the scopes match what we expect
496
608
  // Note: permissions.length > 0 is guaranteed by validation above
497
609
  const scopesMatch = permissions.length > 0 &&
498
610
  permissions.every((perm) => verification.scopes?.includes(perm));
499
611
  if (!scopesMatch) {
500
- 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');
501
616
  }
502
617
  }
503
618
  else {
504
- console.warn('[updateUserPermissions] Token verification failed:', verification.error);
619
+ log.warn('updateUserPermissions', 'Token verification failed:', verification.error);
505
620
  }
506
621
  }
507
622
  catch (verifyError) {
508
- console.warn('[updateUserPermissions] Could not verify token:', verifyError);
623
+ log.warn('updateUserPermissions', 'Could not verify token:', verifyError);
509
624
  }
510
625
  }
511
626
  // Clean up any extra keys (there should only be one)
512
627
  if (userKeys.length > 1) {
628
+ log.trace('updateUserPermissions', 'Cleaning up', userKeys.length - 1, 'extra keys');
513
629
  await Promise.all(userKeys.slice(1).map((key) => makeAdminRequest(`/admin/keys/${key.id}`, {
514
630
  method: 'DELETE'
515
631
  }).catch((err) => {
516
- console.warn(`[updateUserPermissions] Failed to delete extra key ${key.id}:`, err);
632
+ log.warn('updateUserPermissions', `Failed to delete extra key ${key.id}:`, err);
517
633
  })));
518
634
  }
519
635
  }
520
636
  await refreshTokenIfSelfUpdate(userId);
637
+ log.trace('updateUserPermissions', 'Done');
521
638
  }
522
639
  catch (error) {
523
- console.error('[updateUserPermissions] Error:', error);
640
+ log.error('updateUserPermissions', 'Error:', error);
524
641
  throw new Error(`Failed to update user permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
525
642
  }
526
643
  });
527
644
  export const generateApiKey = command('unchecked', async (options) => {
645
+ log.trace('generateApiKey', 'Called with userId:', options.userId, 'permissions:', options.permissions, 'revokeOld:', options.revokeOld);
528
646
  try {
529
647
  // No default permissions - require explicit permissions to be passed
530
648
  if (options.permissions.length === 0) {
@@ -536,9 +654,11 @@ export const generateApiKey = command('unchecked', async (options) => {
536
654
  if (!email) {
537
655
  throw new Error('User has no email address');
538
656
  }
657
+ log.trace('generateApiKey', 'Resolved email:', email);
539
658
  // Check if user has existing active key
540
659
  const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${encodeURIComponent(email)}`);
541
660
  const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
661
+ log.trace('generateApiKey', 'Existing active keys:', userKeys.length);
542
662
  let newApiKey;
543
663
  let wasRotated = false;
544
664
  let oldApiKey;
@@ -547,8 +667,10 @@ export const generateApiKey = command('unchecked', async (options) => {
547
667
  if (userKeys.length > 0 && options.revokeOld) {
548
668
  // Use rotate endpoint (per Mako Auth API spec)
549
669
  const keyId = userKeys[0].id;
670
+ log.trace('generateApiKey', 'Rotating key:', keyId);
550
671
  // Get the old API key from Clerk's private_metadata
551
672
  oldApiKey = currentUser?.private_metadata?.mako_api_key;
673
+ log.trace('generateApiKey', 'Old key exists in metadata:', !!oldApiKey);
552
674
  const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
553
675
  method: 'POST',
554
676
  body: JSON.stringify({
@@ -561,43 +683,48 @@ export const generateApiKey = command('unchecked', async (options) => {
561
683
  if (!newApiKey) {
562
684
  throw new Error('Failed to rotate API key - no key in response');
563
685
  }
686
+ log.trace('generateApiKey', 'Rotation successful, new key obtained');
564
687
  // Verify old key is revoked
565
688
  if (oldApiKey) {
566
689
  try {
567
690
  const oldKeyVerification = await verifyApiKeyToken(oldApiKey);
568
- 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)');
569
692
  if (oldKeyVerification.valid) {
570
- console.warn('[generateApiKey] Old API key still valid after rotation');
693
+ log.warn('generateApiKey', 'Old API key still valid after rotation');
571
694
  }
572
695
  }
573
696
  catch (verifyError) {
574
- console.warn('[generateApiKey] Could not verify old key revocation:', verifyError);
697
+ log.warn('generateApiKey', 'Could not verify old key revocation:', verifyError);
575
698
  }
576
699
  }
577
700
  // Verify new key works with correct scopes
578
701
  try {
579
702
  const newKeyVerification = await verifyApiKeyToken(newApiKey);
580
- console.log('[generateApiKey] New key verification:', newKeyVerification);
703
+ log.info('generateApiKey', 'New key verification:', newKeyVerification);
581
704
  if (newKeyVerification.valid) {
582
705
  // Check if the scopes match what we expect
583
706
  const scopesMatch = options.permissions.every((perm) => newKeyVerification.scopes?.includes(perm));
584
707
  if (!scopesMatch) {
585
- console.warn('[generateApiKey] Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
708
+ log.warn('generateApiKey', 'Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
586
709
  verificationWarning = `New API key scopes do not match expected permissions. Expected: ${options.permissions.join(', ')}, Got: ${newKeyVerification.scopes?.join(', ') || 'none'}`;
587
710
  }
711
+ else {
712
+ log.trace('generateApiKey', 'New key scopes verified successfully');
713
+ }
588
714
  }
589
715
  else {
590
- console.warn('[generateApiKey] New key verification failed:', newKeyVerification.error);
716
+ log.warn('generateApiKey', 'New key verification failed:', newKeyVerification.error);
591
717
  verificationWarning = `New API key failed verification - ${newKeyVerification.error || 'Unknown error'}`;
592
718
  }
593
719
  }
594
720
  catch (verifyError) {
595
- console.warn('[generateApiKey] Could not verify new key:', verifyError);
721
+ log.warn('generateApiKey', 'Could not verify new key:', verifyError);
596
722
  verificationWarning = `Could not verify new API key - ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`;
597
723
  }
598
724
  }
599
725
  else {
600
726
  // Create new key if none exists or revokeOld is false
727
+ log.trace('generateApiKey', 'Creating new key (no rotation)');
601
728
  const createData = await createUserPermissions(email, options.permissions);
602
729
  if (!createData) {
603
730
  throw new Error('Failed to create admin key');
@@ -606,6 +733,7 @@ export const generateApiKey = command('unchecked', async (options) => {
606
733
  if (!newApiKey) {
607
734
  throw new Error('Failed to generate API key - no key in response');
608
735
  }
736
+ log.trace('generateApiKey', 'New key created');
609
737
  }
610
738
  // Update Clerk profile with new key
611
739
  try {
@@ -623,11 +751,12 @@ export const generateApiKey = command('unchecked', async (options) => {
623
751
  }
624
752
  })
625
753
  });
754
+ log.trace('generateApiKey', 'Clerk metadata updated with new key');
626
755
  }
627
756
  }
628
757
  catch (clerkError) {
629
- console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
630
- 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');
631
760
  }
632
761
  const result = {
633
762
  success: true,
@@ -635,18 +764,20 @@ export const generateApiKey = command('unchecked', async (options) => {
635
764
  message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully',
636
765
  verificationWarning
637
766
  };
767
+ log.trace('generateApiKey', 'Done, wasRotated:', wasRotated);
638
768
  return JSON.parse(JSON.stringify(result));
639
769
  }
640
770
  catch (error) {
641
- console.error('[generateApiKey] Error:', error);
771
+ log.error('generateApiKey', 'Error:', error);
642
772
  throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
643
773
  }
644
774
  });
645
775
  export const verifyToken = command('unchecked', async (options) => {
776
+ log.trace('verifyToken', 'Called');
646
777
  try {
647
778
  const result = await verifyApiKeyToken(options.apiKey);
648
- console.log('[verifyToken] Current scopes:', result.scopes);
649
- console.log('[verifyToken] Sub:', result.sub);
779
+ log.info('verifyToken', 'Current scopes:', result.scopes);
780
+ log.info('verifyToken', 'Sub:', result.sub);
650
781
  // Also return the issued token for debugging
651
782
  if (result.valid) {
652
783
  try {
@@ -660,26 +791,30 @@ export const verifyToken = command('unchecked', async (options) => {
660
791
  valid: result.valid,
661
792
  scopes: result.scopes,
662
793
  sub: result.sub,
794
+ client_id: result.client_id,
663
795
  token: tokenResult.data?.data?.access_token
664
796
  };
797
+ log.trace('verifyToken', 'Returning valid result with token');
665
798
  // Ensure result is serializable
666
799
  return JSON.parse(JSON.stringify(finalResult));
667
800
  }
668
801
  catch (tokenError) {
669
- console.warn('[verifyToken] Could not fetch token:', tokenError);
802
+ log.warn('verifyToken', 'Could not fetch token:', tokenError);
670
803
  // Return result without token
671
804
  return JSON.parse(JSON.stringify({
672
805
  valid: result.valid,
673
806
  scopes: result.scopes,
674
- sub: result.sub
807
+ sub: result.sub,
808
+ client_id: result.client_id
675
809
  }));
676
810
  }
677
811
  }
812
+ log.trace('verifyToken', 'Returning invalid result');
678
813
  // Ensure result is serializable
679
814
  return JSON.parse(JSON.stringify(result));
680
815
  }
681
816
  catch (error) {
682
- console.error('[verifyToken] Error:', error);
817
+ log.error('verifyToken', 'Error:', error);
683
818
  const errorResult = {
684
819
  valid: false,
685
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.6",
3
+ "version": "1.7.11",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -23,14 +23,6 @@
23
23
  "test:e2e:debug": "playwright test --debug",
24
24
  "test:e2e:report": "playwright show-report",
25
25
  "test": "npm run test:unit -- --run",
26
- "pub:minor": "git add . && git commit -m \"chore: prepare for publish minor\" && npm version minor && git push --follow-tags && npm publish",
27
- "pub:patch": "git add . && git commit -m \"chore: prepare for publish patch\" && npm version patch && git push --follow-tags && npm publish",
28
- "pub:prelish": "git add . && git commit -m \"chore: prepare for publish premajor\" && npm version premajor && git push --follow-tags && npm publish",
29
- "pub:major": "git add . && git commit -m \"chore: prepare for publish major\" && npm version major && git push --follow-tags && npm publish",
30
- "pub:preminor": "git add . && git commit -m \"chore: prepare for publish preminor\" && npm version preminor && git push --follow-tags && npm publish",
31
- "pub:prepatch": "git add . && git commit -m \"chore: prepare for publish prepatch\" && npm version prepatch && git push --follow-tags && npm publish",
32
- "pub:prerelease": "git add . && git commit -m \"chore: prepare for publish prerelease\" && npm version prerelease && git push --follow-tags && npm publish",
33
- "pub:from-git": "git add . && git commit -m \"chore: prepare for publish from-git\" && npm version from-git && git push --follow-tags && npm publish",
34
26
  "storybook": "storybook dev -p 6006",
35
27
  "build-storybook": "storybook build"
36
28
  },