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

@hablala/client

v0.5.0

Published

SDK headless de Hablalá — cliente tipado de la Data API + helpers de loader para React Router.

Readme

@hablala/client

SDK headless de Hablalá — el cliente oficial de la Data API para tu frontend.

Hablalá es un CMS + Data API: tú modelas tus objects (tu modelo de datos) y editas contenido dentro de Hablalá; el frontend lo escribes tú, en tu propio proyecto React Router. Este paquete te da las "baterías incluidas" para conectarte: un cliente tipado sobre la Data API y helpers de loader.

Cero componentes de UI. El diseño es 100% tuyo. Este SDK solo mueve datos.

Arquitectura de la plataforma: apps/api/ARCHITECTURE.md. Contrato API-first (single source of truth): apps/api/API_FIRST.md.


Instalación

npm install @hablala/client

Peer dependency opcional: react-router (v8) si usas los helpers de loader. El cliente en sí es agnóstico y funciona en cualquier runtime con fetch.

Quickstart

import { createHablalaClient } from "@hablala/client";

const hablala = createHablalaClient({
  token: "sfpk_…", // storefront token — basta con esto (ver abajo)
  // endpoint es opcional (default https://api.hablala.com, host raíz SIN /v1).
});

const { data } = await hablala.query({
  object: "obra",
  where: [{ attr: "estado", op: "eq", value: "en_progreso" }],
  include: [{ relationship: "cliente_obra", select: ["nombre"] }],
  order_by: [{ attr: "fecha_entrega", dir: "asc" }],
});

// data: [{ id: "rec_…", properties: { nombre, estado, cliente_obra: [...] } }]

Un token, cero configuración. No pasas organizationId: la organización se infiere del propio token. Una credencial es una identidad completa (el estándar de la industria) — el token porta su tenant, no falsificable. La postura (público/privado) también sale del prefijo del token, así que tampoco la configuras.

Tipos por tenant (la mejor DX) — hablala codegen

Sin más, query() acepta cualquier slug y properties es genérico. Para tener autocompletado de tus objetos/atributos e inferencia del tipo de respuesta, genera los tipos de tu tenant con el CLI de la plataforma (@hablala/cli) — el patrón estándar de type-generation desde el esquema:

HABLALA_STOREFRONT_TOKEN=sfpk_… npx @hablala/cli codegen
# → escribe hablala.types.ts con `interface HablalaSchema`

Luego parametriza el cliente con el esquema generado:

import { createHablalaClient } from "@hablala/client";
import type { HablalaSchema } from "./hablala.types";

const hablala = createHablalaClient<HablalaSchema>({
  token: "sfpk_…", // la org sale del token; endpoint opcional
});

const { data } = await hablala.query({
  object: "obra", // ← autocompleta los objetos de tu tenant
  select: ["nombre", "estado"], // ← autocompleta los atributos de 'obra'
  where: [{ attr: "estado", op: "eq", value: "en_progreso" }], // ← 'attr' tipado
});

data[0].properties.nombre; // ✅ string — inferido de tu dataType
data[0].properties.no_existe; // ✗ error de compilación

Regenera hablala.types.ts cada vez que cambies tu modelo de datos (idealmente en CI o en un script postinstall/predev). El fichero está marcado como generado; no lo edites a mano. Puedes pasarle prettier si quieres.

Opciones del CLI: --token (requerido; o HABLALA_STOREFRONT_TOKEN), --endpoint (opcional; o HABLALA_ENDPOINT) y --out. No hay --org: la organización se infiere del token, igual que en el cliente.

Tokens: público vs privado (⚠️ léelo)

Un storefront token es una credencial read-only de máquina — nunca un usuario. Solo lee el contenido que tus policies ABAC marcan como publicado. Hay dos posturas:

| | Prefijo | Dónde va | Exposición | | ----------- | -------- | ---------------------------------- | ----------------- | | Public | sfpk_… | En el browser / bundle del cliente | Seguro de exponer | | Private | sfpr_… | Solo en el servidor (loader SSR) | Secreto |

El error más común: meter el token privado en el bundle del cliente. No lo hagas. El private token solo debe leerse de env vars server-side (process.env.…), nunca de una env VITE_…/pública. Si se filtra, revócalo en el panel y genera otro.

Ambos son read-only, así que exponer el public no da acceso de escritura ni a contenido no publicado. La distinción es de exposición, no de permisos.

Uso con React Router (SSR + hidratación)

getHablalaClient elige el token correcto según el entorno: privado en el servidor, público en el cliente. Escribe el loader una vez y corre en ambos lados sin filtrar el token privado.

// app/lib/hablala.ts — compartido por loaders cliente y servidor
import { getHablalaClient, type HablalaConfig } from "@hablala/client";

const config: HablalaConfig = {
  publicToken: import.meta.env.VITE_HABLALA_PUBLIC_TOKEN, // seguro en el bundle
  // endpoint opcional (default https://api.hablala.com). La org sale del token.
  // El privado NO lleva prefijo VITE_ → nunca acaba en el bundle del cliente.
  privateToken: typeof process !== "undefined" ? process.env.HABLALA_PRIVATE_TOKEN : undefined,
};

export const getClient = () => getHablalaClient(config);
// app/routes/obras.tsx
import { getClient } from "~/lib/hablala";

export async function loader() {
  const hablala = getClient(); // privado en SSR, público al navegar en cliente
  return hablala.query({
    object: "obra",
    where: [{ attr: "estado", op: "eq", value: "en_progreso" }],
    order_by: [{ attr: "fecha_entrega", dir: "asc" }],
  });
}

