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

@solwed/mind-sdk

v0.8.2

Published

SDK unificado SOLWED — 69 clases (entities + auth), cliente HTTP Mind/Bridge, Zod schemas, logger, errores, utils y templates de email, todo con herencia y FS-compat

Readme

@solwed/mind-sdk

SDK unificado de SOLWED. Un solo paquete que contiene todo lo que mind, bridge, app y web-astro necesitan compartir: clases de dominio con herencia, validación Zod, cliente HTTP unificado, auth, logger, errores y helpers.

npm install @solwed/mind-sdk

Qué hay dentro

Stack completo con subpaths para tree-shake — frontend solo carga lo que importa.

Núcleo

| Módulo | Contenido | |--------|-----------| | @solwed/mind-sdk | Punto de entrada — SolwedClient + re-exports universales | | @solwed/mind-sdk/entities | 39 clases de dominio con herencia (Domain, BillingDocument, Product, Cart, etc.) | | @solwed/mind-sdk/auth | Identidad: AuthUser, Session, JWTToken, 2FA, Google OAuth | | @solwed/mind-sdk/models | Tipos + Zod schemas para validación runtime | | @solwed/mind-sdk/agents | Contratos de agentes (50 tipos compartidos mind ↔ app) | | @solwed/mind-sdk/logger | createLogger (pino) + PgLogWriter + redact | | @solwed/mind-sdk/errors | AppError + 20 clases de error tipadas | | @solwed/mind-sdk/constants | SOLWED_URLS, EXTERNAL_APIS, paths, IDs, rate limits | | @solwed/mind-sdk/utils | retry, Result<T,E>, tryCatch, sleep, abort signals | | @solwed/mind-sdk/email | Templates HTML (tema dark Orbitron) + helpers | | @solwed/mind-sdk/browser | createBrowserClient — cliente HTTP para Next.js / Astro | | @solwed/mind-sdk/tools | Tool registry + typed definitions | | @solwed/mind-sdk/schemas | Zod schemas crudos | | @solwed/mind-sdk/contracts | Contratos público mind/bridge | | @solwed/mind-sdk/openapi | OpenAPI artifacts | | @solwed/mind-sdk/mcp | MCP server bin | | @solwed/mind-sdk/bridge | Bridge adapter base |

Helpers universales (zero-dep, frontend OK)

| Subpath | Exports | |---------|---------| | @solwed/mind-sdk/validators | validators.is{DNI,NIE,CIF,NIF,IBAN,Email,E164,SpanishMobile,Slug,URL,UUID,SpanishPostalCode} con checksum real | | @solwed/mind-sdk/format | fmt.{eur,esDate,esDateTime,relativeTime,phone,slug,removeAccents,bytes,truncate,...} | | @solwed/mind-sdk/date | dateUtils.{isBusinessDay,addBusinessDays,parseES,startOfMonth,...} | | @solwed/mind-sdk/phone | phoneUtils.{toE164,toWhatsapp,prettyES,whatsappLink,telLink,...} | | @solwed/mind-sdk/pagination | parseLimitOffset, buildPageMeta, parseCursor, encode/decodeCursor | | @solwed/mind-sdk/limits | LIMITS.{DEFAULT_PAGE_SIZE,MAX_FILE_SIZE_BYTES,...} | | @solwed/mind-sdk/tax | taxCalc.{line,invoice,stripIVA,addIVA} (España IVA/IRPF/recargo) | | @solwed/mind-sdk/i18n | MESSAGES_ES, t.{auth,validation,resource,server,payment,...} | | @solwed/mind-sdk/cache | Cache interface + MemoryCache (LRU+TTL) | | @solwed/mind-sdk/flags | FeatureFlags con rollout %, allow/deny, tenant gates |

Server-only (subpaths opt-in)

| Subpath | Exports | Deps runtime | |---------|---------|---| | @solwed/mind-sdk/crypto | cryptoUtils.{encryptAES_GCM,decryptAES_GCM,hmacSign,generateToken,...} | node:crypto | | @solwed/mind-sdk/http | fetchJSON<T> con Result<T,ProviderError> + retries + timeout | global fetch | | @solwed/mind-sdk/http/express | respond.{ok,err,paginated,notFound,validation,rateLimit,...} | type-only Express | | @solwed/mind-sdk/providers | ProviderBase abstract con retry+timeout+lazyClient+timed | global fetch | | @solwed/mind-sdk/webhooks | webhooks.{verifyStripe,verifyGitHub,verifyBrevo,verifyTwilio,verifyHmac} | node:crypto | | @solwed/mind-sdk/tenant | getTenantId, requireTenantId, middleware factories | type-only Express | | @solwed/mind-sdk/auth-mw | authMiddleware.{requireAuth,requireRole,requireAnyRole,requireAdmin} | type-only Express | | @solwed/mind-sdk/api | MindAPI + BridgeAPI waiter pattern (namespaced + escape hatch) | global fetch | | @solwed/mind-sdk/queue | AnyJob typed union + jobId, queueForJobType | — |


