@karbonjs/auth 0.1.0

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/cache/user-cache.d.ts +19 -0
  4. package/dist/cache/user-cache.d.ts.map +1 -0
  5. package/dist/cache/user-cache.js +49 -0
  6. package/dist/cache/user-cache.js.map +1 -0
  7. package/dist/cache/user-cache.test.d.ts +2 -0
  8. package/dist/cache/user-cache.test.d.ts.map +1 -0
  9. package/dist/cache/user-cache.test.js +46 -0
  10. package/dist/cache/user-cache.test.js.map +1 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +4 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/roles/roles.d.ts +33 -0
  16. package/dist/roles/roles.d.ts.map +1 -0
  17. package/dist/roles/roles.js +57 -0
  18. package/dist/roles/roles.js.map +1 -0
  19. package/dist/roles/roles.test.d.ts +2 -0
  20. package/dist/roles/roles.test.d.ts.map +1 -0
  21. package/dist/roles/roles.test.js +50 -0
  22. package/dist/roles/roles.test.js.map +1 -0
  23. package/dist/roles.d.ts +33 -0
  24. package/dist/roles.d.ts.map +1 -0
  25. package/dist/roles.js +57 -0
  26. package/dist/roles.js.map +1 -0
  27. package/dist/token/token-manager.d.ts +26 -0
  28. package/dist/token/token-manager.d.ts.map +1 -0
  29. package/dist/token/token-manager.js +55 -0
  30. package/dist/token/token-manager.js.map +1 -0
  31. package/dist/token-manager.d.ts +35 -0
  32. package/dist/token-manager.d.ts.map +1 -0
  33. package/dist/token-manager.js +56 -0
  34. package/dist/token-manager.js.map +1 -0
  35. package/dist/user-cache.d.ts +30 -0
  36. package/dist/user-cache.d.ts.map +1 -0
  37. package/dist/user-cache.js +51 -0
  38. package/dist/user-cache.js.map +1 -0
  39. package/package.json +28 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KarbonJS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @karbonjs/auth
