@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.
Files changed (39) hide show
  1. package/bin/rebase-server.js +55 -0
  2. package/dist/api/contract-routes.d.ts +36 -0
  3. package/dist/api/errors.d.ts +16 -1
  4. package/dist/api/rest/api-generator.d.ts +9 -1
  5. package/dist/api/rest/query-parser.d.ts +17 -1
  6. package/dist/api/rest/write-validation.d.ts +3 -0
  7. package/dist/api/types.d.ts +2 -2
  8. package/dist/auth/admin-users-route.d.ts +3 -3
  9. package/dist/auth/auth-hooks.d.ts +7 -7
  10. package/dist/auth/interfaces.d.ts +92 -29
  11. package/dist/auth/jwt.d.ts +25 -3
  12. package/dist/auth/magic-link-routes.d.ts +2 -2
  13. package/dist/auth/mfa-routes.d.ts +1 -1
  14. package/dist/auth/middleware.d.ts +3 -3
  15. package/dist/auth/reset-password-admin.d.ts +1 -1
  16. package/dist/auth/routes.d.ts +16 -0
  17. package/dist/auth/session-routes.d.ts +2 -2
  18. package/dist/boot/boot.d.ts +59 -0
  19. package/dist/boot/bundle.d.ts +134 -0
  20. package/dist/boot/driver.d.ts +57 -0
  21. package/dist/boot/env.d.ts +129 -0
  22. package/dist/boot/options.d.ts +20 -0
  23. package/dist/boot/sources.d.ts +77 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.es.js +6763 -2451
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/init/middlewares.d.ts +9 -0
  28. package/dist/init/storage.d.ts +27 -0
  29. package/dist/init.d.ts +44 -1
  30. package/dist/{jwt-BJzQOa8a.js → jwt-Dj7r7QX7.js} +40 -28
  31. package/dist/{jwt-BJzQOa8a.js.map → jwt-Dj7r7QX7.js.map} +1 -1
  32. package/dist/metrics/index.d.ts +83 -0
  33. package/dist/{src-CsHhSKbi.js → src-BITicbgD.js} +172 -2
  34. package/dist/src-BITicbgD.js.map +1 -0
  35. package/dist/storage/routes.d.ts +9 -1
  36. package/dist/storage/types.d.ts +1 -32
  37. package/dist/utils/sql.d.ts +2 -2
  38. package/package.json +9 -5
  39. 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>;
@@ -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
- constructor(statusCode: number, code: string, message: string, details?: unknown);
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>): QueryOptions;
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;
@@ -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
- userId?: string;
12
+ uid?: string;
13
13
  roles?: string[];
14
14
  };
15
15
  driver?: DataDriver;
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Mounts:
5
5
  * GET /users
6
- * GET /users/:userId
6
+ * GET /users/:uid
7
7
  * POST /users
8
- * PUT /users/:userId
9
- * DELETE /users/:userId
8
+ * PUT /users/:uid
9
+ * DELETE /users/:uid
10
10
  * POST /bootstrap
11
11
  */
12
12
  import { Hono } from "hono";
@@ -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?(userId: string): Promise<void>;
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?(userId: string, factorId: string): Promise<void>;
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 (userId, roles).
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?(userId: string): Promise<void>;
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?(userId: string): Promise<void>;
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?(userId: string): Promise<void>;
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?(userId: string, ctx: {
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
- userId: string;
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
- userId: string;
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
- userId: string;
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
- userId: string;
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(userId: string): Promise<UserIdentityData[]>;
215
+ getUserIdentities(uid: string): Promise<UserIdentityData[]>;
189
216
  /**
190
217
  * Link a new OAuth identity to a user
191
218
  */
192
- linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
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(userId: string): Promise<RoleData[]>;
255
+ getUserRoles(uid: string): Promise<RoleData[]>;
229
256
  /**
230
257
  * Get role IDs for a user
231
258
  */
232
- getUserRoleIds(userId: string): Promise<string[]>;
259
+ getUserRoleIds(uid: string): Promise<string[]>;
233
260
  /**
234
261
  * Set roles for a user (replaces existing roles)
235
262
  */
236
- setUserRoles(userId: string, roleIds: string[]): Promise<void>;
263
+ setUserRoles(uid: string, roleIds: string[]): Promise<void>;
237
264
  /**
238
265
  * Assign a specific role to a new user
239
266
  */
240
- assignDefaultRole(userId: string, roleId: string): Promise<void>;
267
+ assignDefaultRole(uid: string, roleId: string): Promise<void>;
241
268
  /**
242
269
  * Get user with their roles
243
270
  */
244
- getUserWithRoles(userId: string): Promise<{
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
- createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
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(userId: string): Promise<void>;
358
+ deleteAllRefreshTokensForUser(uid: string): Promise<void>;
296
359
  /**
297
360
  * List all refresh tokens for a user
298
361
  */
299
- listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]>;
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, userId: string): Promise<void>;
366
+ deleteRefreshTokenById(id: string, uid: string): Promise<void>;
304
367
  /**
305
368
  * Create a password reset token
306
369
  */
307
- createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
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(userId: string): Promise<void>;
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(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
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
- userId: string;
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
- userId: string;
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(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
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(userId: string): Promise<MfaFactor[]>;
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, userId: string): Promise<void>;
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(userId: string, codeHashes: string[]): Promise<void>;
472
+ createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
410
473
  /**
411
474
  * Use a recovery code (mark as used)
412
475
  */
413
- useRecoveryCode(userId: string, codeHash: string): Promise<boolean>;
476
+ useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
414
477
  /**
415
478
  * Get unused recovery code count for a user
416
479
  */
417
- getUnusedRecoveryCodeCount(userId: string): Promise<number>;
480
+ getUnusedRecoveryCodeCount(uid: string): Promise<number>;
418
481
  /**
419
482
  * Delete all recovery codes for a user
420
483
  */
421
- deleteAllRecoveryCodes(userId: string): Promise<void>;
484
+ deleteAllRecoveryCodes(uid: string): Promise<void>;
422
485
  /**
423
486
  * Check if a user has any verified MFA factors
424
487
  */
425
- hasVerifiedMfaFactors(userId: string): Promise<boolean>;
488
+ hasVerifiedMfaFactors(uid: string): Promise<boolean>;
426
489
  }
427
490
  /**
428
491
  * Combined auth repository interface for convenience
@@ -4,9 +4,14 @@ export interface JwtConfig {
4
4
  refreshExpiresIn?: string;
5
5
  }
6
6
  export interface AccessTokenPayload {
7
- userId: string;
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(userId: string, roles: string[], aal?: "aal1" | "aal2", customClaims?: Record<string, unknown>): string;
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: (userId: string, userAgent: string, ipAddress: string) => Promise<{
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, userId: string) => Promise<AuthResponsePayload>;
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, userId: string) => Promise<AuthResponsePayload>): void;
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 `userId` or `uid` = authenticated with user info
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/:userId/reset-password
27
+ * Mounts: POST /users/:uid/reset-password
28
28
  */
29
29
  export declare function createResetPasswordRoute(config: ResetPasswordRouteConfig): Hono<HonoEnv>;
@@ -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.