@rebasepro/server-core 0.0.1-canary.09e5ec5

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/LICENSE +6 -0
  2. package/README.md +40 -0
  3. package/build-errors.txt +52 -0
  4. package/coverage/clover.xml +3739 -0
  5. package/coverage/coverage-final.json +31 -0
  6. package/coverage/lcov-report/base.css +224 -0
  7. package/coverage/lcov-report/block-navigation.js +87 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/index.html +266 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
  15. package/coverage/lcov-report/src/api/errors.ts.html +472 -0
  16. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
  17. package/coverage/lcov-report/src/api/graphql/index.html +116 -0
  18. package/coverage/lcov-report/src/api/index.html +176 -0
  19. package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
  20. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
  21. package/coverage/lcov-report/src/api/rest/index.html +131 -0
  22. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
  23. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
  24. package/coverage/lcov-report/src/api/server.ts.html +823 -0
  25. package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
  26. package/coverage/lcov-report/src/auth/index.html +176 -0
  27. package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
  28. package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
  29. package/coverage/lcov-report/src/auth/password.ts.html +310 -0
  30. package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
  31. package/coverage/lcov-report/src/collections/index.html +116 -0
  32. package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
  33. package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
  34. package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
  35. package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
  36. package/coverage/lcov-report/src/db/index.html +146 -0
  37. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
  38. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
  39. package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
  40. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
  41. package/coverage/lcov-report/src/db/services/index.html +176 -0
  42. package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
  43. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
  44. package/coverage/lcov-report/src/index.html +116 -0
  45. package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
  46. package/coverage/lcov-report/src/services/index.html +131 -0
  47. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
  48. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
  49. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
  50. package/coverage/lcov-report/src/storage/index.html +161 -0
  51. package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
  52. package/coverage/lcov-report/src/storage/types.ts.html +451 -0
  53. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
  54. package/coverage/lcov-report/src/utils/index.html +116 -0
  55. package/coverage/lcov.info +7179 -0
  56. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  57. package/dist/common/src/collections/index.d.ts +1 -0
  58. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  59. package/dist/common/src/index.d.ts +3 -0
  60. package/dist/common/src/util/builders.d.ts +57 -0
  61. package/dist/common/src/util/callbacks.d.ts +6 -0
  62. package/dist/common/src/util/collections.d.ts +11 -0
  63. package/dist/common/src/util/common.d.ts +2 -0
  64. package/dist/common/src/util/conditions.d.ts +26 -0
  65. package/dist/common/src/util/entities.d.ts +58 -0
  66. package/dist/common/src/util/enums.d.ts +3 -0
  67. package/dist/common/src/util/index.d.ts +16 -0
  68. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  69. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  70. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  71. package/dist/common/src/util/paths.d.ts +14 -0
  72. package/dist/common/src/util/permissions.d.ts +5 -0
  73. package/dist/common/src/util/references.d.ts +2 -0
  74. package/dist/common/src/util/relations.d.ts +22 -0
  75. package/dist/common/src/util/resolutions.d.ts +72 -0
  76. package/dist/common/src/util/storage.d.ts +24 -0
  77. package/dist/index-DXVBFp5V.js +37 -0
  78. package/dist/index-DXVBFp5V.js.map +1 -0
  79. package/dist/index.es.js +49934 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/index.umd.js +49968 -0
  82. package/dist/index.umd.js.map +1 -0
  83. package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
  84. package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  85. package/dist/server-core/src/api/errors.d.ts +35 -0
  86. package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
  87. package/dist/server-core/src/api/graphql/index.d.ts +1 -0
  88. package/dist/server-core/src/api/index.d.ts +9 -0
  89. package/dist/server-core/src/api/openapi-generator.d.ts +16 -0
  90. package/dist/server-core/src/api/rest/api-generator.d.ts +64 -0
  91. package/dist/server-core/src/api/rest/index.d.ts +1 -0
  92. package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
  93. package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
  94. package/dist/server-core/src/api/server.d.ts +40 -0
  95. package/dist/server-core/src/api/types.d.ts +90 -0
  96. package/dist/server-core/src/auth/admin-routes.d.ts +16 -0
  97. package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
  98. package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
  99. package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
  100. package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
  101. package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
  102. package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
  103. package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
  104. package/dist/server-core/src/auth/index.d.ts +23 -0
  105. package/dist/server-core/src/auth/interfaces.d.ts +309 -0
  106. package/dist/server-core/src/auth/jwt.d.ts +43 -0
  107. package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
  108. package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
  109. package/dist/server-core/src/auth/middleware.d.ts +81 -0
  110. package/dist/server-core/src/auth/password.d.ts +22 -0
  111. package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
  112. package/dist/server-core/src/auth/routes.d.ts +27 -0
  113. package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
  114. package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
  115. package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
  116. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  117. package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
  118. package/dist/server-core/src/collections/loader.d.ts +5 -0
  119. package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
  120. package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
  121. package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
  122. package/dist/server-core/src/cron/cron-store.d.ts +32 -0
  123. package/dist/server-core/src/cron/index.d.ts +6 -0
  124. package/dist/server-core/src/db/interfaces.d.ts +18 -0
  125. package/dist/server-core/src/email/index.d.ts +6 -0
  126. package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
  127. package/dist/server-core/src/email/templates.d.ts +42 -0
  128. package/dist/server-core/src/email/types.d.ts +107 -0
  129. package/dist/server-core/src/functions/function-loader.d.ts +17 -0
  130. package/dist/server-core/src/functions/function-routes.d.ts +10 -0
  131. package/dist/server-core/src/functions/index.d.ts +3 -0
  132. package/dist/server-core/src/history/history-routes.d.ts +23 -0
  133. package/dist/server-core/src/history/index.d.ts +1 -0
  134. package/dist/server-core/src/index.d.ts +29 -0
  135. package/dist/server-core/src/init.d.ts +159 -0
  136. package/dist/server-core/src/serve-spa.d.ts +30 -0
  137. package/dist/server-core/src/services/driver-registry.d.ts +78 -0
  138. package/dist/server-core/src/singleton.d.ts +35 -0
  139. package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
  140. package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
  141. package/dist/server-core/src/storage/index.d.ts +25 -0
  142. package/dist/server-core/src/storage/routes.d.ts +38 -0
  143. package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
  144. package/dist/server-core/src/storage/types.d.ts +103 -0
  145. package/dist/server-core/src/types/index.d.ts +11 -0
  146. package/dist/server-core/src/utils/dev-port.d.ts +35 -0
  147. package/dist/server-core/src/utils/logger.d.ts +31 -0
  148. package/dist/server-core/src/utils/logging.d.ts +9 -0
  149. package/dist/server-core/src/utils/request-logger.d.ts +19 -0
  150. package/dist/server-core/src/utils/sql.d.ts +27 -0
  151. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  152. package/dist/types/src/controllers/auth.d.ts +119 -0
  153. package/dist/types/src/controllers/client.d.ts +170 -0
  154. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  155. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  156. package/dist/types/src/controllers/data.d.ts +168 -0
  157. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  158. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  159. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  160. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  161. package/dist/types/src/controllers/email.d.ts +34 -0
  162. package/dist/types/src/controllers/index.d.ts +18 -0
  163. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  164. package/dist/types/src/controllers/navigation.d.ts +213 -0
  165. package/dist/types/src/controllers/registry.d.ts +54 -0
  166. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  167. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  168. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  169. package/dist/types/src/controllers/storage.d.ts +171 -0
  170. package/dist/types/src/index.d.ts +4 -0
  171. package/dist/types/src/rebase_context.d.ts +105 -0
  172. package/dist/types/src/types/backend.d.ts +536 -0
  173. package/dist/types/src/types/builders.d.ts +15 -0
  174. package/dist/types/src/types/chips.d.ts +5 -0
  175. package/dist/types/src/types/collections.d.ts +856 -0
  176. package/dist/types/src/types/cron.d.ts +102 -0
  177. package/dist/types/src/types/data_source.d.ts +64 -0
  178. package/dist/types/src/types/entities.d.ts +145 -0
  179. package/dist/types/src/types/entity_actions.d.ts +98 -0
  180. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  181. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  182. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  183. package/dist/types/src/types/entity_views.d.ts +61 -0
  184. package/dist/types/src/types/export_import.d.ts +21 -0
  185. package/dist/types/src/types/index.d.ts +23 -0
  186. package/dist/types/src/types/locales.d.ts +4 -0
  187. package/dist/types/src/types/modify_collections.d.ts +5 -0
  188. package/dist/types/src/types/plugins.d.ts +279 -0
  189. package/dist/types/src/types/properties.d.ts +1176 -0
  190. package/dist/types/src/types/property_config.d.ts +70 -0
  191. package/dist/types/src/types/relations.d.ts +336 -0
  192. package/dist/types/src/types/slots.d.ts +252 -0
  193. package/dist/types/src/types/translations.d.ts +870 -0
  194. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  195. package/dist/types/src/types/websockets.d.ts +78 -0
  196. package/dist/types/src/users/index.d.ts +2 -0
  197. package/dist/types/src/users/roles.d.ts +22 -0
  198. package/dist/types/src/users/user.d.ts +46 -0
  199. package/history_diff.log +385 -0
  200. package/jest.config.cjs +16 -0
  201. package/package.json +86 -0
  202. package/scratch.ts +9 -0
  203. package/src/api/ast-schema-editor.ts +289 -0
  204. package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
  205. package/src/api/errors.ts +179 -0
  206. package/src/api/graphql/graphql-schema-generator.ts +336 -0
  207. package/src/api/graphql/index.ts +2 -0
  208. package/src/api/index.ts +11 -0
  209. package/src/api/openapi-generator.ts +715 -0
  210. package/src/api/rest/api-generator.ts +472 -0
  211. package/src/api/rest/index.ts +2 -0
  212. package/src/api/rest/query-parser.ts +155 -0
  213. package/src/api/schema-editor-routes.ts +41 -0
  214. package/src/api/server.ts +248 -0
  215. package/src/api/types.ts +90 -0
  216. package/src/auth/admin-routes.ts +529 -0
  217. package/src/auth/apple-oauth.ts +130 -0
  218. package/src/auth/bitbucket-oauth.ts +82 -0
  219. package/src/auth/discord-oauth.ts +83 -0
  220. package/src/auth/facebook-oauth.ts +72 -0
  221. package/src/auth/github-oauth.ts +110 -0
  222. package/src/auth/gitlab-oauth.ts +70 -0
  223. package/src/auth/google-oauth.ts +48 -0
  224. package/src/auth/index.ts +34 -0
  225. package/src/auth/interfaces.ts +363 -0
  226. package/src/auth/jwt.ts +181 -0
  227. package/src/auth/linkedin-oauth.ts +81 -0
  228. package/src/auth/microsoft-oauth.ts +88 -0
  229. package/src/auth/middleware.ts +384 -0
  230. package/src/auth/password.ts +77 -0
  231. package/src/auth/rate-limiter.ts +129 -0
  232. package/src/auth/routes.ts +788 -0
  233. package/src/auth/slack-oauth.ts +71 -0
  234. package/src/auth/spotify-oauth.ts +67 -0
  235. package/src/auth/twitter-oauth.ts +120 -0
  236. package/src/bootstrappers/index.ts +1 -0
  237. package/src/collections/BackendCollectionRegistry.ts +20 -0
  238. package/src/collections/loader.ts +49 -0
  239. package/src/cron/cron-loader.ts +89 -0
  240. package/src/cron/cron-routes.test.ts +265 -0
  241. package/src/cron/cron-routes.ts +85 -0
  242. package/src/cron/cron-scheduler.test.ts +421 -0
  243. package/src/cron/cron-scheduler.ts +413 -0
  244. package/src/cron/cron-store.ts +163 -0
  245. package/src/cron/index.ts +6 -0
  246. package/src/db/interfaces.ts +60 -0
  247. package/src/email/index.ts +18 -0
  248. package/src/email/smtp-email-service.ts +91 -0
  249. package/src/email/templates.ts +388 -0
  250. package/src/email/types.ts +105 -0
  251. package/src/functions/function-loader.ts +119 -0
  252. package/src/functions/function-routes.ts +31 -0
  253. package/src/functions/index.ts +3 -0
  254. package/src/history/history-routes.ts +129 -0
  255. package/src/history/index.ts +2 -0
  256. package/src/index.ts +66 -0
  257. package/src/init.ts +727 -0
  258. package/src/serve-spa.ts +81 -0
  259. package/src/services/driver-registry.ts +182 -0
  260. package/src/singleton.test.ts +28 -0
  261. package/src/singleton.ts +70 -0
  262. package/src/storage/LocalStorageController.ts +365 -0
  263. package/src/storage/S3StorageController.ts +298 -0
  264. package/src/storage/index.ts +43 -0
  265. package/src/storage/routes.ts +264 -0
  266. package/src/storage/storage-registry.ts +187 -0
  267. package/src/storage/types.ts +134 -0
  268. package/src/types/index.ts +27 -0
  269. package/src/utils/dev-port.ts +176 -0
  270. package/src/utils/logger.ts +143 -0
  271. package/src/utils/logging.ts +38 -0
  272. package/src/utils/request-logger.ts +66 -0
  273. package/src/utils/sql.ts +38 -0
  274. package/test/admin-routes.test.ts +640 -0
  275. package/test/api-generator.test.ts +501 -0
  276. package/test/ast-schema-editor.test.ts +63 -0
  277. package/test/auth-middleware-hono.test.ts +556 -0
  278. package/test/auth-routes.test.ts +1047 -0
  279. package/test/driver-registry.test.ts +282 -0
  280. package/test/error-propagation.test.ts +226 -0
  281. package/test/errors-hono.test.ts +133 -0
  282. package/test/errors.test.ts +155 -0
  283. package/test/jwt-security.test.ts +182 -0
  284. package/test/jwt.test.ts +324 -0
  285. package/test/middleware.test.ts +300 -0
  286. package/test/password.test.ts +165 -0
  287. package/test/query-parser.test.ts +263 -0
  288. package/test/rate-limiter.test.ts +102 -0
  289. package/test/safe-compare.test.ts +66 -0
  290. package/test/singleton.test.ts +59 -0
  291. package/test/storage-local.test.ts +271 -0
  292. package/test/storage-registry.test.ts +282 -0
  293. package/test/storage-routes.test.ts +222 -0
  294. package/test/storage-s3.test.ts +304 -0
  295. package/test-ast.ts +28 -0
  296. package/test.ts +6 -0
  297. package/test_output.txt +1133 -0
  298. package/tsconfig.json +49 -0
  299. package/tsconfig.prod.json +20 -0
  300. package/vite.config.ts +80 -0
