@plyaz/auth 1.0.0 → 1.0.2

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.
Files changed (99) hide show
  1. package/commits.txt +3 -3
  2. package/dist/common/index.cjs +3 -1
  3. package/dist/common/index.cjs.map +1 -1
  4. package/dist/common/index.mjs +3 -1
  5. package/dist/common/index.mjs.map +1 -1
  6. package/dist/index.cjs +424 -154
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +421 -152
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +2 -1
  11. package/release_message.txt +28 -0
  12. package/src/adapters/auth-adapter-factory.ts +4 -3
  13. package/src/adapters/auth-adapter.mapper.ts +2 -2
  14. package/src/adapters/base-auth.adapter.ts +17 -9
  15. package/src/adapters/clerk/clerk.adapter.ts +9 -12
  16. package/src/adapters/custom/custom.adapter.ts +19 -10
  17. package/src/adapters/index.ts +0 -1
  18. package/src/adapters/next-auth/authOptions.ts +20 -16
  19. package/src/adapters/next-auth/next-auth.adapter.ts +13 -15
  20. package/src/api/client.ts +4 -6
  21. package/src/audit/audit.logger.ts +19 -10
  22. package/src/client/components/ProtectedRoute.tsx +15 -11
  23. package/src/client/hooks/useAuth.ts +23 -21
  24. package/src/client/hooks/useConnectedAccounts.ts +57 -45
  25. package/src/client/hooks/usePermissions.ts +1 -1
  26. package/src/client/hooks/useRBAC.ts +6 -6
  27. package/src/client/hooks/useSession.ts +5 -5
  28. package/src/client/providers/AuthProvider.tsx +23 -17
  29. package/src/client/store/auth.store.ts +71 -62
  30. package/src/client/utils/storage.ts +45 -18
  31. package/src/common/constants/oauth-providers.ts +10 -7
  32. package/src/common/errors/auth.errors.ts +4 -4
  33. package/src/common/errors/specific-auth-errors.ts +5 -9
  34. package/src/common/regex/index.ts +6 -4
  35. package/src/common/types/auth.types.ts +47 -38
  36. package/src/common/types/index.ts +12 -6
  37. package/src/common/utils/index.ts +15 -11
  38. package/src/core/blacklist/token.blacklist.ts +13 -7
  39. package/src/core/index.ts +2 -2
  40. package/src/core/jwt/jwt.manager.ts +47 -22
  41. package/src/core/session/session.manager.ts +17 -14
  42. package/src/db/repositories/connected-account.repository.ts +120 -78
  43. package/src/db/repositories/role.repository.ts +41 -26
  44. package/src/db/repositories/session.repository.ts +9 -10
  45. package/src/db/repositories/user.repository.ts +105 -91
  46. package/src/flows/index.ts +2 -2
  47. package/src/flows/sign-in.flow.ts +28 -14
  48. package/src/flows/sign-up.flow.ts +31 -20
  49. package/src/index.ts +36 -37
  50. package/src/libs/clerk.helper.ts +6 -7
  51. package/src/libs/supabase.helper.ts +79 -61
  52. package/src/libs/supabaseClient.ts +3 -3
  53. package/src/providers/base/auth-provider.interface.ts +13 -11
  54. package/src/providers/base/index.ts +1 -1
  55. package/src/providers/index.ts +1 -1
  56. package/src/providers/oauth/facebook.provider.ts +63 -39
  57. package/src/providers/oauth/github.provider.ts +14 -10
  58. package/src/providers/oauth/google.provider.ts +39 -28
  59. package/src/providers/oauth/index.ts +1 -1
  60. package/src/rbac/dynamic-roles.ts +88 -54
  61. package/src/rbac/index.ts +4 -4
  62. package/src/rbac/permission-checker.ts +147 -75
  63. package/src/rbac/role-hierarchy.ts +8 -8
  64. package/src/rbac/role.manager.ts +11 -8
  65. package/src/security/csrf/csrf.protection.ts +9 -7
  66. package/src/security/index.ts +2 -2
  67. package/src/security/rate-limiting/auth/auth.controller.ts +2 -4
  68. package/src/security/rate-limiting/auth/rate-limiting.interface.ts +26 -6
  69. package/src/security/rate-limiting/auth.module.ts +1 -2
  70. package/src/server/auth.module.ts +55 -52
  71. package/src/server/decorators/auth.decorator.ts +9 -11
  72. package/src/server/decorators/auth.decorators.ts +8 -9
  73. package/src/server/decorators/current-user.decorator.ts +6 -6
  74. package/src/server/decorators/permission.decorator.ts +17 -9
  75. package/src/server/guards/auth.guard.ts +21 -16
  76. package/src/server/guards/custom-throttler.guard.ts +4 -9
  77. package/src/server/guards/permissions.guard.ts +32 -23
  78. package/src/server/guards/roles.guard.ts +14 -12
  79. package/src/server/middleware/auth.middleware.ts +4 -4
  80. package/src/server/middleware/session.middleware.ts +4 -4
  81. package/src/server/services/account.service.ts +96 -48
  82. package/src/server/services/auth.service.ts +57 -28
  83. package/src/server/services/brute-force.service.ts +24 -19
  84. package/src/server/services/index.ts +1 -1
  85. package/src/server/services/rate-limiter.service.ts +9 -4
  86. package/src/server/services/session.service.ts +84 -48
  87. package/src/server/services/token.service.ts +71 -51
  88. package/src/session/cookie-store.ts +47 -34
  89. package/src/session/enhanced-session-manager.ts +69 -48
  90. package/src/session/index.ts +5 -5
  91. package/src/session/memory-store.ts +37 -30
  92. package/src/session/redis-store.ts +105 -72
  93. package/src/strategies/oauth.strategy.ts +10 -9
  94. package/src/strategies/traditional-auth.strategy.ts +41 -29
  95. package/src/tokens/index.ts +4 -4
  96. package/src/tokens/refresh-token-manager.ts +70 -55
  97. package/src/tokens/token-validator.ts +109 -53
  98. package/vitest.setup.d.ts +2 -2
  99. package/vitest.setup.ts +1 -1
