Guías
Ver como MarkdownExpress
La estrategia de Passport, el callback y el token ligado a la petición.
npm install @andco/sdk @andco/passport passport express-sessionSi tu aplicación ya usa Passport, @andco/passport te da la estrategia y no cambias nada más de tu flujo. Si no lo usa, la sección final muestra la integración directa.
Con Passport
import { createAndcoInstanceForServer } from "@andco/sdk/server";
export const andco = createAndcoInstanceForServer({
clientId: process.env.ANDCO_CLIENT_ID!,
clientSecret: process.env.ANDCO_CLIENT_SECRET!,
});La estrategia se registra de forma síncrona y sin I/O: la descripción que el servidor de autorización publica de sí mismo viene incorporada en el SDK, leída del servidor al construirlo.
import { AndCoStrategy } from "@andco/passport";
import passport from "passport";
import { andco } from "./andco.js";
passport.use(
"andco",
AndCoStrategy.create(
{
client: andco,
callbackURL: new URL("/auth/callback", process.env.APP_ORIGIN),
scope: ["email", "profile"],
},
(tokens, done) => {
const rest = andco.with(tokens.access_token).rest;
void rest.http.GET("/userinfo").then(({ data, error }) => {
if (error) return done(error);
done(null, { ...data, andco: { accessToken: tokens.access_token, refreshToken: tokens.refresh_token } });
});
},
),
);
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((serialized, done) => done(null, serialized as Express.User));Se registra una vez, al arrancar. Como es asíncrona, el archivo tiene que ser un módulo ES con await de nivel superior, o envolverse en el arranque de tu servidor.
Las rutas
import express from "express";
import session from "express-session";
import passport from "passport";
const app = express();
// If a proxy terminates TLS, this is what lets the cookie be marked secure.
app.set("trust proxy", 1);
app.use(session({ secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());
app.get("/auth/andco", passport.authenticate("andco", { scope: ["email", "profile"] }));
app.get(
"/auth/callback",
passport.authenticate("andco", { failureRedirect: "/?error=andco" }),
(_req, res) => res.redirect("/"),
);Ligar la credencial a la petición
import type { NextFunction, Request, Response } from "express";
import { andco } from "./andco.js";
export function withAndco(req: Request, res: Response, next: NextFunction) {
const user = req.user as { andco?: { accessToken: string } } | undefined;
if (!user?.andco) return res.status(401).json({ error: "unauthenticated" });
req.andco = andco.with(user.andco.accessToken);
next();
}app.get("/accounts", withAndco, async (req, res) => {
const { data, error } = await req.andco.rest.http.GET("/accounts");
if (error) return res.status(error.status ?? 500).json({ error: error.code });
res.json(data);
});with() devuelve un cliente nuevo y no toca la instancia compartida. Es lo que impide que la credencial de una petición aparezca en otra.
Sin Passport
app.get("/auth/andco", async (req, res) => {
const { data: request, error } = await andco.oauth.createAuthorizationRequest({
redirectTo: `${process.env.APP_ORIGIN}/auth/callback`,
scopes: ["openid", "email", "profile"],
});
if (error) return res.status(500).json({ error: error.code });
req.session.andcoTransaction = { ...request, authorizationUrl: request.authorizationUrl.href };
res.redirect(request.authorizationUrl.href);
});
app.get("/auth/callback", async (req, res) => {
const stored = req.session.andcoTransaction;
if (!stored) return res.status(400).json({ error: "missing_transaction" });
const { data: session, error } = await andco.oauth.exchangeCallback({
callbackUrl: new URL(req.originalUrl, process.env.APP_ORIGIN),
request: { ...stored, authorizationUrl: new URL(stored.authorizationUrl) },
});
delete req.session.andcoTransaction;
if (error) return res.status(400).json({ error: error.code });
req.session.andco = session;
res.redirect("/");
});Webhooks
La firma cubre los bytes crudos del cuerpo, así que la ruta del webhook necesita el cuerpo sin parsear:
import { ANDCO_WEBHOOK_HEADERS, andCoWebhookVerify } from "@andco/protocol";
import express from "express";
app.post("/webhooks/andco", express.raw({ type: "application/json" }), async (req, res) => {
const trusted = await andCoWebhookVerify({
body: (req.body as Buffer).toString("utf8"),
signature: req.header(ANDCO_WEBHOOK_HEADERS.signature),
eventId: req.header(ANDCO_WEBHOOK_HEADERS.eventId),
timestamp: req.header(ANDCO_WEBHOOK_HEADERS.timestamp),
secret: process.env.ANDCO_WEBHOOK_SECRET!,
});
if (!trusted) return res.status(400).json({ error: "invalid_signature" });
await handleEvent(JSON.parse((req.body as Buffer).toString("utf8")));
res.status(204).end();
});express.json() global rompe esto sin avisar: parsea el cuerpo antes de que lo veas y lo firmado deja de existir.
