@rebasepro/server-core 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +2 -2
  2. package/dist/api/ast-schema-editor.d.ts +21 -0
  3. package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  4. package/dist/api/errors.d.ts +53 -0
  5. package/dist/api/index.d.ts +7 -0
  6. package/dist/api/logs-routes.d.ts +37 -0
  7. package/dist/api/openapi-generator.d.ts +16 -0
  8. package/dist/api/rest/api-generator.d.ts +68 -0
  9. package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
  10. package/dist/api/rest/query-parser.d.ts +6 -0
  11. package/dist/api/schema-editor-routes.d.ts +3 -0
  12. package/dist/api/types.d.ts +56 -0
  13. package/dist/auth/adapter-middleware.d.ts +43 -0
  14. package/dist/auth/admin-roles-route.d.ts +18 -0
  15. package/dist/auth/admin-user-ops.d.ts +79 -0
  16. package/dist/auth/admin-users-route.d.ts +28 -0
  17. package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
  18. package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
  19. package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
  20. package/dist/auth/api-keys/api-key-store.d.ts +35 -0
  21. package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
  22. package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
  23. package/dist/auth/apple-oauth.d.ts +30 -0
  24. package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
  25. package/dist/auth/bitbucket-oauth.d.ts +11 -0
  26. package/dist/auth/builtin-auth-adapter.d.ts +55 -0
  27. package/dist/auth/cookie-utils.d.ts +32 -0
  28. package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
  29. package/dist/auth/custom-auth-adapter.d.ts +39 -0
  30. package/dist/auth/discord-oauth.d.ts +14 -0
  31. package/dist/auth/facebook-oauth.d.ts +14 -0
  32. package/dist/auth/github-oauth.d.ts +15 -0
  33. package/dist/auth/gitlab-oauth.d.ts +13 -0
  34. package/dist/auth/google-oauth.d.ts +47 -0
  35. package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
  36. package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
  37. package/dist/auth/jwt.d.ts +67 -0
  38. package/dist/auth/linkedin-oauth.d.ts +18 -0
  39. package/dist/auth/magic-link-routes.d.ts +32 -0
  40. package/dist/auth/mfa-crypto.d.ts +23 -0
  41. package/dist/auth/mfa-routes.d.ts +7 -0
  42. package/dist/auth/mfa.d.ts +49 -0
  43. package/dist/auth/microsoft-oauth.d.ts +16 -0
  44. package/dist/auth/middleware.d.ts +158 -0
  45. package/dist/auth/password.d.ts +22 -0
  46. package/dist/auth/rate-limiter.d.ts +50 -0
  47. package/dist/auth/reset-password-admin.d.ts +29 -0
  48. package/dist/auth/rls-scope.d.ts +31 -0
  49. package/dist/auth/routes.d.ts +66 -0
  50. package/dist/auth/session-routes.d.ts +29 -0
  51. package/dist/auth/slack-oauth.d.ts +12 -0
  52. package/dist/auth/spotify-oauth.d.ts +12 -0
  53. package/dist/auth/twitter-oauth.d.ts +18 -0
  54. package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
  55. package/dist/collections/loader.d.ts +5 -0
  56. package/dist/cron/cron-loader.d.ts +18 -0
  57. package/dist/cron/cron-routes.d.ts +14 -0
  58. package/dist/cron/cron-scheduler.d.ts +106 -0
  59. package/dist/cron/cron-store.d.ts +32 -0
  60. package/dist/cron/define-cron.d.ts +28 -0
  61. package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
  62. package/dist/db/interfaces.d.ts +18 -0
  63. package/dist/dynamic-import-Dvh-K5fl.js +22 -0
  64. package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
  65. package/dist/email/index.d.ts +6 -0
  66. package/dist/email/smtp-email-service.d.ts +30 -0
  67. package/dist/email/templates.d.ts +50 -0
  68. package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
  69. package/dist/env.d.ts +102 -0
  70. package/dist/functions/define-function.d.ts +55 -0
  71. package/dist/functions/function-loader.d.ts +18 -0
  72. package/dist/functions/function-routes.d.ts +10 -0
  73. package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
  74. package/dist/history/history-routes.d.ts +23 -0
  75. package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
  76. package/dist/index.d.ts +35 -0
  77. package/dist/index.es.js +18184 -0
  78. package/dist/index.es.js.map +1 -0
  79. package/dist/index.umd.js +48629 -0
  80. package/dist/index.umd.js.map +1 -0
  81. package/dist/init/docs.d.ts +4 -0
  82. package/dist/init/health.d.ts +2 -0
  83. package/dist/init/middlewares.d.ts +10 -0
  84. package/dist/init/shutdown.d.ts +65 -0
  85. package/dist/init/storage.d.ts +5 -0
  86. package/dist/init.d.ts +344 -0
  87. package/dist/jwt-BwIn8xmk.js +4200 -0
  88. package/dist/jwt-BwIn8xmk.js.map +1 -0
  89. package/dist/logger-BYU66ENZ.js +94 -0
  90. package/dist/logger-BYU66ENZ.js.map +1 -0
  91. package/dist/ms-DnYXB-Wd.js +162 -0
  92. package/dist/ms-DnYXB-Wd.js.map +1 -0
  93. package/dist/openapi-generator-Z9oYWLf_.js +586 -0
  94. package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
  95. package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
  96. package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
  97. package/dist/serve-spa.d.ts +36 -0
  98. package/dist/services/driver-registry.d.ts +78 -0
  99. package/dist/services/routed-realtime-service.d.ts +43 -0
  100. package/dist/services/webhook-service.d.ts +29 -0
  101. package/dist/singleton.d.ts +51 -0
  102. package/dist/src-B4OLmNVa.js +437 -0
  103. package/dist/src-B4OLmNVa.js.map +1 -0
  104. package/dist/src-B5WkP1Rv.js +24778 -0
  105. package/dist/src-B5WkP1Rv.js.map +1 -0
  106. package/dist/storage/GCSStorageController.d.ts +43 -0
  107. package/dist/storage/LocalStorageController.d.ts +46 -0
  108. package/dist/storage/S3StorageController.d.ts +40 -0
  109. package/dist/storage/image-transform.d.ts +50 -0
  110. package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
  111. package/dist/storage/routes.d.ts +70 -0
  112. package/dist/storage/storage-registry.d.ts +78 -0
  113. package/dist/storage/tus-handler.d.ts +53 -0
  114. package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
  115. package/dist/types/index.d.ts +11 -0
  116. package/dist/utils/dev-port.d.ts +45 -0
  117. package/dist/utils/dynamic-import.d.ts +25 -0
  118. package/dist/utils/logger.d.ts +31 -0
  119. package/dist/utils/logging.d.ts +9 -0
  120. package/dist/utils/request-id.d.ts +4 -0
  121. package/dist/utils/request-logger.d.ts +19 -0
  122. package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
  123. package/package.json +49 -22
  124. package/coverage/clover.xml +0 -3739
  125. package/coverage/coverage-final.json +0 -31
  126. package/coverage/lcov-report/base.css +0 -224
  127. package/coverage/lcov-report/block-navigation.js +0 -87
  128. package/coverage/lcov-report/favicon.png +0 -0
  129. package/coverage/lcov-report/index.html +0 -266
  130. package/coverage/lcov-report/prettify.css +0 -1
  131. package/coverage/lcov-report/prettify.js +0 -2
  132. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  133. package/coverage/lcov-report/sorter.js +0 -210
  134. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
  135. package/coverage/lcov-report/src/api/errors.ts.html +0 -472
  136. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
  137. package/coverage/lcov-report/src/api/graphql/index.html +0 -116
  138. package/coverage/lcov-report/src/api/index.html +0 -176
  139. package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
  140. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
  141. package/coverage/lcov-report/src/api/rest/index.html +0 -131
  142. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
  143. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
  144. package/coverage/lcov-report/src/api/server.ts.html +0 -823
  145. package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
  146. package/coverage/lcov-report/src/auth/index.html +0 -176
  147. package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
  148. package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
  149. package/coverage/lcov-report/src/auth/password.ts.html +0 -310
  150. package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
  151. package/coverage/lcov-report/src/collections/index.html +0 -116
  152. package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
  153. package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
  154. package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
  155. package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
  156. package/coverage/lcov-report/src/db/index.html +0 -146
  157. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
  158. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
  159. package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
  160. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
  161. package/coverage/lcov-report/src/db/services/index.html +0 -176
  162. package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
  163. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
  164. package/coverage/lcov-report/src/index.html +0 -116
  165. package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
  166. package/coverage/lcov-report/src/services/index.html +0 -131
  167. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
  168. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
  169. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
  170. package/coverage/lcov-report/src/storage/index.html +0 -161
  171. package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
  172. package/coverage/lcov-report/src/storage/types.ts.html +0 -451
  173. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
  174. package/coverage/lcov-report/src/utils/index.html +0 -116
  175. package/coverage/lcov.info +0 -7179
  176. package/jest.config.cjs +0 -19
  177. package/src/api/ast-schema-editor.ts +0 -289
  178. package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
  179. package/src/api/errors.ts +0 -278
  180. package/src/api/graphql/graphql-schema-generator.ts +0 -383
  181. package/src/api/graphql/index.ts +0 -2
  182. package/src/api/index.ts +0 -11
  183. package/src/api/logs-routes.ts +0 -132
  184. package/src/api/openapi-generator.ts +0 -727
  185. package/src/api/rest/api-generator-count.test.ts +0 -126
  186. package/src/api/rest/api-generator.ts +0 -747
  187. package/src/api/rest/query-parser.ts +0 -300
  188. package/src/api/schema-editor-routes.ts +0 -41
  189. package/src/api/server.ts +0 -271
  190. package/src/api/types.ts +0 -104
  191. package/src/auth/adapter-middleware.ts +0 -114
  192. package/src/auth/admin-roles-route.ts +0 -36
  193. package/src/auth/admin-user-ops.ts +0 -236
  194. package/src/auth/admin-users-route.ts +0 -302
  195. package/src/auth/api-keys/api-key-middleware.ts +0 -133
  196. package/src/auth/api-keys/api-key-routes.ts +0 -193
  197. package/src/auth/api-keys/api-key-store.ts +0 -340
  198. package/src/auth/apple-oauth.ts +0 -122
  199. package/src/auth/bitbucket-oauth.ts +0 -84
  200. package/src/auth/builtin-auth-adapter.ts +0 -380
  201. package/src/auth/custom-auth-adapter.ts +0 -86
  202. package/src/auth/discord-oauth.ts +0 -85
  203. package/src/auth/facebook-oauth.ts +0 -74
  204. package/src/auth/github-oauth.ts +0 -112
  205. package/src/auth/gitlab-oauth.ts +0 -72
  206. package/src/auth/google-oauth.ts +0 -223
  207. package/src/auth/jwt.ts +0 -208
  208. package/src/auth/linkedin-oauth.ts +0 -83
  209. package/src/auth/magic-link-routes.ts +0 -167
  210. package/src/auth/mfa-crypto.ts +0 -91
  211. package/src/auth/mfa-routes.ts +0 -323
  212. package/src/auth/mfa.ts +0 -161
  213. package/src/auth/microsoft-oauth.ts +0 -90
  214. package/src/auth/middleware.ts +0 -369
  215. package/src/auth/password.ts +0 -77
  216. package/src/auth/rate-limiter.ts +0 -225
  217. package/src/auth/reset-password-admin.ts +0 -160
  218. package/src/auth/rls-scope.ts +0 -58
  219. package/src/auth/routes.ts +0 -802
  220. package/src/auth/session-routes.ts +0 -353
  221. package/src/auth/slack-oauth.ts +0 -73
  222. package/src/auth/spotify-oauth.ts +0 -69
  223. package/src/auth/twitter-oauth.ts +0 -128
  224. package/src/collections/BackendCollectionRegistry.ts +0 -20
  225. package/src/collections/loader.ts +0 -50
  226. package/src/cron/cron-loader.ts +0 -84
  227. package/src/cron/cron-routes.test.ts +0 -265
  228. package/src/cron/cron-routes.ts +0 -85
  229. package/src/cron/cron-scheduler.test.ts +0 -559
  230. package/src/cron/cron-scheduler.ts +0 -581
  231. package/src/cron/cron-store.ts +0 -166
  232. package/src/db/interfaces.ts +0 -60
  233. package/src/email/index.ts +0 -19
  234. package/src/email/smtp-email-service.ts +0 -123
  235. package/src/email/templates.ts +0 -470
  236. package/src/env.ts +0 -227
  237. package/src/functions/function-loader.ts +0 -116
  238. package/src/functions/function-routes.ts +0 -31
  239. package/src/history/history-routes.ts +0 -129
  240. package/src/index.ts +0 -70
  241. package/src/init/docs.ts +0 -47
  242. package/src/init/health.ts +0 -37
  243. package/src/init/middlewares.ts +0 -61
  244. package/src/init/shutdown.ts +0 -56
  245. package/src/init/storage.ts +0 -57
  246. package/src/init.ts +0 -1041
  247. package/src/serve-spa.ts +0 -89
  248. package/src/services/driver-registry.ts +0 -183
  249. package/src/services/routed-realtime-service.ts +0 -113
  250. package/src/services/webhook-service.ts +0 -155
  251. package/src/singleton.ts +0 -70
  252. package/src/storage/LocalStorageController.ts +0 -389
  253. package/src/storage/S3StorageController.ts +0 -300
  254. package/src/storage/image-transform.ts +0 -218
  255. package/src/storage/routes.ts +0 -368
  256. package/src/storage/storage-registry.ts +0 -188
  257. package/src/storage/tus-handler.ts +0 -315
  258. package/src/types/index.ts +0 -27
  259. package/src/utils/dev-port.ts +0 -196
  260. package/src/utils/logger.ts +0 -143
  261. package/src/utils/logging.ts +0 -40
  262. package/src/utils/request-id.ts +0 -40
  263. package/src/utils/request-logger.ts +0 -72
  264. package/test/api-generator.test.ts +0 -588
  265. package/test/api-key-permission-guard.test.ts +0 -144
  266. package/test/ast-schema-editor.test.ts +0 -89
  267. package/test/auth-middleware-hono.test.ts +0 -556
  268. package/test/auth-routes.test.ts +0 -1098
  269. package/test/backend-hooks-data.test.ts +0 -477
  270. package/test/custom-auth-adapter.test.ts +0 -189
  271. package/test/driver-registry.test.ts +0 -282
  272. package/test/email-templates.test.ts +0 -174
  273. package/test/env.test.ts +0 -128
  274. package/test/error-propagation.test.ts +0 -226
  275. package/test/errors-hono.test.ts +0 -133
  276. package/test/errors.test.ts +0 -155
  277. package/test/function-loader.test.ts +0 -127
  278. package/test/graphql-schema-generator.test.ts +0 -554
  279. package/test/jwt-security.test.ts +0 -182
  280. package/test/jwt.test.ts +0 -326
  281. package/test/mfa.test.ts +0 -197
  282. package/test/middleware.test.ts +0 -305
  283. package/test/multi-datasource-routing.test.ts +0 -113
  284. package/test/password.test.ts +0 -165
  285. package/test/query-parser.test.ts +0 -233
  286. package/test/rate-limiter.test.ts +0 -102
  287. package/test/reset-password-admin.test.ts +0 -113
  288. package/test/routed-realtime-service.test.ts +0 -86
  289. package/test/safe-compare.test.ts +0 -66
  290. package/test/singleton.test.ts +0 -59
  291. package/test/smtp-email-service.test.ts +0 -169
  292. package/test/storage-local.test.ts +0 -271
  293. package/test/storage-registry.test.ts +0 -282
  294. package/test/storage-routes.test.ts +0 -222
  295. package/test/storage-s3.test.ts +0 -304
  296. package/test/transform-auth-response.test.ts +0 -305
  297. package/test/webhook-service.test.ts +0 -260
  298. package/tsconfig.json +0 -54
  299. package/tsconfig.prod.json +0 -23
  300. package/vite.config.ts +0 -86
