@rebasepro/server-core 0.7.0 → 0.8.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 (155) hide show
  1. package/build-errors.txt +52 -0
  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/graphql/graphql-schema-generator.d.ts +44 -0
  6. package/dist/api/graphql/index.d.ts +1 -0
  7. package/dist/api/index.d.ts +9 -0
  8. package/dist/api/logs-routes.d.ts +37 -0
  9. package/dist/api/openapi-generator.d.ts +16 -0
  10. package/dist/api/rest/api-generator.d.ts +76 -0
  11. package/dist/api/rest/index.d.ts +1 -0
  12. package/dist/api/rest/query-parser.d.ts +8 -0
  13. package/dist/api/schema-editor-routes.d.ts +3 -0
  14. package/dist/api/server.d.ts +40 -0
  15. package/dist/api/types.d.ts +104 -0
  16. package/dist/auth/adapter-middleware.d.ts +43 -0
  17. package/dist/auth/admin-roles-route.d.ts +18 -0
  18. package/dist/auth/admin-user-ops.d.ts +79 -0
  19. package/dist/auth/admin-users-route.d.ts +28 -0
  20. package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
  21. package/dist/auth/api-keys/api-key-permission-guard.d.ts +32 -0
  22. package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
  23. package/dist/auth/api-keys/api-key-store.d.ts +35 -0
  24. package/dist/auth/api-keys/api-key-types.d.ts +96 -0
  25. package/dist/auth/api-keys/index.d.ts +17 -0
  26. package/dist/auth/apple-oauth.d.ts +30 -0
  27. package/dist/auth/auth-hooks.d.ts +242 -0
  28. package/dist/auth/bitbucket-oauth.d.ts +11 -0
  29. package/dist/auth/builtin-auth-adapter.d.ts +51 -0
  30. package/dist/auth/crypto-utils.d.ts +16 -0
  31. package/dist/auth/custom-auth-adapter.d.ts +39 -0
  32. package/dist/auth/discord-oauth.d.ts +14 -0
  33. package/dist/auth/facebook-oauth.d.ts +14 -0
  34. package/dist/auth/github-oauth.d.ts +15 -0
  35. package/dist/auth/gitlab-oauth.d.ts +13 -0
  36. package/dist/auth/google-oauth.d.ts +47 -0
  37. package/dist/auth/index.d.ts +37 -0
  38. package/dist/auth/interfaces.d.ts +431 -0
  39. package/dist/auth/jwt.d.ts +55 -0
  40. package/dist/auth/linkedin-oauth.d.ts +18 -0
  41. package/dist/auth/magic-link-routes.d.ts +30 -0
  42. package/dist/auth/mfa-crypto.d.ts +23 -0
  43. package/dist/auth/mfa-routes.d.ts +7 -0
  44. package/dist/auth/mfa.d.ts +49 -0
  45. package/dist/auth/microsoft-oauth.d.ts +16 -0
  46. package/dist/auth/middleware.d.ts +113 -0
  47. package/dist/auth/password.d.ts +22 -0
  48. package/dist/auth/rate-limiter.d.ts +50 -0
  49. package/dist/auth/reset-password-admin.d.ts +29 -0
  50. package/dist/auth/rls-scope.d.ts +31 -0
  51. package/dist/auth/routes.d.ts +35 -0
  52. package/dist/auth/session-routes.d.ts +27 -0
  53. package/dist/auth/slack-oauth.d.ts +12 -0
  54. package/dist/auth/spotify-oauth.d.ts +12 -0
  55. package/dist/auth/twitter-oauth.d.ts +18 -0
  56. package/dist/backend-CIxN4FVm.js +15 -0
  57. package/dist/backend-CIxN4FVm.js.map +1 -0
  58. package/dist/base64-js-C_frYBkI.js +1687 -0
  59. package/dist/base64-js-C_frYBkI.js.map +1 -0
  60. package/dist/chunk-Dze3rakg.js +42 -0
  61. package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
  62. package/dist/collections/loader.d.ts +5 -0
  63. package/dist/cron/cron-loader.d.ts +17 -0
  64. package/dist/cron/cron-routes.d.ts +14 -0
  65. package/dist/cron/cron-scheduler.d.ts +106 -0
  66. package/dist/cron/cron-store.d.ts +32 -0
  67. package/dist/cron/index.d.ts +6 -0
  68. package/dist/db/interfaces.d.ts +18 -0
  69. package/dist/dist-DMO-zF6D.js +832 -0
  70. package/dist/dist-DMO-zF6D.js.map +1 -0
  71. package/dist/email/index.d.ts +6 -0
  72. package/dist/email/smtp-email-service.d.ts +25 -0
  73. package/dist/email/templates.d.ts +50 -0
  74. package/dist/email/types.d.ts +126 -0
  75. package/dist/env.d.ts +102 -0
  76. package/dist/from-VbwD7xRf.js +3849 -0
  77. package/dist/from-VbwD7xRf.js.map +1 -0
  78. package/dist/functions/define-function.d.ts +50 -0
  79. package/dist/functions/function-loader.d.ts +17 -0
  80. package/dist/functions/function-routes.d.ts +10 -0
  81. package/dist/functions/index.d.ts +5 -0
  82. package/dist/history/history-routes.d.ts +23 -0
  83. package/dist/history/index.d.ts +1 -0
  84. package/dist/index.d.ts +31 -0
  85. package/dist/index.es.js +61322 -0
  86. package/dist/index.es.js.map +1 -0
  87. package/dist/index.umd.js +103633 -0
  88. package/dist/index.umd.js.map +1 -0
  89. package/dist/init/docs.d.ts +4 -0
  90. package/dist/init/health.d.ts +2 -0
  91. package/dist/init/middlewares.d.ts +10 -0
  92. package/dist/init/shutdown.d.ts +11 -0
  93. package/dist/init/storage.d.ts +5 -0
  94. package/dist/init.d.ts +314 -0
  95. package/dist/jws-BqRRaK11.js +616 -0
  96. package/dist/jws-BqRRaK11.js.map +1 -0
  97. package/dist/jwt-BETC8a1J.js +3560 -0
  98. package/dist/jwt-BETC8a1J.js.map +1 -0
  99. package/dist/logger-BYU66ENZ.js +94 -0
  100. package/dist/logger-BYU66ENZ.js.map +1 -0
  101. package/dist/ms-BeBSuOXN.js +125 -0
  102. package/dist/ms-BeBSuOXN.js.map +1 -0
  103. package/dist/multipart-parser-CedBDOeC.js +299 -0
  104. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  105. package/dist/serve-spa.d.ts +30 -0
  106. package/dist/services/driver-registry.d.ts +78 -0
  107. package/dist/services/routed-realtime-service.d.ts +43 -0
  108. package/dist/services/webhook-service.d.ts +29 -0
  109. package/dist/singleton.d.ts +35 -0
  110. package/dist/src-CB2PIpBe.js +1182 -0
  111. package/dist/src-CB2PIpBe.js.map +1 -0
  112. package/dist/src-DjzOT1kG.js +29746 -0
  113. package/dist/src-DjzOT1kG.js.map +1 -0
  114. package/dist/storage/GCSStorageController.d.ts +43 -0
  115. package/dist/storage/LocalStorageController.d.ts +46 -0
  116. package/dist/storage/S3StorageController.d.ts +36 -0
  117. package/dist/storage/image-transform.d.ts +50 -0
  118. package/dist/storage/index.d.ts +31 -0
  119. package/dist/storage/routes.d.ts +70 -0
  120. package/dist/storage/storage-registry.d.ts +78 -0
  121. package/dist/storage/tus-handler.d.ts +53 -0
  122. package/dist/storage/types.d.ts +128 -0
  123. package/dist/types/index.d.ts +11 -0
  124. package/dist/utils/dev-port.d.ts +37 -0
  125. package/dist/utils/logger.d.ts +31 -0
  126. package/dist/utils/logging.d.ts +9 -0
  127. package/dist/utils/request-id.d.ts +4 -0
  128. package/dist/utils/request-logger.d.ts +19 -0
  129. package/dist/utils/sql.d.ts +27 -0
  130. package/package.json +22 -14
  131. package/src/api/graphql/graphql-schema-generator.ts +5 -5
  132. package/src/api/openapi-generator.ts +2 -2
  133. package/src/api/rest/api-generator.ts +44 -123
  134. package/src/api/rest/query-parser.ts +6 -23
  135. package/src/api/types.ts +2 -2
  136. package/src/auth/auth-hooks.ts +1 -1
  137. package/src/auth/builtin-auth-adapter.ts +1 -2
  138. package/src/auth/interfaces.ts +11 -2
  139. package/src/auth/middleware.ts +4 -4
  140. package/src/auth/routes.ts +1 -2
  141. package/src/functions/define-function.ts +59 -0
  142. package/src/functions/function-loader.ts +16 -0
  143. package/src/functions/index.ts +2 -0
  144. package/src/index.ts +70 -37
  145. package/src/init.ts +78 -21
  146. package/src/storage/GCSStorageController.ts +334 -0
  147. package/src/storage/index.ts +9 -3
  148. package/src/storage/routes.ts +191 -23
  149. package/src/storage/tus-handler.ts +16 -4
  150. package/src/storage/types.ts +25 -3
  151. package/test/api-generator.test.ts +1 -1
  152. package/test/auth-config-types.test.ts +40 -0
  153. package/test/define-function.test.ts +45 -0
  154. package/test/storage-routes.test.ts +160 -0
  155. package/test/backend-hooks-data.test.ts +0 -477
