@rebasepro/server-core 0.1.2 → 0.2.3

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 (75) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/data/query_builder.d.ts +51 -0
  3. package/dist/common/src/index.d.ts +1 -0
  4. package/dist/common/src/util/entities.d.ts +2 -2
  5. package/dist/common/src/util/relations.d.ts +1 -1
  6. package/dist/{index-DXVBFp5V.js → index-BZoAtuqi.js} +6 -2
  7. package/dist/index-BZoAtuqi.js.map +1 -0
  8. package/dist/index.es.js +16038 -15240
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.umd.js +15980 -15178
  11. package/dist/index.umd.js.map +1 -1
  12. package/dist/server-core/src/auth/adapter-middleware.d.ts +33 -0
  13. package/dist/server-core/src/auth/admin-routes.d.ts +6 -0
  14. package/dist/server-core/src/auth/auth-overrides.d.ts +139 -0
  15. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +49 -0
  16. package/dist/server-core/src/auth/crypto-utils.d.ts +16 -0
  17. package/dist/server-core/src/auth/custom-auth-adapter.d.ts +39 -0
  18. package/dist/server-core/src/auth/index.d.ts +7 -0
  19. package/dist/server-core/src/auth/interfaces.d.ts +2 -0
  20. package/dist/server-core/src/auth/middleware.d.ts +18 -0
  21. package/dist/server-core/src/auth/rls-scope.d.ts +31 -0
  22. package/dist/server-core/src/auth/routes.d.ts +7 -1
  23. package/dist/server-core/src/env.d.ts +131 -0
  24. package/dist/server-core/src/index.d.ts +2 -0
  25. package/dist/server-core/src/init.d.ts +62 -3
  26. package/dist/types/src/controllers/auth.d.ts +9 -8
  27. package/dist/types/src/controllers/client.d.ts +3 -0
  28. package/dist/types/src/controllers/data.d.ts +21 -0
  29. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  30. package/dist/types/src/types/collections.d.ts +67 -2
  31. package/dist/types/src/types/database_adapter.d.ts +94 -0
  32. package/dist/types/src/types/entity_actions.d.ts +7 -1
  33. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  34. package/dist/types/src/types/entity_views.d.ts +36 -1
  35. package/dist/types/src/types/index.d.ts +2 -0
  36. package/dist/types/src/types/plugins.d.ts +1 -1
  37. package/dist/types/src/types/properties.d.ts +24 -5
  38. package/dist/types/src/types/property_config.d.ts +6 -2
  39. package/dist/types/src/types/relations.d.ts +1 -1
  40. package/dist/types/src/types/translations.d.ts +8 -0
  41. package/dist/types/src/users/user.d.ts +5 -0
  42. package/jest.config.cjs +4 -1
  43. package/package.json +27 -27
  44. package/src/api/errors.ts +1 -1
  45. package/src/api/graphql/graphql-schema-generator.ts +7 -0
  46. package/src/api/openapi-generator.ts +13 -1
  47. package/src/api/rest/api-generator-count.test.ts +14 -12
  48. package/src/api/rest/query-parser.ts +2 -20
  49. package/src/auth/adapter-middleware.ts +83 -0
  50. package/src/auth/admin-routes.ts +36 -43
  51. package/src/auth/auth-overrides.ts +172 -0
  52. package/src/auth/builtin-auth-adapter.ts +384 -0
  53. package/src/auth/crypto-utils.ts +31 -0
  54. package/src/auth/custom-auth-adapter.ts +85 -0
  55. package/src/auth/index.ts +10 -0
  56. package/src/auth/interfaces.ts +2 -0
  57. package/src/auth/jwt.ts +3 -1
  58. package/src/auth/middleware.ts +2 -46
  59. package/src/auth/rls-scope.ts +58 -0
  60. package/src/auth/routes.ts +74 -32
  61. package/src/cron/cron-scheduler.test.ts +9 -9
  62. package/src/cron/cron-scheduler.ts +1 -1
  63. package/src/env.ts +224 -0
  64. package/src/index.ts +4 -0
  65. package/src/init.ts +355 -135
  66. package/src/storage/routes.ts +1 -19
  67. package/src/utils/logging.ts +3 -3
  68. package/test/admin-routes.test.ts +10 -4
  69. package/test/auth-routes.test.ts +2 -2
  70. package/test/backend-hooks-admin.test.ts +32 -12
  71. package/test/custom-auth-adapter.test.ts +177 -0
  72. package/test/env.test.ts +138 -0
  73. package/test/query-parser.test.ts +0 -29
  74. package/tsconfig.json +3 -0
  75. package/dist/index-DXVBFp5V.js.map +0 -1
