arckode-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +546 -0
  2. package/adapters/__tests__/mysql.test.ts +283 -0
  3. package/adapters/jwt.ts +18 -0
  4. package/adapters/mysql.ts +98 -0
  5. package/adapters/postgres.ts +52 -0
  6. package/adapters/redis-cache.ts +64 -0
  7. package/adapters/sqlite.ts +73 -0
  8. package/adapters/vendor.d.ts +48 -0
  9. package/bin/arckode.js +7 -0
  10. package/cli/analyze.ts +506 -0
  11. package/cli/commands/db-migrate.ts +121 -0
  12. package/cli/commands/db-seed.ts +54 -0
  13. package/cli/commands/generate-api-client.ts +106 -0
  14. package/cli/commands/make-adapter.ts +132 -0
  15. package/cli/commands/make-auth.ts +297 -0
  16. package/cli/commands/make-frontend-module.ts +271 -0
  17. package/cli/commands/make-helper.ts +65 -0
  18. package/cli/commands/make-migration.ts +30 -0
  19. package/cli/commands/make-seed.ts +29 -0
  20. package/cli/generate.ts +132 -0
  21. package/cli/index.ts +604 -0
  22. package/cli/stubs/frontend-stub.ts +294 -0
  23. package/cli/stubs/fullstack-stub.ts +46 -0
  24. package/cli/stubs/module-stub.ts +469 -0
  25. package/kernel/__tests__/adapters.test.ts +101 -0
  26. package/kernel/__tests__/analyzer.test.ts +282 -0
  27. package/kernel/__tests__/framework.test.ts +617 -0
  28. package/kernel/__tests__/middlewares.test.ts +174 -0
  29. package/kernel/__tests__/static.test.ts +94 -0
  30. package/kernel/framework.ts +1851 -0
  31. package/kernel/middlewares.ts +179 -0
  32. package/kernel/static.ts +76 -0
  33. package/kernel/testing.ts +237 -0
  34. package/modules/events/index.ts +99 -0
  35. package/modules/mail/index.ts +51 -0
  36. package/modules/mail/smtp-adapter.ts +42 -0
  37. package/modules/queue/index.ts +78 -0
  38. package/modules/storage/index.ts +40 -0
  39. package/modules/storage/local-adapter.ts +41 -0
  40. package/modules/ws/__tests__/ws.test.ts +114 -0
  41. package/modules/ws/index.ts +136 -0
  42. package/package.json +99 -0
  43. package/skills/auth/SKILL.md +243 -0
  44. package/skills/cli/SKILL.md +258 -0
  45. package/skills/config/SKILL.md +253 -0
  46. package/skills/connectors/SKILL.md +259 -0
  47. package/skills/helpers/SKILL.md +206 -0
  48. package/skills/middlewares/SKILL.md +282 -0
  49. package/skills/orm/SKILL.md +260 -0
  50. package/skills/realtime/SKILL.md +307 -0
  51. package/skills/services/SKILL.md +206 -0
  52. package/skills/testing/SKILL.md +257 -0