Cliente HTTP unificado

Un solo SolwedClient que sirve tanto para Mind como para Bridge:

import { SolwedClient } from '@solwed/mind-sdk';

// Cliente Bridge (token estático)
const bridge = new SolwedClient({
  baseUrl: 'http://localhost:3009',
  token: process.env.BRIDGE_TOKEN,
});

// Cliente Mind (credentials + JWT auto-refresh)
const mind = new SolwedClient({
  baseUrl: 'https://mind.solwed.es',
  credentials: { nick: 'ivan', password: '...' },
});

// Misma API para los dos
await bridge.get<CFZone[]>('/cloudflare/zones');
await mind.post('/tools/execute', { name: 'listar_facturas', args: {} });
await mind.listTools();

Desde el navegador:

import { createBrowserClient } from '@solwed/mind-sdk/browser';

const api = createBrowserClient({
  baseUrl: '',
  tokenSource: 'cookie',   // o 'localStorage'
  tokenKey: 'solwed-token',
});

const invoices = await api.get('/api/portal/invoices');
const pdf = await api.fetchRaw('/api/portal/invoices/42/pdf');  // → Blob

Clases de dominio con herencia

Todas extienden BaseEntity y exponen toRedis() / fromRedis() / toJSON() / validate().

Billing alineado con FacturaScripts

import { Invoice, Quote, Order, DeliveryNote } from '@solwed/mind-sdk/entities';

// Hidratar desde el payload crudo de FS
const inv = new Invoice(rawFactura);

// Métodos compartidos desde BillingDocument
inv.displayNumber();         // "FA2025-042"
inv.formattedTotal();        // "1.210,00 €"
inv.customerLabel();         // "ACME SL"
inv.getShippingAddress();    // Address instance from flat fields
inv.hasDiscount();
inv.effectiveVatRate();      // 21

// Métodos específicos de Invoice
inv.isOverdue();             // true
inv.daysOverdue();           // 12
inv.outstanding();           // 1210
inv.status;                  // 'vencida'

// PDF export (usa /api/3/exportar{Type}/{codigo})
inv.pdfExportPath();         // "/api/3/exportarFacturaCliente/FA2025-042?type=PDF&format=0"
inv.pdfUrl('https://erp.solwed.es', { lang: 'es' });
inv.pdfFilename();           // "FA2025-042.pdf"
inv.apiPath();               // "/api/3/facturaclientes/42"

Los 4 tipos (Invoice, Quote, Order, DeliveryNote) heredan de BillingDocument y comparten los mismos helpers.

Catálogo alineado con FS

import { Product, Variant, Stock } from '@solwed/mind-sdk/entities';

const prod = new Product(rawProducto);

prod.isService();            // true/false
prod.isAvailable();          // publico && sevende && !bloqueado
prod.isInStock();            // considera nostock + ventasinstock + stockfis
prod.isPurchasable();        // comprehensive check
prod.priceWithTax();         // precio * (1 + iva/100)
prod.isOnSale();             // precioAntes > precio
prod.discountPercent();
prod.formattedPrice();       // "99,00 €"

// Variantes
const v = new Variant(rawVariante);
v.hasAttributes();
v.marginAmount();            // precio - coste
v.marginPercentFromCost();

// Stock por almacén
const s = new Stock(rawStock);
s.reserve(10);               // mueve disponible → reservada
s.needsRestocking();         // qty para alcanzar stockmin
s.projectedAvailable();      // disponible + pterecibir

Dominios, hosting y assets

import { Domain, WordPressInstall, Mailbox, ERPInstall, Client } from '@solwed/mind-sdk/entities';

const wp = new WordPressInstall(rawData);

// Heredado de Domain
wp.daysUntilExpiry();
wp.isSslHealthy();
wp.isExpiringSoon(30);

// Específico de WordPress
wp.needsCoreUpdate();
wp.outdatedPlugins();
wp.pendingUpdates();
wp.diskGB();

// Agregador
const client = new Client(rawData);
client.totalAssets();
client.expiringDomains(30);
client.mailboxesNearQuota();
client.wordpressNeedingUpdate();

Cart → Order (FS)

import { Cart, Product } from '@solwed/mind-sdk/entities';

const cart = new Cart({ codcliente: '00042' });
cart.addItem(productHosting, 1);
cart.addItem(productDominio, 2);
cart.applyCoupon({ code: 'SUMMER10', percent: 10 });
cart.formattedTotal();       // "133,95 €"