@@ -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";
@@ -20,6 +20,7 @@ export interface UserData {
20
20
  emailVerified: boolean;
21
21
  emailVerificationToken?: string | null;
22
22
  emailVerificationSentAt?: Date | null;
23
+ metadata?: Record<string, unknown>;
23
24
  createdAt: Date;
24
25
  updatedAt: Date;
25
26
  }
@@ -33,6 +34,7 @@ export interface CreateUserData {
33
34
  displayName?: string;
34
35
  photoUrl?: string;
35
36
  emailVerified?: boolean;
37
+ metadata?: Record<string, unknown>;
36
38
  }
37
39
 
38
40
  /**
package/src/auth/jwt.ts CHANGED
@@ -42,7 +42,9 @@ export function configureJwt(config: JwtConfig): void {
42
42
  "example-secret",
43
43
  "please-change-me",
44
44
  "replace-this-with-a-real-secret",
45
- "default-secret"
45
+ "default-secret",
46
+ "rebase_saas_jwt_secret_must_be_long_long_long_long",
47
+ "rebase_saas_service_key_must_be_long_long_long_long"
46
48
  ]);
47
49
 
48
50
  if (!config.secret || config.secret.length < 32) {
@@ -2,7 +2,8 @@ import { MiddlewareHandler, Context } from "hono";
2
2
  import { DataDriver } from "@rebasepro/types";
3
3
  import { verifyAccessToken, AccessTokenPayload } from "./jwt";
4
4
  import { HonoEnv } from "../api/types";
5
- import { timingSafeEqual } from "crypto";
5
+ import { scopeDataDriver } from "./rls-scope";
6
+ import { safeCompare } from "./crypto-utils";
6
7
 
7
8
  /**
8
9
  * Result from a custom auth validator.
@@ -202,23 +203,6 @@ export function extractUserFromToken(token: string): AccessTokenPayload | null {
202
203
  return verifyAccessToken(token);
203
204
  }
204
205
 
205
- /**
206
- * Helper to scope a DataDriver via withAuth() for RLS.
207
- * SECURITY: If withAuth() is available but fails, the error is re-thrown
208
- * so the request is denied rather than proceeding with unscoped access.
209
- */
210
- async function scopeDataDriver(
211
- driver: DataDriver,
212
- user: { uid: string; roles?: string[] }
213
- ): Promise<DataDriver> {
214
- if ("withAuth" in driver && typeof (driver as Record<string, unknown>).withAuth === "function") {
215
- // Fail closed — do NOT catch and swallow errors here.
216
- // If RLS scoping fails the request must be rejected.
217
- return await (driver as unknown as { withAuth: (user: Record<string, unknown>) => Promise<DataDriver> }).withAuth(user);
218
- }
219
- return driver;
220
- }
221
-
222
206
  /**
223
207
  * Create a configurable auth middleware that handles:
224
208
  * 1. Token extraction (via custom validator or JWT Bearer token)
@@ -237,34 +221,6 @@ async function scopeDataDriver(
237
221
  * This is the single source of truth for HTTP auth in Rebase.
238
222
  * Use this instead of manually parsing tokens in route handlers.
239
223
  */
