arckode-framework 1.3.2 → 1.4.1

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 (64) hide show
  1. package/adapters/jwt.ts +6 -4
  2. package/adapters/mysql.ts +7 -2
  3. package/adapters/postgres.ts +37 -0
  4. package/adapters/sqlite.ts +7 -1
  5. package/adapters/vendor.d.ts +48 -0
  6. package/cli/analyze/checks.ts +333 -0
  7. package/cli/analyze/index.ts +44 -0
  8. package/cli/analyze/report.ts +107 -0
  9. package/cli/analyze/types.ts +46 -0
  10. package/cli/analyze/utils.ts +36 -0
  11. package/cli/analyze.ts +2 -647
  12. package/cli/commands/db-migrate.ts +213 -89
  13. package/cli/commands/db-seed.ts +97 -32
  14. package/cli/commands/db-utils.ts +192 -0
  15. package/cli/commands/new.ts +175 -0
  16. package/cli/commands/routes.ts +94 -0
  17. package/cli/index.ts +57 -404
  18. package/cli/stubs/module/core.ts +162 -0
  19. package/cli/stubs/module/data.ts +171 -0
  20. package/cli/stubs/module/index.ts +5 -0
  21. package/cli/stubs/module/service.ts +198 -0
  22. package/cli/stubs/module/types.ts +12 -0
  23. package/cli/stubs/module-stub.ts +2 -552
  24. package/kernel/auth.ts +114 -0
  25. package/kernel/cache.ts +37 -0
  26. package/kernel/config.ts +129 -0
  27. package/kernel/container.ts +64 -0
  28. package/kernel/db/orm-migrate.ts +136 -0
  29. package/kernel/db/orm-repository.ts +45 -0
  30. package/kernel/db/orm-utils.ts +93 -0
  31. package/kernel/db/orm.ts +254 -0
  32. package/kernel/db/transactor.ts +17 -0
  33. package/kernel/db/types.ts +72 -0
  34. package/kernel/errors.ts +102 -0
  35. package/kernel/framework.default.ts +41 -0
  36. package/kernel/framework.ts +8 -2144
  37. package/kernel/http/router.ts +131 -0
  38. package/kernel/http/server.ts +303 -0
  39. package/kernel/http/types.ts +56 -0
  40. package/kernel/index.ts +25 -0
  41. package/kernel/logger.ts +50 -0
  42. package/kernel/middlewares.ts +19 -7
  43. package/kernel/modules/create-module.ts +5 -0
  44. package/kernel/modules/system.ts +149 -0
  45. package/kernel/modules/types.ts +46 -0
  46. package/kernel/seeds.ts +48 -0
  47. package/kernel/static.ts +11 -2
  48. package/kernel/testing.ts +8 -3
  49. package/kernel/validator.ts +116 -0
  50. package/modules/events/index.ts +19 -3
  51. package/modules/mail/index.ts +14 -2
  52. package/modules/storage/local-adapter.ts +19 -5
  53. package/modules/ws/index.ts +123 -18
  54. package/package.json +8 -11
  55. package/skills/auth/SKILL.md +36 -220
  56. package/skills/cli/SKILL.md +32 -251
  57. package/skills/config/SKILL.md +30 -239
  58. package/skills/connectors/SKILL.md +32 -295
  59. package/skills/helpers/SKILL.md +26 -195
  60. package/skills/middlewares/SKILL.md +30 -280
  61. package/skills/orm/SKILL.md +42 -349
  62. package/skills/realtime/SKILL.md +22 -297
  63. package/skills/services/SKILL.md +40 -183
  64. package/skills/testing/SKILL.md +34 -266
