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

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ón

Lo 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

  1. Instalación
  2. Arquitectura del SDK
  3. ¿Cómo funciona el login? (en 1 minuto)
  4. Guía rápida (3 pasos)
  5. Conceptos clave
  6. Guía paso a paso
  7. Módulos y métodos
  8. Dónde se guardan los tokens
  9. Scopes disponibles
  10. Ejemplos por framework
  11. Manejo de errores
  12. Uso en el servidor
  13. Notas de seguridad
  14. Preguntas frecuentes
  15. TypeScript
  16. Licencia

1. Instalación

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

Requisitos 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 en modules/, recibe el contexto compartido (config, http, tokens) y se expone como cliente.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.    │
  1. Inicias el login → el SDK redirige al usuario a Lintted.
  2. Lintted devuelve al usuario a tu redirectUri con un code en la URL.
  3. Intercambias ese code por 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 locales

7. 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 son HttpOnly y no protegen contra XSS (igual que localStorage). Te dan persistencia y los flags Secure/SameSite, no inmunidad ante scripts maliciosos. Para protección real contra XSS, los tokens deben ir en cookies HttpOnly puestas por tu backend (ver HttpOnlyCookieStorage y 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. El code_verifier se genera con muestreo sin sesgo (conjunto unreserved de 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_device y refresh_token viajan en x-unique-device / x-refresh-token, no en la query string.
  • Cookies del cliente ≠ HttpOnly. createCookieStorage aplica Secure y SameSite=Strict, pero el JavaScript de la página puede leerlas. Para máxima protección frente a XSS, guarda los tokens en cookies HttpOnly emitidas por tu propio backend (usa HttpOnlyCookieStorage y 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