@rebasepro/server-core 0.5.0 → 0.6.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 (279) hide show
  1. package/dist/{server-core/src/api → api}/errors.d.ts +5 -2
  2. package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
  3. package/dist/{server-core/src/api → api}/types.d.ts +2 -0
  4. package/dist/auth/admin-user-ops.d.ts +79 -0
  5. package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
  6. package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
  7. package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
  8. package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
  9. package/dist/auth/mfa-routes.d.ts +6 -0
  10. package/dist/auth/reset-password-admin.d.ts +29 -0
  11. package/dist/auth/session-routes.d.ts +25 -0
  12. package/dist/backend-CIxN4FVm.js +15 -0
  13. package/dist/backend-CIxN4FVm.js.map +1 -0
  14. package/dist/chunk-Dze3rakg.js +42 -0
  15. package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
  16. package/dist/dist-CZKP-Xz4.js +832 -0
  17. package/dist/dist-CZKP-Xz4.js.map +1 -0
  18. package/dist/env.d.ts +102 -0
  19. package/dist/from-VbwD7xRf.js +3849 -0
  20. package/dist/from-VbwD7xRf.js.map +1 -0
  21. package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
  22. package/dist/index.es.js +60867 -50462
  23. package/dist/index.es.js.map +1 -1
  24. package/dist/index.umd.js +73151 -51298
  25. package/dist/index.umd.js.map +1 -1
  26. package/dist/init/docs.d.ts +4 -0
  27. package/dist/init/health.d.ts +2 -0
  28. package/dist/init/middlewares.d.ts +10 -0
  29. package/dist/init/shutdown.d.ts +11 -0
  30. package/dist/init/storage.d.ts +5 -0
  31. package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
  32. package/dist/jwt-DHcQRGC3.js +4168 -0
  33. package/dist/jwt-DHcQRGC3.js.map +1 -0
  34. package/dist/logger-BYU66ENZ.js +94 -0
  35. package/dist/logger-BYU66ENZ.js.map +1 -0
  36. package/dist/ms-BeBSuOXN.js +125 -0
  37. package/dist/ms-BeBSuOXN.js.map +1 -0
  38. package/dist/multipart-parser-CedBDOeC.js +299 -0
  39. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  40. package/dist/src-COaj0G3P.js +1182 -0
  41. package/dist/src-COaj0G3P.js.map +1 -0
  42. package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
  43. package/dist/utils/request-id.d.ts +4 -0
  44. package/package.json +29 -29
  45. package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
  46. package/src/api/errors.ts +75 -19
  47. package/src/api/graphql/graphql-schema-generator.ts +15 -10
  48. package/src/api/logs-routes.ts +5 -2
  49. package/src/api/rest/api-generator-count.test.ts +21 -10
  50. package/src/api/rest/api-generator.ts +83 -24
  51. package/src/api/rest/query-parser.ts +18 -12
  52. package/src/api/server.ts +12 -1
  53. package/src/api/types.ts +2 -0
  54. package/src/auth/adapter-middleware.ts +17 -10
  55. package/src/auth/admin-user-ops.ts +236 -0
  56. package/src/auth/api-keys/api-key-middleware.ts +15 -9
  57. package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
  58. package/src/auth/api-keys/api-key-routes.ts +3 -3
  59. package/src/auth/api-keys/api-key-store.ts +14 -8
  60. package/src/auth/api-keys/index.ts +2 -2
  61. package/src/auth/apple-oauth.ts +4 -3
  62. package/src/auth/auth-hooks.ts +50 -10
  63. package/src/auth/bitbucket-oauth.ts +5 -4
  64. package/src/auth/builtin-auth-adapter.ts +60 -39
  65. package/src/auth/custom-auth-adapter.ts +4 -5
  66. package/src/auth/discord-oauth.ts +5 -4
  67. package/src/auth/facebook-oauth.ts +5 -4
  68. package/src/auth/github-oauth.ts +6 -5
  69. package/src/auth/gitlab-oauth.ts +5 -4
  70. package/src/auth/google-oauth.ts +2 -1
  71. package/src/auth/index.ts +6 -2
  72. package/src/auth/jwt.ts +5 -4
  73. package/src/auth/linkedin-oauth.ts +4 -3
  74. package/src/auth/mfa-routes.ts +299 -0
  75. package/src/auth/mfa.ts +2 -1
  76. package/src/auth/microsoft-oauth.ts +5 -4
  77. package/src/auth/middleware.ts +5 -4
  78. package/src/auth/rate-limiter.ts +1 -1
  79. package/src/auth/reset-password-admin.ts +144 -0
  80. package/src/auth/rls-scope.ts +1 -1
  81. package/src/auth/routes.ts +52 -596
  82. package/src/auth/session-routes.ts +341 -0
  83. package/src/auth/slack-oauth.ts +6 -5
  84. package/src/auth/spotify-oauth.ts +5 -4
  85. package/src/auth/twitter-oauth.ts +4 -3
  86. package/src/collections/loader.ts +5 -4
  87. package/src/cron/cron-scheduler.test.ts +23 -11
  88. package/src/cron/cron-scheduler.ts +14 -8
  89. package/src/cron/cron-store.ts +1 -1
  90. package/src/email/smtp-email-service.ts +3 -2
  91. package/src/env.ts +11 -10
  92. package/src/index.ts +1 -0
  93. package/src/init/docs.ts +47 -0
  94. package/src/init/health.ts +37 -0
  95. package/src/init/middlewares.ts +61 -0
  96. package/src/init/shutdown.ts +56 -0
  97. package/src/init/storage.ts +57 -0
  98. package/src/init.ts +85 -279
  99. package/src/serve-spa.ts +12 -5
  100. package/src/services/driver-registry.ts +4 -3
  101. package/src/storage/S3StorageController.ts +1 -2
  102. package/src/storage/image-transform.ts +25 -10
  103. package/src/storage/routes.ts +30 -11
  104. package/src/storage/storage-registry.ts +4 -3
  105. package/src/storage/tus-handler.ts +12 -12
  106. package/src/utils/request-id.ts +40 -0
  107. package/src/utils/request-logger.ts +6 -0
  108. package/test/api-generator.test.ts +90 -2
  109. package/test/api-key-permission-guard.test.ts +24 -12
  110. package/test/auth-routes.test.ts +5 -3
  111. package/test/backend-hooks-data.test.ts +99 -30
  112. package/test/custom-auth-adapter.test.ts +16 -15
  113. package/test/email-templates.test.ts +10 -5
  114. package/test/env.test.ts +10 -10
  115. package/test/function-loader.test.ts +7 -4
  116. package/test/graphql-schema-generator.test.ts +554 -0
  117. package/test/jwt-security.test.ts +1 -1
  118. package/test/query-parser.test.ts +0 -1
  119. package/test/reset-password-admin.test.ts +113 -0
  120. package/test/singleton.test.ts +5 -5
  121. package/test/smtp-email-service.test.ts +21 -21
  122. package/test/webhook-service.test.ts +22 -11
  123. package/tsconfig.json +2 -0
  124. package/tsconfig.prod.json +3 -0
  125. package/vite.config.ts +5 -5
  126. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  127. package/dist/common/src/collections/default-collections.d.ts +0 -9
  128. package/dist/common/src/collections/index.d.ts +0 -2
  129. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  130. package/dist/common/src/data/query_builder.d.ts +0 -55
  131. package/dist/common/src/index.d.ts +0 -4
  132. package/dist/common/src/util/builders.d.ts +0 -57
  133. package/dist/common/src/util/callbacks.d.ts +0 -6
  134. package/dist/common/src/util/collections.d.ts +0 -11
  135. package/dist/common/src/util/common.d.ts +0 -2
  136. package/dist/common/src/util/conditions.d.ts +0 -26
  137. package/dist/common/src/util/entities.d.ts +0 -58
  138. package/dist/common/src/util/enums.d.ts +0 -3
  139. package/dist/common/src/util/index.d.ts +0 -16
  140. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  141. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  142. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  143. package/dist/common/src/util/paths.d.ts +0 -14
  144. package/dist/common/src/util/permissions.d.ts +0 -14
  145. package/dist/common/src/util/references.d.ts +0 -2
  146. package/dist/common/src/util/relations.d.ts +0 -22
  147. package/dist/common/src/util/resolutions.d.ts +0 -72
  148. package/dist/common/src/util/storage.d.ts +0 -24
  149. package/dist/index-Cr1D21av.js +0 -49
  150. package/dist/index-Cr1D21av.js.map +0 -1
  151. package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
  152. package/dist/server-core/src/env.d.ts +0 -137
  153. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  154. package/dist/types/src/controllers/auth.d.ts +0 -104
  155. package/dist/types/src/controllers/client.d.ts +0 -168
  156. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  157. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  158. package/dist/types/src/controllers/data.d.ts +0 -207
  159. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  160. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  161. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  162. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  163. package/dist/types/src/controllers/email.d.ts +0 -36
  164. package/dist/types/src/controllers/index.d.ts +0 -18
  165. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  166. package/dist/types/src/controllers/navigation.d.ts +0 -225
  167. package/dist/types/src/controllers/registry.d.ts +0 -63
  168. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  169. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  170. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  171. package/dist/types/src/controllers/storage.d.ts +0 -171
  172. package/dist/types/src/index.d.ts +0 -4
  173. package/dist/types/src/rebase_context.d.ts +0 -122
  174. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  175. package/dist/types/src/types/backend.d.ts +0 -571
  176. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  177. package/dist/types/src/types/builders.d.ts +0 -15
  178. package/dist/types/src/types/chips.d.ts +0 -5
  179. package/dist/types/src/types/collections.d.ts +0 -961
  180. package/dist/types/src/types/component_ref.d.ts +0 -47
  181. package/dist/types/src/types/cron.d.ts +0 -102
  182. package/dist/types/src/types/data_source.d.ts +0 -64
  183. package/dist/types/src/types/database_adapter.d.ts +0 -94
  184. package/dist/types/src/types/entities.d.ts +0 -145
  185. package/dist/types/src/types/entity_actions.d.ts +0 -104
  186. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  187. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  188. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  189. package/dist/types/src/types/entity_views.d.ts +0 -87
  190. package/dist/types/src/types/export_import.d.ts +0 -21
  191. package/dist/types/src/types/formex.d.ts +0 -40
  192. package/dist/types/src/types/index.d.ts +0 -28
  193. package/dist/types/src/types/locales.d.ts +0 -4
  194. package/dist/types/src/types/modify_collections.d.ts +0 -5
  195. package/dist/types/src/types/plugins.d.ts +0 -282
  196. package/dist/types/src/types/properties.d.ts +0 -1173
  197. package/dist/types/src/types/property_config.d.ts +0 -74
  198. package/dist/types/src/types/relations.d.ts +0 -336
  199. package/dist/types/src/types/slots.d.ts +0 -262
  200. package/dist/types/src/types/translations.d.ts +0 -900
  201. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  202. package/dist/types/src/types/websockets.d.ts +0 -78
  203. package/dist/types/src/users/index.d.ts +0 -1
  204. package/dist/types/src/users/user.d.ts +0 -50
  205. package/src/auth/admin-routes.ts +0 -534
  206. package/test/admin-routes.test.ts +0 -544
  207. package/test/backend-hooks-admin.test.ts +0 -388
  208. /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
  209. /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
  210. /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
  211. /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
  212. /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
  213. /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
  214. /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
  215. /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
  216. /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
  217. /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
  218. /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
  219. /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
  220. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
  221. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
  222. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
  223. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
  224. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
  225. /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
  226. /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
  227. /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
  228. /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
  229. /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
  230. /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
  231. /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
  232. /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
  233. /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
  234. /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
  235. /package/dist/{server-core/src/auth → auth}/jwt.d.ts +0 -0
  236. /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
  237. /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
  238. /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
  239. /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
  240. /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
  241. /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
  242. /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
  243. /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
  244. /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
  245. /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
  246. /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
  247. /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
  248. /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
  249. /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
  250. /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
  251. /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
  252. /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
  253. /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
  254. /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
  255. /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
  256. /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
  257. /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
  258. /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
  259. /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
  260. /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
  261. /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
  262. /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
  263. /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
  264. /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
  265. /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
  266. /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
  267. /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
  268. /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
  269. /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
  270. /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
  271. /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
  272. /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
  273. /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
  274. /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
  275. /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
  276. /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
  277. /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
  278. /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
  279. /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
