@rebasepro/server 0.21.0 → 0.21.1
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/README.md +29 -11
- package/dist/api/errors.d.ts +41 -0
- package/dist/auth/admin-user-ops.d.ts +23 -1
- package/dist/auth/auth-hooks.d.ts +4 -0
- package/dist/auth/reset-password-admin.d.ts +4 -0
- package/dist/auth/token-revocation.d.ts +39 -5
- package/dist/{auth-gzoRjnuh.js → auth-BS4WBA10.js} +1636 -1565
- package/dist/auth-BS4WBA10.js.map +1 -0
- package/dist/backup/backup-common.d.ts +2 -1
- package/dist/{backup-DGu0v9Ku.js → backup-DzI9jLwc.js} +38 -10
- package/dist/backup-DzI9jLwc.js.map +1 -0
- package/dist/collections/validate-config.d.ts +4 -2
- package/dist/{cron-routes-BamgO0Xr.js → cron-routes-B_wlLybo.js} +3 -3
- package/dist/{cron-routes-BamgO0Xr.js.map → cron-routes-B_wlLybo.js.map} +1 -1
- package/dist/{cron-scheduler-D47tdB9T.js → cron-scheduler-COPQxlEq.js} +59 -10
- package/dist/cron-scheduler-COPQxlEq.js.map +1 -0
- package/dist/{cron-store-yXOyIEmn.js → cron-store-Dp0X7zRB.js} +2 -2
- package/dist/{cron-store-yXOyIEmn.js.map → cron-store-Dp0X7zRB.js.map} +1 -1
- package/dist/ddl-bootstrap-CfNvxMuK.js.map +1 -1
- package/dist/deploy/pod-contract.d.ts +14 -0
- package/dist/{errors-DMImyqyR.js → errors-DWsX4yTd.js} +55 -18
- package/dist/errors-DWsX4yTd.js.map +1 -0
- package/dist/{function-routes-C4nB2h0z.js → function-routes-Chet4-lB.js} +2 -2
- package/dist/{function-routes-C4nB2h0z.js.map → function-routes-Chet4-lB.js.map} +1 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-hBGuXcZc.js → history-recorder-5VKT7nss.js} +2 -2
- package/dist/{history-recorder-hBGuXcZc.js.map → history-recorder-5VKT7nss.js.map} +1 -1
- package/dist/{history-store-C9EtGtoL.js → history-store-By2R_G1b.js} +2 -2
- package/dist/{history-store-C9EtGtoL.js.map → history-store-By2R_G1b.js.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +286 -245
- package/dist/index.es.js.map +1 -1
- package/dist/init/middlewares.d.ts +19 -1
- package/dist/init/storage.d.ts +17 -0
- package/dist/init.d.ts +6 -2
- package/dist/{jobs-DZ8DLLcU.js → jobs-1IkI4Lle.js} +2 -2
- package/dist/{jobs-DZ8DLLcU.js.map → jobs-1IkI4Lle.js.map} +1 -1
- package/dist/{logs-routes-DnJINsMu.js → logs-routes-3EEzPjhl.js} +2 -2
- package/dist/{logs-routes-DnJINsMu.js.map → logs-routes-3EEzPjhl.js.map} +1 -1
- package/dist/{openapi-generator-CAFKF36E.js → openapi-generator-BDpoKZQ9.js} +2 -2
- package/dist/{openapi-generator-CAFKF36E.js.map → openapi-generator-BDpoKZQ9.js.map} +1 -1
- package/dist/{query-parser-uqzkZ82i.js → query-parser-C-rl30ce.js} +3 -3
- package/dist/{query-parser-uqzkZ82i.js.map → query-parser-C-rl30ce.js.map} +1 -1
- package/dist/{request-timeout-BR-OBwES.js → request-timeout-C_4C2BeR.js} +2 -2
- package/dist/{request-timeout-BR-OBwES.js.map → request-timeout-C_4C2BeR.js.map} +1 -1
- package/dist/{schema-editor-routes-BKOmdf4M.js → schema-editor-routes-DdLihzp0.js} +2 -2
- package/dist/{schema-editor-routes-BKOmdf4M.js.map → schema-editor-routes-DdLihzp0.js.map} +1 -1
- package/dist/src-Br6ARbs6.js.map +1 -1
- package/dist/{src-DW1cQ_28.js → src-Caz7nREj.js} +2 -3
- package/dist/src-Caz7nREj.js.map +1 -0
- package/package.json +5 -5
- package/dist/auth-gzoRjnuh.js.map +0 -1
- package/dist/backup-DGu0v9Ku.js.map +0 -1
- package/dist/cron-scheduler-D47tdB9T.js.map +0 -1
- package/dist/errors-DMImyqyR.js.map +0 -1
- package/dist/src-DW1cQ_28.js.map +0 -1
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ pnpm add @rebasepro/server
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
ESM-only: `"type": "module"` with no CommonJS build, so it is loaded with
|
|
12
|
-
`import`.
|
|
13
|
-
`require(esm)
|
|
12
|
+
`import`. It needs Node `>=22.22.0` (its `engines` floor), where `require()`
|
|
13
|
+
of it resolves too: Node has supported `require(esm)` since 22.12.
|
|
14
14
|
|
|
15
15
|
## What This Package Does
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ This is the central orchestrator for any Rebase backend. It provides the framewo
|
|
|
21
21
|
| Export | Description |
|
|
22
22
|
|--------|-------------|
|
|
23
23
|
| `initializeRebaseBackend(config)` | Main entry point. Wires up drivers, auth, storage, API routes, cron, and custom functions. Returns a `RebaseBackendInstance`. |
|
|
24
|
-
| `rebase` | Server-side singleton (`
|
|
24
|
+
| `rebase` | Server-side singleton — a `RebaseServerClient` (from `@rebasepro/types`): the client without `data`, plus `dataAsAdmin`. Available after init. Admin-scoped access to data (RLS is evaluated as the service identity, not skipped), auth, email, and storage. |
|
|
25
25
|
| `loadEnv()` | Validates `process.env` against the Rebase env schema (Zod). Auto-generates dev secrets. Supports `extend` for custom vars. |
|
|
26
26
|
| `serveSPA(app, config)` | Mounts SPA static-file serving + index.html fallback on a Hono app. |
|
|
27
27
|
| `RebaseBackendConfig` | Config type for `initializeRebaseBackend`. |
|
|
@@ -31,43 +31,61 @@ This is the central orchestrator for any Rebase backend. It provides the framewo
|
|
|
31
31
|
| `z` | The runtime's own Zod instance. Build `loadEnv({ extend })` schemas with this one — a schema from a second copy of zod is silently ignored. |
|
|
32
32
|
| `_setRebaseMock` / `_resetRebaseMock` | Test helpers to mock the `rebase` singleton (NODE_ENV=test only). |
|
|
33
33
|
|
|
34
|
-
Also
|
|
34
|
+
Also exports `HonoEnv`, the auth, email, storage, history and cron modules, custom functions, logging utilities, and the driver registry. The abstract driver interfaces (`DatabaseAdapter`, `AuthAdapter`, `DataDriver`, …) are not re-exported: import them from `@rebasepro/types`.
|
|
35
|
+
|
|
36
|
+
`@rebasepro/server/functions` is the portable surface a function file imports — `defineFunction`, the `requireAuth`/`requireAdmin` guards, `getUser`, `rebase` and the rest. The package also ships the `rebase-server` binary, which runs a built project bundle (`rebase-server ./dist-bundle`); it is what the `rebasepro/server` image executes.
|
|
35
37
|
|
|
36
38
|
## Quick Start
|
|
37
39
|
|
|
38
40
|
```typescript
|
|
39
|
-
import {
|
|
41
|
+
import { createServer } from "node:http";
|
|
42
|
+
import { getRequestListener } from "@hono/node-server";
|
|
40
43
|
import { Hono } from "hono";
|
|
41
|
-
import { initializeRebaseBackend, loadEnv, serveSPA } from "@rebasepro/server";
|
|
44
|
+
import { initializeRebaseBackend, loadEnv, serveSPA, type HonoEnv } from "@rebasepro/server";
|
|
42
45
|
import { createPostgresAdapter } from "@rebasepro/server-postgres";
|
|
43
46
|
|
|
44
47
|
// 1. Load and validate environment
|
|
45
48
|
const env = loadEnv();
|
|
46
49
|
|
|
47
50
|
// 2. Create Hono app + HTTP server
|
|
48
|
-
const app = new Hono();
|
|
49
|
-
const server =
|
|
51
|
+
const app = new Hono<HonoEnv>();
|
|
52
|
+
const server = createServer(getRequestListener(app.fetch));
|
|
50
53
|
|
|
51
54
|
// 3. Initialize Rebase
|
|
52
55
|
const backend = await initializeRebaseBackend({
|
|
53
56
|
app,
|
|
54
57
|
server,
|
|
55
|
-
database: createPostgresAdapter({ connection: db, schema }),
|
|
58
|
+
database: createPostgresAdapter({ connection: db, schema: { tables, enums, relations } }),
|
|
56
59
|
collections: myCollections,
|
|
57
60
|
auth: {
|
|
58
61
|
collection: defaultUsersCollection,
|
|
59
62
|
jwtSecret: env.JWT_SECRET,
|
|
60
63
|
allowRegistration: env.ALLOW_REGISTRATION,
|
|
61
64
|
serviceKey: env.REBASE_SERVICE_KEY,
|
|
62
|
-
google: { clientId: env.GOOGLE_CLIENT_ID },
|
|
65
|
+
google: env.GOOGLE_CLIENT_ID ? { clientId: env.GOOGLE_CLIENT_ID } : undefined,
|
|
66
|
+
},
|
|
67
|
+
storage: {
|
|
68
|
+
type: "s3",
|
|
69
|
+
bucket: env.S3_BUCKET!,
|
|
70
|
+
accessKeyId: env.S3_ACCESS_KEY_ID!,
|
|
71
|
+
secretAccessKey: env.S3_SECRET_ACCESS_KEY!,
|
|
72
|
+
},
|
|
73
|
+
// Storage is not under row-level security. In production the server refuses
|
|
74
|
+
// to boot storage without an access model: this hook (or `storagePolicies`),
|
|
75
|
+
// or `storagePublicRead: true` for a bucket that really is public.
|
|
76
|
+
storageAuthorize: async ({ key, user, operation }) => {
|
|
77
|
+
if (!user) return false;
|
|
78
|
+
const [ownerId] = key.split("/");
|
|
79
|
+
return ownerId === user.uid || operation === "read";
|
|
63
80
|
},
|
|
64
|
-
storage: { type: "s3", bucket: env.S3_BUCKET },
|
|
65
81
|
functionsDir: "./functions",
|
|
66
82
|
cronsDir: "./crons",
|
|
67
83
|
});
|
|
68
84
|
|
|
69
85
|
// 4. Optionally serve a frontend SPA
|
|
70
86
|
serveSPA(app, { frontendPath: "./frontend/dist" });
|
|
87
|
+
|
|
88
|
+
server.listen(env.PORT);
|
|
71
89
|
```
|
|
72
90
|
|
|
73
91
|
## Related Packages
|
package/dist/api/errors.d.ts
CHANGED
|
@@ -109,6 +109,47 @@ export interface RebaseApiError extends Error {
|
|
|
109
109
|
code?: string;
|
|
110
110
|
details?: unknown;
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The answer an error chose for itself, read the same way at every door.
|
|
114
|
+
*
|
|
115
|
+
* @see declaredErrorAnswer
|
|
116
|
+
*/
|
|
117
|
+
export interface DeclaredErrorAnswer {
|
|
118
|
+
/** The HTTP status the error carries. A socket frame has no slot for it. */
|
|
119
|
+
status: number;
|
|
120
|
+
code: string;
|
|
121
|
+
message: string;
|
|
122
|
+
details?: unknown;
|
|
123
|
+
/** See {@link ApiError.expected}: log it at debug, not warn. */
|
|
124
|
+
expected: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The status, code and message an error carries as its own answer — or
|
|
128
|
+
* `undefined` for an error that carries none, which is a server fault and gets
|
|
129
|
+
* masked.
|
|
130
|
+
*
|
|
131
|
+
* Two classes carry one. The server's `ApiError`, and `RebaseApiError` (or its
|
|
132
|
+
* `RebaseClientError` subclass) from `@rebasepro/types` once it has a status.
|
|
133
|
+
* The second is the browser-safe class: a `config/collections/*.ts` file is
|
|
134
|
+
* bundled into the admin SPA and cannot import this package, so it is what a
|
|
135
|
+
* collection callback throws, and what a callback refusal becomes —
|
|
136
|
+
* `callbackRefusal` returns one, and `toCallbackError` wraps anything thrown
|
|
137
|
+
* that does not already carry a status.
|
|
138
|
+
*
|
|
139
|
+
* One function because several doors turn an error into an answer: the REST
|
|
140
|
+
* error handler, the two WebSocket servers, and the Postgres realtime
|
|
141
|
+
* subscriptions. Each used to list the classes it recognised by hand. The
|
|
142
|
+
* sockets listed only `ApiError`, so a `beforeDelete` veto that REST
|
|
143
|
+
* answered as 400 `CALLBACK_REJECTED` with the author's message reached the
|
|
144
|
+
* admin panel — which writes through the socket — as `INTERNAL_ERROR`, and in
|
|
145
|
+
* production as "An unexpected error occurred".
|
|
146
|
+
*
|
|
147
|
+
* Matched by name as well as `instanceof`: a monorepo can resolve two copies of
|
|
148
|
+
* a package, and `instanceof` is false across them. Name matching is also why
|
|
149
|
+
* this file needs no runtime import of `@rebasepro/types`, which it may not
|
|
150
|
+
* have — it is in the graph of `@rebasepro/server/functions`.
|
|
151
|
+
*/
|
|
152
|
+
export declare function declaredErrorAnswer(error: unknown): DeclaredErrorAnswer | undefined;
|
|
112
153
|
/**
|
|
113
154
|
* Hono error-handling middleware (`app.onError`).
|
|
114
155
|
* Converts any error into the canonical `{ error: { message, code } }` shape.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { AuthRepository } from "./interfaces.js";
|
|
13
13
|
import type { EmailService, EmailConfig } from "../email/index.js";
|
|
14
14
|
import type { ResolvedAuthHooks } from "./auth-hooks.js";
|
|
15
|
-
import type { AuthCollectionConfig } from "@rebasepro/types";
|
|
15
|
+
import type { AuthCollectionConfig, UserCreationFinalizeResult, UserCreationPrepareResult } from "@rebasepro/types";
|
|
16
16
|
/**
|
|
17
17
|
* Generate a cryptographically secure random password that meets strength requirements.
|
|
18
18
|
*
|
|
@@ -78,3 +78,25 @@ export declare function finalizeAdminUserCreation(entity: {
|
|
|
78
78
|
invitationSent: boolean;
|
|
79
79
|
emailDeliveryFailed?: boolean;
|
|
80
80
|
}>;
|
|
81
|
+
/**
|
|
82
|
+
* The step after a new user's row is written: who delivers the credentials,
|
|
83
|
+
* and what the create response says about it.
|
|
84
|
+
*
|
|
85
|
+
* A create hook that ran owns delivery. What it reported is the answer: the
|
|
86
|
+
* invitation it says it sent, and the temporary password it chose, which the
|
|
87
|
+
* admin has to see because it is the one the user will be told. The framework
|
|
88
|
+
* sends nothing of its own. Without a hook, `finalize` runs: the built-in
|
|
89
|
+
* invitation, or the generated password when email is not configured or fails.
|
|
90
|
+
*
|
|
91
|
+
* Both doors that create a user call this, the auth collection's REST route
|
|
92
|
+
* and `POST /admin/users`. The second used to call `finalizeAdminUserCreation`
|
|
93
|
+
* directly and read neither `hookHandledEmail` nor `invitationSent`, so it
|
|
94
|
+
* handled a hook's password as if the framework had generated it. With email
|
|
95
|
+
* configured it sent its own invitation, a second one when the hook had sent
|
|
96
|
+
* its own, and left the password out of the response. The decision lives here
|
|
97
|
+
* so that the two doors cannot disagree about it again.
|
|
98
|
+
*
|
|
99
|
+
* The result has exactly the fields a create response carries, with absent
|
|
100
|
+
* ones left out.
|
|
101
|
+
*/
|
|
102
|
+
export declare function completeUserCreation(prepared: UserCreationPrepareResult, finalize?: (clearPassword: string | undefined) => Promise<UserCreationFinalizeResult>): Promise<UserCreationFinalizeResult>;
|
|
@@ -222,6 +222,10 @@ export interface AuthHooks {
|
|
|
222
222
|
/**
|
|
223
223
|
* Optional hook to customize or override the default password reset flow via the admin panel.
|
|
224
224
|
* When provided, this replaces the built-in password reset token generation, hashing, and email logic.
|
|
225
|
+
*
|
|
226
|
+
* A `temporaryPassword` it returns becomes the account's password: the route hashes and writes
|
|
227
|
+
* it (the same contract as a collection's `auth.onResetPassword`), so the hook need not write it
|
|
228
|
+
* through `authRepo`. The account's existing sessions end whatever the hook returns.
|
|
225
229
|
*/
|
|
226
230
|
onAdminResetPassword?(uid: string, ctx: {
|
|
227
231
|
authRepo: AuthRepository;
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* 1. Collection-level hook (`auth.onResetPassword` on the collection)
|
|
6
6
|
* 2. Backend-level hook (`AuthHooks.onAdminResetPassword`)
|
|
7
7
|
* 3. Built-in default (send reset email, or generate temp password)
|
|
8
|
+
*
|
|
9
|
+
* Whichever of them runs, the account's existing sessions end, and a
|
|
10
|
+
* `temporaryPassword` in its result becomes the account's password before the
|
|
11
|
+
* response shows it to the admin.
|
|
8
12
|
*/
|
|
9
13
|
import { Hono } from "hono";
|
|
10
14
|
import type { AuthRepository } from "./interfaces.js";
|
|
@@ -3,11 +3,12 @@ import type { AccessTokenPayload } from "./jwt.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Has this access token been revoked?
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Everything that ends every session a user holds — a password change or reset
|
|
7
|
+
* of any kind, and `DELETE /auth/sessions` — goes through
|
|
8
|
+
* {@link revokeAllSessions}, which stamps a `tokensValidAfter` watermark on the
|
|
9
|
+
* user. It also deletes refresh-token rows, which is what made the gap easy to
|
|
10
|
+
* miss: the session really is gone, and the *refresh* path really does check
|
|
11
|
+
* the watermark — so signing out looked like it worked.
|
|
11
12
|
*
|
|
12
13
|
* The access token was untouched. It is a bearer credential that nothing
|
|
13
14
|
* consulted a database about, so it stayed valid for its full lifetime after
|
|
@@ -36,3 +37,36 @@ import type { AccessTokenPayload } from "./jwt.js";
|
|
|
36
37
|
* failure is logged at warn so it is visible rather than silent.
|
|
37
38
|
*/
|
|
38
39
|
export declare function isAccessTokenRevoked(authRepo: Pick<AuthRepository, "getTokensValidAfter">, payload: Pick<AccessTokenPayload, "uid" | "iat">): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* End every session this user holds, on every device.
|
|
42
|
+
*
|
|
43
|
+
* Two writes, because each covers what the other cannot. Deleting the refresh
|
|
44
|
+
* rows ends the sessions that exist at this instant, and on a repository
|
|
45
|
+
* without the watermark it is the only revocation there is. The watermark
|
|
46
|
+
* voids what the delete cannot see: a refresh already in flight that inserts
|
|
47
|
+
* its rotated token a moment after the delete ran, and every access token
|
|
48
|
+
* already handed out, which {@link isAccessTokenRevoked} refuses from here on.
|
|
49
|
+
*
|
|
50
|
+
* The watermark write does not fail the caller. By the time it runs the
|
|
51
|
+
* credential has usually already changed and the rows are gone, so a 500 would
|
|
52
|
+
* tell someone their password did not change when it did. It is logged, because
|
|
53
|
+
* a failure here leaves access tokens working until they expire.
|
|
54
|
+
*/
|
|
55
|
+
export declare function revokeAllSessions(authRepo: Pick<AuthRepository, "deleteAllRefreshTokensForUser" | "setTokensValidAfter">, uid: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Replace a user's password and end every session they hold.
|
|
58
|
+
*
|
|
59
|
+
* The one way a password hash is written over an existing account. A new
|
|
60
|
+
* password is what someone sets when they believe the old one — or a session
|
|
61
|
+
* signed in with it — is in someone else's hands, so a password change that
|
|
62
|
+
* leaves those sessions alive does not do the thing it was done for.
|
|
63
|
+
*
|
|
64
|
+
* It used to be a pair of lines repeated after each `updatePassword`, and it
|
|
65
|
+
* reached the two self-service routes and none of the admin ones: an
|
|
66
|
+
* administrator resetting a phished account left the attacker's refresh token
|
|
67
|
+
* minting access tokens for the rest of its lifetime.
|
|
68
|
+
* `test/password-change-revokes-sessions.test.ts` holds every route that sets a
|
|
69
|
+
* password to this, and fails if anything but this function calls
|
|
70
|
+
* `updatePassword`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function replaceUserPassword(authRepo: Pick<AuthRepository, "updatePassword" | "deleteAllRefreshTokensForUser" | "setTokensValidAfter">, uid: string, passwordHash: string): Promise<void>;
|