← Public packages
@kentcdodds/audible
Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.
src/auth.ts
337 lines · 10.8 KB · TypeScriptimport { packageSecrets, packageStorage } from 'kody:runtime'
import type { AudibleAuth } from './types.ts'
import { AUTH_RELATED_HOSTS } from './locales.ts'
export const AUDIBLE_AUTH_SECRET = 'audibleAuth'
/** packageStorage key for audible-cli JSON used for ADP signing (never returned from exports). */
export const AUDIBLE_AUTH_STORAGE_KEY = 'audible-auth-v1'
export function audibleAuthSetupUrl(
packageId?: string,
scope: 'user' | 'package' = 'user',
): string {
const hosts = AUTH_RELATED_HOSTS.join(',')
const params = new URLSearchParams({
name: AUDIBLE_AUTH_SECRET,
description:
'Audible device auth JSON (browser OpenID+PKCE / audible-cli / OpenAudible)',
allowedHosts: hosts,
scope,
})
if (packageId) params.set('allowedPackages', packageId)
return `https://kody.codes/account/secrets/new?${params.toString()}`
}
/** True when a mount value is an unresolved `{{secret:…}}` placeholder (opaque; not parseable). */
export function isSecretPlaceholder(value: unknown): boolean {
if (typeof value !== 'string') return false
const trimmed = value.trim()
return /^\{\{\s*secret\s*:/i.test(trimmed)
}
type MountedSecrets = {
get?: (alias: string) => unknown
[alias: string]: unknown
} | ((alias: string) => unknown)
async function readMountedSecretRaw(alias: string): Promise<unknown> {
try {
const mounts = packageSecrets as MountedSecrets | null
if (typeof mounts === 'function') {
return await (mounts as (name: string) => unknown)(alias)
}
if (mounts && typeof mounts === 'object') {
if (typeof mounts.get === 'function') {
return await mounts.get(alias)
}
const direct = mounts[alias]
if (direct && typeof direct === 'object' && 'value' in direct) {
return (direct as { value?: unknown }).value
}
return direct
}
const env = (
globalThis as { process?: { env?: Record<string, string | undefined> } }
).process?.env
return env?.[alias] ?? env?.[alias.toUpperCase()] ?? null
} catch {
return null
}
}
/** Read audible-cli auth JSON from packageStorage (preferred post opaque-secrets). */
export async function readAuthFromPackageStorage(): Promise<string | null> {
try {
const raw = await packageStorage().get(AUDIBLE_AUTH_STORAGE_KEY)
if (raw == null) return null
if (typeof raw === 'string' && raw.trim()) return raw.trim()
if (raw && typeof raw === 'object') return JSON.stringify(raw)
} catch {
return null
}
return null
}
/** Write audible-cli auth JSON to packageStorage. Never log or return the value. */
export async function writeAuthToPackageStorage(authJson: string): Promise<void> {
await packageStorage().set(AUDIBLE_AUTH_STORAGE_KEY, authJson)
}
/** Normalize OpenAudible credentials.json or audible-cli auth JSON into AudibleAuth. */
export function fromAuthFile(raw: unknown): AudibleAuth {
if (raw == null) {
throw new Error('Auth file content is empty.')
}
if (isSecretPlaceholder(raw)) {
throw new Error(
'Opaque secret placeholder cannot be parsed for ADP signing. Complete Connect re-auth so auth is stored in package storage.',
)
}
const data =
typeof raw === 'string'
? (JSON.parse(raw) as Record<string, unknown>)
: (raw as Record<string, unknown>)
// OpenAudible multi-device map: { "Device Name": { tokens, extensions, additionnel } }
const deviceEntries = Object.entries(data).filter(
([, v]) =>
v &&
typeof v === 'object' &&
'tokens' in (v as object) &&
'extensions' in (v as object),
)
if (deviceEntries.length > 0) {
const [, first] = deviceEntries[0]!
return convertOpenAudibleDevice(first as Record<string, unknown>)
}
// Single OpenAudible device object
if (data.tokens && typeof data.tokens === 'object') {
return convertOpenAudibleDevice(data)
}
// Already audible-cli shape
if (typeof data.adp_token === 'string' && typeof data.device_private_key === 'string') {
return {
adp_token: data.adp_token,
device_private_key: data.device_private_key,
access_token:
typeof data.access_token === 'string' ? data.access_token : undefined,
refresh_token:
typeof data.refresh_token === 'string' ? data.refresh_token : undefined,
expires: typeof data.expires === 'number' ? data.expires : undefined,
website_cookies:
data.website_cookies && typeof data.website_cookies === 'object'
? (data.website_cookies as Record<string, string>)
: undefined,
store_authentication_cookie: data.store_authentication_cookie as
| Record<string, unknown>
| string
| undefined,
device_info:
data.device_info && typeof data.device_info === 'object'
? (data.device_info as AudibleAuth['device_info'])
: undefined,
customer_info:
data.customer_info && typeof data.customer_info === 'object'
? (data.customer_info as AudibleAuth['customer_info'])
: undefined,
locale:
typeof data.locale === 'string'
? data.locale
: typeof data.country_code === 'string'
? data.country_code
: undefined,
}
}
throw new Error(
'Unrecognized auth JSON. Expected audible-cli auth file (adp_token + device_private_key) or OpenAudible credentials.json.',
)
}
function convertOpenAudibleDevice(origin: Record<string, unknown>): AudibleAuth {
const tokens = origin.tokens as Record<string, unknown>
const mac = tokens.mac_dms as Record<string, string>
const bearer = tokens.bearer as Record<string, unknown>
const extensions = origin.extensions as Record<string, unknown>
const additionnel = (origin.additionnel ?? origin.additional ?? {}) as Record<
string,
unknown
>
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, '')
}
}
} else if (rawCookies && typeof rawCookies === 'object') {
Object.assign(websiteCookies, rawCookies as Record<string, string>)
}
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:
typeof additionnel.expires === 'number'
? additionnel.expires
: undefined,
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:
typeof additionnel.region === 'string'
? String(additionnel.region).toLowerCase()
: typeof additionnel.locale === 'string'
? String(additionnel.locale).toLowerCase()
: undefined,
}
}
export async function loadAuthFromSecret(
secretAlias = AUDIBLE_AUTH_SECRET,
): Promise<
| { ok: true; auth: AudibleAuth; pendingSecret: false }
| { ok: false; auth: null; pendingSecret: true; setupUrl: string; reason: string }
> {
const setupUrl = audibleAuthSetupUrl()
// Prefer packageStorage — opaque secret mounts are placeholders and cannot ADP-sign.
const stored = await readAuthFromPackageStorage()
if (stored) {
try {
const auth = fromAuthFile(stored)
if (!auth.adp_token || !auth.device_private_key) {
return {
ok: false,
auth: null,
pendingSecret: true,
setupUrl,
reason:
'packageStorage audible-auth-v1 is present but missing adp_token or device_private_key. Re-run Connect.',
}
}
return { ok: true, auth, pendingSecret: false }
} catch (error) {
return {
ok: false,
auth: null,
pendingSecret: true,
setupUrl,
reason: `packageStorage audible-auth-v1 could not be parsed: ${String((error as Error)?.message ?? error)}. Re-run Connect.`,
}
}
}
const mounted = await readMountedSecretRaw(secretAlias)
if (mounted != null && isSecretPlaceholder(mounted)) {
return {
ok: false,
auth: null,
pendingSecret: true,
setupUrl,
reason:
'audibleAuth is an opaque secret placeholder ({{secret:…}}) and cannot be JSON.parsed for ADP signing after the opaque-secrets change. Complete Connect once — auth is stored in package storage for signing. Opaque secrets cannot be migrated.',
}
}
if (mounted != null && typeof mounted === 'string' && mounted.trim() && !isSecretPlaceholder(mounted)) {
// Legacy readable string (pre-opaque) — try once, then prefer migrating via Connect.
try {
const auth = fromAuthFile(mounted.trim())
if (auth.adp_token && auth.device_private_key) {
try {
await writeAuthToPackageStorage(mounted.trim())
} catch {
// best-effort migrate into packageStorage
}
return { ok: true, auth, pendingSecret: false }
}
} catch {
// fall through
}
}
if (mounted != null && typeof mounted === 'object' && !isSecretPlaceholder(mounted)) {
try {
const auth = fromAuthFile(mounted)
if (auth.adp_token && auth.device_private_key) {
try {
await writeAuthToPackageStorage(JSON.stringify(mounted))
} catch {
// best-effort
}
return { ok: true, auth, pendingSecret: false }
}
} catch {
// fall through
}
}
return {
ok: false,
auth: null,
pendingSecret: true,
setupUrl,
reason:
'No audible auth in package storage (audible-auth-v1). Open the package app Connect tab (or start-auth → complete-auth) once — after opaque secrets, ADP signing keys live in package storage, not in readable secret mounts.',
}
}
export function accessTokenFreshness(auth: AudibleAuth): {
hasAccessToken: boolean
expiresAt: string | null
expired: boolean | null
secondsRemaining: number | null
} {
const hasAccessToken = Boolean(auth.access_token)
if (typeof auth.expires !== 'number') {
return {
hasAccessToken,
expiresAt: null,
expired: null,
secondsRemaining: null,
}
}
const expiresAtMs = auth.expires * 1000
const secondsRemaining = Math.floor((expiresAtMs - Date.now()) / 1000)
return {
hasAccessToken,
expiresAt: new Date(expiresAtMs).toISOString(),
expired: secondsRemaining <= 0,
secondsRemaining,
}
}
/** Public metadata about auth — never includes token or key material. */
export function authPublicSummary(auth: AudibleAuth) {
const freshness = accessTokenFreshness(auth)
return {
hasAdpToken: Boolean(auth.adp_token),
hasDevicePrivateKey: Boolean(auth.device_private_key),
hasRefreshToken: Boolean(auth.refresh_token),
hasWebsiteCookies: Boolean(
auth.website_cookies && Object.keys(auth.website_cookies).length > 0,
),
hasDeviceInfo: Boolean(auth.device_info?.device_serial_number),
hasCustomerInfo: Boolean(auth.customer_info?.user_id),
locale: auth.locale ?? 'us',
deviceName:
typeof auth.device_info?.device_name === 'string'
? auth.device_info.device_name
: null,
...freshness,
}
}