@@ -13,7 +13,6 @@ let sharpFactory: ((input: Buffer | Uint8Array) => any) | undefined;
13
13
  async function getSharp(): Promise<(input: Buffer | Uint8Array) => any> {
14
14
  if (!sharpFactory) {
15
15
  try {
16
- // @ts-ignore - sharp is an optional dependency
17
16
  const mod = await import("sharp");
18
17
  sharpFactory = mod.default;
19
18
  } catch (err) {
@@ -95,7 +94,7 @@ const FORMAT_CONTENT_TYPES: Record<string, string> = {
95
94
  webp: "image/webp",
96
95
  avif: "image/avif",
97
96
  jpeg: "image/jpeg",
98
- png: "image/png",
97
+ png: "image/png"
99
98
  };
100
99
 
101
100
  /** Check whether a content type is a transformable image. */
@@ -122,7 +121,7 @@ export async function transformImage(
122
121
  width: options.width,
123
122
  height: options.height,
124
123
  fit: options.fit || "cover",
125
- withoutEnlargement: true,
124
+ withoutEnlargement: true
126
125
  });
127
126
  }
128
127
 
@@ -145,7 +144,8 @@ export async function transformImage(
145
144
  }
146
145
 
147
146
  const data = await pipeline.toBuffer();
148
- return { data, contentType: FORMAT_CONTENT_TYPES[format] };
147
+ return { data,
148
+ contentType: FORMAT_CONTENT_TYPES[format] };
149
149
  }
