@skalfa/skalfa-api-core 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 (249) hide show
  1. package/.github/workflows/publish.yml +40 -0
  2. package/dist/auth/auth.d.ts +19 -0
  3. package/dist/auth/auth.js +227 -0
  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/auth.util.d.ts +19 -0
  9. package/dist/auth.util.js +183 -0
  10. package/dist/auth.util.js.map +1 -0
  11. package/dist/commands/cli.d.ts +1 -0
  12. package/dist/commands/cli.js +78 -0
  13. package/dist/commands/cli.js.map +1 -0
  14. package/dist/commands/make/basic-controller.d.ts +2 -0
  15. package/dist/commands/make/basic-controller.js +40 -0
  16. package/dist/commands/make/basic-controller.js.map +1 -0
  17. package/dist/commands/make/basic-migration.d.ts +5 -0
  18. package/dist/commands/make/basic-migration.js +60 -0
  19. package/dist/commands/make/basic-migration.js.map +1 -0
  20. package/dist/commands/make/basic-model.d.ts +2 -0
  21. package/dist/commands/make/basic-model.js +25 -0
  22. package/dist/commands/make/basic-model.js.map +1 -0
  23. package/dist/commands/make/basic-seeder.d.ts +3 -0
  24. package/dist/commands/make/basic-seeder.js +32 -0
  25. package/dist/commands/make/basic-seeder.js.map +1 -0
  26. package/dist/commands/make/blueprint.d.ts +2 -0
  27. package/dist/commands/make/blueprint.js +29 -0
  28. package/dist/commands/make/blueprint.js.map +1 -0
  29. package/dist/commands/make/da-migration.d.ts +5 -0
  30. package/dist/commands/make/da-migration.js +60 -0
  31. package/dist/commands/make/da-migration.js.map +1 -0
  32. package/dist/commands/make/light-controller.d.ts +3 -0
  33. package/dist/commands/make/light-controller.js +54 -0
  34. package/dist/commands/make/light-controller.js.map +1 -0
  35. package/dist/commands/make/light-model.d.ts +3 -0
  36. package/dist/commands/make/light-model.js +50 -0
  37. package/dist/commands/make/light-model.js.map +1 -0
  38. package/dist/commands/make/mail.d.ts +2 -0
  39. package/dist/commands/make/mail.js +41 -0
  40. package/dist/commands/make/mail.js.map +1 -0
  41. package/dist/commands/make/notification.d.ts +2 -0
  42. package/dist/commands/make/notification.js +33 -0
  43. package/dist/commands/make/notification.js.map +1 -0
  44. package/dist/commands/make/queue.d.ts +2 -0
  45. package/dist/commands/make/queue.js +35 -0
  46. package/dist/commands/make/queue.js.map +1 -0
  47. package/dist/commands/runner/barrels.d.ts +3 -0
  48. package/dist/commands/runner/barrels.js +78 -0
  49. package/dist/commands/runner/barrels.js.map +1 -0
  50. package/dist/commands/runner/blueprint/controller-generation.d.ts +1 -0
  51. package/dist/commands/runner/blueprint/controller-generation.js +147 -0
  52. package/dist/commands/runner/blueprint/controller-generation.js.map +1 -0
  53. package/dist/commands/runner/blueprint/documentation-generation.d.ts +6 -0
  54. package/dist/commands/runner/blueprint/documentation-generation.js +337 -0
  55. package/dist/commands/runner/blueprint/documentation-generation.js.map +1 -0
  56. package/dist/commands/runner/blueprint/migration-generation.d.ts +1 -0
  57. package/dist/commands/runner/blueprint/migration-generation.js +120 -0
  58. package/dist/commands/runner/blueprint/migration-generation.js.map +1 -0
  59. package/dist/commands/runner/blueprint/model-generation.d.ts +1 -0
  60. package/dist/commands/runner/blueprint/model-generation.js +122 -0
  61. package/dist/commands/runner/blueprint/model-generation.js.map +1 -0
  62. package/dist/commands/runner/blueprint/runner.d.ts +23 -0
  63. package/dist/commands/runner/blueprint/runner.js +139 -0
  64. package/dist/commands/runner/blueprint/runner.js.map +1 -0
  65. package/dist/commands/runner/blueprint/seeder-generation.d.ts +1 -0
  66. package/dist/commands/runner/blueprint/seeder-generation.js +40 -0
  67. package/dist/commands/runner/blueprint/seeder-generation.js.map +1 -0
  68. package/dist/commands/runner/da-migration.d.ts +39 -0
  69. package/dist/commands/runner/da-migration.js +262 -0
  70. package/dist/commands/runner/da-migration.js.map +1 -0
  71. package/dist/commands/runner/migration.d.ts +11 -0
  72. package/dist/commands/runner/migration.js +188 -0
  73. package/dist/commands/runner/migration.js.map +1 -0
  74. package/dist/commands/runner/seeder.d.ts +3 -0
  75. package/dist/commands/runner/seeder.js +40 -0
  76. package/dist/commands/runner/seeder.js.map +1 -0
  77. package/dist/commands/stubs/index.d.ts +14 -0
  78. package/dist/commands/stubs/index.js +277 -0
  79. package/dist/commands/stubs/index.js.map +1 -0
  80. package/dist/context/context.d.ts +7 -0
  81. package/dist/context/context.js +11 -0
  82. package/dist/context/context.js.map +1 -0
  83. package/dist/context/index.d.ts +1 -0
  84. package/dist/context/index.js +2 -0
  85. package/dist/context/index.js.map +1 -0
  86. package/dist/context.util.d.ts +7 -0
  87. package/dist/context.util.js +11 -0
  88. package/dist/context.util.js.map +1 -0
  89. package/dist/controller/controller.d.ts +118 -0
  90. package/dist/controller/controller.js +147 -0
  91. package/dist/controller/controller.js.map +1 -0
  92. package/dist/controller/index.d.ts +1 -0
  93. package/dist/controller/index.js +2 -0
  94. package/dist/controller/index.js.map +1 -0
  95. package/dist/controller.util.d.ts +118 -0
  96. package/dist/controller.util.js +144 -0
  97. package/dist/controller.util.js.map +1 -0
  98. package/dist/conversion/conversion.d.ts +8 -0
  99. package/dist/conversion/conversion.js +52 -0
  100. package/dist/conversion/conversion.js.map +1 -0
  101. package/dist/conversion/index.d.ts +1 -0
  102. package/dist/conversion/index.js +2 -0
  103. package/dist/conversion/index.js.map +1 -0
  104. package/dist/conversion.util.d.ts +8 -0
  105. package/dist/conversion.util.js +52 -0
  106. package/dist/conversion.util.js.map +1 -0
  107. package/dist/db/db.d.ts +84 -0
  108. package/dist/db/db.js +177 -0
  109. package/dist/db/db.js.map +1 -0
  110. package/dist/db/index.d.ts +1 -0
  111. package/dist/db/index.js +2 -0
  112. package/dist/db/index.js.map +1 -0
  113. package/dist/db.util.d.ts +84 -0
  114. package/dist/db.util.js +177 -0
  115. package/dist/db.util.js.map +1 -0
  116. package/dist/index.d.ts +21 -0
  117. package/dist/index.js +14 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/logger/index.d.ts +1 -0
  120. package/dist/logger/index.js +2 -0
  121. package/dist/logger/index.js.map +1 -0
  122. package/dist/logger/logger.d.ts +30 -0
  123. package/dist/logger/logger.js +126 -0
  124. package/dist/logger/logger.js.map +1 -0
  125. package/dist/logger.util.d.ts +30 -0
  126. package/dist/logger.util.js +126 -0
  127. package/dist/logger.util.js.map +1 -0
  128. package/dist/mail/index.d.ts +1 -0
  129. package/dist/mail/index.js +2 -0
  130. package/dist/mail/index.js.map +1 -0
  131. package/dist/mail/mail.d.ts +21 -0
  132. package/dist/mail/mail.js +53 -0
  133. package/dist/mail/mail.js.map +1 -0
  134. package/dist/mail.util.d.ts +21 -0
  135. package/dist/mail.util.js +53 -0
  136. package/dist/mail.util.js.map +1 -0
  137. package/dist/middleware/index.d.ts +1 -0
  138. package/dist/middleware/index.js +2 -0
  139. package/dist/middleware/index.js.map +1 -0
  140. package/dist/middleware/middleware.d.ts +263 -0
  141. package/dist/middleware/middleware.js +233 -0
  142. package/dist/middleware/middleware.js.map +1 -0
  143. package/dist/middleware.util.d.ts +263 -0
  144. package/dist/middleware.util.js +233 -0
  145. package/dist/middleware.util.js.map +1 -0
  146. package/dist/model/index.d.ts +3 -0
  147. package/dist/model/index.js +4 -0
  148. package/dist/model/index.js.map +1 -0
  149. package/dist/model/model.d.ts +204 -0
  150. package/dist/model/model.js +1495 -0
  151. package/dist/model/model.js.map +1 -0
  152. package/dist/model.util.d.ts +204 -0
  153. package/dist/model.util.js +1495 -0
  154. package/dist/model.util.js.map +1 -0
  155. package/dist/permission/index.d.ts +1 -0
  156. package/dist/permission/index.js +2 -0
  157. package/dist/permission/index.js.map +1 -0
  158. package/dist/permission/permission.d.ts +38 -0
  159. package/dist/permission/permission.js +91 -0
  160. package/dist/permission/permission.js.map +1 -0
  161. package/dist/permission.util.d.ts +38 -0
  162. package/dist/permission.util.js +91 -0
  163. package/dist/permission.util.js.map +1 -0
  164. package/dist/registry/index.d.ts +1 -0
  165. package/dist/registry/index.js +2 -0
  166. package/dist/registry/index.js.map +1 -0
  167. package/dist/registry/registry.d.ts +28 -0
  168. package/dist/registry/registry.js +19 -0
  169. package/dist/registry/registry.js.map +1 -0
  170. package/dist/registry.util.d.ts +28 -0
  171. package/dist/registry.util.js +19 -0
  172. package/dist/registry.util.js.map +1 -0
  173. package/dist/route/index.d.ts +1 -0
  174. package/dist/route/index.js +2 -0
  175. package/dist/route/index.js.map +1 -0
  176. package/dist/route/route.d.ts +1 -0
  177. package/dist/route/route.js +12 -0
  178. package/dist/route/route.js.map +1 -0
  179. package/dist/route.util.d.ts +1 -0
  180. package/dist/route.util.js +12 -0
  181. package/dist/route.util.js.map +1 -0
  182. package/dist/storage/index.d.ts +1 -0
  183. package/dist/storage/index.js +2 -0
  184. package/dist/storage/index.js.map +1 -0
  185. package/dist/storage/storage.d.ts +56 -0
  186. package/dist/storage/storage.js +86 -0
  187. package/dist/storage/storage.js.map +1 -0
  188. package/dist/storage.util.d.ts +56 -0
  189. package/dist/storage.util.js +82 -0
  190. package/dist/storage.util.js.map +1 -0
  191. package/dist/validation/index.d.ts +1 -0
  192. package/dist/validation/index.js +2 -0
  193. package/dist/validation/index.js.map +1 -0
  194. package/dist/validation/validation.d.ts +7 -0
  195. package/dist/validation/validation.js +245 -0
  196. package/dist/validation/validation.js.map +1 -0
  197. package/dist/validation.util.d.ts +7 -0
  198. package/dist/validation.util.js +237 -0
  199. package/dist/validation.util.js.map +1 -0
  200. package/package.json +34 -0
  201. package/src/auth/auth.ts +282 -0
  202. package/src/auth/index.ts +1 -0
  203. package/src/commands/cli.ts +89 -0
  204. package/src/commands/make/basic-controller.ts +49 -0
  205. package/src/commands/make/basic-migration.ts +89 -0
  206. package/src/commands/make/basic-model.ts +32 -0
  207. package/src/commands/make/basic-seeder.ts +38 -0
  208. package/src/commands/make/blueprint.ts +36 -0
  209. package/src/commands/make/da-migration.ts +90 -0
  210. package/src/commands/make/light-controller.ts +67 -0
  211. package/src/commands/make/light-model.ts +61 -0
  212. package/src/commands/make/mail.ts +51 -0
  213. package/src/commands/make/notification.ts +43 -0
  214. package/src/commands/make/queue.ts +45 -0
  215. package/src/commands/runner/barrels.ts +85 -0
  216. package/src/commands/runner/blueprint/controller-generation.ts +194 -0
  217. package/src/commands/runner/blueprint/documentation-generation.ts +463 -0
  218. package/src/commands/runner/blueprint/migration-generation.ts +153 -0
  219. package/src/commands/runner/blueprint/model-generation.ts +149 -0
  220. package/src/commands/runner/blueprint/runner.ts +181 -0
  221. package/src/commands/runner/blueprint/seeder-generation.ts +55 -0
  222. package/src/commands/runner/da-migration.ts +333 -0
  223. package/src/commands/runner/migration.ts +245 -0
  224. package/src/commands/runner/seeder.ts +44 -0
  225. package/src/commands/stubs/index.ts +289 -0
  226. package/src/context/context.ts +17 -0
  227. package/src/context/index.ts +1 -0
  228. package/src/controller/controller.ts +240 -0
  229. package/src/controller/index.ts +1 -0
  230. package/src/conversion/conversion.ts +65 -0
  231. package/src/conversion/index.ts +1 -0
  232. package/src/index.ts +22 -0
  233. package/src/logger/index.ts +1 -0
  234. package/src/logger/logger.ts +177 -0
  235. package/src/mail/index.ts +1 -0
  236. package/src/mail/mail.ts +86 -0
  237. package/src/middleware/index.ts +1 -0
  238. package/src/middleware/middleware.ts +289 -0
  239. package/src/permission/index.ts +1 -0
  240. package/src/permission/permission.ts +136 -0
  241. package/src/registry/index.ts +1 -0
  242. package/src/registry/registry.ts +37 -0
  243. package/src/route/index.ts +1 -0
  244. package/src/route/route.ts +12 -0
  245. package/src/storage/index.ts +1 -0
  246. package/src/storage/storage.ts +107 -0
  247. package/src/validation/index.ts +1 -0
  248. package/src/validation/validation.ts +346 -0
  249. package/tsconfig.json +23 -0
