arckode-framework 1.3.0 → 1.3.2

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.
package/README.md CHANGED
@@ -317,13 +317,63 @@ await mail.send({ to: 'user@example.com', subject: 'Bienvenido', html: '<p>Hola<
317
317
  ### Storage
318
318
 
319
319
  ```ts
320
- import { StorageService } from 'arckode-framework/storage'
321
- import { LocalAdapter } from 'arckode-framework/storage/local'
320
+ import { StorageService } from 'arckode-framework/modules/storage'
321
+ import { LocalStorageAdapter } from 'arckode-framework/modules/storage/local-adapter'
322
+
323
+ // En composition-root.ts
324
+ const storage = new StorageService(
325
+ new LocalStorageAdapter('./uploads', '/uploads')
326
+ )
327
+
328
+ // Subir un archivo
329
+ const stored = await storage.upload(file, 'avatars')
330
+ // stored.url → '/uploads/avatars/1234567890-abc.jpg'
331
+ // stored.path → 'avatars/1234567890-abc.jpg'
332
+ ```
333
+
334
+ ### File Uploads (multipart/form-data)
335
+
336
+ El servidor detecta `Content-Type: multipart/form-data` automáticamente y parsea el body
337
+ sin dependencias externas. Los archivos quedan en `req.files`, los campos de texto en `req.body`.
338
+
339
+ ```ts
340
+ import type { UploadedFile } from 'arckode-framework'
341
+ import { ValidationError } from 'arckode-framework'
322
342
 
323
- const storage = new StorageService(new LocalAdapter({ path: './uploads' }))
324
- const url = await storage.save('avatars/user.png', fileBuffer)
343
+ router.post('/avatar', [auth.authenticate()], async (req) => {
344
+ const file = req.files?.['avatar']
345
+ if (!file) throw new ValidationError('Se requiere un archivo')
346
+
347
+ const stored = await storageService.upload(file, 'avatars')
348
+ return { status: 200, body: { url: stored.url } }
349
+ })
325
350
  ```
326
351
 
352
+ **Desde el cliente:**
353
+
354
+ ```ts
355
+ const form = new FormData()
356
+ form.append('avatar', blob, 'photo.jpg')
357
+ await fetch('/avatar', { method: 'POST', body: form })
358
+ ```
359
+
360
+ **Estructura de `UploadedFile`:**
361
+
362
+ ```ts
363
+ interface UploadedFile {
364
+ fieldName: string // nombre del campo en el form
365
+ originalName: string // nombre del archivo enviado
366
+ buffer: Buffer // contenido binario
367
+ mimeType: string // 'image/jpeg', 'application/pdf', etc.
368
+ size: number // bytes
369
+ }
370
+ ```
371
+
372
+ > Combiná con `bodyLimit()` para limitar el tamaño máximo:
373
+ > ```ts
374
+ > router.post('/avatar', [bodyLimit(5 * 1024 * 1024)], handler) // 5MB máx
375
+ > ```
376
+
327
377
  ---
328
378
 
329
379
  ## Middlewares
@@ -67,7 +67,21 @@ src/modules/mi-modulo/
67
67
 
68
68
  ---
69
69
 
70
- ## Protocolo de la IA (4 pasos obligatorios)
70
+ ## Protocolo de la IA (5 pasos obligatorios)
71
+
72
+ ### ⚠ CREAR un módulo/vista NUEVA → usar arckode-agent (OBLIGATORIO)
73
+
74
+ Si el usuario pide crear un módulo, vista o feature nueva:
75
+ 1. \`arckode-agent scan\` → lee los módulos existentes (tipos, campos, endpoints)
76
+ 2. \`arckode-agent research "<descripción>"\` → investiga el dominio en internet (solo si es nuevo)
77
+ 3. Generar código usando scan + research + este CLAUDE.md
78
+ 4. \`arckode-agent verify\` → si falla, corregir y repetir (max 10 iteraciones)
79
+ 5. Solo decir "listo" cuando verify pasa Y el módulo está en composition-root.ts
80
+
81
+ **NUNCA generar código sin hacer scan primero.**
82
+ **NUNCA inventar campos que ya existen en otro módulo.**
83
+
84
+ ### MODIFICAR código existente → protocolo manual
71
85
 
72
86
  \`\`\`
73
87
  PASO 1 — IDENTIFICAR ALCANCE
@@ -82,12 +96,8 @@ PASO 2 — VERIFICAR REGLAS
82
96
  ¿Comparto una tabla con otro módulo? → NO
83
97
  ¿Inyecto ORM al service? → NO (RepositoryAdapter<T> o Repository del dominio)
84
98
 
85
- PASO 3 — GENERAR (estructura nueva)
86
- Obligatorios: index.ts, model.ts, types.ts, sockets.ts,
87
- service.ts, controller.ts (al root),
88
- validators/schema.ts, tests/service.test.ts
89
- Opcionales: repository.ts (si hay queries del dominio repetidas)
90
- usecases/ (si hay 3+ flujos DIFERENTES, no CRUD)
99
+ PASO 3 — MODIFICAR (código existente)
100
+ Respetar estructura existente del módulo
91
101
 
92
102
  PASO 4 — VERIFICAR
93
103
  bun run analyze --json → violations: 0
@@ -169,9 +179,12 @@ Escalar SOLO cuando la condición lo justifica. Si no hay condición → nivel 1
169
179
  bun run dev # desarrollo con hot reload
170
180
  bun run analyze # verificar arquitectura + actualiza arckode.json
171
181
  bun run analyze:ci # modo CI: exit 1 si hay violaciones
172
- bun arckode make:module Nombre # generar módulo completo
182
+ bun arckode make:module Nombre # generar módulo completo (scaffold)
173
183
  bun arckode make:connector nombre-x mod1 mod2 # conectar dos módulos (kebab-case)
174
184
  bun test # correr todos los tests
185
+ arckode-agent research "descripción" # investigar dominio ANTES de generar código
186
+ arckode-agent verify # verificar typecheck + tests + analyze
187
+ arckode-agent status # ver estado del proyecto (módulos, conectores)
175
188
  \`\`\`
176
189
 
177
190
  ---
@@ -22,25 +22,30 @@ export function cors(options: {
22
22
  const credentials = options.credentials ?? true
23
23
 
24
24
  return async (req, next): Promise<HttpResponse> => {
25
+ const requestOrigin = req.headers['origin'] as string | undefined
26
+
27
+ // Access-Control-Allow-Origin must be a single value — never a comma-separated list.
28
+ // When multiple origins are allowed, echo back only the matching request origin.
29
+ const allowedOrigin = origins.includes('*')
30
+ ? '*'
31
+ : requestOrigin && origins.includes(requestOrigin)
32
+ ? requestOrigin
33
+ : undefined
34
+
25
35
  if (req.method === 'OPTIONS') {
26
- return {
27
- status: 204,
28
- body: null,
29
- headers: {
30
- 'Access-Control-Allow-Origin': origins.includes('*') ? '*' : origins.join(', '),
31
- 'Access-Control-Allow-Methods': methods.join(', '),
32
- 'Access-Control-Allow-Headers': headers.join(', '),
33
- 'Access-Control-Allow-Credentials': String(credentials),
34
- 'Access-Control-Max-Age': '86400',
35
- },
36
+ const h: Record<string, string> = {
37
+ 'Access-Control-Allow-Methods': methods.join(', '),
38
+ 'Access-Control-Allow-Headers': headers.join(', '),
39
+ 'Access-Control-Allow-Credentials': String(credentials),
40
+ 'Access-Control-Max-Age': '86400',
36
41
  }
42
+ if (allowedOrigin) h['Access-Control-Allow-Origin'] = allowedOrigin
43
+ return { status: 204, body: null, headers: h }
37
44
  }
38
45
 
39
46
  const res = await next()
40
- const corsHeaders: Record<string, string> = {
41
- ...res.headers,
42
- 'Access-Control-Allow-Origin': origins.includes('*') ? '*' : origins.join(', '),
43
- }
47
+ const corsHeaders: Record<string, string> = { ...res.headers }
48
+ if (allowedOrigin) corsHeaders['Access-Control-Allow-Origin'] = allowedOrigin
44
49
  if (credentials) corsHeaders['Access-Control-Allow-Credentials'] = 'true'
45
50
  return { ...res, headers: corsHeaders }
46
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arckode-framework",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "AI-first TypeScript/Bun framework. Modular, SOLID, zero magic. The AI reads the composition root and knows everything.",
5
5
  "type": "module",
6
6
  "main": "./kernel/framework.ts",
@@ -17,6 +17,7 @@
17
17
  "./adapters/mysql": "./adapters/mysql.ts",
18
18
  "./modules/mail": "./modules/mail/index.ts",
19
19
  "./modules/storage": "./modules/storage/index.ts",
20
+ "./modules/storage/local-adapter": "./modules/storage/local-adapter.ts",
20
21
  "./modules/queue": "./modules/queue/index.ts",
21
22
  "./testing": "./kernel/testing.ts"
22
23
  },
@@ -57,6 +57,19 @@ router.use(cors({
57
57
 
58
58
  **Preflight:** el middleware maneja `OPTIONS` automáticamente y retorna 204.
59
59
 
60
+ **Múltiples orígenes:** `Access-Control-Allow-Origin` solo acepta UN valor por request
61
+ (no una lista separada por comas — los browsers lo rechazan). El middleware compara el
62
+ header `Origin` del request contra la lista y emite solo el origen que hizo match.
63
+ Si el origin no está en la lista, el header se omite (el browser bloquea el request).
64
+
65
+ ```
66
+ Request: Origin: https://mi-app.com
67
+ Response: Access-Control-Allow-Origin: https://mi-app.com ✅
68
+
69
+ Request: Origin: https://evil.com
70
+ Response: (sin Access-Control-Allow-Origin) → blocked ✅
71
+ ```
72
+
60
73
  ---
61
74
 
62
75
  ## 4. RATE LIMIT