150
150
 
151
151
  // ---------------------------------------------------------------------------
@@ -168,10 +168,13 @@ export class TransformCache {
168
168
  private cache = new Map<string, CacheEntry>();
169
169
  private readonly maxEntries: number;
170
170
  private readonly maxAgeMs: number;
171
+ private readonly maxTotalBytes: number;
172
+ private totalBytes = 0;
171
173
 
172
- constructor(maxEntries = 500, maxAgeMs = 3_600_000) {
174
+ constructor(maxEntries = 500, maxAgeMs = 3_600_000, maxTotalBytes = 256 * 1024 * 1024) {
173
175
  this.maxEntries = maxEntries;
174
176
  this.maxAgeMs = maxAgeMs;
177
+ this.maxTotalBytes = maxTotalBytes;
175
178
  }
176
179
 
177
180
  /** Build a deterministic cache key from file key + transform options. */
@@ -183,21 +186,33 @@ export class TransformCache {
183
186
  const entry = this.cache.get(cacheKey);
184
187
  if (!entry) return null;
185
188
  if (Date.now() - entry.timestamp > this.maxAgeMs) {
189
+ this.totalBytes -= entry.data.length;
186
190
  this.cache.delete(cacheKey);
187
191
  return null;
188
192
  }
189
193
  // Move to end (most recently used)
190
194
  this.cache.delete(cacheKey);
191
195
  this.cache.set(cacheKey, entry);
192
- return { data: entry.data, contentType: entry.contentType };
196
+ return { data: entry.data,
197
+ contentType: entry.contentType };
193
198
  }
194
199
 
195
200
  set(cacheKey: string, data: Buffer, contentType: string): void {
196
- // Evict oldest if at capacity
197
- if (this.cache.size >= this.maxEntries) {
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
+ ) {
198
206
  const oldest = this.cache.keys().next().value;
199
- if (oldest !== undefined) this.cache.delete(oldest);
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
+ }
200
212
  }
201
- this.cache.set(cacheKey, { data, contentType, timestamp: Date.now() });
213
+ this.totalBytes += data.length;
214
+ this.cache.set(cacheKey, { data,
215
+ contentType,
216
+ timestamp: Date.now() });
202
217
  }
203
218
  }
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import * as fs from "fs";
6
+ import fs from "node:fs";
7
+ import fsp from "node:fs/promises";
7
8
  import { StorageController } from "./types";