2
+
3
+ Authentication helpers for Karbon — token management, user cache, and role hierarchy. Pure TypeScript, framework-agnostic.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @karbonjs/auth @karbonjs/types
9
+ ```
10
+
11
+ ## User Cache (SSR)
12
+
13
+ Prevents hammering your `/profile` endpoint on every SSR request.
14
+
15
+ ```typescript
16
+ import { createUserCache } from '@karbonjs/auth'
17
+
18
+ const userCache = createUserCache({
19
+ ttl: 120_000, // 2 minutes
20
+ maxSize: 500, // max cached users
21
+ })
22
+
23
+ // In your SSR hook
24
+ let user = userCache.get(token)
25
+ if (!user) {
26
+ user = await fetchProfile(token)
27
+ if (user) userCache.set(token, user)
28
+ }
29
+ ```
30
+
31
+ ## Token Manager
32
+
33
+ Server-side token refresh with deduplication (prevents multiple concurrent refresh requests).
34
+
35
+ ```typescript
36
+ import { createTokenManager } from '@karbonjs/auth'
37
+
38
+ const tokenManager = createTokenManager({
39
+ apiUrl: 'http://localhost:3005/api/v1',
40
+ refreshEndpoint: '/auth/refresh',
41
+ onRefresh: (tokens) => {
42
+ // Set new cookies
43
+ cookies.set('token', tokens.token)
44
+ cookies.set('refresh_token', tokens.refreshToken)
45
+ },
46
+ onExpired: () => {
47
+ // Clear session
48
+ cookies.delete('token')
49
+ },
50
+ })
51
+
52
+ const newTokens = await tokenManager.refresh(refreshToken, sessionId)
53
+ ```
54
+
55
+ ## Role Hierarchy
56
+
57
+ Check roles with inheritance support.
58
+
59
+ ```typescript
60
+ import { hasRole, isAdmin, highestRole } from '@karbonjs/auth'
61
+
62
+ const hierarchy = {
63
+ 'ROLE_SUPER_ADMIN': ['ROLE_ADMIN'],
64
+ 'ROLE_ADMIN': ['ROLE_EDITOR'],
65
+ 'ROLE_EDITOR': ['ROLE_USER'],
66
+ 'ROLE_USER': [],
67
+ }
68
+
69
+ hasRole(['ROLE_ADMIN'], 'ROLE_USER', hierarchy) // true (inherits)
70
+ hasRole(['ROLE_USER'], 'ROLE_ADMIN', hierarchy) // false
71
+ isAdmin(['ROLE_SUPER_ADMIN'], hierarchy) // true
72
+ highestRole(['ROLE_USER', 'ROLE_ADMIN'], hierarchy) // "ROLE_ADMIN"
73
+ ```
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,19 @@
1
+ import type { AuthUser } from '@karbonjs/types';
2
+ export interface UserCacheOptions {
3
+ /** Max entries in cache (default: 500) */
4
+ maxSize?: number;
5
+ /** TTL in milliseconds (default: 2 minutes) */
6
+ ttl?: number;
7
+ }
8
+ /**
9
+ * In-memory user cache for SSR hooks.
10
+ * Prevents hammering the /profile endpoint on every request.
11
+ */
12
+ export declare function createUserCache(opts?: UserCacheOptions): {
13
+ get(token: string): AuthUser | null;
14
+ set(token: string, user: AuthUser): void;
15
+ invalidate(token: string): void;
16
+ clear(): void;
17
+ readonly size: number;
18
+ };
19
+ //# sourceMappingURL=user-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-cache.d.ts","sourceRoot":"","sources":["../../src/cache/user-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAOD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,gBAAqB;eAa5C,MAAM,GAAG,QAAQ,GAAG,IAAI;eAUxB,MAAM,QAAQ,QAAQ,GAAG,IAAI;sBAWtB,MAAM,GAAG,IAAI;aAItB,IAAI;mBAID,MAAM;EAIrB"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * In-memory user cache for SSR hooks.
3
+ * Prevents hammering the /profile endpoint on every request.
4
+ */
5
+ export function createUserCache(opts = {}) {
6
+ const maxSize = opts.maxSize ?? 500;
7
+ const ttl = opts.ttl ?? 120_000;
8
+ const store = new Map();
9
+ function purgeExpired() {
10
+ const now = Date.now();
11
+ for (const [key, entry] of store) {
12
+ if (now - entry.timestamp > ttl)
13
+ store.delete(key);
14
+ }
15
+ }
16
+ return {
17
+ get(token) {
18
+ const entry = store.get(token);
19
+ if (!entry)
20
+ return null;
21
+ if (Date.now() - entry.timestamp > ttl) {
22
+ store.delete(token);
23
+ return null;
24
+ }
25
+ return entry.user;
26
+ },
27
+ set(token, user) {
28
+ if (store.size >= maxSize) {
29
+ purgeExpired();
30
+ }
31
+ if (store.size >= maxSize) {
32
+ const oldest = store.keys().next().value;
33
+ if (oldest)
34
+ store.delete(oldest);
35
+ }
36
+ store.set(token, { user, timestamp: Date.now() });
37
+ },
38
+ invalidate(token) {
39
+ store.delete(token);
40
+ },
41
+ clear() {
42
+ store.clear();
43
+ },
44
+ get size() {
45
+ return store.size;
46
+ },
47
+ };
48
+ }
49
+ //# sourceMappingURL=user-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-cache.js","sourceRoot":"","sources":["../../src/cache/user-cache.ts"],"names":[],"mappings":"AAcA;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAAyB,EAAE;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,GAAG,CAAA;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAA;IAC/B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAA;IAE3C,SAAS,YAAY;QACnB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;YACjC,IAAI,GAAG,GAAG,KAAK,CAAC,SAAS,GAAG,GAAG;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpD,CAAC;IACH,CAAC;IAED,OAAO;QACL,GAAG,CAAC,KAAa;YACf,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YAC9B,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAA;YACvB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;gBACvC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBACnB,OAAO,IAAI,CAAA;YACb,CAAC;YACD,OAAO,KAAK,CAAC,IAAI,CAAA;QACnB,CAAC;QAED,GAAG,CAAC,KAAa,EAAE,IAAc;YAC/B,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC1B,YAAY,EAAE,CAAA;YAChB,CAAC;YACD,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAA;gBACxC,IAAI,MAAM;oBAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAClC,CAAC;YACD,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,UAAU,CAAC,KAAa;YACtB,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;QAED,KAAK;YACH,KAAK,CAAC,KAAK,EAAE,CAAA;QACf,CAAC;QAED,IAAI,IAAI;YACN,OAAO,KAAK,CAAC,IAAI,CAAA;QACnB,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=user-cache.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-cache.test.d.ts","sourceRoot":"","sources":["../../src/cache/user-cache.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createUserCache } from './user-cache';
3
+ const mockUser = { id: 1, username: 'david', email: 'david@test.com', roles: ['ROLE_USER'] };
4
+ describe('createUserCache', () => {
5
+ it('stores and retrieves users', () => {
6
+ const cache = createUserCache();
7
+ cache.set('token123', mockUser);
8
+ expect(cache.get('token123')).toEqual(mockUser);
9
+ });
10
+ it('returns null for missing token', () => {
11
+ const cache = createUserCache();
12
+ expect(cache.get('nonexistent')).toBeNull();
13
+ });
14
+ it('invalidates entries', () => {
15
+ const cache = createUserCache();
16
+ cache.set('token123', mockUser);
17
+ cache.invalidate('token123');
18
+ expect(cache.get('token123')).toBeNull();
19
+ });
20
+ it('clears all entries', () => {
21
+ const cache = createUserCache();
22
+ cache.set('a', mockUser);
23
+ cache.set('b', mockUser);
24
+ cache.clear();
25
+ expect(cache.size).toBe(0);
26
+ });
27
+ it('respects TTL', () => {
28
+ vi.useFakeTimers();
29
+ const cache = createUserCache({ ttl: 1000 });
30
+ cache.set('token123', mockUser);
31
+ expect(cache.get('token123')).toEqual(mockUser);
32
+ vi.advanceTimersByTime(1001);
33
+ expect(cache.get('token123')).toBeNull();
34
+ vi.useRealTimers();
35
+ });
36
+ it('evicts oldest when maxSize reached', () => {
37
+ const cache = createUserCache({ maxSize: 2 });
38
+ cache.set('a', mockUser);
39
+ cache.set('b', mockUser);
40
+ cache.set('c', mockUser);
41
+ expect(cache.size).toBe(2);
42
+ expect(cache.get('a')).toBeNull();
43
+ expect(cache.get('b')).toEqual(mockUser);
44
+ });
45
+ });
46
+ //# sourceMappingURL=user-cache.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-cache.test.js","sourceRoot":"","sources":["../../src/cache/user-cache.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { createUserCache } from './cache/user-cache';
2
+ export type { UserCacheOptions } from './cache/user-cache';
3
+ export { createTokenManager } from './token/token-manager';
4
+ export type { TokenManagerConfig, TokenPair } from './token/token-manager';
5
+ export { hasRole, isAdmin, highestRole } from './roles/roles';
6
+ export type { RoleHierarchy } from './roles/roles';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,YAAY,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAE1E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC7D,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createUserCache } from './cache/user-cache';
2
+ export { createTokenManager } from './token/token-manager';
3
+ export { hasRole, isAdmin, highestRole } from './roles/roles';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAG1D,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Role hierarchy definition.
3
+ * Higher roles inherit permissions from lower roles.
4
+ *
5
+ * ```ts
6
+ * const hierarchy: RoleHierarchy = {
7
+ * 'ROLE_SUPER_ADMIN': ['ROLE_ADMIN'],
8
+ * 'ROLE_ADMIN': ['ROLE_EDITOR'],
9
+ * 'ROLE_EDITOR': ['ROLE_USER'],
10
+ * 'ROLE_USER': [],
11
+ * }
12
+ * ```
13
+ */
14
+ export type RoleHierarchy = Record<string, string[]>;
15
+ /**
16
+ * Check if a user's roles satisfy a required role, considering hierarchy.
17
+ *
18
+ * ```ts
19
+ * hasRole(['ROLE_ADMIN'], 'ROLE_USER', hierarchy) // true (admin inherits user)
20
+ * hasRole(['ROLE_USER'], 'ROLE_ADMIN', hierarchy) // false
21
+ * ```
22
+ */
23
+ export declare function hasRole(userRoles: string[], requiredRole: string, hierarchy?: RoleHierarchy): boolean;
24
+ /**
25
+ * Get the highest priority role from a list.
26
+ * Priority is determined by hierarchy depth (deeper = higher).
27
+ */
28
+ export declare function highestRole(userRoles: string[], hierarchy: RoleHierarchy): string | null;
29
+ /**
30
+ * Shorthand: check if user has admin-level access.
31
+ */
32
+ export declare function isAdmin(userRoles: string[], hierarchy?: RoleHierarchy): boolean;
33
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../src/roles/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAEpD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAE,aAAkB,GAAG,OAAO,CAQzG;AAiBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAexF;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,SAAS,GAAE,aAAkB,GAAG,OAAO,CAEnF"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Check if a user's roles satisfy a required role, considering hierarchy.
3
+ *
4
+ * ```ts
5
+ * hasRole(['ROLE_ADMIN'], 'ROLE_USER', hierarchy) // true (admin inherits user)
6
+ * hasRole(['ROLE_USER'], 'ROLE_ADMIN', hierarchy) // false
7
+ * ```
8
+ */
9
+ export function hasRole(userRoles, requiredRole, hierarchy = {}) {
10
+ if (userRoles.includes(requiredRole))
11
+ return true;
12
+ // Check hierarchy: does any user role inherit the required role?
13
+ for (const role of userRoles) {
14
+ if (resolveRoles(role, hierarchy).includes(requiredRole))
15
+ return true;
16
+ }
17
+ return false;
18
+ }
19
+ /**
20
+ * Resolve all inherited roles for a given role.
21
+ */
22
+ function resolveRoles(role, hierarchy, visited = new Set()) {
23
+ if (visited.has(role))
24
+ return [];
25
+ visited.add(role);
26
+ const children = hierarchy[role] ?? [];
27
+ const all = [role];
28
+ for (const child of children) {
29
+ all.push(...resolveRoles(child, hierarchy, visited));
30
+ }
31
+ return all;
32
+ }
33
+ /**
34
+ * Get the highest priority role from a list.
35
+ * Priority is determined by hierarchy depth (deeper = higher).
36
+ */
37
+ export function highestRole(userRoles, hierarchy) {
38
+ if (userRoles.length === 0)
39
+ return null;
40
+ let best = userRoles[0];
41
+ let bestDepth = 0;
42
+ for (const role of userRoles) {
43
+ const depth = resolveRoles(role, hierarchy).length;
44
+ if (depth > bestDepth) {
45
+ bestDepth = depth;
46
+ best = role;
47
+ }
48
+ }
49
+ return best;
50
+ }
51
+ /**
52
+ * Shorthand: check if user has admin-level access.
53
+ */
54
+ export function isAdmin(userRoles, hierarchy = {}) {
55
+ return hasRole(userRoles, 'ROLE_ADMIN', hierarchy);
56
+ }
57
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","sourceRoot":"","sources":["../../src/roles/roles.ts"],"names":[],"mappings":"AAeA;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,SAAmB,EAAE,YAAoB,EAAE,YAA2B,EAAE;IAC9F,IAAI,SAAS,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjD,iEAAiE;IACjE,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,IAAI,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,OAAO,IAAI,CAAA;IACvE,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,IAAY,EAAE,SAAwB,EAAE,UAAU,IAAI,GAAG,EAAU;IACvF,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IAChC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAEjB,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;IAClB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,GAAG,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAA;IACtD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAmB,EAAE,SAAwB;IACvE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEvC,IAAI,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;IACvB,IAAI,SAAS,GAAG,CAAC,CAAA;IAEjB,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,MAAM,CAAA;QAClD,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,SAAS,GAAG,KAAK,CAAA;YACjB,IAAI,GAAG,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,SAAmB,EAAE,YAA2B,EAAE;IACxE,OAAO,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS,CAAC,CAAA;AACpD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=roles.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.test.d.ts","sourceRoot":"","sources":["../../src/roles/roles.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { hasRole, highestRole, isAdmin } from './roles';
3
+ const hierarchy = {
4
+ 'ROLE_SUPER_ADMIN': ['ROLE_ADMIN'],
5
+ 'ROLE_ADMIN': ['ROLE_EDITOR'],
6
+ 'ROLE_EDITOR': ['ROLE_USER'],
7
+ 'ROLE_USER': [],
8
+ };
9
+ describe('hasRole', () => {
10
+ it('returns true for direct role', () => {
11
+ expect(hasRole(['ROLE_USER'], 'ROLE_USER', hierarchy)).toBe(true);
12
+ });
13
+ it('returns true for inherited role', () => {
14
+ expect(hasRole(['ROLE_ADMIN'], 'ROLE_USER', hierarchy)).toBe(true);
15
+ expect(hasRole(['ROLE_ADMIN'], 'ROLE_EDITOR', hierarchy)).toBe(true);
16
+ });
17
+ it('returns false for higher role', () => {
18
+ expect(hasRole(['ROLE_USER'], 'ROLE_ADMIN', hierarchy)).toBe(false);
19
+ });
20
+ it('works without hierarchy (exact match only)', () => {
21
+ expect(hasRole(['ROLE_USER'], 'ROLE_USER')).toBe(true);
22
+ expect(hasRole(['ROLE_ADMIN'], 'ROLE_USER')).toBe(false);
23
+ });
24
+ it('handles deep hierarchy', () => {
25
+ expect(hasRole(['ROLE_SUPER_ADMIN'], 'ROLE_USER', hierarchy)).toBe(true);
26
+ });
27
+ });
28
+ describe('highestRole', () => {
29
+ it('returns highest role by depth', () => {
30
+ expect(highestRole(['ROLE_USER', 'ROLE_ADMIN'], hierarchy)).toBe('ROLE_ADMIN');
31
+ });
32
+ it('returns null for empty array', () => {
33
+ expect(highestRole([], hierarchy)).toBe(null);
34
+ });
35
+ it('returns super admin as highest', () => {
36
+ expect(highestRole(['ROLE_USER', 'ROLE_SUPER_ADMIN'], hierarchy)).toBe('ROLE_SUPER_ADMIN');
37
+ });
38
+ });
39
+ describe('isAdmin', () => {
40
+ it('returns true for admin', () => {
41
+ expect(isAdmin(['ROLE_ADMIN'], hierarchy)).toBe(true);
42
+ });
43
+ it('returns true for super admin', () => {
44
+ expect(isAdmin(['ROLE_SUPER_ADMIN'], hierarchy)).toBe(true);
45
+ });
46
+ it('returns false for user', () => {
47
+ expect(isAdmin(['ROLE_USER'], hierarchy)).toBe(false);
48
+ });
49
+ });
50
+ //# sourceMappingURL=roles.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.test.js","sourceRoot":"","sources":["../../src/roles/roles.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Role hierarchy definition.
3
+ * Higher roles inherit permissions from lower roles.
4
+ *
5
+ * ```ts
6
+ * const hierarchy: RoleHierarchy = {
7
+ * 'ROLE_SUPER_ADMIN': ['ROLE_ADMIN'],
8
+ * 'ROLE_ADMIN': ['ROLE_EDITOR'],
9
+ * 'ROLE_EDITOR': ['ROLE_USER'],
10
+ * 'ROLE_USER': [],
11
+ * }
12
+ * ```
13
+ */
14
+ export type RoleHierarchy = Record<string, string[]>;
15
+ /**
16
+ * Check if a user's roles satisfy a required role, considering hierarchy.
17
+ *
18
+ * ```ts
19
+ * hasRole(['ROLE_ADMIN'], 'ROLE_USER', hierarchy) // true (admin inherits user)
20
+ * hasRole(['ROLE_USER'], 'ROLE_ADMIN', hierarchy) // false
21
+ * ```
22
+ */
23
+ export declare function hasRole(userRoles: string[], requiredRole: string, hierarchy?: RoleHierarchy): boolean;
24
+ /**
25
+ * Get the highest priority role from a list.
26
+ * Priority is determined by hierarchy depth (deeper = higher).
27
+ */
28
+ export declare function highestRole(userRoles: string[], hierarchy: RoleHierarchy): string | null;
29
+ /**
30
+ * Shorthand: check if user has admin-level access.
31
+ */
32
+ export declare function isAdmin(userRoles: string[], hierarchy?: RoleHierarchy): boolean;
33
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAEpD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAE,aAAkB,GAAG,OAAO,CAQzG;AAiBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAexF;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,SAAS,GAAE,aAAkB,GAAG,OAAO,CAEnF"}
package/dist/roles.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Check if a user's roles satisfy a required role, considering hierarchy.
3
+ *
4
+ * ```ts
5
+ * hasRole(['ROLE_ADMIN'], 'ROLE_USER', hierarchy) // true (admin inherits user)
6
+ * hasRole(['ROLE_USER'], 'ROLE_ADMIN', hierarchy) // false
7
+ * ```
8
+ */
9
+ export function hasRole(userRoles, requiredRole, hierarchy = {}) {
10
+ if (userRoles.includes(requiredRole))
11
+ return true;
12
+ // Check hierarchy: does any user role inherit the required role?
13
+ for (const role of userRoles) {
14
+ if (resolveRoles(role, hierarchy).includes(requiredRole))
15
+ return true;
16
+ }
17
+ return false;
18
+ }
19
+ /**
20
+ * Resolve all inherited roles for a given role.
21
+ */
22
+ function resolveRoles(role, hierarchy, visited = new Set()) {
23
+ if (visited.has(role))
24
+ return [];
25
+ visited.add(role);
26
+ const children = hierarchy[role] ?? [];
27
+ const all = [role];
28
+ for (const child of children) {
29
+ all.push(...resolveRoles(child, hierarchy, visited));
30
+ }
31
+ return all;
32
+ }
33
+ /**
34
+ * Get the highest priority role from a list.
35
+ * Priority is determined by hierarchy depth (deeper = higher).
36
+ */
37
+ export function highestRole(userRoles, hierarchy) {
38
+ if (userRoles.length === 0)
39
+ return null;
40
+ let best = userRoles[0];
41
+ let bestDepth = 0;
42
+ for (const role of userRoles) {
43
+ const depth = resolveRoles(role, hierarchy).length;
44
+ if (depth > bestDepth) {
45
+ bestDepth = depth;
46
+ best = role;
47
+ }
48
+ }
49
+ return best;
50
+ }
51
+ /**
52
+ * Shorthand: check if user has admin-level access.
53
+ */
54
+ export function isAdmin(userRoles, hierarchy = {}) {
55
+ return hasRole(userRoles, 'ROLE_ADMIN', hierarchy);
56
+ }
57
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAeA;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,SAAmB,EAAE,YAAoB,EAAE,YAA2B,EAAE;IAC9F,IAAI,SAAS,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjD,iEAAiE;IACjE,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,IAAI,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,OAAO,IAAI,CAAA;IACvE,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,IAAY,EAAE,SAAwB,EAAE,UAAU,IAAI,GAAG,EAAU;IACvF,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IAChC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAEjB,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;IAClB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,GAAG,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAA;IACtD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAmB,EAAE,SAAwB;IACvE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEvC,IAAI,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;IACvB,IAAI,SAAS,GAAG,CAAC,CAAA;IAEjB,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,MAAM,CAAA;QAClD,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,SAAS,GAAG,KAAK,CAAA;YACjB,IAAI,GAAG,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,SAAmB,EAAE,YAA2B,EAAE;IACxE,OAAO,OAAO,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS,CAAC,CAAA;AACpD,CAAC"}
@@ -0,0 +1,26 @@
1
+ export interface TokenPair {
2
+ token: string;
3
+ refreshToken: string;
4
+ sessionId?: number;
5
+ }
6
+ export interface TokenManagerConfig {
7
+ /** API endpoint for token refresh (e.g. "/auth/refresh") */
8
+ refreshEndpoint: string;
9
+ /** Base API URL */
10
+ apiUrl: string;
11
+ /** Timeout in ms for refresh request (default: 10000) */
12
+ timeout?: number;
13
+ /** Called with new tokens after successful refresh */
14
+ onRefresh?: (tokens: TokenPair) => void;
15
+ /** Called when refresh fails */
16
+ onExpired?: () => void;
17
+ }
18
+ /**
19
+ * Server-side token refresh manager.
20
+ * Handles refresh token exchange and deduplication.
21
+ */
22
+ export declare function createTokenManager(config: TokenManagerConfig): {
23
+ /** Refresh tokens. Deduplicates concurrent calls. */
24
+ refresh(refreshToken: string, sessionId?: number): Promise<TokenPair | null>;
25
+ };
26
+ //# sourceMappingURL=token-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/token/token-manager.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,eAAe,EAAE,MAAM,CAAA;IACvB,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sDAAsD;IACtD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IACvC,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB;IA8CzD,qDAAqD;0BACzB,MAAM,cAAc,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;EAOrF"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Server-side token refresh manager.
3
+ * Handles refresh token exchange and deduplication.
4
+ */
5
+ export function createTokenManager(config) {
6
+ let pending = null;
7
+ async function doRefresh(refreshToken, sessionId) {
8
+ const controller = new AbortController();
9
+ const timer = setTimeout(() => controller.abort(), config.timeout ?? 10_000);
10
+ try {
11
+ const res = await fetch(`${config.apiUrl}${config.refreshEndpoint}`, {
12
+ method: 'POST',
13
+ headers: { 'Content-Type': 'application/json' },
14
+ body: JSON.stringify({
15
+ refresh_token: refreshToken,
16
+ session_id: sessionId,
17
+ }),
18
+ signal: controller.signal,
19
+ });
20
+ if (!res.ok) {
21
+ config.onExpired?.();
22
+ return null;
23
+ }
24
+ const data = await res.json();
25
+ if (!data.token || !data.refresh_token) {
26
+ config.onExpired?.();
27
+ return null;
28
+ }
29
+ const tokens = {
30
+ token: data.token,
31
+ refreshToken: data.refresh_token,
32
+ sessionId: data.session_id,
33
+ };
34
+ config.onRefresh?.(tokens);
35
+ return tokens;
36
+ }
37
+ catch {
38
+ config.onExpired?.();
39
+ return null;
40
+ }
41
+ finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+ return {
46
+ /** Refresh tokens. Deduplicates concurrent calls. */
47
+ async refresh(refreshToken, sessionId) {
48
+ if (!pending) {
49
+ pending = doRefresh(refreshToken, sessionId).finally(() => { pending = null; });
50
+ }
51
+ return pending;
52
+ },
53
+ };
54
+ }
55
+ //# sourceMappingURL=token-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/token/token-manager.ts"],"names":[],"mappings":"AAmBA;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA0B;IAC3D,IAAI,OAAO,GAAqC,IAAI,CAAA;IAEpD,KAAK,UAAU,SAAS,CAAC,YAAoB,EAAE,SAAkB;QAC/D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,CAAA;QAE5E,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,eAAe,EAAE,EAAE;gBACnE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,aAAa,EAAE,YAAY;oBAC3B,UAAU,EAAE,SAAS;iBACtB,CAAC;gBACF,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAA;YAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,CAAC,SAAS,EAAE,EAAE,CAAA;gBACpB,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAC7B,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvC,MAAM,CAAC,SAAS,EAAE,EAAE,CAAA;gBACpB,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,MAAM,GAAc;gBACxB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,YAAY,EAAE,IAAI,CAAC,aAAa;gBAChC,SAAS,EAAE,IAAI,CAAC,UAAU;aAC3B,CAAA;YAED,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;YAC1B,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,SAAS,EAAE,EAAE,CAAA;YACpB,OAAO,IAAI,CAAA;QACb,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IAED,OAAO;QACL,qDAAqD;QACrD,KAAK,CAAC,OAAO,CAAC,YAAoB,EAAE,SAAkB;YACpD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,GAAG,SAAS,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAAA;YAChF,CAAC;YACD,OAAO,OAAO,CAAA;QAChB,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,35 @@
1
+ export interface TokenPair {
2
+ token: string;
3
+ refreshToken: string;
4
+ sessionId?: number;
5
+ }
6
+ export interface TokenManagerConfig {
7
+ /** API endpoint for token refresh (e.g. "/auth/refresh") */
8
+ refreshEndpoint: string;
9
+ /** Base API URL */
10
+ apiUrl: string;
11
+ /** Called with new tokens after successful refresh */
12
+ onRefresh?: (tokens: TokenPair) => void;
13
+ /** Called when refresh fails */
14
+ onExpired?: () => void;
15
+ }
16
+ /**
17
+ * Server-side token refresh manager.
18
+ * Handles refresh token exchange and deduplication.
19
+ *
20
+ * ```ts
21
+ * const tokenManager = createTokenManager({
22
+ * refreshEndpoint: '/auth/refresh',
23
+ * apiUrl: 'http://localhost:3005/api/v1',
24
+ * onRefresh: (tokens) => setCookies(tokens),
25
+ * onExpired: () => clearSession(),
26
+ * })
27
+ *
28
+ * const newTokens = await tokenManager.refresh(refreshToken, sessionId)
29
+ * ```
30
+ */
31
+ export declare function createTokenManager(config: TokenManagerConfig): {
32
+ /** Refresh tokens. Deduplicates concurrent calls. */
33
+ refresh(refreshToken: string, sessionId?: number): Promise<TokenPair | null>;
34
+ };
35
+ //# sourceMappingURL=token-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../src/token-manager.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,eAAe,EAAE,MAAM,CAAA;IACvB,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IACvC,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB;IAmCzD,qDAAqD;0BACzB,MAAM,cAAc,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;EAOrF"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Server-side token refresh manager.
3
+ * Handles refresh token exchange and deduplication.
4
+ *
5
+ * ```ts
6
+ * const tokenManager = createTokenManager({
7
+ * refreshEndpoint: '/auth/refresh',
8
+ * apiUrl: 'http://localhost:3005/api/v1',
9
+ * onRefresh: (tokens) => setCookies(tokens),
10
+ * onExpired: () => clearSession(),
11
+ * })
12
+ *
13
+ * const newTokens = await tokenManager.refresh(refreshToken, sessionId)
14
+ * ```
15
+ */
16
+ export function createTokenManager(config) {
17
+ let pending = null;
18
+ async function doRefresh(refreshToken, sessionId) {
19
+ try {
20
+ const res = await fetch(`${config.apiUrl}${config.refreshEndpoint}`, {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({
24
+ refresh_token: refreshToken,
25
+ session_id: sessionId,
26
+ }),
27
+ });
28
+ if (!res.ok) {
29
+ config.onExpired?.();
30
+ return null;
31
+ }
32
+ const data = await res.json();
33
+ const tokens = {
34
+ token: data.token,
35
+ refreshToken: data.refresh_token,
36
+ sessionId: data.session_id,
37
+ };
38
+ config.onRefresh?.(tokens);
39
+ return tokens;
40
+ }
41
+ catch {
42
+ config.onExpired?.();
43
+ return null;
44
+ }
45
+ }
46
+ return {
47
+ /** Refresh tokens. Deduplicates concurrent calls. */
48
+ async refresh(refreshToken, sessionId) {
49
+ if (!pending) {
50
+ pending = doRefresh(refreshToken, sessionId).finally(() => { pending = null; });
51
+ }
52
+ return pending;
53
+ },
54
+ };
55
+ }
56
+ //# sourceMappingURL=token-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../src/token-manager.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA0B;IAC3D,IAAI,OAAO,GAAqC,IAAI,CAAA;IAEpD,KAAK,UAAU,SAAS,CAAC,YAAoB,EAAE,SAAkB;QAC/D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,eAAe,EAAE,EAAE;gBACnE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,aAAa,EAAE,YAAY;oBAC3B,UAAU,EAAE,SAAS;iBACtB,CAAC;aACH,CAAC,CAAA;YAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,CAAC,SAAS,EAAE,EAAE,CAAA;gBACpB,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAC7B,MAAM,MAAM,GAAc;gBACxB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,YAAY,EAAE,IAAI,CAAC,aAAa;gBAChC,SAAS,EAAE,IAAI,CAAC,UAAU;aAC3B,CAAA;YAED,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;YAC1B,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,SAAS,EAAE,EAAE,CAAA;YACpB,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,OAAO;QACL,qDAAqD;QACrD,KAAK,CAAC,OAAO,CAAC,YAAoB,EAAE,SAAkB;YACpD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,GAAG,SAAS,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAAA;YAChF,CAAC;YACD,OAAO,OAAO,CAAA;QAChB,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { AuthUser } from '@karbonjs/types';
2
+ export interface UserCacheOptions {
3
+ /** Max entries in cache (default: 500) */
4
+ maxSize?: number;
5
+ /** TTL in milliseconds (default: 2 minutes) */
6
+ ttl?: number;
7
+ }
8
+ /**
9
+ * In-memory user cache for SSR hooks.
10
+ * Prevents hammering the /profile endpoint on every request.
11
+ *
12
+ * ```ts
13
+ * const cache = createUserCache({ ttl: 120_000, maxSize: 500 })
14
+ *
15
+ * // In your SSR hook:
16
+ * let user = cache.get(token)
17
+ * if (!user) {
18
+ * user = await fetchProfile(token)
19
+ * if (user) cache.set(token, user)
20
+ * }
21
+ * ```
22
+ */
23
+ export declare function createUserCache(opts?: UserCacheOptions): {
24
+ get(token: string): AuthUser | null;
25
+ set(token: string, user: AuthUser): void;
26
+ invalidate(token: string): void;
27
+ clear(): void;
28
+ readonly size: number;
29
+ };
30
+ //# sourceMappingURL=user-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-cache.d.ts","sourceRoot":"","sources":["../src/user-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAOD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,gBAAqB;eAM5C,MAAM,GAAG,QAAQ,GAAG,IAAI;eAUxB,MAAM,QAAQ,QAAQ,GAAG,IAAI;sBAStB,MAAM,GAAG,IAAI;aAItB,IAAI;mBAID,MAAM;EAIrB"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * In-memory user cache for SSR hooks.
3
+ * Prevents hammering the /profile endpoint on every request.
4
+ *
5
+ * ```ts
6
+ * const cache = createUserCache({ ttl: 120_000, maxSize: 500 })
7
+ *
8
+ * // In your SSR hook:
9
+ * let user = cache.get(token)
10
+ * if (!user) {
11
+ * user = await fetchProfile(token)
12
+ * if (user) cache.set(token, user)
13
+ * }
14
+ * ```
15
+ */
16
+ export function createUserCache(opts = {}) {
17
+ const maxSize = opts.maxSize ?? 500;
18
+ const ttl = opts.ttl ?? 120_000;
19
+ const store = new Map();
20
+ return {
21
+ get(token) {
22
+ const entry = store.get(token);
23
+ if (!entry)
24
+ return null;
25
+ if (Date.now() - entry.timestamp > ttl) {
26
+ store.delete(token);
27
+ return null;
28
+ }
29
+ return entry.user;
30
+ },
31
+ set(token, user) {
32
+ // Evict oldest if full
33
+ if (store.size >= maxSize) {
34
+ const oldest = store.keys().next().value;
35
+ if (oldest)
36
+ store.delete(oldest);
37
+ }
38
+ store.set(token, { user, timestamp: Date.now() });
39
+ },
40
+ invalidate(token) {
41
+ store.delete(token);
42
+ },
43
+ clear() {
44
+ store.clear();
45
+ },
46
+ get size() {
47
+ return store.size;
48
+ },
49
+ };
50
+ }
51
+ //# sourceMappingURL=user-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-cache.js","sourceRoot":"","sources":["../src/user-cache.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,OAAyB,EAAE;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,GAAG,CAAA;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAA;IAC/B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAA;IAE3C,OAAO;QACL,GAAG,CAAC,KAAa;YACf,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YAC9B,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAA;YACvB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;gBACvC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBACnB,OAAO,IAAI,CAAA;YACb,CAAC;YACD,OAAO,KAAK,CAAC,IAAI,CAAA;QACnB,CAAC;QAED,GAAG,CAAC,KAAa,EAAE,IAAc;YAC/B,uBAAuB;YACvB,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAA;gBACxC,IAAI,MAAM;oBAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAClC,CAAC;YACD,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,UAAU,CAAC,KAAa;YACtB,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;QAED,KAAK;YACH,KAAK,CAAC,KAAK,EAAE,CAAA;QACf,CAAC;QAED,IAAI,IAAI;YACN,OAAO,KAAK,CAAC,IAAI,CAAA;QACnB,CAAC;KACF,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@karbonjs/auth",
3
+ "version": "0.1.0",
4
+ "description": "Authentication helpers for Karbon — token management, user cache, hooks",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "@karbonjs/types": "0.1.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "license": "MIT",
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "dev": "tsc --watch"
27
+ }
28
+ }