import { 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.', } }