@kava/kava-api-core 1.0.0 → 1.0.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.
Files changed (133) hide show
  1. package/.github/workflows/publish.yml +40 -0
  2. package/dist/{auth.util.d.ts → auth/auth.d.ts} +1 -5
  3. package/dist/{auth.util.js → auth/auth.js} +38 -30
  4. package/dist/auth/auth.js.map +1 -0
  5. package/dist/auth/index.d.ts +1 -0
  6. package/dist/auth/index.js +2 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/{context.util.js → context/context.js} +1 -1
  9. package/dist/context/context.js.map +1 -0
  10. package/dist/context/index.d.ts +1 -0
  11. package/dist/context/index.js +2 -0
  12. package/dist/context/index.js.map +1 -0
  13. package/dist/{controller.util.js → controller/controller.js} +1 -1
  14. package/dist/controller/controller.js.map +1 -0
  15. package/dist/controller/index.d.ts +1 -0
  16. package/dist/controller/index.js +2 -0
  17. package/dist/controller/index.js.map +1 -0
  18. package/dist/{conversion.util.js → conversion/conversion.js} +1 -1
  19. package/dist/conversion/conversion.js.map +1 -0
  20. package/dist/conversion/index.d.ts +1 -0
  21. package/dist/conversion/index.js +2 -0
  22. package/dist/conversion/index.js.map +1 -0
  23. package/dist/{db.util.d.ts → db/db.d.ts} +9 -5
  24. package/dist/{db.util.js → db/db.js} +40 -29
  25. package/dist/db/db.js.map +1 -0
  26. package/dist/db/index.d.ts +1 -0
  27. package/dist/db/index.js +2 -0
  28. package/dist/db/index.js.map +1 -0
  29. package/dist/index.d.ts +14 -13
  30. package/dist/index.js +14 -13
  31. package/dist/index.js.map +1 -1
  32. package/dist/logger/index.d.ts +1 -0
  33. package/dist/logger/index.js +2 -0
  34. package/dist/logger/index.js.map +1 -0
  35. package/dist/{logger.util.js → logger/logger.js} +16 -7
  36. package/dist/logger/logger.js.map +1 -0
  37. package/dist/mail/index.d.ts +1 -0
  38. package/dist/mail/index.js +2 -0
  39. package/dist/mail/index.js.map +1 -0
  40. package/dist/{mail.util.js → mail/mail.js} +1 -1
  41. package/dist/mail/mail.js.map +1 -0
  42. package/dist/middleware/index.d.ts +1 -0
  43. package/dist/middleware/index.js +2 -0
  44. package/dist/middleware/index.js.map +1 -0
  45. package/dist/{middleware.util.js → middleware/middleware.js} +1 -1
  46. package/dist/middleware/middleware.js.map +1 -0
  47. package/dist/model/index.d.ts +1 -0
  48. package/dist/model/index.js +2 -0
  49. package/dist/model/index.js.map +1 -0
  50. package/dist/{model.util.js → model/model.js} +1 -1
  51. package/dist/model/model.js.map +1 -0
  52. package/dist/permission/index.d.ts +1 -0
  53. package/dist/permission/index.js +2 -0
  54. package/dist/permission/index.js.map +1 -0
  55. package/dist/{permission.util.js → permission/permission.js} +1 -1
  56. package/dist/permission/permission.js.map +1 -0
  57. package/dist/registry/index.d.ts +1 -0
  58. package/dist/registry/index.js +2 -0
  59. package/dist/registry/index.js.map +1 -0
  60. package/dist/registry/registry.d.ts +28 -0
  61. package/dist/registry/registry.js +19 -0
  62. package/dist/registry/registry.js.map +1 -0
  63. package/dist/route/index.d.ts +1 -0
  64. package/dist/route/index.js +2 -0
  65. package/dist/route/index.js.map +1 -0
  66. package/dist/{route.util.js → route/route.js} +1 -1
  67. package/dist/route/route.js.map +1 -0
  68. package/dist/storage/index.d.ts +1 -0
  69. package/dist/storage/index.js +2 -0
  70. package/dist/storage/index.js.map +1 -0
  71. package/dist/{storage.util.js → storage/storage.js} +2 -2
  72. package/dist/storage/storage.js.map +1 -0
  73. package/dist/validation/index.d.ts +1 -0
  74. package/dist/validation/index.js +2 -0
  75. package/dist/validation/index.js.map +1 -0
  76. package/dist/{validation.util.js → validation/validation.js} +1 -1
  77. package/dist/validation/validation.js.map +1 -0
  78. package/package.json +2 -2
  79. package/src/{auth.util.ts → auth/auth.ts} +255 -241
  80. package/src/auth/index.ts +1 -0
  81. package/src/{context.util.ts → context/context.ts} +17 -17
  82. package/src/context/index.ts +1 -0
  83. package/src/{controller.util.ts → controller/controller.ts} +236 -236
  84. package/src/controller/index.ts +1 -0
  85. package/src/{conversion.util.ts → conversion/conversion.ts} +64 -64
  86. package/src/conversion/index.ts +1 -0
  87. package/src/{db.util.ts → db/db.ts} +420 -405
  88. package/src/db/index.ts +1 -0
  89. package/src/index.ts +14 -13
  90. package/src/logger/index.ts +1 -0
  91. package/src/{logger.util.ts → logger/logger.ts} +176 -169
  92. package/src/mail/index.ts +1 -0
  93. package/src/{mail.util.ts → mail/mail.ts} +85 -85
  94. package/src/middleware/index.ts +1 -0
  95. package/src/{middleware.util.ts → middleware/middleware.ts} +288 -288
  96. package/src/model/index.ts +1 -0
  97. package/src/{model.util.ts → model/model.ts} +2210 -2210
  98. package/src/permission/index.ts +1 -0
  99. package/src/{permission.util.ts → permission/permission.ts} +136 -136
  100. package/src/registry/index.ts +1 -0
  101. package/src/registry/registry.ts +37 -0
  102. package/src/route/index.ts +1 -0
  103. package/src/{route.util.ts → route/route.ts} +11 -11
  104. package/src/storage/index.ts +1 -0
  105. package/src/{storage.util.ts → storage/storage.ts} +101 -101
  106. package/src/validation/index.ts +1 -0
  107. package/src/{validation.util.ts → validation/validation.ts} +338 -338
  108. package/tsconfig.json +1 -1
  109. package/bun.lock +0 -160
  110. package/dist/auth.util.js.map +0 -1
  111. package/dist/context.util.js.map +0 -1
  112. package/dist/controller.util.js.map +0 -1
  113. package/dist/conversion.util.js.map +0 -1
  114. package/dist/db.util.js.map +0 -1
  115. package/dist/logger.util.js.map +0 -1
  116. package/dist/mail.util.js.map +0 -1
  117. package/dist/middleware.util.js.map +0 -1
  118. package/dist/model.util.js.map +0 -1
  119. package/dist/permission.util.js.map +0 -1
  120. package/dist/route.util.js.map +0 -1
  121. package/dist/storage.util.js.map +0 -1
  122. package/dist/validation.util.js.map +0 -1
  123. /package/dist/{context.util.d.ts → context/context.d.ts} +0 -0
  124. /package/dist/{controller.util.d.ts → controller/controller.d.ts} +0 -0
  125. /package/dist/{conversion.util.d.ts → conversion/conversion.d.ts} +0 -0
  126. /package/dist/{logger.util.d.ts → logger/logger.d.ts} +0 -0
  127. /package/dist/{mail.util.d.ts → mail/mail.d.ts} +0 -0
  128. /package/dist/{middleware.util.d.ts → middleware/middleware.d.ts} +0 -0
  129. /package/dist/{model.util.d.ts → model/model.d.ts} +0 -0
  130. /package/dist/{permission.util.d.ts → permission/permission.d.ts} +0 -0
  131. /package/dist/{route.util.d.ts → route/route.d.ts} +0 -0
  132. /package/dist/{storage.util.d.ts → storage/storage.d.ts} +0 -0
  133. /package/dist/{validation.util.d.ts → validation/validation.d.ts} +0 -0
