{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "oauth-types", "title": "OAuth Types", "author": "MR ", "description": "Generic OAuth types for cantera components: providers, scopes, connections, accounts. The lingua franca adapters translate into.", "files": [ { "path": "registry/lib/oauth-types.ts", "content": "import type { ReactNode } from 'react'\n\n/**\n * cantera oauth types — the lingua franca for OAuth UI components.\n *\n * Components take these shapes as props and never fetch data themselves.\n * Adapters (e.g. the aps-oauth-preset) translate provider-specific payloads\n * into them, so any OAuth provider — Autodesk, Procore, or your own — renders\n * with the same components.\n */\n\nexport interface OAuthProvider {\n /** Stable identifier, e.g. \"aps\", \"procore\", \"github\". */\n id: string\n /** Human-readable name shown on buttons and cards, e.g. \"Autodesk\". */\n name: string\n /**\n * Brand mark rendered alongside the name. Any ReactNode; usually an SVG.\n *\n * Sizing contract: a mark carries its own default size (the presets ship\n * `className=\"size-4\"`), so it renders correctly wherever it is dropped. A\n * surface that wants a different size wraps it in a `[&_svg]:size-*`\n * container, which wins on specificity.\n */\n icon?: ReactNode\n /** Optional link to the provider's developer or account documentation. */\n docsUrl?: string\n}\n\nexport interface OAuthScope {\n /** The literal scope string sent to the provider, e.g. \"data:read\". */\n id: string\n /** Short label, e.g. \"Read project data\". */\n label: string\n /** One-sentence explanation of what granting this scope allows. */\n description?: string\n /** Required scopes are always selected and cannot be deselected. */\n required?: boolean\n}\n\n/** A named bundle of scopes for a common task, e.g. \"Viewer\" or \"Account admin\". */\nexport interface OAuthScopePreset {\n id: string\n label: string\n description?: string\n scopes: string[]\n}\n\nexport type OAuthConnectionStatus = 'connected' | 'expired' | 'error' | 'disconnected'\n\nexport interface OAuthAccount {\n name?: string\n email?: string\n avatarUrl?: string\n}\n\nexport interface OAuthConnection {\n provider: OAuthProvider\n status: OAuthConnectionStatus\n /** The account this grant belongs to, when known. */\n account?: OAuthAccount\n /** Scope strings held by the current grant. */\n scopes?: string[]\n /** When the current access token expires. */\n expiresAt?: Date | string | number\n /** Human-readable error, shown when status is \"error\". */\n error?: string\n}\n\n/** Normalize an OAuthConnection expiry into a Date, or null when absent. */\nexport function connectionExpiry(connection: OAuthConnection): Date | null {\n if (connection.expiresAt == null) return null\n const date = new Date(connection.expiresAt)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\n/** True when the connection expires within `withinMs` (default five minutes). */\nexport function isExpiringSoon(connection: OAuthConnection, withinMs = 5 * 60_000): boolean {\n const expiry = connectionExpiry(connection)\n if (!expiry) return false\n return expiry.getTime() - Date.now() <= withinMs\n}\n\n/** Initials for an account, for avatar fallbacks: \"Maria Renteria\" -> \"MR\". */\nexport function accountInitials(account: OAuthAccount | undefined): string {\n const source = account?.name ?? account?.email ?? ''\n const parts = source\n .replace(/@.*$/, '')\n .split(/[\\s._-]+/)\n .filter(Boolean)\n if (parts.length === 0) return '?'\n const first = parts[0][0] ?? ''\n const last = parts.length > 1 ? (parts[parts.length - 1][0] ?? '') : ''\n return `${first}${last}`.toUpperCase() || '?'\n}\n", "type": "registry:lib", "target": "lib/oauth-types.ts" } ], "categories": [ "authentication", "types" ], "type": "registry:lib" }