@@ -0,0 +1,365 @@
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 parts = bucket ? [this.basePath, bucket, storagePath] : [this.basePath, storagePath];
81
+ const resolved = path.resolve(path.join(...parts));
82
+ if (!resolved.startsWith(this.basePath + path.sep) && resolved !== this.basePath) {
83
+ throw new Error("Path traversal detected: resolved storage path is outside the base 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
+ const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
268
+
269
+ try {
270
+ await unlink(fullPath);
271
+ // Also delete metadata file if exists
272
+ try {
273
+ await unlink(`${fullPath}.metadata.json`);
274
+ } catch {
275
+ // Metadata file might not exist
276
+ }
277
+ } catch (error: unknown) {
278
+ if (error instanceof Error && (error as NodeJS.ErrnoException).code !== "ENOENT") {
279
+ throw error;
280
+ }
281
+ // File doesn't exist, nothing to delete
282
+ }
283
+ }
284
+
285
+ async listObjects(prefix: string, options?: {
286
+ bucket?: string;
287
+ maxResults?: number;
288
+ pageToken?: string;
289
+ }): Promise<StorageListResult> {
290
+ // Normalize path to handle leading/trailing slashes
291
+ const normalizedPath = normalizeStoragePath(prefix);
292
+ const fullPath = this.getFullPath(normalizedPath, options?.bucket);
293
+ const items: StorageReference[] = [];
294
+ const prefixes: StorageReference[] = [];
295
+
296
+ try {
297
+ await access(fullPath, fs.constants.R_OK);
298
+ const entries = await readdir(fullPath, { withFileTypes: true });
299
+
300
+ let count = 0;
301
+ const maxResults = options?.maxResults ?? 1000;
302
+ const startIndex = options?.pageToken ? parseInt(options.pageToken, 10) : 0;
303
+
304
+ for (let i = startIndex; i < entries.length && count < maxResults; i++) {
305
+ const entry = entries[i];
306
+
307
+ // Skip metadata files
308
+ if (entry.name.endsWith(".metadata.json")) {
309
+ continue;
310
+ }
311
+
312
+ const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
313
+ const bucket = options?.bucket ?? "default";
314
+
315
+ const ref: StorageReference = {
316
+ bucket,
317
+ fullPath: entryPath,
318
+ name: entry.name,
319
+ parent: null as never, // Simplified - not fully implementing parent chain
320
+ root: null as never,
321
+ toString: () => `local://${bucket}/${entryPath}`
322
+ };
323
+
324
+ if (entry.isDirectory()) {
325
+ prefixes.push(ref);
326
+ } else {
327
+ items.push(ref);
328
+ }
329
+ count++;
330
+ }
331
+
332
+ const nextPageToken = startIndex + count < entries.length
333
+ ? String(startIndex + count)
334
+ : undefined;
335
+
336
+ return {
337
+ items,
338
+ prefixes,
339
+ nextPageToken
340
+ };
341
+ } catch (error: unknown) {
342
+ const code = (error as NodeJS.ErrnoException)?.code;
343
+ if (code === "ENOENT" || code === "ENOTDIR") {
344
+ return { items: [],
345
+ prefixes: [] };
346
+ }
347
+ throw error;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Get the absolute filesystem path for serving files
353
+ * Used by the storage routes to serve files directly
354
+ */
355
+ getAbsolutePath(key: string, bucket?: string): string {
356
+ return this.getFullPath(key, bucket);
357
+ }
358
+
359
+ /**
360
+ * Get the base path for the storage
361
+ */
362
+ getBasePath(): string {
363
+ return this.basePath;
364
+ }
365
+ }
@@ -0,0 +1,298 @@
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
+ return bucket ?? this.config.bucket;
76
+ }
77
+
78
+ async putObject({
79
+ file,
80
+ key,
81
+ metadata,
82
+ bucket
83
+ }: UploadFileProps): Promise<UploadFileResult> {
84
+ this.validateFile(file);
85
+
86
+ const usedBucket = this.getBucket(bucket);
87
+
88
+ // Convert File to Buffer
89
+ const arrayBuffer = await file.arrayBuffer();
90
+ const buffer = Buffer.from(arrayBuffer);
91
+
92
+ const command = new PutObjectCommand({
93
+ Bucket: usedBucket,
94
+ Key: key,
95
+ Body: buffer,
96
+ ContentType: file.type,
97
+ Metadata: metadata ? this.flattenMetadata(metadata) : undefined
98
+ });
99
+
100
+ await this.client.send(command);
101
+
102
+ return {
103
+ key,
104
+ bucket: usedBucket,
105
+ storageUrl: `s3://${usedBucket}/${key}`
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Flatten nested metadata to string values (S3 requirement)
111
+ */
112
+ private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {
113
+ const flattened: Record<string, string> = {};
114
+ for (const [key, value] of Object.entries(metadata)) {
115
+ if (typeof value === "string") {
116
+ flattened[key] = value;
117
+ } else if (value !== undefined && value !== null) {
118
+ flattened[key] = JSON.stringify(value);
119
+ }
120
+ }
121
+ return flattened;
122
+ }
123
+
124
+ async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {
125
+ // Handle s3:// and gs:// URLs for backward compatibility
126
+ let resolvedPath = key;
127
+ let resolvedBucket = this.getBucket(bucket);
128
+
129
+ const match = key.match(/^(s3|gs):\/\//);
130
+ if (match) {
131
+ const protocolLength = match[0].length;
132
+ const withoutProtocol = key.substring(protocolLength);
133
+ const firstSlash = withoutProtocol.indexOf("/");
134
+ if (firstSlash > 0) {
135
+ resolvedBucket = withoutProtocol.substring(0, firstSlash);
136
+ resolvedPath = withoutProtocol.substring(firstSlash + 1);
137
+ }
138
+ }
139
+
140
+ try {
141
+ // First check if the object exists and get metadata
142
+ const headCommand = new HeadObjectCommand({
143
+ Bucket: resolvedBucket,
144
+ Key: resolvedPath
145
+ });
146
+
147
+ const headResult = await this.client.send(headCommand);
148
+
149
+ // Generate a signed URL
150
+ const getCommand = new GetObjectCommand({
151
+ Bucket: resolvedBucket,
152
+ Key: resolvedPath
153
+ });
154
+
155
+ const expiresIn = this.config.signedUrlExpiration ?? 3600;
156
+ const url = await getSignedUrl(this.client, getCommand, { expiresIn });
157
+
158
+ const metadata: DownloadMetadata = {
159
+ bucket: resolvedBucket,
160
+ fullPath: resolvedPath,
161
+ name: resolvedPath.split("/").pop() || resolvedPath,
162
+ size: headResult.ContentLength || 0,
163
+ contentType: headResult.ContentType || "application/octet-stream",
164
+ customMetadata: headResult.Metadata || {}
165
+ };
166
+
167
+ return {
168
+ url,
169
+ metadata
170
+ };
171
+ } catch (error: unknown) {
172
+ const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
173
+ if (s3Error.name === "NotFound" || s3Error.$metadata?.httpStatusCode === 404) {
174
+ return {
175
+ url: null,
176
+ fileNotFound: true
177
+ };
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ async getObject(key: string, bucket?: string): Promise<File | null> {
184
+ // Handle s3:// and gs:// URLs
185
+ let resolvedPath = key;
186
+ let resolvedBucket = this.getBucket(bucket);
187
+
188
+ const match = key.match(/^(s3|gs):\/\//);
189
+ if (match) {
190
+ const protocolLength = match[0].length;
191
+ const withoutProtocol = key.substring(protocolLength);
192
+ const firstSlash = withoutProtocol.indexOf("/");
193
+ if (firstSlash > 0) {
194
+ resolvedBucket = withoutProtocol.substring(0, firstSlash);
195
+ resolvedPath = withoutProtocol.substring(firstSlash + 1);
196
+ }
197
+ }
198
+
199
+ try {
200
+ const command = new GetObjectCommand({
201
+ Bucket: resolvedBucket,
202
+ Key: resolvedPath
203
+ });
204
+
205
+ const response = await this.client.send(command);
206
+
207
+ if (!response.Body) {
208
+ return null;
209
+ }
210
+
211
+ // Convert stream to buffer
212
+ const chunks: Uint8Array[] = [];
213
+ // @ts-ignore - Body is a ReadableStream in Node.js
214
+ for await (const chunk of response.Body) {
215
+ chunks.push(chunk);
216
+ }
217
+ const buffer = Buffer.concat(chunks);
218
+
219
+ const contentType = response.ContentType || "application/octet-stream";
220
+ const fileName = resolvedPath.split("/").pop() || resolvedPath;
221
+
222
+ const blob = new Blob([buffer], { type: contentType });
223
+ return new File([blob], fileName, { type: contentType });
224
+ } catch (error: unknown) {
225
+ const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };
226
+ if (s3Error.name === "NoSuchKey" || s3Error.$metadata?.httpStatusCode === 404) {
227
+ return null;
228
+ }
229
+ throw error;
230
+ }
231
+ }
232
+
233
+ async deleteObject(key: string, bucket?: string): Promise<void> {
234
+ // Handle s3:// and gs:// URLs
235
+ let resolvedPath = key;
236
+ let resolvedBucket = this.getBucket(bucket);
237
+
238
+ const match = key.match(/^(s3|gs):\/\//);
239
+ if (match) {
240
+ const protocolLength = match[0].length;
241
+ const withoutProtocol = key.substring(protocolLength);
242
+ const firstSlash = withoutProtocol.indexOf("/");
243
+ if (firstSlash > 0) {
244
+ resolvedBucket = withoutProtocol.substring(0, firstSlash);
245
+ resolvedPath = withoutProtocol.substring(firstSlash + 1);
246
+ }
247
+ }
248
+
249
+ const command = new DeleteObjectCommand({
250
+ Bucket: resolvedBucket,
251
+ Key: resolvedPath
252
+ });
253
+
254
+ await this.client.send(command);
255
+ }
256
+
257
+ async listObjects(prefix: string, options?: {
258
+ bucket?: string;
259
+ maxResults?: number;
260
+ pageToken?: string;
261
+ }): Promise<StorageListResult> {
262
+ const resolvedBucket = this.getBucket(options?.bucket);
263
+
264
+ const command = new ListObjectsV2Command({
265
+ Bucket: resolvedBucket,
266
+ Prefix: prefix || undefined,
267
+ MaxKeys: options?.maxResults ?? 1000,
268
+ ContinuationToken: options?.pageToken,
269
+ Delimiter: "/" // This gives us folder-like behavior
270
+ });
271
+
272
+ const response = await this.client.send(command);
273
+
274
+ const items: StorageReference[] = (response.Contents || []).map(obj => ({
275
+ bucket: resolvedBucket,
276
+ fullPath: obj.Key || "",
277
+ name: (obj.Key || "").split("/").pop() || "",
278
+ parent: null as never,
279
+ root: null as never,
280
+ toString: () => `s3://${resolvedBucket}/${obj.Key}`
281
+ }));
282
+
283
+ const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({
284
+ bucket: resolvedBucket,
285
+ fullPath: prefix.Prefix || "",
286
+ name: (prefix.Prefix || "").replace(/\/$/, "").split("/").pop() || "",
287
+ parent: null as never,
288
+ root: null as never,
289
+ toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`
290
+ }));
291
+
292
+ return {
293
+ items,
294
+ prefixes,
295
+ nextPageToken: response.NextContinuationToken
296
+ };
297
+ }
298
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Storage module for Rebase backend
3
+ *
4
+ * Provides pluggable file storage with two built-in providers:
5
+ * - **Local filesystem** — zero config, great for dev and single-server deployments.
6
+ * - **S3-compatible** — works with AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,
7
+ * Backblaze B2, DigitalOcean Spaces, and GCS (via S3 interop).
8
+ *
9
+ * For other providers (native GCS SDK, Azure Blob, etc.), implement the
10
+ * `StorageController` interface and pass the instance directly to the `storage` config.
11
+ */
12
+
13
+ export * from "./types";
14
+ export { LocalStorageController } from "./LocalStorageController";
15
+ export { S3StorageController } from "./S3StorageController";
16
+ export { createStorageRoutes } from "./routes";
17
+ export type { StorageRoutesConfig } from "./routes";
18
+ export * from "./storage-registry";
19
+
20
+ import { BackendStorageConfig, StorageController } from "./types";
21
+ import { LocalStorageController } from "./LocalStorageController";
22
+ import { S3StorageController } from "./S3StorageController";
23
+
24
+ /**
25
+ * Create a storage controller from a config object.
26
+ *
27
+ * For custom providers, implement `StorageController` directly instead
28
+ * of going through this factory.
29
+ */
30
+ export function createStorageController(config: BackendStorageConfig): StorageController {
31
+ switch (config.type) {
32
+ case "local":
33
+ return new LocalStorageController(config);
34
+ case "s3":
35
+ return new S3StorageController(config);
36
+ default:
37
+ throw new Error(
38
+ `Unknown storage type: ${(config as Record<string, unknown>).type}. ` +
39
+ "Built-in types: local, s3. " +
40
+ "For other providers, implement the StorageController interface directly."
41
+ );
42
+ }
43
+ }