240
- /**
241
- * Constant-time string comparison to prevent timing attacks on service keys.
242
- *
243
- * We intentionally avoid early-returning on length mismatch because that
244
- * would leak the key's length through timing differences. Instead, both
245
- * inputs are padded to the same length so `timingSafeEqual` always runs
246
- * over equal-length buffers.
247
- */
248
- function safeCompare(a: string, b: string): boolean {
249
- const maxLen = Math.max(a.length, b.length);
250
- // Pad both to maxLen so timingSafeEqual always compares equal-length buffers.
251
- // If the original lengths differ the result will be false due to the padding
252
- // difference, but the comparison still takes constant time.
253
- const bufA = Buffer.alloc(maxLen);
254
- const bufB = Buffer.alloc(maxLen);
255
- bufA.write(a);
256
- bufB.write(b);
257
- try {
258
- const isEqual = timingSafeEqual(bufA, bufB);
259
- // Even though padding makes mismatched-length strings compare as
260
- // different bytes, we still need to verify lengths match to avoid
261
- // a padded shorter string accidentally equaling a longer one that
262
- // has trailing null bytes.
263
- return isEqual && a.length === b.length;
264
- } catch {
265
- return false;
266
- }
267
- }
268
224
 
269
225
  export function createAuthMiddleware(options: AuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
270
226
  const { driver, requireAuth: enforceAuth = true, validator, serviceKey } = options;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared RLS (Row-Level Security) scoping helper.
3
+ *
4
+ * DataDrivers may implement a `withAuth()` method that returns a scoped
5
+ * clone of the driver with RLS policies applied for the given user.
6
+ * This is database-specific (e.g. Postgres SET LOCAL ROLE) and is not
7
+ * part of the core DataDriver interface.
8
+ *
9
+ * This module provides the shared duck-typing logic used by the
10
+ * adapter-aware middleware.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import type { DataDriver } from "@rebasepro/types";
16
+
17
+ /**
18
+ * A DataDriver that supports RLS scoping via `withAuth()`.
19
+ *
20
+ * This is not part of the public DataDriver interface because not all
21
+ * database implementations support RLS. Drivers that do (e.g. Postgres)
22
+ * extend DataDriver with this method.
23
+ */
24
+ interface RLSScopedDriver extends DataDriver {
25
+ withAuth(user: { uid: string; roles?: string[] }): Promise<DataDriver>;
26
+ }
27
+
28
+ /**
29
+ * Returns true if the driver supports RLS scoping via `withAuth()`.
30
+ */
31
+ function isRLSScopedDriver(driver: DataDriver): driver is RLSScopedDriver {
32
+ return "withAuth" in driver && typeof (driver as Record<string, unknown>).withAuth === "function";
33
+ }
34
+
35
+ /**
36
+ * Scope a DataDriver via `withAuth()` for RLS.
37
+ *
38
+ * SECURITY: If `withAuth()` is available but fails, the error is re-thrown
39
+ * so the request is **denied** rather than proceeding with unscoped access
40
+ * (fail-closed behavior).
41
+ *
42
+ * If the driver does not support RLS, the original driver is returned.
43
+ *
44
+ * @param driver - The DataDriver to scope.
45
+ * @param user - The authenticated user identity for RLS.
46
+ * @returns The RLS-scoped DataDriver (or the original if RLS is unsupported).
47
+ */
48
+ export async function scopeDataDriver(
49
+ driver: DataDriver,
50
+ user: { uid: string; roles?: string[] },
51
+ ): Promise<DataDriver> {
52
+ if (isRLSScopedDriver(driver)) {
53
+ // Fail closed — do NOT catch and swallow errors here.
54
+ // If RLS scoping fails the request must be rejected.
55
+ return await driver.withAuth(user);
56
+ }
57
+ return driver;
58
+ }