@@ -1,289 +1,289 @@
1
- import { Elysia, status } from 'elysia'
2
- import { auth, context, logger } from '@utils'
3
-
4
- declare module "elysia" {
5
- interface Elysia {
6
- api(
7
- basePath: string,
8
- controller: {
9
- index ?: any
10
- store ?: any
11
- show ?: any
12
- update ?: any
13
- destroy ?: any
14
- }
15
- ): this
16
- }
17
- }
18
-
19
-
20
- const errors = {
21
- unauthorized: {
22
- status: 401,
23
- message: "Unauthorized!"
24
- },
25
- ratelimited: {
26
- status: 429,
27
- message: "Too many requests!"
28
- },
29
- notfound: {
30
- status: 404,
31
- message: "Endpoint not found!"
32
- },
33
- request: {
34
- status: 400,
35
- message: "Bad Request!"
36
- },
37
- error: {
38
- status: 500,
39
- message: "Endpoint not found!"
40
- }
41
- }
42
-
43
-
44
- export const middleware = {
45
- // =============================>
46
- // ## Middleware: Auth hand;er
47
- // =============================>
48
- Auth: (app: Elysia) => app.derive(async ({ request }) => {
49
- const authHeader = request.headers.get('authorization')
50
-
51
- if (!authHeader || !authHeader.startsWith('Bearer ')) return { user: null, permissions: [], token: null }
52
-
53
- const bearer = authHeader.substring(7).trim()
54
- const result = await auth.verifyAccessToken(bearer, request)
55
-
56
- if (!result) return { user: null, permissions: [], token: null };
57
-
58
- return {
59
- user: result.user,
60
- permissions: result.permissions,
61
- token: result.token,
62
- }
63
- }),
64
-
65
-
66
- // =============================>
67
- // ## Middleware: Private handler
68
- // =============================>
69
- Private: (app: Elysia) => app.derive(async ({ user }: Record<string, any> | any) => {
70
- if (!user) {
71
- throw status(errors.unauthorized.status, { message: errors.unauthorized.message })
72
- }
73
- }),
74
-
75
-
76
- // =============================>
77
- // ## Middleware: Cors handler
78
- // =============================>
79
- Cors: (app: Elysia) => app.onRequest(({ request, set }) => {
80
- const origin = request.headers.get('origin') ?? ''
81
- let allowedOrigin: string = '*'
82
-
83
- const originsConf = process.env.APP_CORS_ORIGINS || '*'
84
-
85
- if (originsConf !== '*') {
86
- try {
87
- const allowedOrigins = JSON.parse(originsConf)
88
- if (Array.isArray(allowedOrigins) && allowedOrigins.includes(origin)) {
89
- allowedOrigin = origin || ""
90
- }
91
- } catch (e) {
92
- const em = 'Cors Error: Failed to parse APP_CORS_ORIGINS, fallback to "*"'
93
- logger.error(em, { error: em })
94
- allowedOrigin = ''
95
- }
96
- }
97
-
98
- set.headers['Access-Control-Allow-Origin'] = allowedOrigin
99
- set.headers['Access-Control-Allow-Methods'] = process.env.APP_CORS_METHODS || 'GET, POST, PUT, DELETE, OPTIONS'
100
- set.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Option, x-App'
101
- set.headers['Access-Control-Allow-Credentials'] = 'true'
102
-
103
- if (request.method === 'OPTIONS') {
104
- return new Response(null, { status: 204, })
105
- }
106
- }),
107
-
108
-
109
- // =============================>
110
- // ## Middleware: Rate limiter handler
111
- // =============================>
112
- RateLimiter: (app: Elysia, options?: { windowMs?: number, max?: number }) => app.onRequest(({ request, set, store }) => {
113
- const max = options?.max || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATE_LIMIT) : 60 )
114
- const windowMs = options?.windowMs || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATELIMIT_COUNTDOWN) : 60_000 )
115
-
116
- const user = (store as any)?.user
117
- const key = getClientKey(request, user?.id)
118
-
119
- const now = Date.now()
120
- let record = rateLimitStore.get(key)
121
-
122
- if (!record || record.expiresAt < now) {
123
- record = { count: 1, expiresAt: now + windowMs }
124
- rateLimitStore.set(key, record)
125
- } else {
126
- record.count++
127
- }
128
-
129
- set.headers['X-RateLimit-Limit'] = String(max)
130
- set.headers['X-RateLimit-Remaining'] = String(Math.max(0, max - record.count))
131
- set.headers['X-RateLimit-Reset'] = String(record.expiresAt)
132
-
133
- if (record.count > max) throw status(errors.ratelimited.status, { message: errors.ratelimited.message });
134
- }),
135
-
136
-
137
- // =============================>
138
- // ## Middleware: Body parse handler
139
- // =============================>
140
- BodyParse: (app: Elysia) => app.state<{ rawBody?: any }>({}).onRequest(async ({ request, store }) => {
141
- const text = await request.clone().text();
142
-
143
- const contentType = request.headers.get("content-type") || "";
144
- let rawBody: any = {};
145
-
146
- try {
147
- if (contentType.includes("application/json")) {
148
- rawBody = text ? JSON.parse(text) : {};
149
- } else if (contentType.includes("application/x-www-form-urlencoded")) {
150
- const params = new URLSearchParams(text);
151
- for (const [key, value] of params.entries()) bodyParseNestedSet(rawBody, key, value);
152
- } else if (contentType.includes("multipart/form-data")) {
153
- const formData = await request.clone().formData();
154
- for (const [key, value] of formData.entries()) bodyParseNestedSet(rawBody, key, value);
155
- } else {
156
- rawBody = {};
157
- }
158
- } catch (e) {
159
- const em = e instanceof Error ? e.message : String(e)
160
- logger.error(`Body parse error: ${em}`, { error: em })
161
- rawBody = {};
162
- throw status(errors.request.status, { message: errors.request.message })
163
- }
164
-
165
- store.rawBody = rawBody;
166
- }).derive(({ store }) => {
167
- const payload = bodyParseKeyFormat(store.rawBody || {});
168
- return { payload };
169
- }),
170
-
171
-
172
- AccessLog: (app: Elysia) => app.state<{ startedAt?: number }>({}).onRequest(({ store }) => { store.startedAt = Date.now() }).onAfterResponse(({ request, set, store }) => {
173
- const method = request.method
174
- const url = new URL(request.url)
175
- const path = url.pathname
176
- const status = Number(set.status) ?? 200
177
- const latency = Date.now() - (store.startedAt ?? Date.now())
178
- const agent = request.headers.get("user-agent") || 'unknown'
179
- const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
180
-
181
- logger.info(`${method} : ${path} - ${status} - ${latency}ms - ${ip}]`)
182
- logger.access({ method, path, status, latency, ip, agent })
183
- }),
184
-
185
-
186
- // =============================>
187
- // ## Middleware: Error handler
188
- // =============================>
189
- ErrorHandler: (app: Elysia) => app.onError(({ code, set, error, request }) => {
190
- if (code === 'NOT_FOUND') {
191
- set.status = errors.notfound.status
192
- return { message: errors.notfound.message }
193
- }
194
-
195
- if (code === 'INTERNAL_SERVER_ERROR') {
196
- set.status = errors.error.status
197
- const em = error.message
198
- const url = new URL(request.url)
199
- const path = url.pathname
200
-
201
- logger.error(`error: ${em}`, { error: em, reference: path })
202
- return { message: em }
203
- }
204
- }),
205
-
206
- Context: (app: Elysia) => app.derive(async ({ store }) => {
207
- const userId = (store as any)?.user?.id
208
-
209
- return context.run({
210
- user_id: userId,
211
- },() => ({})
212
- )
213
- }),
214
- }
215
-
216
-
217
-
218
- // =============================>
219
- // ## Middleware: Body parse helpers
220
- // =============================>
221
- function bodyParseKeyFormat(input: any): any {
222
- if ( typeof input !== "object" || input === null || input instanceof File ) return input;
223
-
224
- if (Array.isArray(input)) return input.map(bodyParseKeyFormat)
225
-
226
- const result: any = {}
227
- for (const [key, value] of Object.entries(input)) {
228
- if (key.includes(".") || key.includes("[")) {
229
- bodyParseNestedSet(result, key, bodyParseKeyFormat(value))
230
- } else {
231
- result[key] = bodyParseKeyFormat(value)
232
- }
233
- }
234
- return result
235
- }
236
-
237
-
238
- function bodyParseNestedSet(obj: any, path: string, value: any) {
239
- const parts = bodyParsePathFormat(path);
240
- let current = obj;
241
-
242
- for (let i = 0; i < parts.length; i++) {
243
- const key = parts[i];
244
- const isLast = i === parts.length - 1;
245
-
246
- if (isLast) {
247
- current[key] = bodyParseValueFormat(value);
248
- } else {
249
- if (!(key in current)) {
250
- const nextKey = parts[i + 1];
251
- current[key] = isNaN(Number(nextKey)) ? {} : [];
252
- }
253
- current = current[key];
254
- }
255
- }
256
- }
257
-
258
- function bodyParsePathFormat(path: string): string[] {
259
- return path.replace(/\[(\w+)\]/g, ".$1").replace(/^\./, "").split(".");
260
- }
261
-
262
- function bodyParseValueFormat(value: any) {
263
- if (value == "" || value == null || value == "null") return null;
264
- if (typeof value !== "string") return value;
265
- if (value === "true") return true;
266
- if (value === "false") return false;
267
- if (!isNaN(Number(value))) return Number(value);
268
- return value;
269
- }
270
-
271
-
272
-
273
- // =============================>
274
- // ## Middleware: Rate Limiter Helpers
275
- // =============================>
276
- type RateLimitRecord = {
277
- count: number
278
- expiresAt: number
279
- }
280
-
281
- const rateLimitStore = new Map<string, RateLimitRecord>()
282
-
283
- function getClientKey(request: Request, userId?: string | number) {
284
- if (userId) return `user:${userId}`
285
-
286
- const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
287
-
288
- return `ip:${ip}`
1
+ import { Elysia, status } from 'elysia'
2
+ import { auth, context, logger } from '@utils'
3
+
4
+ declare module "elysia" {
5
+ interface Elysia {
6
+ api(
7
+ basePath: string,
8
+ controller: {
9
+ index ?: any
10
+ store ?: any
11
+ show ?: any
12
+ update ?: any
13
+ destroy ?: any
14
+ }
15
+ ): this
16
+ }
17
+ }
18
+
19
+
20
+ const errors = {
21
+ unauthorized: {
22
+ status: 401,
23
+ message: "Unauthorized!"
24
+ },
25
+ ratelimited: {
26
+ status: 429,
27
+ message: "Too many requests!"
28
+ },
29
+ notfound: {
30
+ status: 404,
31
+ message: "Endpoint not found!"
32
+ },
33
+ request: {
34
+ status: 400,
35
+ message: "Bad Request!"
36
+ },
37
+ error: {
38
+ status: 500,
39
+ message: "Endpoint not found!"
40
+ }
41
+ }
42
+
43
+
44
+ export const middleware = {
45
+ // =============================>
46
+ // ## Middleware: Auth hand;er
47
+ // =============================>
48
+ Auth: (app: Elysia) => app.derive(async ({ request }) => {
49
+ const authHeader = request.headers.get('authorization')
50
+
51
+ if (!authHeader || !authHeader.startsWith('Bearer ')) return { user: null, permissions: [], token: null }
52
+
53
+ const bearer = authHeader.substring(7).trim()
54
+ const result = await auth.verifyAccessToken(bearer, request)
55
+
56
+ if (!result) return { user: null, permissions: [], token: null };
57
+
58
+ return {
59
+ user: result.user,
60
+ permissions: result.permissions,
61
+ token: result.token,
62
+ }
63
+ }),
64
+
65
+
66
+ // =============================>
67
+ // ## Middleware: Private handler
68
+ // =============================>
69
+ Private: (app: Elysia) => app.derive(async ({ user }: Record<string, any> | any) => {
70
+ if (!user) {
71
+ throw status(errors.unauthorized.status, { message: errors.unauthorized.message })
72
+ }
73
+ }),
74
+
75
+
76
+ // =============================>
77
+ // ## Middleware: Cors handler
78
+ // =============================>
79
+ Cors: (app: Elysia) => app.onRequest(({ request, set }) => {
80
+ const origin = request.headers.get('origin') ?? ''
81
+ let allowedOrigin: string = '*'
82
+
83
+ const originsConf = process.env.APP_CORS_ORIGINS || '*'
84
+
85
+ if (originsConf !== '*') {
86
+ try {
87
+ const allowedOrigins = JSON.parse(originsConf)
88
+ if (Array.isArray(allowedOrigins) && allowedOrigins.includes(origin)) {
89
+ allowedOrigin = origin || ""
90
+ }
91
+ } catch (e) {
92
+ const em = 'Cors Error: Failed to parse APP_CORS_ORIGINS, fallback to "*"'
93
+ logger.error(em, { error: em })
94
+ allowedOrigin = ''
95
+ }
96
+ }
97
+
98
+ set.headers['Access-Control-Allow-Origin'] = allowedOrigin
99
+ set.headers['Access-Control-Allow-Methods'] = process.env.APP_CORS_METHODS || 'GET, POST, PUT, DELETE, OPTIONS'
100
+ set.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Option, x-App'
101
+ set.headers['Access-Control-Allow-Credentials'] = 'true'
102
+
103
+ if (request.method === 'OPTIONS') {
104
+ return new Response(null, { status: 204, })
105
+ }
106
+ }),
107
+
108
+
109
+ // =============================>
110
+ // ## Middleware: Rate limiter handler
111
+ // =============================>
112
+ RateLimiter: (app: Elysia, options?: { windowMs?: number, max?: number }) => app.onRequest(({ request, set, store }) => {
113
+ const max = options?.max || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATE_LIMIT) : 60 )
114
+ const windowMs = options?.windowMs || ( process.env.APP_RATELIMIT_COUNTDOWN ? Number(process.env.APP_RATELIMIT_COUNTDOWN) : 60_000 )
115
+
116
+ const user = (store as any)?.user
117
+ const key = getClientKey(request, user?.id)
118
+
119
+ const now = Date.now()
120
+ let record = rateLimitStore.get(key)
121
+
122
+ if (!record || record.expiresAt < now) {
123
+ record = { count: 1, expiresAt: now + windowMs }
124
+ rateLimitStore.set(key, record)
125
+ } else {
126
+ record.count++
127
+ }
128
+
129
+ set.headers['X-RateLimit-Limit'] = String(max)
130
+ set.headers['X-RateLimit-Remaining'] = String(Math.max(0, max - record.count))
131
+ set.headers['X-RateLimit-Reset'] = String(record.expiresAt)
132
+
133
+ if (record.count > max) throw status(errors.ratelimited.status, { message: errors.ratelimited.message });
134
+ }),
135
+
136
+
137
+ // =============================>
138
+ // ## Middleware: Body parse handler
139
+ // =============================>
140
+ BodyParse: (app: Elysia) => app.state<{ rawBody?: any }>({}).onRequest(async ({ request, store }) => {
141
+ const text = await request.clone().text();
142
+
143
+ const contentType = request.headers.get("content-type") || "";
144
+ let rawBody: any = {};
145
+
146
+ try {
147
+ if (contentType.includes("application/json")) {
148
+ rawBody = text ? JSON.parse(text) : {};
149
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
150
+ const params = new URLSearchParams(text);
151
+ for (const [key, value] of params.entries()) bodyParseNestedSet(rawBody, key, value);
152
+ } else if (contentType.includes("multipart/form-data")) {
153
+ const formData = await request.clone().formData();
154
+ for (const [key, value] of formData.entries()) bodyParseNestedSet(rawBody, key, value);
155
+ } else {
156
+ rawBody = {};
157
+ }
158
+ } catch (e) {
159
+ const em = e instanceof Error ? e.message : String(e)
160
+ logger.error(`Body parse error: ${em}`, { error: em })
161
+ rawBody = {};
162
+ throw status(errors.request.status, { message: errors.request.message })
163
+ }
164
+
165
+ store.rawBody = rawBody;
166
+ }).derive(({ store }) => {
167
+ const payload = bodyParseKeyFormat(store.rawBody || {});
168
+ return { payload };
169
+ }),
170
+
171
+
172
+ AccessLog: (app: Elysia) => app.state<{ startedAt?: number }>({}).onRequest(({ store }) => { store.startedAt = Date.now() }).onAfterResponse(({ request, set, store }) => {
173
+ const method = request.method
174
+ const url = new URL(request.url)
175
+ const path = url.pathname
176
+ const status = Number(set.status) ?? 200
177
+ const latency = Date.now() - (store.startedAt ?? Date.now())
178
+ const agent = request.headers.get("user-agent") || 'unknown'
179
+ const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
180
+
181
+ logger.info(`${method} : ${path} - ${status} - ${latency}ms - ${ip}]`)
182
+ logger.access({ method, path, status, latency, ip, agent })
183
+ }),
184
+
185
+
186
+ // =============================>
187
+ // ## Middleware: Error handler
188
+ // =============================>
189
+ ErrorHandler: (app: Elysia) => app.onError(({ code, set, error, request }) => {
190
+ if (code === 'NOT_FOUND') {
191
+ set.status = errors.notfound.status
192
+ return { message: errors.notfound.message }
193
+ }
194
+
195
+ if (code === 'INTERNAL_SERVER_ERROR') {
196
+ set.status = errors.error.status
197
+ const em = error.message
198
+ const url = new URL(request.url)
199
+ const path = url.pathname
200
+
201
+ logger.error(`error: ${em}`, { error: em, reference: path })
202
+ return { message: em }
203
+ }
204
+ }),
205
+
206
+ Context: (app: Elysia) => app.derive(async ({ store }) => {
207
+ const userId = (store as any)?.user?.id
208
+
209
+ return context.run({
210
+ user_id: userId,
211
+ },() => ({})
212
+ )
213
+ }),
214
+ }
215
+
216
+
217
+
218
+ // =============================>
219
+ // ## Middleware: Body parse helpers
220
+ // =============================>
221
+ function bodyParseKeyFormat(input: any): any {
222
+ if ( typeof input !== "object" || input === null || input instanceof File ) return input;
223
+
224
+ if (Array.isArray(input)) return input.map(bodyParseKeyFormat)
225
+
226
+ const result: any = {}
227
+ for (const [key, value] of Object.entries(input)) {
228
+ if (key.includes(".") || key.includes("[")) {
229
+ bodyParseNestedSet(result, key, bodyParseKeyFormat(value))
230
+ } else {
231
+ result[key] = bodyParseKeyFormat(value)
232
+ }
233
+ }
234
+ return result
235
+ }
236
+
237
+
238
+ function bodyParseNestedSet(obj: any, path: string, value: any) {
239
+ const parts = bodyParsePathFormat(path);
240
+ let current = obj;
241
+
242
+ for (let i = 0; i < parts.length; i++) {
243
+ const key = parts[i];
244
+ const isLast = i === parts.length - 1;
245
+
246
+ if (isLast) {
247
+ current[key] = bodyParseValueFormat(value);
248
+ } else {
249
+ if (!(key in current)) {
250
+ const nextKey = parts[i + 1];
251
+ current[key] = isNaN(Number(nextKey)) ? {} : [];
252
+ }
253
+ current = current[key];
254
+ }
255
+ }
256
+ }
257
+
258
+ function bodyParsePathFormat(path: string): string[] {
259
+ return path.replace(/\[(\w+)\]/g, ".$1").replace(/^\./, "").split(".");
260
+ }
261
+
262
+ function bodyParseValueFormat(value: any) {
263
+ if (value == "" || value == null || value == "null") return null;
264
+ if (typeof value !== "string") return value;
265
+ if (value === "true") return true;
266
+ if (value === "false") return false;
267
+ if (!isNaN(Number(value))) return Number(value);
268
+ return value;
269
+ }
270
+
271
+
272
+
273
+ // =============================>
274
+ // ## Middleware: Rate Limiter Helpers
275
+ // =============================>
276
+ type RateLimitRecord = {
277
+ count: number
278
+ expiresAt: number
279
+ }
280
+
281
+ const rateLimitStore = new Map<string, RateLimitRecord>()
282
+
283
+ function getClientKey(request: Request, userId?: string | number) {
284
+ if (userId) return `user:${userId}`
285
+
286
+ const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('cf-connecting-ip') || 'unknown'
287
+
288
+ return `ip:${ip}`
289
289
  }
@@ -0,0 +1 @@
1
+ export * from "./model";