@@ -1,13 +1,20 @@
1
1
  /**
2
2
  * Storage REST API routes using Hono
3
+ *
4
+ * Supports multi-backend routing via `StorageRegistry`. Each endpoint
5
+ * accepts an optional `storageId` parameter (query string or form field)
6
+ * to target a named storage backend. When omitted, the default backend
7
+ * is used.
3
8
  */
4
9
 
5
- import { Hono } from "hono";
10
+ import { Hono, type MiddlewareHandler } from "hono";
6
11
  import fs from "node:fs";
7
12
  import fsp from "node:fs/promises";
8
13
  import { StorageController } from "./types";
9
14
  import { LocalStorageController } from "./LocalStorageController";
10
- import { requireAuth as jwtRequireAuth, optionalAuth } from "../auth/middleware";
15
+ import type { StorageRegistry } from "./storage-registry";
16
+ import { DEFAULT_STORAGE_SOURCE_KEY, type StorageSourceDefinition, type AuthAdapter } from "@rebasepro/types";
17
+ import { requireAuth as jwtRequireAuth, optionalAuth as jwtOptionalAuth } from "../auth/middleware";
11
18
  import { ApiError, errorHandler } from "../api/errors";
12
19
  import { HonoEnv } from "../api/types";
13
20
  import { parseTransformOptions, transformImage, isTransformableImage, TransformCache } from "./image-transform";
