@rebasepro/server 0.11.0 → 0.11.1-canary.g16c8254

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.
@@ -29,7 +29,6 @@ export interface ContractRoutesConfig {
29
29
  * at boot, so there was nothing to hash when it was built.
30
30
  */
31
31
  schemaVersion?: string;
32
- mode: "cms" | "baas";
33
32
  /** Runtime package version, surfaced so a client can report what it built against. */
34
33
  runtimeVersion?: string;
35
34
  }
@@ -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
- }
@@ -13,10 +13,23 @@ export interface LoadedBundle {
13
13
  collectionsDir?: string;
14
14
  functionsDir?: string;
15
15
  cronsDir?: string;
16
- /** Absolute path to built admin assets, when the admin panel is bundled. */
17
- adminDir?: string;
18
- /** Absolute path to built static assets to serve from this process. */
19
- staticDir?: string;
16
+ /**
17
+ * Built static apps to serve from this process, in mount order.
18
+ *
19
+ * A list, not a single directory: one process serves a site at `/` and an
20
+ * admin at `/admin`. Entries whose directory is missing are dropped with a
21
+ * warning, so a partially-built bundle still boots its API.
22
+ */
23
+ staticApps: LoadedStaticApp[];
24
+ }
25
+ /** One built static app inside a loaded bundle, with an absolute directory. */
26
+ export interface LoadedStaticApp {
27
+ /** Public base path, e.g. `/` or `/admin`. */
28
+ path: string;
29
+ /** Absolute path to the built assets. */
30
+ dir: string;
31
+ /** Serve `index.html` for unmatched paths under `path`. */
32
+ spa: boolean;
20
33
  }
21
34
  /**
22
35
  * Read and validate a bundle's manifest.
@@ -83,7 +96,6 @@ export declare function createSourceBundle(options: {
83
96
  functions?: string;
84
97
  crons?: string;
85
98
  schema?: string;
86
- mode?: "cms" | "baas" | "static";
87
99
  app?: string;
88
100
  }): LoadedBundle;
89
101
  /**
@@ -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;
@@ -30,8 +30,21 @@ export type EnvBag = Record<string, string | undefined>;
30
30
  *
31
31
  * The default key maps to no suffix at all, which is what keeps every existing
32
32
  * single-database deployment working untouched.
33
+ *
34
+ * The rule itself lives in `@rebasepro/types` so the CLI and any control plane
35
+ * derive identical names from identical keys; this wrapper exists only to raise
36
+ * it as a `BundleError`, which is what the rest of boot reports failures as.
33
37
  */
34
38
  export declare function envSuffixForKey(key: string, defaultKey: string): string;
39
+ /**
40
+ * Guard against two distinct keys collapsing onto the same variable name.
41
+ *
42
+ * `media-cdn` and `media_cdn` are different source keys but the same suffix, and
43
+ * without this check one of them would silently read the other's configuration.
44
+ */
45
+ export declare function assertDistinctSuffixes(definitions: {
46
+ key: string;
47
+ }[], defaultKey: string, what: string): void;
35
48
  /** A data source resolved to everything needed to build a driver for it. */
