@rebasepro/server-core 0.1.2 → 0.2.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.
Files changed (71) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/util/entities.d.ts +2 -2
  3. package/dist/common/src/util/relations.d.ts +1 -1
  4. package/dist/{index-DXVBFp5V.js → index-BZoAtuqi.js} +6 -2
  5. package/dist/index-BZoAtuqi.js.map +1 -0
  6. package/dist/index.es.js +15851 -15065
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +15825 -15035
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-core/src/auth/adapter-middleware.d.ts +33 -0
  11. package/dist/server-core/src/auth/admin-routes.d.ts +6 -0
  12. package/dist/server-core/src/auth/auth-overrides.d.ts +139 -0
  13. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +49 -0
  14. package/dist/server-core/src/auth/crypto-utils.d.ts +16 -0
  15. package/dist/server-core/src/auth/custom-auth-adapter.d.ts +39 -0
  16. package/dist/server-core/src/auth/index.d.ts +7 -0
  17. package/dist/server-core/src/auth/interfaces.d.ts +2 -0
  18. package/dist/server-core/src/auth/middleware.d.ts +18 -0
  19. package/dist/server-core/src/auth/rls-scope.d.ts +31 -0
  20. package/dist/server-core/src/auth/routes.d.ts +7 -1
  21. package/dist/server-core/src/env.d.ts +131 -0
  22. package/dist/server-core/src/index.d.ts +2 -0
  23. package/dist/server-core/src/init.d.ts +62 -3
  24. package/dist/types/src/controllers/auth.d.ts +9 -8
  25. package/dist/types/src/controllers/client.d.ts +3 -0
  26. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  27. package/dist/types/src/types/collections.d.ts +67 -2
  28. package/dist/types/src/types/database_adapter.d.ts +94 -0
  29. package/dist/types/src/types/entity_actions.d.ts +7 -1
  30. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  31. package/dist/types/src/types/entity_views.d.ts +36 -1
  32. package/dist/types/src/types/index.d.ts +2 -0
  33. package/dist/types/src/types/plugins.d.ts +1 -1
  34. package/dist/types/src/types/properties.d.ts +24 -5
  35. package/dist/types/src/types/property_config.d.ts +6 -2
  36. package/dist/types/src/types/relations.d.ts +1 -1
  37. package/dist/types/src/types/translations.d.ts +8 -0
  38. package/dist/types/src/users/user.d.ts +5 -0
  39. package/package.json +26 -26
  40. package/src/api/errors.ts +1 -1
  41. package/src/api/graphql/graphql-schema-generator.ts +7 -0
  42. package/src/api/openapi-generator.ts +13 -1
  43. package/src/api/rest/api-generator-count.test.ts +14 -12
  44. package/src/api/rest/query-parser.ts +2 -20
  45. package/src/auth/adapter-middleware.ts +83 -0
  46. package/src/auth/admin-routes.ts +36 -43
  47. package/src/auth/auth-overrides.ts +172 -0
  48. package/src/auth/builtin-auth-adapter.ts +384 -0
  49. package/src/auth/crypto-utils.ts +31 -0
  50. package/src/auth/custom-auth-adapter.ts +85 -0
  51. package/src/auth/index.ts +10 -0
  52. package/src/auth/interfaces.ts +2 -0
  53. package/src/auth/jwt.ts +3 -1
  54. package/src/auth/middleware.ts +2 -46
  55. package/src/auth/rls-scope.ts +58 -0
  56. package/src/auth/routes.ts +74 -32
  57. package/src/cron/cron-scheduler.test.ts +9 -9
  58. package/src/cron/cron-scheduler.ts +1 -1
  59. package/src/env.ts +224 -0
  60. package/src/index.ts +4 -0
  61. package/src/init.ts +355 -135
  62. package/src/storage/routes.ts +1 -19
  63. package/src/utils/logging.ts +3 -3
  64. package/test/admin-routes.test.ts +10 -4
  65. package/test/auth-routes.test.ts +2 -2
  66. package/test/backend-hooks-admin.test.ts +32 -12
  67. package/test/custom-auth-adapter.test.ts +177 -0
  68. package/test/env.test.ts +138 -0
  69. package/test/query-parser.test.ts +0 -29
  70. package/tsconfig.json +3 -0
  71. package/dist/index-DXVBFp5V.js.map +0 -1
