@rebasepro/server 0.9.1-canary.ff338b5 → 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/errors.d.ts +16 -1
- package/dist/api/rest/api-generator.d.ts +9 -1
- package/dist/api/rest/query-parser.d.ts +17 -1
- package/dist/api/rest/write-validation.d.ts +3 -0
- package/dist/api/types.d.ts +2 -2
- package/dist/auth/admin-users-route.d.ts +3 -3
- package/dist/auth/auth-hooks.d.ts +7 -7
- package/dist/auth/interfaces.d.ts +92 -29
- package/dist/auth/jwt.d.ts +25 -3
- package/dist/auth/magic-link-routes.d.ts +2 -2
- package/dist/auth/mfa-routes.d.ts +1 -1
- package/dist/auth/middleware.d.ts +3 -3
- package/dist/auth/reset-password-admin.d.ts +1 -1
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/session-routes.d.ts +2 -2
- 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 +6763 -2451
- 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 +44 -1
- package/dist/{jwt-BJzQOa8a.js → jwt-Dj7r7QX7.js} +40 -28
- package/dist/{jwt-BJzQOa8a.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/dist/utils/sql.d.ts +2 -2
- 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>;
|
package/dist/api/errors.d.ts
CHANGED
|
@@ -9,9 +9,24 @@ export declare class ApiError extends Error {
|
|
|
9
9
|
readonly statusCode: number;
|
|
10
10
|
readonly code: string;
|
|
11
11
|
readonly details?: unknown;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Whether this outcome is a routine part of normal operation rather than
|
|
14
|
+
* something an operator should look at. Expected errors log at debug; every
|
|
15
|
+
* other operational error logs at warn.
|
|
16
|
+
*
|
|
17
|
+
* The motivating case is `POST /auth/refresh` with no session: clients
|
|
18
|
+
* refresh on page load before they know whether one exists, so every
|
|
19
|
+
* anonymous page view is a 401 — correct, and not worth a warning line.
|
|
20
|
+
*/
|
|
21
|
+
readonly expected: boolean;
|
|
22
|
+
constructor(statusCode: number, code: string, message: string, details?: unknown, expected?: boolean);
|
|
13
23
|
static badRequest(message: string, code?: string, details?: unknown): ApiError;
|
|
14
24
|
static unauthorized(message: string, code?: string): ApiError;
|
|
25
|
+
/**
|
|
26
|
+
* A 401 that is a normal outcome, not an incident — logged at debug.
|
|
27
|
+
* See {@link ApiError.expected}.
|
|
28
|
+
*/
|
|
29
|
+
static unauthenticated(message: string, code?: string): ApiError;
|
|
15
30
|
static forbidden(message: string, code?: string): ApiError;
|
|
16
31
|
static notFound(message: string, code?: string): ApiError;
|
|
17
32
|
static conflict(message: string, code?: string): ApiError;
|
|
@@ -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;
|
|
@@ -12,8 +12,11 @@ import { CollectionConfig } from "@rebasepro/types";
|
|
|
12
12
|
* columns, so the set is exact);
|
|
13
13
|
* - the foreign-key column behind an owning relation, which callers may write
|
|
14
14
|
* directly instead of through the relation property;
|
|
15
|
+
* - anything named in `options.extraKnownFields` — for an auth collection the
|
|
16
|
+
* credential keys the auth adapter consumes before a row is ever built;
|
|
15
17
|
* - nothing else. `id` in particular is not automatically known — see below.
|
|
16
18
|
*/
|
|
17
19
|
export declare function assertKnownWriteFields(values: Record<string, unknown>, collection: CollectionConfig, options?: {
|
|
18
20
|
rowIndex?: number;
|
|
21
|
+
extraKnownFields?: readonly string[];
|
|
19
22
|
}): void;
|
package/dist/api/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VectorSearchParams, LogicalCondition, FilterValues } from "@rebasepro/types";
|
|
2
|
-
import { AuthResult } from "../auth/middleware";
|
|
2
|
+
import type { AuthResult } from "../auth/middleware";
|
|
3
3
|
import { DataDriver } from "@rebasepro/types";
|
|
4
4
|
import type { ApiKeyMasked } from "../auth/api-keys/api-key-types";
|
|
5
5
|
/**
|
|
@@ -9,7 +9,7 @@ import type { ApiKeyMasked } from "../auth/api-keys/api-key-types";
|
|
|
9
9
|
export type HonoEnv = {
|
|
10
10
|
Variables: {
|
|
11
11
|
user?: AuthResult | {
|
|
12
|
-
|
|
12
|
+
uid?: string;
|
|
13
13
|
roles?: string[];
|
|
14
14
|
};
|
|
15
15
|
driver?: DataDriver;
|
|
@@ -135,7 +135,7 @@ export interface AuthHooks {
|
|
|
135
135
|
*
|
|
136
136
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
137
137
|
*/
|
|
138
|
-
afterLogout?(
|
|
138
|
+
afterLogout?(uid: string): Promise<void>;
|
|
139
139
|
/**
|
|
140
140
|
* Called after successful MFA verification.
|
|
141
141
|
*
|
|
@@ -143,12 +143,12 @@ export interface AuthHooks {
|
|
|
143
143
|
*
|
|
144
144
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
145
145
|
*/
|
|
146
|
-
onMfaVerified?(
|
|
146
|
+
onMfaVerified?(uid: string, factorId: string): Promise<void>;
|
|
147
147
|
/**
|
|
148
148
|
* Customize JWT access token claims before signing.
|
|
149
149
|
*
|
|
150
150
|
* Return the modified claims object. The returned claims are merged
|
|
151
|
-
* into the JWT payload alongside standard claims (
|
|
151
|
+
* into the JWT payload alongside standard claims (uid, roles).
|
|
152
152
|
*
|
|
153
153
|
* @param claims - The default claims that would be included.
|
|
154
154
|
* @param user - The authenticated user data.
|
|
@@ -178,14 +178,14 @@ export interface AuthHooks {
|
|
|
178
178
|
*
|
|
179
179
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
180
180
|
*/
|
|
181
|
-
onPasswordReset?(
|
|
181
|
+
onPasswordReset?(uid: string): Promise<void>;
|
|
182
182
|
/**
|
|
183
183
|
* Called before a user is deleted.
|
|
184
184
|
*
|
|
185
185
|
* Throw an error to prevent deletion (e.g. for users with active
|
|
186
186
|
* subscriptions, pending transactions, etc.).
|
|
187
187
|
*/
|
|
188
|
-
beforeUserDelete?(
|
|
188
|
+
beforeUserDelete?(uid: string): Promise<void>;
|
|
189
189
|
/**
|
|
190
190
|
* Called after a user is deleted.
|
|
191
191
|
*
|
|
@@ -193,7 +193,7 @@ export interface AuthHooks {
|
|
|
193
193
|
*
|
|
194
194
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
195
195
|
*/
|
|
196
|
-
afterUserDelete?(
|
|
196
|
+
afterUserDelete?(uid: string): Promise<void>;
|
|
197
197
|
/**
|
|
198
198
|
* Optional hook to customize or override the default user creation flow via the admin panel/REST API.
|
|
199
199
|
* When provided, this replaces the built-in password generation, hashing, and invitation email logic.
|
|
@@ -212,7 +212,7 @@ export interface AuthHooks {
|
|
|
212
212
|
* Optional hook to customize or override the default password reset flow via the admin panel.
|
|
213
213
|
* When provided, this replaces the built-in password reset token generation, hashing, and email logic.
|
|
214
214
|
*/
|
|
215
|
-
onAdminResetPassword?(
|
|
215
|
+
onAdminResetPassword?(uid: string, ctx: {
|
|
216
216
|
authRepo: AuthRepository;
|
|
217
217
|
emailService?: EmailService;
|
|
218
218
|
emailConfig?: EmailConfig;
|
|
@@ -40,7 +40,7 @@ export interface CreateUserData {
|
|
|
40
40
|
*/
|
|
41
41
|
export interface UserIdentityData {
|
|
42
42
|
id: string;
|
|
43
|
-
|
|
43
|
+
uid: string;
|
|
44
44
|
provider: string;
|
|
45
45
|
providerId: string;
|
|
46
46
|
profileData?: Record<string, unknown> | null;
|
|
@@ -113,25 +113,52 @@ export interface CreateRoleData {
|
|
|
113
113
|
*/
|
|
114
114
|
export interface RefreshTokenInfo {
|
|
115
115
|
id: string;
|
|
116
|
-
|
|
116
|
+
uid: string;
|
|
117
117
|
tokenHash: string;
|
|
118
118
|
expiresAt: Date;
|
|
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
|
|
125
152
|
*/
|
|
126
153
|
export interface PasswordResetTokenInfo {
|
|
127
|
-
|
|
154
|
+
uid: string;
|
|
128
155
|
expiresAt: Date;
|
|
129
156
|
}
|
|
130
157
|
/**
|
|
131
158
|
* Magic link token info
|
|
132
159
|
*/
|
|
133
160
|
export interface MagicLinkTokenInfo {
|
|
134
|
-
|
|
161
|
+
uid: string;
|
|
135
162
|
expiresAt: Date;
|
|
136
163
|
}
|
|
137
164
|
/**
|
|
@@ -185,11 +212,11 @@ export interface UserRepository {
|
|
|
185
212
|
/**
|
|
186
213
|
* Get all identities linked to a user
|
|
187
214
|
*/
|
|
188
|
-
getUserIdentities(
|
|
215
|
+
getUserIdentities(uid: string): Promise<UserIdentityData[]>;
|
|
189
216
|
/**
|
|
190
217
|
* Link a new OAuth identity to a user
|
|
191
218
|
*/
|
|
192
|
-
linkUserIdentity(
|
|
219
|
+
linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
|
|
193
220
|
/**
|
|
194
221
|
* Update a user
|
|
195
222
|
*/
|
|
@@ -225,23 +252,23 @@ export interface UserRepository {
|
|
|
225
252
|
/**
|
|
226
253
|
* Get roles for a user
|
|
227
254
|
*/
|
|
228
|
-
getUserRoles(
|
|
255
|
+
getUserRoles(uid: string): Promise<RoleData[]>;
|
|
229
256
|
/**
|
|
230
257
|
* Get role IDs for a user
|
|
231
258
|
*/
|
|
232
|
-
getUserRoleIds(
|
|
259
|
+
getUserRoleIds(uid: string): Promise<string[]>;
|
|
233
260
|
/**
|
|
234
261
|
* Set roles for a user (replaces existing roles)
|
|
235
262
|
*/
|
|
236
|
-
setUserRoles(
|
|
263
|
+
setUserRoles(uid: string, roleIds: string[]): Promise<void>;
|
|
237
264
|
/**
|
|
238
265
|
* Assign a specific role to a new user
|
|
239
266
|
*/
|
|
240
|
-
assignDefaultRole(
|
|
267
|
+
assignDefaultRole(uid: string, roleId: string): Promise<void>;
|
|
241
268
|
/**
|
|
242
269
|
* Get user with their roles
|
|
243
270
|
*/
|
|
244
|
-
getUserWithRoles(
|
|
271
|
+
getUserWithRoles(uid: string): Promise<{
|
|
245
272
|
user: UserData;
|
|
246
273
|
roles: RoleData[];
|
|
247
274
|
} | null>;
|
|
@@ -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
|
*/
|
|
@@ -292,19 +355,19 @@ export interface TokenRepository {
|
|
|
292
355
|
/**
|
|
293
356
|
* Delete all refresh tokens for a user
|
|
294
357
|
*/
|
|
295
|
-
deleteAllRefreshTokensForUser(
|
|
358
|
+
deleteAllRefreshTokensForUser(uid: string): Promise<void>;
|
|
296
359
|
/**
|
|
297
360
|
* List all refresh tokens for a user
|
|
298
361
|
*/
|
|
299
|
-
listRefreshTokensForUser(
|
|
362
|
+
listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
|
|
300
363
|
/**
|
|
301
364
|
* Delete a specific refresh token by its primary key ID
|
|
302
365
|
*/
|
|
303
|
-
deleteRefreshTokenById(id: string,
|
|
366
|
+
deleteRefreshTokenById(id: string, uid: string): Promise<void>;
|
|
304
367
|
/**
|
|
305
368
|
* Create a password reset token
|
|
306
369
|
*/
|
|
307
|
-
createPasswordResetToken(
|
|
370
|
+
createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
308
371
|
/**
|
|
309
372
|
* Find a valid (not expired, not used) password reset token by hash
|
|
310
373
|
*/
|
|
@@ -316,7 +379,7 @@ export interface TokenRepository {
|
|
|
316
379
|
/**
|
|
317
380
|
* Delete all password reset tokens for a user
|
|
318
381
|
*/
|
|
319
|
-
deleteAllPasswordResetTokensForUser(
|
|
382
|
+
deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
|
|
320
383
|
/**
|
|
321
384
|
* Clean up expired tokens
|
|
322
385
|
*/
|
|
@@ -324,7 +387,7 @@ export interface TokenRepository {
|
|
|
324
387
|
/**
|
|
325
388
|
* Create a magic link token
|
|
326
389
|
*/
|
|
327
|
-
createMagicLinkToken(
|
|
390
|
+
createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
328
391
|
/**
|
|
329
392
|
* Find a valid (not expired, not used) magic link token by hash
|
|
330
393
|
*/
|
|
@@ -339,7 +402,7 @@ export interface TokenRepository {
|
|
|
339
402
|
*/
|
|
340
403
|
export interface MfaFactor {
|
|
341
404
|
id: string;
|
|
342
|
-
|
|
405
|
+
uid: string;
|
|
343
406
|
factorType: "totp";
|
|
344
407
|
friendlyName?: string;
|
|
345
408
|
verified: boolean;
|
|
@@ -361,7 +424,7 @@ export interface MfaChallengeInfo {
|
|
|
361
424
|
*/
|
|
362
425
|
export interface RecoveryCode {
|
|
363
426
|
id: string;
|
|
364
|
-
|
|
427
|
+
uid: string;
|
|
365
428
|
usedAt?: Date;
|
|
366
429
|
}
|
|
367
430
|
/**
|
|
@@ -372,11 +435,11 @@ export interface MfaRepository {
|
|
|
372
435
|
/**
|
|
373
436
|
* Create a new MFA factor for a user
|
|
374
437
|
*/
|
|
375
|
-
createMfaFactor(
|
|
438
|
+
createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
|
|
376
439
|
/**
|
|
377
440
|
* Get all MFA factors for a user
|
|
378
441
|
*/
|
|
379
|
-
getMfaFactors(
|
|
442
|
+
getMfaFactors(uid: string): Promise<MfaFactor[]>;
|
|
380
443
|
/**
|
|
381
444
|
* Get a specific MFA factor by ID
|
|
382
445
|
*/
|
|
@@ -390,7 +453,7 @@ export interface MfaRepository {
|
|
|
390
453
|
/**
|
|
391
454
|
* Delete an MFA factor
|
|
392
455
|
*/
|
|
393
|
-
deleteMfaFactor(factorId: string,
|
|
456
|
+
deleteMfaFactor(factorId: string, uid: string): Promise<void>;
|
|
394
457
|
/**
|
|
395
458
|
* Create an MFA challenge
|
|
396
459
|
*/
|
|
@@ -406,23 +469,23 @@ export interface MfaRepository {
|
|
|
406
469
|
/**
|
|
407
470
|
* Create recovery codes for a user
|
|
408
471
|
*/
|
|
409
|
-
createRecoveryCodes(
|
|
472
|
+
createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
|
|
410
473
|
/**
|
|
411
474
|
* Use a recovery code (mark as used)
|
|
412
475
|
*/
|
|
413
|
-
useRecoveryCode(
|
|
476
|
+
useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
|
|
414
477
|
/**
|
|
415
478
|
* Get unused recovery code count for a user
|
|
416
479
|
*/
|
|
417
|
-
getUnusedRecoveryCodeCount(
|
|
480
|
+
getUnusedRecoveryCodeCount(uid: string): Promise<number>;
|
|
418
481
|
/**
|
|
419
482
|
* Delete all recovery codes for a user
|
|
420
483
|
*/
|
|
421
|
-
deleteAllRecoveryCodes(
|
|
484
|
+
deleteAllRecoveryCodes(uid: string): Promise<void>;
|
|
422
485
|
/**
|
|
423
486
|
* Check if a user has any verified MFA factors
|
|
424
487
|
*/
|
|
425
|
-
hasVerifiedMfaFactors(
|
|
488
|
+
hasVerifiedMfaFactors(uid: string): Promise<boolean>;
|
|
426
489
|
}
|
|
427
490
|
/**
|
|
428
491
|
* Combined auth repository interface for convenience
|
package/dist/auth/jwt.d.ts
CHANGED
|
@@ -4,9 +4,14 @@ export interface JwtConfig {
|
|
|
4
4
|
refreshExpiresIn?: string;
|
|
5
5
|
}
|
|
6
6
|
export interface AccessTokenPayload {
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The user's id — the same spelling the domain model, the auth adapters and
|
|
9
|
+
* the RLS layer (`auth.uid()`) all use. Tokens minted before this rename
|
|
10
|
+
* carry `uid` instead, and older external IdPs may send `sub`;
|
|
11
|
+
* {@link verifyAccessToken} accepts all three and normalises to this.
|
|
12
|
+
*/
|
|
13
|
+
uid: string;
|
|
8
14
|
roles: string[];
|
|
9
|
-
uid?: string;
|
|
10
15
|
/** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
|
|
11
16
|
aal?: "aal1" | "aal2";
|
|
12
17
|
/** Email claim from the JWT, if present */
|
|
@@ -28,7 +33,7 @@ export declare function configureJwt(config: JwtConfig): void;
|
|
|
28
33
|
/**
|
|
29
34
|
* Generate an access token (short-lived, 1 hour by default)
|
|
30
35
|
*/
|
|
31
|
-
export declare function generateAccessToken(
|
|
36
|
+
export declare function generateAccessToken(uid: string, roles: string[], aal?: "aal1" | "aal2", customClaims?: Record<string, unknown>): string;
|
|
32
37
|
/**
|
|
33
38
|
* Get the expiration time of an access token in milliseconds from now
|
|
34
39
|
*/
|
|
@@ -60,6 +65,23 @@ export declare function generateRefreshToken(): string;
|
|
|
60
65
|
* Hash a refresh token for database storage (don't store raw tokens)
|
|
61
66
|
*/
|
|
62
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;
|
|
63
85
|
/**
|
|
64
86
|
* Calculate refresh token expiration date
|
|
65
87
|
*/
|
|
@@ -23,10 +23,10 @@ export declare function mountMagicLinkRoutes(deps: {
|
|
|
23
23
|
isAnonymous?: boolean;
|
|
24
24
|
metadata?: Record<string, unknown> | null;
|
|
25
25
|
}, roleIds: string[], accessToken: string, refreshToken: string, providerId: string) => unknown;
|
|
26
|
-
createSessionAndTokens: (
|
|
26
|
+
createSessionAndTokens: (uid: string, userAgent: string, ipAddress: string) => Promise<{
|
|
27
27
|
roleIds: string[];
|
|
28
28
|
accessToken: string;
|
|
29
29
|
refreshToken: string;
|
|
30
30
|
}>;
|
|
31
|
-
applyTransformHook: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request,
|
|
31
|
+
applyTransformHook: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request, uid: string) => Promise<AuthResponsePayload>;
|
|
32
32
|
}): void;
|
|
@@ -4,4 +4,4 @@ import { HonoEnv } from "../api/types";
|
|
|
4
4
|
import type { AuthModuleConfig } from "./routes";
|
|
5
5
|
import { resolveAuthHooks } from "./auth-hooks";
|
|
6
6
|
import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
|
|
7
|
-
export declare function mountMfaRoutes(router: Hono<HonoEnv>, config: AuthModuleConfig, ops: ReturnType<typeof resolveAuthHooks>, parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T, applyTransformHook?: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request,
|
|
7
|
+
export declare function mountMfaRoutes(router: Hono<HonoEnv>, config: AuthModuleConfig, ops: ReturnType<typeof resolveAuthHooks>, parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T, applyTransformHook?: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request, uid: string) => Promise<AuthResponsePayload>): void;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { MiddlewareHandler, Context } from "hono";
|
|
2
2
|
import { DataDriver } from "@rebasepro/types";
|
|
3
3
|
import { AccessTokenPayload } from "./jwt";
|
|
4
|
-
import { HonoEnv } from "../api/types";
|
|
4
|
+
import type { HonoEnv } from "../api/types";
|
|
5
5
|
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
6
6
|
/**
|
|
7
7
|
* Result from a custom auth validator.
|
|
8
8
|
* - `false`/`null`/`undefined` = not authenticated
|
|
9
9
|
* - `true` = authenticated as default user
|
|
10
|
-
* - object with `
|
|
10
|
+
* - object with `uid` (or legacy `userId`) = authenticated with user info
|
|
11
11
|
*/
|
|
12
12
|
export type AuthResult = boolean | null | undefined | {
|
|
13
|
-
userId?: string;
|
|
14
13
|
uid?: string;
|
|
14
|
+
userId?: string;
|
|
15
15
|
roles?: string[];
|
|
16
16
|
[key: string]: unknown;
|
|
17
17
|
};
|
|
@@ -24,6 +24,6 @@ export interface ResetPasswordRouteConfig {
|
|
|
24
24
|
/**
|
|
25
25
|
* Create a standalone admin route for resetting user passwords.
|
|
26
26
|
*
|
|
27
|
-
* Mounts: POST /users/:
|
|
27
|
+
* Mounts: POST /users/:uid/reset-password
|
|
28
28
|
*/
|
|
29
29
|
export declare function createResetPasswordRoute(config: ResetPasswordRouteConfig): Hono<HonoEnv>;
|
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.
|