// Al checkout, se convierte en un PedidoCliente nativo de FS
const order = cart.toOrder({ stripePaymentId: 'pi_xxx' });
// order.lineas contiene DocumentLine[] listos para POST /api/3/pedidoclientes

Auth

Identidad + sesiones

import { AuthUser, Session, hasAtLeastRole } from '@solwed/mind-sdk/auth';

const user = new AuthUser({
  id: 1,
  email: '[email protected]',
  role: 'admin',
  codcliente: '00001',
  hasPassword: true,
  two_factor_enabled: true,
});

user.displayName();              // "Ivan Moreno"
user.isSolwedStaff();            // email domain === 'solwed.es'
user.canLogin();                 // { ok: true } | { ok: false, reason }
user.needsPasswordReset(180);
hasAtLeastRole(user, 'empleado'); // true

JWT sin dependencias

import { JWTToken } from '@solwed/mind-sdk/auth';

const token = JWTToken.decode(rawJwt);
if (token?.isExpired()) redirect('/login');

token.subject();         // user id
token.role();            // 'admin'
token.tenantId();        // 1
token.codcliente();      // '00042'
token.isInRefreshMargin(5 * 60 * 1000);   // debería refrescar

Google OAuth (login + delegated access)

import { GoogleAccount, AuthIdentity } from '@solwed/mind-sdk/auth';

// Al volver del callback OAuth
const identity = new AuthIdentity({
  provider: 'google',
  providerUserId: googleUserId,
  email: '[email protected]',
  email_verified: true,
  hd: 'solwed.es',
  scopes: [...],
  accessToken, refreshToken, expiresIn,
});

if (identity.isFromSolwedDomain()) {
  // Asignar rol de empleado automáticamente
}

// Cuando se usa para acceder a servicios Google
const google = new GoogleAccount(dbRow);

if (google.needsRefresh()) await refresh(google);

const check = google.canUseFeature('gmail');
if (!check.ok) throw new GoogleScopeError(['gmail'], google.missingScopesForGroups(['gmail']));

// Enabled features
google.enabledFeatures();   // ['openid', 'profile', 'email', 'gmail', 'calendar']

Lockout y attempts

import { AuthAttempt } from '@solwed/mind-sdk/auth';

const attempts = await attemptRepo.findRecent(nick);
if (AuthAttempt.shouldLockout(attempts, nick, 5, 15)) {
  throw new AccountLockedError(900);
}
if (AuthAttempt.isDistributedAttack(attempts, 5)) {
  alert('Possible distributed brute force');
}

Logger estructurado

import { createLogger, PgLogWriter } from '@solwed/mind-sdk/logger';

const logger = createLogger({
  name: 'solwed-mind',
  level: 'info',
  mixin: () => ({ requestId: getCurrentRequestId() }),
});

// PostgreSQL transport
const pgWriter = new PgLogWriter({ pool, tableName: 'system_logs' });
logger.addLogHook(pgWriter.capture);
pgWriter.start();

logger.info('User logged in', { userId: 42 });
logger.error('Query failed', { error, query: 'SELECT ...' });

Los mensajes y objetos se redactan automáticamente (passwords, tokens, JWTs, API keys, IBANs, CIFs, tarjetas).


Errores tipados

import {
  AppError,
  NotFoundError,
  ValidationError,
  UnauthorizedError,
  ProviderError,
  toAppError,
} from '@solwed/mind-sdk/errors';

try {
  await fetchInvoice(id);
} catch (err) {
  const appErr = toAppError(err);
  res.status(appErr.statusCode).json(appErr.toJSON());
}

// Errores específicos
throw new NotFoundError('Invoice', id);
throw new ValidationError('email is invalid');
throw new ProviderError('stripe', 'Rate limited', 429);

Zod schemas + modelos

import { StripeCustomerSchema, parseArray } from '@solwed/mind-sdk/models';

// Valida una respuesta externa antes de guardar en Redis
const customers = parseArray(StripeCustomerSchema, rawResponse, 'stripe:customers');
await redis.set('stripe:customers', JSON.stringify(customers));

Schemas disponibles: Stripe, Cloudflare, FacturaScripts, Plesk, DonDominio, OVH, Kolab, Brevo, SEO, Meta Ads, Google, etc.


Utilidades

import {
  formatDate,
  formatCurrency,
  daysUntil,
  normalizePhone,
  slugify,
  sanitizeFilename,
  validateCifNif,
} from '@solwed/mind-sdk/utils';

formatDate('2026-04-11');               // "11 abr 2026"
formatCurrency(1234.5);                 // "1.234,50 €"
daysUntil('2026-12-31');                // 264
normalizePhone('+34 612 345 678');      // "612345678"
slugify('Héllo Wörld!');                // "hello-world"
validateCifNif('B12345678');            // { valid, tipo, tipo_entidad, es_asociacion }

