CLI y dispositivos

Autenticar desde una terminal, con receptor loopback o con el flujo de dispositivo.

bash
npm install @andco/sdk

Una herramienta de terminal tiene dos caminos, y la diferencia es si la máquina que ejecuta el comando puede abrir un navegador.

Con navegador: receptor loopback

src/auth.tsts
import { AndcoCliPresenter, createAndcoInstanceForCLI } from "@andco/sdk/cli";

const andco = createAndcoInstanceForCLI({
  clientId: "tu-client-id",
  // El puerto 0 pide uno libre; el SDK pone en la autorización el que realmente obtuvo.
  redirectTo: "http://127.0.0.1:0/callback",
  initialScopes: ["openid", "email", "profile"],
  presenter: new AndcoCliPresenter({
    open: (url) => open(url.href),
  }),
});

const { data: session, error } = await andco.auth.signIn();

El presentador levanta un servidor loopback de un solo uso: acepta exactamente la ruta registrada, comprueba el state, responde una vez y cierra el puerto. Escribir eso a mano son unas noventa líneas y cada atajo —aceptar cualquier ruta, saltarse el state, dejar el puerto abierto— es un agujero de seguridad en la herramienta de alguien.

Si no le pasas open, imprime la URL para que la persona la pegue.

ts
new AndcoCliPresenter({
  print: (message) => console.log(message),
  timeoutMs: 5 * 60 * 1_000,
});

Guardar la sesión entre ejecuciones

El almacenamiento por omisión vive en memoria y desaparece al terminar el proceso. Entrega el tuyo para que la persona no vuelva a autenticarse en cada comando.

ts
import type { AndcoStorage } from "@andco/sdk";

const keychain: AndcoStorage = {
  async getItem(key) {
    return readFromKeychain(key);
  },
  async setItem(key, value) {
    await writeToKeychain(key, value);
  },
  async removeItem(key) {
    await deleteFromKeychain(key);
  },
};

const andco = createAndcoInstanceForCLI({ clientId, redirectTo, storage: keychain });

El almacenamiento puede ser asíncrono, así que un llavero del sistema operativo o un archivo cifrado sirven igual.

Si varios comandos pueden correr a la vez, entrega también un lock para que solo uno renueve el token: los refresh tokens rotan, y dos renovaciones simultáneas invalidan una a la otra.

Sin navegador: flujo de dispositivo

En un contenedor, por SSH o en un servidor de compilación no hay navegador que abrir. El flujo de dispositivo muestra un código que la persona escribe en otro equipo.

ts
const { data: device, error } = await andco.oauth.createDeviceAuthorizationRequest({
  scopes: ["openid", "email", "profile"],
});
if (error) throw error;

console.log(`Abre ${device.verificationUriComplete} y escribe el código ${device.userCode}`);

const { data: session } = await andco.oauth.awaitDeviceAuthorization(device);
await andco.session.set(session);

awaitDeviceAuthorization hace el sondeo completo: respeta el intervalo que pide el servidor, obedece los avisos de ir más lento y termina cuando la autorización se aprueba, se rechaza o vence. No escribas tu propio bucle.

Se puede cancelar:

ts
const controller = new AbortController();
const { data: session } = await andco.oauth.awaitDeviceAuthorization(device, { signal: controller.signal });

Llamar a la API

ts
const { data, error } = await andco.rest.http.GET("/accounts");
if (error) {
  console.error(error.code, error.message);
  process.exitCode = 1;
}

Si el error trae el permiso que faltaba, dilo: quien usa una terminal puede actuar sobre esa información.

ts
const missing = error?.details?.["required_scope"];
if (missing) console.error(`Falta el permiso ${String(missing)}. Vuelve a autenticarte pidiéndolo.`);