@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.
- package/README.md +93 -0
- package/dist/__stubs__/dink-web-auth-react.d.ts +5 -0
- package/dist/__stubs__/dink-web-auth-react.js +9 -0
- package/dist/__stubs__/dink-web-authz-react.d.ts +3 -0
- package/dist/__stubs__/dink-web-authz-react.js +7 -0
- package/dist/__stubs__/dink-web-react.d.ts +6 -0
- package/dist/__stubs__/dink-web-react.js +10 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/permission-check.d.ts +31 -0
- package/dist/auth/permission-check.js +79 -0
- package/dist/auth/token-refresh.d.ts +20 -0
- package/dist/auth/token-refresh.js +62 -0
- package/dist/auth/use-auth.d.ts +21 -0
- package/dist/auth/use-auth.js +20 -0
- package/dist/auth/use-can.d.ts +11 -0
- package/dist/auth/use-can.js +25 -0
- package/dist/auth/use-capability-token.d.ts +38 -0
- package/dist/auth/use-capability-token.js +134 -0
- package/dist/auth/use-permission-check.d.ts +19 -0
- package/dist/auth/use-permission-check.js +57 -0
- package/dist/auth/with-permission.d.ts +18 -0
- package/dist/auth/with-permission.js +23 -0
- package/dist/connection/ConnectionBanner.d.ts +7 -0
- package/dist/connection/ConnectionBanner.js +18 -0
- package/dist/connection/index.d.ts +3 -0
- package/dist/connection/index.js +2 -0
- package/dist/connection/use-connection-state.d.ts +13 -0
- package/dist/connection/use-connection-state.js +28 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/surface/AdaptivePage.d.ts +9 -0
- package/dist/surface/AdaptivePage.js +17 -0
- package/dist/surface/SurfaceProvider.d.ts +9 -0
- package/dist/surface/SurfaceProvider.js +38 -0
- package/dist/surface/adaptive.d.ts +20 -0
- package/dist/surface/adaptive.js +26 -0
- package/dist/surface/index.d.ts +7 -0
- package/dist/surface/index.js +5 -0
- package/dist/surface/types.d.ts +13 -0
- package/dist/surface/types.js +6 -0
- package/dist/surface/use-surface.d.ts +6 -0
- package/dist/surface/use-surface.js +13 -0
- package/dist/theme/colors.d.ts +22 -0
- package/dist/theme/colors.js +44 -0
- package/dist/theme/dink-theme.d.ts +8 -0
- package/dist/theme/dink-theme.js +56 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.js +2 -0
- package/dist/workspace/PluginSlot.d.ts +8 -0
- package/dist/workspace/PluginSlot.js +9 -0
- package/dist/workspace/Room.d.ts +9 -0
- package/dist/workspace/Room.js +11 -0
- package/dist/workspace/WorkspaceProvider.d.ts +13 -0
- package/dist/workspace/WorkspaceProvider.js +11 -0
- package/dist/workspace/WorkspaceShell.d.ts +11 -0
- package/dist/workspace/WorkspaceShell.js +19 -0
- package/dist/workspace/index.d.ts +14 -0
- package/dist/workspace/index.js +10 -0
- package/dist/workspace/resource-types.d.ts +65 -0
- package/dist/workspace/resource-types.js +56 -0
- package/dist/workspace/store.d.ts +23 -0
- package/dist/workspace/store.js +55 -0
- package/dist/workspace/types.d.ts +54 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/use-room.d.ts +6 -0
- package/dist/workspace/use-room.js +9 -0
- package/dist/workspace/use-workspace-call.d.ts +17 -0
- package/dist/workspace/use-workspace-call.js +29 -0
- package/dist/workspace/use-workspace.d.ts +18 -0
- package/dist/workspace/use-workspace.js +23 -0
- package/dist/workspace/workspace-client.d.ts +16 -0
- package/dist/workspace/workspace-client.js +109 -0
- 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,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;
|