@rebasepro/server 0.11.1-canary.gfadf355 → 0.12.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/dist/auth/api-keys/api-key-types.d.ts +6 -70
- package/dist/auth/builtin-auth-adapter.d.ts +11 -0
- package/dist/auth/registration-policy.d.ts +57 -0
- package/dist/boot/driver.d.ts +17 -3
- package/dist/boot/env.d.ts +16 -2
- package/dist/env.d.ts +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +121 -18
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +13 -0
- package/dist/{openapi-generator-DUocM036.js → openapi-generator-Bjzmb5cn.js} +3 -3
- package/dist/{openapi-generator-DUocM036.js.map → openapi-generator-Bjzmb5cn.js.map} +1 -1
- package/dist/{schema-editor-routes-CbKEoeiH.js → schema-editor-routes-DDxfOIid.js} +2 -2
- package/dist/{schema-editor-routes-CbKEoeiH.js.map → schema-editor-routes-DDxfOIid.js.map} +1 -1
- package/dist/{src-CyC3JXAq.js → src-CoOAMnBh.js} +4 -4
- package/dist/src-CoOAMnBh.js.map +1 -0
- package/dist/{src-DzAgLF8X.js → src-Ivjud8jD.js} +141 -109
- package/dist/src-Ivjud8jD.js.map +1 -0
- package/package.json +5 -5
- package/dist/src-CyC3JXAq.js.map +0 -1
- package/dist/src-DzAgLF8X.js.map +0 -1
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type definitions for Service API Keys.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The wire contract — permissions, the masked key, the create/update payloads —
|
|
5
|
+
* lives in `@rebasepro/types`, because the client SDK needs the same shapes and
|
|
6
|
+
* the two declarations had already drifted apart. Only {@link ApiKey}, the
|
|
7
|
+
* database row carrying `key_hash`, is server-side and stays here.
|
|
7
8
|
*
|
|
8
9
|
* @module
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
* Use `"*"` as the collection value to grant access to all collections
|
|
14
|
-
* (and all custom functions). Custom functions are addressed with the
|
|
15
|
-
* `functions` namespace: `"functions"` grants every function,
|
|
16
|
-
* `"functions/<name>"` grants a single one.
|
|
17
|
-
*/
|
|
18
|
-
export interface ApiKeyPermission {
|
|
19
|
-
/** Collection slug, `"functions"`/`"functions/<name>"`, or `"*"` for everything. */
|
|
20
|
-
collection: string;
|
|
21
|
-
/** Allowed operations on the collection. */
|
|
22
|
-
operations: ("read" | "write" | "delete")[];
|
|
23
|
-
}
|
|
11
|
+
import type { ApiKeyPermission } from "@rebasepro/types";
|
|
12
|
+
export type { ApiKeyPermission, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest } from "@rebasepro/types";
|
|
24
13
|
/**
|
|
25
14
|
* Full database row for an API key.
|
|
26
15
|
* The `key_hash` is never exposed via the API — only stored for lookup.
|
|
@@ -54,56 +43,3 @@ export interface ApiKey {
|
|
|
54
43
|
expires_at: string | null;
|
|
55
44
|
revoked_at: string | null;
|
|
56
45
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Masked version of an API key, safe for API responses.
|
|
59
|
-
* Omits `key_hash` and shows only the prefix.
|
|
60
|
-
*/
|
|
61
|
-
export interface ApiKeyMasked {
|
|
62
|
-
id: string;
|
|
63
|
-
name: string;
|
|
64
|
-
key_prefix: string;
|
|
65
|
-
permissions: ApiKeyPermission[];
|
|
66
|
-
/** When true, the key is granted the `admin` role (admin routes + RLS `default_admin` policies). */
|
|
67
|
-
admin: boolean;
|
|
68
|
-
rate_limit: number | null;
|
|
69
|
-
created_by: string;
|
|
70
|
-
created_at: string;
|
|
71
|
-
updated_at: string;
|
|
72
|
-
last_used_at: string | null;
|
|
73
|
-
expires_at: string | null;
|
|
74
|
-
revoked_at: string | null;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Request body for creating a new API key.
|
|
78
|
-
*/
|
|
79
|
-
export interface CreateApiKeyRequest {
|
|
80
|
-
name: string;
|
|
81
|
-
permissions: ApiKeyPermission[];
|
|
82
|
-
/** When true, grants the `admin` role (admin routes + RLS `default_admin` policies). */
|
|
83
|
-
admin?: boolean;
|
|
84
|
-
/** Requests per 15-minute window. Omit or `null` to use the server default (1000/window). */
|
|
85
|
-
rate_limit?: number | null;
|
|
86
|
-
/** ISO-8601 expiration timestamp. Omit for no expiration. */
|
|
87
|
-
expires_at?: string | null;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Request body for updating an existing API key.
|
|
91
|
-
* All fields are optional — only provided fields are updated.
|
|
92
|
-
*/
|
|
93
|
-
export interface UpdateApiKeyRequest {
|
|
94
|
-
name?: string;
|
|
95
|
-
permissions?: ApiKeyPermission[];
|
|
96
|
-
/** When true, grants the `admin` role (admin routes + RLS `default_admin` policies). */
|
|
97
|
-
admin?: boolean;
|
|
98
|
-
rate_limit?: number | null;
|
|
99
|
-
expires_at?: string | null;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Returned exactly once when a key is created.
|
|
103
|
-
* The `key` field contains the full plaintext key — it is never stored
|
|
104
|
-
* or returned again after creation.
|
|
105
|
-
*/
|
|
106
|
-
export interface ApiKeyWithSecret extends ApiKeyMasked {
|
|
107
|
-
/** Full plaintext API key (e.g. `rk_live_abc123...`). */
|
|
108
|
-
key: string;
|
|
109
|
-
}
|
|
@@ -28,6 +28,17 @@ export interface BuiltinAuthAdapterConfig {
|
|
|
28
28
|
emailConfig?: EmailConfig;
|
|
29
29
|
/** Whether to allow new user registration. */
|
|
30
30
|
allowRegistration?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Hard kill switch: block self-registration outright, including the
|
|
33
|
+
* first-user bootstrap window that an empty database would otherwise open.
|
|
34
|
+
*
|
|
35
|
+
* Was declared on the route module and read by both config endpoints, but
|
|
36
|
+
* never plumbed through here — so nothing a user of the framework could
|
|
37
|
+
* write ever reached it, and the tests that covered it passed only because
|
|
38
|
+
* they built `createAuthRoutes` directly, bypassing this adapter (the sole
|
|
39
|
+
* wiring path a real backend uses).
|
|
40
|
+
*/
|
|
41
|
+
disableSelfRegistration?: boolean;
|
|
31
42
|
/** Whether to expose the authenticated email→minimal-profile lookup route. */
|
|
32
43
|
allowUserLookup?: boolean;
|
|
33
44
|
/** Default role to assign to new users. */
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a registration may proceed — computed in exactly one place.
|
|
3
|
+
*
|
|
4
|
+
* This predicate had drifted across three independent implementations, which is
|
|
5
|
+
* the shape the bug always takes: one endpoint *advertises* that registration is
|
|
6
|
+
* open, another *enforces* that it is closed, and the user lands on a form that
|
|
7
|
+
* can only ever 403.
|
|
8
|
+
*
|
|
9
|
+
* 1. `POST /auth/register` (routes.ts) — enforces.
|
|
10
|
+
* 2. `GET /auth/config` (session-routes.ts) — advertises.
|
|
11
|
+
* 3. `getCapabilities()` (builtin-auth-adapter.ts) — also advertises, and is
|
|
12
|
+
* the one that actually answers `GET /auth/config` on a real backend:
|
|
13
|
+
* `init.ts` registers that path directly *before* mounting the auth router,
|
|
14
|
+
* so Hono resolves it first and the session-routes copy never runs for it.
|
|
15
|
+
*
|
|
16
|
+
* Two of the three had already been fixed for the empty-database case when the
|
|
17
|
+
* third was still returning `allowRegistration || needsSetup` — missing the kill
|
|
18
|
+
* switch entirely. The fix is not to correct the third copy but to delete all
|
|
19
|
+
* three: every caller now goes through this function, so the next term added
|
|
20
|
+
* here reaches every surface at once.
|
|
21
|
+
*
|
|
22
|
+
* The rule itself:
|
|
23
|
+
*
|
|
24
|
+
* - `disableSelfRegistration` is absolute. It blocks even the first user, for
|
|
25
|
+
* operators who provision accounts out of band and never want a public
|
|
26
|
+
* first-come-first-admin window.
|
|
27
|
+
* - Otherwise an **empty** user table always admits the first registration,
|
|
28
|
+
* which auto-promotes to admin. Without this a backend deployed with
|
|
29
|
+
* `allowRegistration: false` is a dead end: `POST /admin/bootstrap` needs an
|
|
30
|
+
* authenticated caller, and an empty database cannot produce one.
|
|
31
|
+
* - Otherwise `allowRegistration` decides, as it always did.
|
|
32
|
+
*/
|
|
33
|
+
export interface RegistrationPolicy {
|
|
34
|
+
/** The hard kill switch. Blocks registration including first-user bootstrap. */
|
|
35
|
+
disableSelfRegistration?: boolean;
|
|
36
|
+
/** Steady-state self-registration, once at least one user exists. */
|
|
37
|
+
allowRegistration?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* True when the user table is empty — the bootstrap window.
|
|
40
|
+
*
|
|
41
|
+
* Callers that already know this (the config endpoints compute it to report
|
|
42
|
+
* `needsSetup`) pass it directly. `POST /auth/register` must not: it serves
|
|
43
|
+
* anonymous callers, so it checks {@link isSteadyStateRegistrationOpen}
|
|
44
|
+
* first and only pays for the count when that says no.
|
|
45
|
+
*/
|
|
46
|
+
needsSetup: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** The full predicate, for callers that already know whether setup is needed. */
|
|
49
|
+
export declare function isRegistrationOpen(policy: RegistrationPolicy): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The same predicate with the bootstrap window assumed closed.
|
|
52
|
+
*
|
|
53
|
+
* Exists so `POST /auth/register` can reject the common case without counting
|
|
54
|
+
* rows. A `false` here does **not** mean "refuse" — it means "the answer depends
|
|
55
|
+
* on whether the table is empty, so now go and look".
|
|
56
|
+
*/
|
|
57
|
+
export declare function isSteadyStateRegistrationOpen(policy: Omit<RegistrationPolicy, "needsSetup">): boolean;
|
package/dist/boot/driver.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { BackendBootstrapper } from "@rebasepro/types";
|
|
2
2
|
import type { ResolvedDataSourceConfig } from "./sources";
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The connection handle a driver hands back at boot: the client object, the
|
|
5
|
+
* pool to close on shutdown, and how to probe it.
|
|
6
|
+
*
|
|
7
|
+
* Named `DatabaseConnection` until that collided with `DatabaseConnection` in
|
|
8
|
+
* `@rebasepro/types` — an abstract `{ type, isConnected, close() }` that
|
|
9
|
+
* `MongoDBConnection` implements. The two share no field, and both are public:
|
|
10
|
+
* one is re-exported from `@rebasepro/server`'s index, the other from
|
|
11
|
+
* `@rebasepro/types`, packages that are installed together.
|
|
12
|
+
*/
|
|
13
|
+
export interface DriverConnection {
|
|
5
14
|
db: unknown;
|
|
6
15
|
/**
|
|
7
16
|
* Present for pool-based drivers. Closed during shutdown, and used to probe
|
|
@@ -31,7 +40,7 @@ export interface InitializedDataSource {
|
|
|
31
40
|
engine: string;
|
|
32
41
|
driverPackage: string;
|
|
33
42
|
bootstrapper: BackendBootstrapper;
|
|
34
|
-
connection:
|
|
43
|
+
connection: DriverConnection;
|
|
35
44
|
}
|
|
36
45
|
export interface BundleSchema {
|
|
37
46
|
tables?: Record<string, unknown>;
|
|
@@ -55,3 +64,8 @@ export declare function initializeDataSource(source: ResolvedDataSourceConfig, s
|
|
|
55
64
|
* order a human would expect.
|
|
56
65
|
*/
|
|
57
66
|
export declare function initializeDataSources(sources: ResolvedDataSourceConfig[], schema: BundleSchema | undefined, resolveFrom?: string[]): Promise<InitializedDataSource[]>;
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated Use {@link DriverConnection}. This name collides with
|
|
69
|
+
* `DatabaseConnection` from `@rebasepro/types`, which is a different shape.
|
|
70
|
+
*/
|
|
71
|
+
export type DatabaseConnection = DriverConnection;
|
package/dist/boot/env.d.ts
CHANGED
|
@@ -78,11 +78,11 @@ declare const bootEnvExtension: z.ZodObject<{
|
|
|
78
78
|
MICROSOFT_CLIENT_ID: z.ZodOptional<z.ZodString>;
|
|
79
79
|
MICROSOFT_CLIENT_SECRET: z.ZodOptional<z.ZodString>;
|
|
80
80
|
REBASE_BASE_PATH: z.ZodDefault<z.ZodString>;
|
|
81
|
-
REBASE_ENABLE_SWAGGER: z.ZodPipe<z.
|
|
81
|
+
REBASE_ENABLE_SWAGGER: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
|
|
82
82
|
"": "";
|
|
83
83
|
true: "true";
|
|
84
84
|
false: "false";
|
|
85
|
-
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
85
|
+
}>>, z.ZodTransform<boolean | undefined, "" | "true" | "false" | undefined>>;
|
|
86
86
|
REBASE_MAX_BODY_SIZE: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
87
87
|
REBASE_COMPRESSION: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
88
88
|
"": "";
|
|
@@ -115,6 +115,20 @@ export declare function loadBootEnv(): RebaseBootEnv;
|
|
|
115
115
|
*/
|
|
116
116
|
export declare function isLocalhostOrigin(origin: string): boolean;
|
|
117
117
|
/** A CORS origin resolver of the shape Hono's `cors()` middleware expects. */
|
|
118
|
+
/**
|
|
119
|
+
* Whether this process serves the OpenAPI docs.
|
|
120
|
+
*
|
|
121
|
+
* An explicit `REBASE_ENABLE_SWAGGER` wins in either direction. Left unset, the
|
|
122
|
+
* docs follow the environment: on in development, where they are part of how a
|
|
123
|
+
* scaffolded project is meant to be explored, and off in production, where the
|
|
124
|
+
* spec enumerates every collection and field to anyone who asks for it.
|
|
125
|
+
*
|
|
126
|
+
* Returning `undefined` for development is the point rather than an oversight —
|
|
127
|
+
* it hands the decision to the server's own policy in `init/docs.ts`, which also
|
|
128
|
+
* knows to withhold the Swagger UI while still serving the spec. Two defaults
|
|
129
|
+
* that can disagree about the same route is the bug this replaces.
|
|
130
|
+
*/
|
|
131
|
+
export declare function resolveEnableSwagger(env: RebaseBootEnv): boolean | undefined;
|
|
118
132
|
export type CorsOriginResolver = (origin: string) => string | null;
|
|
119
133
|
/**
|
|
120
134
|
* Build the CORS origin policy.
|
package/dist/env.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ declare const rebaseEnvSchema: z.ZodObject<{
|
|
|
22
22
|
true: "true";
|
|
23
23
|
false: "false";
|
|
24
24
|
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
25
|
+
DISABLE_SELF_REGISTRATION: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
|
|
26
|
+
"": "";
|
|
27
|
+
true: "true";
|
|
28
|
+
false: "false";
|
|
29
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false" | undefined>>;
|
|
25
30
|
ALLOW_LOCALHOST_IN_PRODUCTION: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
|
|
26
31
|
"": "";
|
|
27
32
|
true: "true";
|
package/dist/index.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export { resolveAuthOptions, resolveEmailOptions } from "./boot/options";
|
|
|
48
48
|
export { envSuffixForKey, assertDistinctSuffixes, loadDeclaredStorageSources, resolveDataSources, resolveStorageSources, resolveStorageBackend } from "./boot/sources";
|
|
49
49
|
export type { ResolvedDataSourceConfig, EnvBag } from "./boot/sources";
|
|
50
50
|
export { initializeDataSource, initializeDataSources } from "./boot/driver";
|
|
51
|
-
export type { InitializedDataSource, DatabaseConnection, BundleSchema } from "./boot/driver";
|
|
51
|
+
export type { InitializedDataSource, DriverConnection, DatabaseConnection, BundleSchema } from "./boot/driver";
|
|
52
52
|
export { MetricsRegistry, createMetricsMiddleware, createMetricsRoutes, classifySurface } from "./metrics";
|
|
53
53
|
export type { MetricSurface, MetricsHandle } from "./metrics";
|
|
54
54
|
export { createContractRoutes } from "./api/contract-routes";
|
package/dist/index.es.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { i as __toESM, n as __exportAll } from "./chunk-DSJWtz9O.js";
|
|
5
|
-
import { C as
|
|
6
|
-
import { _ as toSnakeCase, a as deserializeFilter, c as serializeLogicalCondition, d as CollectionRegistry, f as createDataSourceRegistry, g as buildCompositeId, h as resolveCollectionRelations, i as buildSdkData, l as collectAllPages, m as findRelation, n as serializeOrderBy, o as deserializeLogicalCondition, p as resolveDataSource, r as buildRoutedRebaseData, s as serializeFilter, t as deserializeOrderBy, u as paginateFind } from "./src-
|
|
5
|
+
import { C as EntityReference, D as RebaseApiError, E as Vector, O as RebaseClientError, S as toCanonicalOp, T as GeoPoint, _ as ADMIN_COLLECTION_KEYS, a as findStorageSuffixCollision, c as isSQLAdmin, d as getCollectionDataPath, h as DEFAULT_DATA_SOURCE_KEY, i as DEFAULT_STORAGE_SOURCE_KEY, n as serializeCollections, o as normalizeStorageSources, p as isPostgresCollectionConfig, r as SCHEMA_VERSION_HEADER, s as storageEnvSuffix, t as computeSchemaVersion, v as ADMIN_PROPERTY_KEYS, w as EntityRelation } from "./src-Ivjud8jD.js";
|
|
6
|
+
import { _ as toSnakeCase, a as deserializeFilter, c as serializeLogicalCondition, d as CollectionRegistry, f as createDataSourceRegistry, g as buildCompositeId, h as resolveCollectionRelations, i as buildSdkData, l as collectAllPages, m as findRelation, n as serializeOrderBy, o as deserializeLogicalCondition, p as resolveDataSource, r as buildRoutedRebaseData, s as serializeFilter, t as deserializeOrderBy, u as paginateFind } from "./src-CoOAMnBh.js";
|
|
7
7
|
import { t as logger } from "./logger-BYU66ENZ.js";
|
|
8
8
|
import { a as generateRefreshToken, c as getRefreshTokenTtlMs, d as verifyAccessToken, f as verifyDownloadToken, i as generateDownloadToken, l as hashRefreshToken, n as configureJwt, o as getAccessTokenExpiry, p as require_jsonwebtoken, r as generateAccessToken, s as getRefreshTokenExpiry, t as MAX_COOKIE_AGE_MS } from "./jwt-D-eI6TTu.js";
|
|
9
9
|
import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
|
|
@@ -7714,6 +7714,26 @@ function redactRefreshToken(response, c, refreshToken, config) {
|
|
|
7714
7714
|
};
|
|
7715
7715
|
}
|
|
7716
7716
|
//#endregion
|
|
7717
|
+
//#region src/auth/registration-policy.ts
|
|
7718
|
+
/** The full predicate, for callers that already know whether setup is needed. */
|
|
7719
|
+
function isRegistrationOpen(policy) {
|
|
7720
|
+
if (policy.disableSelfRegistration) return false;
|
|
7721
|
+
return policy.needsSetup || !!policy.allowRegistration;
|
|
7722
|
+
}
|
|
7723
|
+
/**
|
|
7724
|
+
* The same predicate with the bootstrap window assumed closed.
|
|
7725
|
+
*
|
|
7726
|
+
* Exists so `POST /auth/register` can reject the common case without counting
|
|
7727
|
+
* rows. A `false` here does **not** mean "refuse" — it means "the answer depends
|
|
7728
|
+
* on whether the table is empty, so now go and look".
|
|
7729
|
+
*/
|
|
7730
|
+
function isSteadyStateRegistrationOpen(policy) {
|
|
7731
|
+
return isRegistrationOpen({
|
|
7732
|
+
...policy,
|
|
7733
|
+
needsSetup: false
|
|
7734
|
+
});
|
|
7735
|
+
}
|
|
7736
|
+
//#endregion
|
|
7717
7737
|
//#region src/auth/session-routes.ts
|
|
7718
7738
|
function mountSessionRoutes(opts) {
|
|
7719
7739
|
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = opts;
|
|
@@ -7875,13 +7895,34 @@ function mountSessionRoutes(opts) {
|
|
|
7875
7895
|
});
|
|
7876
7896
|
/**
|
|
7877
7897
|
* GET /auth/config
|
|
7878
|
-
* Get public auth configuration
|
|
7898
|
+
* Get public auth configuration.
|
|
7899
|
+
*
|
|
7900
|
+
* ⚠️ SHADOWED on a backend booted through `initializeRebaseBackend`.
|
|
7901
|
+
* `init.ts` registers `${basePath}/auth/config` directly and only mounts
|
|
7902
|
+
* this router afterwards, so Hono resolves that registration first and this
|
|
7903
|
+
* handler never runs. The live implementation is `getCapabilities()` in
|
|
7904
|
+
* `builtin-auth-adapter.ts`.
|
|
7905
|
+
*
|
|
7906
|
+
* Both return `needsSetup` and `registrationEnabled`, so the response shape
|
|
7907
|
+
* cannot tell them apart — which is how a fix for the empty-database dead
|
|
7908
|
+
* end was once applied here, to no effect, while the live copy kept
|
|
7909
|
+
* advertising the wrong answer. `bootstrap-e2e.test.ts` pins which handler
|
|
7910
|
+
* actually answers.
|
|
7911
|
+
*
|
|
7912
|
+
* It is kept because this router is also mounted standalone (tests, and any
|
|
7913
|
+
* embedder that wires `createAuthRoutes` without init.ts). If you change the
|
|
7914
|
+
* registration rule, change it in `registration-policy.ts` — both callers
|
|
7915
|
+
* read it from there, so neither can drift again.
|
|
7879
7916
|
*/
|
|
7880
7917
|
router.get("/config", defaultAuthLimiter, async (c) => {
|
|
7881
7918
|
let needsSetup;
|
|
7882
7919
|
if (config.isBootstrapCompleted) needsSetup = !await config.isBootstrapCompleted();
|
|
7883
7920
|
else needsSetup = (await authRepo.listUsers()).length === 0;
|
|
7884
|
-
const registrationAllowed =
|
|
7921
|
+
const registrationAllowed = isRegistrationOpen({
|
|
7922
|
+
disableSelfRegistration: config.disableSelfRegistration,
|
|
7923
|
+
allowRegistration: config.allowRegistration,
|
|
7924
|
+
needsSetup
|
|
7925
|
+
});
|
|
7885
7926
|
const enabledProviders = (config.oauthProviders || []).map((p) => p.id);
|
|
7886
7927
|
return c.json({
|
|
7887
7928
|
needsSetup,
|
|
@@ -8121,13 +8162,22 @@ function createAuthRoutes(config) {
|
|
|
8121
8162
|
return !!(emailService && emailService.isConfigured());
|
|
8122
8163
|
}
|
|
8123
8164
|
/**
|
|
8124
|
-
*
|
|
8125
|
-
*
|
|
8126
|
-
*
|
|
8165
|
+
* Whether registration is open without consulting the user table.
|
|
8166
|
+
*
|
|
8167
|
+
* The rule lives in `registration-policy.ts` and is shared with both config
|
|
8168
|
+
* endpoints, so what this route enforces and what they advertise cannot
|
|
8169
|
+
* drift apart — which is exactly how the empty-database dead end happened.
|
|
8170
|
+
*
|
|
8171
|
+
* `false` here does not mean "refuse": it means the answer depends on
|
|
8172
|
+
* whether the table is empty, which `POST /auth/register` checks only at
|
|
8173
|
+
* that point, because it serves anonymous callers and a count per rejected
|
|
8174
|
+
* attempt is a free hit on the database.
|
|
8127
8175
|
*/
|
|
8128
8176
|
function isRegistrationAllowed() {
|
|
8129
|
-
|
|
8130
|
-
|
|
8177
|
+
return isSteadyStateRegistrationOpen({
|
|
8178
|
+
disableSelfRegistration: config.disableSelfRegistration,
|
|
8179
|
+
allowRegistration
|
|
8180
|
+
});
|
|
8131
8181
|
}
|
|
8132
8182
|
/**
|
|
8133
8183
|
* Send welcome email to a newly registered user (fire-and-forget).
|
|
@@ -8183,7 +8233,12 @@ function createAuthRoutes(config) {
|
|
|
8183
8233
|
router.post("/register", defaultAuthLimiter, async (c) => {
|
|
8184
8234
|
const { email, password, displayName } = parseBody(registerSchema, await c.req.json());
|
|
8185
8235
|
if (config.disableSelfRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
8186
|
-
|
|
8236
|
+
let bootstrapRegistration = false;
|
|
8237
|
+
if (!isRegistrationAllowed()) {
|
|
8238
|
+
const { total } = await authRepo.listUsersPaginated({ limit: 1 });
|
|
8239
|
+
bootstrapRegistration = total === 0;
|
|
8240
|
+
if (!bootstrapRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
8241
|
+
}
|
|
8187
8242
|
const passwordValidation = ops.validatePasswordStrength(password);
|
|
8188
8243
|
if (!passwordValidation.valid) throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
8189
8244
|
if (await authRepo.getUserByEmail(email)) throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
|
|
@@ -8196,7 +8251,12 @@ function createAuthRoutes(config) {
|
|
|
8196
8251
|
if (ops.beforeUserCreate) createData = await ops.beforeUserCreate(createData);
|
|
8197
8252
|
const user = await authRepo.createUser(createData);
|
|
8198
8253
|
const existingUsers = await authRepo.listUsers();
|
|
8199
|
-
|
|
8254
|
+
const isFirstUser = existingUsers.length === 1 && existingUsers[0].id === user.id;
|
|
8255
|
+
if (bootstrapRegistration && !isFirstUser) {
|
|
8256
|
+
await authRepo.deleteUser(user.id);
|
|
8257
|
+
throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
8258
|
+
}
|
|
8259
|
+
if (isFirstUser) await authRepo.setUserRoles(user.id, ["admin"]);
|
|
8200
8260
|
else if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
8201
8261
|
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(user.id, c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown");
|
|
8202
8262
|
sendWelcomeEmail({
|
|
@@ -8936,7 +8996,7 @@ function createAdminUsersRoute(config) {
|
|
|
8936
8996
|
* when the user passes a plain `RebaseAuthConfig` object.
|
|
8937
8997
|
*/
|
|
8938
8998
|
function createBuiltinAuthAdapter(config) {
|
|
8939
|
-
const { authRepository, emailService, emailConfig, allowRegistration = false, allowUserLookup = false, defaultRole, oauthProviders = [], serviceKey, authHooks, collectionAuthConfig, enableMagicLink = false, cookieAuth } = config;
|
|
8999
|
+
const { authRepository, emailService, emailConfig, allowRegistration = false, disableSelfRegistration = false, allowUserLookup = false, defaultRole, oauthProviders = [], serviceKey, authHooks, collectionAuthConfig, enableMagicLink = false, cookieAuth } = config;
|
|
8940
9000
|
const resolvedOps = resolveAuthHooks(authHooks);
|
|
8941
9001
|
return {
|
|
8942
9002
|
id: "rebase-builtin",
|
|
@@ -9009,6 +9069,7 @@ function createBuiltinAuthAdapter(config) {
|
|
|
9009
9069
|
emailService,
|
|
9010
9070
|
emailConfig,
|
|
9011
9071
|
allowRegistration,
|
|
9072
|
+
disableSelfRegistration,
|
|
9012
9073
|
allowUserLookup,
|
|
9013
9074
|
defaultRole,
|
|
9014
9075
|
oauthProviders,
|
|
@@ -9078,11 +9139,16 @@ function createBuiltinAuthAdapter(config) {
|
|
|
9078
9139
|
needsSetup = (await authRepository.listUsersPaginated({ limit: 1 })).total === 0;
|
|
9079
9140
|
} catch {}
|
|
9080
9141
|
const enabledProviders = oauthProviders.map((p) => p.id);
|
|
9142
|
+
const registrationAllowed = isRegistrationOpen({
|
|
9143
|
+
disableSelfRegistration,
|
|
9144
|
+
allowRegistration,
|
|
9145
|
+
needsSetup
|
|
9146
|
+
});
|
|
9081
9147
|
return {
|
|
9082
9148
|
hasBuiltInAuthRoutes: true,
|
|
9083
9149
|
emailPasswordLogin: true,
|
|
9084
|
-
registration:
|
|
9085
|
-
registrationEnabled:
|
|
9150
|
+
registration: registrationAllowed,
|
|
9151
|
+
registrationEnabled: registrationAllowed,
|
|
9086
9152
|
passwordReset: !!emailService?.isConfigured(),
|
|
9087
9153
|
adminPasswordReset: true,
|
|
9088
9154
|
sessionManagement: true,
|
|
@@ -12374,7 +12440,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
12374
12440
|
//#region src/init/docs.ts
|
|
12375
12441
|
async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
|
|
12376
12442
|
if (enableSwagger === false || activeCollections.length === 0) return;
|
|
12377
|
-
const { generateOpenApiSpec } = await import("./openapi-generator-
|
|
12443
|
+
const { generateOpenApiSpec } = await import("./openapi-generator-Bjzmb5cn.js");
|
|
12378
12444
|
app.get(`${basePath}/docs`, (c) => {
|
|
12379
12445
|
const spec = generateOpenApiSpec(activeCollections, {
|
|
12380
12446
|
basePath,
|
|
@@ -18393,6 +18459,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
18393
18459
|
emailService: authConfigResult.emailService,
|
|
18394
18460
|
emailConfig: safeAuthConfig.email,
|
|
18395
18461
|
allowRegistration: safeAuthConfig.allowRegistration ?? false,
|
|
18462
|
+
disableSelfRegistration: safeAuthConfig.disableSelfRegistration ?? false,
|
|
18396
18463
|
allowUserLookup: safeAuthConfig.allowUserLookup ?? false,
|
|
18397
18464
|
defaultRole: safeAuthConfig.defaultRole,
|
|
18398
18465
|
oauthProviders,
|
|
@@ -18435,7 +18502,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
18435
18502
|
if (schemaEditorEnabled && config.collectionsDir) {
|
|
18436
18503
|
let editorModule;
|
|
18437
18504
|
try {
|
|
18438
|
-
editorModule = await import("./schema-editor-routes-
|
|
18505
|
+
editorModule = await import("./schema-editor-routes-DDxfOIid.js");
|
|
18439
18506
|
} catch (err) {
|
|
18440
18507
|
if (err?.code === "ERR_MODULE_NOT_FOUND") logger.warn("Schema Editor disabled: its dependency ts-morph is not installed. Run `npm install ts-morph@28.0.0` to enable it.");
|
|
18441
18508
|
else throw err;
|
|
@@ -19982,6 +20049,7 @@ var rebaseEnvSchema = object({
|
|
|
19982
20049
|
GOOGLE_CLIENT_SECRET: string().optional(),
|
|
19983
20050
|
REBASE_SERVICE_KEY: string().optional(),
|
|
19984
20051
|
ALLOW_REGISTRATION: boolString,
|
|
20052
|
+
DISABLE_SELF_REGISTRATION: optionalBoolString,
|
|
19985
20053
|
ALLOW_LOCALHOST_IN_PRODUCTION: optionalBoolString,
|
|
19986
20054
|
CORS_ORIGINS: string().optional(),
|
|
19987
20055
|
FRONTEND_URL: string().optional(),
|
|
@@ -20727,11 +20795,27 @@ var bootEnvExtension = object({
|
|
|
20727
20795
|
MICROSOFT_CLIENT_ID: string().optional(),
|
|
20728
20796
|
MICROSOFT_CLIENT_SECRET: string().optional(),
|
|
20729
20797
|
REBASE_BASE_PATH: string().default("/api"),
|
|
20798
|
+
/**
|
|
20799
|
+
* The OpenAPI surface: `/api/docs` (the spec) and `/api/swagger` (the UI).
|
|
20800
|
+
*
|
|
20801
|
+
* Deliberately tri-state, and resolved against NODE_ENV by
|
|
20802
|
+
* {@link resolveEnableSwagger} rather than defaulted here. Unset means "on
|
|
20803
|
+
* in development, off in production" — an explicit `true` or `false` always
|
|
20804
|
+
* wins in both.
|
|
20805
|
+
*
|
|
20806
|
+
* It used to default to `"false"` outright, which reads as a safe default
|
|
20807
|
+
* and was not one: the runtime is how every scaffolded project boots, so
|
|
20808
|
+
* the docs disappeared from projects that never asked for that. `rebase
|
|
20809
|
+
* init` prints "docs are at /api/swagger" on completion, the headless
|
|
20810
|
+
* README repeats it, and the console's API Explorer fetches `/api/docs` —
|
|
20811
|
+
* all three 404'd against a project running the runtime, and the baas e2e
|
|
20812
|
+
* failed on exactly that.
|
|
20813
|
+
*/
|
|
20730
20814
|
REBASE_ENABLE_SWAGGER: _enum([
|
|
20731
20815
|
"true",
|
|
20732
20816
|
"false",
|
|
20733
20817
|
""
|
|
20734
|
-
]).
|
|
20818
|
+
]).optional().transform((v) => v === void 0 || v === "" ? void 0 : v === "true"),
|
|
20735
20819
|
/**
|
|
20736
20820
|
* Maximum request body size, in **bytes**.
|
|
20737
20821
|
*
|
|
@@ -20791,6 +20875,24 @@ function isLocalhostOrigin(origin) {
|
|
|
20791
20875
|
return false;
|
|
20792
20876
|
}
|
|
20793
20877
|
}
|
|
20878
|
+
/** A CORS origin resolver of the shape Hono's `cors()` middleware expects. */
|
|
20879
|
+
/**
|
|
20880
|
+
* Whether this process serves the OpenAPI docs.
|
|
20881
|
+
*
|
|
20882
|
+
* An explicit `REBASE_ENABLE_SWAGGER` wins in either direction. Left unset, the
|
|
20883
|
+
* docs follow the environment: on in development, where they are part of how a
|
|
20884
|
+
* scaffolded project is meant to be explored, and off in production, where the
|
|
20885
|
+
* spec enumerates every collection and field to anyone who asks for it.
|
|
20886
|
+
*
|
|
20887
|
+
* Returning `undefined` for development is the point rather than an oversight —
|
|
20888
|
+
* it hands the decision to the server's own policy in `init/docs.ts`, which also
|
|
20889
|
+
* knows to withhold the Swagger UI while still serving the spec. Two defaults
|
|
20890
|
+
* that can disagree about the same route is the bug this replaces.
|
|
20891
|
+
*/
|
|
20892
|
+
function resolveEnableSwagger(env) {
|
|
20893
|
+
if (env.REBASE_ENABLE_SWAGGER !== void 0) return env.REBASE_ENABLE_SWAGGER;
|
|
20894
|
+
return env.NODE_ENV === "production" ? false : void 0;
|
|
20895
|
+
}
|
|
20794
20896
|
/**
|
|
20795
20897
|
* Build the CORS origin policy.
|
|
20796
20898
|
*
|
|
@@ -21241,6 +21343,7 @@ function resolveAuthOptions(env, usersCollection) {
|
|
|
21241
21343
|
serviceKey: env.REBASE_SERVICE_KEY,
|
|
21242
21344
|
requireAuth: env.AUTH_REQUIRE,
|
|
21243
21345
|
allowRegistration: env.ALLOW_REGISTRATION,
|
|
21346
|
+
disableSelfRegistration: env.DISABLE_SELF_REGISTRATION,
|
|
21244
21347
|
allowUserLookup: env.AUTH_ALLOW_USER_LOOKUP,
|
|
21245
21348
|
email: resolveEmailOptions(env),
|
|
21246
21349
|
cookieAuth: { sameSite: env.AUTH_COOKIE_SAME_SITE || "Lax" }
|
|
@@ -21578,7 +21681,7 @@ async function bootFromBundle(options = {}) {
|
|
|
21578
21681
|
callbacks: configExports.callbacks,
|
|
21579
21682
|
auth: resolveAuthOptions(env, usersCollection),
|
|
21580
21683
|
history: env.REBASE_HISTORY,
|
|
21581
|
-
enableSwagger: env
|
|
21684
|
+
enableSwagger: resolveEnableSwagger(env),
|
|
21582
21685
|
compression: env.REBASE_COMPRESSION,
|
|
21583
21686
|
maxBodySize: env.REBASE_MAX_BODY_SIZE,
|
|
21584
21687
|
logging: env.LOG_LEVEL ? { level: env.LOG_LEVEL } : void 0,
|