import { ACCESS_TOKEN_SECRET, ARCHIVE_ENABLED, REFRESH_TOKEN_SECRET } from './config.ts' import { loadArchiveSigning, loadDevice, type DeviceRecord } from './device.ts' import { resolveLocale } from './locales.ts' import audibleRequest from './request.ts' import { secretStates } from './secrets.ts' import { requiredHosts } from './tokens.ts' const KODY_ORIGIN = 'https://kody.codes' export type HostApproval = { /** Secret that needs the hosts. */ secret: string /** Hosts this secret may be sent to. */ hosts: string[] /** Kody's Allow page with the secret and hosts filled in; null without a package id. */ url: string | null } function approvalUrl(secret: string, hosts: string[], packageId: string | undefined) { if (!packageId) return null const params = new URLSearchParams({ name: secret, hosts: hosts.join(','), scope: 'package', packageId, }) return `${KODY_ORIGIN}/connect/secrets?${params}` } /** * Report whether Audible is connected and usable. Returns metadata only: * no token, key, or secret value. * * @returns connection state, the host approvals still needed with one-click links, and archive state * * @example * import authStatus from 'kody:@danj/audible-auth/status' * * const status = await authStatus() * // { connected: true, ready: true, locale: 'ca', hostApprovals: [], archiveEnabled: false, … } */ export default async function authStatus() { let device = await loadDevice() const states = await secretStates([ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET]) if (device && !states && !device.verifiedAt) device = await verifyHosts(device) const required: Record = device ? requiredHosts(device) : {} const packageId = device?.packageId // Without secret metadata, a successful Audible call is the proof of approval. const hostsKnown = Boolean(states) || Boolean(device?.verifiedAt) const hostApprovals: HostApproval[] = [] for (const [secret, hosts] of Object.entries(required)) { const missing = states ? hosts.filter((host) => !(states[secret]?.hosts ?? []).includes(host)) : device?.verifiedAt ? [] : hosts if (missing.length) { hostApprovals.push({ secret, hosts: missing, url: approvalUrl(secret, missing, packageId) }) } } const connected = Boolean(device) return { ok: true as const, connected, ready: connected && hostsKnown && hostApprovals.length === 0, locale: device ? resolveLocale(device.locale).countryCode : null, deviceName: device?.deviceName ?? null, connectedAt: device?.connectedAt ?? null, accessTokenExpiresAt: device ? new Date(device.accessTokenExpiresAt * 1000).toISOString() : null, hostApprovals, archiveEnabled: ARCHIVE_ENABLED, archiveReady: ARCHIVE_ENABLED && Boolean(await loadArchiveSigning()), } } /** Where secret metadata is unavailable, one small Library call proves the hosts are approved. */ async function verifyHosts(device: DeviceRecord): Promise { try { await audibleRequest({ path: '/1.0/library', query: { num_results: 1 } }) return (await loadDevice()) ?? device } catch { return device } }