Guías
Ver como MarkdownHono
Better Auth como camino principal, con el flujo OAuth manual como alternativa.
Hono corre en Node, Bun, Deno y en runtimes de borde. El SDK no usa APIs de Node, así que la misma integración sirve en todos.
Con Better Auth
@andco/better-auth conecta Andco a Better Auth reutilizando la configuración de protocolo del SDK — es el camino recomendado si no tienes ya una estrategia OAuth propia.
npm install @andco/sdk @andco/better-auth better-auth honoimport { createAndcoOAuthProvider } from "@andco/better-auth";
import { createAndcoInstanceForServer } from "@andco/sdk/server";
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
export const andco = createAndcoInstanceForServer({
clientId: process.env.ANDCO_CLIENT_ID!,
clientSecret: process.env.ANDCO_CLIENT_SECRET!,
});
export const auth = betterAuth({
database: myDatabaseAdapter,
plugins: [
genericOAuth({
config: [createAndcoOAuthProvider({ providerId: "andco", client: andco })],
}),
],
});El resto de las opciones — PAR automático, loginHint, authorizations por Resource Server — están en @andco/better-auth.
Montar las rutas
Better Auth expone un handler estándar (Request → Response); montarlo en Hono es una línea:
import { Hono } from "hono";
import { auth } from "./auth.js";
const app = new Hono();
app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw));Iniciar sesión, cerrar sesión y el callback quedan cubiertos por esas rutas — no escribes ninguna a mano.
Ligar la credencial a la petición
El token de Andco vive en la cuenta vinculada de Better Auth, no en su sesión. getAccessToken lo resuelve y lo renueva si hace falta:
import type { AndcoBoundClient } from "@andco/sdk";
import { createMiddleware } from "hono/factory";
import { andco, auth } from "../auth.js";
export const withAndco = createMiddleware<{ Variables: { andco: AndcoBoundClient } }>(async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) return c.json({ error: "unauthenticated" }, 401);
const accounts = await auth.api.listUserAccounts({ headers: c.req.raw.headers });
const andcoAccount = accounts.find((account) => account.providerId === "andco");
if (!andcoAccount) return c.json({ error: "no_andco_account" }, 401);
const { accessToken } = await auth.api.getAccessToken({
body: { accountId: andcoAccount.accountId, userId: session.user.id },
headers: c.req.raw.headers,
});
c.set("andco", andco.with(accessToken));
await next();
});app.get("/accounts", withAndco, async (c) => {
const { data, error } = await c.var.andco.rest.http.GET("/accounts");
if (error) return c.json({ error: error.code }, error.status ?? 500);
return c.json(data);
});Sin Better Auth
El flujo OAuth manual, para cuando no quieres la dependencia.
npm install @andco/sdk honoimport { createAndcoInstanceForServer } from "@andco/sdk/server";
export const andco = createAndcoInstanceForServer({
clientId: process.env.ANDCO_CLIENT_ID!,
clientSecret: process.env.ANDCO_CLIENT_SECRET!,
});Una sola instancia para todo el proceso. No guarda sesión: cada petición liga la suya.
Iniciar la autorización
import { Hono } from "hono";
import { getCookie, setCookie } from "hono/cookie";
import { andco } from "../andco.js";
export const auth = new Hono();
auth.get("/auth/andco", async (c) => {
const { data: request, error } = await andco.oauth.createAuthorizationRequest({
redirectTo: `${process.env.APP_ORIGIN}/auth/andco/callback`,
scopes: ["openid", "email", "profile"],
});
if (error) return c.json({ error: error.code }, 500);
setCookie(c, "andco_tx", JSON.stringify({ ...request, authorizationUrl: request.authorizationUrl.href }), {
httpOnly: true,
sameSite: "Lax",
secure: true,
maxAge: 600,
});
return c.redirect(request.authorizationUrl.href);
});La cookie de transacción lleva el verificador PKCE. No puede viajar en la URL y no puede reconstruirse: sin ella el callback trae un código que nadie puede canjear.
Recibir el callback
auth.get("/auth/andco/callback", async (c) => {
const raw = getCookie(c, "andco_tx");
if (!raw) return c.json({ error: "missing_transaction" }, 400);
const stored = JSON.parse(raw);
const { data: session, error } = await andco.oauth.exchangeCallback({
callbackUrl: new URL(c.req.url),
request: { ...stored, authorizationUrl: new URL(stored.authorizationUrl) },
});
setCookie(c, "andco_tx", "", { maxAge: 0 });
if (error) return c.json({ error: error.code }, 400);
await saveToYourSession(c, session);
return c.redirect("/");
});Ligar la credencial a la petición
Un middleware que deja el cliente ligado en el contexto evita repetir with() en cada ruta.
import type { AndcoBoundClient } from "@andco/sdk";
import { createMiddleware } from "hono/factory";
import { andco } from "../andco.js";
export const withAndco = createMiddleware<{ Variables: { andco: AndcoBoundClient } }>(async (c, next) => {
const session = await sessionFromYourStore(c);
if (!session) return c.json({ error: "unauthenticated" }, 401);
c.set("andco", andco.with(session));
await next();
});app.get("/accounts", withAndco, async (c) => {
const { data, error } = await c.var.andco.rest.http.GET("/accounts");
if (error) return c.json({ error: error.code }, error.status ?? 500);
return c.json(data);
});with() no muta la instancia compartida: devuelve un cliente nuevo. Por eso dos peticiones simultáneas no pueden mezclar credenciales.
Refrescar
Si ligas la sesión completa —y no solo el token— sigue sin renovarse sola: with() es una consulta directa al token, sin persistencia ni renovación (igual que en el flujo genérico del servidor). Renueva a mano y guarda el resultado, porque los refresh tokens rotan:
const { data: refreshed } = await andco.oauth.refresh(session.refreshToken!);
await saveToYourSession(c, refreshed);Webhooks
@andco/protocol trae los esquemas y el verificador de firma, así que tu manejador no reimplementa el formato:
import { ANDCO_WEBHOOK_HEADERS, andCoWebhookVerify } from "@andco/protocol";
app.post("/webhooks/andco", async (c) => {
const body = await c.req.text();
const trusted = await andCoWebhookVerify({
body,
signature: c.req.header(ANDCO_WEBHOOK_HEADERS.signature),
eventId: c.req.header(ANDCO_WEBHOOK_HEADERS.eventId),
timestamp: c.req.header(ANDCO_WEBHOOK_HEADERS.timestamp),
secret: process.env.ANDCO_WEBHOOK_SECRET!,
});
if (!trusted) return c.json({ error: "invalid_signature" }, 400);
await handleEvent(JSON.parse(body));
return c.body(null, 204);
});El cuerpo tiene que ser el texto crudo. Si lo parseas antes de verificar, lo que firmó Andco ya no existe: volver a serializarlo produce otros bytes y la comprobación falla.