8
9
  import { LocalStorageController } from "./LocalStorageController";
9
10
  import { requireAuth as jwtRequireAuth, optionalAuth } from "../auth/middleware";
@@ -48,6 +49,23 @@ export function extractWildcardPath(c: { req: { path: string; routePath: string
48
49
  return fullPath.substring(idx + prefix.length + 1);
49
50
  }
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
+
51
69
  /**
52
70
  * Create storage REST API routes
53
71
  */
@@ -99,7 +117,7 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
99
117
  const key = typeof body["key"] === "string" ? body["key"] : "";
100
118
  const bucket = typeof body["bucket"] === "string" ? body["bucket"] : undefined;
101
119
 
102
- const finalKey = key || uploadedFile.name || "unnamed";
120
+ const finalKey = sanitizeStorageKey(key || uploadedFile.name || "unnamed");
103
121
 
104
122
  // Extract custom metadata from request body
105
123
  const metadata: Record<string, unknown> = {};
@@ -145,23 +163,24 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
145
163
  const absolutePath = localController.getAbsolutePath(resolvedPath, bucket);
146
164
 
147
165
  // Check if file exists
148
- if (!fs.existsSync(absolutePath)) {
166
+ try {
167
+ await fsp.access(absolutePath);
168
+ } catch {
149
169
  throw ApiError.notFound("File not found");
150
170
  }
151
171
 
152
172
  // Get content type from metadata or infer from extension
153
173
  let contentType = "application/octet-stream";
154
174
  const metadataPath = `${absolutePath}.metadata.json`;
155
- if (fs.existsSync(metadataPath)) {
156
- try {
157
- const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
158
- contentType = metadata.contentType || contentType;
159
- } catch {
160
- // Ignore metadata errors
161
- }
175
+ try {
176
+ const metadataRaw = await fsp.readFile(metadataPath, "utf-8");
177
+ const metadata = JSON.parse(metadataRaw);
178
+ contentType = metadata.contentType || contentType;
179
+ } catch {
180
+ // Ignore metadata errors (file may not exist)
162
181
  }
163
182
 
164
- const fileContent = fs.readFileSync(absolutePath);
183
+ const fileContent = await fsp.readFile(absolutePath);
165
184
 
166
185
  // Apply image transforms if requested and the file is a transformable image
167
186
  if (transformOpts && isTransformableImage(contentType)) {
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { StorageController } from "./types";
15
+ import { logger } from "../utils/logger";
15
16
 
16
17
  /**
17
18
  * The default storage identifier used when:
@@ -98,7 +99,7 @@ export class DefaultStorageRegistry implements StorageRegistry {
98
99
  // If no explicit "(default)", use the first one as default
99
100
  const firstId = Object.keys(input).find(k => isStorageController(input[k]));
100
101
  if (firstId) {
101
- console.warn(
102
+ logger.warn(
102
103
  `[StorageRegistry] No "${DEFAULT_STORAGE_ID}" storage provided. ` +
103
104
  `Using "${firstId}" as the default.`
104
105
  );
@@ -112,7 +113,7 @@ export class DefaultStorageRegistry implements StorageRegistry {
112
113
 
113
114
  register(id: string, controller: StorageController): void {
114
115
  if (this.controllers.has(id)) {
115
- console.warn(`[StorageRegistry] Overwriting storage with id "${id}"`);
116
+ logger.warn(`[StorageRegistry] Overwriting storage with id "${id}"`);
116
117
  }
117
118
  this.controllers.set(id, controller);
118
119
  }
@@ -148,7 +149,7 @@ export class DefaultStorageRegistry implements StorageRegistry {
148
149
  }
149
150
 
150
151
  // Fallback to default with warning
151
- console.warn(
152
+ logger.warn(
152
153
  `[StorageRegistry] Storage "${id}" not found, falling back to "${DEFAULT_STORAGE_ID}"`
153
154
  );
154
155
  return this.getDefault();
@@ -57,7 +57,7 @@ export class TusHandler {
57
57
 
58
58
  constructor(
59
59
  storageBaseDir: string,
60
- private storageController?: StorageController,
60
+ private storageController?: StorageController
61
61
  ) {
62
62
  this.tusDir = join(storageBaseDir, ".tus-uploads");
63
63
  }
@@ -126,8 +126,8 @@ export class TusHandler {
126
126
  "Tus-Resumable": "1.0.0",
127
127
  "Tus-Version": "1.0.0",
128
128
  "Tus-Extension": "creation,termination",
129
- "Tus-Max-Size": String(MAX_UPLOAD_SIZE),
130
- },
129
+ "Tus-Max-Size": String(MAX_UPLOAD_SIZE)
130
+ }
131
131
  });
132
132
  }
133
133
 
@@ -164,7 +164,7 @@ export class TusHandler {
164
164
  filePath,
165
165
  bucket: metadata.bucket || undefined,
166
166
  key: metadata.key || metadata.filename || undefined,
167
- completed: false,
167
+ completed: false
168
168
  };
169
169
  this.uploads.set(id, upload);
170
170
 
@@ -177,8 +177,8 @@ export class TusHandler {
177
177
  headers: {
178
178
  Location: location,
179
179
  "Tus-Resumable": "1.0.0",
180
- "Upload-Offset": "0",
181
- },
180
+ "Upload-Offset": "0"
181
+ }
182
182
  });
183
183
  }
184
184
 
@@ -195,8 +195,8 @@ export class TusHandler {
195
195
  "Tus-Resumable": "1.0.0",
196
196
  "Upload-Offset": String(upload.offset),
197
197
  "Upload-Length": String(upload.size),
198
- "Cache-Control": "no-store",
199
- },
198
+ "Cache-Control": "no-store"
199
+ }
200
200
  });
201
201
  }
202
202
 
@@ -252,8 +252,8 @@ export class TusHandler {
252
252
  status: 204,
253
253
  headers: {
254
254
  "Tus-Resumable": "1.0.0",
255
- "Upload-Offset": String(upload.offset),
256
- },
255
+ "Upload-Offset": String(upload.offset)
256
+ }
257
257
  });
258
258
  }
259
259
 
@@ -269,7 +269,7 @@ export class TusHandler {
269
269
 
270
270
  return new Response(null, {
271
271
  status: 204,
272
- headers: { "Tus-Resumable": "1.0.0" },
272
+ headers: { "Tus-Resumable": "1.0.0" }
273
273
  });
274
274
  }
275
275
 
@@ -300,7 +300,7 @@ export class TusHandler {
300
300
  await this.storageController.putObject({
301
301
  file,
302
302
  key: fileName,
303
- bucket: upload.bucket,
303
+ bucket: upload.bucket
304
304
  });
305
305
 
306
306
  // Clean up temp file
@@ -0,0 +1,40 @@
1
+ /**
2
+ * X-Request-ID Middleware for Hono.
3
+ *
4
+ * Generates a unique request identifier (UUID v4) for every inbound
5
+ * request, or propagates an existing `X-Request-ID` header from the
6
+ * caller. The ID is:
7
+ *
8
+ * 1. Stored in the Hono context (`c.get("requestId")`)
9
+ * 2. Echoed back on the response as `X-Request-ID`
10
+ *
11
+ * Downstream middleware and handlers (request logger, error handler,
12
+ * etc.) read the ID from context to include it in log entries and
13
+ * error responses, enabling end-to-end request tracing across services.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { requestId } from "@rebasepro/server-core";
18
+ * app.use("/*", requestId());
19
+ * ```
20
+ */
21
+ import { randomUUID } from "node:crypto";
22
+ import type { MiddlewareHandler } from "hono";
23
+ import type { HonoEnv } from "../api/types";
24
+
25
+ export const REQUEST_ID_HEADER = "X-Request-ID";
26
+
27
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
+
29
+ export function requestId(): MiddlewareHandler<HonoEnv> {
30
+ return async (c, next) => {
31
+ const incoming = c.req.header(REQUEST_ID_HEADER);
32
+ const id = incoming && UUID_RE.test(incoming) ? incoming : randomUUID();
33
+
34
+ c.set("requestId", id);
35
+
36
+ await next();
37
+
38
+ c.header(REQUEST_ID_HEADER, id);
39
+ };
40
+ }
@@ -45,6 +45,12 @@ export function requestLogger(options?: RequestLoggerOptions): MiddlewareHandler
45
45
  latencyMs
46
46
  };
47
47
 
48
+ // Include request correlation ID if available
49
+ const reqId = c.get("requestId");
50
+ if (reqId) {
51
+ data.requestId = reqId;
52
+ }
53
+
48
54
  if (contentLength) {
49
55
  data.contentLength = parseInt(contentLength, 10);
50
56
  }
@@ -46,6 +46,12 @@ describe("RestApiGenerator", () => {
46
46
  function createApp() {
47
47
  const app = new Hono();
48
48
  app.onError(errorHandler);
49
+ // RestApiGenerator.getScopedDriver reads from c.get("driver") which is
50
+ // normally set by auth middleware. Replicate that here for tests.
51
+ app.use("/api/*", async (c, next) => {
52
+ c.set("driver", mockDriver);
53
+ await next();
54
+ });
49
55
  const generator = new RestApiGenerator(mockCollections, mockDriver);
50
56
  app.route("/api", generator.generateRoutes());
51
57
  return app;
@@ -56,9 +62,14 @@ describe("RestApiGenerator", () => {
56
62
  * so we can reliably test route matching without nesting interference.
57
63
  */
58
64
  function createFlatApp() {
59
- const generator = new RestApiGenerator(mockCollections, mockDriver);
60
- const app = generator.generateRoutes();
65
+ const app = new Hono();
61
66
  app.onError(errorHandler);
67
+ app.use("/*", async (c, next) => {
68
+ c.set("driver", mockDriver);
69
+ await next();
70
+ });
71
+ const generator = new RestApiGenerator(mockCollections, mockDriver);
72
+ app.route("/", generator.generateRoutes());
62
73
  return app;
63
74
  }
64
75
 
@@ -497,4 +508,81 @@ values: { title: "UUID" } } as any
497
508
  expect(mockDriver.deleteEntity).not.toHaveBeenCalled();
498
509
  });
499
510
  });
511
+
512
+ describe("Auth Hook Overrides (onAdminCreateUser via AuthAdapter)", () => {
513
+
514
+ it("calls adapter.prepareUserCreation and saves user with returned values", async () => {
515
+ const mockPrepare = jest.fn().mockResolvedValue({
516
+ values: { email: "custom@example.com",
517
+ name: "Custom Name",
518
+ passwordHash: "custom-hash" },
519
+ clearPassword: "custom-temp-password",
520
+ hookHandledEmail: true,
521
+ invitationSent: true
522
+ });
523
+
524
+ const mockAuthAdapter = {
525
+ id: "test-adapter",
526
+ verifyRequest: jest.fn(),
527
+ getCapabilities: jest.fn(),
528
+ prepareUserCreation: mockPrepare
529
+ };
530
+
531
+ const app = new Hono();
532
+ app.onError(errorHandler);
533
+ app.use("/api/*", async (c, next) => {
534
+ c.set("driver", mockDriver);
535
+ await next();
536
+ });
537
+
538
+ const authCollections = [
539
+ {
540
+ slug: "users",
541
+ name: "Users",
542
+ singularName: "User",
543
+ auth: true,
544
+ properties: {}
545
+ } as any
546
+ ];
547
+
548
+ const generator = new RestApiGenerator(authCollections, mockDriver, undefined, mockAuthAdapter as any);
549
+ app.route("/api", generator.generateRoutes());
550
+
551
+ mockDriver.saveEntity.mockResolvedValue({
552
+ id: "custom-id",
553
+ path: "users",
554
+ values: { email: "custom@example.com",
555
+ name: "Custom Name",
556
+ passwordHash: "custom-hash" }
557
+ } as any);
558
+
559
+ const res = await app.request("/api/users", {
560
+ method: "POST",
561
+ headers: { "Content-Type": "application/json" },
562
+ body: JSON.stringify({ email: "original@example.com",
563
+ name: "Original Name" })
564
+ });
565
+
566
+ expect(res.status).toBe(201);
567
+ const body = await res.json() as any;
568
+ expect(body.email).toBe("custom@example.com");
569
+ expect(body.invitationSent).toBe(true);
570
+ expect(body.temporaryPassword).toBe("custom-temp-password");
571
+
572
+ expect(mockPrepare).toHaveBeenCalledWith(
573
+ expect.objectContaining({ email: "original@example.com",
574
+ name: "Original Name" }),
575
+ undefined // auth: true → collectionAuthConfig is undefined
576
+ );
577
+ expect(mockDriver.saveEntity).toHaveBeenCalledWith(
578
+ expect.objectContaining({
579
+ path: "users",
580
+ values: { email: "custom@example.com",
581
+ name: "Custom Name",
582
+ passwordHash: "custom-hash" },
583
+ status: "new"
584
+ })
585
+ );
586
+ });
587
+ });
500
588
  });
@@ -51,7 +51,8 @@ describe("httpMethodToOperation", () => {
51
51
  describe("isOperationAllowed", () => {
52
52
  it("allows operation with exact collection match", () => {
53
53
  const permissions: ApiKeyPermission[] = [
54
- { collection: "users", operations: ["read", "write"] }
54
+ { collection: "users",
55
+ operations: ["read", "write"] }
55
56
  ];
56
57
  expect(isOperationAllowed(permissions, "users", "read")).toBe(true);
57
58
  expect(isOperationAllowed(permissions, "users", "write")).toBe(true);
@@ -59,7 +60,8 @@ describe("isOperationAllowed", () => {
59
60
 
60
61
  it("denies operation not in the operations list", () => {
61
62
  const permissions: ApiKeyPermission[] = [
62
- { collection: "users", operations: ["read"] }
63
+ { collection: "users",
64
+ operations: ["read"] }
63
65
  ];
64
66
  expect(isOperationAllowed(permissions, "users", "write")).toBe(false);
65
67
  expect(isOperationAllowed(permissions, "users", "delete")).toBe(false);
@@ -67,14 +69,16 @@ describe("isOperationAllowed", () => {
67
69
 
68
70
  it("denies operation for non-matching collection", () => {
69
71
  const permissions: ApiKeyPermission[] = [
70
- { collection: "users", operations: ["read", "write", "delete"] }
72
+ { collection: "users",
73
+ operations: ["read", "write", "delete"] }
71
74
  ];
72
75
  expect(isOperationAllowed(permissions, "posts", "read")).toBe(false);
73
76
  });
74
77
 
75
78
  it("allows wildcard collection '*' to match any collection", () => {
76
79
  const permissions: ApiKeyPermission[] = [
77
- { collection: "*", operations: ["read"] }
80
+ { collection: "*",
81
+ operations: ["read"] }
78
82
  ];
79
83
  expect(isOperationAllowed(permissions, "users", "read")).toBe(true);
80
84
  expect(isOperationAllowed(permissions, "posts", "read")).toBe(true);
@@ -83,7 +87,8 @@ describe("isOperationAllowed", () => {
83
87
 
84
88
  it("wildcard collection still checks operations", () => {
85
89
  const permissions: ApiKeyPermission[] = [
86
- { collection: "*", operations: ["read"] }
90
+ { collection: "*",
91
+ operations: ["read"] }
87
92
  ];
88
93
  expect(isOperationAllowed(permissions, "users", "write")).toBe(false);
89
94
  expect(isOperationAllowed(permissions, "users", "delete")).toBe(false);
@@ -95,9 +100,12 @@ describe("isOperationAllowed", () => {
95
100
 
96
101
  it("checks all permission entries and grants if any match", () => {
97
102
  const permissions: ApiKeyPermission[] = [
98
- { collection: "users", operations: ["read"] },
99
- { collection: "posts", operations: ["write"] },
100
- { collection: "comments", operations: ["read", "write", "delete"] }
103
+ { collection: "users",
104
+ operations: ["read"] },
105
+ { collection: "posts",
106
+ operations: ["write"] },
107
+ { collection: "comments",
108
+ operations: ["read", "write", "delete"] }
101
109
  ];
102
110
  expect(isOperationAllowed(permissions, "users", "read")).toBe(true);
103
111
  expect(isOperationAllowed(permissions, "posts", "write")).toBe(true);
@@ -108,8 +116,10 @@ describe("isOperationAllowed", () => {
108
116
 
109
117
  it("handles overlapping permissions correctly", () => {
110
118
  const permissions: ApiKeyPermission[] = [
111
- { collection: "users", operations: ["read"] },
112
- { collection: "users", operations: ["write"] }
119
+ { collection: "users",
120
+ operations: ["read"] },
121
+ { collection: "users",
122
+ operations: ["write"] }
113
123
  ];
114
124
  expect(isOperationAllowed(permissions, "users", "read")).toBe(true);
115
125
  expect(isOperationAllowed(permissions, "users", "write")).toBe(true);
@@ -118,8 +128,10 @@ describe("isOperationAllowed", () => {
118
128
 
119
129
  it("handles mixed wildcard and specific permissions", () => {
120
130
  const permissions: ApiKeyPermission[] = [
121
- { collection: "*", operations: ["read"] },
122
- { collection: "users", operations: ["write", "delete"] }
131
+ { collection: "*",
132
+ operations: ["read"] },
133
+ { collection: "users",
134
+ operations: ["write", "delete"] }
123
135
  ];
124
136
  // Wildcard read for everything
125
137
  expect(isOperationAllowed(permissions, "posts", "read")).toBe(true);
@@ -193,7 +193,7 @@ accessExpiresIn: "1h" });
193
193
 
194
194
  describe("Configuration Security", () => {
195
195
  it("throws an error when defaultRole is set to 'admin'", () => {
196
- expect(() => createApp({ defaultRole: "admin" })).toThrowError(/CRITICAL SECURITY ERROR/);
196
+ expect(() => createApp({ defaultRole: "admin" })).toThrow(/CRITICAL SECURITY ERROR/);
197
197
  });
198
198
  });
199
199
 
@@ -885,7 +885,8 @@ ipAddress: "1.2.3.4" }
885
885
  });
886
886
 
887
887
  it("returns needsSetup=false when isBootstrapCompleted returns true", async () => {
888
- const app = createApp({ allowRegistration: false, isBootstrapCompleted: async () => true });
888
+ const app = createApp({ allowRegistration: false,
889
+ isBootstrapCompleted: async () => true });
889
890
 
890
891
  const res = await app.request("/auth/config");
891
892
  expect(res.status).toBe(200);
@@ -1029,7 +1030,8 @@ withEmail: false });
1029
1030
  // this test documents the current behavior — it would assign admin.
1030
1031
  // The fix is in the config validation at startup (outside routes).
1031
1032
  // Here we verify the defaultRole IS what's passed, and the auto-escalation is gone.
1032
- const app = createApp({ allowRegistration: true, defaultRole: "viewer" });
1033
+ const app = createApp({ allowRegistration: true,
1034
+ defaultRole: "viewer" });
1033
1035
 
1034
1036
  await app.request("/auth/register", json({
1035
1037
  email: "new@test.com",