@rebasepro/server 0.10.0 → 0.10.1-canary.14e53ae
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/bin/rebase-server.js +55 -0
- package/dist/api/contract-routes.d.ts +36 -0
- package/dist/api/rest/api-generator.d.ts +9 -1
- package/dist/api/rest/query-parser.d.ts +17 -1
- package/dist/auth/interfaces.d.ts +65 -2
- package/dist/auth/jwt.d.ts +17 -0
- package/dist/auth/routes.d.ts +16 -0
- package/dist/boot/boot.d.ts +59 -0
- package/dist/boot/bundle.d.ts +134 -0
- package/dist/boot/driver.d.ts +57 -0
- package/dist/boot/env.d.ts +129 -0
- package/dist/boot/options.d.ts +20 -0
- package/dist/boot/sources.d.ts +77 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.es.js +6263 -2286
- package/dist/index.es.js.map +1 -1
- package/dist/init/middlewares.d.ts +9 -0
- package/dist/init/storage.d.ts +27 -0
- package/dist/init.d.ts +43 -0
- package/dist/{jwt-B3zjddCa.js → jwt-Dj7r7QX7.js} +36 -24
- package/dist/{jwt-B3zjddCa.js.map → jwt-Dj7r7QX7.js.map} +1 -1
- package/dist/metrics/index.d.ts +83 -0
- package/dist/{src-CsHhSKbi.js → src-BITicbgD.js} +172 -2
- package/dist/src-BITicbgD.js.map +1 -0
- package/dist/storage/routes.d.ts +9 -1
- package/dist/storage/types.d.ts +1 -32
- package/package.json +9 -5
- package/dist/src-CsHhSKbi.js.map +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The Rebase runtime entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* Runs a built project bundle. This is what the official `rebasepro/server`
|
|
6
|
+
* container image executes, and what a self-hosted deployment runs directly:
|
|
7
|
+
*
|
|
8
|
+
* rebase-server ./dist-bundle
|
|
9
|
+
* REBASE_BUNDLE=/bundle rebase-server
|
|
10
|
+
*
|
|
11
|
+
* The bundle is the project; this process is the engine. Keeping them separate
|
|
12
|
+
* is what allows the engine to be upgraded — a security patch, a performance
|
|
13
|
+
* fix — without rebuilding anyone's application.
|
|
14
|
+
*/
|
|
15
|
+
import { runFromBundle } from "../dist/index.es.js";
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
20
|
+
console.log(`
|
|
21
|
+
rebase-server — run a built Rebase project bundle
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
rebase-server [bundle-dir]
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
bundle-dir Path to the bundle. Defaults to $REBASE_BUNDLE, then ./dist-bundle
|
|
28
|
+
|
|
29
|
+
Key environment variables:
|
|
30
|
+
DATABASE_URL Connection string for the default database (required)
|
|
31
|
+
JWT_SECRET Signing secret, >=32 chars (required in production)
|
|
32
|
+
PORT Port to bind (default 3001)
|
|
33
|
+
CORS_ORIGINS Comma-separated allowed origins (required in production)
|
|
34
|
+
REBASE_METRICS "true" to expose Prometheus metrics at /metrics
|
|
35
|
+
REBASE_MIGRATE_ON_BOOT none | ensure (collection tables: run 'rebase db push')
|
|
36
|
+
|
|
37
|
+
Additional databases and buckets are configured by suffixing the variable with
|
|
38
|
+
the source key, e.g. DATABASE_URL__ANALYTICS or S3_BUCKET__MEDIA.
|
|
39
|
+
|
|
40
|
+
Docs: https://rebase.pro/docs/deployment/self-hosting/
|
|
41
|
+
`.trim());
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
46
|
+
const { readFileSync } = await import("node:fs");
|
|
47
|
+
const { fileURLToPath } = await import("node:url");
|
|
48
|
+
const { dirname, join } = await import("node:path");
|
|
49
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
50
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
|
|
51
|
+
console.log(pkg.version);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await runFromBundle({ bundleDir: args[0] });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { type CollectionConfig } from "@rebasepro/types";
|
|
3
|
+
import type { HonoEnv } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* The project contract endpoint.
|
|
6
|
+
*
|
|
7
|
+
* This is what makes a repository able to build against a project it does not
|
|
8
|
+
* contain. Without it, a typed client can only be generated from local
|
|
9
|
+
* collection *source*, which means every frontend must live in the same
|
|
10
|
+
* repository as the backend. Serving the contract turns that around: an app
|
|
11
|
+
* asks the project what its shape is, so a web app, a second web app and a
|
|
12
|
+
* mobile app can each live wherever they like and none of them needs to know
|
|
13
|
+
* about the others.
|
|
14
|
+
*
|
|
15
|
+
* Admin-gated. Collection definitions describe every table, column and relation
|
|
16
|
+
* in the project, including ones no security rule would ever expose — that is a
|
|
17
|
+
* map of the database, not public API documentation.
|
|
18
|
+
*/
|
|
19
|
+
export interface ContractRoutesConfig {
|
|
20
|
+
collectionRegistry: {
|
|
21
|
+
getRawCollections(): CollectionConfig[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The schema version recorded at build time.
|
|
25
|
+
*
|
|
26
|
+
* Preferred over recomputing, so that what a client is told matches exactly
|
|
27
|
+
* what the bundle claims. It is recomputed only when a bundle did not record
|
|
28
|
+
* one — a `baas`-mode project derives its collections from the live database
|
|
29
|
+
* at boot, so there was nothing to hash when it was built.
|
|
30
|
+
*/
|
|
31
|
+
schemaVersion?: string;
|
|
32
|
+
mode: "cms" | "baas";
|
|
33
|
+
/** Runtime package version, surfaced so a client can report what it built against. */
|
|
34
|
+
runtimeVersion?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function createContractRoutes(config: ContractRoutesConfig): Hono<HonoEnv>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { AuthAdapter, DataDriver, CollectionConfig } from "@rebasepro/types";
|
|
3
3
|
import { HonoEnv } from "../types";
|
|
4
|
+
import { type ListLimitOptions } from "./query-parser";
|
|
4
5
|
/**
|
|
5
6
|
* Lightweight REST API generator that leverages existing Rebase DataDriver.
|
|
6
7
|
* Supports `include` query parameter for eager-loading relations via Drizzle.
|
|
@@ -12,8 +13,15 @@ export declare class RestApiGenerator {
|
|
|
12
13
|
private router;
|
|
13
14
|
private driver;
|
|
14
15
|
private maxBulkRows;
|
|
16
|
+
private listLimits;
|
|
15
17
|
private authAdapter?;
|
|
16
|
-
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter, maxBulkRows?: number);
|
|
18
|
+
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter, maxBulkRows?: number, listLimits?: ListLimitOptions);
|
|
19
|
+
/**
|
|
20
|
+
* Parse request query params into QueryOptions, applying this generator's
|
|
21
|
+
* list-pagination bounds (default page size + hard max limit) so no read
|
|
22
|
+
* path can be tricked into buffering an entire table into memory.
|
|
23
|
+
*/
|
|
24
|
+
private parseQuery;
|
|
17
25
|
/**
|
|
18
26
|
* Generate REST routes using existing DataDriver
|
|
19
27
|
*/
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { QueryOptions } from "../types";
|
|
2
2
|
export declare const mapOperator: (op: string) => import("@rebasepro/types").WhereFilterOp | null;
|
|
3
|
+
export { DEFAULT_LIST_LIMIT, DEFAULT_VECTOR_LIST_LIMIT, MAX_LIST_LIMIT } from "@rebasepro/types";
|
|
4
|
+
/**
|
|
5
|
+
* Overridable list-pagination bounds for {@link parseQueryOptions}. Without
|
|
6
|
+
* these, `GET /<collection>` with no `?limit` would buffer the ENTIRE table
|
|
7
|
+
* into a JS array + JSON response (a trivial OOM/DoS), and `?limit=100000000`
|
|
8
|
+
* would be honoured verbatim.
|
|
9
|
+
*/
|
|
10
|
+
export interface ListLimitOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Page size used when the client sends no `?limit`. Applied to plain and
|
|
13
|
+
* text-search reads — a vector search falls back to its own default (10).
|
|
14
|
+
*/
|
|
15
|
+
defaultLimit?: number;
|
|
16
|
+
/** Upper bound clamped onto any client-supplied `?limit`. */
|
|
17
|
+
maxLimit?: number;
|
|
18
|
+
}
|
|
3
19
|
/**
|
|
4
20
|
* Parse query parameters into QueryOptions
|
|
5
21
|
*/
|
|
6
|
-
export declare function parseQueryOptions(query: Record<string, unknown
|
|
22
|
+
export declare function parseQueryOptions(query: Record<string, unknown>, limits?: ListLimitOptions): QueryOptions;
|
|
@@ -119,6 +119,33 @@ export interface RefreshTokenInfo {
|
|
|
119
119
|
createdAt: Date;
|
|
120
120
|
userAgent?: string | null;
|
|
121
121
|
ipAddress?: string | null;
|
|
122
|
+
/**
|
|
123
|
+
* The sign-in this token descends from. Every token minted by rotating
|
|
124
|
+
* this one carries the same id.
|
|
125
|
+
*
|
|
126
|
+
* Optional because a custom {@link TokenRepository} written against an
|
|
127
|
+
* older release does not supply it; the refresh endpoint then treats the
|
|
128
|
+
* token as a session of one, which costs reuse tolerance but still works.
|
|
129
|
+
*/
|
|
130
|
+
sessionId?: string;
|
|
131
|
+
/**
|
|
132
|
+
* Set when this token was superseded by a rotation. Being superseded is
|
|
133
|
+
* not an error — a client whose response was lost still holds it — so it
|
|
134
|
+
* stays usable to mint a sibling for a short window after this instant.
|
|
135
|
+
*/
|
|
136
|
+
rotatedAt?: Date | null;
|
|
137
|
+
/** Hard kill (logout, remote session revoke). Never usable again. */
|
|
138
|
+
revoked?: boolean;
|
|
139
|
+
/** When the sign-in happened; carried across rotations, unlike createdAt. */
|
|
140
|
+
sessionStartedAt?: Date;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Identity of the sign-in a refresh token belongs to, threaded through
|
|
144
|
+
* rotation so descendants stay grouped.
|
|
145
|
+
*/
|
|
146
|
+
export interface RefreshTokenSession {
|
|
147
|
+
id: string;
|
|
148
|
+
startedAt: Date;
|
|
122
149
|
}
|
|
123
150
|
/**
|
|
124
151
|
* Password reset token info
|
|
@@ -278,9 +305,45 @@ export interface RoleRepository {
|
|
|
278
305
|
*/
|
|
279
306
|
export interface TokenRepository {
|
|
280
307
|
/**
|
|
281
|
-
* Create a new refresh token
|
|
308
|
+
* Create a new refresh token.
|
|
309
|
+
*
|
|
310
|
+
* `session` groups this token with the sign-in it descends from. It is
|
|
311
|
+
* optional so that repositories written against an older release keep
|
|
312
|
+
* satisfying this interface; implementations that ignore it degrade to one
|
|
313
|
+
* session per token.
|
|
314
|
+
*/
|
|
315
|
+
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
|
|
316
|
+
/**
|
|
317
|
+
* Mark a token as superseded by a rotation, WITHOUT making it unusable.
|
|
318
|
+
*
|
|
319
|
+
* The distinction from deletion is the entire point: a client that never
|
|
320
|
+
* received the rotated response still holds this token, and must be able
|
|
321
|
+
* to present it and be recognised. Implementations that omit this method
|
|
322
|
+
* fall back to {@link TokenRepository.deleteRefreshToken}, which restores
|
|
323
|
+
* the old, lossy behaviour.
|
|
324
|
+
*/
|
|
325
|
+
markRefreshTokenRotated?(tokenHash: string): Promise<void>;
|
|
326
|
+
/**
|
|
327
|
+
* Hard-kill every token of one sign-in (logout, remote session revoke).
|
|
328
|
+
* Unlike rotation this is final — no grace, no replay.
|
|
329
|
+
*/
|
|
330
|
+
revokeRefreshTokenSession?(sessionId: string): Promise<void>;
|
|
331
|
+
/**
|
|
332
|
+
* Drop tokens of a session that were superseded before `supersededBefore`,
|
|
333
|
+
* plus anything already expired. Keeps rotation from growing a row per
|
|
334
|
+
* refresh forever; called opportunistically, never load-bearing.
|
|
335
|
+
*/
|
|
336
|
+
pruneRefreshTokens?(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
|
|
337
|
+
/**
|
|
338
|
+
* The instant before which every session of this user is void, or null if
|
|
339
|
+
* none is set. See `users.tokens_valid_after`.
|
|
340
|
+
*/
|
|
341
|
+
getTokensValidAfter?(uid: string): Promise<Date | null>;
|
|
342
|
+
/**
|
|
343
|
+
* Void every session that began before `at`. Set alongside deleting the
|
|
344
|
+
* user's tokens so a rotation racing the delete cannot survive it.
|
|
282
345
|
*/
|
|
283
|
-
|
|
346
|
+
setTokensValidAfter?(uid: string, at: Date): Promise<void>;
|
|
284
347
|
/**
|
|
285
348
|
* Find a refresh token by hash
|
|
286
349
|
*/
|
package/dist/auth/jwt.d.ts
CHANGED
|
@@ -65,6 +65,23 @@ export declare function generateRefreshToken(): string;
|
|
|
65
65
|
* Hash a refresh token for database storage (don't store raw tokens)
|
|
66
66
|
*/
|
|
67
67
|
export declare function hashRefreshToken(token: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* The longest a cookie can live. Chrome (since 104) and RFC 6265bis silently
|
|
70
|
+
* rewrite any `Max-Age` beyond 400 days down to 400 days, so promising a
|
|
71
|
+
* browser more is not a stricter policy — it is a policy that differs from
|
|
72
|
+
* what is actually enforced, which is worse than knowing the ceiling.
|
|
73
|
+
*/
|
|
74
|
+
export declare const MAX_COOKIE_AGE_MS: number;
|
|
75
|
+
/**
|
|
76
|
+
* How long a refresh token is valid for, in milliseconds.
|
|
77
|
+
*
|
|
78
|
+
* Every rotation issues a token with a fresh TTL, so this is a sliding window:
|
|
79
|
+
* a user who visits at all keeps their session indefinitely, and one who
|
|
80
|
+
* disappears loses it this long after their last visit. That is what both
|
|
81
|
+
* Firebase and Supabase do by default, and it is the behaviour people mean
|
|
82
|
+
* when they say they expect to still be signed in.
|
|
83
|
+
*/
|
|
84
|
+
export declare function getRefreshTokenTtlMs(): number;
|
|
68
85
|
/**
|
|
69
86
|
* Calculate refresh token expiration date
|
|
70
87
|
*/
|
package/dist/auth/routes.d.ts
CHANGED
|
@@ -47,6 +47,22 @@ export interface AuthModuleConfig {
|
|
|
47
47
|
* auth endpoints, and CORS must allow credentials (no `origin: "*"`).
|
|
48
48
|
*/
|
|
49
49
|
cookieAuth?: CookieAuthConfig;
|
|
50
|
+
/**
|
|
51
|
+
* How long a refresh token stays usable after it has been rotated away,
|
|
52
|
+
* in seconds. Default 10, matching GoTrue's `refresh_token_reuse_interval`.
|
|
53
|
+
*
|
|
54
|
+
* Rotation is only safe if the client is guaranteed to receive the
|
|
55
|
+
* replacement, and no network guarantees that. A pod rolls mid-response, a
|
|
56
|
+
* laptop suspends, a second tab boots at the same instant — and the client
|
|
57
|
+
* is left holding a token the database has moved past. Within this window
|
|
58
|
+
* that client is handed a fresh token of the same session instead of a
|
|
59
|
+
* 401, which is the difference between a hiccup and being silently signed
|
|
60
|
+
* out of an app you were using.
|
|
61
|
+
*
|
|
62
|
+
* Widen it if your clients are flaky or your deploys are long; the cost is
|
|
63
|
+
* how long a captured token stays useful to someone who copied it.
|
|
64
|
+
*/
|
|
65
|
+
refreshTokenReuseIntervalSeconds?: number;
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
52
68
|
* Configuration for httpOnly refresh-token cookies.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Server } from "http";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { type RebaseBackendInstance } from "../init";
|
|
4
|
+
import type { HonoEnv } from "../api/types";
|
|
5
|
+
import { type RebaseBootEnv } from "./env";
|
|
6
|
+
import { type LoadedBundle } from "./bundle";
|
|
7
|
+
import { type InitializedDataSource } from "./driver";
|
|
8
|
+
/** A running runtime, and the handle to stop it. */
|
|
9
|
+
export interface BootedRuntime {
|
|
10
|
+
app: Hono<HonoEnv>;
|
|
11
|
+
server: Server;
|
|
12
|
+
backend: RebaseBackendInstance;
|
|
13
|
+
bundle: LoadedBundle;
|
|
14
|
+
env: RebaseBootEnv;
|
|
15
|
+
/** The port actually bound, which in development may not be the one asked for. */
|
|
16
|
+
port: number;
|
|
17
|
+
dataSources: InitializedDataSource[];
|
|
18
|
+
shutdown: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface BootOptions {
|
|
21
|
+
/** Bundle directory. Defaults to `REBASE_BUNDLE` or `./dist-bundle`. */
|
|
22
|
+
bundleDir?: string;
|
|
23
|
+
/**
|
|
24
|
+
* A bundle that has already been resolved.
|
|
25
|
+
*
|
|
26
|
+
* `rebase dev` passes one built from source (see `createSourceBundle`), so
|
|
27
|
+
* development and production run the identical boot path rather than two
|
|
28
|
+
* implementations that drift apart.
|
|
29
|
+
*/
|
|
30
|
+
bundle?: LoadedBundle;
|
|
31
|
+
/** Skip binding a port. Used by tests that drive `app.fetch` directly. */
|
|
32
|
+
listen?: boolean;
|
|
33
|
+
/** Install SIGTERM/SIGINT handlers. Off for tests. */
|
|
34
|
+
handleSignals?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Boot a Rebase runtime from a built bundle.
|
|
38
|
+
*
|
|
39
|
+
* This is the entrypoint the official container image runs, and it is the same
|
|
40
|
+
* code path a self-hosted deployment uses — there is no separate "platform"
|
|
41
|
+
* runtime. Everything it does was previously the responsibility of a
|
|
42
|
+
* hand-written `backend/src/index.ts` in every project: wiring CORS and security
|
|
43
|
+
* headers, opening database connections, resolving storage, mounting health and
|
|
44
|
+
* metrics, serving the client bundle, and shutting all of it down cleanly.
|
|
45
|
+
*
|
|
46
|
+
* Moving it here is what makes a project's *code* separable from the *engine*
|
|
47
|
+
* that runs it: the bundle can then be handed to a newer runtime without being
|
|
48
|
+
* rebuilt, which is the precondition for patching a fleet.
|
|
49
|
+
*/
|
|
50
|
+
export declare function bootFromBundle(options?: BootOptions): Promise<BootedRuntime>;
|
|
51
|
+
/**
|
|
52
|
+
* Boot and keep running, reporting failures the way a container should.
|
|
53
|
+
*
|
|
54
|
+
* A `BundleError` is a configuration problem with a known fix, so it prints the
|
|
55
|
+
* message and its hint without a stack trace — the stack is noise when the
|
|
56
|
+
* answer is "set DATABASE_URL". Anything else keeps its stack, because it is a
|
|
57
|
+
* bug and the trace is the point.
|
|
58
|
+
*/
|
|
59
|
+
export declare function runFromBundle(options?: BootOptions): Promise<BootedRuntime>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type CollectionConfig, type CollectionCallbacks, type DataSourceDefinition, type RebaseBundleManifest, type StorageSourceDefinition } from "@rebasepro/types";
|
|
2
|
+
import type { StorageAuthorize } from "../storage/types";
|
|
3
|
+
/** Thrown when a bundle cannot be read, or claims a contract this runtime cannot honour. */
|
|
4
|
+
export declare class BundleError extends Error {
|
|
5
|
+
readonly hint?: string | undefined;
|
|
6
|
+
constructor(message: string, hint?: string | undefined);
|
|
7
|
+
}
|
|
8
|
+
/** A bundle that has been located and whose manifest has been validated. */
|
|
9
|
+
export interface LoadedBundle {
|
|
10
|
+
dir: string;
|
|
11
|
+
manifest: RebaseBundleManifest;
|
|
12
|
+
/** Absolute path to the compiled collections directory, when present. */
|
|
13
|
+
collectionsDir?: string;
|
|
14
|
+
functionsDir?: string;
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read and validate a bundle's manifest.
|
|
23
|
+
*
|
|
24
|
+
* The checks here are the runtime half of the compatibility contract, and they
|
|
25
|
+
* all fail loudly at boot rather than at the first request. A container that
|
|
26
|
+
* refuses to start is a deploy that rolls back; a container that starts and then
|
|
27
|
+
* misbehaves is an incident.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readBundleManifest(bundleDir: string): RebaseBundleManifest;
|
|
30
|
+
/**
|
|
31
|
+
* Locate a bundle and resolve every directory the runtime needs from it.
|
|
32
|
+
*
|
|
33
|
+
* Entry paths in the manifest are bundle-relative and are resolved here, once,
|
|
34
|
+
* so nothing downstream has to know the layout. A declared directory that does
|
|
35
|
+
* not exist is dropped with a warning rather than failing the boot: an empty
|
|
36
|
+
* `functions/` is a perfectly ordinary project, and refusing to start over one
|
|
37
|
+
* would be the runtime inventing a requirement the developer never stated.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a bundle-relative entry, refusing anything that escapes the bundle.
|
|
41
|
+
*
|
|
42
|
+
* Applied to the entries that are `import()`ed — the schema, the config index,
|
|
43
|
+
* the users collection — and not only to the ones that are merely scanned. Those
|
|
44
|
+
* three *execute code*, so they are precisely the ones a malformed or hostile
|
|
45
|
+
* manifest would target, and leaving them unchecked while guarding the read-only
|
|
46
|
+
* paths would be defending the wrong door.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveBundlePath(bundleDir: string, entry: string, label: string): string;
|
|
49
|
+
export declare function loadBundle(bundleDir: string): LoadedBundle;
|
|
50
|
+
/**
|
|
51
|
+
* The Drizzle schema a bundle ships: tables, enums and relations, as generated
|
|
52
|
+
* from the project's collections.
|
|
53
|
+
*/
|
|
54
|
+
export interface BundleSchemaExports {
|
|
55
|
+
tables?: Record<string, unknown>;
|
|
56
|
+
enums?: Record<string, unknown>;
|
|
57
|
+
relations?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Import the bundle's Drizzle schema module.
|
|
61
|
+
*
|
|
62
|
+
* Returns `undefined` when the bundle declares none — `baas` mode introspects the
|
|
63
|
+
* live database instead of shipping a schema.
|
|
64
|
+
*/
|
|
65
|
+
export declare function loadBundleSchema(bundle: LoadedBundle): Promise<BundleSchemaExports | undefined>;
|
|
66
|
+
/**
|
|
67
|
+
* Build a bundle view over a project's **source** directories.
|
|
68
|
+
*
|
|
69
|
+
* `rebase dev` runs TypeScript directly through tsx, so there is no compiled
|
|
70
|
+
* bundle to load — but everything downstream of loading (drivers, storage, auth,
|
|
71
|
+
* routes) should be identical, or development stops predicting production. This
|
|
72
|
+
* produces the same {@link LoadedBundle} shape from source paths, so the one boot
|
|
73
|
+
* path serves both.
|
|
74
|
+
*
|
|
75
|
+
* The schema version is left empty deliberately: nothing has been built, so
|
|
76
|
+
* there is no build-time answer, and the runtime computes one from the live
|
|
77
|
+
* collections instead.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createSourceBundle(options: {
|
|
80
|
+
projectRoot: string;
|
|
81
|
+
config?: string;
|
|
82
|
+
collections?: string;
|
|
83
|
+
functions?: string;
|
|
84
|
+
crons?: string;
|
|
85
|
+
schema?: string;
|
|
86
|
+
mode?: "cms" | "baas" | "static";
|
|
87
|
+
app?: string;
|
|
88
|
+
}): LoadedBundle;
|
|
89
|
+
/**
|
|
90
|
+
* Declarations a bundle's config package exports alongside its collections.
|
|
91
|
+
*
|
|
92
|
+
* These describe *topology* — which databases and which buckets exist — so they
|
|
93
|
+
* belong to the project rather than to the environment. The environment then
|
|
94
|
+
* supplies credentials for each declared key. Splitting it this way is what lets
|
|
95
|
+
* a deploy be validated before it runs: the set of things needing configuration
|
|
96
|
+
* is known from the bundle, without reading anyone's secrets.
|
|
97
|
+
*/
|
|
98
|
+
export interface BundleConfigExports {
|
|
99
|
+
dataSources?: DataSourceDefinition[];
|
|
100
|
+
storageSources?: StorageSourceDefinition[];
|
|
101
|
+
/**
|
|
102
|
+
* Per-object storage access control.
|
|
103
|
+
*
|
|
104
|
+
* A function, so it can only come from the project's own code — there is no
|
|
105
|
+
* environment variable that could express "this user may read this key".
|
|
106
|
+
* Without a way to supply it, a production deployment with a bucket would be
|
|
107
|
+
* forced to choose between `STORAGE_PUBLIC_READ` (world-readable) and
|
|
108
|
+
* `STORAGE_ALLOW_ANY_AUTHENTICATED` (every signed-in user can read, overwrite
|
|
109
|
+
* and delete every other user's files) — the runtime would be making an
|
|
110
|
+
* insecure choice on the developer's behalf.
|
|
111
|
+
*/
|
|
112
|
+
storageAuthorize?: StorageAuthorize;
|
|
113
|
+
/** Lifecycle callbacks applied to every collection. */
|
|
114
|
+
callbacks?: CollectionCallbacks;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read the config package's `index` for declarations.
|
|
118
|
+
*
|
|
119
|
+
* Absent, empty or unreadable all mean the same thing: a single default database
|
|
120
|
+
* and a single default bucket. That is the overwhelmingly common project, and it
|
|
121
|
+
* must not be required to say so. A malformed export is reported and ignored
|
|
122
|
+
* rather than fatal — a typo in an optional declaration should not take down a
|
|
123
|
+
* server whose collections are fine.
|
|
124
|
+
*/
|
|
125
|
+
export declare function loadBundleConfigExports(bundle: LoadedBundle): Promise<BundleConfigExports>;
|
|
126
|
+
/**
|
|
127
|
+
* Import the collection that backs authentication.
|
|
128
|
+
*
|
|
129
|
+
* Auth needs to know which table holds users. The bundle names the module; the
|
|
130
|
+
* convention (`collections/users`) covers every project that did not rename it.
|
|
131
|
+
* Returning `undefined` is valid — a `baas`-mode project has no config package,
|
|
132
|
+
* and the auth bootstrapper falls back to its own default users table.
|
|
133
|
+
*/
|
|
134
|
+
export declare function loadUsersCollection(bundle: LoadedBundle): Promise<CollectionConfig | undefined>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BackendBootstrapper } from "@rebasepro/types";
|
|
2
|
+
import type { ResolvedDataSourceConfig } from "./sources";
|
|
3
|
+
/** The connection handle a driver hands back. */
|
|
4
|
+
export interface DatabaseConnection {
|
|
5
|
+
db: unknown;
|
|
6
|
+
/**
|
|
7
|
+
* Present for pool-based drivers. Closed during shutdown, and used to probe
|
|
8
|
+
* the source for health — `query` lives here, not on the connection itself.
|
|
9
|
+
*/
|
|
10
|
+
pool?: {
|
|
11
|
+
end: () => Promise<void>;
|
|
12
|
+
query?: (sql: string) => Promise<unknown>;
|
|
13
|
+
};
|
|
14
|
+
/** Some drivers expose a query directly instead of via a pool. */
|
|
15
|
+
query?: (sql: string) => Promise<unknown>;
|
|
16
|
+
connectionString?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run a trivial query against a source, to see whether it answers.
|
|
20
|
+
*
|
|
21
|
+
* Returns `undefined` when the driver exposes no way to ask — a driver that
|
|
22
|
+
* cannot be probed must not be reported as unhealthy, only as unknown.
|
|
23
|
+
*/
|
|
24
|
+
export declare function probeDataSource(source: InitializedDataSource): Promise<{
|
|
25
|
+
healthy: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
} | undefined>;
|
|
28
|
+
/** One initialized data source: its bootstrapper plus the handle to close. */
|
|
29
|
+
export interface InitializedDataSource {
|
|
30
|
+
key: string;
|
|
31
|
+
engine: string;
|
|
32
|
+
driverPackage: string;
|
|
33
|
+
bootstrapper: BackendBootstrapper;
|
|
34
|
+
connection: DatabaseConnection;
|
|
35
|
+
}
|
|
36
|
+
export interface BundleSchema {
|
|
37
|
+
tables?: Record<string, unknown>;
|
|
38
|
+
enums?: Record<string, unknown>;
|
|
39
|
+
relations?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a driver for one data source.
|
|
43
|
+
*
|
|
44
|
+
* Only the default source receives the bundle's Drizzle schema: the schema
|
|
45
|
+
* describes the tables generated from this project's collections, which live in
|
|
46
|
+
* the default database. Handing it to a secondary source would tell that
|
|
47
|
+
* driver's adapter about tables it does not have.
|
|
48
|
+
*/
|
|
49
|
+
export declare function initializeDataSource(source: ResolvedDataSourceConfig, schema: BundleSchema | undefined, resolveFrom?: string[]): Promise<InitializedDataSource>;
|
|
50
|
+
/**
|
|
51
|
+
* Build drivers for every resolved data source.
|
|
52
|
+
*
|
|
53
|
+
* Sequential on purpose: a failure on the second source should not leave a
|
|
54
|
+
* half-opened pool from a third racing behind it, and the log then reads in the
|
|
55
|
+
* order a human would expect.
|
|
56
|
+
*/
|
|
57
|
+
export declare function initializeDataSources(sources: ResolvedDataSourceConfig[], schema: BundleSchema | undefined, resolveFrom?: string[]): Promise<InitializedDataSource[]>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type RebaseEnv } from "../env";
|
|
3
|
+
/**
|
|
4
|
+
* The environment a bundle-booted runtime understands.
|
|
5
|
+
*
|
|
6
|
+
* This extends the base {@link loadEnv} schema with the variables an application
|
|
7
|
+
* used to declare for itself in its own `env.ts`. They live here now because the
|
|
8
|
+
* runtime, not the application, is what reads them: a project ships a bundle and
|
|
9
|
+
* a set of environment variables, and everything either side needs to agree on
|
|
10
|
+
* has to be part of the contract rather than a convention each project reinvents.
|
|
11
|
+
*/
|
|
12
|
+
declare const bootEnvExtension: z.ZodObject<{
|
|
13
|
+
SMTP_HOST: z.ZodOptional<z.ZodString>;
|
|
14
|
+
SMTP_PORT: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<number, string>>;
|
|
15
|
+
SMTP_SECURE: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
16
|
+
"": "";
|
|
17
|
+
true: "true";
|
|
18
|
+
false: "false";
|
|
19
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
20
|
+
SMTP_USER: z.ZodOptional<z.ZodString>;
|
|
21
|
+
SMTP_PASS: z.ZodOptional<z.ZodString>;
|
|
22
|
+
SMTP_FROM: z.ZodOptional<z.ZodString>;
|
|
23
|
+
SMTP_NAME: z.ZodOptional<z.ZodString>;
|
|
24
|
+
APP_NAME: z.ZodDefault<z.ZodString>;
|
|
25
|
+
REBASE_SERVE_STATIC: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
26
|
+
"": "";
|
|
27
|
+
true: "true";
|
|
28
|
+
false: "false";
|
|
29
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
30
|
+
REBASE_MIGRATE_ON_BOOT: z.ZodOptional<z.ZodEnum<{
|
|
31
|
+
"": "";
|
|
32
|
+
push: "push";
|
|
33
|
+
none: "none";
|
|
34
|
+
ensure: "ensure";
|
|
35
|
+
}>>;
|
|
36
|
+
REBASE_METRICS: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
37
|
+
"": "";
|
|
38
|
+
true: "true";
|
|
39
|
+
false: "false";
|
|
40
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
41
|
+
REBASE_METRICS_TOKEN: z.ZodOptional<z.ZodString>;
|
|
42
|
+
LOG_LEVEL: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
"": "";
|
|
44
|
+
debug: "debug";
|
|
45
|
+
info: "info";
|
|
46
|
+
warn: "warn";
|
|
47
|
+
error: "error";
|
|
48
|
+
}>>;
|
|
49
|
+
STORAGE_PUBLIC_READ: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
50
|
+
"": "";
|
|
51
|
+
true: "true";
|
|
52
|
+
false: "false";
|
|
53
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
54
|
+
STORAGE_ALLOW_ANY_AUTHENTICATED: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
55
|
+
"": "";
|
|
56
|
+
true: "true";
|
|
57
|
+
false: "false";
|
|
58
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
59
|
+
AUTH_REQUIRE: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
60
|
+
"": "";
|
|
61
|
+
true: "true";
|
|
62
|
+
false: "false";
|
|
63
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
64
|
+
AUTH_ALLOW_USER_LOOKUP: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
65
|
+
"": "";
|
|
66
|
+
true: "true";
|
|
67
|
+
false: "false";
|
|
68
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
69
|
+
AUTH_COOKIE_SAME_SITE: z.ZodOptional<z.ZodEnum<{
|
|
70
|
+
"": "";
|
|
71
|
+
Strict: "Strict";
|
|
72
|
+
Lax: "Lax";
|
|
73
|
+
None: "None";
|
|
74
|
+
}>>;
|
|
75
|
+
AUTH_DEFAULT_ROLE: z.ZodOptional<z.ZodString>;
|
|
76
|
+
GITHUB_CLIENT_ID: z.ZodOptional<z.ZodString>;
|
|
77
|
+
GITHUB_CLIENT_SECRET: z.ZodOptional<z.ZodString>;
|
|
78
|
+
MICROSOFT_CLIENT_ID: z.ZodOptional<z.ZodString>;
|
|
79
|
+
MICROSOFT_CLIENT_SECRET: z.ZodOptional<z.ZodString>;
|
|
80
|
+
REBASE_BASE_PATH: z.ZodDefault<z.ZodString>;
|
|
81
|
+
REBASE_ENABLE_SWAGGER: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
82
|
+
"": "";
|
|
83
|
+
true: "true";
|
|
84
|
+
false: "false";
|
|
85
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
86
|
+
REBASE_MAX_BODY_SIZE: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
87
|
+
REBASE_COMPRESSION: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
88
|
+
"": "";
|
|
89
|
+
true: "true";
|
|
90
|
+
false: "false";
|
|
91
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
92
|
+
REBASE_HISTORY: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
93
|
+
"": "";
|
|
94
|
+
true: "true";
|
|
95
|
+
false: "false";
|
|
96
|
+
}>>, z.ZodTransform<boolean, "" | "true" | "false">>;
|
|
97
|
+
CORS_ORIGINS: z.ZodOptional<z.ZodString>;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
export type RebaseBootEnv = RebaseEnv & z.infer<typeof bootEnvExtension>;
|
|
100
|
+
/**
|
|
101
|
+
* Load and validate the environment for a bundle boot.
|
|
102
|
+
*
|
|
103
|
+
* Does not read `.env` files — that is the deployment's job (a container gets
|
|
104
|
+
* real environment variables; `rebase dev` and `rebase start` load dotenv before
|
|
105
|
+
* calling in).
|
|
106
|
+
*/
|
|
107
|
+
export declare function loadBootEnv(): RebaseBootEnv;
|
|
108
|
+
/**
|
|
109
|
+
* Whether an origin is a loopback address.
|
|
110
|
+
*
|
|
111
|
+
* In development the runtime reflects only localhost origins. It cannot reflect
|
|
112
|
+
* an arbitrary `Origin`, because credentials are enabled: any site the developer
|
|
113
|
+
* happened to visit could otherwise make credentialed requests against the dev
|
|
114
|
+
* server with the developer's session and read the responses.
|
|
115
|
+
*/
|
|
116
|
+
export declare function isLocalhostOrigin(origin: string): boolean;
|
|
117
|
+
/** A CORS origin resolver of the shape Hono's `cors()` middleware expects. */
|
|
118
|
+
export type CorsOriginResolver = (origin: string) => string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Build the CORS origin policy.
|
|
121
|
+
*
|
|
122
|
+
* Production serves an explicit allow-list and nothing else. `loadEnv` already
|
|
123
|
+
* refuses to start a production process with neither `CORS_ORIGINS` nor
|
|
124
|
+
* `FRONTEND_URL`, so an empty list here can only mean the values were blank
|
|
125
|
+
* strings — still worth failing on, because the alternative is an API that
|
|
126
|
+
* quietly rejects its own frontend.
|
|
127
|
+
*/
|
|
128
|
+
export declare function resolveCorsOrigin(env: RebaseBootEnv): CorsOriginResolver;
|
|
129
|
+
export {};
|