@rebasepro/cli 0.13.0 → 0.13.1-canary.g06dbe5b
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/bundle.d.ts +4 -3
- package/dist/commands/api-keys.d.ts +51 -0
- package/dist/commands/auth.d.ts +62 -0
- package/dist/commands/cloud/context.d.ts +56 -7
- package/dist/commands/cloud/databases.d.ts +39 -0
- package/dist/commands/cloud/debug.d.ts +1 -0
- package/dist/commands/cloud/deployments.d.ts +22 -0
- package/dist/commands/cloud/domains.d.ts +10 -0
- package/dist/commands/cloud/env.d.ts +51 -0
- package/dist/commands/cloud/extensions.d.ts +21 -0
- package/dist/commands/cloud/orgs.d.ts +1 -0
- package/dist/commands/cloud/projects.d.ts +29 -0
- package/dist/commands/cloud/resources.d.ts +14 -0
- package/dist/commands/cloud/settings.d.ts +1 -0
- package/dist/commands/dev.d.ts +17 -6
- package/dist/commands/eject.d.ts +42 -0
- package/dist/commands/init.d.ts +67 -0
- package/dist/commands/skills.d.ts +81 -0
- package/dist/fold-static.d.ts +47 -0
- package/dist/index.es.js +1440 -415
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +16 -1
- package/dist/telemetry/payload.d.ts +1 -1
- package/dist/utils/args.d.ts +76 -0
- package/dist/utils/collection-drift.d.ts +27 -0
- package/dist/utils/project.d.ts +20 -0
- package/package.json +7 -7
- package/templates/eject/Dockerfile +29 -4
- package/templates/eject/backend/src/index.ts +49 -5
- package/templates/eject/docker-compose.custom.yml +13 -5
- package/templates/overlays/baas/backend/tsconfig.json +6 -1
- package/templates/template/.env.example +13 -4
- package/templates/template/backend/functions/hello.ts +8 -4
- package/templates/template/backend/tsconfig.json +6 -1
- package/templates/template/config/package.json +1 -0
- package/templates/template/docker-compose.yml +4 -4
package/dist/manifest.d.ts
CHANGED
|
@@ -54,7 +54,22 @@ export declare function manifestExists(projectRoot: string): boolean;
|
|
|
54
54
|
* available behaviour.
|
|
55
55
|
*/
|
|
56
56
|
export declare function loadManifest(projectRoot: string): LoadedManifest;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Write a manifest, with a trailing newline so it plays well with other tools.
|
|
59
|
+
*
|
|
60
|
+
* **Every key on disk survives.** This used to emit exactly `$schema`, `rebase`
|
|
61
|
+
* and `apps`, so a rewrite deleted the rest of the file — and two commands with
|
|
62
|
+
* no visible relationship to either key rewrite it: `rebase eject` and
|
|
63
|
+
* `rebase apps init --force`. A repository that had committed
|
|
64
|
+
* `"telemetry": false` lost its opt-out, and a multi-bucket project lost its
|
|
65
|
+
* whole `storage` block, in a commit whose stated change was `runtime: custom`.
|
|
66
|
+
*
|
|
67
|
+
* So: the caller's manifest wins for what it models, the file supplies the rest.
|
|
68
|
+
* `storage` and `telemetry` fall back to the file because the callers that
|
|
69
|
+
* synthesize a manifest (`apps init --force`) cannot know them — they are
|
|
70
|
+
* authored, not inferred — and unknown top-level keys are copied verbatim
|
|
71
|
+
* rather than listed, since a hand-listed set loses the next key too.
|
|
72
|
+
*/
|
|
58
73
|
export declare function writeManifest(projectRoot: string, manifest: RebaseProjectManifest): string;
|
|
59
74
|
/** Find the single backend app, if this repository declares one. */
|
|
60
75
|
export declare function findBackendApp(manifest: RebaseProjectManifest): {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
export declare const TELEMETRY_SCHEMA_VERSION = 1;
|
|
33
33
|
/** The events the CLI reports. Closed set — a name not listed cannot be sent. */
|
|
34
|
-
export type TelemetryEventName = "cli.init" | "cli.dev" | "cli.deploy" | "cli.
|
|
34
|
+
export type TelemetryEventName = "cli.init" | "cli.dev" | "cli.deploy" | "cli.schema" | "cli.db" | "cli.error";
|
|
35
35
|
/** A single non-free-text value. */
|
|
36
36
|
export type TelemetryValue = string | number | boolean;
|
|
37
37
|
export type TelemetryEvent = {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing for the commands that take positional arguments.
|
|
3
|
+
*
|
|
4
|
+
* Every command in the small group used to parse its own line the same way:
|
|
5
|
+
* `arg(spec, { argv: rawArgs.slice(4), permissive: true })`, then read `_[0]`
|
|
6
|
+
* and `_[1]`. Both halves of that are wrong in a way that costs data.
|
|
7
|
+
*
|
|
8
|
+
* - **`permissive: true` turns an unknown flag into a positional.** `arg`
|
|
9
|
+
* pushes an undeclared flag into `_` as a bare token, so `_[1]` is whatever
|
|
10
|
+
* came second on the line, flag or not. `rebase auth reset-password
|
|
11
|
+
* bob@example.com --debug` set Bob's password to the literal `--debug` — and
|
|
12
|
+
* `--debug` is what `bin/rebase.js` prints after *every* failure as the thing
|
|
13
|
+
* to re-run with, so the single most likely next keystroke after a failed
|
|
14
|
+
* reset was the one that reset the account to a two-word string.
|
|
15
|
+
* - **`slice(4)` assumes the command words are at fixed indices.** They are
|
|
16
|
+
* not: a flag before the command shifts everything, so `rebase --debug auth
|
|
17
|
+
* reset-password bob@example.com NewPass1!` read the email as
|
|
18
|
+
* `reset-password` and the password as `bob@example.com`.
|
|
19
|
+
*
|
|
20
|
+
* So: parse the *whole* line — `rawArgs` is `process.argv` — against a spec
|
|
21
|
+
* strictly, with no permissive mode. `arg` then consumes every flag wherever it
|
|
22
|
+
* appears and rejects the ones nobody declared, which leaves `_` holding the
|
|
23
|
+
* command words followed by the real positionals, in order and at a known
|
|
24
|
+
* offset. An unrecognised flag becomes an error naming the command's help,
|
|
25
|
+
* which is the only safe answer: the alternative is guessing that it was meant
|
|
26
|
+
* as a value.
|
|
27
|
+
*
|
|
28
|
+
* `commands/cloud/index.ts` resolves its positionals against its own spec for
|
|
29
|
+
* the same reason; this is that idea for the commands whose positionals are
|
|
30
|
+
* credentials rather than resource names.
|
|
31
|
+
*/
|
|
32
|
+
import arg from "arg";
|
|
33
|
+
/**
|
|
34
|
+
* Flags accepted on top of whatever a command declares.
|
|
35
|
+
*
|
|
36
|
+
* `--debug` is read by `bin/rebase.js` off `process.argv` and never by a
|
|
37
|
+
* command, but it has to be *declared* somewhere or strict parsing rejects the
|
|
38
|
+
* exact flag the CLI tells people to add. `--help`/`-h` are answered by each
|
|
39
|
+
* command's dispatcher before any work happens.
|
|
40
|
+
*/
|
|
41
|
+
export declare const GLOBAL_COMMAND_FLAGS: {
|
|
42
|
+
readonly "--debug": BooleanConstructor;
|
|
43
|
+
readonly "--help": BooleanConstructor;
|
|
44
|
+
readonly "-h": "--help";
|
|
45
|
+
};
|
|
46
|
+
export interface ParsedCommand<S extends arg.Spec> {
|
|
47
|
+
/** The declared flags, as `arg` resolved them. */
|
|
48
|
+
flags: arg.Result<S>;
|
|
49
|
+
/** What is left after the command words — never a flag. */
|
|
50
|
+
positionals: string[];
|
|
51
|
+
/** `--help` or `-h` appeared anywhere on the line. */
|
|
52
|
+
help: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Did the line ask for help? Answered before dispatch, never by a handler. */
|
|
55
|
+
export declare function wantsHelp(rawArgs: string[]): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a command's flags and positionals from the full `process.argv`.
|
|
58
|
+
*
|
|
59
|
+
* `commandWords` is how many words name the command itself — 2 for
|
|
60
|
+
* `auth reset-password`, 1 for `start` — and is applied to the *parsed*
|
|
61
|
+
* positionals rather than to `argv`, so a flag placed before the command no
|
|
62
|
+
* longer shifts them.
|
|
63
|
+
*
|
|
64
|
+
* `command` names the command in error messages, e.g. `auth reset-password`.
|
|
65
|
+
*
|
|
66
|
+
* Throws on an unknown flag, on a positional that looks like a flag, and on
|
|
67
|
+
* more positionals than the command takes. `bin/rebase.js` turns each into a
|
|
68
|
+
* one-line `✗ …` and exit 1.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseCommandArgs<S extends arg.Spec>({ spec, rawArgs, commandWords, command, maxPositionals }: {
|
|
71
|
+
spec: S;
|
|
72
|
+
rawArgs: string[];
|
|
73
|
+
commandWords: number;
|
|
74
|
+
command: string;
|
|
75
|
+
maxPositionals?: number;
|
|
76
|
+
}): ParsedCommand<S>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Would `loadCollectionsFromDirectory` load this file?
|
|
3
|
+
*
|
|
4
|
+
* Mirrors that loader's own `isCollectionFile` plus its flat (non-recursive)
|
|
5
|
+
* scan. `relativePath` is relative to the collections directory, as `fs.watch`
|
|
6
|
+
* reports it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isLoadedCollectionFile(relativePath: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Does this collection source declare anything a SQL toolchain would own?
|
|
11
|
+
*
|
|
12
|
+
* The fallback order matches `resolveDataSource`: an explicit `engine` wins,
|
|
13
|
+
* and a collection that only names a `dataSource` is resolved as if the key
|
|
14
|
+
* were the engine — which is what that function does when no registry is
|
|
15
|
+
* available, and the CLI has none. An engine nobody recognises counts as
|
|
16
|
+
* relational, for the reason `isRelationalCollection` gives.
|
|
17
|
+
*
|
|
18
|
+
* A file declaring several collections is SQL-affecting if *any* of them is.
|
|
19
|
+
*/
|
|
20
|
+
export declare function declaresRelationalCollection(rawSource: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Can this edit have changed the generated SQL schema?
|
|
23
|
+
*
|
|
24
|
+
* Answers `true` when it cannot tell — an unreadable file is a reason to warn,
|
|
25
|
+
* not a reason to go quiet.
|
|
26
|
+
*/
|
|
27
|
+
export declare function affectsSqlSchema(collectionsDir: string, relativePath: string): boolean;
|
package/dist/utils/project.d.ts
CHANGED
|
@@ -34,6 +34,26 @@ export declare function findFrontendDir(projectRoot: string): string | null;
|
|
|
34
34
|
* Find the .env file. Checks the project root first, then backend.
|
|
35
35
|
*/
|
|
36
36
|
export declare function findEnvFile(projectRoot: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Read the project's `.env` into a plain object.
|
|
39
|
+
*
|
|
40
|
+
* One reader, because there were four: `dotenv` in `start`, a hand-rolled
|
|
41
|
+
* `indexOf("=")` loop in `api-keys`, a single-key regex in `auth`, and its own
|
|
42
|
+
* splitting in `cloud env`. `dotenv` is a declared dependency of this package,
|
|
43
|
+
* so the other three existed for no reason and disagreed with the correct one
|
|
44
|
+
* on the two things people actually write in a `.env`:
|
|
45
|
+
*
|
|
46
|
+
* - `export KEY=value`, which the hand-rolled parser keyed as
|
|
47
|
+
* `export KEY` — so the command reported the key as unset while it was
|
|
48
|
+
* right there in the file;
|
|
49
|
+
* - `KEY=value # comment`, whose comment travelled into the value and then
|
|
50
|
+
* into an `Authorization` header, coming back as a 401 with nothing
|
|
51
|
+
* pointing at the cause.
|
|
52
|
+
*
|
|
53
|
+
* Returns `{}` when the project has no `.env`, so callers can treat "absent"
|
|
54
|
+
* and "empty" alike.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readEnvFile(projectRoot: string): Record<string, string>;
|
|
37
57
|
/**
|
|
38
58
|
* Resolve a binary from the project's node_modules/.bin.
|
|
39
59
|
* Checks backend, root, parent monorepo root, then falls back to PATH.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1-canary.g06dbe5b",
|
|
4
4
|
"description": "Developer tools for Rebase projects",
|
|
5
5
|
"main": "./dist/index.es.js",
|
|
6
6
|
"module": "./dist/index.es.js",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"execa": "^9.6.1",
|
|
32
32
|
"inquirer": "14.0.2",
|
|
33
33
|
"jiti": "^2.7.0",
|
|
34
|
-
"@rebasepro/agent-skills": "0.13.
|
|
35
|
-
"@rebasepro/client": "0.13.
|
|
36
|
-
"@rebasepro/codegen": "0.13.
|
|
37
|
-
"@rebasepro/
|
|
38
|
-
"@rebasepro/server": "0.13.
|
|
39
|
-
"@rebasepro/
|
|
34
|
+
"@rebasepro/agent-skills": "0.13.1-canary.g06dbe5b",
|
|
35
|
+
"@rebasepro/client": "0.13.1-canary.g06dbe5b",
|
|
36
|
+
"@rebasepro/codegen": "0.13.1-canary.g06dbe5b",
|
|
37
|
+
"@rebasepro/server-postgres": "0.13.1-canary.g06dbe5b",
|
|
38
|
+
"@rebasepro/server": "0.13.1-canary.g06dbe5b",
|
|
39
|
+
"@rebasepro/types": "0.13.1-canary.g06dbe5b"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^26.1.2",
|
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
# not reach this project.
|
|
8
8
|
#
|
|
9
9
|
# Assumes pnpm, which is what `rebase init` scaffolds a workspace for. On npm,
|
|
10
|
-
#
|
|
10
|
+
# every line below that mentions pnpm has to change — the `pnpm install` and
|
|
11
|
+
# `pnpm --filter … run build` lines become `npm ci` and
|
|
12
|
+
# `npm run build --workspace <package>`, `CMD ["pnpm", "start"]` becomes
|
|
13
|
+
# `CMD ["npm", "start"]`, and the two `COPY` lines that name `pnpm-lock.yaml`
|
|
14
|
+
# and `pnpm-workspace.yaml` have to name `package-lock.json` instead. Those
|
|
15
|
+
# COPYs fail first, before any of the build lines runs.
|
|
11
16
|
#
|
|
12
17
|
# Build context: the project root (where pnpm-workspace.yaml lives)
|
|
13
18
|
# Usage:
|
|
@@ -33,15 +38,31 @@ WORKDIR /app
|
|
|
33
38
|
# Copy workspace root files first (cache-friendly layer)
|
|
34
39
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
|
35
40
|
|
|
36
|
-
# Copy workspace packages
|
|
41
|
+
# Copy workspace packages. Every workspace the lockfile has an importer for,
|
|
42
|
+
# so `--frozen-lockfile` below sees the same set of packages it was written from.
|
|
37
43
|
COPY backend ./backend
|
|
38
44
|
COPY config ./config
|
|
45
|
+
# {{#frontend}}
|
|
46
|
+
COPY frontend ./frontend
|
|
47
|
+
# {{/frontend}}
|
|
48
|
+
|
|
49
|
+
# The entrypoint reads rebase.json at boot — it is where this project declares
|
|
50
|
+
# its storage buckets, so that the platform, the console and this process all
|
|
51
|
+
# read one list. An image without it boots believing nothing was declared, and
|
|
52
|
+
# every upload lands in the wrong bucket or 501s.
|
|
53
|
+
COPY rebase.json ./
|
|
39
54
|
|
|
40
55
|
# Install all deps (including devDependencies for build)
|
|
41
56
|
RUN pnpm install --frozen-lockfile
|
|
42
57
|
|
|
43
|
-
# Build config first, then backend
|
|
58
|
+
# Build config first, then the rest: both the frontend and the backend import it.
|
|
44
59
|
RUN pnpm --filter "*-config" run build
|
|
60
|
+
# {{#frontend}}
|
|
61
|
+
# The image serves the built site itself (see the `serveSPA` call in
|
|
62
|
+
# backend/src/index.ts), so the frontend is built here rather than deployed
|
|
63
|
+
# separately. Vite reads no API URL: same origin, same container.
|
|
64
|
+
RUN pnpm --filter "*-frontend" run build
|
|
65
|
+
# {{/frontend}}
|
|
45
66
|
RUN pnpm --filter "*-backend" run build
|
|
46
67
|
|
|
47
68
|
# Prune dev dependencies for a smaller runtime
|
|
@@ -63,10 +84,14 @@ RUN addgroup -g 1001 rebase && adduser -u 1001 -G rebase -s /bin/sh -D rebase
|
|
|
63
84
|
WORKDIR /app
|
|
64
85
|
|
|
65
86
|
# Copy only production artifacts
|
|
66
|
-
COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml /app/.npmrc ./
|
|
87
|
+
COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml /app/.npmrc /app/rebase.json ./
|
|
67
88
|
COPY --from=builder /app/node_modules ./node_modules
|
|
68
89
|
COPY --from=builder /app/backend ./backend
|
|
69
90
|
COPY --from=builder /app/config ./config
|
|
91
|
+
# {{#frontend}}
|
|
92
|
+
# The built site only — its sources and devDependencies stay in the builder.
|
|
93
|
+
COPY --from=builder /app/frontend/dist ./frontend/dist
|
|
94
|
+
# {{/frontend}}
|
|
70
95
|
|
|
71
96
|
# Create uploads directory
|
|
72
97
|
RUN mkdir -p /app/backend/uploads && chown -R rebase:rebase /app
|
|
@@ -3,12 +3,15 @@ import { cors } from "hono/cors";
|
|
|
3
3
|
import { secureHeaders } from "hono/secure-headers";
|
|
4
4
|
import { getRequestListener } from "@hono/node-server";
|
|
5
5
|
import { createServer } from "http";
|
|
6
|
+
import fs from "fs";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import { fileURLToPath } from "url";
|
|
8
9
|
import {
|
|
9
10
|
initializeRebaseBackend,
|
|
10
11
|
installShutdownHandlers,
|
|
12
|
+
// {{#frontend}}
|
|
11
13
|
serveSPA,
|
|
14
|
+
// {{/frontend}}
|
|
12
15
|
HonoEnv,
|
|
13
16
|
listenWithPortRetry,
|
|
14
17
|
cleanupDevPortFile,
|
|
@@ -17,10 +20,14 @@ import {
|
|
|
17
20
|
logger
|
|
18
21
|
} from "@rebasepro/server";
|
|
19
22
|
import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
|
|
23
|
+
// {{#collections}}
|
|
20
24
|
import { enums, relations, tables } from "./schema.generated.js";
|
|
25
|
+
// {{/collections}}
|
|
21
26
|
import { storageAuthorize } from "../../config/storage.js";
|
|
22
27
|
import { env } from "./env.js";
|
|
28
|
+
// {{#collections}}
|
|
23
29
|
import usersCollection from "../../config/collections/users.js";
|
|
30
|
+
// {{/collections}}
|
|
24
31
|
|
|
25
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
33
|
const __dirname = path.dirname(__filename);
|
|
@@ -86,21 +93,44 @@ async function startServer() {
|
|
|
86
93
|
const PORT = env.PORT;
|
|
87
94
|
const server = createServer(getRequestListener(app.fetch));
|
|
88
95
|
|
|
96
|
+
// `backend/crons` holds the scheduled jobs — nightly backups among them.
|
|
97
|
+
// Passed only when the directory exists, because a configured `cronsDir`
|
|
98
|
+
// with nothing in it mounts the cron routes and warns at every boot.
|
|
99
|
+
// Sibling-relative like `functionsDir`: both compile alongside this file, so
|
|
100
|
+
// the same path is right from source and from `backend/dist/backend/src`.
|
|
101
|
+
const cronsDir = path.resolve(__dirname, "../crons");
|
|
102
|
+
|
|
89
103
|
const backend = await initializeRebaseBackend({
|
|
104
|
+
// {{#collections}}
|
|
90
105
|
collectionsDir: path.resolve(__dirname, "../../config/collections"),
|
|
106
|
+
// {{/collections}}
|
|
107
|
+
// {{^collections}}
|
|
108
|
+
// No `collectionsDir`: this project declares no collections in code, so
|
|
109
|
+
// the server derives them from the live database schema at boot —
|
|
110
|
+
// exactly what the managed runtime did for it.
|
|
111
|
+
// {{/collections}}
|
|
91
112
|
functionsDir: path.resolve(__dirname, "../functions"),
|
|
113
|
+
cronsDir: fs.existsSync(cronsDir) ? cronsDir : undefined,
|
|
92
114
|
server,
|
|
93
115
|
app,
|
|
94
116
|
database: createPostgresAdapter({
|
|
95
117
|
connection: db,
|
|
118
|
+
// {{#collections}}
|
|
96
119
|
schema: { tables,
|
|
97
120
|
enums,
|
|
98
121
|
relations },
|
|
122
|
+
// {{/collections}}
|
|
99
123
|
adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
|
|
100
124
|
connectionString
|
|
101
125
|
}),
|
|
102
126
|
auth: {
|
|
127
|
+
// {{#collections}}
|
|
103
128
|
collection: usersCollection,
|
|
129
|
+
// {{/collections}}
|
|
130
|
+
// {{^collections}}
|
|
131
|
+
// No users collection is declared here, so auth falls back to its
|
|
132
|
+
// own default users table — the same fallback a headless bundle gets.
|
|
133
|
+
// {{/collections}}
|
|
104
134
|
jwtSecret,
|
|
105
135
|
accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
|
|
106
136
|
refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
|
|
@@ -173,9 +203,13 @@ pass: env.SMTP_PASS! }
|
|
|
173
203
|
//
|
|
174
204
|
// `requireAuth` answers 401 without a valid token; `requireAdmin` answers
|
|
175
205
|
// 403 without the `admin` role and must follow `requireAuth`. Note that
|
|
176
|
-
// `c.get("driver")` — the
|
|
177
|
-
// routers, so out here reach for
|
|
178
|
-
//
|
|
206
|
+
// `c.get("driver")` — the driver carrying the caller's identity — is only
|
|
207
|
+
// set inside the Rebase routers, so out here reach for
|
|
208
|
+
// `rebase.dataAsAdmin`. That one is **admin-scoped, not an RLS bypass**: it
|
|
209
|
+
// runs as `{ uid: "service", roles: ["admin"] }` and your policies still
|
|
210
|
+
// apply, evaluated against that identity — which is exactly an admin's
|
|
211
|
+
// reach, and therefore belongs behind one of those guards. (`rebase.sql()`
|
|
212
|
+
// is the real bypass: owner connection, no policies.)
|
|
179
213
|
|
|
180
214
|
// ─── Health check ─────────────────────────────────────────────
|
|
181
215
|
// Deliberately public: an orchestrator's probe has no token to send.
|
|
@@ -189,10 +223,20 @@ pass: env.SMTP_PASS! }
|
|
|
189
223
|
}, status);
|
|
190
224
|
});
|
|
191
225
|
|
|
192
|
-
//
|
|
226
|
+
// {{#frontend}}
|
|
227
|
+
// Serve the frontend in production.
|
|
228
|
+
//
|
|
229
|
+
// Four levels up, not two: in production this file runs compiled, from
|
|
230
|
+
// `backend/dist/backend/src`. The paths above are the same in both modes
|
|
231
|
+
// because `config/` is compiled alongside this file; `frontend/dist` is not
|
|
232
|
+
// in the compiled tree at all, so it stays where the repository put it.
|
|
233
|
+
// `serveSPA` only warns when the path is wrong, which is why the Dockerfile
|
|
234
|
+
// that builds this image also copies `frontend` — verify a mount by
|
|
235
|
+
// fetching `/`, never by reading the log.
|
|
193
236
|
if (isProduction) {
|
|
194
|
-
serveSPA(app, { frontendPath: path.
|
|
237
|
+
serveSPA(app, { frontendPath: path.resolve(__dirname, "../../../../frontend/dist") });
|
|
195
238
|
}
|
|
239
|
+
// {{/frontend}}
|
|
196
240
|
|
|
197
241
|
if (!isProduction) {
|
|
198
242
|
// Dev mode: retry the next port if the current one is in use
|
|
@@ -25,7 +25,7 @@ services:
|
|
|
25
25
|
image: postgres:18-alpine
|
|
26
26
|
restart: unless-stopped
|
|
27
27
|
environment:
|
|
28
|
-
POSTGRES_USER:
|
|
28
|
+
POSTGRES_USER: rebase_app
|
|
29
29
|
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
|
|
30
30
|
POSTGRES_DB: rebase
|
|
31
31
|
ports:
|
|
@@ -33,7 +33,7 @@ services:
|
|
|
33
33
|
volumes:
|
|
34
34
|
- postgres_data:/var/lib/postgresql
|
|
35
35
|
healthcheck:
|
|
36
|
-
test: ["CMD-SHELL", "pg_isready -U
|
|
36
|
+
test: ["CMD-SHELL", "pg_isready -U rebase_app -d rebase"]
|
|
37
37
|
interval: 5s
|
|
38
38
|
timeout: 5s
|
|
39
39
|
retries: 10
|
|
@@ -48,12 +48,20 @@ services:
|
|
|
48
48
|
- "${PORT:-3001}:3001"
|
|
49
49
|
env_file: .env
|
|
50
50
|
environment:
|
|
51
|
-
DATABASE_URL: postgresql://
|
|
52
|
-
ADMIN_CONNECTION_STRING: postgresql://
|
|
51
|
+
DATABASE_URL: postgresql://rebase_app:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
|
|
52
|
+
ADMIN_CONNECTION_STRING: postgresql://rebase_app:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
|
|
53
53
|
NODE_ENV: production
|
|
54
54
|
PORT: "3001"
|
|
55
|
+
# {{#frontend}}
|
|
55
56
|
# Your entrypoint serves the built frontend itself (see the `serveSPA`
|
|
56
|
-
# call in backend/src/index.ts
|
|
57
|
+
# call in backend/src/index.ts, and the frontend build stage in the
|
|
58
|
+
# Dockerfile), so this is one container, same origin.
|
|
59
|
+
# {{/frontend}}
|
|
60
|
+
# {{^frontend}}
|
|
61
|
+
# This image serves the API only — there is no frontend workspace in this
|
|
62
|
+
# project — so CORS_ORIGINS has to list the origins your clients are
|
|
63
|
+
# served from.
|
|
64
|
+
# {{/frontend}}
|
|
57
65
|
CORS_ORIGINS: ${CORS_ORIGINS:?set CORS_ORIGINS to the origin you browse to}
|
|
58
66
|
# A durable named volume, which is the case this flag acknowledges.
|
|
59
67
|
STORAGE_PATH: /uploads
|
|
@@ -21,5 +21,10 @@
|
|
|
21
21
|
//
|
|
22
22
|
// No `rootDir`: it would have to be `.` for both, and letting tsc infer the
|
|
23
23
|
// common root gets there without a second place to keep in sync.
|
|
24
|
-
|
|
24
|
+
//
|
|
25
|
+
// `crons/` too: `rebase build` compiles it through a generated tsconfig, but
|
|
26
|
+
// an ejected project builds with *this* file, so leaving it out meant an
|
|
27
|
+
// ejected server had no compiled jobs to load and silently stopped running
|
|
28
|
+
// every schedule.
|
|
29
|
+
"include": ["src/**/*", "functions/**/*", "crons/**/*"]
|
|
25
30
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# sslmode=disable matches the local docker-compose database, which has no TLS;
|
|
11
11
|
# schema tooling (atlas) would otherwise default to requiring SSL. Remove it
|
|
12
12
|
# when pointing at a managed/cloud database.
|
|
13
|
-
DATABASE_URL=postgresql://
|
|
13
|
+
DATABASE_URL=postgresql://rebase_app:changeme@localhost:5432/rebase?options=-c%20search_path=public&sslmode=disable
|
|
14
14
|
# DATABASE_URL=mongodb://localhost:27017/rebase
|
|
15
15
|
#
|
|
16
16
|
# Seeing "SSL is not enabled on the server"? Something in your environment
|
|
@@ -22,7 +22,7 @@ DATABASE_URL=postgresql://rebase:changeme@localhost:5432/rebase?options=-c%20sea
|
|
|
22
22
|
# downgrade SSL for you when you have asked for it, not even on localhost:
|
|
23
23
|
# quietly ignoring a security setting is worse than a clear failure. To opt out
|
|
24
24
|
# explicitly for a local database, say so:
|
|
25
|
-
# DATABASE_URL=postgresql://
|
|
25
|
+
# DATABASE_URL=postgresql://rebase_app:changeme@localhost:5432/rebase?options=-c%20search_path=public&sslmode=disable
|
|
26
26
|
|
|
27
27
|
# Separate admin connection string for migrations and schema operations (optional)
|
|
28
28
|
# Falls back to DATABASE_URL if not set
|
|
@@ -77,9 +77,18 @@ FRONTEND_URL=http://localhost:5173
|
|
|
77
77
|
# GOOGLE_CLIENT_ID=
|
|
78
78
|
|
|
79
79
|
# ── Frontend (Vite) ──────────────────────────────────────────────────────────
|
|
80
|
-
#
|
|
80
|
+
# Leave this EMPTY unless your API lives on a different origin than the site.
|
|
81
|
+
#
|
|
82
|
+
# Empty means "same origin as the page", which is what a deployed build wants:
|
|
83
|
+
# the frontend is served by the backend it talks to, and a same-origin bundle
|
|
84
|
+
# keeps working when you add a custom domain. A value here is baked in at BUILD
|
|
85
|
+
# time — `vite build` reads this file (frontend/vite.config.ts sets
|
|
86
|
+
# `envDir: ".."`), so a stray `http://localhost:3001` follows the bundle into
|
|
87
|
+
# production and points every request at the machine that ran the build.
|
|
88
|
+
#
|
|
89
|
+
# In dev you need nothing here either: `rebase dev` injects the backend port it
|
|
81
90
|
# actually bound, overriding this — see the PORT note above.
|
|
82
|
-
VITE_API_URL=
|
|
91
|
+
VITE_API_URL=
|
|
83
92
|
# VITE_GOOGLE_CLIENT_ID=
|
|
84
93
|
|
|
85
94
|
# ── Storage ───────────────────────────────────────────────────────────────────
|
|
@@ -30,10 +30,14 @@ import { defineFunction, requireAuth, requireAdmin } from "@rebasepro/server";
|
|
|
30
30
|
* slot over `app.use("/*", requireAuth)`: `use()` only covers routes declared
|
|
31
31
|
* *below* it, so a route appended later above it is silently unprotected.
|
|
32
32
|
*
|
|
33
|
-
* `rebase.dataAsAdmin` gives you admin-level access to your data
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* `rebase.dataAsAdmin` gives you admin-level access to your data: it runs as
|
|
34
|
+
* `{ uid: "service", roles: ["admin"] }`, which is **admin-scoped, not an RLS
|
|
35
|
+
* bypass** — your policies are still evaluated, just against that identity. So
|
|
36
|
+
* `policy.serverContext()` is false for it (that arm means "no uid at all"),
|
|
37
|
+
* and anything an `admin` user can reach, it can reach too. For request-scoped
|
|
38
|
+
* data access use c.get("user") and c.get("driver"), which carry the caller's
|
|
39
|
+
* identity. `rebase.sql()` is the real bypass — owner connection, no policies.
|
|
40
|
+
* (`rebase` also exposes auth, storage, email.)
|
|
37
41
|
*/
|
|
38
42
|
export default defineFunction((app, { rebase }) => {
|
|
39
43
|
void rebase; // available for dataAsAdmin/auth/storage/email — see commented usage below
|
|
@@ -36,5 +36,10 @@
|
|
|
36
36
|
// unnoticed until runtime. The inferred common root is already the project
|
|
37
37
|
// root because of `../config`, so this adds no `rootDir` and moves nothing
|
|
38
38
|
// that dist/ already contains.
|
|
39
|
-
|
|
39
|
+
//
|
|
40
|
+
// `crons/` for the same reason, and for one more: `rebase build` compiles it
|
|
41
|
+
// through a generated tsconfig, but an ejected project builds with *this*
|
|
42
|
+
// file — so leaving crons out meant an ejected server had no compiled jobs to
|
|
43
|
+
// load and stopped running every schedule, nightly backups included.
|
|
44
|
+
"include": ["src/**/*", "functions/**/*", "crons/**/*", "../config/**/*", "drizzle.config.ts"]
|
|
40
45
|
}
|
|
@@ -33,7 +33,7 @@ services:
|
|
|
33
33
|
image: postgres:18-alpine
|
|
34
34
|
restart: unless-stopped
|
|
35
35
|
environment:
|
|
36
|
-
POSTGRES_USER:
|
|
36
|
+
POSTGRES_USER: rebase_app
|
|
37
37
|
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
|
|
38
38
|
POSTGRES_DB: rebase
|
|
39
39
|
# Published so `rebase db push` can reach it from the host. Remove this
|
|
@@ -47,7 +47,7 @@ services:
|
|
|
47
47
|
# The runtime must not start before the database can answer, or its first
|
|
48
48
|
# boot fails on a connection refused and the container restarts for no
|
|
49
49
|
# reason a reader would understand.
|
|
50
|
-
test: ["CMD-SHELL", "pg_isready -U
|
|
50
|
+
test: ["CMD-SHELL", "pg_isready -U rebase_app -d rebase"]
|
|
51
51
|
interval: 5s
|
|
52
52
|
timeout: 5s
|
|
53
53
|
retries: 10
|
|
@@ -75,8 +75,8 @@ services:
|
|
|
75
75
|
ports:
|
|
76
76
|
- "${PORT:-3001}:3001"
|
|
77
77
|
environment:
|
|
78
|
-
DATABASE_URL: postgresql://
|
|
79
|
-
ADMIN_CONNECTION_STRING: postgresql://
|
|
78
|
+
DATABASE_URL: postgresql://rebase_app:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
|
|
79
|
+
ADMIN_CONNECTION_STRING: postgresql://rebase_app:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
|
|
80
80
|
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env — `rebase init` generates one}
|
|
81
81
|
REBASE_SERVICE_KEY: ${REBASE_SERVICE_KEY:?set REBASE_SERVICE_KEY in .env — `rebase init` generates one}
|
|
82
82
|
NODE_ENV: production
|