@rebasepro/server-core 0.4.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.
- package/README.md +62 -25
- package/dist/{server-core/src/api → api}/errors.d.ts +20 -2
- package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
- package/dist/{server-core/src/api → api}/types.d.ts +2 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
- package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
- package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
- package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
- package/dist/{server-core/src/auth → auth}/jwt.d.ts +10 -0
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/session-routes.d.ts +25 -0
- package/dist/backend-CIxN4FVm.js +15 -0
- package/dist/backend-CIxN4FVm.js.map +1 -0
- package/dist/chunk-Dze3rakg.js +42 -0
- package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
- package/dist/dist-CZKP-Xz4.js +832 -0
- package/dist/dist-CZKP-Xz4.js.map +1 -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/{server-core/src/index.d.ts → index.d.ts} +1 -0
- package/dist/index.es.js +60871 -50424
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +73151 -51256
- package/dist/index.umd.js.map +1 -1
- 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/{server-core/src/init.d.ts → init.d.ts} +4 -4
- package/dist/jwt-DHcQRGC3.js +4168 -0
- package/dist/jwt-DHcQRGC3.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/src-COaj0G3P.js +1182 -0
- package/dist/src-COaj0G3P.js.map +1 -0
- package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
- package/dist/utils/request-id.d.ts +4 -0
- package/package.json +29 -29
- package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
- package/src/api/errors.ts +95 -20
- package/src/api/graphql/graphql-schema-generator.ts +15 -10
- package/src/api/logs-routes.ts +5 -2
- package/src/api/openapi-generator.ts +1 -1
- package/src/api/rest/api-generator-count.test.ts +21 -10
- package/src/api/rest/api-generator.ts +151 -36
- package/src/api/rest/query-parser.ts +18 -12
- package/src/api/server.ts +13 -2
- package/src/api/types.ts +2 -0
- package/src/auth/adapter-middleware.ts +17 -10
- package/src/auth/admin-user-ops.ts +236 -0
- package/src/auth/api-keys/api-key-middleware.ts +15 -9
- package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
- package/src/auth/api-keys/api-key-routes.ts +3 -3
- package/src/auth/api-keys/api-key-store.ts +14 -8
- package/src/auth/api-keys/index.ts +2 -2
- package/src/auth/apple-oauth.ts +4 -3
- package/src/auth/auth-hooks.ts +50 -10
- package/src/auth/bitbucket-oauth.ts +5 -4
- package/src/auth/builtin-auth-adapter.ts +64 -54
- package/src/auth/custom-auth-adapter.ts +4 -5
- package/src/auth/discord-oauth.ts +5 -4
- package/src/auth/facebook-oauth.ts +5 -4
- package/src/auth/github-oauth.ts +6 -5
- package/src/auth/gitlab-oauth.ts +5 -4
- package/src/auth/google-oauth.ts +2 -1
- package/src/auth/index.ts +6 -2
- package/src/auth/jwt.ts +15 -4
- package/src/auth/linkedin-oauth.ts +4 -3
- package/src/auth/mfa-routes.ts +299 -0
- package/src/auth/mfa.ts +2 -1
- package/src/auth/microsoft-oauth.ts +5 -4
- package/src/auth/middleware.ts +5 -4
- package/src/auth/rate-limiter.ts +1 -1
- package/src/auth/reset-password-admin.ts +144 -0
- package/src/auth/rls-scope.ts +1 -1
- package/src/auth/routes.ts +52 -600
- package/src/auth/session-routes.ts +341 -0
- package/src/auth/slack-oauth.ts +6 -5
- package/src/auth/spotify-oauth.ts +5 -4
- package/src/auth/twitter-oauth.ts +4 -3
- package/src/collections/loader.ts +5 -4
- package/src/cron/cron-scheduler.test.ts +23 -11
- package/src/cron/cron-scheduler.ts +14 -8
- package/src/cron/cron-store.ts +1 -1
- package/src/email/smtp-email-service.ts +3 -2
- package/src/env.ts +11 -10
- package/src/index.ts +1 -0
- package/src/init/docs.ts +47 -0
- package/src/init/health.ts +37 -0
- package/src/init/middlewares.ts +61 -0
- package/src/init/shutdown.ts +56 -0
- package/src/init/storage.ts +57 -0
- package/src/init.ts +93 -295
- package/src/serve-spa.ts +12 -5
- package/src/services/driver-registry.ts +4 -3
- package/src/storage/S3StorageController.ts +1 -2
- package/src/storage/image-transform.ts +25 -10
- package/src/storage/routes.ts +30 -11
- package/src/storage/storage-registry.ts +4 -3
- package/src/storage/tus-handler.ts +12 -12
- package/src/utils/request-id.ts +40 -0
- package/src/utils/request-logger.ts +6 -0
- package/test/api-generator.test.ts +90 -2
- package/test/api-key-permission-guard.test.ts +24 -12
- package/test/auth-routes.test.ts +5 -3
- package/test/backend-hooks-data.test.ts +99 -30
- package/test/custom-auth-adapter.test.ts +16 -15
- package/test/email-templates.test.ts +10 -5
- package/test/env.test.ts +10 -10
- package/test/function-loader.test.ts +7 -4
- package/test/graphql-schema-generator.test.ts +554 -0
- package/test/jwt-security.test.ts +1 -1
- package/test/query-parser.test.ts +0 -1
- package/test/reset-password-admin.test.ts +113 -0
- package/test/singleton.test.ts +5 -5
- package/test/smtp-email-service.test.ts +21 -21
- package/test/webhook-service.test.ts +22 -11
- package/tsconfig.json +2 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -6
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/index-Cr1D21av.js +0 -49
- package/dist/index-Cr1D21av.js.map +0 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
- package/dist/server-core/src/env.d.ts +0 -137
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -536
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -941
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1181
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/src/auth/admin-routes.ts +0 -534
- package/test/admin-routes.test.ts +0 -544
- package/test/backend-hooks-admin.test.ts +0 -388
- package/test.ts +0 -6
- /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
- /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
- /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
- /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
- /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
- /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
- /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
- /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
|
@@ -213,8 +213,7 @@ export class S3StorageController implements StorageController {
|
|
|
213
213
|
|
|
214
214
|
// Convert stream to buffer
|
|
215
215
|
const chunks: Uint8Array[] = [];
|
|
216
|
-
|
|
217
|
-
for await (const chunk of response.Body) {
|
|
216
|
+
for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {
|
|
218
217
|
chunks.push(chunk);
|
|
219
218
|
}
|
|
220
219
|
const buffer = Buffer.concat(chunks);
|
|
@@ -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,
|
|
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,
|
|
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
|
|
197
|
-
|
|
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)
|
|
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.
|
|
213
|
+
this.totalBytes += data.length;
|
|
214
|
+
this.cache.set(cacheKey, { data,
|
|
215
|
+
contentType,
|
|
216
|
+
timestamp: Date.now() });
|
|
202
217
|
}
|
|
203
218
|
}
|
package/src/storage/routes.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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",
|
|
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: "*",
|
|
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: "*",
|
|
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",
|
|
99
|
-
|
|
100
|
-
{ collection: "
|
|
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",
|
|
112
|
-
|
|
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: "*",
|
|
122
|
-
|
|
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);
|
package/test/auth-routes.test.ts
CHANGED
|
@@ -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" })).
|
|
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,
|
|
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,
|
|
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",
|