@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.
@@ -1,26 +1,15 @@
1
1
  /**
2
2
  * Type definitions for Service API Keys.
3
3
  *
4
- * API keys provide machine-to-machine authentication for scripts, cron jobs,
5
- * and third-party integrations. Each key is scoped to specific collections
6
- * and operations via the `ApiKeyPermission` model.
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
- * A single permission entry scoping an API key to a collection and set of operations.
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;
@@ -1,7 +1,16 @@
1
1
  import type { BackendBootstrapper } from "@rebasepro/types";
2
2
  import type { ResolvedDataSourceConfig } from "./sources";
3
- /** The connection handle a driver hands back. */
4
- export interface DatabaseConnection {
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: DatabaseConnection;
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;
@@ -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.ZodDefault<z.ZodEnum<{
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 EntityRelation, D as RebaseClientError, E as RebaseApiError, S as EntityReference, T as Vector, _ as ADMIN_PROPERTY_KEYS, a as findStorageSuffixCollision, c as DEFAULT_DATA_SOURCE_KEY, g as ADMIN_COLLECTION_KEYS, h as isPostgresCollectionConfig, i as DEFAULT_STORAGE_SOURCE_KEY, n as serializeCollections, o as normalizeStorageSources, p as getCollectionDataPath, r as SCHEMA_VERSION_HEADER, s as storageEnvSuffix, t as computeSchemaVersion, u as isSQLAdmin, w as GeoPoint, x as toCanonicalOp } from "./src-DzAgLF8X.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-CyC3JXAq.js";
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 = needsSetup || !!config.allowRegistration;
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
- * Check if registration is allowed.
8125
- * Registration is only allowed when explicitly enabled via `allowRegistration`.
8126
- * First-user bootstrap must use POST /admin/bootstrap instead.
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
- if (config.disableSelfRegistration) return false;
8130
- return !!allowRegistration;
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
- if (!isRegistrationAllowed()) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
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
- if (existingUsers.length === 1 && existingUsers[0].id === user.id) await authRepo.setUserRoles(user.id, ["admin"]);
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: allowRegistration || needsSetup,
9085
- registrationEnabled: allowRegistration || needsSetup,
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-DUocM036.js");
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-CbKEoeiH.js");
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
- ]).default("false").transform((v) => v === "true"),
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.REBASE_ENABLE_SWAGGER,
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,