@lintted/accounts-sdk
v2.0.0
Published
SDK para el servicio OAuth de accounts.lintted.com
Readme
@lintted/accounts-sdk
SDK oficial de JavaScript / TypeScript para iniciar sesión con Lintted Accounts en tu aplicación.
Implementa el flujo estándar OAuth 2.0 Authorization Code + PKCE con una API modular y limpia:
const lintted = new LinttedAccounts({ clientId, redirectUri, scopes });
await lintted.auth.login(); // → autenticación
await lintted.users.getData(["email"]); // → datos del usuario
await lintted.sessions.validate(); // → sesiónLo que obtienes sin pelearte con los detalles:
- 🔐 Login seguro con PKCE (
S256) y protección CSRF integrada. - 🔄 Renovación automática del access token cuando caduca.
- 💾 Almacenamiento flexible de tokens (
sessionStorage, cookies o el tuyo propio). - 👤 Perfil del usuario por scopes y roles/permisos (RBAC).
- 🧩 Arquitectura modular fácil de extender (
auth,users,sessions…). - 🟦 Tipado completo en TypeScript, para navegador y servidor (Node, Next.js…).
Tabla de contenidos
- Instalación
- Arquitectura del SDK
- ¿Cómo funciona el login? (en 1 minuto)
- Guía rápida (3 pasos)
- Conceptos clave
- Guía paso a paso
- Módulos y métodos
- Dónde se guardan los tokens
- Scopes disponibles
- Ejemplos por framework
- Manejo de errores
- Uso en el servidor
- Notas de seguridad
- Preguntas frecuentes
- TypeScript
- Licencia
1. Instalación
npm install @lintted/accounts-sdk
# o
yarn add @lintted/accounts-sdk
# o
pnpm add @lintted/accounts-sdkRequisitos previos: necesitas un clientId y una redirectUri registrada en tu aplicación de Lintted Accounts. La redirectUri es la URL de tu app a la que Lintted devolverá al usuario tras el login (por ejemplo https://tuapp.com/callback).
2. Arquitectura del SDK
El SDK expone un único cliente (LinttedAccounts) que agrupa la funcionalidad en módulos. Tú llamas siempre a cliente.modulo.metodo():
new LinttedAccounts(options)
│
├── .auth Autenticación: login, callback, logout y estado de tokens
├── .users Datos del usuario: perfil por scopes y permisos (RBAC)
├── .sessions Sesión: validación y renovación del access token
└── .http Cliente HTTP de bajo nivel (uso avanzado / servidor)Por dentro, el paquete está organizado así:
src/
├── client/ → LinttedAccounts (cliente), HttpClient, config
├── modules/ → auth/ · users/ · sessions/ (cada feature, aislada)
├── errors/ → jerarquía de errores (LinttedError y derivados)
├── types/ → tipos públicos de TypeScript
├── utils/ → TokenStore, almacenamiento en cookies, helpers
└── index.ts → punto de entrada / exports públicos🧩 Añadir un módulo nuevo (p. ej.
organizations) es trivial: se crea enmodules/, recibe el contexto compartido (config, http, tokens) y se expone comocliente.organizations.
3. ¿Cómo funciona el login? (en 1 minuto)
El SDK usa el flujo Authorization Code + PKCE. Suena complejo, pero son solo 3 viajes:
┌─────────────┐ ┌──────────────────────┐
│ Tu app │ │ Lintted Accounts │
└──────┬──────┘ └──────────┬───────────┘
│ │
│ 1. lintted.auth.login() │
│ ───────────────────────────────────────────────► │
│ Redirige al usuario a la pantalla de login │
│ │
│ El usuario inicia sesión y acepta los permisos │
│ │
│ 2. Vuelve a tu redirectUri con ?code=...&state=... │
│ ◄─────────────────────────────────────────────── │
│ │
│ 3. lintted.auth.processCallbackUrl() │
│ ───────────────────────────────────────────────► │
│ Intercambia el code por tokens │
│ ◄─────────────────────────────────────────────── │
│ { access_token, refresh_token, unique_device } │
│ │
│ ✅ Usuario autenticado. Tokens guardados solos. │- Inicias el login → el SDK redirige al usuario a Lintted.
- Lintted devuelve al usuario a tu
redirectUricon uncodeen la URL. - Intercambias ese
codepor tokens → el SDK los guarda automáticamente.
4. Guía rápida (3 pasos)
import { LinttedAccounts } from "@lintted/accounts-sdk";
// Crea una instancia (normalmente una sola, compartida en toda la app)
const lintted = new LinttedAccounts({
clientId: "tu-client-id",
redirectUri: "https://tuapp.com/callback",
scopes: ["id", "email", "username"],
});
// PASO 1 — En el botón de "Iniciar sesión":
await lintted.auth.login(); // redirige al usuario a Lintted
// PASO 2 — En tu página de callback (la redirectUri):
await lintted.auth.processCallbackUrl();
// ✅ Ya estás autenticado; los tokens quedan guardados
// PASO 3 — En cualquier parte de tu app:
if (lintted.auth.isLoggedIn()) {
const user = await lintted.users.getData(["id", "email", "username"]);
console.log(user); // { id, email, username }
}5. Conceptos clave
| Concepto | Qué es |
| ----------------- | ------------------------------------------------------------------------------------------------------- |
| PKCE | Protege el intercambio del código. El SDK genera un code_verifier que nunca sale del navegador. Automático. |
| CSRF state | Valor aleatorio que el SDK guarda al iniciar el login y verifica al volver. Automático. |
| access_token | Token de corta duración (1 hora) que autoriza cada petición a la API. |
| refresh_token | Token de larga duración (15 días) para renovar el access token sin volver a loguear. |
| unique_device | Identificador del dispositivo que devuelve el login. Obligatorio en las llamadas posteriores (lo gestiona el SDK). |
💡 No tienes que manejar PKCE ni el CSRF state manualmente: el SDK los crea, los guarda y los valida solo.
6. Guía paso a paso
Paso 1 — Crear el cliente
Crea una única instancia de LinttedAccounts y reutilízala en toda tu app (por ejemplo en lib/lintted.ts).
// lib/lintted.ts
import { LinttedAccounts } from "@lintted/accounts-sdk";
export const lintted = new LinttedAccounts({
clientId: "tu-client-id",
redirectUri: "https://tuapp.com/callback",
scopes: ["id", "email", "username", "first_name"],
});Paso 2 — Iniciar el login
await lintted.auth.login(); // redirige automáticamente¿Prefieres controlar tú la navegación? Pide solo la URL:
const url = await lintted.auth.login(false);
router.push(url);Paso 3 — Procesar el callback
En la página correspondiente a tu redirectUri:
await lintted.auth.processCallbackUrl(); // valida el state e intercambia el code
window.location.href = "/dashboard";¿Quieres comprobar primero que la URL es un callback?
import { LinttedAccounts } from "@lintted/accounts-sdk";
if (LinttedAccounts.isCallbackUrl()) {
await lintted.auth.processCallbackUrl();
}Paso 4 — Usar la sesión
lintted.auth.isLoggedIn(); // boolean
const user = await lintted.users.getData(["id", "email"]);
const { roles, permissions } = await lintted.users.getPermissions();
const result = await lintted.sessions.validate(); // renueva el token si caducóPaso 5 — Cerrar sesión
await lintted.auth.logout(); // invalida la sesión en el servidor y borra los tokens locales7. Módulos y métodos
new LinttedAccounts(options)
| Opción | Tipo | Requerido | Por defecto | Descripción |
| ------------- | ------------- | :-------: | ----------------------------------- | -------------------------------------------- |
| clientId | string | ✅ | — | Client ID de tu app OAuth |
| redirectUri | string | ✅ | — | URL de callback registrada |
| scopes | string[] | ❌ | [] | Scopes a solicitar |
| clientName | string | ❌ | — | Nombre mostrado en la pantalla de consentimiento |
| authUrl | string | ❌ | endpoint de autorización de Lintted | Sobreescribe el endpoint de autorización |
| tokenUrl | string | ❌ | endpoint de tokens de Lintted | Sobreescribe el endpoint de tokens |
| baseUrl | string | ❌ | API de Lintted | Sobreescribe la URL base de la API |
| storage | StorageLike | ❌ | sessionStorage | Adaptador de almacenamiento de tokens |
| fetcher | typeof fetch| ❌ | cross-fetch | Implementación de fetch personalizada |
| timeoutMs | number | ❌ | 10000 | Timeout de las peticiones (ms) |
Lanza un Error si falta clientId o redirectUri. Método estático: LinttedAccounts.isCallbackUrl(url?) → boolean.
lintted.auth — Autenticación
| Método | Devuelve | Descripción |
| ------ | -------- | ----------- |
| login(redirect = true) | Promise<string> | Genera PKCE + state y redirige al login (o devuelve la URL si redirect es false). |
| processCallbackUrl(url?) | Promise<TokenResponse> | Lee la URL del callback, valida el state e intercambia el code por tokens. |
| handleCallback(code, state) | Promise<TokenResponse> | Igual, pero cuando ya tienes el code y el state por separado. |
| logout() | Promise<LogoutResponse> | Cierra la sesión en el servidor y limpia los tokens locales. |
| getAccessToken() | string \| null | Access token guardado. |
| getRefreshToken() | string \| null | Refresh token guardado. |
| getUniqueDevice() | string \| null | Identificador de dispositivo. |
| getStoredTokens() | StoredTokens \| null | Todos los tokens, o null. |
| isLoggedIn() | boolean | true si hay access token. |
| hasTokens() | boolean | true si hay cualquier token. |
| saveTokens(tokens) | void | Guarda tokens manualmente. |
| clearTokens() | void | Borra todos los tokens. |
lintted.users — Datos del usuario
| Método | Devuelve | Descripción |
| ------ | -------- | ----------- |
| getData(scopes) | Promise<UserDataResponse> | Campos del perfil para los scopes indicados. Reintenta una vez tras revalidar si recibe un 401. |
| getPermissions() | Promise<PermissionsResponse> | { roles, permissions, auth_version }. |
lintted.sessions — Sesión
| Método | Devuelve | Descripción |
| ------ | -------- | ----------- |
| validate() | Promise<SessionValidationResponse> | Valida la sesión y renueva el access token si el servidor emitió uno nuevo. |
lintted.http — Cliente de bajo nivel
Instancia de HttpClient para casos avanzados o uso en servidor (ver sección 12).
8. Dónde se guardan los tokens
El SDK guarda los tokens mediante un adaptador de almacenamiento. Tres opciones:
Opción A — sessionStorage (por defecto)
Sin configurar nada. Los tokens viven mientras la pestaña esté abierta.
const lintted = new LinttedAccounts({ clientId, redirectUri });Opción B — Cookies
Persisten entre pestañas y recargas, con flags Secure y SameSite.
import { LinttedAccounts, createCookieStorage } from "@lintted/accounts-sdk";
const lintted = new LinttedAccounts({
clientId: "tu-client-id",
redirectUri: "https://tuapp.com/callback",
storage: createCookieStorage({ secure: true, sameSite: "Strict" }),
});Duración por defecto: lintted_auth_token → 1 h · lintted_refresh_token y lintted_unique_device → 15 días.
⚠️ Seguridad: estas cookies se leen con JavaScript (
document.cookie), así que NO sonHttpOnlyy no protegen contra XSS (igual quelocalStorage). Te dan persistencia y los flagsSecure/SameSite, no inmunidad ante scripts maliciosos. Para protección real contra XSS, los tokens deben ir en cookiesHttpOnlypuestas por tu backend (verHttpOnlyCookieStoragey la sección de seguridad).
Opción C — Almacenamiento personalizado
Implementa la interfaz StorageLike (IndexedDB, AsyncStorage, memoria…):
import type { StorageLike } from "@lintted/accounts-sdk";
function createMemoryStorage(): StorageLike {
const data = new Map<string, string>();
return {
getItem: (key) => data.get(key) ?? null,
setItem: (key, value) => void data.set(key, value),
removeItem: (key) => void data.delete(key),
};
}
const lintted = new LinttedAccounts({
clientId,
redirectUri,
storage: createMemoryStorage(),
});La interfaz es mínima:
interface StorageLike {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}9. Scopes disponibles
Los scopes son los datos del usuario que tu app solicita. Se piden en el constructor (scopes: [...]) y se leen con lintted.users.getData([...]). Solo puedes leer scopes que el usuario autorizó en el login.
| Scope | Dato |
| --------------------- | -------------------------- |
| id | Identificador del usuario |
| first_name | Nombre |
| last_name | Apellido |
| username | Nombre de usuario / handle |
| email | Correo electrónico |
| gender | Género |
| profile_picture_url | URL del avatar |
| date_of_birth | Fecha de nacimiento |
| country | País |
| phone_number | Teléfono |
| state | Estado / provincia |
| city | Ciudad |
| postal_code | Código postal |
| address_line_1 | Dirección (línea 1) |
| address_line_2 | Dirección (línea 2) |
La lista completa también está disponible como constante:
import { OAUTH_SCOPES } from "@lintted/accounts-sdk";
// readonly ["id", "first_name", "last_name", ...]ℹ️ Para leer un scope nuevo que no pediste al hacer login, debes repetir el flujo de login incluyendo ese scope.
10. Ejemplos por framework
React (SPA con react-router)
// lib/lintted.ts
import { LinttedAccounts } from "@lintted/accounts-sdk";
export const lintted = new LinttedAccounts({
clientId: import.meta.env.VITE_CLIENT_ID,
redirectUri: `${window.location.origin}/callback`,
scopes: ["id", "email", "username", "first_name"],
});// LoginButton.tsx
import { lintted } from "./lib/lintted";
export function LoginButton() {
return <button onClick={() => lintted.auth.login()}>Iniciar sesión con Lintted</button>;
}// CallbackPage.tsx — ruta /callback
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { lintted } from "./lib/lintted";
export function CallbackPage() {
const navigate = useNavigate();
useEffect(() => {
lintted.auth
.processCallbackUrl()
.then(() => navigate("/dashboard"))
.catch((err) => console.error("Fallo de autenticación:", err));
}, []);
return <p>Autenticando…</p>;
}// Dashboard.tsx
import { useEffect, useState } from "react";
import { lintted } from "./lib/lintted";
export function Dashboard() {
const [user, setUser] = useState<Record<string, unknown> | null>(null);
useEffect(() => {
lintted.users.getData(["id", "email", "username"]).then(setUser);
}, []);
const handleLogout = async () => {
await lintted.auth.logout();
window.location.href = "/";
};
return (
<div>
<pre>{JSON.stringify(user, null, 2)}</pre>
<button onClick={handleLogout}>Cerrar sesión</button>
</div>
);
}Next.js (App Router)
En el cliente el uso es idéntico al de React. En el servidor, usa lintted.http para validar peticiones (ver sección 12).
// app/callback/page.tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { lintted } from "@/lib/lintted";
export default function Callback() {
const router = useRouter();
useEffect(() => {
lintted.auth.processCallbackUrl().then(() => router.replace("/dashboard"));
}, []);
return <p>Autenticando…</p>;
}JavaScript / HTML sin framework
<button id="login">Iniciar sesión</button>
<script type="module">
import { LinttedAccounts } from "https://esm.sh/@lintted/accounts-sdk";
const lintted = new LinttedAccounts({
clientId: "tu-client-id",
redirectUri: window.location.origin + "/callback.html",
scopes: ["id", "email"],
});
if (LinttedAccounts.isCallbackUrl()) {
await lintted.auth.processCallbackUrl();
window.location.href = "/";
}
document.getElementById("login").onclick = () => lintted.auth.login();
</script>11. Manejo de errores
Todos los errores del SDK heredan de LinttedError, así que puedes capturar la familia entera o un caso concreto.
import {
LinttedApiError,
SessionExpiredError,
NotAuthenticatedError,
OAuthCallbackError,
} from "@lintted/accounts-sdk";
try {
const user = await lintted.users.getData(["id", "email"]);
} catch (error) {
if (error instanceof SessionExpiredError) {
await lintted.auth.login(); // el refresh token caducó (>15 días)
} else if (error instanceof NotAuthenticatedError) {
await lintted.auth.login(); // no hay tokens guardados
} else if (error instanceof OAuthCallbackError) {
console.warn("El login no se completó:", error.message);
} else if (error instanceof LinttedApiError) {
console.error(`HTTP ${error.status}: ${error.body}`);
} else {
throw error;
}
}Jerarquía de errores:
| Clase | Cuándo ocurre |
| ----------------------- | ------------------------------------------------------------- |
| LinttedError | Clase base de todos los errores del SDK |
| LinttedApiError | La API respondió con un código no 2xx (tiene .status y .body; 408 = timeout) |
| InvalidStateError | El state del callback no coincide (posible CSRF) |
| OAuthCallbackError | El proveedor devolvió ?error=... (tiene .error y .description) |
| SessionExpiredError | El refresh token caducó — re-login necesario |
| NotAuthenticatedError | Se pidió una operación con sesión pero no hay tokens |
12. Uso en el servidor
El cliente funciona también en Node (builds ESM y CJS + fetch vía cross-fetch). Para validar peticiones entrantes en el servidor usa el cliente HTTP de bajo nivel, lintted.http, que acepta credenciales explícitas.
// app/api/me/route.ts (Next.js)
import { LinttedAccounts, LinttedApiError } from "@lintted/accounts-sdk";
import { NextRequest, NextResponse } from "next/server";
const lintted = new LinttedAccounts({
clientId: process.env.LINTTED_CLIENT_ID!,
redirectUri: process.env.LINTTED_REDIRECT_URI!,
});
export async function GET(req: NextRequest) {
const accessToken = req.headers.get("authorization")?.split(" ")[1];
const uniqueDevice = req.headers.get("x-unique-device");
const refreshToken = req.headers.get("x-refresh-token");
if (!accessToken || !uniqueDevice || !refreshToken) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
}
try {
const session = await lintted.http.post(
"/authorization/validate/session",
{ unique_device: uniqueDevice, refresh_token: refreshToken },
{
accessToken,
headers: {
"x-unique-device": uniqueDevice,
"x-refresh-token": refreshToken,
},
},
);
return NextResponse.json(session);
} catch (err) {
const status = err instanceof LinttedApiError ? err.status : 500;
return NextResponse.json({ error: "Sesión inválida" }, { status });
}
}En el navegador no necesitas esto:
lintted.sessions.validate()lee los tokens del almacenamiento y renueva el access token automáticamente.
13. Notas de seguridad
- PKCE (
S256) siempre activo. Elcode_verifierse genera con muestreo sin sesgo (conjuntounreservedde RFC 7636) y nunca sale del cliente. - CSRF state generado con
crypto.randomUUID()y validado en cada callback. - Tokens en cabeceras, no en URLs.
unique_deviceyrefresh_tokenviajan enx-unique-device/x-refresh-token, no en la query string. - Cookies del cliente ≠
HttpOnly.createCookieStorageaplicaSecureySameSite=Strict, pero el JavaScript de la página puede leerlas. Para máxima protección frente a XSS, guarda los tokens en cookiesHttpOnlyemitidas por tu propio backend (usaHttpOnlyCookieStoragey emite/lee las cookies en el servidor). - Caducidades. access token: 1 h · refresh token: 15 días. Un refresh token caducado exige re-login completo.
14. Preguntas frecuentes
¿Tengo que gestionar yo PKCE o el CSRF state? No. El SDK los crea, los guarda y los valida automáticamente.
¿Cuándo se renueva el access token?
Al llamar a lintted.sessions.validate(), o automáticamente cuando una operación de users/auth recibe un 401: el SDK revalida y reintenta una vez.
El usuario recarga la página y pierde la sesión. ¿Por qué?
Con sessionStorage (por defecto) los tokens viven solo en esa pestaña. Usa createCookieStorage o un storage propio para persistir.
¿Funciona en el servidor (SSR / API)?
Sí. Usa lintted.http con credenciales explícitas (ver sección 12).
¿Puedo usar mi propia función fetch?
Sí: new LinttedAccounts({ ..., fetcher: miFetch }).
¿Puedo añadir mis propios módulos?
La arquitectura está pensada para ello: cada módulo recibe el contexto compartido (config, http, tokens) y se expone como cliente.miModulo.
15. TypeScript
El paquete está escrito en TypeScript e incluye todos los tipos. Convención: interfaces en PascalCase; las claves JSON que viajan al backend (access_token, refresh_token, unique_device, client_id…) permanecen en snake_case porque son el contrato HTTP.
import type {
LinttedAccountsOptions,
StorageLike,
HttpClientOptions,
TokenResponse,
StoredTokens,
SessionValidationResponse,
UserDataResponse,
PermissionsResponse,
LogoutResponse,
OAuthScope,
} from "@lintted/accounts-sdk";
import { OAUTH_SCOPES } from "@lintted/accounts-sdk"; // valor (constante)Los tipos también están disponibles en el subpath @lintted/accounts-sdk/types.
Formas principales:
interface TokenResponse {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
unique_device: string;
}
interface SessionValidationResponse {
message: string;
session: Record<string, unknown>;
renewed: boolean;
auth_token?: string; // solo presente cuando renewed === true
}
interface PermissionsResponse {
roles: string[];
permissions: string[];
auth_version: number;
}16. Licencia
MIT © Lintted
