@fatagnus/dink-ui-core 2.32.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 (74) hide show
  1. package/README.md +93 -0
  2. package/dist/__stubs__/dink-web-auth-react.d.ts +5 -0
  3. package/dist/__stubs__/dink-web-auth-react.js +9 -0
  4. package/dist/__stubs__/dink-web-authz-react.d.ts +3 -0
  5. package/dist/__stubs__/dink-web-authz-react.js +7 -0
  6. package/dist/__stubs__/dink-web-react.d.ts +6 -0
  7. package/dist/__stubs__/dink-web-react.js +10 -0
  8. package/dist/auth/index.d.ts +12 -0
  9. package/dist/auth/index.js +7 -0
  10. package/dist/auth/permission-check.d.ts +31 -0
  11. package/dist/auth/permission-check.js +79 -0
  12. package/dist/auth/token-refresh.d.ts +20 -0
  13. package/dist/auth/token-refresh.js +62 -0
  14. package/dist/auth/use-auth.d.ts +21 -0
  15. package/dist/auth/use-auth.js +20 -0
  16. package/dist/auth/use-can.d.ts +11 -0
  17. package/dist/auth/use-can.js +25 -0
  18. package/dist/auth/use-capability-token.d.ts +38 -0
  19. package/dist/auth/use-capability-token.js +134 -0
  20. package/dist/auth/use-permission-check.d.ts +19 -0
  21. package/dist/auth/use-permission-check.js +57 -0
  22. package/dist/auth/with-permission.d.ts +18 -0
  23. package/dist/auth/with-permission.js +23 -0
  24. package/dist/connection/ConnectionBanner.d.ts +7 -0
  25. package/dist/connection/ConnectionBanner.js +18 -0
  26. package/dist/connection/index.d.ts +3 -0
  27. package/dist/connection/index.js +2 -0
  28. package/dist/connection/use-connection-state.d.ts +13 -0
  29. package/dist/connection/use-connection-state.js +28 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +11 -0
  32. package/dist/surface/AdaptivePage.d.ts +9 -0
  33. package/dist/surface/AdaptivePage.js +17 -0
  34. package/dist/surface/SurfaceProvider.d.ts +9 -0
  35. package/dist/surface/SurfaceProvider.js +38 -0
  36. package/dist/surface/adaptive.d.ts +20 -0
  37. package/dist/surface/adaptive.js +26 -0
  38. package/dist/surface/index.d.ts +7 -0
  39. package/dist/surface/index.js +5 -0
  40. package/dist/surface/types.d.ts +13 -0
  41. package/dist/surface/types.js +6 -0
  42. package/dist/surface/use-surface.d.ts +6 -0
  43. package/dist/surface/use-surface.js +13 -0
  44. package/dist/theme/colors.d.ts +22 -0
  45. package/dist/theme/colors.js +44 -0
  46. package/dist/theme/dink-theme.d.ts +8 -0
  47. package/dist/theme/dink-theme.js +56 -0
  48. package/dist/theme/index.d.ts +3 -0
  49. package/dist/theme/index.js +2 -0
  50. package/dist/workspace/PluginSlot.d.ts +8 -0
  51. package/dist/workspace/PluginSlot.js +9 -0
  52. package/dist/workspace/Room.d.ts +9 -0
  53. package/dist/workspace/Room.js +11 -0
  54. package/dist/workspace/WorkspaceProvider.d.ts +13 -0
  55. package/dist/workspace/WorkspaceProvider.js +11 -0
  56. package/dist/workspace/WorkspaceShell.d.ts +11 -0
  57. package/dist/workspace/WorkspaceShell.js +19 -0
  58. package/dist/workspace/index.d.ts +14 -0
  59. package/dist/workspace/index.js +10 -0
  60. package/dist/workspace/resource-types.d.ts +65 -0
  61. package/dist/workspace/resource-types.js +56 -0
  62. package/dist/workspace/store.d.ts +23 -0
  63. package/dist/workspace/store.js +55 -0
  64. package/dist/workspace/types.d.ts +54 -0
  65. package/dist/workspace/types.js +1 -0
  66. package/dist/workspace/use-room.d.ts +6 -0
  67. package/dist/workspace/use-room.js +9 -0
  68. package/dist/workspace/use-workspace-call.d.ts +17 -0
  69. package/dist/workspace/use-workspace-call.js +29 -0
  70. package/dist/workspace/use-workspace.d.ts +18 -0
  71. package/dist/workspace/use-workspace.js +23 -0
  72. package/dist/workspace/workspace-client.d.ts +16 -0
  73. package/dist/workspace/workspace-client.js +109 -0
  74. package/package.json +81 -0
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @fatagnus/dink-ui-core
2
+
3
+ Adaptive shell, workspace/room manager, and Mantine theme for Dink apps. Provides the layout infrastructure that sits between the semantic layer and your application components.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @fatagnus/dink-ui-core
9
+ ```
10
+
11
+ Peer dependencies: `@fatagnus/dink-semantic`, `@fatagnus/dink-web`, `react`, `react-dom`.
12
+
13
+ ## Quick Start
14
+
15
+ ```tsx
16
+ import { MantineProvider } from '@mantine/core';
17
+ import { SemRoot } from '@fatagnus/dink-semantic';
18
+ import { createDinkTheme, SurfaceProvider, WorkspaceProvider, WorkspaceShell, Room } from '@fatagnus/dink-ui-core';
19
+
20
+ const theme = createDinkTheme();
21
+
22
+ function App() {
23
+ return (
24
+ <MantineProvider theme={theme} defaultColorScheme="dark">
25
+ <SemRoot app="my-app" version="1.0.0">
26
+ <SurfaceProvider>
27
+ <WorkspaceProvider>
28
+ <WorkspaceShell>
29
+ <Room id="alerts" label="Alerts">
30
+ <AlertsPage />
31
+ </Room>
32
+ </WorkspaceShell>
33
+ </WorkspaceProvider>
34
+ </SurfaceProvider>
35
+ </SemRoot>
36
+ </MantineProvider>
37
+ );
38
+ }
39
+ ```
40
+
41
+ ## Key Exports
42
+
43
+ ### Theme (`./theme`)
44
+
45
+ | Export | Description |
46
+ |---|---|
47
+ | `createDinkTheme()` | Returns a Mantine v7 theme with Dink color palette and defaults |
48
+ | `dinkColors` / `dinkSurfaceColors` | Raw color tokens |
49
+
50
+ ### Adaptive Surface
51
+
52
+ | Export | Description |
53
+ |---|---|
54
+ | `SurfaceProvider` | Detects current surface (desktop/tablet/mobile) and provides context |
55
+ | `useSurface()` | Access current `SurfaceInfo` |
56
+ | `defineAdaptivePage(config)` | Declare a page component with per-surface layouts |
57
+ | `AdaptivePage` | Renders the correct layout for the current surface |
58
+
59
+ ### Workspace & Rooms
60
+
61
+ | Export | Description |
62
+ |---|---|
63
+ | `WorkspaceProvider` | MobX-backed workspace state (rooms, layout, navigation) |
64
+ | `WorkspaceShell` | AppShell wrapper with sidebar, header, room tabs |
65
+ | `Room` | Declares a navigable room within the workspace |
66
+ | `PluginSlot` | Mounting point for third-party plugin content |
67
+ | `useWorkspace()` / `useRoom()` | Access workspace and room state |
68
+ | `WorkspaceStore` | MobX store class for direct access |
69
+
70
+ ### Auth (`./auth`)
71
+
72
+ | Export | Description |
73
+ |---|---|
74
+ | `useAuth()` | Current user identity and session from Dink backend |
75
+ | `useCan(permission)` | Permission check hook |
76
+
77
+ ### Connection (`./connection`)
78
+
79
+ | Export | Description |
80
+ |---|---|
81
+ | `useConnectionState()` | NATS/WebSocket connection status |
82
+ | `ConnectionBanner` | Auto-show banner on disconnect/reconnect |
83
+
84
+ ## Subpath Exports
85
+
86
+ - `.` -- Everything
87
+ - `./theme` -- Theme only
88
+ - `./auth` -- Auth hooks only
89
+ - `./connection` -- Connection hooks and banner
90
+
91
+ ## Design Spec
92
+
93
+ [docs/superpowers/specs/2026-03-16-dink-frontend-framework-design.md](../../docs/superpowers/specs/2026-03-16-dink-frontend-framework-design.md)
@@ -0,0 +1,5 @@
1
+ export declare function useDinkAuth(): {
2
+ user: null;
3
+ isAuthenticated: boolean;
4
+ isLoading: boolean;
5
+ };
@@ -0,0 +1,9 @@
1
+ // Stub for @fatagnus/dink-web/dink-auth-react
2
+ // Real implementation is provided by dink-web; tests mock this module.
3
+ export function useDinkAuth() {
4
+ return {
5
+ user: null,
6
+ isAuthenticated: false,
7
+ isLoading: true,
8
+ };
9
+ }
@@ -0,0 +1,3 @@
1
+ export declare function useDinkAuthZ(): {
2
+ can: () => boolean;
3
+ };
@@ -0,0 +1,7 @@
1
+ // Stub for @fatagnus/dink-web/dink-authz-react
2
+ // Real implementation is provided by dink-web; tests mock this module.
3
+ export function useDinkAuthZ() {
4
+ return {
5
+ can: () => true,
6
+ };
7
+ }
@@ -0,0 +1,6 @@
1
+ export declare function useDinkWeb(): {
2
+ client: null;
3
+ status: "connected";
4
+ stats: {};
5
+ error: null;
6
+ };
@@ -0,0 +1,10 @@
1
+ // Stub for @fatagnus/dink-web/react
2
+ // Real implementation is provided by dink-web; tests mock this module.
3
+ export function useDinkWeb() {
4
+ return {
5
+ client: null,
6
+ status: 'connected',
7
+ stats: {},
8
+ error: null,
9
+ };
10
+ }
@@ -0,0 +1,12 @@
1
+ export { useAuth } from './use-auth.js';
2
+ export type { AuthInfo, AuthUser } from './use-auth.js';
3
+ export { useCan } from './use-can.js';
4
+ export type { CanResult } from './use-can.js';
5
+ export { useCapabilityToken } from './use-capability-token.js';
6
+ export type { UseCapabilityTokenOptions, UseCapabilityTokenResult, CapabilityTokenClaims } from './use-capability-token.js';
7
+ export { usePermissionCheck } from './use-permission-check.js';
8
+ export type { UsePermissionCheckOptions, UsePermissionCheckResult } from './use-permission-check.js';
9
+ export { withPermission } from './with-permission.js';
10
+ export { checkPermission } from './permission-check.js';
11
+ export type { PermissionClaims, PermissionResult } from './permission-check.js';
12
+ export { TokenRefreshManager } from './token-refresh.js';
@@ -0,0 +1,7 @@
1
+ export { useAuth } from './use-auth.js';
2
+ export { useCan } from './use-can.js';
3
+ export { useCapabilityToken } from './use-capability-token.js';
4
+ export { usePermissionCheck } from './use-permission-check.js';
5
+ export { withPermission } from './with-permission.js';
6
+ export { checkPermission } from './permission-check.js';
7
+ export { TokenRefreshManager } from './token-refresh.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pure permission check function.
3
+ * Evaluates whether a JWT's claims grant a specific scope on a resource.
4
+ */
5
+ export interface PermissionClaims {
6
+ sub: string;
7
+ iss: string;
8
+ aud: string;
9
+ exp: number;
10
+ iat: number;
11
+ scopes?: string[];
12
+ resources?: string[];
13
+ }
14
+ export interface PermissionResult {
15
+ allowed: boolean;
16
+ reason?: string;
17
+ }
18
+ /**
19
+ * Check whether claims grant the requested scope on the given resource.
20
+ *
21
+ * Scope matching:
22
+ * - Wildcard `*` in scopes matches any scope
23
+ * - `scope:kind` pattern (e.g. `read:tasks`) matches if scope matches
24
+ * the requested scope and kind matches the resource scheme
25
+ *
26
+ * Resource matching:
27
+ * - Wildcard `*` in resources matches any resource
28
+ * - `scheme://*` pattern matches any resource with that scheme
29
+ * - Exact match on full URI
30
+ */
31
+ export declare function checkPermission(claims: PermissionClaims, scope: string, resource: string): PermissionResult;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Pure permission check function.
3
+ * Evaluates whether a JWT's claims grant a specific scope on a resource.
4
+ */
5
+ /**
6
+ * Check whether claims grant the requested scope on the given resource.
7
+ *
8
+ * Scope matching:
9
+ * - Wildcard `*` in scopes matches any scope
10
+ * - `scope:kind` pattern (e.g. `read:tasks`) matches if scope matches
11
+ * the requested scope and kind matches the resource scheme
12
+ *
13
+ * Resource matching:
14
+ * - Wildcard `*` in resources matches any resource
15
+ * - `scheme://*` pattern matches any resource with that scheme
16
+ * - Exact match on full URI
17
+ */
18
+ export function checkPermission(claims, scope, resource) {
19
+ // Check token expiration
20
+ const now = Math.floor(Date.now() / 1000);
21
+ if (claims.exp <= now) {
22
+ return { allowed: false, reason: 'Token expired' };
23
+ }
24
+ const claimScopes = claims.scopes ?? [];
25
+ const claimResources = claims.resources ?? [];
26
+ // Check scope match
27
+ const scopeMatched = matchScope(claimScopes, scope, resource);
28
+ if (!scopeMatched) {
29
+ return { allowed: false, reason: `Scope '${scope}' not granted` };
30
+ }
31
+ // Check resource match
32
+ const resourceMatched = matchResource(claimResources, resource);
33
+ if (!resourceMatched) {
34
+ return { allowed: false, reason: `Resource '${resource}' not granted` };
35
+ }
36
+ return { allowed: true };
37
+ }
38
+ function matchScope(claimScopes, requestedScope, resource) {
39
+ for (const s of claimScopes) {
40
+ if (s === '*')
41
+ return true;
42
+ // Pattern: `scope:kind` e.g. `read:tasks`
43
+ const colonIdx = s.indexOf(':');
44
+ if (colonIdx !== -1) {
45
+ const scopePart = s.substring(0, colonIdx);
46
+ const kindPart = s.substring(colonIdx + 1);
47
+ // scope matches if the scope part matches the requested scope
48
+ // and the kind matches the resource scheme prefix
49
+ if (scopePart === requestedScope || scopePart === '*') {
50
+ const resourceScheme = resource.split('://')[0];
51
+ if (kindPart === '*' || kindPart === resourceScheme || resource.startsWith(kindPart)) {
52
+ return true;
53
+ }
54
+ }
55
+ }
56
+ else {
57
+ // Exact scope match
58
+ if (s === requestedScope)
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+ function matchResource(claimResources, requestedResource) {
65
+ for (const r of claimResources) {
66
+ if (r === '*')
67
+ return true;
68
+ // Pattern: `scheme://*` — wildcard for all resources under that scheme
69
+ if (r.endsWith('://*')) {
70
+ const scheme = r.slice(0, -4); // remove `://*`
71
+ if (requestedResource.startsWith(scheme + '://'))
72
+ return true;
73
+ }
74
+ // Exact match
75
+ if (r === requestedResource)
76
+ return true;
77
+ }
78
+ return false;
79
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * TokenRefreshManager — schedules token refresh callbacks before expiry.
3
+ *
4
+ * Decodes the JWT exp claim and fires a callback at 80% of TTL.
5
+ */
6
+ export declare class TokenRefreshManager {
7
+ private timers;
8
+ /**
9
+ * Monitor a JWT token and call `onRefresh` at 80% of its TTL.
10
+ * Returns an unsubscribe function that cancels the scheduled callback.
11
+ */
12
+ monitor(token: string, onRefresh: () => void): () => void;
13
+ /** Clear all scheduled refresh timers. */
14
+ destroy(): void;
15
+ }
16
+ /**
17
+ * Decode JWT claims from a token string (base64url decode of second segment).
18
+ * Returns null if the token is malformed.
19
+ */
20
+ export declare function decodeJwtClaims(token: string): Record<string, unknown> | null;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * TokenRefreshManager — schedules token refresh callbacks before expiry.
3
+ *
4
+ * Decodes the JWT exp claim and fires a callback at 80% of TTL.
5
+ */
6
+ export class TokenRefreshManager {
7
+ timers = new Set();
8
+ /**
9
+ * Monitor a JWT token and call `onRefresh` at 80% of its TTL.
10
+ * Returns an unsubscribe function that cancels the scheduled callback.
11
+ */
12
+ monitor(token, onRefresh) {
13
+ const claims = decodeJwtClaims(token);
14
+ if (!claims || typeof claims.exp !== 'number') {
15
+ return () => { };
16
+ }
17
+ const now = Math.floor(Date.now() / 1000);
18
+ const ttl = claims.exp - now;
19
+ if (ttl <= 0) {
20
+ // Already expired, fire immediately
21
+ onRefresh();
22
+ return () => { };
23
+ }
24
+ // Schedule refresh at 80% of TTL
25
+ const refreshAt = Math.floor(ttl * 0.8) * 1000;
26
+ const timer = setTimeout(() => {
27
+ this.timers.delete(timer);
28
+ onRefresh();
29
+ }, refreshAt);
30
+ this.timers.add(timer);
31
+ return () => {
32
+ clearTimeout(timer);
33
+ this.timers.delete(timer);
34
+ };
35
+ }
36
+ /** Clear all scheduled refresh timers. */
37
+ destroy() {
38
+ for (const timer of this.timers) {
39
+ clearTimeout(timer);
40
+ }
41
+ this.timers.clear();
42
+ }
43
+ }
44
+ /**
45
+ * Decode JWT claims from a token string (base64url decode of second segment).
46
+ * Returns null if the token is malformed.
47
+ */
48
+ export function decodeJwtClaims(token) {
49
+ try {
50
+ const parts = token.split('.');
51
+ if (parts.length < 2)
52
+ return null;
53
+ const payload = parts[1];
54
+ // Handle base64url encoding
55
+ const padded = payload.replace(/-/g, '+').replace(/_/g, '/');
56
+ const decoded = atob(padded);
57
+ return JSON.parse(decoded);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Unified auth hook wrapping dink-web's auth context.
3
+ *
4
+ * This is a thin wrapper that re-surfaces the existing auth
5
+ * in a convenient shape for shell components.
6
+ */
7
+ export interface AuthUser {
8
+ id: string;
9
+ email: string;
10
+ name?: string;
11
+ role?: string;
12
+ }
13
+ export interface AuthInfo {
14
+ user: AuthUser | null;
15
+ isAuthenticated: boolean;
16
+ isLoading: boolean;
17
+ }
18
+ /**
19
+ * Returns the current auth state from the underlying DinkAuth context.
20
+ */
21
+ export declare function useAuth(): AuthInfo;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Unified auth hook wrapping dink-web's auth context.
3
+ *
4
+ * This is a thin wrapper that re-surfaces the existing auth
5
+ * in a convenient shape for shell components.
6
+ */
7
+ // We use dynamic import resolution -- the mock in tests will replace this module.
8
+ // In production, @fatagnus/dink-web/dink-auth-react provides useDinkAuth.
9
+ import { useDinkAuth } from '@fatagnus/dink-web/dink-auth-react';
10
+ /**
11
+ * Returns the current auth state from the underlying DinkAuth context.
12
+ */
13
+ export function useAuth() {
14
+ const auth = useDinkAuth();
15
+ return {
16
+ user: auth.user,
17
+ isAuthenticated: auth.isAuthenticated,
18
+ isLoading: auth.isLoading,
19
+ };
20
+ }
@@ -0,0 +1,11 @@
1
+ export interface CanResult {
2
+ allowed: boolean;
3
+ }
4
+ /**
5
+ * Evaluates a permission check and returns whether the action is allowed.
6
+ *
7
+ * @example
8
+ * const { allowed } = useCan('write', 'tasks');
9
+ * <Button disabled={!allowed}>Create Task</Button>
10
+ */
11
+ export declare function useCan(scope: string, resource: string): CanResult;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Permission-gating hook for Dink UI components.
3
+ *
4
+ * Wraps the underlying authz context. Uses the authz client's
5
+ * simulate() method to check permissions.
6
+ */
7
+ import { useDinkAuthZ } from '@fatagnus/dink-web/dink-authz-react';
8
+ /**
9
+ * Evaluates a permission check and returns whether the action is allowed.
10
+ *
11
+ * @example
12
+ * const { allowed } = useCan('write', 'tasks');
13
+ * <Button disabled={!allowed}>Create Task</Button>
14
+ */
15
+ export function useCan(scope, resource) {
16
+ const authz = useDinkAuthZ();
17
+ // The authz context provides a `can` method when available,
18
+ // falling back to always-allowed when no authz is configured.
19
+ const can = authz.can;
20
+ if (typeof can === 'function') {
21
+ return { allowed: can(scope, resource) };
22
+ }
23
+ // Default: allowed (no authz configured)
24
+ return { allowed: true };
25
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * useCapabilityToken() — React hook for token lifecycle management.
3
+ *
4
+ * Acquires a capability token, decodes JWT claims, tracks expiry,
5
+ * auto-refreshes via TokenRefreshManager, and provides manual refresh/revoke.
6
+ */
7
+ export interface CapabilityTokenClaims {
8
+ sub: string;
9
+ iss: string;
10
+ aud: string;
11
+ exp: number;
12
+ iat: number;
13
+ scopes?: string[];
14
+ resources?: string[];
15
+ }
16
+ export interface UseCapabilityTokenOptions {
17
+ scopes: string[];
18
+ resources: string[];
19
+ autoRefresh?: boolean;
20
+ ttlSeconds?: number;
21
+ onRefresh?: (token: string) => void;
22
+ onRefreshError?: (error: Error) => void;
23
+ }
24
+ export interface UseCapabilityTokenResult {
25
+ token: string | null;
26
+ state: 'loading' | 'valid' | 'expired' | 'refreshing' | 'error';
27
+ expiresIn: number | null;
28
+ error: Error | null;
29
+ refresh: () => Promise<void>;
30
+ revoke: () => Promise<void>;
31
+ claims: CapabilityTokenClaims | null;
32
+ }
33
+ type GetTokenFn = (options: {
34
+ scopes: string[];
35
+ resources: string[];
36
+ }) => Promise<string>;
37
+ export declare function useCapabilityToken(options: UseCapabilityTokenOptions, getToken: GetTokenFn): UseCapabilityTokenResult;
38
+ export {};
@@ -0,0 +1,134 @@
1
+ /**
2
+ * useCapabilityToken() — React hook for token lifecycle management.
3
+ *
4
+ * Acquires a capability token, decodes JWT claims, tracks expiry,
5
+ * auto-refreshes via TokenRefreshManager, and provides manual refresh/revoke.
6
+ */
7
+ import { useState, useEffect, useCallback, useRef } from 'react';
8
+ import { TokenRefreshManager, decodeJwtClaims } from './token-refresh.js';
9
+ export function useCapabilityToken(options, getToken) {
10
+ const [token, setToken] = useState(null);
11
+ const [state, setState] = useState('loading');
12
+ const [error, setError] = useState(null);
13
+ const [claims, setClaims] = useState(null);
14
+ const [expiresIn, setExpiresIn] = useState(null);
15
+ const managerRef = useRef(null);
16
+ const intervalRef = useRef(null);
17
+ const unsubRef = useRef(null);
18
+ // Stable refs for callback parameters to avoid dependency loops
19
+ const getTokenRef = useRef(getToken);
20
+ getTokenRef.current = getToken;
21
+ const optionsRef = useRef(options);
22
+ optionsRef.current = options;
23
+ const autoRefresh = options.autoRefresh ?? true;
24
+ const processToken = useCallback((raw) => {
25
+ const decoded = decodeJwtClaims(raw);
26
+ if (!decoded) {
27
+ setError(new Error('Failed to decode token'));
28
+ setState('error');
29
+ return;
30
+ }
31
+ const tokenClaims = {
32
+ sub: decoded.sub,
33
+ iss: decoded.iss,
34
+ aud: decoded.aud,
35
+ exp: decoded.exp,
36
+ iat: decoded.iat,
37
+ scopes: decoded.scopes,
38
+ resources: decoded.resources,
39
+ };
40
+ setToken(raw);
41
+ setClaims(tokenClaims);
42
+ setState('valid');
43
+ setError(null);
44
+ // Update expiresIn immediately
45
+ const now = Math.floor(Date.now() / 1000);
46
+ setExpiresIn(Math.max(0, tokenClaims.exp - now));
47
+ }, []);
48
+ const acquire = useCallback(async () => {
49
+ const opts = optionsRef.current;
50
+ const getFn = getTokenRef.current;
51
+ try {
52
+ const raw = await getFn({ scopes: opts.scopes, resources: opts.resources });
53
+ processToken(raw);
54
+ return raw;
55
+ }
56
+ catch (err) {
57
+ const e = err instanceof Error ? err : new Error(String(err));
58
+ setError(e);
59
+ setState('error');
60
+ opts.onRefreshError?.(e);
61
+ return null;
62
+ }
63
+ }, [processToken]);
64
+ // Initial acquisition — runs once
65
+ useEffect(() => {
66
+ setState('loading');
67
+ acquire();
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ }, []);
70
+ // Expiry countdown
71
+ useEffect(() => {
72
+ if (claims?.exp == null)
73
+ return;
74
+ intervalRef.current = setInterval(() => {
75
+ const now = Math.floor(Date.now() / 1000);
76
+ const remaining = Math.max(0, claims.exp - now);
77
+ setExpiresIn(remaining);
78
+ if (remaining === 0) {
79
+ setState('expired');
80
+ }
81
+ }, 1000);
82
+ return () => {
83
+ if (intervalRef.current)
84
+ clearInterval(intervalRef.current);
85
+ };
86
+ }, [claims?.exp]);
87
+ // Auto-refresh via TokenRefreshManager
88
+ useEffect(() => {
89
+ if (!autoRefresh || !token)
90
+ return;
91
+ if (!managerRef.current) {
92
+ managerRef.current = new TokenRefreshManager();
93
+ }
94
+ unsubRef.current = managerRef.current.monitor(token, async () => {
95
+ setState('refreshing');
96
+ const newToken = await acquire();
97
+ if (newToken) {
98
+ optionsRef.current.onRefresh?.(newToken);
99
+ }
100
+ });
101
+ return () => {
102
+ unsubRef.current?.();
103
+ };
104
+ }, [autoRefresh, token, acquire]);
105
+ // Cleanup
106
+ useEffect(() => {
107
+ return () => {
108
+ managerRef.current?.destroy();
109
+ };
110
+ }, []);
111
+ const refresh = useCallback(async () => {
112
+ setState('refreshing');
113
+ const newToken = await acquire();
114
+ if (newToken) {
115
+ optionsRef.current.onRefresh?.(newToken);
116
+ }
117
+ }, [acquire]);
118
+ const revoke = useCallback(async () => {
119
+ setToken(null);
120
+ setClaims(null);
121
+ setExpiresIn(null);
122
+ setState('loading');
123
+ unsubRef.current?.();
124
+ }, []);
125
+ return {
126
+ token,
127
+ state,
128
+ expiresIn,
129
+ error,
130
+ refresh,
131
+ revoke,
132
+ claims,
133
+ };
134
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * usePermissionCheck() — React hook for evaluating permissions against JWT claims.
3
+ *
4
+ * Decodes JWT claims from a token string, calls checkPermission(),
5
+ * and provides cached permission results.
6
+ */
7
+ export interface UsePermissionCheckOptions {
8
+ token: string | null;
9
+ scope: string;
10
+ resource: string;
11
+ cacheTtl?: number;
12
+ }
13
+ export interface UsePermissionCheckResult {
14
+ allowed: boolean;
15
+ isLoading: boolean;
16
+ reason?: string;
17
+ recheck: () => void;
18
+ }
19
+ export declare function usePermissionCheck(options: UsePermissionCheckOptions): UsePermissionCheckResult;