@@ -17,7 +24,25 @@ import { TusHandler } from "./tus-handler";
17
24
  const transformCache = new TransformCache();
18
25
 
19
26
  export interface StorageRoutesConfig {
20
- controller: StorageController;
27
+ /**
28
+ * Single storage controller (backward-compatible).
29
+ * Used as fallback when no `registry` is provided.
30
+ */
31
+ controller?: StorageController;
32
+ /**
33
+ * Full storage registry for multi-backend routing.
34
+ * When provided, endpoints resolve the controller from `storageId`
35
+ * parameter. Takes precedence over `controller`.
36
+ */
37
+ registry?: StorageRegistry;
38
+ /**
39
+ * Declared storage sources, surfaced by `GET /sources` so the client can
40
+ * bootstrap its registry. Carries the frontend `transport` (server vs
41
+ * direct) and human-readable labels. Server-transport sources are also
42
+ * derived from the registry; `direct` sources (e.g. Firebase Storage) only
43
+ * exist here since the backend does not proxy them.
44
+ */
45
+ sources?: StorageSourceDefinition[];
21
46
  /** Base path for storage routes (default: '/api/storage') */
22
47
  basePath?: string;
23
48
  /** Require authentication for write operations (default: true) */
@@ -25,6 +50,13 @@ export interface StorageRoutesConfig {
25
50
  /** Allow unauthenticated read access to stored files (default: false).
26
51
  * When false and requireAuth is true, reads also require authentication. */
27
52
  publicRead?: boolean;
53
+ /**
54
+ * When provided, storage routes delegate auth to this adapter instead
55
+ * of the built-in JWT module. This mirrors how data routes use
56
+ * `createAdapterAuthMiddleware()` and avoids the "JWT secret not
57
+ * configured" crash when `configureJwt()` was never called.
58
+ */
59
+ authAdapter?: AuthAdapter;
28
60
  }
29
61
 
30
62
  /**
@@ -66,19 +98,98 @@ function sanitizeStorageKey(key: string): string {
66
98
  return sanitized;
67
99
  }
68
100
 
101
+ /**
102
+ * Build adapter-aware auth middleware for storage routes.
103
+ *
104
+ * When an `AuthAdapter` is provided, token verification is delegated to the
105
+ * adapter instead of the built-in JWT module. This mirrors how data routes
106
+ * use `createAdapterAuthMiddleware()`, but without RLS driver scoping (storage
107
+ * routes don't interact with the DataDriver).
108
+ *
109
+ * Returns both a "write" middleware (enforces auth when `requireAuth` is true)
110
+ * and a "read" middleware (enforces auth unless `publicRead` is set).
111
+ */
112
+ function buildAdapterAuthMiddleware(
113
+ adapter: AuthAdapter,
114
+ requireAuth: boolean,
115
+ publicRead: boolean
116
+ ): { writeAuthMiddleware: MiddlewareHandler<HonoEnv>; readAuthMiddleware: MiddlewareHandler<HonoEnv> } {
117
+ /**
118
+ * Core middleware: verifies the request via the adapter. When `enforce`
119
+ * is true, returns 401 if no authenticated user is resolved.
120
+ */
121
+ const createMiddleware = (enforce: boolean): MiddlewareHandler<HonoEnv> => {
122
+ return async (c, next) => {
123
+ let authenticatedUser = null;
124
+ try {
125
+ authenticatedUser = await adapter.verifyRequest(c.req.raw);
126
+ } catch {
127
+ return c.json({ error: { message: "Unauthorized", code: "UNAUTHORIZED" } }, 401);
128
+ }
129
+
130
+ if (authenticatedUser) {
131
+ c.set("user", {
132
+ userId: authenticatedUser.uid,
133
+ email: authenticatedUser.email,
134
+ roles: authenticatedUser.roles
135
+ });
136
+ }
137
+
138
+ if (enforce && !authenticatedUser) {
139
+ return c.json({ error: { message: "Unauthorized: Authentication required", code: "UNAUTHORIZED" } }, 401);
140
+ }
141
+
142
+ return next();
143
+ };
144
+ };
145
+
146
+ return {
147
+ writeAuthMiddleware: createMiddleware(requireAuth),
148
+ readAuthMiddleware: createMiddleware(!publicRead && requireAuth)
149
+ };
150
+ }
151
+
69
152
  /**
70
153
  * Create storage REST API routes
71
154
  */
72
155
  export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv> {
73
156
  const router = new Hono<HonoEnv>();
74
157
  router.onError(errorHandler);
75
- const { controller, requireAuth = true, publicRead = false } = config;
158
+ const { controller, registry, sources: declaredSources, requireAuth = true, publicRead = false, authAdapter } = config;
76
159
 
77
- // Use actual JWT auth middleware from auth module
78
- const writeAuthMiddleware = requireAuth ? jwtRequireAuth : optionalAuth;
160
+ /**
161
+ * Resolve the storage controller for a request.
162
+ * Looks up by `storageId` in the registry, falls back to the single
163
+ * controller, and finally to the registry default.
164
+ */
165
+ const resolveController = (storageId?: string | null): StorageController => {
166
+ if (registry) {
167
+ return registry.getOrDefault(storageId);
168
+ }
169
+ if (controller) {
170
+ return controller;
171
+ }
172
+ throw new Error("No storage controller or registry available");
173
+ };
174
+
175
+ /** Get the default controller (used for TUS and base-path derivation). */
176
+ const getDefaultController = (): StorageController => {
177
+ if (registry) return registry.getDefault();
178
+ if (controller) return controller;
179
+ throw new Error("No storage controller or registry available");
180
+ };
79
181
 
80
- // For read operations: respect publicRead config.
81
- const readAuthMiddleware = (publicRead || !requireAuth) ? optionalAuth : jwtRequireAuth;
182
+ // ── Auth middleware selection ────────────────────────────────────────
183
+ // When an AuthAdapter is available, delegate token verification to it
184
+ // (mirroring the data-routes pattern). This avoids calling the JWT
185
+ // module which may not have been configured (e.g. custom auth).
186
+ // When no adapter is present, fall back to the built-in JWT middleware.
187
+ const { writeAuthMiddleware, readAuthMiddleware } = authAdapter
188
+ ? buildAdapterAuthMiddleware(authAdapter, requireAuth, publicRead)
189
+ : {
190
+ writeAuthMiddleware: requireAuth ? jwtRequireAuth : jwtOptionalAuth,
191
+ readAuthMiddleware: (publicRead || !requireAuth) ? jwtOptionalAuth : jwtRequireAuth
192
+ };
82
193
 
83
194
  /**
84
195
  * Parse bucket and path from a combined file path.
@@ -116,6 +227,7 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
116
227
 
117
228
  const key = typeof body["key"] === "string" ? body["key"] : "";
118
229
  const bucket = typeof body["bucket"] === "string" ? body["bucket"] : undefined;
230
+ const storageId = typeof body["storageId"] === "string" ? body["storageId"] : c.req.query("storageId");
119
231
 
120
232
  const finalKey = sanitizeStorageKey(key || uploadedFile.name || "unnamed");
121
233
 
@@ -127,7 +239,8 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
127
239
  }
128
240
  }
129
241
 
130
- const result = await controller.putObject({
242
+ const resolved = resolveController(storageId);
243
+ const result = await resolved.putObject({
131
244
  file: uploadedFile,
132
245
  key: finalKey,
133
246
  metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
@@ -155,13 +268,15 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
155
268
  }
156
269
 
157
270
  const filePath = decodeURIComponent(rawPath);
271
+ const storageId = c.req.query("storageId");
272
+ const resolved = resolveController(storageId);
158
273
 
159
274
  // Parse image transform query params (e.g. ?width=300&format=webp)
160
275
  const transformOpts = parseTransformOptions(c.req.query() as Record<string, string>);
161
276
 
162
277
  // For local storage, serve the file directly from disk
163
- if (controller.getType() === "local") {
164
- const localController = controller as LocalStorageController;
278
+ if (resolved.getType() === "local") {
279
+ const localController = resolved as LocalStorageController;
165
280
  const { bucket, resolvedPath } = parseBucketAndPath(filePath);
166
281
 
167
282
  const absolutePath = localController.getAbsolutePath(resolvedPath, bucket);
@@ -208,7 +323,7 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
208
323
  // 1. Mixed-content (HTTPS page → HTTP MinIO) is blocked by browsers
209
324
  // 2. Internal IPs / VPC endpoints are unreachable from the browser
210
325
  const { bucket: parsedBucket, resolvedPath: parsedPath } = parseBucketAndPath(filePath);
211
- const fileObject = await controller.getObject(parsedPath, parsedBucket);
326
+ const fileObject = await resolved.getObject(parsedPath, parsedBucket);
212
327
  if (!fileObject) {
213
328
  throw ApiError.notFound("File not found");
214
329
  }
@@ -249,9 +364,11 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
249
364
  }
250
365
 
251
366
  const filePath = decodeURIComponent(rawPath);
367
+ const storageId = c.req.query("storageId");
368
+ const resolved = resolveController(storageId);
252
369
  const { bucket, resolvedPath } = parseBucketAndPath(filePath);
253
370
 
254
- const downloadConfig = await controller.getSignedUrl(resolvedPath, bucket);
371
+ const downloadConfig = await resolved.getSignedUrl(resolvedPath, bucket);
255
372
 
256
373
  if (downloadConfig.fileNotFound) {
257
374
  throw ApiError.notFound("File not found");
@@ -274,9 +391,11 @@ message: "No file to delete" });
274
391
  }
275
392
 
276
393
  const filePath = decodeURIComponent(rawPath);
394
+ const storageId = c.req.query("storageId");
395
+ const resolved = resolveController(storageId);
277
396
  const { bucket, resolvedPath } = parseBucketAndPath(filePath);
278
397
 
279
- await controller.deleteObject(resolvedPath, bucket);
398
+ await resolved.deleteObject(resolvedPath, bucket);
280
399
 
281
400
  return c.json({
282
401
  success: true,
@@ -293,11 +412,13 @@ message: "No file to delete" });
293
412
  const bucket = c.req.query("bucket");
294
413
  const maxResults = c.req.query("maxResults");
295
414
  const pageToken = c.req.query("pageToken");
415
+ const storageId = c.req.query("storageId");
416
+ const resolved = resolveController(storageId);
296
417
 
297
- const result = await controller.listObjects(
418
+ const result = await resolved.listObjects(
298
419
  storagePrefix,
299
420
  {
300
- bucket: bucket ?? (controller.getType() === "local" ? "default" : undefined),
421
+ bucket: bucket ?? (resolved.getType() === "local" ? "default" : undefined),
301
422
  maxResults: maxResults ? parseInt(maxResults, 10) : undefined,
302
423
  pageToken
303
424
  }
@@ -316,27 +437,29 @@ message: "No file to delete" });
316
437
  router.post("/folder", writeAuthMiddleware, async (c) => {
317
438
  const body = await c.req.json();
318
439
  const folderPath = body.path;
440
+ const storageId = typeof body.storageId === "string" ? body.storageId : c.req.query("storageId");
319
441
 
320
442
  if (!folderPath || typeof folderPath !== "string") {
321
443
  throw ApiError.badRequest("Folder path is required");
322
444
  }
323
445
 
446
+ const resolved = resolveController(storageId);
324
447
  const { bucket, resolvedPath } = parseBucketAndPath(folderPath);
325
448
 
326
449
  if (!resolvedPath || resolvedPath.trim() === "") {
327
450
  throw ApiError.badRequest("Invalid folder path");
328
451
  }
329
452
 
330
- if (controller.getType() === "local") {
453
+ if (resolved.getType() === "local") {
331
454
  // For local storage, create the directory
332
- const localController = controller as LocalStorageController;
455
+ const localController = resolved as LocalStorageController;
333
456
  const absolutePath = localController.getAbsolutePath(resolvedPath, bucket);
334
457
  fs.mkdirSync(absolutePath, { recursive: true });
335
458
  } else {
336
- // For S3-compatible storage, create a zero-byte marker object with trailing slash
459
+ // For S3/GCS-compatible storage, create a zero-byte marker object with trailing slash
337
460
  const key = resolvedPath.endsWith("/") ? resolvedPath : resolvedPath + "/";
338
461
  const emptyFile = new File([], key, { type: "application/x-directory" });
339
- await controller.putObject({
462
+ await resolved.putObject({
340
463
  file: emptyFile,
341
464
  key
342
465
  });
@@ -352,10 +475,11 @@ message: "No file to delete" });
352
475
  // TUS Resumable Uploads
353
476
  // -----------------------------------------------------------------------
354
477
 
355
- const tusBaseDir = controller.getType() === "local"
356
- ? (controller as LocalStorageController).getBasePath()
478
+ const defaultCtrl = getDefaultController();
479
+ const tusBaseDir = defaultCtrl.getType() === "local"
480
+ ? (defaultCtrl as LocalStorageController).getBasePath()
357
481
  : (process.env.STORAGE_PATH || "./uploads");
358
- const tusHandler = new TusHandler(tusBaseDir, controller);
482
+ const tusHandler = new TusHandler(tusBaseDir, defaultCtrl, registry);
359
483
  tusHandler.startCleanup();
360
484
 
361
485
  router.options("/tus", (_c) => tusHandler.options());
@@ -364,5 +488,49 @@ message: "No file to delete" });
364
488
  router.patch("/tus/:id", writeAuthMiddleware, async (c) => tusHandler.patch(c, c.req.param("id")));
365
489
  router.delete("/tus/:id", writeAuthMiddleware, async (c) => tusHandler.delete(c, c.req.param("id")));
366
490
 
491
+ // -----------------------------------------------------------------------
492
+ // Storage Sources Discovery
493
+ // -----------------------------------------------------------------------
494
+
495
+ /**
496
+ * GET /sources — list all registered storage backends.
497
+ * The client can bootstrap its StorageSourceRegistry from this endpoint.
498
+ */
499
+ router.get("/sources", (c) => {
500
+ const byKey = new Map<string, { key: string; engine: string; transport: "server" | "direct"; label?: string }>();
501
+
502
+ // 1. Server-backed sources derived from the registry (source of truth
503
+ // for the actual engine type), or the single controller.
504
+ if (registry) {
505
+ for (const key of registry.list()) {
506
+ byKey.set(key, {
507
+ key,
508
+ engine: registry.get(key)?.getType() ?? "unknown",
509
+ transport: "server",
510
+ });
511
+ }
512
+ } else {
513
+ byKey.set(DEFAULT_STORAGE_SOURCE_KEY, {
514
+ key: DEFAULT_STORAGE_SOURCE_KEY,
515
+ engine: defaultCtrl.getType(),
516
+ transport: "server",
517
+ });
518
+ }
519
+
520
+ // 2. Overlay declared definitions: adds `direct` sources the backend
521
+ // does not proxy, plus labels and explicit transport/engine.
522
+ for (const def of declaredSources ?? []) {
523
+ const existing = byKey.get(def.key);
524
+ byKey.set(def.key, {
525
+ key: def.key,
526
+ engine: def.engine ?? existing?.engine ?? "unknown",
527
+ transport: def.transport ?? existing?.transport ?? "server",
528
+ label: def.label ?? existing?.label,
529
+ });
530
+ }
531
+
532
+ return c.json({ success: true, data: Array.from(byKey.values()) });
533
+ });
534
+
367
535
  return router;
368
536
  }
@@ -14,6 +14,7 @@ import { existsSync } from "fs";
14
14
  import { join } from "path";
15
15
  import type { Context } from "hono";
16
16
  import type { StorageController } from "./types";
17
+ import type { StorageRegistry } from "./storage-registry";
17
18
  import { logger } from "../utils/logger.js";
18
19
 
19
20
  /** Metadata for an in-progress resumable upload. */
@@ -57,7 +58,8 @@ export class TusHandler {
57
58
 
58
59
  constructor(
59
60
  storageBaseDir: string,
60
- private storageController?: StorageController
61
+ private storageController?: StorageController,
62
+ private storageRegistry?: StorageRegistry
61
63
  ) {
62
64
  this.tusDir = join(storageBaseDir, ".tus-uploads");
63
65
  }
@@ -283,7 +285,17 @@ export class TusHandler {
283
285
  private async finalize(upload: TusUpload): Promise<void> {
284
286
  upload.completed = true;
285
287
 
286
- if (!this.storageController) {
288
+ // Resolve the target controller: prefer storageId from TUS metadata,
289
+ // then fall back to the registry default, then the single controller.
290
+ const storageId = upload.metadata.storageId;
291
+ let targetController = this.storageController;
292
+ if (this.storageRegistry) {
293
+ targetController = storageId
294
+ ? this.storageRegistry.getOrDefault(storageId)
295
+ : this.storageRegistry.getDefault();
296
+ }
297
+
298
+ if (!targetController) {
287
299
  // No controller — leave temp file in place
288
300
  logger.warn("[TUS] Upload completed but no StorageController configured. Temp file remains:", { filePath: upload.filePath });
289
301
  return;
@@ -297,7 +309,7 @@ export class TusHandler {
297
309
 
298
310
  const file = new File([data], fileName, { type: mimeType });
299
311
 
300
- await this.storageController.putObject({
312
+ await targetController.putObject({
301
313
  file,
302
314
  key: fileName,
303
315
  bucket: upload.bucket
@@ -307,7 +319,7 @@ export class TusHandler {
307
319
  try { await unlink(upload.filePath); } catch { /* ok */ }
308
320
  this.uploads.delete(upload.id);
309
321
 
310
- logger.info(`[TUS] Upload ${upload.id} finalized → ${fileName}`);
322
+ logger.info(`[TUS] Upload ${upload.id} finalized → ${fileName}`, storageId ? { storageId } : {});
311
323
  } catch (err) {
312
324
  logger.error(`[TUS] Failed to finalize upload ${upload.id}`, { error: err });
313
325
  }
@@ -45,18 +45,40 @@ export interface S3StorageConfig {
45
45
  }
46
46
 
47
47
  /**
48
- * Storage configuration local filesystem or S3-compatible.
48
+ * Google Cloud Storage configuration (works with GCS and Firebase Storage)
49
+ */
50
+ export interface GCSStorageConfig {
51
+ type: "gcs";
52
+ /** GCS bucket name (e.g. "my-project.appspot.com" for Firebase Storage) */
53
+ bucket: string;
54
+ /** GCP project ID (optional, auto-detected from credentials) */
55
+ projectId?: string;
56
+ /** Path to service account JSON key file */
57
+ keyFilename?: string;
58
+ /** Service account credentials object (alternative to keyFilename) */
59
+ credentials?: { client_email: string; private_key: string; project_id?: string };
60
+ /** Maximum file size in bytes (default: 50MB) */
61
+ maxFileSize?: number;
62
+ /** Allowed MIME types */
63
+ allowedMimeTypes?: string[];
64
+ /** Signed URL expiration in seconds (default: 3600) */
65
+ signedUrlExpiration?: number;
66
+ }
67
+
68
+ /**
69
+ * Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.
49
70
  *
50
71
  * **Built-in providers:**
51
72
  * - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).
52
73
  * - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,
53
74
  * Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).
75
+ * - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.
54
76
  *
55
77
  * **Custom providers:**
56
- * For cloud-native storage (GCS, Azure Blob, etc.), implement the `StorageController`
78
+ * For other cloud storage (Azure Blob, etc.), implement the `StorageController`
57
79
  * interface and pass the instance directly to the `storage` config.
58
80
  */
59
- export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig;
81
+ export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;
60
82
 
61
83
  /**
62
84
  * Storage controller interface for backend implementations
@@ -545,7 +545,7 @@ passwordHash: "custom-hash" },
545
545
  } as any
546
546
  ];
547
547
 
548
- const generator = new RestApiGenerator(authCollections, mockDriver, undefined, mockAuthAdapter as any);
548
+ const generator = new RestApiGenerator(authCollections, mockDriver, mockAuthAdapter as any);
549
549
  app.route("/api", generator.generateRoutes());
550
550
 
551
551
  mockDriver.saveEntity.mockResolvedValue({
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Compile-time tests for `RebaseAuthConfig`.
3
+ *
4
+ * The Jest assertions are trivial (always true) — the real value is that
5
+ * `tsc` validates the `@ts-expect-error` annotations: if a line compiles
6
+ * when it shouldn't, tsc reports an error. This guards against a
7
+ * regression of the `[key: string]: unknown` catch-all that used to make
8
+ * every key (including typos) compile silently.
9
+ */
10
+ import type { RebaseAuthConfig } from "../src/init";
11
+
12
+ describe("RebaseAuthConfig", () => {
13
+
14
+ it("accepts known top-level keys", () => {
15
+ const config: RebaseAuthConfig = {
16
+ jwtSecret: "secret",
17
+ allowRegistration: true,
18
+ defaultRole: "viewer",
19
+ google: { clientId: "id" }
20
+ };
21
+ expect(config.jwtSecret).toBe("secret");
22
+ });
23
+
24
+ it("rejects a misspelled top-level key (compile-time)", () => {
25
+ const config: RebaseAuthConfig = {
26
+ jwtSecret: "secret",
27
+ // @ts-expect-error — "jwtSecrt" is a typo, not a real RebaseAuthConfig key
28
+ jwtSecrt: "secret"
29
+ };
30
+ expect(config).toBeDefined();
31
+ });
32
+
33
+ it("rejects an unknown provider-shaped key (compile-time)", () => {
34
+ const config: RebaseAuthConfig = {
35
+ // @ts-expect-error — "seedDefaultRoles" is not a supported option
36
+ seedDefaultRoles: true
37
+ };
38
+ expect(config).toBeDefined();
39
+ });
40
+ });
@@ -0,0 +1,45 @@
1
+ import { Hono } from "hono";
2
+ import { defineFunction } from "../src/functions/define-function";
3
+
4
+ describe("defineFunction", () => {
5
+ it("returns a loader-compatible Hono app (has .fetch and .routes)", () => {
6
+ const app = defineFunction((a) => {
7
+ a.get("/ping", (c) => c.text("pong"));
8
+ });
9
+
10
+ // These are exactly the properties the loader duck-types on.
11
+ expect(typeof app.fetch).toBe("function");
12
+ expect(Array.isArray(app.routes)).toBe(true);
13
+ });
14
+
15
+ it("registers routes on the provided app", async () => {
16
+ const app = defineFunction((a) => {
17
+ a.get("/hello", (c) => c.text("hi"));
18
+ });
19
+
20
+ const res = await app.request("/hello");
21
+ expect(res.status).toBe(200);
22
+ expect(await res.text()).toBe("hi");
23
+ });
24
+
25
+ it("exposes the rebase singleton via the injected context", () => {
26
+ let received: unknown;
27
+ defineFunction((_a, ctx) => {
28
+ received = ctx.rebase;
29
+ });
30
+ // The singleton reference is passed without being dereferenced,
31
+ // so no "not initialized" error is thrown at define time.
32
+ expect(received).toBeDefined();
33
+ });
34
+
35
+ it("uses a returned Hono app when the definition returns one", async () => {
36
+ const custom = new Hono();
37
+ custom.get("/custom", (c) => c.text("custom"));
38
+
39
+ const app = defineFunction(() => custom);
40
+
41
+ const res = await app.request("/custom");
42
+ expect(res.status).toBe(200);
43
+ expect(await res.text()).toBe("custom");
44
+ });
45
+ });