@@ -1,218 +0,0 @@
1
- /**
2
- * Image Transformation Service
3
- *
4
- * Provides on-the-fly image resize, crop, format conversion, and quality
5
- * adjustment using the `sharp` library. Results are cached in an LRU
6
- * in-memory cache to avoid redundant processing.
7
- */
8
-
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- let sharpFactory: ((input: Buffer | Uint8Array) => any) | undefined;
11
-
12
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
- async function getSharp(): Promise<(input: Buffer | Uint8Array) => any> {
14
- if (!sharpFactory) {
15
- try {
16
- const mod = await import("sharp");
17
- sharpFactory = mod.default;
18
- } catch (err) {
19
- throw new Error("Failed to load optional 'sharp' dependency for image transformation.");
20
- }
21
- }
22
- if (!sharpFactory) {
23
- throw new Error("Failed to load optional 'sharp' dependency for image transformation.");
24
- }
25
- return sharpFactory;
26
- }
27
-
28
- /** Options that can be specified via query parameters. */
29
- export interface ImageTransformOptions {
30
- width?: number;
31
- height?: number;
32
- quality?: number;
33
- format?: "webp" | "avif" | "jpeg" | "png";
34
- fit?: "cover" | "contain" | "fill" | "inside" | "outside";
35
- }
36
-
37
- /** Maximum dimension allowed (prevents abuse). */
38
- const MAX_DIMENSION = 4096;
39
- /** Maximum quality value. */
40
- const MAX_QUALITY = 100;
41
- /** Minimum quality value. */
42
- const MIN_QUALITY = 1;
43
-
44
- const VALID_FORMATS = new Set(["webp", "avif", "jpeg", "png"]);
45
- const VALID_FITS = new Set(["cover", "contain", "fill", "inside", "outside"]);
46
-
47
- /**
48
- * Parse transform options from URL query parameters.
49
- * Returns `null` when no transformation is requested.
50
- */
51
- export function parseTransformOptions(query: Record<string, string>): ImageTransformOptions | null {
52
- const opts: ImageTransformOptions = {};
53
- let hasTransform = false;
54
-
55
- if (query.width) {
56
- const w = parseInt(query.width, 10);
57
- if (!Number.isNaN(w) && w > 0) {
58
- opts.width = Math.min(w, MAX_DIMENSION);
59
- hasTransform = true;
60
- }
61
- }
62
-
63
- if (query.height) {
64
- const h = parseInt(query.height, 10);
65
- if (!Number.isNaN(h) && h > 0) {
66
- opts.height = Math.min(h, MAX_DIMENSION);
67
- hasTransform = true;
68
- }
69
- }
70
-
71
- if (query.quality) {
72
- const q = parseInt(query.quality, 10);
73
- if (!Number.isNaN(q)) {
74
- opts.quality = Math.min(Math.max(q, MIN_QUALITY), MAX_QUALITY);
75
- hasTransform = true;
76
- }
77
- }
78
-
79
- if (query.format && VALID_FORMATS.has(query.format)) {
80
- opts.format = query.format as ImageTransformOptions["format"];
81
- hasTransform = true;
82
- }
83
-
84
- if (query.fit && VALID_FITS.has(query.fit)) {
85
- opts.fit = query.fit as ImageTransformOptions["fit"];
86
- hasTransform = true;
87
- }
88
-
89
- return hasTransform ? opts : null;
90
- }
91
-
92
- /** MIME types that can be used as a Content-Type header. */
93
- const FORMAT_CONTENT_TYPES: Record<string, string> = {
94
- webp: "image/webp",
95
- avif: "image/avif",
96
- jpeg: "image/jpeg",
97
- png: "image/png"
98
- };
99
-
100
- /** Check whether a content type is a transformable image. */
101
- export function isTransformableImage(contentType: string): boolean {
102
- return (
103
- contentType.startsWith("image/") &&
104
- !contentType.includes("svg") &&
105
- !contentType.includes("gif")
106
- );
107
- }
108
-
109
- /**
110
- * Apply image transformations and return the result buffer + content type.
111
- */
112
- export async function transformImage(
113
- buffer: Buffer | Uint8Array,
114
- options: ImageTransformOptions
115
- ): Promise<{ data: Buffer; contentType: string }> {
116
- const sharp = await getSharp();
117
- let pipeline = sharp(buffer);
118
-
119
- if (options.width || options.height) {
120
- pipeline = pipeline.resize({
121
- width: options.width,
122
- height: options.height,
123
- fit: options.fit || "cover",
124
- withoutEnlargement: true
125
- });
126
- }
127
-
128
- const format = options.format || "webp";
129
- const quality = options.quality || 80;
130
-
131
- switch (format) {
132
- case "webp":
133
- pipeline = pipeline.webp({ quality });
134
- break;
135
- case "avif":
136
- pipeline = pipeline.avif({ quality });
137
- break;
138
- case "jpeg":
139
- pipeline = pipeline.jpeg({ quality });
140
- break;
141
- case "png":
142
- pipeline = pipeline.png({ quality });
143
- break;
144
- }
145
-
146
- const data = await pipeline.toBuffer();
147
- return { data,
148
- contentType: FORMAT_CONTENT_TYPES[format] };
149
- }
150
-
151
- // ---------------------------------------------------------------------------
152
- // LRU Transform Cache
153
- // ---------------------------------------------------------------------------
154
-
155
- interface CacheEntry {
156
- data: Buffer;
157
- contentType: string;
158
- timestamp: number;
159
- }
160
-
161
- /**
162
- * Simple LRU cache for transformed images.
163
- *
164
- * Entries expire after `maxAgeMs` (default: 1 hour) and the cache
165
- * evicts the oldest entry when `maxEntries` is exceeded.
166
- */
167
- export class TransformCache {
168
- private cache = new Map<string, CacheEntry>();
169
- private readonly maxEntries: number;
170
- private readonly maxAgeMs: number;
171
- private readonly maxTotalBytes: number;
172
- private totalBytes = 0;
173
-
174
- constructor(maxEntries = 500, maxAgeMs = 3_600_000, maxTotalBytes = 256 * 1024 * 1024) {
175
- this.maxEntries = maxEntries;
176
- this.maxAgeMs = maxAgeMs;
177
- this.maxTotalBytes = maxTotalBytes;
178
- }
179
-
180
- /** Build a deterministic cache key from file key + transform options. */
181
- buildKey(fileKey: string, options: ImageTransformOptions): string {
182
- return `${fileKey}::${JSON.stringify(options)}`;
183
- }
184
-
185
- get(cacheKey: string): { data: Buffer; contentType: string } | null {
186
- const entry = this.cache.get(cacheKey);
187
- if (!entry) return null;
188
- if (Date.now() - entry.timestamp > this.maxAgeMs) {
189
- this.totalBytes -= entry.data.length;
190
- this.cache.delete(cacheKey);
191
- return null;
192
- }
193
- // Move to end (most recently used)
194
- this.cache.delete(cacheKey);
195
- this.cache.set(cacheKey, entry);
196
- return { data: entry.data,
197
- contentType: entry.contentType };
198
- }
199
-
200
- set(cacheKey: string, data: Buffer, contentType: string): void {
201
- // Evict oldest entries while over capacity (entry count or total bytes)
202
- while (
203
- (this.cache.size >= this.maxEntries || this.totalBytes + data.length > this.maxTotalBytes)
204
- && this.cache.size > 0
205
- ) {
206
- const oldest = this.cache.keys().next().value;
207
- if (oldest !== undefined) {
208
- const evicted = this.cache.get(oldest);
209
- if (evicted) this.totalBytes -= evicted.data.length;
210
- this.cache.delete(oldest);
211
- }
212
- }
213
- this.totalBytes += data.length;
214
- this.cache.set(cacheKey, { data,
215
- contentType,
216
- timestamp: Date.now() });
217
- }
218
- }
@@ -1,368 +0,0 @@
1
- /**
2
- * Storage REST API routes using Hono
3
- */
4
-
5
- import { Hono } from "hono";
6
- import fs from "node:fs";
7
- import fsp from "node:fs/promises";
8
- import { StorageController } from "./types";
9
- import { LocalStorageController } from "./LocalStorageController";
10
- import { requireAuth as jwtRequireAuth, optionalAuth } from "../auth/middleware";
11
- import { ApiError, errorHandler } from "../api/errors";
12
- import { HonoEnv } from "../api/types";
13
- import { parseTransformOptions, transformImage, isTransformableImage, TransformCache } from "./image-transform";
14
- import { TusHandler } from "./tus-handler";
15
-
16
- /** Shared image transform cache (LRU, 500 entries, 1 hour TTL). */
17
- const transformCache = new TransformCache();
18
-
19
- export interface StorageRoutesConfig {
20
- controller: StorageController;
21
- /** Base path for storage routes (default: '/api/storage') */
22
- basePath?: string;
23
- /** Require authentication for write operations (default: true) */
24
- requireAuth?: boolean;
25
- /** Allow unauthenticated read access to stored files (default: false).
26
- * When false and requireAuth is true, reads also require authentication. */
27
- publicRead?: boolean;
28
- }
29
-
30
- /**
31
- * Extract the wildcard portion of a route path from the full request path.
32
- *
33
- * Hono's `c.req.param('*')` does not work reliably in sub-routers mounted
34
- * via `app.route(prefix, subRouter)`. Instead we derive the wildcard value
35
- * from the fully-resolved `c.req.path` and `c.req.routePath`.
36
- *
37
- * For a route `/metadata/*` mounted at `/api/storage`, a request to
38
- * `/api/storage/metadata/default/file.jpg` yields routePath
39
- * `/api/storage/metadata/*`. We strip the prefix (everything before `/*`)
40
- * plus one character for the trailing `/` to obtain `default/file.jpg`.
41
- */
42
- export function extractWildcardPath(c: { req: { path: string; routePath: string } }): string {
43
- const routePath = c.req.routePath; // e.g. "/api/storage/metadata/*"
44
- const prefix = routePath.replace("/*", ""); // e.g. "/api/storage/metadata"
45
- const fullPath = c.req.path; // e.g. "/api/storage/metadata/default/file.jpg"
46
- const idx = fullPath.indexOf(prefix);
47
- if (idx < 0) return "";
48
- // +1 to skip the '/' after the prefix
49
- return fullPath.substring(idx + prefix.length + 1);
50
- }
51
-
52
- /**
53
- * Sanitize a user-supplied storage key to prevent path traversal and other attacks.
54
- * Removes null bytes, ../ sequences, leading slashes, and limits length.
55
- */
56
- function sanitizeStorageKey(key: string): string {
57
- let sanitized = key;
58
- // Remove null bytes
59
- sanitized = sanitized.replace(/\0/g, "");
60
- // Remove ../ sequences (and ..\ on Windows)
61
- sanitized = sanitized.replace(/\.\.\/|\.\.\\/g, "");
62
- // Remove leading slashes
63
- sanitized = sanitized.replace(/^\/+/, "");
64
- // Limit length
65
- sanitized = sanitized.slice(0, 1024);
66
- return sanitized;
67
- }
68
-
69
- /**
70
- * Create storage REST API routes
71
- */
72
- export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv> {
73
- const router = new Hono<HonoEnv>();
74
- router.onError(errorHandler);
75
- const { controller, requireAuth = true, publicRead = false } = config;
76
-
77
- // Use actual JWT auth middleware from auth module
78
- const writeAuthMiddleware = requireAuth ? jwtRequireAuth : optionalAuth;
79
-
80
- // For read operations: respect publicRead config.
81
- const readAuthMiddleware = (publicRead || !requireAuth) ? optionalAuth : jwtRequireAuth;
82
-
83
- /**
84
- * Parse bucket and path from a combined file path.
85
- */
86
- const parseBucketAndPath = (filePath: string): { bucket: string; resolvedPath: string } => {
87
- const parts = filePath.split("/");
88
-
89
- // Only recognize 'default' as an explicit bucket prefix
90
- if (parts.length > 1 && parts[0].toLowerCase() === "default") {
91
- return {
92
- bucket: "default",
93
- resolvedPath: parts.slice(1).join("/")
94
- };
95
- }
96
-
97
- // All other paths use 'default' bucket with the full path
98
- return {
99
- bucket: "default",
100
- resolvedPath: filePath
101
- };
102
- };
103
-
104
- /**
105
- * POST /upload - Upload a file
106
- * Body: multipart/form-data with 'file' field
107
- * Request body can also contain metadata keys 'metadata_*'
108
- */
109
- router.post("/upload", writeAuthMiddleware, async (c) => {
110
- const body = await c.req.parseBody();
111
- const uploadedFile = body["file"];
112
-
113
- if (!uploadedFile || typeof uploadedFile === "string") {
114
- throw ApiError.badRequest("No file provided");
115
- }
116
-
117
- const key = typeof body["key"] === "string" ? body["key"] : "";
118
- const bucket = typeof body["bucket"] === "string" ? body["bucket"] : undefined;
119
-
120
- const finalKey = sanitizeStorageKey(key || uploadedFile.name || "unnamed");
121
-
122
- // Extract custom metadata from request body
123
- const metadata: Record<string, unknown> = {};
124
- for (const [k, value] of Object.entries(body)) {
125
- if (k.startsWith("metadata_")) {
126
- metadata[k.replace("metadata_", "")] = value;
127
- }
128
- }
129
-
130
- const result = await controller.putObject({
131
- file: uploadedFile,
132
- key: finalKey,
133
- metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
134
- bucket
135
- });
136
-
137
- return c.json({
138
- success: true,
139
- data: result
140
- }, 201);
141
- });
142
-
143
- /**
144
- * GET /file/* - Download/serve a file
145
- * Path: /file/{bucket}/{path} or /file/{path}
146
- */
147
- router.get("/file/*", readAuthMiddleware, async (c) => {
148
- // Allow cross-origin loading so admin frontends on different
149
- // ports (dev) or domains (CDN) can render images via <img>.
150
- c.header("Cross-Origin-Resource-Policy", "cross-origin");
151
-
152
- const rawPath = extractWildcardPath(c);
153
- if (!rawPath) {
154
- throw ApiError.notFound("File not found");
155
- }
156
-
157
- const filePath = decodeURIComponent(rawPath);
158
-
159
- // Parse image transform query params (e.g. ?width=300&format=webp)
160
- const transformOpts = parseTransformOptions(c.req.query() as Record<string, string>);
161
-
162
- // For local storage, serve the file directly from disk
163
- if (controller.getType() === "local") {
164
- const localController = controller as LocalStorageController;
165
- const { bucket, resolvedPath } = parseBucketAndPath(filePath);
166
-
167
- const absolutePath = localController.getAbsolutePath(resolvedPath, bucket);
168
-
169
- // Check if file exists
170
- try {
171
- await fsp.access(absolutePath);
172
- } catch {
173
- throw ApiError.notFound("File not found");
174
- }
175
-
176
- // Get content type from metadata or infer from extension
177
- let contentType = "application/octet-stream";
178
- const metadataPath = `${absolutePath}.metadata.json`;
179
- try {
180
- const metadataRaw = await fsp.readFile(metadataPath, "utf-8");
181
- const metadata = JSON.parse(metadataRaw);
182
- contentType = metadata.contentType || contentType;
183
- } catch {
184
- // Ignore metadata errors (file may not exist)
185
- }
186
-
187
- const fileContent = await fsp.readFile(absolutePath);
188
-
189
- // Apply image transforms if requested and the file is a transformable image
190
- if (transformOpts && isTransformableImage(contentType)) {
191
- const cacheKey = transformCache.buildKey(filePath, transformOpts);
192
- let cached = transformCache.get(cacheKey);
193
- if (!cached) {
194
- cached = await transformImage(Buffer.from(fileContent), transformOpts);
195
- transformCache.set(cacheKey, cached.data, cached.contentType);
196
- }
197
- c.header("Content-Type", cached.contentType);
198
- c.header("Cache-Control", "public, max-age=31536000, immutable");
199
- return c.body(new Uint8Array(cached.data));
200
- }
201
-
202
- c.header("Content-Type", contentType);
203
- return c.body(new Uint8Array(fileContent));
204
- }
205
-
206
- // For remote storage (S3, GCS, etc.), proxy the file through the backend.
207
- // We avoid redirecting to signed URLs because:
208
- // 1. Mixed-content (HTTPS page → HTTP MinIO) is blocked by browsers
209
- // 2. Internal IPs / VPC endpoints are unreachable from the browser
210
- const { bucket: parsedBucket, resolvedPath: parsedPath } = parseBucketAndPath(filePath);
211
- const fileObject = await controller.getObject(parsedPath, parsedBucket);
212
- if (!fileObject) {
213
- throw ApiError.notFound("File not found");
214
- }
215
-
216
- const remoteContentType = fileObject.type || "application/octet-stream";
217
-
218
- // Apply image transforms for remote storage too
219
- if (transformOpts && isTransformableImage(remoteContentType)) {
220
- const cacheKey = transformCache.buildKey(filePath, transformOpts);
221
- let cached = transformCache.get(cacheKey);
222
- if (!cached) {
223
- const buf = Buffer.from(await fileObject.arrayBuffer());
224
- cached = await transformImage(buf, transformOpts);
225
- transformCache.set(cacheKey, cached.data, cached.contentType);
226
- }
227
- c.header("Content-Type", cached.contentType);
228
- c.header("Cache-Control", "public, max-age=31536000, immutable");
229
- return c.body(new Uint8Array(cached.data));
230
- }
231
-
232
- c.header("Content-Type", remoteContentType);
233
- c.header("Cache-Control", "public, max-age=3600, immutable");
234
- const buf = await fileObject.arrayBuffer();
235
- return c.body(new Uint8Array(buf));
236
- });
237
-
238
- /**
239
- * GET /metadata/* - Get file metadata
240
- */
241
- router.get("/metadata/*", readAuthMiddleware, async (c) => {
242
- const rawPath = extractWildcardPath(c);
243
- if (!rawPath) {
244
- return c.json({
245
- success: true,
246
- data: null,
247
- fileNotFound: true
248
- }, 404);
249
- }
250
-
251
- const filePath = decodeURIComponent(rawPath);
252
- const { bucket, resolvedPath } = parseBucketAndPath(filePath);
253
-
254
- const downloadConfig = await controller.getSignedUrl(resolvedPath, bucket);
255
-
256
- if (downloadConfig.fileNotFound) {
257
- throw ApiError.notFound("File not found");
258
- }
259
-
260
- return c.json({
261
- success: true,
262
- data: downloadConfig.metadata
263
- });
264
- });
265
-
266
- /**
267
- * DELETE /file/* - Delete a file
268
- */
269
- router.delete("/file/*", writeAuthMiddleware, async (c) => {
270
- const rawPath = extractWildcardPath(c);
271
- if (!rawPath) {
272
- return c.json({ success: true,
273
- message: "No file to delete" });
274
- }
275
-
276
- const filePath = decodeURIComponent(rawPath);
277
- const { bucket, resolvedPath } = parseBucketAndPath(filePath);
278
-
279
- await controller.deleteObject(resolvedPath, bucket);
280
-
281
- return c.json({
282
- success: true,
283
- message: "File deleted"
284
- });
285
- });
286
-
287
- /**
288
- * GET /list - List files in a path
289
- */
290
- router.get("/list", writeAuthMiddleware, async (c) => {
291
- // Fallback to path for backward compatibility
292
- const storagePrefix = c.req.query("prefix") || c.req.query("path") || "";
293
- const bucket = c.req.query("bucket");
294
- const maxResults = c.req.query("maxResults");
295
- const pageToken = c.req.query("pageToken");
296
-
297
- const result = await controller.listObjects(
298
- storagePrefix,
299
- {
300
- bucket: bucket ?? (controller.getType() === "local" ? "default" : undefined),
301
- maxResults: maxResults ? parseInt(maxResults, 10) : undefined,
302
- pageToken
303
- }
304
- );
305
-
306
- return c.json({
307
- success: true,
308
- data: result
309
- });
310
- });
311
-
312
- /**
313
- * POST /folder - Create a new folder
314
- * Body: { path: string, bucket?: string }
315
- */
316
- router.post("/folder", writeAuthMiddleware, async (c) => {
317
- const body = await c.req.json();
318
- const folderPath = body.path;
319
-
320
- if (!folderPath || typeof folderPath !== "string") {
321
- throw ApiError.badRequest("Folder path is required");
322
- }
323
-
324
- const { bucket, resolvedPath } = parseBucketAndPath(folderPath);
325
-
326
- if (!resolvedPath || resolvedPath.trim() === "") {
327
- throw ApiError.badRequest("Invalid folder path");
328
- }
329
-
330
- if (controller.getType() === "local") {
331
- // For local storage, create the directory
332
- const localController = controller as LocalStorageController;
333
- const absolutePath = localController.getAbsolutePath(resolvedPath, bucket);
334
- fs.mkdirSync(absolutePath, { recursive: true });
335
- } else {
336
- // For S3-compatible storage, create a zero-byte marker object with trailing slash
337
- const key = resolvedPath.endsWith("/") ? resolvedPath : resolvedPath + "/";
338
- const emptyFile = new File([], key, { type: "application/x-directory" });
339
- await controller.putObject({
340
- file: emptyFile,
341
- key
342
- });
343
- }
344
-
345
- return c.json({
346
- success: true,
347
- message: "Folder created"
348
- }, 201);
349
- });
350
-
351
- // -----------------------------------------------------------------------
352
- // TUS Resumable Uploads
353
- // -----------------------------------------------------------------------
354
-
355
- const tusBaseDir = controller.getType() === "local"
356
- ? (controller as LocalStorageController).getBasePath()
357
- : (process.env.STORAGE_PATH || "./uploads");
358
- const tusHandler = new TusHandler(tusBaseDir, controller);
359
- tusHandler.startCleanup();
360
-
361
- router.options("/tus", (_c) => tusHandler.options());
362
- router.post("/tus", writeAuthMiddleware, async (c) => tusHandler.create(c));
363
- router.get("/tus/:id", readAuthMiddleware, (c) => tusHandler.head(c, c.req.param("id")));
364
- router.patch("/tus/:id", writeAuthMiddleware, async (c) => tusHandler.patch(c, c.req.param("id")));
365
- router.delete("/tus/:id", writeAuthMiddleware, async (c) => tusHandler.delete(c, c.req.param("id")));
366
-
367
- return router;
368
- }