← Public packages
@danj/audible-auth
Locked Audible credential gateway: Connect, token refresh, and narrow Audible calls for other Kody packages.
src/status.ts
87 lines · 3.0 KB · TypeScriptimport { 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<string, string[]> = 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<DeviceRecord> {
try {
await audibleRequest({ path: '/1.0/library', query: { num_results: 1 } })
return (await loadDevice()) ?? device
} catch {
return device
}
}