npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

npm install @lintted/accounts-sdk
# o
yarn add @lintted/accounts-sdk
# o
pnpm add @lintted/accounts-sdk

Inicio 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_token se renueva en este endpoint. Si el refresh_token expira (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 readonly

Tipos 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_device y el refresh_token se 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 con Secure y SameSite=Strict por defecto.
  • Los refresh tokens expiran tras 15 días. Un refresh token expirado requiere un re-login completo.

Licencia

MIT © Lintted

License

MIT © Lintted