mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 10:21:40 +00:00
277 lines
9.7 KiB
TypeScript
277 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
AlertCircle,
|
|
CheckCircle,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Clock,
|
|
Copy,
|
|
ExternalLink,
|
|
Key,
|
|
Shield,
|
|
} from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { cn } from '../../../lib/utils';
|
|
import { Badge } from '../../ui/badge';
|
|
import { Button } from '../../ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../ui/tabs';
|
|
|
|
/**
|
|
* UCAN capability structure
|
|
*/
|
|
export interface UCANCapability {
|
|
with: string; // Resource identifier
|
|
can: string; // Action/permission
|
|
nb?: Record<string, any>; // Additional constraints
|
|
}
|
|
|
|
/**
|
|
* UCAN token structure
|
|
*/
|
|
export interface UCANToken {
|
|
iss: string; // Issuer DID
|
|
aud: string; // Audience DID
|
|
exp?: number; // Expiration timestamp
|
|
nbf?: number; // Not before timestamp
|
|
att: UCANCapability[]; // Attenuations/capabilities
|
|
prf?: string[]; // Proof chain
|
|
fct?: Record<string, any>; // Facts
|
|
}
|
|
|
|
/**
|
|
* Props for UCANViewer component
|
|
*/
|
|
export interface UCANViewerProps {
|
|
token: UCANToken;
|
|
showRaw?: boolean;
|
|
showProofChain?: boolean;
|
|
onCopyToken?: () => void;
|
|
onVerify?: () => Promise<boolean>;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Viewer for UCAN token visualization and capability display
|
|
*/
|
|
export function UCANViewer({
|
|
token,
|
|
showRaw = true,
|
|
showProofChain = true,
|
|
onCopyToken,
|
|
onVerify,
|
|
className,
|
|
}: UCANViewerProps) {
|
|
const [expandedCapabilities, setExpandedCapabilities] = useState<Set<number>>(new Set());
|
|
const [verificationStatus, setVerificationStatus] = useState<
|
|
'idle' | 'verifying' | 'valid' | 'invalid'
|
|
>('idle');
|
|
|
|
const toggleCapability = (index: number) => {
|
|
const newExpanded = new Set(expandedCapabilities);
|
|
if (newExpanded.has(index)) {
|
|
newExpanded.delete(index);
|
|
} else {
|
|
newExpanded.add(index);
|
|
}
|
|
setExpandedCapabilities(newExpanded);
|
|
};
|
|
|
|
const handleVerify = async () => {
|
|
if (!onVerify) return;
|
|
setVerificationStatus('verifying');
|
|
try {
|
|
const isValid = await onVerify();
|
|
setVerificationStatus(isValid ? 'valid' : 'invalid');
|
|
} catch {
|
|
setVerificationStatus('invalid');
|
|
}
|
|
};
|
|
|
|
const formatDate = (timestamp?: number) => {
|
|
if (!timestamp) return 'Never';
|
|
return new Date(timestamp * 1000).toLocaleString();
|
|
};
|
|
|
|
const isExpired = token.exp && token.exp * 1000 < Date.now();
|
|
const isNotYetValid = token.nbf && token.nbf * 1000 > Date.now();
|
|
|
|
return (
|
|
<div className={cn('space-y-4', className)}>
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5" />
|
|
<CardTitle>UCAN Token</CardTitle>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{isExpired && (
|
|
<Badge variant="destructive" className="text-xs">
|
|
<AlertCircle className="h-3 w-3 mr-1" />
|
|
Expired
|
|
</Badge>
|
|
)}
|
|
{isNotYetValid && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
<Clock className="h-3 w-3 mr-1" />
|
|
Not Yet Valid
|
|
</Badge>
|
|
)}
|
|
{verificationStatus === 'valid' && (
|
|
<Badge variant="default" className="text-xs">
|
|
<CheckCircle className="h-3 w-3 mr-1" />
|
|
Verified
|
|
</Badge>
|
|
)}
|
|
{verificationStatus === 'invalid' && (
|
|
<Badge variant="destructive" className="text-xs">
|
|
<AlertCircle className="h-3 w-3 mr-1" />
|
|
Invalid
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<CardDescription>
|
|
User-Controlled Authorization Network token with delegated capabilities
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs defaultValue="details" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-3">
|
|
<TabsTrigger value="details">Details</TabsTrigger>
|
|
<TabsTrigger value="capabilities">Capabilities ({token.att.length})</TabsTrigger>
|
|
{showRaw && <TabsTrigger value="raw">Raw Token</TabsTrigger>}
|
|
</TabsList>
|
|
|
|
<TabsContent value="details" className="space-y-4">
|
|
<div className="space-y-3">
|
|
<div className="flex items-start gap-2">
|
|
<Key className="h-4 w-4 mt-0.5 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Issuer</p>
|
|
<code className="text-xs text-muted-foreground break-all">{token.iss}</code>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-2">
|
|
<Key className="h-4 w-4 mt-0.5 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Audience</p>
|
|
<code className="text-xs text-muted-foreground break-all">{token.aud}</code>
|
|
</div>
|
|
</div>
|
|
|
|
{token.exp && (
|
|
<div className="flex items-start gap-2">
|
|
<Clock className="h-4 w-4 mt-0.5 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Expires</p>
|
|
<p className="text-xs text-muted-foreground">{formatDate(token.exp)}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{token.nbf && (
|
|
<div className="flex items-start gap-2">
|
|
<Clock className="h-4 w-4 mt-0.5 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Not Before</p>
|
|
<p className="text-xs text-muted-foreground">{formatDate(token.nbf)}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showProofChain && token.prf && token.prf.length > 0 && (
|
|
<div className="flex items-start gap-2">
|
|
<ExternalLink className="h-4 w-4 mt-0.5 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Proof Chain</p>
|
|
<div className="space-y-1 mt-1">
|
|
{token.prf.map((proof, index) => (
|
|
<code
|
|
key={index}
|
|
className="block text-xs text-muted-foreground break-all"
|
|
>
|
|
{proof}
|
|
</code>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
{onVerify && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={handleVerify}
|
|
disabled={verificationStatus === 'verifying'}
|
|
>
|
|
{verificationStatus === 'verifying' ? 'Verifying...' : 'Verify Token'}
|
|
</Button>
|
|
)}
|
|
{onCopyToken && (
|
|
<Button size="sm" variant="outline" onClick={onCopyToken}>
|
|
<Copy className="h-3 w-3 mr-1" />
|
|
Copy Token
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="capabilities" className="space-y-3">
|
|
{token.att.map((capability, index) => (
|
|
<Card key={index} className="border-muted">
|
|
<CardHeader
|
|
className="cursor-pointer py-3"
|
|
onClick={() => toggleCapability(index)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{expandedCapabilities.has(index) ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
<Badge variant="outline">{capability.can}</Badge>
|
|
<code className="text-xs text-muted-foreground">{capability.with}</code>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
{expandedCapabilities.has(index) && capability.nb && (
|
|
<CardContent className="pt-0">
|
|
<div className="space-y-2">
|
|
<p className="text-xs font-medium text-muted-foreground">Constraints:</p>
|
|
<pre className="text-xs bg-muted p-2 rounded overflow-auto">
|
|
{JSON.stringify(capability.nb, null, 2)}
|
|
</pre>
|
|
</div>
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
))}
|
|
{token.att.length === 0 && (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No capabilities defined
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{showRaw && (
|
|
<TabsContent value="raw" className="space-y-3">
|
|
<pre className="text-xs bg-muted p-4 rounded overflow-auto">
|
|
{JSON.stringify(token, null, 2)}
|
|
</pre>
|
|
</TabsContent>
|
|
)}
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|