← Public packages
@kentcdodds/audible
Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.
src/register.ts
203 lines · 6.0 KB · TypeScript/**
* Audible virtual-device registration + access-token refresh.
* Ported from mkb79/Audible register.py / auth.py (logic only).
*/
import type { AudibleAuth } from './types.ts'
import { buildClientId, DEVICE_TYPE } from './login.ts'
import type { LocaleInfo } from './locales.ts'
export type RegisterResult = AudibleAuth & {
with_username?: boolean
}
export async function registerDevice(input: {
authorizationCode: string
codeVerifier: string
locale: LocaleInfo
serial: string
withUsername?: boolean
}): Promise<RegisterResult> {
const withUsername = Boolean(input.withUsername)
const domain = input.locale.domain
const body = {
requested_token_type: [
'bearer',
'mac_dms',
'website_cookies',
'store_authentication_cookie',
],
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 = withUsername ? 'audible' : 'amazon'
const url = `https://api.${target}.${domain}/auth/register`
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(body),
})
const respJson = (await resp.json().catch(() => null)) as Record<
string,
unknown
> | null
if (!resp.ok) {
const message =
respJson && typeof respJson === 'object'
? JSON.stringify(summarizeRegisterError(respJson))
: `HTTP ${resp.status}`
throw new Error(`Device registration failed: ${message}`)
}
if (!respJson) throw new Error('Device registration returned empty body.')
const success = (
(respJson.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, string>
const bearer = tokens.bearer as Record<string, unknown>
const extensions = success.extensions as Record<string, unknown>
const expiresIn = Number(bearer.expires_in ?? 0)
const expires = Math.floor(Date.now() / 1000) + expiresIn
const websiteCookies: Record<string, string> = {}
const rawCookies = tokens.website_cookies
if (Array.isArray(rawCookies)) {
for (const cookie of rawCookies as Array<{ Name?: string; Value?: string }>) {
if (cookie.Name && cookie.Value != null) {
websiteCookies[cookie.Name] = String(cookie.Value).replace(/"/g, '')
}
}
}
return {
adp_token: mac.adp_token,
device_private_key: mac.device_private_key,
access_token:
typeof bearer.access_token === 'string' ? bearer.access_token : undefined,
refresh_token:
typeof bearer.refresh_token === 'string' ? bearer.refresh_token : undefined,
expires,
website_cookies: websiteCookies,
store_authentication_cookie: tokens.store_authentication_cookie as
| Record<string, unknown>
| string
| undefined,
device_info: extensions?.device_info as AudibleAuth['device_info'],
customer_info: extensions?.customer_info as AudibleAuth['customer_info'],
locale: input.locale.countryCode,
with_username: withUsername,
}
}
function summarizeRegisterError(body: Record<string, unknown>): unknown {
// Never echo tokens if Amazon somehow included them in an error payload.
const clone = { ...body }
for (const key of Object.keys(clone)) {
const lower = key.toLowerCase()
if (
lower.includes('token') ||
lower.includes('key') ||
lower.includes('cookie') ||
lower.includes('secret')
) {
clone[key] = '[redacted]'
}
}
return clone
}
/** Refresh bearer access_token from refresh_token (valid ~60 minutes). */
export async function refreshAccessToken(input: {
refreshToken: string
domain: string
withUsername?: boolean
}): Promise<{ access_token: string; expires: number }> {
const target = input.withUsername ? 'audible' : 'amazon'
const url = `https://api.${target}.${input.domain}/auth/token`
const form = new URLSearchParams({
app_name: 'Audible',
app_version: '3.56.2',
source_token: input.refreshToken,
requested_token_type: 'access_token',
source_token_type: 'refresh_token',
})
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: form.toString(),
})
if (!resp.ok) {
const text = await resp.text().catch(() => '')
throw new Error(
`Access token refresh failed (HTTP ${resp.status})${text ? `: ${text.slice(0, 200)}` : ''}`,
)
}
const data = (await resp.json()) as {
access_token?: string
expires_in?: number | string
}
if (typeof data.access_token !== 'string') {
throw new Error('Refresh response missing access_token.')
}
const expiresIn = Number(data.expires_in ?? 3600)
return {
access_token: data.access_token,
expires: Math.floor(Date.now() / 1000) + expiresIn,
}
}
/** audible-cli-compatible auth JSON (no secrets echoed to callers). */
export function toAudibleCliAuthJson(auth: RegisterResult): string {
const payload = {
adp_token: auth.adp_token,
device_private_key: auth.device_private_key,
access_token: auth.access_token,
refresh_token: auth.refresh_token,
expires: auth.expires,
website_cookies: auth.website_cookies,
store_authentication_cookie: auth.store_authentication_cookie,
device_info: auth.device_info,
customer_info: auth.customer_info,
locale_code: auth.locale,
locale: auth.locale,
with_username: auth.with_username ?? false,
}
return JSON.stringify(payload, null, 2)
}