@cantera/oauth-typestypes
Generic OAuth types for cantera components: providers, scopes, connections, accounts. The lingua franca adapters translate into.
npx shadcn@latest add @cantera/oauth-typesThe lingua franca every cantera component speaks. Components take these shapes as props and never fetch data themselves — adapters translate provider payloads into them, so Autodesk, Procore, or your own provider all render with the same components.
import type { OAuthConnection, OAuthProvider } from '@/lib/oauth-types'
const fieldlink: OAuthProvider = {
id: 'fieldlink',
name: 'FieldLink',
}
const connection: OAuthConnection = {
provider: fieldlink,
status: 'connected',
account: { name: 'Dana Alvarez', email: 'dana@ridgelinebuilders.com' },
scopes: ['rfis:read', 'submittals:read'],
expiresAt: Date.now() + 55 * 60_000,
}| Export | Type | Description |
|---|---|---|
| OAuthProvider | interface | A provider identity: id, name, optional icon and docsUrl. Marks carry their own default size, so one renders correctly wherever it is dropped; a [&_svg]:size-* wrapper still wins. |
| OAuthScope | interface | One grantable scope: id (the literal scope string), label, description, required. |
| OAuthScopePreset | interface | A named bundle of scope ids for a common task, e.g. "Viewer". |
| OAuthConnectionStatus | type | 'connected' | 'expired' | 'error' | 'disconnected'. |
| OAuthAccount | interface | The human behind a grant: name, email, avatarUrl — all optional. |
| OAuthConnection | interface | A provider grant: provider, status, and optional account, scopes, expiresAt, error. |
| connectionExpiry | (connection: OAuthConnection) => Date | null | Normalizes expiresAt (Date, string, or number) into a Date, or null when absent. |
| isExpiringSoon | (connection: OAuthConnection, withinMs?: number) => boolean | True when the connection expires within withinMs — five minutes by default. |
| accountInitials | (account?: OAuthAccount) => string | Initials for avatar fallbacks: "Dana Alvarez" becomes "DA". |
This is the exact code the CLI installs into your project — you own it from there.
import type { ReactNode } from 'react'
export interface OAuthProvider {
id: string
name: string
/** A mark carries its own default size (the presets ship `className="size-4"`);
* a surface wanting another size wraps it in `[&_svg]:size-*`, which wins. */
icon?: ReactNode
docsUrl?: string
}
export interface OAuthScope {
/** The literal scope string sent to the provider, e.g. "data:read". */
id: string
label: string
description?: string
/** Required scopes are always selected and cannot be deselected. */
required?: boolean
}
export interface OAuthScopePreset {
id: string
label: string
description?: string
scopes: string[]
}
export type OAuthConnectionStatus = 'connected' | 'expired' | 'error' | 'disconnected'
export interface OAuthAccount {
name?: string
email?: string
avatarUrl?: string
}
export interface OAuthConnection {
provider: OAuthProvider
status: OAuthConnectionStatus
account?: OAuthAccount
scopes?: string[]
expiresAt?: Date | string | number
error?: string
}
export function connectionExpiry(connection: OAuthConnection): Date | null {
if (connection.expiresAt == null) return null
const date = new Date(connection.expiresAt)
return Number.isNaN(date.getTime()) ? null : date
}
export function isExpiringSoon(connection: OAuthConnection, withinMs = 5 * 60_000): boolean {
const expiry = connectionExpiry(connection)
if (!expiry) return false
return expiry.getTime() - Date.now() <= withinMs
}
const EMAIL_DOMAIN = /@.*$/
const NAME_SEPARATORS = /[\s._-]+/
export function accountInitials(account: OAuthAccount | undefined): string {
const source = account?.name ?? account?.email ?? ''
const parts = source.replace(EMAIL_DOMAIN, '').split(NAME_SEPARATORS).filter(Boolean)
if (parts.length === 0) return '?'
const first = parts[0][0] ?? ''
const last = parts.length > 1 ? (parts[parts.length - 1][0] ?? '') : ''
return `${first}${last}`.toUpperCase() || '?'
}