/**
* Device login flow and API key management. The device endpoints work
* anonymously; the API key management endpoints require an account JWT
* (API keys cannot manage API keys).
*/
import type { Client } from './client.js';
import { isApiError } from './errors.js';
import * as http from './http.js';
import type { Body, Res } from './types.js';
/** POST /auth/device/start — Start a device login. Anonymous allowed. */
export const deviceStart = (client: Client) => http.post(client, '/auth/device/start');
/** POST /auth/device/poll — Poll a device login. Anonymous allowed. */
export const devicePoll = (client: Client, deviceCode: string) =>
http.post(client, '/auth/device/poll', { body: { device_code: deviceCode } });
/** POST /auth/device/complete — Complete a device login. Anonymous allowed. */
export const deviceComplete = (client: Client, deviceCode: string, confirmationCode: string) =>
http.post(client, '/auth/device/complete', {
body: { device_code: deviceCode, confirmation_code: confirmationCode },
});
/** GET /auth/api-keys — List API keys (account JWT only). */
export const listApiKeys = (client: Client) => http.get(client, '/auth/api-keys');
/** POST /auth/api-keys — Create an API key (account JWT only). */
export const createApiKey = (client: Client, body: Body<'/auth/api-keys', 'post'>) =>
http.post(client, '/auth/api-keys', { body });
/** POST /auth/api-keys/{id}/revoke — Revoke an API key (account JWT only). */
export const revokeApiKey = (client: Client, id: string) =>
http.post(client, '/auth/api-keys/{id}/revoke', { params: { id } });
export type DeviceStart = Res<'/auth/device/start', 'post'>;
export type DeviceLoginOptions = Readonly<{
/** Called with the start response so a person can approve in a browser; default prints instructions. */
onPrompt?: (start: DeviceStart) => void | Promise<void>;
/** Returns the confirmation code shown after approval; default reads a line from stdin. */
confirm?: (start: DeviceStart) => string | Promise<string>;
/** Poll interval; defaults to the server-suggested `interval` (seconds). */
intervalMs?: number;
/** Overall deadline; defaults to the server's `expires_in`. */
timeoutMs?: number;
}>;
const defaultPrompt = (start: DeviceStart): void => {
console.log(`To sign in, open: ${start.verification_uri_complete ?? start.verification_uri}`);
console.log(`Code: ${start.user_code}`);
};
const defaultConfirm = async (): Promise<string> => {
const { createInterface } = await import('node:readline/promises');
const rl = createInterface({ input: process.stdin, output: process.stdout });
try {
return (await rl.question('Enter the confirmation code shown after approving: ')).trim();
} finally {
rl.close();
}
};
/**
* Run the whole device login flow: start, prompt, poll until approved,
* complete. Returns the completion body; feed its `access_token` to
* `createClient` as `apiKey`. Throws a timeout error when the login is not
* approved in time.
*/
export const deviceLogin = async (client: Client, options: DeviceLoginOptions = {}) => {
const start = await deviceStart(client);
const { onPrompt = defaultPrompt, confirm = defaultConfirm } = options;
const intervalMs = options.intervalMs ?? 1000 * (start.interval ?? 5);
const timeoutMs = options.timeoutMs ?? 1000 * (start.expires_in ?? 600);
// While the user hasn't approved, the API answers 400 authorization-pending;
// 429 slow-down asks for a longer interval (pollUntil's backoff grows it).
// Anything else (401 expired, 403 denied) is terminal and rethrown.
const poll = async (): Promise<{ approved: boolean }> => {
try {
return await devicePoll(client, start.device_code);
} catch (e) {
if (isApiError(e) && (e.status === 400 || e.status === 429)) return { approved: false };
throw e;
}
};
await onPrompt(start);
await http.pollUntil(poll, (r) => r.approved, {
intervalMs,
timeoutMs,
message: 'Device login timed out before approval',
sleep: client.sleep,
});
return deviceComplete(client, start.device_code, await confirm(start));
};