@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.
- package/build-errors.txt +52 -0
- package/dist/api/ast-schema-editor.d.ts +21 -0
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
- package/dist/api/errors.d.ts +53 -0
- package/dist/api/graphql/graphql-schema-generator.d.ts +44 -0
- package/dist/api/graphql/index.d.ts +1 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/logs-routes.d.ts +37 -0
- package/dist/api/openapi-generator.d.ts +16 -0
- package/dist/api/rest/api-generator.d.ts +76 -0
- package/dist/api/rest/index.d.ts +1 -0
- package/dist/api/rest/query-parser.d.ts +8 -0
- package/dist/api/schema-editor-routes.d.ts +3 -0
- package/dist/api/server.d.ts +40 -0
- package/dist/api/types.d.ts +104 -0
- package/dist/auth/adapter-middleware.d.ts +43 -0
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +32 -0
- package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/auth/api-keys/api-key-store.d.ts +35 -0
- package/dist/auth/api-keys/api-key-types.d.ts +96 -0
- package/dist/auth/api-keys/index.d.ts +17 -0
- package/dist/auth/apple-oauth.d.ts +30 -0
- package/dist/auth/auth-hooks.d.ts +242 -0
- package/dist/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/auth/builtin-auth-adapter.d.ts +51 -0
- package/dist/auth/crypto-utils.d.ts +16 -0
- package/dist/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/auth/discord-oauth.d.ts +14 -0
- package/dist/auth/facebook-oauth.d.ts +14 -0
- package/dist/auth/github-oauth.d.ts +15 -0
- package/dist/auth/gitlab-oauth.d.ts +13 -0
- package/dist/auth/google-oauth.d.ts +47 -0
- package/dist/auth/index.d.ts +37 -0
- package/dist/auth/interfaces.d.ts +431 -0
- package/dist/auth/jwt.d.ts +55 -0
- package/dist/auth/linkedin-oauth.d.ts +18 -0
- package/dist/auth/magic-link-routes.d.ts +30 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +7 -0
- package/dist/auth/mfa.d.ts +49 -0
- package/dist/auth/microsoft-oauth.d.ts +16 -0
- package/dist/auth/middleware.d.ts +113 -0
- package/dist/auth/password.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/rls-scope.d.ts +31 -0
- package/dist/auth/routes.d.ts +35 -0
- package/dist/auth/session-routes.d.ts +27 -0
- package/dist/auth/slack-oauth.d.ts +12 -0
- package/dist/auth/spotify-oauth.d.ts +12 -0
- package/dist/auth/twitter-oauth.d.ts +18 -0
- package/dist/backend-CIxN4FVm.js +15 -0
- package/dist/backend-CIxN4FVm.js.map +1 -0
- package/dist/base64-js-C_frYBkI.js +1687 -0
- package/dist/base64-js-C_frYBkI.js.map +1 -0
- package/dist/chunk-Dze3rakg.js +42 -0
- package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
- package/dist/collections/loader.d.ts +5 -0
- package/dist/cron/cron-loader.d.ts +17 -0
- package/dist/cron/cron-routes.d.ts +14 -0
- package/dist/cron/cron-scheduler.d.ts +106 -0
- package/dist/cron/cron-store.d.ts +32 -0
- package/dist/cron/index.d.ts +6 -0
- package/dist/db/interfaces.d.ts +18 -0
- package/dist/dist-DMO-zF6D.js +832 -0
- package/dist/dist-DMO-zF6D.js.map +1 -0
- package/dist/email/index.d.ts +6 -0
- package/dist/email/smtp-email-service.d.ts +25 -0
- package/dist/email/templates.d.ts +50 -0
- package/dist/email/types.d.ts +126 -0
- package/dist/env.d.ts +102 -0
- package/dist/from-VbwD7xRf.js +3849 -0
- package/dist/from-VbwD7xRf.js.map +1 -0
- package/dist/functions/define-function.d.ts +50 -0
- package/dist/functions/function-loader.d.ts +17 -0
- package/dist/functions/function-routes.d.ts +10 -0
- package/dist/functions/index.d.ts +5 -0
- package/dist/history/history-routes.d.ts +23 -0
- package/dist/history/index.d.ts +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.es.js +61322 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +103633 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +11 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/init.d.ts +314 -0
- package/dist/jws-BqRRaK11.js +616 -0
- package/dist/jws-BqRRaK11.js.map +1 -0
- package/dist/jwt-BETC8a1J.js +3560 -0
- package/dist/jwt-BETC8a1J.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-BeBSuOXN.js +125 -0
- package/dist/ms-BeBSuOXN.js.map +1 -0
- package/dist/multipart-parser-CedBDOeC.js +299 -0
- package/dist/multipart-parser-CedBDOeC.js.map +1 -0
- package/dist/serve-spa.d.ts +30 -0
- package/dist/services/driver-registry.d.ts +78 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/services/webhook-service.d.ts +29 -0
- package/dist/singleton.d.ts +35 -0
- package/dist/src-CB2PIpBe.js +1182 -0
- package/dist/src-CB2PIpBe.js.map +1 -0
- package/dist/src-DjzOT1kG.js +29746 -0
- package/dist/src-DjzOT1kG.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/LocalStorageController.d.ts +46 -0
- package/dist/storage/S3StorageController.d.ts +36 -0
- package/dist/storage/image-transform.d.ts +50 -0
- package/dist/storage/index.d.ts +31 -0
- package/dist/storage/routes.d.ts +70 -0
- package/dist/storage/storage-registry.d.ts +78 -0
- package/dist/storage/tus-handler.d.ts +53 -0
- package/dist/storage/types.d.ts +128 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/dev-port.d.ts +37 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logging.d.ts +9 -0
- package/dist/utils/request-id.d.ts +4 -0
- package/dist/utils/request-logger.d.ts +19 -0
- package/dist/utils/sql.d.ts +27 -0
- package/package.json +22 -14
- package/src/api/graphql/graphql-schema-generator.ts +5 -5
- package/src/api/openapi-generator.ts +2 -2
- package/src/api/rest/api-generator.ts +44 -123
- package/src/api/rest/query-parser.ts +6 -23
- package/src/api/types.ts +2 -2
- package/src/auth/auth-hooks.ts +1 -1
- package/src/auth/builtin-auth-adapter.ts +1 -2
- package/src/auth/interfaces.ts +11 -2
- package/src/auth/middleware.ts +4 -4
- package/src/auth/routes.ts +1 -2
- package/src/functions/define-function.ts +59 -0
- package/src/functions/function-loader.ts +16 -0
- package/src/functions/index.ts +2 -0
- package/src/index.ts +70 -37
- package/src/init.ts +78 -21
- package/src/storage/GCSStorageController.ts +334 -0
- package/src/storage/index.ts +9 -3
- package/src/storage/routes.ts +191 -23
- package/src/storage/tus-handler.ts +16 -4
- package/src/storage/types.ts +25 -3
- package/test/api-generator.test.ts +1 -1
- package/test/auth-config-types.test.ts +40 -0
- package/test/define-function.test.ts +45 -0
- package/test/storage-routes.test.ts +160 -0
- package/test/backend-hooks-data.test.ts +0 -477
package/src/storage/routes.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
//
|
|
81
|
-
|
|
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
|
|
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 (
|
|
164
|
-
const localController =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
418
|
+
const result = await resolved.listObjects(
|
|
298
419
|
storagePrefix,
|
|
299
420
|
{
|
|
300
|
-
bucket: bucket ?? (
|
|
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 (
|
|
453
|
+
if (resolved.getType() === "local") {
|
|
331
454
|
// For local storage, create the directory
|
|
332
|
-
const localController =
|
|
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
|
|
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
|
|
356
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/storage/types.ts
CHANGED
|
@@ -45,18 +45,40 @@ export interface S3StorageConfig {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Storage configuration
|
|
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
|
|
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,
|
|
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
|
+
});
|