export default function Obras({ loaderData }: { loaderData: Awaited<ReturnType<typeof loader>> }) {
  // Tu diseño, tu código. El SDK no opina.
  return (
    <ul>
      {loaderData.data.map((obra) => (
        <li key={obra.id}>{String(obra.properties.nombre)}</li>
      ))}
    </ul>
  );
}

Consultas

La consulta lógica usa slugs de tu diccionario (los que definiste en tu modelo de datos). Campos disponibles:

  • object (requerido) — slug del objeto raíz.
  • select — slugs a devolver (default: todos los permitidos por las policies).
  • where — condiciones AND. Operadores: eq, neq, gt, gte, lt, lte, in, contains, is_null, is_not_null. Para OR: { or: [ ...condiciones... ] }.
  • include — expansión de relaciones (un nivel).
  • order_by[{ attr, dir: "asc" | "desc" }].
  • limit (máx 200), offset.
  • paginate + cursor — paginación keyset (ver abajo).
  • semantic — búsqueda vectorial: { query, top_k } (combinable con where).

Paginación (keyset)

Manual, con cursor:

let cursor: string | null | undefined;
do {
  const page = await hablala.query({ object: "obra", paginate: true, cursor });
  render(page.data);
  cursor = page.nextCursor; // null en la última página
} while (cursor);

O automática con queryAll (async iterator — no llevas el cursor a mano):

for await (const page of hablala.queryAll({ object: "obra" })) {
  for (const obra of page) render(obra);
}

Búsqueda semántica

await hablala.query({
  object: "obra",
  where: [{ attr: "estado", op: "eq", value: "en_progreso" }], // filtra primero
  semantic: { query: "torres residenciales cerca de la costa", top_k: 5 }, // rankea después
});

Errores

query() lanza HablalaQueryError con un code accionable y, cuando aplica, una sugerencia por sinónimos:

import { HablalaQueryError } from "@hablala/client";

try {
  await hablala.query({ object: "obra", where: [{ attr: "nombre", op: "eq", value: "x" }] });
} catch (err) {
  if (err instanceof HablalaQueryError) {
    err.code; // "unknown_attribute" | "forbidden" | "unauthorized" | …
    err.didYouMean; // ["titulo"]  ← sugerencia por synonyms
    err.status; // status HTTP (0 si fue error de red)
  }
}

Códigos: unknown_object, unknown_attribute, unknown_relationship, type_mismatch, forbidden, limit_exceeded, unauthorized, not_found, invalid_query, rate_limited, network_error, unknown.

En vez de comparar códigos a mano, usa los helpers semánticos:

if (err.isAuthError()) rotateToken(); // 401
if (err.isForbidden()) showNotPublished(); // 403 / ABAC
if (err.isRateLimited()) wait(err.retryAfter); // 429 (retryAfter en segundos)
if (err.isQueryError()) fixQuery(); // slug/tipo/límite
if (err.isNetworkError()) retryLater(); // sin respuesta HTTP

Un forbidden casi siempre significa que no hay una policy allow read que exponga ese contenido al principal storefront — revisa que esté publicado y que exista la policy ABAC correspondiente en tu tenant.

Resiliencia: reintentos y hooks

Por defecto el cliente reintenta 2 veces los fallos transitorios (red, 429, 5xx) con backoff exponencial + jitter, respetando Retry-After. Configúralo:

const hablala = createHablalaClient({
  // ...
  retries: 3, // 0 para desactivar
  debug: true, // loguea cada request/response por console.debug
  hooks: {
    beforeRequest: (req) => new Request(req, { headers: { ...h(req), "x-app": "web" } }),
    afterResponse: (res, req) => metrics.record(req.url, res.status),
  },
});

Ejemplo end-to-end: constructora

Modelas tu modelo de datos en Hablalá (sin migraciones): objeto obra con atributos nombre, presupuesto (number, indexed), estado (select), fecha_entrega (date), relación obra → cliente_obra. Marcas como publicadas las obras a mostrar (una policy ABAC allow read para storefront filtrada por estado/visibilidad). Luego, en tu frontend:

export async function loader() {
  const hablala = getClient();
  const [obras, clientes] = await Promise.all([
    hablala.query({
      object: "obra",
      where: [{ attr: "estado", op: "eq", value: "en_progreso" }],
      include: [{ relationship: "cliente_obra", select: ["nombre"] }],
      order_by: [{ attr: "fecha_entrega", dir: "asc" }],
      limit: 20,
    }),
    hablala.query({ object: "cliente_obra", order_by: [{ attr: "nombre", dir: "asc" }] }),
  ]);
  return { obras: obras.data, clientes: clientes.data };
}

El diseño de la página lo escribes tú. Hablalá decide qué dice; tu código decide cómo se ve.


API

  • createHablalaClient<S>(options): HablalaClient<S> — cliente con un token fijo. Parametrízalo con el HablalaSchema de hablala codegen para tipos por tenant.
  • getHablalaClient<S>(config): HablalaClient<S> — cliente con postura por entorno (privado en SSR, público en cliente).
  • hablalaLoader(config, run) — azúcar: crea el cliente y ejecuta run(client).
  • client.query(logicalQuery) — ejecuta una consulta (respuesta inferida si hay S).
  • client.queryAll(logicalQuery) — async iterator que auto-pagina.
  • HablalaQueryError — error tipado con code / didYouMean / status / retryAfter y helpers isAuthError() / isForbidden() / isRateLimited() / isQueryError() / isNetworkError().

CLI: npx @hablala/cli codegen — genera hablala.types.ts (introspección del esquema de tu tenant). El bin hablala vive en @hablala/cli (un solo CLI de marca, con subcomandos); este paquete es librería pura.

Todos los tipos crudos del contrato (paths, components, operations) se reexportan por si los necesitas.