@@ -0,0 +1,172 @@
1
+ /**
2
+ * AuthOverrides
3
+ *
4
+ * Override specific behaviors of the built-in Rebase auth system.
5
+ *
6
+ * Each method replaces one piece of the default implementation.
7
+ * Unset methods fall through to the built-in defaults (scrypt passwords,
8
+ * standard validation rules, etc.).
9
+ *
10
+ * This interface is intentionally open for extension — new overrides
11
+ * can be added as optional methods without breaking existing configurations.
12
+ *
13
+ * @example bcrypt password support
14
+ * ```ts
15
+ * import bcrypt from "bcrypt";
16
+ *
17
+ * const overrides: AuthOverrides = {
18
+ * hashPassword: (pw) => bcrypt.hash(pw, 12),
19
+ * verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
20
+ * validatePasswordStrength: (pw) => ({
21
+ * valid: pw.length >= 6,
22
+ * errors: pw.length < 6 ? ["Password must be at least 6 characters"] : []
23
+ * })
24
+ * };
25
+ * ```
26
+ *
27
+ * @example Override the entire login credential check
28
+ * ```ts
29
+ * const overrides: AuthOverrides = {
30
+ * verifyCredentials: async (email, password, repo) => {
31
+ * const user = await repo.getUserByEmail(email);
32
+ * if (!user || !user.passwordHash) return null;
33
+ * const valid = await myCustomVerify(password, user.passwordHash);
34
+ * return valid ? user : null;
35
+ * }
36
+ * };
37
+ * ```
38
+ */
39
+
40
+ import {
41
+ hashPassword as defaultHashPassword,
42
+ verifyPassword as defaultVerifyPassword,
43
+ validatePasswordStrength as defaultValidatePasswordStrength
44
+ } from "./password";
45
+ import type { PasswordValidationResult } from "./password";
46
+ import type { AuthRepository, UserData, CreateUserData } from "./interfaces";
47
+
48
+ /**
49
+ * Authentication method identifier for lifecycle hooks.
50
+ */
51
+ export type AuthMethod = "login" | "register" | "oauth" | "refresh" | "password-reset";
52
+
53
+ /**
54
+ * Override specific parts of the built-in Rebase auth implementation.
55
+ *
56
+ * Every method is optional. The built-in defaults apply for any method
57
+ * that is not provided.
58
+ */
59
+ export interface AuthOverrides {
60
+ // ─── Password Operations ──────────────────────────────────────────────
61
+
62
+ /**
63
+ * Hash a cleartext password for storage.
64
+ *
65
+ * Default: scrypt (Node.js crypto, 64-byte key, random 32-byte salt).
66
+ *
67
+ * @param password - The cleartext password.
68
+ * @returns The hashed password string (format is implementation-defined).
69
+ */
70
+ hashPassword?(password: string): Promise<string>;
71
+
72
+ /**
73
+ * Verify a cleartext password against a stored hash.
74
+ *
75
+ * Default: scrypt verification with timing-safe comparison.
76
+ *
77
+ * @param password - The cleartext password to check.
78
+ * @param storedHash - The hash string retrieved from the database.
79
+ * @returns `true` if the password matches the hash.
80
+ */
81
+ verifyPassword?(password: string, storedHash: string): Promise<boolean>;
82
+
83
+ /**
84
+ * Validate password strength before hashing.
85
+ *
86
+ * Default: minimum 8 characters, at least one uppercase, one lowercase, one digit.
87
+ *
88
+ * @param password - The cleartext password to validate.
89
+ * @returns Validation result with `valid` flag and error messages.
90
+ */
91
+ validatePasswordStrength?(password: string): PasswordValidationResult;
92
+
93
+ // ─── Credential Resolution ────────────────────────────────────────────
94
+
95
+ /**
96
+ * Override the complete credential verification during email/password login.
97
+ *
98
+ * When set, this replaces the default flow:
99
+ * 1. Look up user by email
100
+ * 2. Verify password hash
101
+ *
102
+ * The auth repository is provided for database access. Return the user
103
+ * data if credentials are valid, or `null` to reject the login.
104
+ *
105
+ * Default: `getUserByEmail(email)` + `verifyPassword(password, user.passwordHash)`.
106
+ */
107
+ verifyCredentials?(email: string, password: string, repo: AuthRepository): Promise<UserData | null>;
108
+
109
+ // ─── Lifecycle Hooks ──────────────────────────────────────────────────
110
+
111
+ /**
112
+ * Called after any successful authentication event (login, register,
113
+ * OAuth, token refresh, password reset).
114
+ *
115
+ * Use for audit logging, syncing external state, updating
116
+ * last-login timestamps, etc.
117
+ *
118
+ * This is fire-and-forget — errors are logged but do not fail the request.
119
+ */
120
+ onAuthenticated?(user: UserData, method: AuthMethod): Promise<void>;
121
+
122
+ /**
123
+ * Called before a new user is created (registration or admin creation).
124
+ *
125
+ * Return modified data to alter what gets stored, or throw an error
126
+ * to reject the creation entirely.
127
+ *
128
+ * Default: passthrough (returns data unchanged).
129
+ */
130
+ beforeUserCreate?(data: CreateUserData): Promise<CreateUserData>;
131
+
132
+ /**
133
+ * Called after a new user is created.
134
+ *
135
+ * Use for provisioning external resources, sending notifications
136
+ * to third-party systems, etc.
137
+ *
138
+ * This is fire-and-forget — errors are logged but do not fail the request.
139
+ */
140
+ afterUserCreate?(user: UserData): Promise<void>;
141
+ }
142
+
143
+ /**
144
+ * Resolved auth operations — every method is guaranteed to exist.
145
+ * Created by `resolveAuthOverrides()` which merges user overrides
146
+ * with built-in defaults.
147
+ */
148
+ export interface ResolvedAuthOperations {
149
+ hashPassword(password: string): Promise<string>;
150
+ verifyPassword(password: string, storedHash: string): Promise<boolean>;
151
+ validatePasswordStrength(password: string): PasswordValidationResult;
152
+ }
153
+
154
+ /**
155
+ * Merge user-provided overrides with the built-in defaults to produce
156
+ * a complete set of resolved operations.
157
+ *
158
+ * This is the single point where defaults are applied — all consumers
159
+ * call this once and use the resolved operations throughout.
160
+ */
161
+ export function resolveAuthOverrides(overrides?: AuthOverrides): ResolvedAuthOperations {
162
+ return {
163
+ hashPassword: overrides?.hashPassword
164
+ ?? defaultHashPassword,
165
+
166
+ verifyPassword: overrides?.verifyPassword
167
+ ?? defaultVerifyPassword,
168
+
169
+ validatePasswordStrength: overrides?.validatePasswordStrength
170
+ ?? defaultValidatePasswordStrength,
171
+ };
172
+ }
@@ -0,0 +1,384 @@
1
+ /**
2
+ * RebaseBuiltinAuthAdapter
3
+ *
4
+ * Wraps Rebase's existing built-in JWT auth system (routes, middleware, user/role
5
+ * management) into the `AuthAdapter` interface. This is the default adapter used
6
+ * when the user passes a plain `RebaseAuthConfig` object.
7
+ *
8
+ * This is NOT a rewrite — it delegates to the existing `createAuthRoutes()`,
9
+ * `createAdminRoutes()`, and `verifyAccessToken()` functions. The goal is to
10
+ * present the same functionality through the pluggable `AuthAdapter` contract.
11
+ */
12
+
13
+ import type {
14
+ AuthAdapter,
15
+ AuthenticatedUser,
16
+ AuthAdapterCapabilities,
17
+ UserManagementAdapter,
18
+ RoleManagementAdapter,
19
+ AuthUserListOptions,
20
+ AuthUserListResult,
21
+ AuthUserData,
22
+ AuthCreateUserData,
23
+ AuthRoleData,
24
+ AuthCreateRoleData,
25
+ BootstrappedAuth,
26
+ BackendHooks,
27
+ } from "@rebasepro/types";
28
+
29
+ import type { Hono } from "hono";
30
+ import { verifyAccessToken } from "./jwt";
31
+ import type { AccessTokenPayload } from "./jwt";
32
+ import { createAuthRoutes } from "./routes";
33
+ import { createAdminRoutes } from "./admin-routes";
34
+ import type { AuthRepository, OAuthProvider } from "./interfaces";
35
+ import type { AuthOverrides, ResolvedAuthOperations } from "./auth-overrides";
36
+ import { resolveAuthOverrides } from "./auth-overrides";
37
+ import type { EmailService, EmailConfig } from "../email";
38
+ import type { HonoEnv } from "../api/types";
39
+ import { safeCompare } from "./crypto-utils";
40
+
41
+ /**
42
+ * Configuration for the built-in Rebase auth adapter.
43
+ *
44
+ * This mirrors the existing `RebaseAuthConfig` — users pass this and
45
+ * server-core auto-wraps it in a `RebaseBuiltinAuthAdapter`.
46
+ */
47
+ export interface BuiltinAuthAdapterConfig {
48
+ /** The bootstrapper-provided auth repository (users, roles, tokens). */
49
+ authRepository: AuthRepository;
50
+ /** Email service for password resets, verification, etc. */
51
+ emailService?: EmailService;
52
+ /** Email configuration. */
53
+ emailConfig?: EmailConfig;
54
+ /** Whether to allow new user registration. */
55
+ allowRegistration?: boolean;
56
+ /** Default role to assign to new users. */
57
+ defaultRole?: string;
58
+ /** OAuth providers to register. */
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ oauthProviders?: OAuthProvider<any>[];
61
+ /** Static service key for server-to-server auth. */
62
+ serviceKey?: string;
63
+ /** Backend hooks for intercepting admin data. */
64
+ hooks?: BackendHooks;
65
+ /** Auth overrides for customizing password, credentials, lifecycle, etc. */
66
+ overrides?: AuthOverrides;
67
+ }
68
+
69
+ /**
70
+ * Create the built-in Rebase auth adapter.
71
+ *
72
+ * This wraps the existing auth infrastructure (JWT, OAuth, user/role management)
73
+ * into the `AuthAdapter` interface. It's used internally by `initializeRebaseBackend()`
74
+ * when the user passes a plain `RebaseAuthConfig` object.
75
+ */
76
+ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): AuthAdapter {
77
+ const {
78
+ authRepository,
79
+ emailService,
80
+ emailConfig,
81
+ allowRegistration = false,
82
+ defaultRole,
83
+ oauthProviders = [],
84
+ serviceKey,
85
+ hooks,
86
+ overrides,
87
+ } = config;
88
+
89
+ const resolvedOps = resolveAuthOverrides(overrides);
90
+
91
+ const adapter: AuthAdapter = {
92
+ id: "rebase-builtin",
93
+
94
+ serviceKey,
95
+
96
+ async verifyRequest(request: Request): Promise<AuthenticatedUser | null> {
97
+ const authHeader = request.headers.get("authorization");
98
+ const url = new URL(request.url, "http://localhost");
99
+ const queryToken = url.searchParams.get("token");
100
+ const hasBearer = authHeader?.startsWith("Bearer ");
101
+
102
+ if (!hasBearer && !queryToken) {
103
+ return null;
104
+ }
105
+
106
+ const token = hasBearer ? authHeader!.substring(7) : queryToken!;
107
+
108
+ // Check service key first (constant-time)
109
+ if (serviceKey && safeCompare(token, serviceKey)) {
110
+ return {
111
+ uid: "service",
112
+ email: "service@rebase.internal",
113
+ roles: ["admin"],
114
+ isAdmin: true,
115
+ rawToken: token,
116
+ };
117
+ }
118
+
119
+ // JWT verification
120
+ const payload = verifyAccessToken(token);
121
+ if (!payload) {
122
+ return null;
123
+ }
124
+
125
+ // The decoded JWT may contain additional claims beyond the typed payload
126
+ const extendedPayload = payload as AccessTokenPayload & {
127
+ email?: string;
128
+ displayName?: string;
129
+ };
130
+
131
+ // Resolve roles from the repository
132
+ let roles: string[] = payload.roles || [];
133
+ try {
134
+ const userRoles = await authRepository.getUserRoles(payload.userId);
135
+ roles = userRoles.map((r) => r.id);
136
+ } catch {
137
+ // Fall back to token roles if repository lookup fails
138
+ }
139
+
140
+ const isAdmin = roles.some((r) => r === "admin" || r === "schema-admin");
141
+
142
+ return {
143
+ uid: payload.userId,
144
+ email: extendedPayload.email ?? "",
145
+ displayName: extendedPayload.displayName ?? null,
146
+ roles,
147
+ isAdmin,
148
+ rawToken: token,
149
+ };
150
+ },
151
+
152
+ async verifyToken(token: string): Promise<AuthenticatedUser | null> {
153
+ // Service key check (constant-time)
154
+ if (serviceKey && safeCompare(token, serviceKey)) {
155
+ return {
156
+ uid: "service",
157
+ email: "service@rebase.internal",
158
+ roles: ["admin"],
159
+ isAdmin: true,
160
+ rawToken: token,
161
+ };
162
+ }
163
+
164
+ // JWT verification
165
+ const payload = verifyAccessToken(token);
166
+ if (!payload) {
167
+ return null;
168
+ }
169
+
170
+ const extendedPayload = payload as AccessTokenPayload & {
171
+ email?: string;
172
+ displayName?: string;
173
+ };
174
+
175
+ let roles: string[] = payload.roles || [];
176
+ try {
177
+ const userRoles = await authRepository.getUserRoles(payload.userId);
178
+ roles = userRoles.map((r) => r.id);
179
+ } catch {
180
+ // Fall back to token roles if repository lookup fails
181
+ }
182
+
183
+ const isAdmin = roles.some((r) => r === "admin" || r === "schema-admin");
184
+
185
+ return {
186
+ uid: payload.userId,
187
+ email: extendedPayload.email ?? "",
188
+ displayName: extendedPayload.displayName ?? null,
189
+ roles,
190
+ isAdmin,
191
+ rawToken: token,
192
+ };
193
+ },
194
+
195
+ userManagement: createUserManagementFromRepo(authRepository, resolvedOps, overrides),
196
+
197
+ roleManagement: createRoleManagementFromRepo(authRepository),
198
+
199
+ createAuthRoutes(): Hono<HonoEnv> | undefined {
200
+ return createAuthRoutes({
201
+ authRepo: authRepository,
202
+ emailService,
203
+ emailConfig,
204
+ allowRegistration,
205
+ defaultRole,
206
+ oauthProviders,
207
+ overrides,
208
+ });
209
+ },
210
+
211
+ createAdminRoutes(): Hono<HonoEnv> | undefined {
212
+ return createAdminRoutes({
213
+ authRepo: authRepository,
214
+ emailService,
215
+ emailConfig,
216
+ serviceKey,
217
+ hooks,
218
+ overrides,
219
+ });
220
+ },
221
+
222
+ async getCapabilities(): Promise<AuthAdapterCapabilities> {
223
+ // Detect bootstrap mode: are there any users?
224
+ let needsSetup = false;
225
+ try {
226
+ const result = await authRepository.listUsersPaginated({ limit: 1 });
227
+ needsSetup = result.total === 0;
228
+ } catch {
229
+ // If the check fails, assume not in setup mode
230
+ }
231
+
232
+ const enabledProviders = oauthProviders.map((p) => p.id);
233
+
234
+ return {
235
+ hasBuiltInAuthRoutes: true,
236
+ emailPasswordLogin: true,
237
+ registration: allowRegistration || needsSetup,
238
+ registrationEnabled: allowRegistration || needsSetup,
239
+ passwordReset: !!emailService?.isConfigured(),
240
+ sessionManagement: true,
241
+ profileUpdate: true,
242
+ emailVerification: !!emailService?.isConfigured(),
243
+ enabledProviders,
244
+ needsSetup,
245
+ };
246
+ },
247
+ };
248
+
249
+ return adapter;
250
+ }
251
+
252
+ // ─── Internal Helpers ────────────────────────────────────────────────────────
253
+
254
+ function createUserManagementFromRepo(repo: AuthRepository, resolvedOps: ResolvedAuthOperations, overrides?: AuthOverrides): UserManagementAdapter {
255
+ return {
256
+ async listUsers(options?: AuthUserListOptions): Promise<AuthUserListResult> {
257
+ const result = await repo.listUsersPaginated({
258
+ limit: options?.limit,
259
+ offset: options?.offset,
260
+ search: options?.search,
261
+ orderBy: options?.orderBy,
262
+ orderDir: options?.orderDir,
263
+ roleId: options?.roleId,
264
+ });
265
+ return {
266
+ users: result.users.map(toAuthUserData),
267
+ total: result.total,
268
+ limit: result.limit,
269
+ offset: result.offset,
270
+ };
271
+ },
272
+
273
+ async getUserById(id: string): Promise<AuthUserData | null> {
274
+ const user = await repo.getUserById(id);
275
+ return user ? toAuthUserData(user) : null;
276
+ },
277
+
278
+ async createUser(data: AuthCreateUserData): Promise<AuthUserData> {
279
+ const passwordHash = data.password ? await resolvedOps.hashPassword(data.password) : undefined;
280
+ let createData: import("./interfaces").CreateUserData = {
281
+ email: data.email,
282
+ passwordHash,
283
+ displayName: data.displayName,
284
+ photoUrl: data.photoUrl,
285
+ metadata: data.metadata,
286
+ };
287
+ if (overrides?.beforeUserCreate) {
288
+ createData = await overrides.beforeUserCreate(createData);
289
+ }
290
+ const user = await repo.createUser(createData);
291
+ if (overrides?.afterUserCreate) {
292
+ overrides.afterUserCreate(user).catch(err => {
293
+ console.error("[AuthOverrides] afterUserCreate error:", err instanceof Error ? err.message : err);
294
+ });
295
+ }
296
+ return toAuthUserData(user);
297
+ },
298
+
299
+ async updateUser(id: string, data: Partial<AuthCreateUserData>): Promise<AuthUserData | null> {
300
+ const updateData: Record<string, unknown> = {};
301
+ if (data.email !== undefined) updateData.email = data.email;
302
+ if (data.displayName !== undefined) updateData.displayName = data.displayName;
303
+ if (data.photoUrl !== undefined) updateData.photoUrl = data.photoUrl;
304
+ if (data.metadata !== undefined) updateData.metadata = data.metadata;
305
+ if (data.password) {
306
+ updateData.passwordHash = await resolvedOps.hashPassword(data.password);
307
+ }
308
+ const user = await repo.updateUser(id, updateData);
309
+ return user ? toAuthUserData(user) : null;
310
+ },
311
+
312
+ async deleteUser(id: string): Promise<void> {
313
+ await repo.deleteUser(id);
314
+ },
315
+
316
+ async getUserRoles(userId: string): Promise<AuthRoleData[]> {
317
+ const roles = await repo.getUserRoles(userId);
318
+ return roles.map(toAuthRoleData);
319
+ },
320
+
321
+ async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
322
+ await repo.setUserRoles(userId, roleIds);
323
+ },
324
+ };
325
+ }
326
+
327
+ function createRoleManagementFromRepo(repo: AuthRepository): RoleManagementAdapter {
328
+ return {
329
+ async listRoles(): Promise<AuthRoleData[]> {
330
+ const roles = await repo.listRoles();
331
+ return roles.map(toAuthRoleData);
332
+ },
333
+
334
+ async getRoleById(id: string): Promise<AuthRoleData | null> {
335
+ const role = await repo.getRoleById(id);
336
+ return role ? toAuthRoleData(role) : null;
337
+ },
338
+
339
+ async createRole(data: AuthCreateRoleData): Promise<AuthRoleData> {
340
+ const role = await repo.createRole({
341
+ id: data.id,
342
+ name: data.name,
343
+ isAdmin: data.isAdmin,
344
+ defaultPermissions: data.defaultPermissions,
345
+ collectionPermissions: data.collectionPermissions,
346
+ config: data.config,
347
+ });
348
+ return toAuthRoleData(role);
349
+ },
350
+
351
+ async updateRole(id: string, data: Partial<AuthRoleData>): Promise<AuthRoleData | null> {
352
+ const role = await repo.updateRole(id, data);
353
+ return role ? toAuthRoleData(role) : null;
354
+ },
355
+
356
+ async deleteRole(id: string): Promise<void> {
357
+ await repo.deleteRole(id);
358
+ },
359
+ };
360
+ }
361
+
362
+ function toAuthUserData(user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; metadata?: Record<string, unknown>; createdAt?: Date; updatedAt?: Date }): AuthUserData {
363
+ return {
364
+ id: user.id,
365
+ email: user.email,
366
+ displayName: user.displayName,
367
+ photoUrl: user.photoUrl,
368
+ emailVerified: user.emailVerified,
369
+ metadata: user.metadata,
370
+ createdAt: user.createdAt,
371
+ updatedAt: user.updatedAt,
372
+ };
373
+ }
374
+
375
+ function toAuthRoleData(role: { id: string; name: string; isAdmin: boolean; defaultPermissions?: unknown; collectionPermissions?: unknown; config?: unknown }): AuthRoleData {
376
+ return {
377
+ id: role.id,
378
+ name: role.name,
379
+ isAdmin: role.isAdmin,
380
+ defaultPermissions: role.defaultPermissions as AuthRoleData["defaultPermissions"],
381
+ collectionPermissions: role.collectionPermissions as AuthRoleData["collectionPermissions"],
382
+ config: role.config as AuthRoleData["config"],
383
+ };
384
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Cryptographic utility functions for auth.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { timingSafeEqual } from "crypto";
8
+
9
+ /**
10
+ * Constant-time string comparison to prevent timing attacks.
11
+ *
12
+ * Used for comparing service keys, tokens, and other secrets where
13
+ * timing side-channels could leak information about the expected value.
14
+ *
15
+ * @param a - First string to compare.
16
+ * @param b - Second string to compare.
17
+ * @returns `true` if the strings are identical, `false` otherwise.
18
+ */
19
+ export function safeCompare(a: string, b: string): boolean {
20
+ const maxLen = Math.max(a.length, b.length);
21
+ const bufA = Buffer.alloc(maxLen);
22
+ const bufB = Buffer.alloc(maxLen);
23
+ bufA.write(a);
24
+ bufB.write(b);
25
+ try {
26
+ const isEqual = timingSafeEqual(bufA, bufB);
27
+ return isEqual && a.length === b.length;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Custom Auth Adapter Factory
3
+ *
4
+ * Provides a minimal-config way for users with existing auth systems
5
+ * to plug into Rebase. Only `verifyRequest` is required — everything
6
+ * else is optional.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createCustomAuthAdapter } from "@rebasepro/server-core";
11
+ *
12
+ * const auth = createCustomAuthAdapter({
13
+ * verifyRequest: async (request) => {
14
+ * const token = request.headers.get("Authorization")?.replace("Bearer ", "");
15
+ * if (!token) return null;
16
+ * const decoded = jwt.verify(token, MY_SECRET);
17
+ * return {
18
+ * uid: decoded.sub,
19
+ * email: decoded.email,
20
+ * displayName: decoded.name,
21
+ * roles: decoded.roles || [],
22
+ * isAdmin: decoded.roles?.includes("admin") ?? false,
23
+ * };
24
+ * },
25
+ * });
26
+ * ```
27
+ */
28
+
29
+ import type {
30
+ AuthAdapter,
31
+ AuthAdapterCapabilities,
32
+ CustomAuthAdapterOptions,
33
+ } from "@rebasepro/types";
34
+
35
+ /**
36
+ * Create a custom auth adapter from minimal options.
37
+ *
38
+ * This is the recommended entry point for users who already have their own
39
+ * auth system and just need to tell Rebase how to extract the user from
40
+ * incoming requests.
41
+ *
42
+ * @param options - Configuration options. Only `verifyRequest` is required.
43
+ * @returns A fully-formed `AuthAdapter` ready for `initializeRebaseBackend()`.
44
+ */
45
+ export function createCustomAuthAdapter(options: CustomAuthAdapterOptions): AuthAdapter {
46
+ const defaultCapabilities: AuthAdapterCapabilities = {
47
+ hasBuiltInAuthRoutes: false,
48
+ emailPasswordLogin: false,
49
+ registration: false,
50
+ passwordReset: false,
51
+ sessionManagement: false,
52
+ profileUpdate: false,
53
+ emailVerification: false,
54
+ enabledProviders: [],
55
+ ...options.capabilities,
56
+ };
57
+
58
+ const resolvedVerifyToken = options.verifyToken
59
+ ?? (async (token: string) => {
60
+ // Synthesize a minimal Request so adapters that only implement
61
+ // verifyRequest still work for WebSocket token verification.
62
+ const syntheticRequest = new Request("http://localhost/_ws_auth", {
63
+ headers: { Authorization: `Bearer ${token}` },
64
+ });
65
+ return options.verifyRequest(syntheticRequest);
66
+ });
67
+
68
+ return {
69
+ id: "custom",
70
+
71
+ serviceKey: options.serviceKey,
72
+
73
+ verifyRequest: options.verifyRequest,
74
+
75
+ verifyToken: resolvedVerifyToken,
76
+
77
+ userManagement: options.userManagement,
78
+
79
+ roleManagement: options.roleManagement,
80
+
81
+ getCapabilities() {
82
+ return defaultCapabilities;
83
+ },
84
+ };
85
+ }
package/src/auth/index.ts CHANGED
@@ -7,6 +7,9 @@ export type { JwtConfig, AccessTokenPayload } from "./jwt";
7
7
  export { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
8
8
  export type { PasswordValidationResult } from "./password";
9
9
 
10
+ export type { AuthOverrides, AuthMethod, ResolvedAuthOperations } from "./auth-overrides";
11
+ export { resolveAuthOverrides } from "./auth-overrides";
12
+
10
13
  // OAuth Providers
11
14
  export { createGoogleProvider } from "./google-oauth";
12
15
  export type { GoogleProviderConfig } from "./google-oauth";
@@ -33,3 +36,10 @@ export { createAdminRoutes } from "./admin-routes";
33
36
 
34
37
 
35
38
  export { createRateLimiter, defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";
39
+
40
+ // Auth Adapters
41
+ export { createBuiltinAuthAdapter } from "./builtin-auth-adapter";
42
+ export type { BuiltinAuthAdapterConfig } from "./builtin-auth-adapter";
43
+ export { createCustomAuthAdapter } from "./custom-auth-adapter";
44
+ export { createAdapterAuthMiddleware } from "./adapter-middleware";
45
+ export type { AdapterAuthMiddlewareOptions } from "./adapter-middleware";