@lintted/accounts-sdk
v1.0.1
Published
SDK para el servicio OAuth de accounts.lintted.com
Downloads
157
Readme
@lintted/accounts-sdk
SDK oficial de JavaScript/TypeScript para el servicio de autenticación OAuth2 de Lintted. Proporciona una implementación completa del flujo Authorization Code + PKCE con protección CSRF integrada, almacenamiento de tokens flexible y un cliente API completo para el backend.
Tabla de Contenidos
- Instalación
- Inicio Rápido
- Conceptos Clave
- LinttedAuth — Cliente Frontend
- AccountsBackend — Cliente API
- Almacenamiento de Tokens
- Scopes de OAuth
- Tipos de TypeScript
- Ejemplos de Integración Completos
- Manejo de Errores
- Notas de Seguridad
- Licencia
Instalación
npm install @lintted/accounts-sdk
# o
yarn add @lintted/accounts-sdk
# o
pnpm add @lintted/accounts-sdkInicio Rápido
import { LinttedAuth } from "@lintted/accounts-sdk";
// 1. Inicializar el cliente
const auth = new LinttedAuth({
client_id: "tu-client-id",
redirect_uri: "https://tuapp.com/callback",
scopes: ["id", "email", "username"],
});
// 2. Redirigir al usuario a la página de login de Lintted
await auth.login();
// 3. En la página de callback, intercambiar el código por tokens
const params = new URLSearchParams(window.location.search);
const tokens = await auth.handle_callback(
params.get("code")!,
params.get("state")!,
);
// tokens: { access_token, refresh_token, unique_device }
// 4. Verificar el estado de autenticación en cualquier parte de la app
if (auth.isLoggedIn()) {
const accessToken = auth.getAccessToken();
}
// 5. Validar la sesión en el servidor
const session = await auth.backend.validate_session(accessToken, {
unique_device: auth.getUniqueDevice()!,
refresh_token: auth.getRefreshToken()!,
});Conceptos Clave
| Concepto | Descripción | | ----------------- | ---------------------------------------------------------------------------------------------------------------- | | PKCE | Par de code verifier/challenge generado en cada login para prevenir ataques de interceptación de código | | CSRF State | UUID aleatorio almacenado en sesión y verificado en el callback para prevenir ataques de falsificación de origen | | unique_device | Identificador opaco de dispositivo devuelto en el login; requerido en todas las llamadas API posteriores | | refresh_token | Token de larga duración (15 días) usado para renovar access tokens sin necesidad de re-login | | access_token | Token Bearer de corta duración enviado en cada solicitud a la API |
LinttedAuth — Cliente Frontend
import { LinttedAuth } from "@lintted/accounts-sdk";Opciones del Constructor
const auth = new LinttedAuth(options: LinttedAuthOptions);| Opción | Tipo | Requerido | Por defecto | Descripción |
| -------------- | ------------- | --------- | --------------------------------------------------------------------- | ----------------------------------------------------- |
| client_id | string | ✅ | — | Client ID de tu aplicación OAuth |
| redirect_uri | string | ✅ | — | URL de callback registrada en tu aplicación |
| scopes | string[] | ❌ | [] | Lista de scopes OAuth a solicitar |
| client_name | string | ❌ | — | Nombre de la app mostrado en la pantalla de consentimiento |
| auth_url | string | ❌ | https://accounts.lintted.com/allow/connection | Sobreescribe el endpoint de autorización |
| token_url | string | ❌ | https://accounts.lintted.com/api/oauth/authorization/validate/token | Sobreescribe el endpoint de tokens |
| storage | StorageLike | ❌ | sessionStorage | Adaptador de almacenamiento personalizado para tokens |
Se aceptan tanto aliases en camelCase (
clientId,redirectUri) como en snake_case (client_id,redirect_uri) para todas las opciones.
Métodos
login(redirect?: boolean): Promise<string>
Genera un challenge PKCE y un estado CSRF, luego redirige al usuario a la página de autorización de Lintted.
// Redirigir automáticamente (por defecto)
await auth.login();
// Obtener la URL sin redirigir (p. ej. para navegación personalizada)
const url = await auth.login(false);
router.navigate(url);handle_callback(code: string, state: string): Promise<TokenResponse>
Valida el estado CSRF e intercambia el código de autorización por tokens. Los tokens se persisten automáticamente en el almacenamiento.
const tokens = await auth.handle_callback(code, state);
// { access_token, refresh_token, unique_device, token_type, expires_in }Lanza Error("Invalid CSRF state") si el parámetro state no coincide.
getAccessToken(): string | null
Devuelve el access token almacenado, o null si no está autenticado.
getRefreshToken(): string | null
Devuelve el refresh token almacenado, o null.
getUniqueDevice(): string | null
Devuelve el identificador único de dispositivo almacenado, o null.
getStoredTokens(): Record<string, any> | null
Devuelve todos los tokens almacenados como un objeto { access_token, refresh_token, unique_device }, o null si no existe ninguno.
isLoggedIn(): boolean
Devuelve true si hay un access token en el almacenamiento.
hasTokens(): boolean
Devuelve true si existe cualquier token (access, refresh o device ID) en el almacenamiento.
saveTokens(tokens: Record<string, any>): void
Persiste tokens manualmente en el almacenamiento. Útil al renovar el access token cuando validate_session devuelve renewed: true.
if (result.renewed && result.auth_token) {
auth.saveTokens({ access_token: result.auth_token });
}clearTokens(): void
Elimina todos los tokens de Lintted del almacenamiento. Úsalo para implementar el logout en el lado del cliente.
auth.clearTokens();backend: AccountsBackend
Acceso directo a la instancia de AccountsBackend asociada a este cliente.
AccountsBackend — Cliente API
La clase AccountsBackend es el cliente HTTP de bajo nivel para la API de cuentas de Lintted. Se instancia automáticamente como auth.backend, pero también puede usarse de forma independiente en un servidor.
import { AccountsBackend } from "@lintted/accounts-sdk";Opciones del Constructor
const backend = new AccountsBackend(options?: AccountsBackendOptions);| Opción | Tipo | Por defecto | Descripción |
| ----------- | -------------- | ---------------------------------------- | ------------------------------------------ |
| baseUrl | string | https://accounts.lintted.com/api/oauth | Sobreescribe la URL base de la API |
| fetcher | typeof fetch | cross-fetch | Implementación personalizada de fetch |
| timeoutMs | number | 10000 | Tiempo máximo de espera de la solicitud (ms) |
Métodos
validate_authorization(params): Promise<validate_token_response>
Intercambia un código de autorización por tokens (usado internamente por handle_callback).
const tokens = await backend.validate_authorization({
grant_type: "authorization_code",
code: "auth-code",
client_id: "tu-client-id",
redirect_uri: "https://tuapp.com/callback",
code_verifier: "pkce-verifier",
});
// { access_token, refresh_token, token_type, expires_in, unique_device }validate_session(accessToken, params): Promise<validate_session_response>
Valida la sesión actual. Si el access token ha expirado, el servidor lo renueva usando el refresh token.
const result = await backend.validate_session(accessToken, {
unique_device: "device-id",
refresh_token: "refresh-token",
});
if (result.renewed && result.auth_token) {
// Guardar el nuevo access token
auth.saveTokens({ access_token: result.auth_token });
}Campos de la respuesta:
| Campo | Tipo | Descripción |
| ------------ | --------- | ------------------------------------------------------------- |
| message | string | Estado: "Token renovado" o "Sesión válida" |
| session | object | Registro completo de sesión de la base de datos |
| renewed | boolean | true si el access token fue renovado |
| auth_token | string? | Nuevo access token — solo presente cuando renewed es true |
Errores:
401— Refresh token inválido o expirado (debe volver a hacer login)404— Sesión no encontrada
Nota: Solo el
access_tokense renueva en este endpoint. Si elrefresh_tokenexpira (tras 15 días), el usuario debe iniciar sesión nuevamente.
get_oauth_user_data(accessToken, params): Promise<oauth_user_data_response>
Obtiene los campos del perfil del usuario para los scopes solicitados.
const user = await backend.get_oauth_user_data(accessToken, {
scopes: ["id", "email", "username", "first_name"],
unique_device: "device-id",
refresh_token: "refresh-token",
});
// { id, email, username, first_name, ... }logout(accessToken, params): Promise<logout_response>
Invalida la sesión en el servidor.
const result = await backend.logout(accessToken, {
unique_device: "device-id",
refresh_token: "refresh-token",
});
// { message: "..." }get_permissions(accessToken, params): Promise<get_permissions_response>
Obtiene los roles y permisos del usuario.
const perms = await backend.get_permissions(accessToken, {
unique_device: "device-id",
refresh_token: "refresh-token",
});
// { roles: [...], permissions: [...], auth_version: 1 }Almacenamiento de Tokens
sessionStorage (por defecto)
Los tokens se almacenan en sessionStorage por defecto. Se eliminan automáticamente cuando se cierra la pestaña del navegador.
Almacenamiento en Cookies
Usa createCookieStorage para persistir tokens en cookies del navegador con flags de seguridad.
import { LinttedAuth, createCookieStorage } from "@lintted/accounts-sdk";
const auth = new LinttedAuth({
client_id: "tu-client-id",
redirect_uri: "https://tuapp.com/callback",
storage: createCookieStorage({
expirationDays: 7, // por defecto: 7
secure: true, // por defecto: true — requiere HTTPS
sameSite: "Strict", // por defecto: "Strict"
}),
});| Opción | Tipo | Por defecto | Descripción |
| ---------------- | ----------------------------- | ----------- | -------------------------------------- |
| expirationDays | number | 7 | Días hasta que expiren las cookies |
| secure | boolean | true | Añade el flag Secure (solo HTTPS) |
| sameSite | "Strict" \| "Lax" \| "None" | "Strict" | Política SameSite de la cookie |
Almacenamiento Personalizado
Implementa la interfaz StorageLike para usar cualquier backend de almacenamiento (IndexedDB, AsyncStorage, en memoria, etc.):
import type { StorageLike } from "@lintted/accounts-sdk";
const miAlmacenamiento: StorageLike = {
getItem(key: string): string | null {
/* ... */
},
setItem(key: string, value: string): void {
/* ... */
},
removeItem(key: string): void {
/* ... */
},
};
const auth = new LinttedAuth({
client_id: "...",
redirect_uri: "...",
storage: miAlmacenamiento,
});Scopes de OAuth
Los siguientes scopes pueden solicitarse:
| Scope | Descripción |
| --------------------- | ------------------------------ |
| id | Identificador único 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 | Número de 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 |
import { OAUTH_SCOPES } from "@lintted/accounts-sdk/types";
// Todos los valores de scope disponibles como una tupla readonlyTipos de TypeScript
Todos los tipos se exportan desde el paquete:
import type {
LinttedAuthOptions,
StorageLike,
AccountsBackendOptions,
validate_token_params,
validate_token_response,
validate_session_params,
validate_session_response,
oauth_user_data_params,
oauth_user_data_response,
logout_params,
logout_response,
get_permissions_params,
get_permissions_response,
OAuthScope,
} from "@lintted/accounts-sdk";Ejemplos de Integración Completos
React SPA
// auth.ts — instancia singleton
import { LinttedAuth } from "@lintted/accounts-sdk";
export const auth = new LinttedAuth({
client_id: import.meta.env.VITE_CLIENT_ID,
redirect_uri: `${window.location.origin}/callback`,
scopes: ["id", "email", "username", "first_name", "last_name"],
});// LoginButton.tsx
import { auth } from "./auth";
export function LoginButton() {
return (
<button onClick={() => auth.login()}>Iniciar sesión con Lintted</button>
);
}// CallbackPage.tsx
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { auth } from "./auth";
export function CallbackPage() {
const navigate = useNavigate();
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const state = params.get("state");
if (code && state) {
auth
.handle_callback(code, state)
.then(() => navigate("/dashboard"))
.catch((err) => console.error("Error de autenticación:", err));
}
}, []);
return <p>Autenticando...</p>;
}// Dashboard.tsx
import { useEffect, useState } from "react";
import { auth } from "./auth";
export function Dashboard() {
const [user, setUser] = useState(null);
useEffect(() => {
const tokens = auth.getStoredTokens();
if (!tokens) return;
auth.backend
.get_oauth_user_data(tokens.access_token, {
scopes: ["id", "email", "username"],
unique_device: tokens.unique_device,
refresh_token: tokens.refresh_token,
})
.then(setUser);
}, []);
const handleLogout = async () => {
const tokens = auth.getStoredTokens();
if (tokens) {
await auth.backend.logout(tokens.access_token, {
unique_device: tokens.unique_device,
refresh_token: tokens.refresh_token,
});
}
auth.clearTokens();
window.location.href = "/";
};
return (
<div>
<pre>{JSON.stringify(user, null, 2)}</pre>
<button onClick={handleLogout}>Cerrar sesión</button>
</div>
);
}Next.js
// lib/auth.ts — cliente backend para el servidor
import { AccountsBackend } from "@lintted/accounts-sdk";
export const backend = new AccountsBackend();
export async function validateRequest(
accessToken: string,
uniqueDevice: string,
refreshToken: string,
) {
try {
return await backend.validate_session(accessToken, {
unique_device: uniqueDevice,
refresh_token: refreshToken,
});
} catch {
return null;
}
}// app/api/me/route.ts
import { validateRequest } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
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 });
}
const session = await validateRequest(
accessToken,
uniqueDevice,
refreshToken,
);
if (!session) {
return NextResponse.json({ error: "Sesión inválida" }, { status: 401 });
}
return NextResponse.json(session);
}Manejo de Errores
AccountsBackend lanza AccountsBackendError en respuestas no 2xx:
import { AccountsBackendError } from "@lintted/accounts-sdk";
try {
await auth.backend.validate_session(accessToken, {
unique_device,
refresh_token,
});
} catch (err) {
if (err instanceof AccountsBackendError) {
console.error(`HTTP ${err.status}: ${err.body}`);
if (err.status === 401) {
// Refresh token expirado — forzar re-login
auth.clearTokens();
await auth.login();
}
}
}LinttedAuth lanza objetos Error estándar:
| Mensaje | Causa |
| ----------------------------------------- | ---------------------------------------------------------- |
| "LinttedAuth: client_id is required" | Falta client_id en las opciones del constructor |
| "LinttedAuth: redirect_uri is required" | Falta redirect_uri en las opciones del constructor |
| "Invalid CSRF state" | El state no coincide en el callback — posible ataque CSRF |
Notas de Seguridad
- PKCE (
S256) siempre está activado — el code verifier nunca sale del cliente. - El estado CSRF es un UUID criptográfico generado con
crypto.randomUUID()y validado en cada callback. - El
unique_devicey elrefresh_tokense transmiten en cabeceras HTTP (x-unique-device,x-refresh-token) — no como parámetros de URL — para evitar que queden expuestos en los logs del servidor. - Al usar
createCookieStorage, las cookies se configuran conSecureySameSite=Strictpor defecto. - Los refresh tokens expiran tras 15 días. Un refresh token expirado requiere un re-login completo.
Licencia
MIT © Lintted
License
MIT © Lintted
