Skip to content
← Public packages

@kentcdodds/audible

Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.

src/persist-auth.ts

168 lines · 4.7 KB · TypeScript
/**
 * Persist audible-cli auth JSON in packageStorage for ADP signing.
 * secretSet remains optional/best-effort for host allowlist naming only —
 * opaque secret mounts are not readable for JSON.parse after Sept 2026.
 * Never returns secret values.
 */

import { kody } from 'kody:runtime'
import {
	AUDIBLE_AUTH_SECRET,
	audibleAuthSetupUrl,
	authPublicSummary,
	fromAuthFile,
	writeAuthToPackageStorage,
} from './auth.ts'
import { AUTH_RELATED_HOSTS } from './locales.ts'
import { loadAuthSecretScope, rememberAuthSecretScope } from './auth-session.ts'
import type { AudibleAuth } from './types.ts'

export type PersistAuthResult = {
	ok: true
	secretName: string
	scope: 'package' | 'user' | 'packageStorage'
	authStatus: ReturnType<typeof authPublicSummary>
	setupUrl: string
	hostsToApprove: string[]
	note: string
	secretSetOk: boolean
} | {
	ok: false
	secretName: string
	setupUrl: string
	hostsToApprove: string[]
	reason: string
}

function errorMessage(error: unknown): string {
	if (error instanceof Error) return error.message
	return String(error)
}

function authToJson(auth: AudibleAuth): string {
	return JSON.stringify(
		{
			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,
		},
		null,
		2,
	)
}

/**
 * Write auth for ADP signing to packageStorage first. Optionally best-effort
 * secretSet (package then user) for host allowlist naming — do not rely on
 * reading the secret back.
 */
export async function persistAudibleAuth(
	authJson: string,
): Promise<PersistAuthResult> {
	const auth = fromAuthFile(authJson)
	const summary = authPublicSummary(auth)
	const setupUrl = audibleAuthSetupUrl()
	const hosts = AUTH_RELATED_HOSTS
	const description =
		'Audible device auth JSON from browser OpenID+PKCE registration (audible-cli compatible). Opaque; ADP signing uses packageStorage audible-auth-v1.'

	try {
		await writeAuthToPackageStorage(authJson)
	} catch (error) {
		return {
			ok: false,
			secretName: AUDIBLE_AUTH_SECRET,
			setupUrl,
			hostsToApprove: hosts,
			reason: `packageStorage write failed for audible-auth-v1: ${errorMessage(error)}`,
		}
	}

	const attempts: Array<'package' | 'user'> = ['package', 'user']
	const errors: string[] = []
	let secretScope: 'package' | 'user' | null = null

	for (const scope of attempts) {
		try {
			await kody.secretSet({
				name: AUDIBLE_AUTH_SECRET,
				value: authJson,
				description,
				scope,
			})
			await rememberAuthSecretScope(scope)
			secretScope = scope
			break
		} catch (error) {
			errors.push(`${scope}: ${errorMessage(error)}`)
		}
	}

	return {
		ok: true,
		secretName: AUDIBLE_AUTH_SECRET,
		scope: secretScope ?? 'packageStorage',
		authStatus: summary,
		setupUrl,
		hostsToApprove: hosts,
		secretSetOk: secretScope != null,
		note:
			secretScope != null
				? `Saved ADP signing auth in packageStorage (audible-auth-v1) and best-effort secretSet as ${secretScope}-scoped audibleAuth (opaque; not re-read for signing).`
				: `Saved ADP signing auth in packageStorage (audible-auth-v1). secretSet optional and denied (${errors.join(' | ')}); Connect/signing still works from package storage.`,
	}
}

/** Re-persist updated access_token / expires after refresh (packageStorage first). */
export async function persistRefreshedAuth(auth: AudibleAuth): Promise<{
	persisted: boolean
	scope: 'package' | 'user' | 'packageStorage' | null
	reason?: string
}> {
	const authJson = authToJson(auth)

	try {
		await writeAuthToPackageStorage(authJson)
	} catch (error) {
		return {
			persisted: false,
			scope: null,
			reason: `packageStorage write failed while refreshing: ${errorMessage(error)}; in-memory token still updated for this run.`,
		}
	}

	const remembered = await loadAuthSecretScope()
	const order: Array<'package' | 'user'> = remembered
		? [remembered, remembered === 'package' ? 'user' : 'package']
		: ['package', 'user']

	for (const scope of order) {
		try {
			await kody.secretSet({
				name: AUDIBLE_AUTH_SECRET,
				value: authJson,
				description:
					'Audible device auth JSON (access token refreshed). Opaque; ADP signing uses packageStorage.',
				scope,
			})
			await rememberAuthSecretScope(scope)
			return { persisted: true, scope }
		} catch (error) {
			void error
		}
	}
	return {
		persisted: true,
		scope: 'packageStorage',
		reason:
			'packageStorage updated; secretSet denied while refreshing (optional). In-memory + storage token updated for this run.',
	}
}