@@ -1,13 +1,18 @@
1
- import { createClient } from '@supabase/supabase-js';
2
- import type { ConnectedAccount, ConnectedAccountRepository as IConnectedAccountRepository, CreateConnectedAccountData, UpdateConnectedAccountData } from '@/common/types/auth.types';
1
+ import { createClient } from "@supabase/supabase-js";
2
+ import type {
3
+ ConnectedAccount,
4
+ ConnectedAccountRepository as IConnectedAccountRepository,
5
+ CreateConnectedAccountData,
6
+ UpdateConnectedAccountData,
7
+ } from "@/common/types/auth.types";
3
8
 
4
9
  /**
5
10
  * Repository for managing connected accounts (provider account linking)
6
- *
11
+ *
7
12
  * @description
8
13
  * Handles CRUD operations for external provider accounts linked to users.
9
14
  * Supports OAuth providers (Clerk, Google, Facebook, etc.) and stores tokens.
10
- *
15
+ *
11
16
  * @example
12
17
  * ```typescript
13
18
  * const repo = new ConnectedAccountRepository(supabaseUrl, supabaseKey);
@@ -26,42 +31,39 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
26
31
  this.supabase = createClient(supabaseUrl, supabaseKey);
27
32
  }
28
33
 
29
-
30
-
31
-
32
-
33
34
  /**
34
35
  * STEP 2: Create a new connected account
35
36
  * Links an external provider account to a user
36
- *
37
+ *
37
38
  * @param data - Connected account creation data
38
39
  * @returns Promise resolving to created ConnectedAccount
39
40
  * @throws Error if creation fails
40
41
  */
