Skip to content

src/device.ts

77 lines · 2.2 KB · TypeScript
Raw
/** Non-secret facts about the registered virtual device, in package storage. */

import { packageStorage } from 'kody:runtime'
import { ARCHIVE_SIGNING_KEY, DEVICE_KEY } from './config.ts'

export type DeviceRecord = {
	locale: string
	withUsername: boolean
	serial: string
	deviceType: string
	customerId?: string
	deviceName?: string
	connectedAt: string
	/** Unix seconds when the current access token expires. */
	accessTokenExpiresAt: number
	/** Saved package id that owns the token secrets; builds host-approval links. */
	packageId?: string
	/** First time an Audible call succeeded, which proves the hosts are approved. */
	verifiedAt?: string
}

/** ADP signing material. Stored only while archive is enabled. */
export type ArchiveSigning = {
	adpToken: string
	devicePrivateKey: string
}

export class NotConnectedError extends Error {
	constructor() {
		super('Audible is not connected. Open the Connect tab in the Audible app.')
		this.name = 'NotConnectedError'
	}
}

function parse<T>(raw: unknown): T | null {
	if (raw == null) return null
	if (typeof raw === 'string') {
		try {
			return JSON.parse(raw) as T
		} catch {
			return null
		}
	}
	return raw as T
}

export async function loadDevice(): Promise<DeviceRecord | null> {
	const device = parse<DeviceRecord>(await packageStorage().get(DEVICE_KEY))
	return device?.serial && device.locale ? device : null
}

export async function requireDevice(): Promise<DeviceRecord> {
	const device = await loadDevice()
	if (!device) throw new NotConnectedError()
	return device
}

export async function saveDevice(device: DeviceRecord): Promise<void> {
	await packageStorage().set(DEVICE_KEY, device)
}

export async function clearDevice(): Promise<void> {
	await packageStorage().delete(DEVICE_KEY)
}

export async function loadArchiveSigning(): Promise<ArchiveSigning | null> {
	const signing = parse<ArchiveSigning>(await packageStorage().get(ARCHIVE_SIGNING_KEY))
	return signing?.adpToken && signing.devicePrivateKey ? signing : null
}

export async function saveArchiveSigning(signing: ArchiveSigning): Promise<void> {
	await packageStorage().set(ARCHIVE_SIGNING_KEY, signing)
}

export async function clearArchiveSigning(): Promise<void> {
	await packageStorage().delete(ARCHIVE_SIGNING_KEY)
}