@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,389 +0,0 @@
1
- /**
2
- * Local filesystem storage controller
3
- */
4
-
5
- import * as fs from "fs";
6
- import * as path from "path";
7
- import { promisify } from "util";
8
- import {
9
- StorageController,
10
- LocalStorageConfig,
11
- DEFAULT_MAX_FILE_SIZE
12
- } from "./types";
13
- import {
14
- UploadFileProps,
15
- UploadFileResult,
16
- DownloadConfig,
17
- DownloadMetadata,
18
- StorageListResult,
19
- StorageReference
20
- } from "@rebasepro/types";
21
-
22
- const mkdir = promisify(fs.mkdir);
23
- const writeFile = promisify(fs.writeFile);
24
- const readFile = promisify(fs.readFile);
25
- const unlink = promisify(fs.unlink);
26
- const readdir = promisify(fs.readdir);
27
- const stat = promisify(fs.stat);
28
- const access = promisify(fs.access);
29
-
30
- /**
31
- * Remove initial and trailing slashes from a path.
32
- * Handles paths like "/images/", "images/", "/images" → "images"
33
- */
34
- function normalizeStoragePath(s: string): string {
35
- let result = s;
36
- while (result.startsWith("/")) {
37
- result = result.slice(1);
38
- }
39
- while (result.endsWith("/")) {
40
- result = result.slice(0, -1);
41
- }
42
- return result;
43
- }
44
-
45
- /**
46
- * Local filesystem storage implementation
47
- * Stores files in a directory structure: {basePath}/{bucket}/{path}
48
- */
49
- export class LocalStorageController implements StorageController {
50
- private config: LocalStorageConfig;
51
- private basePath: string;
52
-
53
- constructor(config: LocalStorageConfig) {
54
- this.config = config;
55
- this.basePath = path.resolve(config.basePath);
56
- }
57
-
58
- getType(): "local" {
59
- return "local";
60
- }
61
-
62
- /**
63
- * Ensure directory exists, creating it if necessary
64
- */
65
- private async ensureDir(dirPath: string): Promise<void> {
66
- try {
67
- await mkdir(dirPath, { recursive: true });
68
- } catch (error: unknown) {
69
- if (error instanceof Error && (error as NodeJS.ErrnoException).code !== "EEXIST") {
70
- throw error;
71
- }
72
- }
73
- }
74
-
75
- /**
76
- * Get the full filesystem path for a storage path.
77
- * Includes a path traversal guard to prevent escaping the base directory.
78
- */
79
- private getFullPath(storagePath: string, bucket?: string): string {
80
- const bucketPath = bucket ? path.join(this.basePath, bucket) : this.basePath;
81
- const resolved = path.resolve(path.join(bucketPath, storagePath));
82
- if (!resolved.startsWith(bucketPath + path.sep) && resolved !== bucketPath) {
83
- throw new Error("Path traversal detected: resolved storage path is outside the bucket directory.");
84
- }
85
- return resolved;
86
- }
87
-
88
- /**
89
- * Validate file before upload
90
- */
91
- private validateFile(file: File): void {
92
- const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
93
- if (file.size > maxSize) {
94
- throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);
95
- }
96
-
97
- if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
98
- if (!this.config.allowedMimeTypes.includes(file.type)) {
99
- throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(", ")}`);
100
- }
101
- }
102
- }
103
-
104
- async putObject({
105
- file,
106
- key,
107
- metadata,
108
- bucket
109
- }: UploadFileProps): Promise<UploadFileResult> {
110
- this.validateFile(file);
111
-
112
- // Always use a bucket (default to 'default')
113
- const usedBucket = bucket ?? "default";
114
- const fullStoragePath = key;
115
- const fullPath = this.getFullPath(fullStoragePath, usedBucket);
116
-
117
- // Ensure parent directory exists
118
- await this.ensureDir(path.dirname(fullPath));
119
-
120
- // Convert File to Buffer and write
121
- const arrayBuffer = await file.arrayBuffer();
122
- const buffer = Buffer.from(arrayBuffer);
123
- await writeFile(fullPath, buffer);
124
-
125
- // Always save metadata file with at least contentType (required for preview)
126
- const metadataPath = `${fullPath}.metadata.json`;
127
- await writeFile(metadataPath, JSON.stringify({
128
- ...(metadata || {}),
129
- contentType: file.type,
130
- size: file.size,
131
- uploadedAt: new Date().toISOString()
132
- }, null, 2));
133
-
134
- return {
135
- key: fullStoragePath,
136
- bucket: usedBucket,
137
- storageUrl: `local://${usedBucket}/${fullStoragePath}`
138
- };
139
- }
140
-
141
- async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
142
- // Handle local:// URLs
143
- let resolvedPath = key;
144
- let resolvedBucket = bucket;
145
-
146
- if (key.startsWith("local://")) {
147
- const withoutProtocol = key.substring("local://".length);
148
- const firstSlash = withoutProtocol.indexOf("/");
149
- if (firstSlash > 0) {
150
- resolvedBucket = withoutProtocol.substring(0, firstSlash);
151
- resolvedPath = withoutProtocol.substring(firstSlash + 1);
152
- }
153
- }
154
-
155
- // Normalize path to handle leading/trailing slashes
156
- resolvedPath = normalizeStoragePath(resolvedPath);
157
- const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
158
-
159
- try {
160
- await access(fullPath, fs.constants.R_OK);
161
- } catch {
162
- return {
163
- url: null,
164
- fileNotFound: true
165
- };
166
- }
167
-
168
- // Read metadata if available
169
- let metadata: DownloadMetadata | undefined;
170
- const metadataPath = `${fullPath}.metadata.json`;
171
- try {
172
- const metadataContent = await readFile(metadataPath, "utf-8");
173
- const savedMetadata = JSON.parse(metadataContent);
174
- const fileStat = await stat(fullPath);
175
-
176
- metadata = {
177
- bucket: resolvedBucket ?? "default",
178
- fullPath: resolvedPath,
179
- name: path.basename(resolvedPath),
180
- size: fileStat.size,
181
- contentType: savedMetadata.contentType || "application/octet-stream",
182
- customMetadata: savedMetadata
183
- };
184
- } catch {
185
- // No metadata file, create basic metadata from stat
186
- try {
187
- const fileStat = await stat(fullPath);
188
- metadata = {
189
- bucket: resolvedBucket ?? "default",
190
- fullPath: resolvedPath,
191
- name: path.basename(resolvedPath),
192
- size: fileStat.size,
193
- contentType: "application/octet-stream",
194
- customMetadata: {}
195
- };
196
- } catch {
197
- // Stat failed
198
- }
199
- }
200
-
201
- // Return a relative URL that will be served by the storage routes
202
- const bucketPath = resolvedBucket ? `${resolvedBucket}/` : "";
203
- const url = `/api/storage/file/${bucketPath}${resolvedPath}`;
204
-
205
- return {
206
- url,
207
- metadata
208
- };
209
- }
210
-
211
- async getObject(key: string, bucket?: string): Promise<File | null> {
212
- // Handle local:// URLs
213
- let resolvedPath = key;
214
- let resolvedBucket = bucket;
215
-
216
- if (key.startsWith("local://")) {
217
- const withoutProtocol = key.substring("local://".length);
218
- const firstSlash = withoutProtocol.indexOf("/");
219
- if (firstSlash > 0) {
220
- resolvedBucket = withoutProtocol.substring(0, firstSlash);
221
- resolvedPath = withoutProtocol.substring(firstSlash + 1);
222
- }
223
- }
224
-
225
- // Normalize path to handle leading/trailing slashes
226
- resolvedPath = normalizeStoragePath(resolvedPath);
227
- const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
228
-
229
- try {
230
- await access(fullPath, fs.constants.R_OK);
231
- const buffer = await readFile(fullPath);
232
-
233
- // Try to get content type from metadata
234
- let contentType = "application/octet-stream";
235
- try {
236
- const metadataPath = `${fullPath}.metadata.json`;
237
- const metadataContent = await readFile(metadataPath, "utf-8");
238
- const metadata = JSON.parse(metadataContent);
239
- contentType = metadata.contentType || contentType;
240
- } catch {
241
- // No metadata, use default content type
242
- }
243
-
244
- const blob = new Blob([buffer], { type: contentType });
245
- return new File([blob], path.basename(resolvedPath), { type: contentType });
246
- } catch {
247
- return null;
248
- }
249
- }
250
-
251
- async deleteObject(key: string, bucket?: string): Promise<void> {
252
- // Handle local:// URLs
253
- let resolvedPath = key;
254
- let resolvedBucket = bucket;
255
-
256
- if (key.startsWith("local://")) {
257
- const withoutProtocol = key.substring("local://".length);
258
- const firstSlash = withoutProtocol.indexOf("/");
259
- if (firstSlash > 0) {
260
- resolvedBucket = withoutProtocol.substring(0, firstSlash);
261
- resolvedPath = withoutProtocol.substring(firstSlash + 1);
262
- }
263
- }
264
-
265
- // Normalize path to handle leading/trailing slashes
266
- resolvedPath = normalizeStoragePath(resolvedPath);
267
-
268
- if (!resolvedPath) {
269
- // Safety: never delete the bucket root
270
- return;
271
- }
272
-
273
- const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
274
-
275
- // Check if path exists before attempting to delete
276
- try {
277
- await access(fullPath, fs.constants.F_OK);
278
- } catch {
279
- // File doesn't exist — nothing to delete
280
- return;
281
- }
282
-
283
- try {
284
- const stats = await stat(fullPath);
285
- if (stats.isDirectory()) {
286
- // Only remove if empty — client must delete contents first
287
- await fs.promises.rmdir(fullPath);
288
- } else {
289
- await unlink(fullPath);
290
- // Also delete metadata file if exists
291
- try {
292
- await unlink(`${fullPath}.metadata.json`);
293
- } catch {
294
- // Metadata file might not exist
295
- }
296
- }
297
- } catch (error: unknown) {
298
- if (error instanceof Error) {
299
- const code = (error as NodeJS.ErrnoException).code;
300
- if (code === "ENOENT" || code === "ENOTEMPTY") {
301
- // File doesn't exist or directory not empty — ignore
302
- return;
303
- }
304
- }
305
- throw error;
306
- }
307
- }
308
-
309
- async listObjects(prefix: string, options?: {
310
- bucket?: string;
311
- maxResults?: number;
312
- pageToken?: string;
313
- }): Promise<StorageListResult> {
314
- // Normalize path to handle leading/trailing slashes
315
- const normalizedPath = normalizeStoragePath(prefix);
316
- const fullPath = this.getFullPath(normalizedPath, options?.bucket);
317
- const items: StorageReference[] = [];
318
- const prefixes: StorageReference[] = [];
319
-
320
- try {
321
- await access(fullPath, fs.constants.R_OK);
322
- const entries = await readdir(fullPath, { withFileTypes: true });
323
-
324
- let count = 0;
325
- const maxResults = options?.maxResults ?? 1000;
326
- const startIndex = options?.pageToken ? parseInt(options.pageToken, 10) : 0;
327
-
328
- for (let i = startIndex; i < entries.length && count < maxResults; i++) {
329
- const entry = entries[i];
330
-
331
- // Skip metadata files
332
- if (entry.name.endsWith(".metadata.json")) {
333
- continue;
334
- }
335
-
336
- const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
337
- const bucket = options?.bucket ?? "default";
338
-
339
- const ref: StorageReference = {
340
- bucket,
341
- fullPath: entryPath,
342
- name: entry.name,
343
- parent: null as never, // Simplified - not fully implementing parent chain
344
- root: null as never,
345
- toString: () => `local://${bucket}/${entryPath}`
346
- };
347
-
348
- if (entry.isDirectory()) {
349
- prefixes.push(ref);
350
- } else {
351
- items.push(ref);
352
- }
353
- count++;
354
- }
355
-
356
- const nextPageToken = startIndex + count < entries.length
357
- ? String(startIndex + count)
358
- : undefined;
359
-
360
- return {
361
- items,
362
- prefixes,
363
- nextPageToken
364
- };
365
- } catch (error: unknown) {
366
- const code = (error as NodeJS.ErrnoException)?.code;
367
- if (code === "ENOENT" || code === "ENOTDIR") {
368
- return { items: [],
369
- prefixes: [] };
370
- }
371
- throw error;
372
- }
373
- }
374
-
375
- /**
376
- * Get the absolute filesystem path for serving files
377
- * Used by the storage routes to serve files directly
378
- */
379
- getAbsolutePath(key: string, bucket?: string): string {
380
- return this.getFullPath(key, bucket);
381
- }
382
-
383
- /**
384
- * Get the base path for the storage
385
- */
386
- getBasePath(): string {
387
- return this.basePath;
388
- }
389
- }
@@ -1,300 +0,0 @@
1
- /**
2
- * S3-compatible storage controller (works with AWS S3 and MinIO)
3
- */
4
-
5
- import {
6
- S3Client,
7
- PutObjectCommand,
8
- GetObjectCommand,
9
- DeleteObjectCommand,
10
- ListObjectsV2Command,
11
- HeadObjectCommand,
12
- _Object,
13
- CommonPrefix
14
- } from "@aws-sdk/client-s3";
15
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
16
- import {
17
- StorageController,
18
- S3StorageConfig,
19
- DEFAULT_MAX_FILE_SIZE
20
- } from "./types";
21
- import {
22
- UploadFileProps,
23
- UploadFileResult,
24
- DownloadConfig,
25
- DownloadMetadata,
26
- StorageListResult,
27
- StorageReference
28
- } from "@rebasepro/types";
29
-
30
- /**
31
- * S3-compatible storage implementation
32
- * Works with AWS S3 and MinIO (with forcePathStyle option)
33
- */
34
- export class S3StorageController implements StorageController {
35
- private config: S3StorageConfig;
36
- private client: S3Client;
37
-
38
- constructor(config: S3StorageConfig) {
39
- this.config = config;
40
- this.client = new S3Client({
41
- region: config.region || "us-east-1",
42
- endpoint: config.endpoint,
43
- forcePathStyle: config.forcePathStyle ?? !!config.endpoint, // Auto-enable for custom endpoints (MinIO)
44
- credentials: {
45
- accessKeyId: config.accessKeyId,
46
- secretAccessKey: config.secretAccessKey
47
- }
48
- });
49
- }
50
-
51
- getType(): "s3" {
52
- return "s3";
53
- }
54
-
55
- /**
56
- * Validate file before upload
57
- */
58
- private validateFile(file: File): void {
59
- const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
60
- if (file.size > maxSize) {
61
- throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);
62
- }
63
-
64
- if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {
65
- if (!this.config.allowedMimeTypes.includes(file.type)) {
66
- throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(", ")}`);
67
- }
68
- }
69
- }
70
-
71
- /**
72
- * Get the bucket name - either from parameter or config
73
- */
74
- private getBucket(bucket?: string): string {
75
- // "default" is a logical bucket name used by local storage;
76
- // for S3 it should resolve to the configured bucket.
77
- if (!bucket || bucket === "default") return this.config.bucket;
78
- return bucket;
79
- }
80
-
81
- async putObject({
82
- file,
83
- key,
84
- metadata,
85
- bucket
86
- }: UploadFileProps): Promise<UploadFileResult> {
87
- this.validateFile(file);
88
-
89
- const usedBucket = this.getBucket(bucket);
90
-
91
- // Convert File to Buffer
92
- const arrayBuffer = await file.arrayBuffer();
93
- const buffer = Buffer.from(arrayBuffer);
94
-
95
- const command = new PutObjectCommand({
96
- Bucket: usedBucket,
97
- Key: key,
98
- Body: buffer,
99
- ContentType: file.type,
100
- Metadata: metadata ? this.flattenMetadata(metadata) : undefined
101
- });
102
-
103
- await this.client.send(command);
104
-
105
- return {
106
- key,
107
- bucket: usedBucket,
108
- storageUrl: `s3://${usedBucket}/${key}`
109
- };
110
- }
111
-
112
- /**
113
- * Flatten nested metadata to string values (S3 requirement)
114
- */
115
- private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {
116
- const flattened: Record<string, string> = {};
117
- for (const [key, value] of Object.entries(metadata)) {
118
- if (typeof value === "string") {
119
- flattened[key] = value;
120
- } else if (value !== undefined && value !== null) {
121
- flattened[key] = JSON.stringify(value);
122
- }
123
- }
124
- return flattened;
125
- }
126
-
127
- async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
128
- // Handle s3:// and gs:// URLs for backward compatibility
129
- let resolvedPath = key;
130
- let resolvedBucket = this.getBucket(bucket);
131
-
132
- const match = key.match(/^(s3|gs):\/\//);
133
- if (match) {
134
- const protocolLength = match[0].length;
135
- const withoutProtocol = key.substring(protocolLength);
136
- const firstSlash = withoutProtocol.indexOf("/");
137
- if (firstSlash > 0) {
138
- resolvedBucket = withoutProtocol.substring(0, firstSlash);
139
- resolvedPath = withoutProtocol.substring(firstSlash + 1);
140
- }
141
- }
142
-
143
- try {
144
- // First check if the object exists and get metadata
145
- const headCommand = new HeadObjectCommand({
146
- Bucket: resolvedBucket,
147
- Key: resolvedPath
148
- });
149
-
150
- const headResult = await this.client.send(headCommand);
151
-
152
- // Generate a signed URL
153
- const getCommand = new GetObjectCommand({
154
- Bucket: resolvedBucket,
155
- Key: resolvedPath
156
- });
157
-
158
- const expiresIn = this.config.signedUrlExpiration ?? 3600;
159
- const url = await getSignedUrl(this.client, getCommand, { expiresIn });
160
-
161
- const metadata: DownloadMetadata = {
162
- bucket: resolvedBucket,
163
- fullPath: resolvedPath,
164
- name: resolvedPath.split("/").pop() || resolvedPath,
165
- size: headResult.ContentLength || 0,
166
- contentType: headResult.ContentType || "application/octet-stream",
167
- customMetadata: headResult.Metadata || {}
168
- };
169
-
170
- return {
171
- url,
172
- metadata
173
- };
174
- } catch (error: unknown) {
175
- const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
176
- if (s3Error.name === "NotFound" || s3Error.$metadata?.httpStatusCode === 404) {
177
- return {
178
- url: null,
179
- fileNotFound: true
180
- };
181
- }
182
- throw error;
183
- }
184
- }
185
-
186
- async getObject(key: string, bucket?: string): Promise<File | null> {
187
- // Handle s3:// and gs:// URLs
188
- let resolvedPath = key;
189
- let resolvedBucket = this.getBucket(bucket);
190
-
191
- const match = key.match(/^(s3|gs):\/\//);
192
- if (match) {
193
- const protocolLength = match[0].length;
194
- const withoutProtocol = key.substring(protocolLength);
195
- const firstSlash = withoutProtocol.indexOf("/");
196
- if (firstSlash > 0) {
197
- resolvedBucket = withoutProtocol.substring(0, firstSlash);
198
- resolvedPath = withoutProtocol.substring(firstSlash + 1);
199
- }
200
- }
201
-
202
- try {
203
- const command = new GetObjectCommand({
204
- Bucket: resolvedBucket,
205
- Key: resolvedPath
206
- });
207
-
208
- const response = await this.client.send(command);
209
-
210
- if (!response.Body) {
211
- return null;
212
- }
213
-
214
- // Convert stream to buffer
215
- const chunks: Uint8Array[] = [];
216
- for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {
217
- chunks.push(chunk);
218
- }
219
- const buffer = Buffer.concat(chunks);
220
-
221
- const contentType = response.ContentType || "application/octet-stream";
222
- const fileName = resolvedPath.split("/").pop() || resolvedPath;
223
-
224
- const blob = new Blob([buffer], { type: contentType });
225
- return new File([blob], fileName, { type: contentType });
226
- } catch (error: unknown) {
227
- const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
228
- if (s3Error.name === "NoSuchKey" || s3Error.$metadata?.httpStatusCode === 404) {
229
- return null;
230
- }
231
- throw error;
232
- }
233
- }
234
-
235
- async deleteObject(key: string, bucket?: string): Promise<void> {
236
- // Handle s3:// and gs:// URLs
237
- let resolvedPath = key;
238
- let resolvedBucket = this.getBucket(bucket);
239
-
240
- const match = key.match(/^(s3|gs):\/\//);
241
- if (match) {
242
- const protocolLength = match[0].length;
243
- const withoutProtocol = key.substring(protocolLength);
244
- const firstSlash = withoutProtocol.indexOf("/");
245
- if (firstSlash > 0) {
246
- resolvedBucket = withoutProtocol.substring(0, firstSlash);
247
- resolvedPath = withoutProtocol.substring(firstSlash + 1);
248
- }
249
- }
250
-
251
- const command = new DeleteObjectCommand({
252
- Bucket: resolvedBucket,
253
- Key: resolvedPath
254
- });
255
-
256
- await this.client.send(command);
257
- }
258
-
259
- async listObjects(prefix: string, options?: {
260
- bucket?: string;
261
- maxResults?: number;
262
- pageToken?: string;
263
- }): Promise<StorageListResult> {
264
- const resolvedBucket = this.getBucket(options?.bucket);
265
-
266
- const command = new ListObjectsV2Command({
267
- Bucket: resolvedBucket,
268
- Prefix: prefix || undefined,
269
- MaxKeys: options?.maxResults ?? 1000,
270
- ContinuationToken: options?.pageToken,
271
- Delimiter: "/" // This gives us folder-like behavior
272
- });
273
-
274
- const response = await this.client.send(command);
275
-
276
- const items: StorageReference[] = (response.Contents || []).map(obj => ({
277
- bucket: resolvedBucket,
278
- fullPath: obj.Key || "",
279
- name: (obj.Key || "").split("/").pop() || "",
280
- parent: null as never,
281
- root: null as never,
282
- toString: () => `s3://${resolvedBucket}/${obj.Key}`
283
- }));
284
-
285
- const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({
286
- bucket: resolvedBucket,
287
- fullPath: prefix.Prefix || "",
288
- name: (prefix.Prefix || "").replace(/\/$/, "").split("/").pop() || "",
289
- parent: null as never,
290
- root: null as never,
291
- toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`
292
- }));
293
-
294
- return {
295
- items,
296
- prefixes,
297
- nextPageToken: response.NextContinuationToken
298
- };
299
- }
300
- }