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.23.0

Published

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

Readme

@hablala/client

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

Hablalá es un aPaaS headless, metadata-driven: modelas tus objects (tu propio modelo de datos, cualquier dominio — tienda, clínica, escuela), los operas por API, y tu app corre sobre nuestra infraestructura. El vertical (commerce, crm…) es un package instalable, no la identidad de la plataforma: el core es horizontal. El frontend lo escribes tú, en tu propio proyecto React Router. Este paquete te da las "baterías incluidas": un cliente tipado que lee y escribe la Public API, cuentas de usuario final (auth + me) y helpers de loader.

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

De un vistazo (los slugs product/order son de ejemplo — usa los objects de tu modelo):

const hablala = createHablalaClient({ token: "pk_…" });

await hablala.query({ object: "product" });                      // leer (contenido publicado)
await hablala.create("product_review", { rating: 5 });           // escribir (canal público)
await hablala.auth.login({ email, password });                   // cuenta de usuario final
await hablala.me.query({ object: "order" });                     // "mis records" (auto-acotado)

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: "pk_…", // API key (publishable) — 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 (publishable/secret) también sale del prefijo de la key, 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_TOKEN=pk_… 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: "pk_…", // 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_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)

Una API key es la credencial de máquina de tu app — nunca un usuario. Es el principal public del canal público. Hay dos posturas (canónico Stripe/Clerk):

| | Prefijo | Dónde va | Exposición | | --------------- | ------- | ---------------------------------- | ----------------- | | Publishable | pk_… | En el browser / bundle del cliente | Seguro de exponer | | Secret | sk_… | Solo en el servidor (loader SSR) | Secreto |

El error más común: meter la secret key en el bundle del cliente. No lo hagas. La sk_… solo debe leerse de env vars server-side (process.env.…), nunca de una env VITE_…/pública. Si se filtra, revócala en el panel y genera otra.

La distinción publishable/secret es de exposición, no de permisos: ambas tienen el mismo alcance. Exponer la pk_… es seguro porque no da acceso a nada por sí misma — lo que se puede leer o escribir lo decide el ABAC (default-deny): solo el contenido publicado y solo los objetos que la app abre explícitamente al principal public. La key no es una llave maestra; es una identidad acotada por policies.

El end-user session token (sess_…) es distinto: es la sesión de un usuario final (no de la app), la emite auth.login/register, y el cliente la gestiona por ti (ver Cuentas de comprador). No lo pongas en env vars: nace y muere con la sesión del visitante.

Uso con React Router (SSR + hidratación)

getHablalaClient elige la key correcta según el entorno: la secret key en el servidor, la publishable key en el cliente. Escribe el loader una vez y corre en ambos lados sin filtrar la secret key.

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

const config: HablalaConfig = {
  publishableKey: import.meta.env.VITE_HABLALA_PUBLISHABLE_KEY, // seguro en el bundle
  // endpoint opcional (default https://api.hablala.com). La org sale de la key.
  // La secret key NO lleva prefijo VITE_ → nunca acaba en el bundle del cliente.
  secretKey: typeof process !== "undefined" ? process.env.HABLALA_SECRET_KEY : undefined,
};

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

export async function loader() {
  const hablala = getClient(); // secret key en SSR, publishable 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)
    err.docUrl; // doc de la clase de error (RFC 9457 `type`) — ábrela para resolver
  }
}

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 public — revisa que esté publicado y que exista la policy ABAC correspondiente en tu tenant.

Escritura (formularios, reseñas, capturas)

La tienda no solo lee: escribe de vuelta a la plataforma. Un formulario de contacto crea un lead; una reseña, un product_review. Verbos genéricos con el objeto como argumento (el estándar para un backend schemaless: el patrón de Sanity):

await hablala.create("product_review", { rating: 5, body: "Excelente" }); // → { id, properties }
await hablala.update("product_review", "rec_…", { body: "Editado" }); // merge parcial (PATCH)
await hablala.delete("product_review", "rec_…"); // soft-delete

Nada es escribible por defecto. La escritura pública la gobierna el ABAC: la marca abre cada objeto explícitamente con una policy allow/create para el principal public (en su Account as Code). Sin ella, recibes un 403 forbidden — así una app no puede escribir en objetos que no le corresponden (p. ej. product o customer). Es la misma disciplina que la lectura "publicada", aplicada a la escritura.

Cuentas de comprador (auth + me)

Para "mis pedidos", dejar una reseña como cliente, o un checkout con cuenta, el usuario final inicia sesión. El cliente es stateful: tras login/register guarda el end-user session token (sess_…) y lo auto-adjunta — no lo re-pasas (patrón de Medusa/Saleor/commercetools).

