Skip to content

src/register.ts

98 lines · 3.3 KB · TypeScript
Raw
/**
 * Audible virtual-device registration.
 * Ported from mkb79/Audible register.py (logic only).
 */

import { buildClientId, DEVICE_TYPE } from './login.ts'
import type { LocaleInfo } from './locales.ts'

/** Everything Amazon returns that this package uses. Lives in memory only. */
export type RegisteredDevice = {
	adpToken: string
	devicePrivateKey: string
	accessToken: string
	refreshToken: string
	/** Unix seconds when accessToken expires. */
	expires: number
	serial: string
	deviceType: string
	customerId?: string
	deviceName?: string
}

export async function registerDevice(input: {
	authorizationCode: string
	codeVerifier: string
	locale: LocaleInfo
	serial: string
	withUsername?: boolean
}): Promise<RegisteredDevice> {
	const domain = input.locale.domain
	const body = {
		requested_token_type: ['bearer', 'mac_dms'],
		cookies: { website_cookies: [], domain: `.amazon.${domain}` },
		registration_data: {
			domain: 'Device',
			app_version: '3.56.2',
			device_serial: input.serial,
			device_type: DEVICE_TYPE,
			device_name:
				'%FIRST_NAME%%FIRST_NAME_POSSESSIVE_STRING%%DUPE_STRATEGY_1ST%Audible for iPhone',
			os_version: '15.0.0',
			software_version: '35602678',
			device_model: 'iPhone',
			app_name: 'Audible',
		},
		auth_data: {
			client_id: buildClientId(input.serial),
			authorization_code: input.authorizationCode,
			code_verifier: input.codeVerifier,
			code_algorithm: 'SHA-256',
			client_domain: 'DeviceLegacy',
		},
		requested_extensions: ['device_info', 'customer_info'],
	}

	const target = input.withUsername ? 'audible' : 'amazon'
	const response = await fetch(`https://api.${target}.${domain}/auth/register`, {
		method: 'POST',
		headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
		body: JSON.stringify(body),
	})
	const json = (await response.json().catch(() => null)) as Record<string, unknown> | null
	if (!response.ok) {
		throw new Error(`Device registration failed (HTTP ${response.status}).`)
	}
	const success = (json?.response as Record<string, unknown> | undefined)?.success as
		| Record<string, unknown>
		| undefined
	if (!success) throw new Error('Device registration response missing success payload.')

	const tokens = success.tokens as Record<string, unknown>
	const mac = tokens.mac_dms as Record<string, unknown>
	const bearer = tokens.bearer as Record<string, unknown>
	const extensions = (success.extensions ?? {}) as Record<string, unknown>
	const deviceInfo = (extensions.device_info ?? {}) as Record<string, unknown>
	const customerInfo = (extensions.customer_info ?? {}) as Record<string, unknown>

	if (
		typeof mac?.adp_token !== 'string' ||
		typeof mac?.device_private_key !== 'string' ||
		typeof bearer?.access_token !== 'string' ||
		typeof bearer?.refresh_token !== 'string'
	) {
		throw new Error('Device registration response is missing tokens.')
	}

	return {
		adpToken: mac.adp_token,
		devicePrivateKey: mac.device_private_key,
		accessToken: bearer.access_token,
		refreshToken: bearer.refresh_token,
		expires: Math.floor(Date.now() / 1000) + Number(bearer.expires_in ?? 3600),
		serial: String(deviceInfo.device_serial_number ?? input.serial),
		deviceType: String(deviceInfo.device_type ?? DEVICE_TYPE),
		customerId: typeof customerInfo.user_id === 'string' ? customerInfo.user_id : undefined,
		deviceName: typeof deviceInfo.device_name === 'string' ? deviceInfo.device_name : undefined,
	}
}