package/README.md ADDED
@@ -0,0 +1,546 @@
1
+ # Arckode Framework
2
+
3
+ Framework TypeScript/Bun para construir APIs modulares. Diseñado desde el principio para trabajar con IA: arquitectura predecible, límites claros, cero magia.
4
+
5
+ ```bash
6
+ arckode new mi-api
7
+ cd mi-api && bun install
8
+ arckode make:auth
9
+ arckode make:module Productos
10
+ bun run dev
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Por qué Arckode
16
+
17
+ La mayoría de los frameworks están optimizados para que los humanos escriban código. Arckode está optimizado para que **la IA genere código correcto sin supervisión constante**.
18
+
19
+ La IA lee el `composition-root.ts` y sabe todo lo que necesita:
20
+ - Qué módulos existen y qué hacen
21
+ - Cómo se conectan entre sí
22
+ - Qué datos maneja cada uno
23
+ - Qué reglas tienen que cumplir
24
+
25
+ No hay decoradores, no hay magia de inyección, no hay convenciones implícitas. Todo está explícito en un solo lugar.
26
+
27
+ ---
28
+
29
+ ## Instalación
30
+
31
+ **Requisito: [Bun](https://bun.sh) >= 1.0**
32
+
33
+ ```bash
34
+ # 1. Instalar el CLI globalmente — una sola vez por máquina
35
+ bun install -g arckode-framework
36
+
37
+ # 2. Crear tu proyecto
38
+ arckode new mi-api
39
+
40
+ # 3. Entrar al proyecto e instalar dependencias
41
+ cd mi-api && bun install
42
+
43
+ # 4. Configurar entorno
44
+ echo 'JWT_SECRET=cambia-esto-en-produccion' > .env
45
+
46
+ # 5. Iniciar
47
+ bun run src/composition-root.ts
48
+ ```
49
+
50
+ > El CLI instala `arckode-framework` como dependencia de tu proyecto automáticamente. No necesitás referenciar rutas del framework — todo se importa como `from 'arckode-framework'`.
51
+
52
+ ---
53
+
54
+ ## Inicio rápido
55
+
56
+ ```bash
57
+ arckode new mi-api
58
+ cd mi-api && bun install
59
+ arckode make:auth # módulo completo de auth con JWT
60
+ arckode make:module Clientes
61
+ arckode analyze # 0 violaciones garantizado
62
+ bun run dev # hot reload
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Conceptos core
68
+
69
+ ### Módulo
70
+
71
+ La unidad básica del sistema. Cada módulo es dueño de sus datos y su lógica. **Nunca importa de otro módulo directamente.**
72
+
73
+ ```
74
+ modules/productos/
75
+ index.ts ← puerta pública (solo exports)
76
+ types.ts ← DTOs y ModelDefinition
77
+ sockets.ts ← hooks para conectores (opcional)
78
+ actions/
79
+ service.ts ← lógica de negocio
80
+ controller.ts ← capa HTTP (sin lógica)
81
+ validators/
82
+ schema.ts ← validación de entrada
83
+ tests/
84
+ service.test.ts
85
+ ```
86
+
87
+ ```ts
88
+ // modules/productos/index.ts
89
+ export function ProductosModule() {
90
+ return createModule({
91
+ name: 'productos',
92
+ version: '1.0.0',
93
+ description: 'Gestión del catálogo de productos',
94
+ contract: {
95
+ actions: ['listar', 'crear', 'actualizar', 'eliminar'],
96
+ events: ['onProductoCreado'],
97
+ tables: ['productos'],
98
+ },
99
+ create({ logger, orm, cache, router }) {
100
+ const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
101
+ const service = new ProductosService(repo, logger.child('productos'), cache)
102
+ const controller = new ProductosController(service, logger.child('productos'))
103
+
104
+ router.get('/productos', (req) => controller.index(req))
105
+ router.post('/productos', (req) => controller.store(req))
106
+ router.put('/productos/:id', (req) => controller.update(req))
107
+ router.delete('/productos/:id', (req) => controller.destroy(req))
108
+
109
+ return service // resolveModule('productos') devuelve esto
110
+ },
111
+ })
112
+ }
113
+ ```
114
+
115
+ ### RepositoryAdapter\<T\>
116
+
117
+ Los servicios nunca dependen del ORM directamente. Dependen de la interfaz genérica — el ORM concreto se elige en `composition-root.ts`.
118
+
119
+ ```ts
120
+ // ❌ PROHIBIDO — acoplado al ORM, imposible cambiar a MongoDB/Prisma
121
+ class ProductosService {
122
+ constructor(private orm: ORM) {}
123
+ listar() { return this.orm.findMany('Producto') }
124
+ }
125
+
126
+ // ✅ CORRECTO — desacoplado, testeable, intercambiable
127
+ class ProductosService {
128
+ constructor(private repo: RepositoryAdapter<ProductoDTO>) {}
129
+ listar() { return this.repo.findMany() }
130
+ }
131
+ ```
132
+
133
+ ```ts
134
+ // composition-root.ts — se elige la implementación una sola vez
135
+ const repo = new OrmRepository<ProductoDTO>(orm, 'Producto') // SQLite/Postgres
136
+ // O mañana:
137
+ const repo = new MongoProductoRepo(collection) // MongoDB
138
+ // El service nunca cambia.
139
+ ```
140
+
141
+ ### Connector
142
+
143
+ El único puente entre módulos. Solo delegación — nunca lógica de negocio.
144
+
145
+ ```ts
146
+ // connectors/pedido-stock.ts
147
+ export function conectarPedidoConStock(ctx: ConnectorContext): void {
148
+ const productos = ctx.resolveModule<ProductosService>('productos')
149
+
150
+ ctx.resolveModule('pedidos', {
151
+ onPedidoCreado: async (pedido) => {
152
+ await productos.descontarStock(pedido.productoId, pedido.cantidad)
153
+ },
154
+ })
155
+ }
156
+ ```
157
+
158
+ ### Composition Root
159
+
160
+ El único archivo que conoce todo el sistema. La IA lo lee y entiende la arquitectura completa.
161
+
162
+ ```ts
163
+ // src/composition-root.ts
164
+ import { ConfigStore, ORM, Router, NodeServer, MemoryCache, System, Auth, loadEnv } from 'arckode-framework'
165
+ import { SqliteAdapter } from 'arckode-framework/adapters/sqlite'
166
+ import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
167
+ import { ProductoModel } from './modules/productos/types'
168
+ import { ProductosModule } from './modules/productos'
169
+ import { PedidosModule } from './modules/pedidos'
170
+ import { conectarPedidoConStock } from './connectors/pedido-stock'
171
+
172
+ const env = await loadEnv() // carga .env + .env.{NODE_ENV}
173
+
174
+ const config = new ConfigStore()
175
+ config.define({
176
+ PORT: { type: 'number', default: 3000 },
177
+ DB_PATH: { type: 'string', default: './data/db.sqlite' },
178
+ JWT_SECRET: { type: 'string', required: true },
179
+ }).load(env)
180
+
181
+ const db = new SqliteAdapter({ path: config.get('DB_PATH') })
182
+ await db.connect()
183
+ const orm = new ORM(db)
184
+
185
+ orm.define('Producto', ProductoModel) // cada módulo es dueño de su ModelDefinition
186
+ await orm.migrate()
187
+
188
+ const system = new System({ config, orm, router, http, cache, auth, ... })
189
+ system.addModule(ProductosModule())
190
+ system.addModule(PedidosModule())
191
+ system.addConnector('pedido-stock', conectarPedidoConStock)
192
+ await system.start()
193
+ ```
194
+
195
+ ---
196
+
197
+ ## CLI — Referencia completa
198
+
199
+ ### Proyectos
200
+
201
+ | Comando | Descripción |
202
+ |---|---|
203
+ | `arckode new <nombre>` | Crear proyecto backend completo |
204
+ | `arckode new:frontend [nombre]` | Crear frontend Vue 3 + Vite + TypeScript |
205
+
206
+ ### Generadores de backend
207
+
208
+ | Comando | Descripción |
209
+ |---|---|
210
+ | `arckode make:auth` | Módulo de autenticación completo (login, register, JWT, perfil) |
211
+ | `arckode make:module <Nombre>` | Módulo con service, controller, types, validators, tests |
212
+ | `arckode make:connector <nombre> <mod1> <mod2>` | Conector entre módulos |
213
+ | `arckode make:seed <Nombre>` | Seed de datos |
214
+ | `arckode make:migration <nombre>` | Migración SQL con `up()` y `down()` |
215
+ | `arckode make:helper <nombre>` | Helper puro (sin efectos secundarios) |
216
+ | `arckode make:adapter <Adapter> <Interfaz>` | Adapter de librería externa |
217
+
218
+ ### Generadores de frontend
219
+
220
+ | Comando | Descripción |
221
+ |---|---|
222
+ | `arckode make:page <Nombre>` | Módulo frontend (API client + composable + página + router) |
223
+ | `arckode generate:api [frontend-path]` | Genera API clients desde módulos del backend |
224
+
225
+ ### Base de datos
226
+
227
+ | Comando | Descripción |
228
+ |---|---|
229
+ | `arckode db:migrate` | Ejecutar migraciones pendientes (`src/migrations/`) |
230
+ | `arckode db:migrate down` | Revertir la última migración |
231
+ | `arckode db:seed` | Listar seeds disponibles |
232
+
233
+ ### Análisis y diagnóstico
234
+
235
+ | Comando | Descripción |
236
+ |---|---|
237
+ | `arckode analyze` | Detecta 15+ tipos de violaciones de arquitectura |
238
+ | `arckode routes` | Lista todas las rutas registradas (análisis estático) |
239
+
240
+ ---
241
+
242
+ ## Adapters
243
+
244
+ ### Base de datos
245
+
246
+ ```ts
247
+ // SQLite — desarrollo y apps de baja escala
248
+ import { SqliteAdapter } from 'arckode-framework/adapters/sqlite'
249
+ const db = new SqliteAdapter({ path: './data/app.sqlite' })
250
+
251
+ // PostgreSQL — producción
252
+ import { PostgresAdapter } from 'arckode-framework/adapters/postgres'
253
+ const db = new PostgresAdapter({ connectionString: process.env.DATABASE_URL, poolMax: 10 })
254
+ ```
255
+
256
+ ### Cache
257
+
258
+ ```ts
259
+ // En memoria — desarrollo (se pierde al reiniciar)
260
+ const cache = new MemoryCache()
261
+
262
+ // Redis — producción
263
+ import { RedisCacheAdapter } from 'arckode-framework/adapters/redis-cache'
264
+ const cache = new RedisCacheAdapter({ url: process.env.REDIS_URL })
265
+ await cache.connect()
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Módulos opcionales
271
+
272
+ ### Queue
273
+
274
+ ```ts
275
+ import { QueueService, MemoryQueueAdapter } from 'arckode-framework/queue'
276
+
277
+ const queue = new QueueService(new MemoryQueueAdapter())
278
+
279
+ queue.register('enviar-email', async (job) => {
280
+ await mail.send(job.data as EmailData)
281
+ })
282
+
283
+ await queue.dispatch('enviar-email', { to: 'user@example.com', subject: 'Bienvenido' })
284
+ await queue.dispatch('enviar-email', { ... }, { delay: 5000, maxAttempts: 3 })
285
+ ```
286
+
287
+ ### Events (pub-sub)
288
+
289
+ ```ts
290
+ import { EventBus } from 'arckode-framework/events'
291
+
292
+ const events = new EventBus()
293
+ events.on('pedido.creado', async (pedido) => { ... })
294
+ events.emit('pedido.creado', pedido)
295
+ ```
296
+
297
+ ### WebSockets
298
+
299
+ ```ts
300
+ import { WsServer } from 'arckode-framework/ws'
301
+
302
+ const ws = new WsServer()
303
+ ws.on('connection', (client) => { client.send({ type: 'welcome' }) })
304
+ ws.broadcast({ type: 'stock-bajo', productoId: id })
305
+ ```
306
+
307
+ ### Mail
308
+
309
+ ```ts
310
+ import { MailService } from 'arckode-framework/mail'
311
+ import { SmtpAdapter } from 'arckode-framework/mail/smtp'
312
+
313
+ const mail = new MailService(new SmtpAdapter({ host: 'smtp.gmail.com', port: 587, ... }))
314
+ await mail.send({ to: 'user@example.com', subject: 'Bienvenido', html: '<p>Hola</p>' })
315
+ ```
316
+
317
+ ### Storage
318
+
319
+ ```ts
320
+ import { StorageService } from 'arckode-framework/storage'
321
+ import { LocalAdapter } from 'arckode-framework/storage/local'
322
+
323
+ const storage = new StorageService(new LocalAdapter({ path: './uploads' }))
324
+ const url = await storage.save('avatars/user.png', fileBuffer)
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Middlewares
330
+
331
+ ```ts
332
+ import { cors, rateLimit, requestLogger, timeout, compression, bodyLimit } from 'arckode-framework/middlewares'
333
+
334
+ // Globales
335
+ router.use(cors({ origins: ['https://miapp.com'] }))
336
+ router.use(rateLimit({ windowMs: 60_000, max: 100 }))
337
+ router.use(requestLogger(logger))
338
+ router.use(compression())
339
+
340
+ // Por ruta
341
+ router.get('/admin', handler, [auth.authenticate('admin'), timeout(3000)])
342
+ router.post('/upload', handler, [bodyLimit(10 * 1024 * 1024)]) // 10MB
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Auth
348
+
349
+ ```ts
350
+ const auth = new Auth(jwtTokenAdapter, process.env.JWT_SECRET, logger)
351
+
352
+ // Crear token
353
+ const token = await auth.createToken({ id: user.id, role: 'admin' })
354
+
355
+ // Hashear password (scrypt — sin dependencias externas)
356
+ const hash = await auth.hashPassword(password)
357
+ const ok = await auth.comparePassword(password, hash)
358
+
359
+ // Proteger rutas
360
+ router.get('/perfil', handler, [auth.authenticate()]) // cualquier usuario autenticado
361
+ router.get('/admin', handler, [auth.authenticate('admin')]) // solo admins
362
+ router.delete('/users/:id', handler, [auth.authenticate('admin', 'superadmin')])
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Variables de entorno
368
+
369
+ `loadEnv()` carga `.env` base y `.env.{NODE_ENV}` con override por stage. `process.env` siempre tiene prioridad máxima.
370
+
371
+ ```bash
372
+ # .env
373
+ PORT=3000
374
+ DB_PATH=./data/dev.sqlite
375
+ LOG_LEVEL=debug
376
+
377
+ # .env.production
378
+ DB_PATH=./data/prod.sqlite
379
+ LOG_LEVEL=warn
380
+ ```
381
+
382
+ ```bash
383
+ NODE_ENV=production bun run src/composition-root.ts
384
+ # Lee .env → sobrescribe con .env.production → process.env tiene prioridad
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Testing
390
+
391
+ ```ts
392
+ import { createTestClient, createRecordingOrm } from 'arckode-framework/testing'
393
+ import { OrmRepository } from 'arckode-framework'
394
+
395
+ // ORM que registra llamadas en memoria — sin base de datos real
396
+ const orm = createRecordingOrm()
397
+ const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
398
+ const service = new ProductosService(repo, logger, cache)
399
+
400
+ // Cliente HTTP que hace requests al Router sin levantar un server real
401
+ const client = createTestClient(router)
402
+
403
+ test('listar productos vacío', async () => {
404
+ const res = await client.get('/productos')
405
+ expect(res.status).toBe(200)
406
+ expect(res.body).toEqual([])
407
+ })
408
+
409
+ test('crear producto', async () => {
410
+ const res = await client.post('/productos', {
411
+ body: { nombre: 'Laptop', precio: 1500, stock: 10 },
412
+ })
413
+ expect(res.status).toBe(201)
414
+ expect(res.body.nombre).toBe('Laptop')
415
+ })
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Análisis de arquitectura
421
+
422
+ ```bash
423
+ arckode analyze
424
+ ```
425
+
426
+ ```
427
+ ══════════════════════════════════════════════
428
+ Arckode — Análisis de Arquitectura
429
+ ══════════════════════════════════════════════
430
+
431
+ VIOLACIONES ENCONTRADAS: 2
432
+
433
+ [Acoplamiento]
434
+ ❌ modules/pedidos/actions/service.ts:12
435
+ Importa directamente de otro módulo (CLAUDE #1)
436
+ → Usar un conector en /connectors/
437
+
438
+ [Portabilidad]
439
+ ❌ modules/clientes/actions/service.ts:8
440
+ El service inyecta ORM directamente (CLAUDE #18)
441
+ → Usar RepositoryAdapter<ClienteDTO>
442
+ ```
443
+
444
+ | Categoría | Violations detectadas |
445
+ |---|---|
446
+ | Estructura | `MISSING_INDEX`, `MISSING_TYPES`, `MISSING_SERVICE`, `MISSING_CONTROLLER`, `MISSING_TESTS` |
447
+ | Acoplamiento | `DIRECT_MODULE_IMPORT` |
448
+ | Diseño | `BUSINESS_LOGIC_IN_CONTROLLER`, `CONTROLLER_MISSING_VALIDATION`, `EMPTY_MODULE_DESCRIPTION` |
449
+ | Calidad | `TESTS_WITHOUT_CASES`, `GOD_SERVICE` |
450
+ | Seguridad | `IDOR_RISK`, `HARDCODED_SECRET`, `INSECURE_PASSWORD` |
451
+ | Performance | `N_PLUS_ONE_RISK` |
452
+ | Portabilidad | `SERVICE_DEPENDS_ON_ORM` |
453
+
454
+ ---
455
+
456
+ ## Ejemplos incluidos
457
+
458
+ | Ejemplo | Descripción |
459
+ |---|---|
460
+ | `examples/ecommerce/` | Productos, pedidos, conector de stock, auth |
461
+ | `examples/completo/` | Auth, productos, pedidos, mail, storage, queue, WebSockets |
462
+
463
+ ---
464
+
465
+ ## Documentación adicional
466
+
467
+ | Archivo | Contenido |
468
+ |---|---|
469
+ | `CLAUDE.md` | 18 reglas inmutables — el contrato que la IA sigue |
470
+ | `kernel/framework.ts` | Fuente completa del kernel (la IA lo lee entero) |
471
+ | `kernel/testing.ts` | Utilidades de testing |
472
+ | `kernel/middlewares.ts` | Middlewares disponibles |
473
+
474
+ ---
475
+
476
+ ## Cómo funciona la distribución
477
+
478
+ ### Para usuarios del framework
479
+
480
+ ```bash
481
+ # Instalar CLI una vez
482
+ bun install -g arckode-framework
483
+
484
+ # Crear proyecto — el CLI genera todo: composition-root.ts, CLAUDE.md, .env, package.json
485
+ arckode new mi-tienda --db=postgres
486
+
487
+ # CLAUDE.md se genera automáticamente en tu proyecto con las 18 reglas del framework.
488
+ # La IA lo lee primero antes de escribir cualquier línea de código.
489
+
490
+ # Actualizar el framework en tu proyecto
491
+ bun update arckode-framework
492
+ ```
493
+
494
+ ### Para la IA (flujo de trabajo)
495
+
496
+ Cuando la IA trabaja en un proyecto con Arckode, hace esto en orden:
497
+
498
+ ```
499
+ 1. Lee CLAUDE.md → entiende las 18 reglas que NO puede violar
500
+ 2. Lee composition-root.ts → entiende todos los módulos, conectores y dependencias
501
+ 3. Corre arckode analyze → verifica que el estado actual tiene 0 violaciones
502
+ 4. Genera código → siguiendo la estructura exacta del framework
503
+ 5. Corre arckode analyze → verifica que sus cambios no introdujeron violaciones
504
+ 6. Corre bun test → verifica que nada se rompió
505
+ ```
506
+
507
+ ### Para el autor del framework (actualizaciones)
508
+
509
+ ```bash
510
+ # 1. Hacer los cambios
511
+ # 2. Correr tests y typecheck
512
+ bun test && bun run typecheck
513
+
514
+ # 3. Subir la versión en package.json (semver)
515
+ # 1.0.0 → 1.0.1 patch: bugfix
516
+ # 1.0.0 → 1.1.0 minor: feature nueva, retrocompatible
517
+ # 1.0.0 → 2.0.0 major: breaking change
518
+
519
+ # 4. Publicar
520
+ npm publish
521
+
522
+ # Los usuarios actualizan corriendo en su proyecto:
523
+ bun update arckode-framework
524
+ ```
525
+
526
+ ---
527
+
528
+ ## Adapters opcionales
529
+
530
+ Arckode solo instala lo que usás. Cada adapter tiene su dependencia peer:
531
+
532
+ | Adapter | Instalar |
533
+ |---|---|
534
+ | SQLite | `bun add better-sqlite3` |
535
+ | PostgreSQL | `bun add pg` |
536
+ | MySQL | `bun add mysql2` |
537
+ | Redis (cache) | `bun add redis` |
538
+ | Mail (SMTP) | `bun add nodemailer` |
539
+
540
+ El framework core (`kernel/framework.ts`) **no depende de ninguna librería externa** — solo de Node.js/Bun nativo.
541
+
542
+ ---
543
+
544
+ ## Licencia
545
+
546
+ MIT