Something went wrong. Try again.
AtAuth forked from bkb.arcnode.xyz/atauth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155/** * Token decoding and utilities * * NOTE: This is CLIENT-SIDE decoding only. Actual verification * should be done server-side using the Rust library. */
import type { TokenPayload } from './types';
/** * Base64url decode (RFC 4648) */function base64UrlDecode(str: string): string { // Add padding if needed let padded = str; while (padded.length % 4 !== 0) { padded += '='; }
// Convert base64url to base64 const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
// Decode if (typeof atob === 'function') { return atob(base64); }
// Node.js fallback return Buffer.from(base64, 'base64').toString('utf-8');}
/** * Decode a token payload WITHOUT verification. * * WARNING: This does NOT verify the signature! * Use this only for client-side display purposes. * Always verify tokens server-side before trusting them. * * @param token - The token string in format "payload.signature" * @returns Decoded payload or null if invalid format */export function decodeToken(token: string): TokenPayload | null { try { // Split token const parts = token.split('.'); if (parts.length !== 2) { console.warn('Invalid token format: expected payload.signature'); return null; }
// Decode payload const payloadJson = base64UrlDecode(parts[0]); const payload = JSON.parse(payloadJson) as TokenPayload;
return payload; } catch (error) { console.error('Failed to decode token:', error); return null; }}
/** * Check if a decoded token is expired. * * @param payload - Decoded token payload * @param clockSkewSeconds - Tolerance for clock differences (default: 30) * @returns True if expired */export function isTokenExpired( payload: TokenPayload, clockSkewSeconds = 30): boolean { const now = Math.floor(Date.now() / 1000); return now > payload.exp + clockSkewSeconds;}
/** * Get remaining token validity in seconds. * * @param payload - Decoded token payload * @returns Seconds remaining (0 if expired) */export function getTokenRemainingSeconds(payload: TokenPayload): number { const now = Math.floor(Date.now() / 1000); return Math.max(0, payload.exp - now);}
/** * Get token age in seconds since issuance. * * @param payload - Decoded token payload * @returns Seconds since token was issued */export function getTokenAgeSeconds(payload: TokenPayload): number { const now = Math.floor(Date.now() / 1000); return Math.max(0, now - payload.iat);}
/** * Check if a token should be refreshed. * * @param payload - Decoded token payload * @param thresholdSeconds - Refresh if less than this many seconds remaining * @returns True if token should be refreshed */export function shouldRefreshToken( payload: TokenPayload, thresholdSeconds = 300): boolean { return getTokenRemainingSeconds(payload) < thresholdSeconds;}
/** * Extract display name from handle. * * @param handle - Full handle (e.g., "alice.bsky.social") * @returns Username portion (e.g., "alice") */export function getDisplayName(handle: string): string { const parts = handle.split('.'); return parts[0] || handle;}
/** * Validate DID format (basic client-side check). * * @param did - DID string to validate * @returns True if format is valid */export function isValidDid(did: string): boolean { if (!did || did.length > 512) return false; if (!did.startsWith('did:')) return false;
const parts = did.split(':'); return parts.length >= 3 && parts[1].length > 0 && parts[2].length > 0;}
/** * Validate handle format (basic client-side check). * * @param handle - Handle string to validate * @returns True if format is valid */export function isValidHandle(handle: string): boolean { if (!handle || handle.length > 256) return false; if (!handle.includes('.')) return false;
const parts = handle.split('.'); if (parts.some((p) => p.length === 0 || p.length > 63)) return false;
// TLD must be at least 2 chars const tld = parts[parts.length - 1]; return tld.length >= 2;}