@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-sdkQué 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'); // → BlobClases 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 + pterecibirDominios, 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/pedidoclientesAuth
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'); // trueJWT 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 refrescarGoogle 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 --noEmitInstalació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 navegadorLicencia
UNLICENSED — uso interno SOLWED.