36
49
  export interface ResolvedDataSourceConfig {
37
50
  /** Data-source key — becomes the driver-registry id collections route by. */
@@ -75,3 +88,21 @@ export declare function resolveStorageBackend(env: EnvBag, key: string, engineHi
75
88
  * they just had no way to be configured from the environment.
76
89
  */
77
90
  export declare function resolveStorageSources(env: EnvBag, definitions: StorageSourceDefinition[] | undefined, defaultBasePath: string): Record<string, BackendStorageConfig> | undefined;
91
+ /**
92
+ * Read a project's declared storage sources from its `rebase.json`.
93
+ *
94
+ * A managed bundle carries its topology in `manifest.json`, resolved at build
95
+ * time. A **custom** runtime has no manifest — it builds its own image and its
96
+ * own entrypoint — so without this it would have to re-declare in code what
97
+ * `rebase.json` already says, and the two would drift. Since a custom image
98
+ * contains the repository anyway, reading the file it already ships is what
99
+ * keeps one declaration authoritative for both runtimes.
100
+ *
101
+ * Walks up from `startDir` because an entrypoint lives at `backend/src` in the
102
+ * scaffolded layout and somewhere else in a hand-rolled one. A missing,
103
+ * unreadable or malformed file means "declared nothing" — one default source —
104
+ * which is the correct reading of every project that predates this and must
105
+ * never be an error: a storage declaration is optional, and failing to boot a
106
+ * whole backend over an absent optional file would be the worse bug.
107
+ */
108
+ export declare function loadDeclaredStorageSources(startDir: string, levels?: number): StorageSourceDefinition[];
@@ -1,4 +1,5 @@
1
1
  import { CollectionConfig, SecurityRule } from "@rebasepro/types";
2
+ import { type ValidateCollectionConfigOptions } from "./validate-config";
2
3
  /**
3
4
  * The one definition of "the collections".
4
5
  *
@@ -35,5 +36,14 @@ export declare function applyCollectionDefaults(collections: CollectionConfig[],
35
36
  * configuration error, and continuing produces the worst outcome available: an
36
37
  * API missing a route, or a policy file missing a table, with a successful exit
37
38
  * code. Both read as "no data" rather than as a failure.
39
+ *
40
+ * Every collection is strict-parsed on the way out — see `validate-config` for
41
+ * why a key that moved is fatal and a key nobody recognises only warns. It
42
+ * happens here, at the one definition of "the collections", so the runtime, the
43
+ * schema generator, the policy generator and the doctor all see the same
44
+ * verdict rather than three of them silently accepting a config the fourth
45
+ * rejects.
38
46
  */
39
- export declare function loadCollectionsFromDirectory(source: string): Promise<CollectionConfig[]>;
47
+ export declare function loadCollectionsFromDirectory(source: string, options?: {
48
+ validate?: false | ValidateCollectionConfigOptions;
49
+ }): Promise<CollectionConfig[]>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A strict parse of every collection config, run at boot.
3
+ *
4
+ * Nothing used to check these files. A config written against an older version
5
+ * loaded clean, and whichever keys had moved since were simply ignored — no
6
+ * warning, no log line, no failed boot. The collection still served rows, so
7
+ * the only signal was the feature quietly not being there: an icon that never
8
+ * appeared, a relation that answered `[]`, a `readOnly` field the panel let you
9
+ * edit. The renames are not the problem; a rename with no runtime signal is.
10
+ *
11
+ * Two severities, because two different things are being detected:
12
+ *
13
+ * - A **known-removed or known-renamed key** is high-confidence and actionable —
14
+ * we know what it used to mean and what replaced it. That is an error, and
15
+ * refusing to boot is the point. A minute of downtime beats a week of "where
16
+ * did my icons go".
17
+ * - An **unrecognised key** is not. Configs legitimately carry extra metadata,
18
+ * and a key we do not know may simply be newer than this list. That warns,
19
+ * loudly, and escalates to an error only when asked
20
+ * (`REBASE_STRICT_COLLECTION_CONFIG=error`, or an explicit option).
21
+ *
22
+ * Everything is reported in one pass. Someone migrating a project wants the
23
+ * whole list once, not fifty-five sequential boots.
24
+ *
25
+ * This is not `validateCollectionJson` in `@rebasepro/admin`. That one parses a
26
+ * JSON string pasted into the panel's import dialog and checks value *shapes*
27
+ * against the flat `AdminCollection` view model. This one checks key *identity*
28
+ * against the authoring contract, on live objects, in a package that may not
29
+ * import the admin. The two answer different questions about different types,
30
+ * and merging them would mean the server depending on the admin panel.
31
+ */
32
+ /** How an unrecognised key is treated. */
33
+ export type UnknownKeyPolicy = "warn" | "error" | "off";
34
+ export interface ConfigProblem {
35
+ severity: "error" | "warning";
36
+ /** Dotted path into the config, e.g. `posts.properties.author`. */
37
+ path: string;
38
+ message: string;
39
+ }
40
+ export interface ValidateCollectionConfigOptions {
41
+ /**
42
+ * What to do with a key that is in no known list. Defaults to the
43
+ * `REBASE_STRICT_COLLECTION_CONFIG` environment variable, and to `"warn"`
44
+ * when that is unset.
45
+ */
46
+ unknownKeys?: UnknownKeyPolicy;
47
+ }
48
+ /**
49
+ * Read the unknown-key policy from the environment.
50
+ *
51
+ * `REBASE_STRICT_COLLECTION_CONFIG` accepts `error`/`strict`/`1`/`true` to
52
+ * escalate, `off`/`0`/`false` to silence, and anything else warns.
53
+ */
54
+ export declare function unknownKeyPolicyFromEnv(env?: Record<string, string | undefined>): UnknownKeyPolicy;
55
+ /**
56
+ * Every problem across every collection, in one pass.
57
+ *
58
+ * Pure: it logs nothing and throws nothing, so callers that want to render the
59
+ * list themselves (the doctor, a test) can.
60
+ */
61
+ export declare function findCollectionConfigProblems(collections: readonly unknown[], options?: ValidateCollectionConfigOptions): ConfigProblem[];
62
+ /**
63
+ * Warn about everything questionable, then refuse to boot if anything is wrong.
64
+ *
65
+ * Warnings are logged even when there are errors: someone migrating wants the
66
+ * whole picture in one run, and the second-most annoying thing after a broken
67
+ * boot is a boot that breaks again on something it could have told you the
68
+ * first time.
69
+ */
70
+ export declare function assertCollectionConfigs(collections: readonly unknown[], options?: ValidateCollectionConfigOptions): void;
@@ -77,6 +77,15 @@ export declare class CronScheduler {
77
77
  * a `skipped: true` result rather than running concurrently.
78
78
  */
79
79
  triggerJob(id: string): Promise<CronJobLogEntry | undefined>;
80
+ /**
81
+ * Warn once at start when the process looks like it is running on a
82
+ * platform that freezes or evicts instances between requests, where the
83
+ * in-process timers this scheduler relies on never fire.
84
+ *
85
+ * Advisory only: any failure here is swallowed so a detection bug can
86
+ * never take a production boot down.
87
+ */
88
+ private warnIfScaleToZero;
80
89
  /**
81
90
  * Schedule the next execution for a job.
82
91
  *
@@ -4,4 +4,5 @@ export { defineCron } from "./define-cron";
4
4
  export { CronScheduler, validateCronExpression } from "./cron-scheduler";
5
5
  export { createCronRoutes } from "./cron-routes";
6
6
  export { createCronStore } from "./cron-store";
7
+ export { detectFreezableRuntime, buildScaleToZeroWarning, CRON_ALWAYS_ON_ENV } from "./scale-to-zero";
7
8
  export type { CronStore } from "./cron-store";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Scale-to-zero detection for the cron scheduler.
3
+ *
4
+ * The scheduler drives jobs with in-process `setTimeout`. That works on any
5
+ * always-running instance, but on a platform that freezes or evicts the
6
+ * container between requests (Cloud Run with `--min-instances=0`, AWS Lambda,
7
+ * Vercel functions) the timers simply never fire — the process boots, logs the
8
+ * jobs as registered, and silently runs nothing.
9
+ *
10
+ * None of these platforms expose their scaling floor to the container, so this
11
+ * detection is a heuristic: it identifies the *platform*, not the setting. It
12
+ * is a warning only — it must never influence boot.
13
+ *
14
+ * Environment variables used here were verified against vendor documentation:
15
+ * - `K_SERVICE` / `K_REVISION` / `K_CONFIGURATION` — Cloud Run services
16
+ * (Cloud Run container contract; no variable exposes min-instances).
17
+ * - `CLOUD_RUN_JOB` — Cloud Run jobs (same contract).
18
+ * - `AWS_LAMBDA_FUNCTION_NAME` — reserved AWS Lambda runtime variable.
19
+ * - `VERCEL=1` — Vercel system environment variable, available at runtime.
20
+ * - `KUBERNETES_SERVICE_HOST` — injected into every pod by the kubelet. Used
21
+ * as an *exclusion*: a Deployment pod runs continuously, and Knative on
22
+ * Kubernetes also sets `K_SERVICE`, so a pod is never warned about.
23
+ */
24
+ /** Environment variable that permanently silences the scale-to-zero warning. */
25
+ export declare const CRON_ALWAYS_ON_ENV = "REBASE_CRON_ALWAYS_ON";
26
+ /** The subset of `process.env` this module reads. */
27
+ export type EnvLike = Record<string, string | undefined>;
28
+ export interface FreezableRuntime {
29
+ /** Human-readable platform name, used verbatim in the warning. */
30
+ platform: string;
31
+ /** Names of the environment variables that identified the platform. */
32
+ signals: string[];
33
+ }
34
+ /** Minimal shape of a registered job needed to build the warning. */
35
+ export interface WarnableJob {
36
+ id: string;
37
+ enabled: boolean;
38
+ }
39
+ export interface ScaleToZeroWarning {
40
+ message: string;
41
+ data: Record<string, unknown>;
42
+ }
43
+ /**
44
+ * Identify a runtime whose instances can be frozen or torn down between
45
+ * requests. Returns `undefined` when the platform is unknown or known to run
46
+ * continuously.
47
+ */
48
+ export declare function detectFreezableRuntime(env?: EnvLike): FreezableRuntime | undefined;
49
+ /**
50
+ * Build the boot-time warning, or `undefined` when it does not apply.
51
+ *
52
+ * Fires only when all of the following hold:
53
+ * 1. `NODE_ENV=production` — a laptop or CI run is not at risk.
54
+ * 2. At least one *enabled* job is registered — nothing to lose otherwise.
55
+ * 3. The environment looks like a freezable platform (see above).
56
+ * 4. `REBASE_CRON_ALWAYS_ON` is not set to a truthy value.
57
+ */
58
+ export declare function buildScaleToZeroWarning(jobs: WarnableJob[], env?: EnvLike): ScaleToZeroWarning | undefined;
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export { initializeRebaseBackend, isAuthAdapter, isDatabaseAdapter } from "./ini
10
10
  export type { RebaseBackendConfig, RebaseBackendInstance, RebaseAuthConfig, BaasOptions } from "./init";
11
11
  export { rebase, _setRebaseMock, _resetRebaseMock } from "./singleton";
12
12
  export { loadCollectionsFromDirectory, applyCollectionDefaults, type CollectionDefaults } from "./collections/loader";
13
+ export { assertCollectionConfigs, findCollectionConfigProblems, unknownKeyPolicyFromEnv, type ConfigProblem, type UnknownKeyPolicy, type ValidateCollectionConfigOptions } from "./collections/validate-config";
13
14
  export * from "./db/interfaces";
14
15
  export * from "./auth/interfaces";
15
16
  export { requireAuth, requireAdmin, optionalAuth, queryTokenAuth, fileTokenAuth, extractUserFromToken, hashPassword, verifyPassword, validatePasswordStrength, generateSecurePassword, resolveAuthHooks, createBuiltinAuthAdapter, createCustomAuthAdapter, createGoogleProvider, createLinkedinProvider, createGitHubProvider, createMicrosoftProvider, createAppleProvider, createFacebookProvider, createTwitterProvider, createDiscordProvider, createGitLabProvider, createBitbucketProvider, createSlackProvider, createSpotifyProvider, isApiKeyToken, validateApiKey, httpMethodToOperation, isOperationAllowed, safeCompare } from "./auth";
@@ -32,6 +33,7 @@ export { loadEnv } from "./env";
32
33
  export type { RebaseEnv } from "./env";
33
34
  export * from "./types";
34
35
  export * from "./services/driver-registry";
36
+ export * from "./services/webhook-service";
35
37
  export { cleanupDevPortFile, listenWithPortRetry } from "./utils/dev-port";
36
38
  export { serveSPA } from "./serve-spa";
37
39
  export { installShutdownHandlers } from "./init/shutdown";
@@ -43,10 +45,10 @@ export type { LoadedBundle, BundleConfigExports } from "./boot/bundle";
43
45
  export { loadBootEnv, resolveCorsOrigin, isLocalhostOrigin } from "./boot/env";
44
46
  export type { RebaseBootEnv, CorsOriginResolver } from "./boot/env";
45
47
  export { resolveAuthOptions, resolveEmailOptions } from "./boot/options";
46
- export { envSuffixForKey, resolveDataSources, resolveStorageSources, resolveStorageBackend } from "./boot/sources";
48
+ export { envSuffixForKey, assertDistinctSuffixes, loadDeclaredStorageSources, resolveDataSources, resolveStorageSources, resolveStorageBackend } from "./boot/sources";
47
49
  export type { ResolvedDataSourceConfig, EnvBag } from "./boot/sources";
48
50
  export { initializeDataSource, initializeDataSources } from "./boot/driver";
49
- export type { InitializedDataSource, DatabaseConnection, BundleSchema } from "./boot/driver";
51
+ export type { InitializedDataSource, DriverConnection, DatabaseConnection, BundleSchema } from "./boot/driver";
50
52
  export { MetricsRegistry, createMetricsMiddleware, createMetricsRoutes, classifySurface } from "./metrics";
51
53
  export type { MetricSurface, MetricsHandle } from "./metrics";
52
54
  export { createContractRoutes } from "./api/contract-routes";