Email templates

import { emailTemplate, wrapAgentEmail } from '@solwed/mind-sdk/email';

// Template completo con secciones
const html = emailTemplate.render({
  badge: 'Informe SEO mensual',
  greeting: { name: 'Juan', message: 'Aquí va tu informe de abril' },
  sections: [
    { title: 'Resumen', content: '<p>...</p>' },
    { title: 'Métricas', content: metricsHtml },
  ],
  cta: { label: 'VER INFORME', url: 'https://app.solwed.es/...' },
});

// Wrapper para salidas de agentes
const agentHtml = wrapAgentEmail({
  agentName: 'SEO Agent',
  tenantName: 'ACME SL',
  body: '<p>Tu web ha subido 12 puntos en SEO este mes...</p>',
  cta: { label: 'VER DETALLE', url: '...' },
});

Patrón Redis → API → Frontend

Las clases están diseñadas para viajar por toda la cadena sin conversiones manuales:

// 1. En un sync worker de bridge
const wp = new WordPressInstall(rawFromPlesk);
await redis.set(`wp:${wp.name}`, wp.toRedis(), 'EX', 2100);

// 2. En un router de bridge
const raw = await redis.get(`wp:${domain}`);
const wp = WordPressInstall.fromRedis(raw);
res.json({ success: true, data: wp.toJSON() });

// 3. En una tool de mind
const response = await bridge.get<WordPressData>(`/wp/${domain}`);
const wp = new WordPressInstall(response);
if (wp.pendingUpdates() > 0) {
  await notify(client, `Tu WordPress tiene ${wp.pendingUpdates()} actualizaciones pendientes`);
}

// 4. En el frontend (Next.js)
const wp = new WordPressInstall(apiResponse);
return (
  <Card
    title={wp.name}
    badge={wp.isSslHealthy() ? 'SSL OK' : 'SSL expira'}
    updates={wp.pendingUpdates()}
    disk={wp.diskGB() + ' GB'}
  />
);

Herencia y polimorfismo

Gracias al code-splitting del build, instanceof funciona cross-module:

import { BaseEntity, BillingDocument, Invoice, Quote, Order } from '@solwed/mind-sdk/entities';
import { AuthUser } from '@solwed/mind-sdk/auth';

const user = new AuthUser(...);
const invoice = new Invoice(...);

user instanceof BaseEntity;            // true
invoice instanceof BaseEntity;         // true
invoice instanceof BillingDocument;    // true

// Polimorfismo con documentos heterogéneos
const docs: BillingDocument[] = [invoice, quote, order, deliveryNote];
for (const doc of docs) {
  console.log(doc.formattedTotal(), doc.pdfUrl(erpBase));
}

Scripts

npm run build          # tsup → dist/ (CJS + ESM + d.ts, con code splitting)
npm run test           # vitest (108 tests entities + auth)
npm run typecheck      # tsc --noEmit

Instalación local (monorepo SOLWED)

Para desarrollo dentro del workspace:

// package.json de cada proyecto
{
  "dependencies": {
    "@solwed/mind-sdk": "file:../mind-sdk"
  }
}

Cambios en mind-sdk/src/ se reflejan tras npm run build.


Arquitectura

@solwed/mind-sdk
│
├── SolwedClient            ← cliente HTTP server-side (mind + bridge)
├── createBrowserClient     ← cliente HTTP browser (cookie / localStorage)
│
├── entities/               ← 39 clases con herencia
│   ├── BaseEntity (abstract)
│   ├── Domain → WordPressInstall, Mailbox, ERPInstall
│   ├── BillingDocument → Invoice, Quote, Order, DeliveryNote
│   ├── Product, Variant, Stock
│   ├── Conversation + ChatMessage
│   ├── Tenant, Subscription, Reminder, Ticket, Bot, Tool
│   ├── Cart, Contact, Deal, Activity
│   └── ...
│
├── auth/                   ← 8 clases de identidad
│   ├── AuthUser, Session, JWTToken, TwoFactorConfig, AuthAttempt
│   ├── OAuthAccount (abstract) → GoogleAccount
│   └── AuthIdentity
│
├── models/                 ← Zod schemas + tipos
├── agents/                 ← contratos compartidos mind ↔ app
├── logger/                 ← pino + redact + PG transport
├── errors/                 ← AppError hierarchy
├── constants/              ← DEFAULTS, enums, rate limits
├── utils/                  ← format, phone, cifnif, slugify
├── email/                  ← templates HTML (dark theme)
└── browser/                ← HTTP client para navegador

Licencia

UNLICENSED — uso interno SOLWED.