@@ -0,0 +1,131 @@
1
+ import { ErrorContract } from '../errors'
2
+ import type { Logger } from '../logger'
3
+ import type { HttpRequest, HttpResponse, MiddlewareHandler, RouteHandler } from './types'
4
+
5
+ interface RouteEntry {
6
+ method: string
7
+ pattern: RegExp
8
+ paramNames: string[]
9
+ handler: RouteHandler
10
+ middlewares: MiddlewareHandler[]
11
+ }
12
+
13
+ export class Router {
14
+ private routes: RouteEntry[] = []
15
+ private globalMiddlewares: MiddlewareHandler[] = []
16
+ private logger?: Logger
17
+
18
+ setLogger(logger: Logger): void { this.logger = logger }
19
+
20
+ use(middleware: MiddlewareHandler): void {
21
+ this.globalMiddlewares.push(middleware)
22
+ }
23
+
24
+ private add(method: string, path: string, handler: RouteHandler, middlewares: MiddlewareHandler[] = []): void {
25
+ const paramNames: string[] = []
26
+ const parts = path.split('/').filter(Boolean)
27
+ const regexParts = parts.map(part => {
28
+ if (part.startsWith(':')) {
29
+ const rawName = part.slice(1)
30
+ if (rawName.endsWith('(*)')) {
31
+ paramNames.push(rawName.slice(0, -3))
32
+ return '(.*)'
33
+ }
34
+ paramNames.push(rawName)
35
+ return '([^/]+)'
36
+ }
37
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
38
+ })
39
+
40
+ this.routes.push({
41
+ method,
42
+ pattern: new RegExp(`^/${regexParts.join('/')}/?$`),
43
+ paramNames,
44
+ handler,
45
+ middlewares,
46
+ })
47
+ }
48
+
49
+ private resolveRouteArgs(
50
+ handlerOrMw: RouteHandler | MiddlewareHandler[],
51
+ handlerOrUndefined?: RouteHandler | MiddlewareHandler[],
52
+ ): { handler: RouteHandler; mw: MiddlewareHandler[] } {
53
+ if (Array.isArray(handlerOrMw)) {
54
+ return { handler: handlerOrUndefined as RouteHandler, mw: handlerOrMw }
55
+ }
56
+ return { handler: handlerOrMw, mw: (handlerOrUndefined as MiddlewareHandler[] | undefined) ?? [] }
57
+ }
58
+
59
+ get(path: string, handlerOrMw: RouteHandler | MiddlewareHandler[], h?: RouteHandler | MiddlewareHandler[]): void { const { handler, mw } = this.resolveRouteArgs(handlerOrMw, h); this.add('GET', path, handler, mw) }
60
+ post(path: string, handlerOrMw: RouteHandler | MiddlewareHandler[], h?: RouteHandler | MiddlewareHandler[]): void { const { handler, mw } = this.resolveRouteArgs(handlerOrMw, h); this.add('POST', path, handler, mw) }
61
+ put(path: string, handlerOrMw: RouteHandler | MiddlewareHandler[], h?: RouteHandler | MiddlewareHandler[]): void { const { handler, mw } = this.resolveRouteArgs(handlerOrMw, h); this.add('PUT', path, handler, mw) }
62
+ patch(path: string, handlerOrMw: RouteHandler | MiddlewareHandler[], h?: RouteHandler | MiddlewareHandler[]): void { const { handler, mw } = this.resolveRouteArgs(handlerOrMw, h); this.add('PATCH', path, handler, mw) }
63
+ delete(path: string, handlerOrMw: RouteHandler | MiddlewareHandler[], h?: RouteHandler | MiddlewareHandler[]): void { const { handler, mw } = this.resolveRouteArgs(handlerOrMw, h); this.add('DELETE', path, handler, mw) }
64
+ options(path: string, handlerOrMw: RouteHandler | MiddlewareHandler[], h?: RouteHandler | MiddlewareHandler[]): void { const { handler, mw } = this.resolveRouteArgs(handlerOrMw, h); this.add('OPTIONS', path, handler, mw) }
65
+
66
+ async resolve(method: string, path: string, extras?: Partial<HttpRequest>): Promise<HttpResponse> {
67
+ const reqId = crypto.randomUUID().slice(0, 8)
68
+
69
+ const buildRequest = (route: RouteEntry, match: RegExpMatchArray): HttpRequest => {
70
+ const params: Record<string, string> = {}
71
+ route.paramNames.forEach((name, i) => { params[name] = decodeURIComponent(match[i + 1] ?? '') })
72
+ return {
73
+ id: reqId,
74
+ method,
75
+ path,
76
+ params,
77
+ query: extras?.query ?? {},
78
+ headers: extras?.headers ?? {},
79
+ body: extras?.body ?? null,
80
+ user: extras?.user,
81
+ }
82
+ }
83
+
84
+ const runAll = async (req: HttpRequest, routeMiddlewares: MiddlewareHandler[], routeHandler: RouteHandler): Promise<HttpResponse> => {
85
+ const allMiddlewares = [...this.globalMiddlewares, ...routeMiddlewares]
86
+ let index = 0
87
+ const next = async (): Promise<HttpResponse> => {
88
+ if (index < allMiddlewares.length) {
89
+ const mw = allMiddlewares[index++]!
90
+ return mw(req, next)
91
+ }
92
+ return routeHandler(req)
93
+ }
94
+ try {
95
+ return await next()
96
+ } catch (error) {
97
+ if (error instanceof ErrorContract) {
98
+ return { status: error.httpStatus, body: error.toJSON() }
99
+ }
100
+ if (this.logger) {
101
+ const stack = error instanceof Error ? error.stack : String(error)
102
+ this.logger.error(`Unhandled error in ${method} ${path}`, { stack, requestId: reqId })
103
+ }
104
+ return { status: 500, body: { error: 'Error interno del servidor', code: 'INTERNAL_ERROR' } }
105
+ }
106
+ }
107
+
108
+ if (method === 'OPTIONS') {
109
+ let anyMatch: { route: RouteEntry; match: RegExpMatchArray } | null = null
110
+ for (const route of this.routes) {
111
+ const m = path.match(route.pattern)
112
+ if (m) { anyMatch = { route, match: m }; break }
113
+ }
114
+ if (anyMatch) {
115
+ const req = buildRequest(anyMatch.route, anyMatch.match)
116
+ return runAll(req, [], () => Promise.resolve({ status: 204, body: null }))
117
+ }
118
+ }
119
+
120
+ for (const route of this.routes) {
121
+ if (route.method !== method) continue
122
+ const match = path.match(route.pattern)
123
+ if (!match) continue
124
+
125
+ const req = buildRequest(route, match)
126
+ return runAll(req, route.middlewares, route.handler)
127
+ }
128
+
129
+ return { status: 404, body: { error: 'Route not found', code: 'NOT_FOUND' } }
130
+ }
131
+ }
@@ -0,0 +1,303 @@
1
+ import { createServer as createNodeServer, IncomingMessage, ServerResponse } from 'node:http'
2
+ import { AsyncLocalStorage } from 'node:async_hooks'
3
+ import { PayloadTooLargeError } from '../errors'
4
+ import type { Logger } from '../logger'
5
+ import type { ApiResponse, HttpRequest, HttpResponse, UploadedFile } from './types'
6
+
7
+ export const requestStorage = new AsyncLocalStorage<{ requestId: string; startTime: number }>()
8
+
9
+ export function getRequestId(): string | undefined {
10
+ return requestStorage.getStore()?.requestId
11
+ }
12
+
13
+ export function getRequestElapsed(): number | undefined {
14
+ const store = requestStorage.getStore()
15
+ return store ? Date.now() - store.startTime : undefined
16
+ }
17
+
18
+ export interface ServerAdapter {
19
+ start(handler: (req: HttpRequest) => Promise<HttpResponse>): Promise<void>
20
+ stop(): Promise<void>
21
+ }
22
+
23
+ function buildEnvelope(status: number, body: unknown): string {
24
+ if (status >= 400) {
25
+ const b = (body ?? {}) as Record<string, unknown>
26
+ return JSON.stringify({
27
+ success: false,
28
+ data: null,
29
+ meta: null,
30
+ error: {
31
+ code: (b.code ?? 'ERROR') as string,
32
+ message: (b.error ?? 'Error') as string,
33
+ details: b.details ?? null,
34
+ },
35
+ } satisfies ApiResponse)
36
+ }
37
+
38
+ if (body === null || body === undefined) {
39
+ return JSON.stringify({ success: true, data: null, meta: null, error: null } satisfies ApiResponse)
40
+ }
41
+
42
+ const b = body as Record<string, unknown>
43
+
44
+ if (Array.isArray(b.data) && (b.pagination !== undefined || b.total !== undefined)) {
45
+ const pagination = b.pagination ?? { total: b.total, limit: b.limit, offset: b.offset, pages: b.pages }
46
+ return JSON.stringify({
47
+ success: true,
48
+ data: b.data,
49
+ meta: { pagination },
50
+ error: null,
51
+ } satisfies ApiResponse)
52
+ }
53
+
54
+ return JSON.stringify({ success: true, data: body, meta: null, error: null } satisfies ApiResponse)
55
+ }
56
+
57
+ function indexOfBuffer(haystack: Buffer, needle: Buffer, offset = 0): number {
58
+ const limit = haystack.length - needle.length
59
+ outer: for (let i = offset; i <= limit; i++) {
60
+ for (let j = 0; j < needle.length; j++) {
61
+ if (haystack[i + j] !== needle[j]) continue outer
62
+ }
63
+ return i
64
+ }
65
+ return -1
66
+ }
67
+
68
+ export class NodeServer implements ServerAdapter {
69
+ private server?: ReturnType<typeof createNodeServer>
70
+ private maxBodyBytes: number
71
+ private drainTimeoutMs: number
72
+ private activeRequests = 0
73
+
74
+ constructor(
75
+ private port: number,
76
+ private logger: Logger,
77
+ opts: { maxBodyBytes?: number; drainTimeoutMs?: number } = {},
78
+ ) {
79
+ this.maxBodyBytes = opts.maxBodyBytes ?? 10 * 1024 * 1024
80
+ this.drainTimeoutMs = opts.drainTimeoutMs ?? 30_000
81
+ }
82
+
83
+ async start(handler: (req: HttpRequest) => Promise<HttpResponse>): Promise<void> {
84
+ return new Promise((resolve) => {
85
+ this.server = createNodeServer(async (nodeReq: IncomingMessage, nodeRes: ServerResponse) => {
86
+ this.activeRequests++
87
+ const requestId = crypto.randomUUID().slice(0, 8)
88
+
89
+ await requestStorage.run({ requestId, startTime: Date.now() }, async () => {
90
+ try {
91
+ const { body, files } = await this.readBody(nodeReq, this.maxBodyBytes)
92
+
93
+ const url = new URL(nodeReq.url ?? '/', `http://${nodeReq.headers.host ?? 'localhost'}`)
94
+ const query: Record<string, string> = {}
95
+ url.searchParams.forEach((value, key) => { query[key] = value })
96
+
97
+ const req: HttpRequest = {
98
+ id: requestId,
99
+ method: nodeReq.method ?? 'GET',
100
+ path: url.pathname,
101
+ params: {},
102
+ query,
103
+ headers: nodeReq.headers as Record<string, string>,
104
+ body,
105
+ remoteAddress: nodeReq.socket?.remoteAddress,
106
+ ...(files ? { files } : {}),
107
+ }
108
+
109
+ const res = await handler(req)
110
+
111
+ if (res.stream) {
112
+ nodeRes.writeHead(res.status, {
113
+ 'Content-Type': 'text/event-stream',
114
+ 'Cache-Control': 'no-cache',
115
+ Connection: 'keep-alive',
116
+ 'X-Request-Id': req.id,
117
+ ...res.headers,
118
+ })
119
+ try {
120
+ for await (const chunk of res.stream) {
121
+ nodeRes.write(`data: ${chunk}\n\n`)
122
+ }
123
+ } finally {
124
+ nodeRes.end()
125
+ }
126
+ return
127
+ }
128
+
129
+ // RFC 7231 §6.3.5 — 204 No Content: no body
130
+ if (res.status === 204) {
131
+ nodeRes.writeHead(204, { 'X-Request-Id': req.id, ...res.headers })
132
+ nodeRes.end()
133
+ return
134
+ }
135
+
136
+ const isBuffer = Buffer.isBuffer(res.body)
137
+ const responseBody = isBuffer ? res.body : buildEnvelope(res.status, res.body)
138
+ nodeRes.writeHead(res.status, {
139
+ 'Content-Type': 'application/json',
140
+ 'X-Request-Id': req.id,
141
+ ...res.headers,
142
+ })
143
+ nodeRes.end(responseBody)
144
+ } catch (error) {
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ const httpStatus = (error as any)?.httpStatus as number | undefined
147
+ if (httpStatus) {
148
+ const msg = httpStatus < 500
149
+ ? (error as Error).message
150
+ : 'Error interno'
151
+ if (httpStatus >= 500) {
152
+ this.logger.error('Server error', { error: String(error), httpStatus })
153
+ }
154
+ nodeRes.writeHead(httpStatus, { 'Content-Type': 'application/json' })
155
+ nodeRes.end(buildEnvelope(httpStatus, { error: msg, code: 'REQUEST_ERROR' }))
156
+ return
157
+ }
158
+
159
+ const stack = error instanceof Error ? error.stack : String(error)
160
+ this.logger.error('Error no manejado en HTTP', { error: String(error), stack })
161
+ nodeRes.writeHead(500, { 'Content-Type': 'application/json' })
162
+ nodeRes.end(buildEnvelope(500, { error: 'Error interno', code: 'INTERNAL_ERROR' }))
163
+ } finally {
164
+ this.activeRequests--
165
+ }
166
+ })
167
+ })
168
+
169
+ this.server.listen(this.port, () => {
170
+ this.logger.info(`Servidor HTTP escuchando en :${this.port}`)
171
+ resolve()
172
+ })
173
+ })
174
+ }
175
+
176
+ async stop(): Promise<void> {
177
+ return new Promise((resolve) => {
178
+ let resolved = false
179
+ const done = () => { if (!resolved) { resolved = true; resolve() } }
180
+
181
+ this.server?.close(done)
182
+
183
+ const deadline = Date.now() + this.drainTimeoutMs
184
+ const poll = setInterval(() => {
185
+ if (this.activeRequests === 0 || Date.now() >= deadline) {
186
+ clearInterval(poll)
187
+ if (this.activeRequests > 0) {
188
+ this.logger.warn(`Graceful shutdown: ${this.activeRequests} request(s) sin terminar, forzando cierre`)
189
+ }
190
+ done()
191
+ }
192
+ }, 50)
193
+ })
194
+ }
195
+
196
+ getPort(): number {
197
+ const addr = this.server?.address()
198
+ return typeof addr === 'object' && addr ? addr.port : this.port
199
+ }
200
+
201
+ private readBody(
202
+ req: IncomingMessage,
203
+ maxBytes = 10 * 1024 * 1024,
204
+ ): Promise<{ body: unknown; files?: Record<string, UploadedFile> }> {
205
+ return new Promise((resolve, reject) => {
206
+ const chunks: Buffer[] = []
207
+ let total = 0
208
+
209
+ req.on('data', (chunk: Buffer) => {
210
+ total += chunk.length
211
+ if (total > maxBytes) {
212
+ req.destroy()
213
+ reject(new PayloadTooLargeError())
214
+ return
215
+ }
216
+ chunks.push(chunk)
217
+ })
218
+
219
+ req.on('end', () => {
220
+ const rawBuffer = Buffer.concat(chunks)
221
+ if (!rawBuffer.length) return resolve({ body: null })
222
+
223
+ const contentType = (req.headers['content-type'] ?? '').toLowerCase()
224
+ const boundaryMatch = contentType.match(/multipart\/form-data;\s*boundary=(.+)/)
225
+
226
+ if (boundaryMatch) {
227
+ const boundary = (boundaryMatch[1] ?? '').trim()
228
+ const { fields, files } = this.parseMultipart(rawBuffer, boundary)
229
+ return resolve({ body: fields, files })
230
+ }
231
+
232
+ const raw = rawBuffer.toString()
233
+ try { resolve({ body: JSON.parse(raw) }) } catch { resolve({ body: raw }) }
234
+ })
235
+
236
+ req.on('error', reject)
237
+ })
238
+ }
239
+
240
+ private parseMultipart(
241
+ buffer: Buffer,
242
+ boundary: string,
243
+ ): { fields: Record<string, string>; files: Record<string, UploadedFile> } {
244
+ const fields: Record<string, string> = {}
245
+ const files: Record<string, UploadedFile> = {}
246
+
247
+ const firstDelim = Buffer.from(`--${boundary}`)
248
+ const innerDelim = Buffer.from(`\r\n--${boundary}`)
249
+ const doubleCRLF = Buffer.from('\r\n\r\n')
250
+
251
+ let pos = indexOfBuffer(buffer, firstDelim, 0)
252
+ if (pos === -1) return { fields, files }
253
+ pos += firstDelim.length
254
+
255
+ while (pos < buffer.length) {
256
+ if (buffer[pos] === 0x2d && buffer[pos + 1] === 0x2d) break
257
+ if (buffer[pos] === 0x0d && buffer[pos + 1] === 0x0a) pos += 2
258
+ else break
259
+
260
+ const headerEnd = indexOfBuffer(buffer, doubleCRLF, pos)
261
+ if (headerEnd === -1) break
262
+
263
+ const headerStr = buffer.subarray(pos, headerEnd).toString()
264
+ pos = headerEnd + 4
265
+
266
+ const nextBound = indexOfBuffer(buffer, innerDelim, pos)
267
+ if (nextBound === -1) break
268
+
269
+ const partBody = buffer.subarray(pos, nextBound)
270
+ pos = nextBound + innerDelim.length
271
+
272
+ const headers: Record<string, string> = {}
273
+ for (const line of headerStr.split('\r\n')) {
274
+ const colon = line.indexOf(':')
275
+ if (colon === -1) continue
276
+ headers[line.slice(0, colon).toLowerCase().trim()] = line.slice(colon + 1).trim()
277
+ }
278
+
279
+ const disp = headers['content-disposition'] ?? ''
280
+ const nameMatch = /name="([^"]*)"/.exec(disp)
281
+ const fileMatch = /filename="([^"]*)"/.exec(disp)
282
+ if (!nameMatch) continue
283
+
284
+ const fieldName = nameMatch[1] ?? ''
285
+
286
+ if (fileMatch) {
287
+ files[fieldName] = {
288
+ fieldName,
289
+ originalName: fileMatch[1] ?? '',
290
+ buffer: partBody,
291
+ mimeType: headers['content-type'] ?? 'application/octet-stream',
292
+ size: partBody.length,
293
+ }
294
+ } else {
295
+ fields[fieldName] = partBody.toString()
296
+ }
297
+
298
+ if (buffer[pos] === 0x2d && buffer[pos + 1] === 0x2d) break
299
+ }
300
+
301
+ return { fields, files }
302
+ }
303
+ }
@@ -0,0 +1,56 @@
1
+ export interface UploadedFile {
2
+ fieldName: string
3
+ originalName: string
4
+ buffer: Buffer
5
+ mimeType: string
6
+ size: number
7
+ }
8
+
9
+ export interface HttpRequest {
10
+ id: string
11
+ method: string
12
+ path: string
13
+ params: Record<string, string>
14
+ query: Record<string, string>
15
+ headers: Record<string, string>
16
+ body: unknown
17
+ files?: Record<string, UploadedFile>
18
+ user?: { id: string; role: string }
19
+ /** IP real del cliente extraída del socket TCP. No spoofeable. */
20
+ remoteAddress?: string
21
+ }
22
+
23
+ export interface HttpResponse {
24
+ status: number
25
+ body?: unknown
26
+ headers?: Record<string, string>
27
+ /** SSE / streaming: async generator que emite chunks de texto plano */
28
+ stream?: AsyncGenerator<string>
29
+ }
30
+
31
+ /** Helper: crea una respuesta SSE a partir de un async generator. */
32
+ export function sseResponse(
33
+ generator: AsyncGenerator<string>,
34
+ headers?: Record<string, string>,
35
+ ): HttpResponse {
36
+ return {
37
+ status: 200,
38
+ headers: {
39
+ 'Content-Type': 'text/event-stream',
40
+ 'Cache-Control': 'no-cache',
41
+ Connection: 'keep-alive',
42
+ ...headers,
43
+ },
44
+ stream: generator,
45
+ }
46
+ }
47
+
48
+ export type RouteHandler = (req: HttpRequest) => HttpResponse | Promise<HttpResponse>
49
+ export type MiddlewareHandler = (req: HttpRequest, next: () => Promise<HttpResponse>) => Promise<HttpResponse>
50
+
51
+ export interface ApiResponse<T = unknown> {
52
+ success: boolean
53
+ data: T | null
54
+ meta: { pagination?: unknown } | null
55
+ error: { code: string; message: string; details?: unknown } | null
56
+ }
@@ -0,0 +1,25 @@
1
+ // Arckode Framework — Kernel index
2
+ // Re-exporta todos los módulos del kernel. Backward compat: cualquier
3
+ // import de 'arckode-framework' sigue funcionando sin cambios.
4
+
5
+ export * from './errors'
6
+ export * from './logger'
7
+ export * from './config'
8
+ export * from './container'
9
+ export * from './cache'
10
+ export * from './validator'
11
+ export * from './auth'
12
+ export * from './seeds'
13
+
14
+ export * from './db/types'
15
+ export * from './db/orm'
16
+ export * from './db/orm-repository'
17
+ export * from './db/transactor'
18
+
19
+ export * from './http/types'
20
+ export * from './http/router'
21
+ export * from './http/server'
22
+
23
+ export * from './modules/types'
24
+ export * from './modules/create-module'
25
+ export * from './modules/system'
@@ -0,0 +1,50 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
2
+
3
+ export interface LoggerTransport {
4
+ write(entry: { timestamp: string; level: LogLevel; source: string; message: string; meta?: Record<string, unknown> }): void
5
+ }
6
+
7
+ export class Logger {
8
+ constructor(
9
+ public readonly source: string = 'app',
10
+ private level: LogLevel = 'info',
11
+ private transports: LoggerTransport[] = [new ConsoleTransport()],
12
+ ) {}
13
+
14
+ child(name: string): Logger {
15
+ return new Logger(`${this.source}.${name}`, this.level, this.transports)
16
+ }
17
+
18
+ debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta) }
19
+ info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta) }
20
+ warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta) }
21
+ error(message: string, meta?: Record<string, unknown>): void { this.emit('error', message, meta) }
22
+
23
+ private emit(level: LogLevel, message: string, meta?: Record<string, unknown>): void {
24
+ const weight = { debug: 0, info: 1, warn: 2, error: 3 }
25
+ if (weight[level] < weight[this.level]) return
26
+
27
+ const entry: Record<string, unknown> = {
28
+ timestamp: new Date().toISOString(),
29
+ level,
30
+ source: this.source,
31
+ message,
32
+ }
33
+
34
+ if (meta && Object.keys(meta).length > 0) {
35
+ entry.meta = { ...meta }
36
+ }
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ for (const t of this.transports) t.write(entry as any)
40
+ }
41
+ }
42
+
43
+ export class ConsoleTransport implements LoggerTransport {
44
+ write(entry: { level: LogLevel; message: string; source: string } & Record<string, unknown>): void {
45
+ const line = JSON.stringify(entry)
46
+ if (entry.level === 'error') console.error(line)
47
+ else if (entry.level === 'warn') console.warn(line)
48
+ else console.log(line)
49
+ }
50
+ }
@@ -14,12 +14,16 @@ export function cors(options: {
14
14
  origins?: string[]
15
15
  methods?: string[]
16
16
  headers?: string[]
17
+ /**
18
+ * Por defecto false. Si true, envía Access-Control-Allow-Credentials: true.
19
+ * Requiere origins específicos (no '*') — los browsers rechazan la combinación.
20
+ */
17
21
  credentials?: boolean
18
22
  } = {}): MiddlewareHandler {
19
23
  const origins = options.origins ?? ['*']
20
24
  const methods = options.methods ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
21
25
  const headers = options.headers ?? ['Content-Type', 'Authorization']
22
- const credentials = options.credentials ?? true
26
+ const credentials = options.credentials ?? false
23
27
 
24
28
  return async (req, next): Promise<HttpResponse> => {
25
29
  const requestOrigin = req.headers['origin'] as string | undefined
@@ -61,14 +65,17 @@ export function rateLimit(opts: {
61
65
  max?: number
62
66
  /**
63
67
  * Función para derivar la clave de rate limit.
64
- * Por defecto: IP del cliente.
65
- * Para rate limit por usuario: `(req) => req.user?.id ?? req.headers['x-forwarded-for'] ?? 'anon'`
68
+ * Por defecto: IP real del socket TCP (no spoofeable).
69
+ * Si el servidor está detrás de un proxy confiable (nginx/Cloudflare), usar:
70
+ * `(req) => req.headers['x-forwarded-for']?.split(',')[0]?.trim() ?? req.remoteAddress ?? 'unknown'`
71
+ * ADVERTENCIA: x-forwarded-for es spoofeable si no validás el proxy.
72
+ * Para rate limit por usuario autenticado: `(req) => req.user?.id ?? req.remoteAddress ?? 'anon'`
66
73
  */
67
74
  keyBy?: (req: HttpRequest) => string
68
75
  } = {}): RateLimitMiddleware {
69
76
  const windowMs = opts.windowMs ?? 60000
70
77
  const max = opts.max ?? 100
71
- const keyBy = opts.keyBy ?? ((req) => (req.headers['x-forwarded-for'] ?? req.headers['host'] ?? 'unknown') as string)
78
+ const keyBy = opts.keyBy ?? ((req) => req.remoteAddress ?? 'unknown')
72
79
  const hits = new Map<string, { count: number; resetAt: number }>()
73
80
 
74
81
  const mw = async (req: HttpRequest, next: () => Promise<HttpResponse>): Promise<HttpResponse> => {
@@ -122,11 +129,16 @@ export function requestLogger(logger: { info: (msg: string, meta?: any) => void
122
129
  }
123
130
 
124
131
  // ─── Body Size Limit ───────────────────────────────────
132
+ // Nota: el límite de bytes en la wire va en NodeServer({ maxBodyBytes }).
133
+ // Este middleware aplica un límite adicional a nivel de aplicación sobre el
134
+ // body ya parseado — útil para JSON muy anidado que expande mucho en memoria.
125
135
  export function bodyLimit(maxBytes: number = 1024 * 1024): MiddlewareHandler {
126
136
  return async (req, next) => {
127
- const bodyStr = JSON.stringify(req.body ?? '')
128
- if (bodyStr.length > maxBytes) {
129
- return { status: 413, body: { error: `Request body too large. Max ${maxBytes} bytes` } }
137
+ if (req.body !== null && req.body !== undefined) {
138
+ const size = Buffer.byteLength(JSON.stringify(req.body), 'utf-8')
139
+ if (size > maxBytes) {
140
+ return { status: 413, body: { error: `Request body too large. Max ${maxBytes} bytes` } }
141
+ }
130
142
  }
131
143
  return next()
132
144
  }
@@ -0,0 +1,5 @@
1
+ import type { ModuleDefinition } from './types'
2
+
3
+ export function createModule<T>(def: ModuleDefinition<T>): ModuleDefinition<T> {
4
+ return def
5
+ }