41
42
  async create(data: CreateConnectedAccountData): Promise<ConnectedAccount> {
42
43
  const insertData = this.transformToDbFormat(data);
43
-
44
+
44
45
  const { data: accountData, error } = await this.supabase
45
- .from('connected_accounts')
46
+ .from("connected_accounts")
46
47
  .insert(insertData)
47
48
  .select()
48
49
  .single();
49
50
 
50
- if (error || !accountData) throw new Error(`Failed to create connected account: ${error?.message}`);
51
+ if (error || !accountData)
52
+ throw new Error(`Failed to create connected account: ${error?.message}`);
51
53
  return this.mapToConnectedAccount(accountData);
52
54
  }
53
55
 
54
56
  /**
55
57
  * STEP 3: Find connected account by ID
56
- *
58
+ *
57
59
  * @param id - Connected account UUID
58
60
  * @returns Promise resolving to ConnectedAccount or null if not found
59
61
  */
60
62
  async findById(id: string): Promise<ConnectedAccount | null> {
61
63
  const { data, error } = await this.supabase
62
- .from('connected_accounts')
63
- .select('*')
64
- .eq('id', id)
64
+ .from("connected_accounts")
65
+ .select("*")
66
+ .eq("id", id)
65
67
  .single();
66
68
 
67
69
  if (error || !data) return null;
@@ -71,16 +73,16 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
71
73
  /**
72
74
  * STEP 4: Find all connected accounts for a user
73
75
  * Returns accounts ordered by creation date (newest first)
74
- *
76
+ *
75
77
  * @param userId - User UUID
76
78
  * @returns Promise resolving to array of ConnectedAccount
77
79
  */
78
80
  async findByUserId(userId: string): Promise<ConnectedAccount[]> {
79
81
  const { data, error } = await this.supabase
80
- .from('connected_accounts')
81
- .select('*')
82
- .eq('user_id', userId)
83
- .order('created_at', { ascending: false });
82
+ .from("connected_accounts")
83
+ .select("*")
84
+ .eq("user_id", userId)
85
+ .order("created_at", { ascending: false });
84
86
 
85
87
  if (error || !data) return [];
86
88
  return data.map(this.mapToConnectedAccount);
@@ -89,17 +91,20 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
89
91
  /**
90
92
  * STEP 5: Find connected account by provider credentials
91
93
  * Used for authentication - matches (provider, provider_account_id)
92
- *
94
+ *
93
95
  * @param provider - Provider name (e.g., 'clerk', 'google')
94
96
  * @param providerAccountId - Provider's user ID
95
97
  * @returns Promise resolving to ConnectedAccount or null if not found
96
98
  */
97
- async findByProvider(provider: string, providerAccountId: string): Promise<ConnectedAccount | null> {
99
+ async findByProvider(
100
+ provider: string,
101
+ providerAccountId: string,
102
+ ): Promise<ConnectedAccount | null> {
98
103
  const { data, error } = await this.supabase
99
- .from('connected_accounts')
100
- .select('*')
101
- .eq('provider', provider)
102
- .eq('provider_account_id', providerAccountId)
104
+ .from("connected_accounts")
105
+ .select("*")
106
+ .eq("provider", provider)
107
+ .eq("provider_account_id", providerAccountId)
103
108
  .single();
104
109
 
105
110
  if (error || !data) return null;
@@ -109,28 +114,35 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
109
114
  /**
110
115
  * Find connected account by provider and ID (alias for compatibility)
111
116
  */
112
- async findByProviderAndId(provider: string, providerAccountId: string): Promise<ConnectedAccount | null> {
117
+ async findByProviderAndId(
118
+ provider: string,
119
+ providerAccountId: string,
120
+ ): Promise<ConnectedAccount | null> {
113
121
  return this.findByProvider(provider, providerAccountId);
114
122
  }
115
123
 
116
124
  /**
117
125
  * Update tokens for connected account
118
126
  */
119
- async updateTokens(accountId: string, accessToken: string, refreshToken?: string): Promise<void> {
127
+ async updateTokens(
128
+ accountId: string,
129
+ accessToken: string,
130
+ refreshToken?: string,
131
+ ): Promise<void> {
120
132
  const updateData = {
121
133
  access_token_encrypted: accessToken,
122
134
  updated_at: new Date(),
123
- refresh_token_encrypted:refreshToken
135
+ refresh_token_encrypted: refreshToken,
124
136
  };
125
-
137
+
126
138
  if (refreshToken) {
127
139
  updateData.refresh_token_encrypted = refreshToken;
128
140
  }
129
141
 
130
142
  const { error } = await this.supabase
131
- .from('connected_accounts')
143
+ .from("connected_accounts")
132
144
  .update(updateData)
133
- .eq('id', accountId);
145
+ .eq("id", accountId);
134
146
 
135
147
  if (error) throw new Error(`Failed to update tokens: ${error.message}`);
136
148
  }
@@ -138,39 +150,44 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
138
150
  /**
139
151
  * STEP 6: Update connected account
140
152
  * Updates tokens, profile info, or metadata
141
- *
153
+ *
142
154
  * @param id - Connected account UUID
143
155
  * @param data - Partial update data
144
156
  * @returns Promise resolving to updated ConnectedAccount
145
157
  * @throws Error if update fails
146
158
  */
147
- async update(id: string, data: UpdateConnectedAccountData): Promise<ConnectedAccount> {
159
+ async update(
160
+ id: string,
161
+ data: UpdateConnectedAccountData,
162
+ ): Promise<ConnectedAccount> {
148
163
  const updateData = this.buildUpdateData(data);
149
164
 
150
165
  const { data: accountData, error } = await this.supabase
151
- .from('connected_accounts')
166
+ .from("connected_accounts")
152
167
  .update(updateData)
153
- .eq('id', id)
168
+ .eq("id", id)
154
169
  .select()
155
170
  .single();
156
171
 
157
- if (error || !accountData) throw new Error(`Failed to update connected account: ${error?.message}`);
172
+ if (error || !accountData)
173
+ throw new Error(`Failed to update connected account: ${error?.message}`);
158
174
  return this.mapToConnectedAccount(accountData);
159
175
  }
160
176
 
161
177
  /**
162
178
  * STEP 7: Delete connected account (unlink provider)
163
- *
179
+ *
164
180
  * @param id - Connected account UUID
165
181
  * @throws Error if deletion fails
166
182
  */
167
183
  async delete(id: string): Promise<void> {
168
184
  const { error } = await this.supabase
169
- .from('connected_accounts')
185
+ .from("connected_accounts")
170
186
  .delete()
171
- .eq('id', id);
187
+ .eq("id", id);
172
188
 
173
- if (error) throw new Error(`Failed to delete connected account: ${error.message}`);
189
+ if (error)
190
+ throw new Error(`Failed to delete connected account: ${error.message}`);
174
191
  }
175
192
 
176
193
  /**
@@ -181,11 +198,11 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
181
198
  */
182
199
  async findPrimary(userId: string): Promise<ConnectedAccount | null> {
183
200
  const { data, error } = await this.supabase
184
- .from('connected_accounts')
185
- .select('*')
186
- .eq('user_id', userId)
187
- .eq('is_primary', true)
188
- .eq('is_active', true)
201
+ .from("connected_accounts")
202
+ .select("*")
203
+ .eq("user_id", userId)
204
+ .eq("is_primary", true)
205
+ .eq("is_active", true)
189
206
  .single();
190
207
 
191
208
  if (error || !data) return null;
@@ -201,18 +218,19 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
201
218
  async setPrimary(userId: string, accountId: string): Promise<void> {
202
219
  // First, unset all primary accounts for user
203
220
  await this.supabase
204
- .from('connected_accounts')
221
+ .from("connected_accounts")
205
222
  .update({ is_primary: false, updated_at: new Date() })
206
- .eq('user_id', userId);
223
+ .eq("user_id", userId);
207
224
 
208
225
  // Then set the specified account as primary
209
226
  const { error } = await this.supabase
210
- .from('connected_accounts')
227
+ .from("connected_accounts")
211
228
  .update({ is_primary: true, updated_at: new Date() })
212
- .eq('id', accountId)
213
- .eq('user_id', userId);
229
+ .eq("id", accountId)
230
+ .eq("user_id", userId);
214
231
 
215
- if (error) throw new Error(`Failed to set primary account: ${error.message}`);
232
+ if (error)
233
+ throw new Error(`Failed to set primary account: ${error.message}`);
216
234
  }
217
235
 
218
236
  /**
@@ -223,10 +241,14 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
223
241
  * @param providerData - Provider account data
224
242
  * @returns Created connected account
225
243
  */
226
- async linkAccount(userId: string, provider: string, providerData: ConnectedAccount): Promise<ConnectedAccount> {
244
+ async linkAccount(
245
+ userId: string,
246
+ provider: string,
247
+ providerData: ConnectedAccount,
248
+ ): Promise<ConnectedAccount> {
227
249
  const accountData = {
228
250
  user_id: userId,
229
- provider_type: providerData.providerType ?? 'OAUTH',
251
+ provider_type: providerData.providerType ?? "OAUTH",
230
252
  provider,
231
253
  provider_account_id: providerData.providerAccountId,
232
254
  provider_email: providerData.providerEmail,
@@ -239,22 +261,23 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
239
261
  is_active: true,
240
262
  linked_at: new Date(),
241
263
  created_at: new Date(),
242
- updated_at: new Date()
264
+ updated_at: new Date(),
243
265
  };
244
266
 
245
267
  const { data, error } = await this.supabase
246
- .from('connected_accounts')
268
+ .from("connected_accounts")
247
269
  .insert(accountData)
248
270
  .select()
249
271
  .single();
250
272
 
251
- if (error || !data) throw new Error(`Failed to link account: ${error?.message}`);
252
-
273
+ if (error || !data)
274
+ throw new Error(`Failed to link account: ${error?.message}`);
275
+
253
276
  const connectedAccount = this.mapToConnectedAccount(data);
254
-
277
+
255
278
  // Emit account linked event
256
279
  this.emitAccountLinkedEvent(userId, provider, connectedAccount.id);
257
-
280
+
258
281
  return connectedAccount;
259
282
  }
260
283
 
@@ -267,18 +290,18 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
267
290
  // Get account details
268
291
  const account = await this.findById(accountId);
269
292
  if (!account) {
270
- throw new Error('Connected account not found');
293
+ throw new Error("Connected account not found");
271
294
  }
272
295
 
273
296
  // Check if this is the last authentication method
274
297
  const userAccounts = await this.findByUserId(account.userId);
275
298
  if (userAccounts.length <= 1) {
276
- throw new Error('Cannot unlink the last authentication method');
299
+ throw new Error("Cannot unlink the last authentication method");
277
300
  }
278
301
 
279
302
  // Delete the account
280
303
  await this.delete(accountId);
281
-
304
+
282
305
  // Emit account unlinked event
283
306
  this.emitAccountUnlinkedEvent(account.userId, account.provider, accountId);
284
307
  }
@@ -290,13 +313,17 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
290
313
  * @param accountId - Account identifier
291
314
  * @private
292
315
  */
293
- private emitAccountLinkedEvent(userId: string, provider: string, accountId: string): void {
316
+ private emitAccountLinkedEvent(
317
+ userId: string,
318
+ provider: string,
319
+ accountId: string,
320
+ ): void {
294
321
  // Mock event emission - in real implementation would use event system
295
- globalThis.console.log('Event: auth.account.linked', {
322
+ globalThis.console.log("Event: auth.account.linked", {
296
323
  userId,
297
324
  provider,
298
325
  accountId,
299
- timestamp: new Date()
326
+ timestamp: new Date(),
300
327
  });
301
328
  }
302
329
 
@@ -307,13 +334,17 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
307
334
  * @param accountId - Account identifier
308
335
  * @private
309
336
  */
310
- private emitAccountUnlinkedEvent(userId: string, provider: string, accountId: string): void {
337
+ private emitAccountUnlinkedEvent(
338
+ userId: string,
339
+ provider: string,
340
+ accountId: string,
341
+ ): void {
311
342
  // Mock event emission - in real implementation would use event system
312
- globalThis.console.log('Event: auth.account.unlinked', {
343
+ globalThis.console.log("Event: auth.account.unlinked", {
313
344
  userId,
314
345
  provider,
315
346
  accountId,
316
- timestamp: new Date()
347
+ timestamp: new Date(),
317
348
  });
318
349
  }
319
350
 
@@ -321,7 +352,12 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
321
352
  * Transform camelCase DTO to snake_case database format
322
353
  * @private
323
354
  */
324
- private transformToDbFormat(data: CreateConnectedAccountData): Record<string, string| undefined | Record<string, unknown> |Date | boolean > {
355
+ private transformToDbFormat(
356
+ data: CreateConnectedAccountData,
357
+ ): Record<
358
+ string,
359
+ string | undefined | Record<string, unknown> | Date | boolean
360
+ > {
325
361
  return {
326
362
  user_id: data.userId,
327
363
  provider_type: data.providerType,
@@ -351,9 +387,13 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
351
387
  * Build update data with only defined fields
352
388
  * @private
353
389
  */
354
- private buildUpdateData(data: UpdateConnectedAccountData): Record<string, string> {
355
- const result: Record<string, string> = { updated_at: new Date().toString() };
356
-
390
+ private buildUpdateData(
391
+ data: UpdateConnectedAccountData,
392
+ ): Record<string, string> {
393
+ const result: Record<string, string> = {
394
+ updated_at: new Date().toString(),
395
+ };
396
+
357
397
  // Only include defined fields
358
398
  Object.entries(data).forEach(([key, value]) => {
359
399
  if (value !== undefined) {
@@ -361,7 +401,7 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
361
401
  result[dbKey] = value;
362
402
  }
363
403
  });
364
-
404
+
365
405
  return result;
366
406
  }
367
407
 
@@ -370,13 +410,13 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
370
410
  * @private
371
411
  */
372
412
  private camelToSnake(str: string): string {
373
- return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
413
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
374
414
  }
375
415
 
376
416
  /**
377
417
  * STEP 8: Map database row to ConnectedAccount interface
378
418
  * Converts snake_case to camelCase
379
- *
419
+ *
380
420
  * @param data - Raw database row
381
421
  * @returns Mapped ConnectedAccount object
382
422
  * @private
@@ -398,7 +438,9 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
398
438
  chainId: data.chainId,
399
439
  accessTokenEncrypted: data.accessTokenEncrypted,
400
440
  refreshTokenEncrypted: data.refreshTokenEncrypted,
401
- tokenExpiresAt: data.tokenExpiresAt ? new Date(data.tokenExpiresAt) : undefined,
441
+ tokenExpiresAt: data.tokenExpiresAt
442
+ ? new Date(data.tokenExpiresAt)
443
+ : undefined,
402
444
  tokenScope: data.tokenScope,
403
445
  isPrimary: data.isPrimary,
404
446
  isVerified: data.isVerified,
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * @fileoverview Role hierarchy manager for @plyaz/auth
3
3
  * @module @plyaz/auth/rbac/role-hierarchy
4
- *
4
+ *
5
5
  * @description
6
6
  * Manages role hierarchy and inheritance for role-based access control.
7
7
  * Handles role precedence, permission inheritance, and hierarchical
8
8
  * role validation. Enables complex organizational structures with
9
9
  * inherited permissions and role-based delegation.
10
- *
10
+ *
11
11
  * @example
12
12
  * ```typescript
13
13
  * import { RoleHierarchy } from '@plyaz/auth';
14
- *
14
+ *
15
15
  * const hierarchy = new RoleHierarchy();
16
16
  * hierarchy.addRole({ id: 'admin', name: 'Admin', permissions: ['*'] });
17
17
  * hierarchy.addRole({ id: 'moderator', name: 'Moderator', permissions: ['read', 'write'] });
18
18
  * hierarchy.addRoleRelationship('moderator', 'admin');
19
- *
19
+ *
20
20
  * const hasAdminAccess = hierarchy.hasPermission('moderator', 'delete');
21
21
  * console.log(hierarchy.getHierarchyTree());
22
22
  * ```
@@ -29,9 +29,9 @@ import type { Role, RoleHierarchyConfig, RoleNode } from "@plyaz/types";
29
29
  * Manages role relationships and permission inheritance
30
30
  */
31
31
 
32
- interface AddRole extends Role {
33
- permissions?:[]
34
- }
32
+ interface AddRole extends Role {
33
+ permissions?: [];
34
+ }
35
35
 
36
36
  export class RoleHierarchy {
37
37
  private readonly config: RoleHierarchyConfig;
@@ -44,7 +44,7 @@ export class RoleHierarchy {
44
44
  maxDepth: 10,
45
45
  detectCircular: true,
46
46
  cacheInheritance: true,
47
- ...config
47
+ ...config,
48
48
  };
49
49
  }
50
50
 
@@ -57,11 +57,11 @@ export class RoleHierarchy {
57
57
  role,
58
58
  parents: new Set(),
59
59
  children: new Set(),
60
- permissions: new Set(role.permissions ?? [])
60
+ permissions: new Set(role.permissions ?? []),
61
61
  };
62
62
 
63
63
  this.roles.set(role.id, roleNode);
64
-
64
+
65
65
  // Clear cache when hierarchy changes
66
66
  if (this.config.cacheInheritance) {
67
67
  this.hierarchyCache.clear();
@@ -73,7 +73,7 @@ export class RoleHierarchy {
73
73
  */
74
74
  removeRole(roleId: string): void {
75
75
  const roleNode = this.roles.get(roleId);
76
-
76
+
77
77
  if (!roleNode) {
78
78
  return;
79
79
  }
@@ -94,7 +94,7 @@ export class RoleHierarchy {
94
94
  }
95
95
 
96
96
  this.roles.delete(roleId);
97
-
97
+
98
98
  // Clear cache when hierarchy changes
99
99
  if (this.config.cacheInheritance) {
100
100
  this.hierarchyCache.clear();
@@ -109,18 +109,23 @@ export class RoleHierarchy {
109
109
  const parentRole = this.roles.get(parentRoleId);
110
110
 
111
111
  if (!childRole || !parentRole) {
112
- throw new Error('Role not found');
112
+ throw new Error("Role not found");
113
113
  }
114
114
 
115
115
  // Check for circular dependencies
116
- if (this.config.detectCircular && this.inheritsFrom(parentRoleId, childRoleId)) {
117
- throw new Error('Adding relationship would create circular dependency');
116
+ if (
117
+ this.config.detectCircular &&
118
+ this.inheritsFrom(parentRoleId, childRoleId)
119
+ ) {
120
+ throw new Error("Adding relationship would create circular dependency");
118
121
  }
119
122
 
120
123
  // Check hierarchy depth
121
124
  const depth = this.calculateDepth(parentRoleId);
122
125
  if (depth >= this.config.maxDepth) {
123
- throw new Error(`Maximum hierarchy depth exceeded: ${depth}/${this.config.maxDepth}`);
126
+ throw new Error(
127
+ `Maximum hierarchy depth exceeded: ${depth}/${this.config.maxDepth}`,
128
+ );
124
129
  }
125
130
 
126
131
  // Add relationship
@@ -290,7 +295,6 @@ export class RoleHierarchy {
290
295
  return effectivePermissions;
291
296
  }
292
297
 
293
-
294
298
  /**
295
299
  * Validate hierarchy integrity
296
300
  */
@@ -313,7 +317,9 @@ export class RoleHierarchy {
313
317
  for (const roleId of this.roles.keys()) {
314
318
  const depth = this.calculateDepth(roleId);
315
319
  if (depth > this.config.maxDepth) {
316
- issues.push(`Role ${roleId} exceeds maximum hierarchy depth: ${depth}/${this.config.maxDepth}`);
320
+ issues.push(
321
+ `Role ${roleId} exceeds maximum hierarchy depth: ${depth}/${this.config.maxDepth}`,
322
+ );
317
323
  }
318
324
  }
319
325
 
@@ -321,20 +327,24 @@ export class RoleHierarchy {
321
327
  for (const [roleId, roleNode] of this.roles.entries()) {
322
328
  for (const parentId of roleNode.parents) {
323
329
  if (!this.roles.has(parentId)) {
324
- issues.push(`Role ${roleId} references non-existent parent: ${parentId}`);
330
+ issues.push(
331
+ `Role ${roleId} references non-existent parent: ${parentId}`,
332
+ );
325
333
  }
326
334
  }
327
335
 
328
336
  for (const childId of roleNode.children) {
329
337
  if (!this.roles.has(childId)) {
330
- issues.push(`Role ${roleId} references non-existent child: ${childId}`);
338
+ issues.push(
339
+ `Role ${roleId} references non-existent child: ${childId}`,
340
+ );
331
341
  }
332
342
  }
333
343
  }
334
344
 
335
345
  return {
336
346
  valid: issues.length === 0,
337
- issues
347
+ issues,
338
348
  };
339
349
  }
340
350
 
@@ -363,7 +373,7 @@ export class RoleHierarchy {
363
373
  for (const [roleId, roleNode] of this.roles.entries()) {
364
374
  if (roleNode.parents.size === 0) rootRoles++;
365
375
  if (roleNode.children.size === 0) leafRoles++;
366
-
376
+
367
377
  const depth = this.calculateDepth(roleId);
368
378
  maxDepth = Math.max(maxDepth, depth);
369
379
  totalPermissions += roleNode.permissions.size;
@@ -374,7 +384,10 @@ export class RoleHierarchy {
374
384
  maxDepth,
375
385
  rootRoles,
376
386
  leafRoles,
377
- avgPermissions: this.roles.size > 0 ? Math.round(totalPermissions / this.roles.size) : 0
387
+ avgPermissions:
388
+ this.roles.size > 0
389
+ ? Math.round(totalPermissions / this.roles.size)
390
+ : 0,
378
391
  };
379
392
  }
380
393
 
@@ -461,7 +474,7 @@ export class RoleHierarchy {
461
474
  private detectCircularRecursive(
462
475
  roleId: string,
463
476
  visited: Set<string>,
464
- recursionStack: Set<string>
477
+ recursionStack: Set<string>,
465
478
  ): boolean {
466
479
  visited.add(roleId);
467
480
  recursionStack.add(roleId);
@@ -496,7 +509,9 @@ export class RoleHierarchy {
496
509
  /**
497
510
  * Build role tree recursively
498
511
  */
499
- private buildRoleTree(roleId: string): { role: Role; children: Record<string, unknown> } | null {
512
+ private buildRoleTree(
513
+ roleId: string,
514
+ ): { role: Role; children: Record<string, unknown> } | null {
500
515
  const roleNode = this.roles.get(roleId);
501
516
  if (!roleNode) {
502
517
  return null;
@@ -504,7 +519,7 @@ export class RoleHierarchy {
504
519
 
505
520
  const tree: { role: Role; children: Record<string, unknown> } = {
506
521
  role: roleNode.role,
507
- children: {}
522
+ children: {},
508
523
  };
509
524
 
510
525
  for (const childId of roleNode.children) {