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
@@ -0,0 +1,243 @@
1
+ # SKILL: Arckode Auth — JWT, Roles, Passwords y IDOR
2
+
3
+ > Activar cuando: implementar login, registro, proteger rutas, verificar permisos, manejar passwords.
4
+
5
+ ---
6
+
7
+ ## 1. SETUP EN COMPOSITION-ROOT
8
+
9
+ ```ts
10
+ import { Auth } from 'arckode-framework'
11
+ import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
12
+
13
+ const auth = new Auth(
14
+ jwtTokenAdapter,
15
+ config.get('JWT_SECRET'), // REQUERIDO — string largo y aleatorio
16
+ logger,
17
+ {
18
+ accessTokenExpiry: '15m', // default: '15m'
19
+ refreshTokenExpiry: '7d', // default: '7d'
20
+ adminRole: 'admin', // default: 'admin'
21
+ }
22
+ )
23
+
24
+ // Pasar al System y a los módulos que lo necesiten
25
+ const system = new System({ config, logger, orm, router, http, cache, auth })
26
+ ```
27
+
28
+ **Config requerida en .env:**
29
+ ```env
30
+ JWT_SECRET=una-cadena-larga-y-aleatoria-de-al-menos-32-chars
31
+ JWT_REFRESH_SECRET=otra-cadena-distinta-para-refresh-tokens
32
+ ```
33
+
34
+ ---
35
+
36
+ ## 2. REGISTRO Y LOGIN (patrón completo)
37
+
38
+ ```ts
39
+ // En el módulo de autenticación — actions/service.ts
40
+ export class AuthService {
41
+ constructor(
42
+ private repo: RepositoryAdapter<UserDTO>,
43
+ private auth: Auth,
44
+ private logger: Logger,
45
+ ) {}
46
+
47
+ async registrar(dto: RegisterDTO): Promise<{ accessToken: string; refreshToken: string }> {
48
+ const existente = await this.repo.findOne({ email: dto.email })
49
+ if (existente) throw new ConflictError('El email ya está registrado')
50
+
51
+ const hashedPassword = await this.auth.hashPassword(dto.password)
52
+ const user = await this.repo.create({
53
+ email: dto.email,
54
+ password: hashedPassword,
55
+ role: 'user',
56
+ } as Omit<UserDTO, 'id'>)
57
+
58
+ return {
59
+ accessToken: this.auth.createToken({ id: user.id, role: user.role }),
60
+ refreshToken: this.auth.createRefreshToken({ id: user.id, role: user.role }),
61
+ }
62
+ }
63
+
64
+ async login(dto: LoginDTO): Promise<{ accessToken: string; refreshToken: string }> {
65
+ const user = await this.repo.findOne({ email: dto.email })
66
+ if (!user) throw new AuthError('Credenciales inválidas')
67
+
68
+ const ok = await this.auth.comparePassword(dto.password, user.password as string)
69
+ if (!ok) throw new AuthError('Credenciales inválidas')
70
+
71
+ return {
72
+ accessToken: this.auth.createToken({ id: user.id, role: user.role }),
73
+ refreshToken: this.auth.createRefreshToken({ id: user.id, role: user.role }),
74
+ }
75
+ }
76
+
77
+ async refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
78
+ return this.auth.refresh(refreshToken)
79
+ }
80
+ }
81
+ ```
82
+
83
+ **Crítico:** Siempre usar el mismo mensaje de error para usuario no encontrado y contraseña incorrecta — previene user enumeration.
84
+
85
+ ---
86
+
87
+ ## 3. PROTEGER RUTAS
88
+
89
+ ```ts
90
+ // En index.ts del módulo — al registrar rutas:
91
+
92
+ // Requiere cualquier usuario autenticado
93
+ router.get('/perfil', [auth.authenticate()], req => controller.perfil(req))
94
+
95
+ // Requiere rol específico
96
+ router.post('/admin/config', [auth.authenticate('admin')], req => controller.setConfig(req))
97
+
98
+ // Múltiples roles permitidos
99
+ router.get('/reportes', [auth.authenticate('admin', 'supervisor')], req => controller.reportes(req))
100
+
101
+ // Ruta pública (sin middleware)
102
+ router.post('/auth/login', req => controller.login(req))
103
+ router.post('/auth/registro', req => controller.registrar(req))
104
+ ```
105
+
106
+ **El middleware agrega `req.user` con `{ id: string, role: string }` para handlers autenticados.**
107
+
108
+ ---
109
+
110
+ ## 4. ACCESO AL USUARIO ACTUAL
111
+
112
+ ```ts
113
+ // En el controller — req.user está disponible en rutas protegidas
114
+ async perfil(req: HttpRequest): Promise<HttpResponse> {
115
+ const user = req.user! // { id: string, role: string }
116
+ const data = await this.service.getPerfil(user.id, user)
117
+ return { status: 200, body: data }
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 5. PREVENCIÓN DE IDOR (obligatorio)
124
+
125
+ Todo `findById` que devuelve datos de un usuario específico **DEBE** verificar ownership:
126
+
127
+ ```ts
128
+ // ❌ PROHIBIDO — cualquier usuario autenticado puede ver datos de otro
129
+ async getMiPedido(id: string): Promise<PedidoDTO> {
130
+ const pedido = await this.repo.findById(id)
131
+ if (!pedido) throw new NotFoundError('Pedido no encontrado')
132
+ return pedido // IDOR: usuario B puede ver pedidos de usuario A con ID correcto
133
+ }
134
+
135
+ // ✅ OBLIGATORIO — verificar ownership
136
+ async getMiPedido(id: string, currentUser: { id: string; role: string }): Promise<PedidoDTO> {
137
+ const pedido = await this.repo.findById(id)
138
+ if (!pedido) throw new NotFoundError('Pedido no encontrado')
139
+
140
+ // Lanza ForbiddenError si currentUser.id !== pedido.usuarioId
141
+ // EXCEPTO si currentUser.role === adminRole (admin bypassa el check)
142
+ this.auth.assertOwnership(pedido.usuarioId as string, currentUser.id, currentUser.role)
143
+
144
+ return pedido
145
+ }
146
+ ```
147
+
148
+ **`arckode analyze` detecta:** `IDOR_RISK` — findById sin assertOwnership.
149
+
150
+ ---
151
+
152
+ ## 6. ROLES Y AUTORIZACIÓN
153
+
154
+ ```ts
155
+ // Verificar rol manualmente en service (cuando la lógica es más compleja)
156
+ async cancelarPedido(id: string, currentUser: { id: string; role: string }): Promise<void> {
157
+ const pedido = await this.repo.findById(id)
158
+ if (!pedido) throw new NotFoundError('Pedido no encontrado')
159
+
160
+ // Solo el dueño o admin puede cancelar
161
+ this.auth.assertOwnership(pedido.usuarioId as string, currentUser.id, currentUser.role)
162
+
163
+ // Solo se puede cancelar si está pendiente (lógica de negocio)
164
+ if (pedido.estado !== 'pendiente') {
165
+ throw new ConflictError('Solo se pueden cancelar pedidos pendientes')
166
+ }
167
+
168
+ await this.repo.update(id, { estado: 'cancelado' })
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 7. HASHING DE PASSWORDS
175
+
176
+ ```ts
177
+ // Hashear al registrar o cambiar contraseña
178
+ const hash = await auth.hashPassword(plainPassword)
179
+
180
+ // Verificar al hacer login
181
+ const matches = await auth.comparePassword(plainPassword, storedHash)
182
+
183
+ // Usar timing-safe comparison internamente (previene timing attacks)
184
+ ```
185
+
186
+ **Algoritmo:** scrypt + salt aleatorio. No implementar hashing propio.
187
+
188
+ ---
189
+
190
+ ## 8. REFRESH DE TOKEN
191
+
192
+ ```ts
193
+ // En el controller de auth
194
+ async refreshToken(req: HttpRequest): Promise<HttpResponse> {
195
+ const { refreshToken } = req.body as { refreshToken: string }
196
+ if (!refreshToken) throw new ValidationError('refreshToken requerido')
197
+
198
+ const tokens = await this.service.refresh(refreshToken)
199
+ return { status: 200, body: tokens }
200
+ }
201
+
202
+ // En el service (delegar a auth)
203
+ async refresh(refreshToken: string) {
204
+ return this.auth.refresh(refreshToken) // lanza AuthError si expiró o es inválido
205
+ }
206
+ ```
207
+
208
+ **Importante:** El refresh token tiene `type: 'refresh'` en el payload. `auth.verifyToken()` (para access tokens) rechaza tokens con `type: 'refresh'` — son tokens distintos por diseño.
209
+
210
+ ---
211
+
212
+ ## 9. NUNCA EXPONER AL CLIENTE
213
+
214
+ ```ts
215
+ // ❌ PROHIBIDO en responses HTTP
216
+ {
217
+ password: user.password, // hash de contraseña
218
+ jwtSecret: 'el-secreto', // secreto JWT
219
+ internalId: 'uuid-interno', // IDs internos innecesarios
220
+ stackTrace: error.stack, // stack trace de error
221
+ }
222
+
223
+ // ✅ Proyectar solo lo necesario en el DTO de respuesta
224
+ export interface UserPublicDTO {
225
+ id: string
226
+ email: string
227
+ role: string
228
+ createdAt: string
229
+ // SIN password, SIN tokens internos
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## 10. CHECKLIST AUTH
236
+
237
+ - [ ] `JWT_SECRET` en .env, no hardcodeado
238
+ - [ ] Login usa mismo mensaje para "usuario no existe" y "contraseña incorrecta"
239
+ - [ ] Passwords hasheados con `auth.hashPassword()` al guardar
240
+ - [ ] `auth.authenticate()` en array de middlewares: `[auth.authenticate('admin')]`
241
+ - [ ] Todo `findById` de recurso de usuario tiene `assertOwnership()`
242
+ - [ ] El DTO de respuesta nunca incluye password, tokens ni stack traces
243
+ - [ ] Refresh token endpoint separado del login
@@ -0,0 +1,258 @@
1
+ # SKILL: Arckode CLI — Generadores y Comandos
2
+
3
+ > Activar cuando: crear módulo nuevo, generar código, correr migraciones, analizar arquitectura, listar rutas.
4
+
5
+ ---
6
+
7
+ ## 1. COMANDOS DISPONIBLES
8
+
9
+ ```bash
10
+ arckode new <nombre> # Crear proyecto nuevo
11
+ arckode make:module <Nombre> # Generar módulo completo (7 archivos)
12
+ arckode make:connector <nombre> <m1> <m2> # Generar conector entre módulos
13
+ arckode make:migration <descripcion> # Crear archivo de migración SQL
14
+ arckode make:seed <nombre> # Crear archivo de seed
15
+ arckode make:auth # Generar módulo de autenticación completo
16
+ arckode analyze # Detectar violaciones de arquitectura
17
+ arckode analyze --strict # Idem, falla con exit 1 si hay violaciones
18
+ arckode routes # Listar todas las rutas del proyecto
19
+ arckode db:migrate # Correr migraciones pendientes
20
+ arckode db:migrate --rollback # Revertir última migración
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 2. `arckode make:module` — Lo que genera
26
+
27
+ ```bash
28
+ arckode make:module Producto
29
+ ```
30
+
31
+ Genera en `src/modules/producto/`:
32
+ ```
33
+ index.ts ← ProductoModule() con createModule + OrmRepository + rutas
34
+ types.ts ← ProductoModel (ModelDefinition) + ProductoDTO + CreateProductoDTO
35
+ sockets.ts ← ProductosSockets interface + SocketsAware
36
+ actions/
37
+ service.ts ← ProductosService con RepositoryAdapter<ProductoDTO>
38
+ controller.ts ← ProductosController con validateSchema en store/update
39
+ validators/
40
+ schema.ts ← crearProductoSchema + actualizarProductoSchema
41
+ tests/
42
+ service.test.ts ← 2 test cases: listar + crear con error
43
+ ```
44
+
45
+ **Importante:** El nombre se usa en PascalCase para clases, camelCase para variables, kebab-case para rutas:
46
+ - `arckode make:module LineaPedido` → clase `LineaPedidoService`, tabla `linea_pedidos`, ruta `/linea-pedidos`
47
+
48
+ **Después de generar:**
49
+ 1. Revisar `types.ts` → ajustar campos al dominio real
50
+ 2. Revisar `validators/schema.ts` → ajustar validaciones
51
+ 3. Agregar a `composition-root.ts`:
52
+ ```ts
53
+ import { ProductoModel } from './modules/producto/types'
54
+ import { ProductoModule } from './modules/producto'
55
+
56
+ orm.define('Producto', ProductoModel)
57
+ system.addModule(ProductoModule())
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 3. `arckode make:connector` — Lo que genera
63
+
64
+ ```bash
65
+ arckode make:connector pedido-inventario pedidos inventario
66
+ ```
67
+
68
+ Genera `src/connectors/pedido-inventario.ts`:
69
+ ```ts
70
+ import type { ConnectorContext } from 'arckode-framework'
71
+ import type { PedidosService } from '../modules/pedidos'
72
+ import type { InventarioService } from '../modules/inventario'
73
+
74
+ export function conectarPedidoConInventario(ctx: ConnectorContext): void {
75
+ const pedidos = ctx.resolveModule<PedidosService>('pedidos')
76
+ const inventario = ctx.resolveModule<InventarioService>('inventario')
77
+
78
+ // TODO: inyectar sockets
79
+ pedidos.setSockets({
80
+ // onEventoEjemplo: async (data) => { await inventario.accion(data) }
81
+ })
82
+ }
83
+ ```
84
+
85
+ **Después de generar:**
86
+ 1. Implementar los sockets necesarios
87
+ 2. Agregar a `composition-root.ts`:
88
+ ```ts
89
+ import { conectarPedidoConInventario } from './connectors/pedido-inventario'
90
+ system.addConnector('pedido-inventario', conectarPedidoConInventario)
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 4. `arckode analyze` — Violaciones detectadas
96
+
97
+ ```bash
98
+ arckode analyze
99
+ ```
100
+
101
+ Output ejemplo:
102
+ ```
103
+ 🔍 Analizando proyecto...
104
+
105
+ ❌ DIRECT_MODULE_IMPORT modules/pedidos/actions/service.ts:5
106
+ "import { ProductosService } from '../productos/actions/service'"
107
+ → Los módulos no pueden importar de otros módulos directamente.
108
+
109
+ ❌ CONTROLLER_MISSING_VALIDATION modules/productos/actions/controller.ts:23
110
+ "router.post sin validateSchema()"
111
+ → Todo POST debe validar el body antes de llamar al service.
112
+
113
+ ⚠️ MISSING_SOCKETS modules/usuarios/
114
+ → El módulo no tiene sockets.ts
115
+
116
+ ✅ Sin violaciones en: pedidos, inventario, auth
117
+
118
+ Resumen: 2 errores, 1 advertencia
119
+ ```
120
+
121
+ **Violaciones críticas (❌):** Deben corregirse antes del merge.
122
+ **Advertencias (⚠️):** Recomendadas pero no bloqueantes.
123
+
124
+ ### Tabla completa de checks
125
+
126
+ | Código | Severidad | Descripción |
127
+ |--------|-----------|-------------|
128
+ | `MISSING_INDEX` | ❌ | Módulo sin index.ts |
129
+ | `MISSING_SERVICE` | ❌ | Sin actions/service.ts |
130
+ | `MISSING_CONTROLLER` | ❌ | Sin actions/controller.ts |
131
+ | `MISSING_TYPES` | ❌ | Sin types.ts |
132
+ | `MISSING_VALIDATORS` | ❌ | Sin validators/schema.ts |
133
+ | `MISSING_TESTS` | ❌ | Sin directorio tests/ |
134
+ | `MISSING_SOCKETS` | ⚠️ | Sin sockets.ts |
135
+ | `MISSING_CONNECTORS` | ⚠️ | 2+ módulos sin conectores definidos |
136
+ | `DIRECT_MODULE_IMPORT` | ❌ | Un módulo importa de otro |
137
+ | `CONNECTOR_BUSINESS_LOGIC` | ❌ | Conector con if/for/lógica |
138
+ | `CONTROLLER_MISSING_VALIDATION` | ❌ | POST/PUT/PATCH sin validateSchema |
139
+ | `BUSINESS_LOGIC_IN_CONTROLLER` | ❌ | ORM directo en controller |
140
+ | `EMPTY_MODULE_DESCRIPTION` | ❌ | description: '' en createModule |
141
+ | `TESTS_WITHOUT_CASES` | ❌ | Archivo test sin test() |
142
+ | `SERVICE_IMPORTS_OTHER_MODULE` | ❌ | Service importa de otro módulo |
143
+ | `GOD_SERVICE` | ⚠️ | Service > 200 líneas |
144
+ | `N_PLUS_ONE_RISK` | ⚠️ | ORM dentro de loop |
145
+ | `IDOR_RISK` | ❌ | findById sin assertOwnership |
146
+ | `SERVICE_DEPENDS_ON_ORM` | ❌ | Service recibe ORM, no RepositoryAdapter |
147
+
148
+ ---
149
+
150
+ ## 5. `arckode routes` — Ver rutas registradas
151
+
152
+ ```bash
153
+ arckode routes
154
+ ```
155
+
156
+ Output:
157
+ ```
158
+ GET /productos
159
+ POST /productos [validate: crearProductoSchema]
160
+ GET /productos/:id
161
+ PUT /productos/:id [auth: admin] [validate: actualizarProductoSchema]
162
+ DELETE /productos/:id [auth: admin]
163
+ POST /auth/login
164
+ POST /auth/registro
165
+ POST /auth/refresh
166
+ ```
167
+
168
+ ---
169
+
170
+ ## 6. `arckode db:migrate` — Migraciones SQL
171
+
172
+ ```bash
173
+ # Ver pendientes y correrlos
174
+ arckode db:migrate
175
+
176
+ # Revertir último
177
+ arckode db:migrate --rollback
178
+ ```
179
+
180
+ **Tabla de control:** `_arckode_migrations` (columnas: name, runAt, direction)
181
+
182
+ **Los archivos de migración van en `src/migrations/`:**
183
+ ```
184
+ src/migrations/
185
+ 1716000000_create_productos.ts
186
+ 1716100000_add_stock_to_productos.ts
187
+ 1716200000_add_index_pedidos_usuario.ts
188
+ ```
189
+
190
+ El timestamp en el nombre determina el orden de ejecución.
191
+
192
+ ---
193
+
194
+ ## 7. `arckode new` — Proyecto desde cero
195
+
196
+ ```bash
197
+ arckode new mi-api
198
+ cd mi-api
199
+ ```
200
+
201
+ Genera:
202
+ ```
203
+ mi-api/
204
+ ├── src/
205
+ │ ├── composition-root.ts ← con loadEnv(), ORM, Router, System
206
+ │ └── modules/
207
+ │ └── .gitkeep
208
+ ├── .env.example
209
+ ├── .gitignore
210
+ ├── CLAUDE.md ← apunta a este skill
211
+ ├── package.json ← arckode-framework como dependencia
212
+ └── tsconfig.json ← strict mode
213
+ ```
214
+
215
+ ---
216
+
217
+ ## 8. INTEGRAR CON CI/CD
218
+
219
+ ```yaml
220
+ # .github/workflows/ci.yml
221
+ - name: Analyze architecture
222
+ run: arckode analyze --strict # falla el CI si hay violaciones
223
+
224
+ - name: Run tests
225
+ run: bun test
226
+
227
+ - name: Type check
228
+ run: bun run --bun tsc --noEmit
229
+ ```
230
+
231
+ ---
232
+
233
+ ## 9. WORKFLOW COMPLETO — Agregar feature
234
+
235
+ ```bash
236
+ # 1. Generar el módulo
237
+ arckode make:module Pago
238
+
239
+ # 2. Ajustar types.ts con los campos reales
240
+
241
+ # 3. Ajustar validators/schema.ts
242
+
243
+ # 4. Implementar service.ts (lógica de negocio)
244
+
245
+ # 5. Si se conecta con otro módulo
246
+ arckode make:connector pago-pedido pagos pedidos
247
+
248
+ # 6. Agregar a composition-root.ts
249
+
250
+ # 7. Si necesita migración SQL
251
+ arckode make:migration add_pagos_table
252
+
253
+ # 8. Verificar todo
254
+ bun run src/composition-root.ts # sin errores TypeScript
255
+ arckode analyze # 0 violaciones críticas
256
+ bun test modules/pago # tests pasan
257
+ arckode routes # rutas visibles
258
+ ```