← Public packages
@kentcdodds/audible
Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.
src/complete-auth.ts
143 lines · 4.5 KB · TypeScript/**
* Finish browser OpenID+PKCE login: exchange code, register device, persist audibleAuth.
*/
import { extractAuthorizationCode } from './login.ts'
import { registerDevice, toAudibleCliAuthJson } from './register.ts'
import { deleteAuthSession, loadAuthSession } from './auth-session.ts'
import { persistAudibleAuth } from './persist-auth.ts'
import { resolveLocale } from './locales.ts'
import { authPublicSummary, AUDIBLE_AUTH_SECRET } from './auth.ts'
import { packageStorage } from 'kody:runtime'
export type CompleteAuthInput = {
/** Final Amazon maplanding URL containing openid.oa2.authorization_code. */
redirectUrl: string
/** Session id from start-auth (preferred). */
authSessionId?: string
/** Fallback if authSessionId omitted: code_verifier from a parallel start. */
codeVerifier?: string
locale?: string
serial?: string
withUsername?: boolean
}
/**
* Complete browser re-auth: parse the maplanding redirect URL, register a
* virtual Audible device, and persist audible-cli-compatible auth JSON in
* packageStorage (`audible-auth-v1`) for ADP signing. Optional best-effort
* secretSet as `audibleAuth` for host naming only. Never returns secret values.
*
* @param input - redirectUrl required; authSessionId from start-auth preferred
* @returns ok + public authStatus (or setupUrl / reason when persist failed)
*
* @example
* import completeAuth from 'kody:@kentcdodds/audible/complete-auth'
*
* const result = await completeAuth({
* redirectUrl: 'https://www.amazon.com/ap/maplanding?...',
* authSessionId: 'abc123',
* })
*/
export default async function completeAuth(input: CompleteAuthInput) {
if (!input?.redirectUrl || typeof input.redirectUrl !== 'string') {
throw new Error('redirectUrl is required (paste the full maplanding URL).')
}
const authorizationCode = extractAuthorizationCode(input.redirectUrl)
let codeVerifier = input.codeVerifier?.trim() || ''
let serial = input.serial?.trim() || ''
let localeCode = input.locale?.trim() || 'us'
let withUsername = Boolean(input.withUsername)
let sessionId = input.authSessionId?.trim() || ''
if (sessionId) {
const session = await loadAuthSession(sessionId)
if (!session) {
throw new Error(
`Unknown or expired authSessionId "${sessionId}". Call start-auth again and use the new login URL.`,
)
}
codeVerifier = session.codeVerifier
serial = session.serial
localeCode = session.locale
withUsername = session.withUsername
} else if (!codeVerifier || !serial) {
// Try latest session pointer if present
const latest = await loadLatestSessionId()
if (latest) {
const session = await loadAuthSession(latest)
if (session) {
codeVerifier = session.codeVerifier
serial = session.serial
localeCode = session.locale
withUsername = session.withUsername
sessionId = latest
}
}
}
if (!codeVerifier) {
throw new Error(
'Missing PKCE code verifier. Pass authSessionId from start-auth (or codeVerifier + serial).',
)
}
if (!serial) {
throw new Error(
'Missing device serial. Pass authSessionId from start-auth (or serial).',
)
}
const locale = resolveLocale(localeCode)
const registered = await registerDevice({
authorizationCode,
codeVerifier,
locale,
serial,
withUsername,
})
const authJson = toAudibleCliAuthJson(registered)
const persisted = await persistAudibleAuth(authJson)
if (sessionId) await deleteAuthSession(sessionId)
if (!persisted.ok) {
return {
ok: false as const,
secretName: AUDIBLE_AUTH_SECRET,
authStatus: authPublicSummary(registered),
setupUrl: persisted.setupUrl,
hostsToApprove: persisted.hostsToApprove,
reason: persisted.reason,
message:
'Device registered, but packageStorage could not persist audible-auth-v1. Retry complete-auth or open the Connect tab again (do not paste auth JSON into chat).',
}
}
return {
ok: true as const,
secretName: persisted.secretName,
secretScope: persisted.scope,
authStatus: persisted.authStatus,
setupUrl: persisted.setupUrl,
hostsToApprove: persisted.hostsToApprove,
note: persisted.note,
message:
'Audible device registered; ADP signing auth saved in package storage. Call auth-status next; re-auth only if the device is invalidated.',
}
}
async function loadLatestSessionId(): Promise<string | null> {
try {
const raw = await packageStorage().get('auth-session:latest')
if (typeof raw === 'string' && raw.trim()) return raw.trim()
if (raw && typeof raw === 'object' && 'authSessionId' in (raw as object)) {
return String((raw as { authSessionId: string }).authSessionId)
}
} catch {
// ignore
}
return null
}