// Registro (crea el record del usuario final + su credencial) e inicio de sesión, en un paso:
await hablala.auth.register({
  email: "[email protected]",
  password: "…",
  properties: { first_name: "María" }, // properties adicionales del record del usuario final
});

// O login de un usuario final existente:
const session = await hablala.auth.login({ email: "[email protected]", password: "…" });
session.endUserId; // "rec_…" (el record del usuario final)

// Ya identificado: `me.*` va al canal auto-acotado — solo SUS records.
const { data: misPedidos } = await hablala.me.query({ object: "order" });
await hablala.me.create("product_review", { rating: 5, body: "Volveré a comprar" });
await hablala.me.update("customer", session.endUserId!, { phone: "+52…" });

await hablala.auth.logout(); // revoca la sesión en el server y la borra del storage

me.* está auto-acotado por el backend: el ABAC solo deja ver/tocar los records del propio usuario final (por principal.end_user_id), sin que tú filtres nada en el cliente. Un usuario final nunca ve los pedidos de otro.

Sesión anónima → merge al identificarse

El carrito/actividad antes de que el usuario final se identifique usa una sesión anónima; al hacer login/register se fusiona (merge) con su cuenta — el carrito no se pierde:

await hablala.auth.anonymous(); // token `sess_…` sin usuario final (el carrito de invitado)
// … el visitante añade cosas …
await hablala.auth.login({ email, password }); // la sesión anónima se identifica (merge)

Checkout nativo (me.checkout)

Hablalá es la fuente de verdad del pedido (no espejo de una tienda externa): el checkout es una transacción de dominio atómica server-side — valida stock por variante, numera el pedido (#1001), crea el order + sus renglones con snapshots de precio, y decrementa el inventario, todo en una transacción. El comprador sale de la sesión (sess_…), no del cuerpo.

El flujo completo: carrito client-side → (sesión anónima o cuenta) → me.checkout.

// El carrito lo llevas tú en el cliente (localStorage, un store, lo que sea).
// Al pagar, aseguras una sesión (anónima basta) y llamas al checkout:
await hablala.auth.anonymous(); // o auth.login() si el comprador tiene cuenta

const order = await hablala.me.checkout(
  {
    items: [
      { variantId: "rec_variante_talla_m", quantity: 2 },
      { variantId: "rec_variante_talla_l", quantity: 1 },
    ],
    currency: "MXN",
  },
  { idempotencyKey: cartId }, // header `Idempotency-Key` (Stripe): reintentos → un solo pedido
);

order.id; // "rec_…"
order.properties.order_number; // "#1001"
order.properties.financial_status; // "pending"
order.lineItems; // un renglón por artículo, con snapshots de título/sku/precio

Si una variante se quedó sin stock, el checkout responde 409 y nada se crea (rollback total). El SDK lo expone como un error accionable:

try {
  await hablala.me.checkout({ items, currency: "MXN" });
} catch (err) {
  if (err instanceof HablalaQueryError && err.isOutOfStock()) {
    // Refresca disponibilidad y marca "Agotado" en el line item; el pedido NO se creó.
  }
}

Dónde se guarda la sesión (storage)

Por defecto la sesión vive en memoria (una por instancia del cliente — ideal en SSR: cada request su comprador). Para persistirla, inyecta un storage:

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

// Browser SPA: la sesión sobrevive recargas.
const hablala = createHablalaClient({ token: "pk_…", auth: { storage: createLocalStorageStorage() } });

AuthStorage es un contrato mínimo (get/set/clear); en SSR con cookies, pasa un adaptador propio sobre tus cookies. auth.login() devuelve también el token por si prefieres gestionarlo tú.

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 public 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 (secret key en SSR, publishable 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.
  • client.create(object, properties) / client.update(object, id, properties) / client.delete(object, id) — escritura pública (canal public, gobernada por ABAC).
  • client.auth.register(input) / login(input) / anonymous() / logout() / token() — cuentas de usuario final (stateful; guarda el sess_… en el storage inyectable).
  • client.me.query/create/update/delete — superficie del usuario final identificado, auto-acotada por el backend a sus propios records (/me/*).
  • client.me.checkout(input, { idempotencyKey? }) — checkout nativo atómico: valida stock, numera, crea el pedido + renglones y decrementa inventario en una transacción server-side (/me/checkout).
  • createMemoryStorage() / createLocalStorageStorage(key?) — adaptadores de AuthStorage para la sesión. (memoryStorage/localStorageStorage son alias deprecados.)
  • HablalaQueryError — error tipado con code / didYouMean / status / retryAfter / docUrl 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.