@@ -0,0 +1,177 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { registry } from '@utils/registry';
4
+
5
+
6
+
7
+ type LogType = "start" | "info" | "error" | "warning" | "cron" | "queue" | "queueError" | "cronError" | "socket" | "socketError";
8
+
9
+ export interface AccessLog {
10
+ method : string
11
+ path : string
12
+ status : number
13
+ latency : number
14
+ ip ?: string | null
15
+ agent ?: string | null
16
+ at ?: string
17
+ }
18
+
19
+ export interface ErrorLog {
20
+ service ?: string
21
+ key ?: string
22
+ feature ?: string
23
+ error : string | null
24
+ reference ?: string | null
25
+ at ?: string
26
+ }
27
+
28
+
29
+
30
+
31
+ const colors: Record<LogType | "default", string> = {
32
+ default : "\x1b[0m", // default
33
+ start : "\x1b[32m", // green
34
+ info : "\x1b[36m", // cyan
35
+ error : "\x1b[31m", // red
36
+ warning : "\x1b[33m", // yellow
37
+ queue : "\x1b[34m", // blue
38
+ queueError : "\x1b[31m", // red
39
+ cron : "\x1b[35m", // magenta
40
+ cronError : "\x1b[31m", // red
41
+ socket : "\x1b[35m", // blue
42
+ socketError : "\x1b[31m", // red
43
+ };
44
+
45
+ const prefixes: Record<LogType, string> = {
46
+ start : "START",
47
+ info : "INFO",
48
+ error : "ERROR",
49
+ warning : "WARNING",
50
+ cron : "CRON",
51
+ queue : "QUEUE",
52
+ socket : "SOCKET",
53
+ queueError : "QUEUE ERROR",
54
+ cronError : "CRON ERROR",
55
+ socketError : "SOCKET ERROR",
56
+ };
57
+
58
+ function log(type: LogType, msg: string) {
59
+ const color = colors[type];
60
+ const prefix = prefixes[type];
61
+ // eslint-disable-next-line no-console
62
+ console.log(`${color}[${prefix}]${colors.default}`, msg);
63
+ }
64
+
65
+ export const logger = {
66
+ start : (msg: string) => log("start", msg),
67
+ info : (msg: string) => log("info", msg),
68
+ warning : (msg: string) => log("warning", msg),
69
+ queue : (msg: string) => log("queue", msg),
70
+ cron : (msg: string) => log("cron", msg),
71
+ socket : (msg: string) => log("socket", msg),
72
+
73
+ access : (msg: AccessLog) => logAccess(msg),
74
+
75
+ error: (msg: string, payload?: ErrorLog) => {
76
+ log("error", msg)
77
+ payload && logError({...payload, service: payload.service || 'app'})
78
+ },
79
+ queueError: (msg: string, payload?: ErrorLog) => {
80
+ log("queueError", msg)
81
+ payload && logError({...payload, service: payload.service || 'queue'})
82
+ },
83
+ cronError: (msg: string, payload?: ErrorLog) => {
84
+ log("cronError", msg)
85
+ payload && logError({...payload, service: payload.service || 'cron'})
86
+ },
87
+ socketError: (msg: string, payload?: ErrorLog) => {
88
+ log("socketError", msg)
89
+ payload && logError({...payload, service: payload.service || 'socket'})
90
+ },
91
+ };
92
+
93
+
94
+
95
+
96
+
97
+ type DriverName = "file" | "da"
98
+
99
+
100
+ const ACCESS_LOG_DRIVER = process.env.ACCESS_LOG_DRIVER || "file"
101
+ const ACCESS_LOG_LOG_DIR = process.env.ACCESS_LOG_DIR || "storage/logs/access"
102
+ const ACCESS_LOG_QUEUE = process.env.ACCESS_LOG_QUEUE || "access-log"
103
+
104
+
105
+ const ERROR_LOG_DRIVER = process.env.ERROR_LOG_DRIVER || "file"
106
+ const ERROR_LOG_LOG_DIR = process.env.ERROR_LOG_DIR || "storage/logs/error"
107
+ const ERROR_LOG_QUEUE = process.env.ERROR_LOG_QUEUE_PREFIX || "error-log"
108
+
109
+
110
+
111
+ // =====================
112
+ // ## Access Log Drivers
113
+ // =====================
114
+ const filePath = () => {
115
+ const d = new Date().toISOString().slice(0, 10)
116
+ return path.resolve( ACCESS_LOG_LOG_DIR, `access-${d}.log`)
117
+ }
118
+
119
+ const handlers: Record<DriverName, (log: AccessLog) => Promise<void>> = {
120
+ file: async (log) => {
121
+ const dir = path.resolve(ACCESS_LOG_LOG_DIR);
122
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
123
+
124
+ fs.appendFile(filePath(), JSON.stringify(log) + "\n", () => {})
125
+ },
126
+ da: async (log) => {
127
+ const queue = registry.get('queue')
128
+ if (queue) {
129
+ try {
130
+ await queue.add(ACCESS_LOG_QUEUE, log)
131
+ } catch {}
132
+ }
133
+ }
134
+ }
135
+
136
+ const activeDrivers: DriverName[] = (ACCESS_LOG_DRIVER).split(",").map(v => v.trim()).filter((v): v is DriverName => v in handlers)
137
+
138
+ function logAccess(payload: AccessLog) {
139
+ for (const d of activeDrivers) {
140
+ handlers[d](payload)
141
+ }
142
+ }
143
+
144
+
145
+
146
+ // =====================
147
+ // ## Error Log Drivers
148
+ // =====================
149
+ const errorFilePath = () => {
150
+ const d = new Date().toISOString().slice(0, 10)
151
+ return path.resolve(ERROR_LOG_LOG_DIR, `error-${d}.log`)
152
+ }
153
+
154
+ const errorHandlers: Record<DriverName, (log: ErrorLog) => Promise<void>> = {
155
+ file: async (log) => {
156
+ const dir = path.resolve(ERROR_LOG_LOG_DIR);
157
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
158
+
159
+ fs.appendFile(errorFilePath(), JSON.stringify(log) + "\n", () => {});
160
+ },
161
+ da: async (log) => {
162
+ const queue = registry.get('queue')
163
+ if (queue) {
164
+ try {
165
+ await queue.add(ERROR_LOG_QUEUE, log)
166
+ } catch {}
167
+ }
168
+ }
169
+ }
170
+
171
+ const activeErrorDrivers: DriverName[] = ERROR_LOG_DRIVER.split(",").map(v => v.trim()).filter((v): v is DriverName => v in errorHandlers)
172
+
173
+ function logError(payload: ErrorLog) {
174
+ for (const d of activeErrorDrivers) {
175
+ errorHandlers[d](payload)
176
+ }
177
+ }
@@ -0,0 +1 @@
1
+ export * from "./mail";
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import nodemailer, { SentMessageInfo } from "nodemailer";
4
+ import { logger } from "@utils";
5
+
6
+
7
+
8
+ export interface SendMailOptions {
9
+ to : string;
10
+ subject : string;
11
+ content ?: string;
12
+ text ?: string;
13
+ attachments ?: {
14
+ filename : string;
15
+ path : string;
16
+ }[];
17
+ }
18
+
19
+
20
+
21
+ // =============================>
22
+ // ## Mail: Send mail
23
+ // =============================>
24
+ export async function sendMail(options: {
25
+ to : string;
26
+ subject : string;
27
+ text ?: string;
28
+ content ?: string;
29
+ attachments ?: { filename: string; path: string }[];
30
+ }) {
31
+ const transporter = nodemailer.createTransport({
32
+ host : process.env.MAIL_HOST,
33
+ port : Number(process.env.MAIL_PORT),
34
+ secure : Number(process.env.MAIL_PORT) === 465,
35
+ auth : {
36
+ user : process.env.MAIL_USERNAME,
37
+ pass : process.env.MAIL_PASSWORD,
38
+ },
39
+ });
40
+
41
+ const info = (await transporter.sendMail({
42
+ from : `${process.env.MAIL_FROM_NAME || process.env.APP_NAME} <${process.env.MAIL_FROM_ADDRESS || process.env.MAIL_USERNAME}>`,
43
+ to : options.to,
44
+ subject : options.subject,
45
+ text : options.text,
46
+ html : options.content,
47
+ attachments : options.attachments,
48
+ })) as SentMessageInfo;
49
+
50
+ logger.info(`Email sent successfully: ${info.messageId}`)
51
+ return info;
52
+ }
53
+
54
+
55
+
56
+ // =============================>
57
+ // ## Mail: Render mail template
58
+ // =============================>
59
+ export function renderMailTemplate(template: string, options: Record<string, string>) {
60
+ const templateDir = join(import.meta.dir, "./../outputs/mails/templates");
61
+
62
+ const contentPath = join(templateDir, `${template}.mail.stub`);
63
+ let content = readFileSync(contentPath, "utf-8");
64
+
65
+ for (const [key, value] of Object.entries(options)) {
66
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
67
+ content = content.replace(regex, value);
68
+ }
69
+
70
+ let layout = readFileSync(join(templateDir, "layout.mail.stub"), "utf-8");
71
+
72
+ const globalVars = {
73
+ ...options,
74
+ date : "20-10-2025",
75
+ app_name : process.env.APP_NAME || "",
76
+ };
77
+
78
+ for (const [key, value] of Object.entries(globalVars)) {
79
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
80
+ layout = layout.replace(regex, value);
81
+ }
82
+
83
+ layout = layout.replace("{{content}}", content);
84
+
85
+ return layout;
86
+ }
@@ -0,0 +1 @@
1
+ export * from "./middleware";
@@ -0,0 +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}`
289
+ }
@@ -0,0 +1 @@
1
+ export * from "./permission";
@@ -0,0 +1,136 @@
1
+ import { ControllerContext } from "elysia"
2
+
3
+ type KeyDigit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
4
+ export type KeyFeature = `${"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"}${KeyDigit}${KeyDigit}`
5
+ export type KeyAccess = `${KeyDigit}${KeyDigit}`
6
+ export type KeyPermission = `${KeyFeature}.${KeyAccess}` | KeyAccess
7
+
8
+ const features = new Map<KeyFeature, { key: KeyFeature; name: string }>()
9
+ const accesses = new Map<KeyAccess, any>()
10
+
11
+ type FeatureAccess = Partial<Record<KeyFeature, {
12
+ name: string
13
+ accesses: Partial<Record<KeyAccess, string>>
14
+ }>>
15
+
16
+ export const permission = {
17
+ register: (def: FeatureAccess) => {
18
+ const featureAccessMap = new Map<string, string>()
19
+ let defaultFeature: KeyFeature | null = null
20
+
21
+ for (const [featureKey, feature] of Object.entries(def)) {
22
+ if (!defaultFeature) defaultFeature = featureKey as KeyFeature
23
+
24
+ registerFeature({
25
+ key: featureKey as KeyFeature,
26
+ name: feature.name
27
+ })
28
+
29
+ for (const [accessKey, accessName] of Object.entries(feature.accesses)) {
30
+ const permKey =
31
+ `${featureKey}.${String(accessKey).padStart(2, "0")}`
32
+
33
+ registerAccess({
34
+ featureKey,
35
+ accessKey,
36
+ accessName,
37
+ permKey
38
+ })
39
+
40
+ featureAccessMap.set(
41
+ `${featureKey}.${accessKey}`,
42
+ permKey
43
+ )
44
+ }
45
+ }
46
+
47
+ return createScopeApi(defaultFeature!)
48
+ },
49
+
50
+ getFeatures: () => [...features.values()],
51
+
52
+ getAccesses: () => {
53
+ const result: Record<string, {
54
+ key: string
55
+ name: string
56
+ accesses: { key: string; name: string }[]
57
+ }> = {}
58
+
59
+ for (const feature of features.values()) {
60
+ result[String(feature.key)] = {
61
+ key: String(feature.key),
62
+ name: feature.name,
63
+ accesses: []
64
+ }
65
+ }
66
+
67
+ for (const access of accesses.values()) {
68
+ const featureKey = String(access.featureKey)
69
+
70
+ if (!result[featureKey]) continue
71
+
72
+ result[featureKey].accesses.push({
73
+ key: String(access.accessKey).padStart(2, "0"),
74
+ name: access.accessName
75
+ })
76
+ }
77
+
78
+ return Object.values(result)
79
+ },
80
+ }
81
+
82
+
83
+ function normalize(
84
+ raw: KeyPermission,
85
+ defaultFeature?: KeyFeature
86
+ ): KeyPermission {
87
+ if (!raw.includes(".") && defaultFeature) {
88
+ return `${defaultFeature}.${String(raw).padStart(2, "0") as KeyAccess}`
89
+ }
90
+
91
+ return raw
92
+ }
93
+
94
+ function registerFeature(f: { key: KeyFeature; name: string }) {
95
+ if (!features.has(f.key)) {
96
+ features.set(f.key, f)
97
+ }
98
+ }
99
+
100
+ function registerAccess(a: any) {
101
+ if (!accesses.has(a.permKey)) {
102
+ accesses.set(a.permKey, a)
103
+ }
104
+ }
105
+
106
+ function createPermission(keys: KeyPermission[]) {
107
+ return {
108
+ keys,
109
+
110
+ orHave(raw: KeyPermission) {
111
+ return createPermission([
112
+ ...this.keys,
113
+ normalize(raw) as KeyPermission
114
+ ])
115
+ },
116
+
117
+ guard(c: ControllerContext) {
118
+ const permissions = new Set(c.permissions || [])
119
+
120
+ const ok = this.keys.some(k => permissions?.has(k))
121
+ if (!ok) {
122
+ c.responseForbidden()
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ export function createScopeApi(defaultFeature: KeyFeature) {
129
+ return {
130
+ have(raw: KeyPermission) {
131
+ const key = normalize(raw, defaultFeature)
132
+ return createPermission([key])
133
+ }
134
+ }
135
+ }
136
+
@@ -0,0 +1 @@
1
+ export * from "./registry";