← Public packages
@danj/audible-auth
Locked Audible credential gateway: Connect, token refresh, and narrow Audible calls for other Kody packages.
src/complete-auth.ts
98 lines · 3.3 KB · TypeScriptimport { deleteAuthSession, loadAuthSession } from './auth-session.ts'
import { ACCESS_TOKEN_SECRET, ARCHIVE_ENABLED, REFRESH_TOKEN_SECRET } from './config.ts'
import { clearArchiveSigning, loadDevice, saveArchiveSigning, saveDevice } from './device.ts'
import { resolveLocale } from './locales.ts'
import { extractAuthorizationCode } from './login.ts'
import { registerDevice } from './register.ts'
import { saveSecret } from './secrets.ts'
import authStatus from './status.ts'
import { ACCESS_TOKEN_DESCRIPTION, deregisterDevice, REFRESH_TOKEN_DESCRIPTION } from './tokens.ts'
export type CompleteAuthInput = {
/** Final Amazon maplanding URL that contains openid.oa2.authorization_code. */
redirectUrl: string
/** Session id from start-auth. Defaults to the latest session. */
authSessionId?: string
}
/**
* Finish Connect: register a virtual Audible device, store its tokens as
* package secrets, and remove the previous device from the Amazon account.
* While NAS archive is off, the ADP token and private key are discarded. Never
* returns credentials.
*
* @param input - redirectUrl, and authSessionId from start-auth
* @returns ok, the previous device's fate, and the public status
*
* Internal: only the Connect page (`app.ts`) calls it.
*/
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)
const session = await loadAuthSession(input.authSessionId)
if (!session) {
throw new Error('Unknown or expired login session. Start Connect again.')
}
const locale = resolveLocale(session.locale)
const registered = await registerDevice({
authorizationCode,
codeVerifier: session.codeVerifier,
locale,
serial: session.serial,
withUsername: session.withUsername,
})
await deleteAuthSession(session.authSessionId)
// Uses the previous device's secrets, so it must run before they are replaced.
const previous = await loadDevice()
const previousDevice = previous
? (await deregisterDevice(previous))
? ('removed' as const)
: ('not-removed' as const)
: ('none' as const)
const { packageId } = await saveSecret({
name: REFRESH_TOKEN_SECRET,
value: registered.refreshToken,
description: REFRESH_TOKEN_DESCRIPTION,
expiresAt: null,
})
await saveSecret({
name: ACCESS_TOKEN_SECRET,
value: registered.accessToken,
description: ACCESS_TOKEN_DESCRIPTION,
expiresAt: new Date(registered.expires * 1000).toISOString(),
})
await saveDevice({
locale: locale.countryCode,
withUsername: session.withUsername,
serial: registered.serial,
deviceType: registered.deviceType,
customerId: registered.customerId,
deviceName: registered.deviceName,
connectedAt: new Date().toISOString(),
accessTokenExpiresAt: registered.expires,
...(packageId ? { packageId } : {}),
})
if (ARCHIVE_ENABLED) {
await saveArchiveSigning({
adpToken: registered.adpToken,
devicePrivateKey: registered.devicePrivateKey,
})
} else {
await clearArchiveSigning()
}
const status = await authStatus()
return {
ok: true as const,
previousDevice,
status,
message: status.ready
? 'Audible connected.'
: 'Audible connected. Approve the listed hosts before the app can use it.',
}
}