@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
@@ -2,7 +2,8 @@ import { Hono } from "hono";
2
2
  import { ApiError, errorHandler } from "../api/errors";
3
3
  import type { AuthRepository } from "./interfaces";
4
4
  import { requireAuth, requireAdmin, createRequireAuth } from "./middleware";
5
- import { hashPassword, validatePasswordStrength } from "./password";
5
+ import type { AuthOverrides } from "./auth-overrides";
6
+ import { resolveAuthOverrides } from "./auth-overrides";
6
7
  import { AuthModuleConfig } from "./routes";
7
8
  import type { BackendHooks, AdminUser, AdminRole, BackendHookContext } from "@rebasepro/types";
8
9
 
@@ -17,6 +18,11 @@ interface AdminRouteOptions extends AuthModuleConfig {
17
18
  * Backend-level hooks for intercepting admin data.
18
19
  */
19
20
  hooks?: BackendHooks;
21
+ /**
22
+ * Auth overrides for customizing password hashing, credential
23
+ * verification, lifecycle hooks, etc.
24
+ */
25
+ overrides?: AuthOverrides;
20
26
  }
21
27
  import { HonoEnv } from "../api/types";
22
28
  import { randomBytes, createHash } from "crypto";
@@ -68,7 +74,8 @@ function hashToken(token: string): string {
68
74
  export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
69
75
  const router = new Hono<HonoEnv>();
70
76
  const authRepo = config.authRepo;
71
- const { emailService, emailConfig, hooks } = config;
77
+ const { emailService, emailConfig, hooks, overrides } = config;
78
+ const ops = resolveAuthOverrides(overrides);
72
79
 
73
80
  /** Build a BackendHookContext from Hono's context object */
74
81
  function buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
@@ -192,41 +199,20 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
192
199
  const orderDir = c.req.query("orderDir") as "asc" | "desc" | undefined;
193
200
  const hookCtx = buildHookContext(c, "GET");
194
201
 
195
- // If pagination params are provided, use the paginated path
196
- if (limitParam !== undefined || search) {
197
- const limit = limitParam ? parseInt(limitParam, 10) : 25;
198
- const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
199
-
200
- const result = await authRepo.listUsersPaginated({
201
- limit,
202
- offset,
203
- search: search || undefined,
204
- orderBy: orderBy || undefined,
205
- orderDir: orderDir || undefined,
206
- roleId: c.req.query("role") || undefined
207
- });
208
-
209
- let usersWithRoles: AdminUser[] = await Promise.all(
210
- result.users.map(async (u) => {
211
- const roles = await authRepo.getUserRoleIds(u.id);
212
- return toAdminUser(u, roles);
213
- })
214
- );
215
-
216
- usersWithRoles = await applyUserAfterReadBatch(usersWithRoles, hookCtx);
202
+ const limit = limitParam ? parseInt(limitParam, 10) : 25;
203
+ const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
217
204
 
218
- return c.json({
219
- users: usersWithRoles,
220
- total: result.total,
221
- limit: result.limit,
222
- offset: result.offset
223
- });
224
- }
205
+ const result = await authRepo.listUsersPaginated({
206
+ limit,
207
+ offset,
208
+ search: search || undefined,
209
+ orderBy: orderBy || undefined,
210
+ orderDir: orderDir || undefined,
211
+ roleId: c.req.query("role") || undefined
212
+ });
225
213
 
226
- // Legacy: return all users (no pagination)
227
- const users = await authRepo.listUsers();
228
214
  let usersWithRoles: AdminUser[] = await Promise.all(
229
- users.map(async (u) => {
215
+ result.users.map(async (u) => {
230
216
  const roles = await authRepo.getUserRoleIds(u.id);
231
217
  return toAdminUser(u, roles);
232
218
  })
@@ -234,7 +220,12 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
234
220
 
235
221
  usersWithRoles = await applyUserAfterReadBatch(usersWithRoles, hookCtx);
236
222
 
237
- return c.json({ users: usersWithRoles });
223
+ return c.json({
224
+ users: usersWithRoles,
225
+ total: result.total,
226
+ limit: result.limit,
227
+ offset: result.offset
228
+ });
238
229
  });
239
230
 
240
231
  router.get("/users/:userId", requireAdmin, async (c) => {
@@ -258,7 +249,8 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
258
249
 
259
250
  router.post("/users", requireAdmin, async (c) => {
260
251
  const body = await c.req.json();
261
- let { email, displayName, password, roles } = body;
252
+ const { password } = body;
253
+ let { email, displayName, roles } = body;
262
254
 
263
255
  if (!email) {
264
256
  throw ApiError.badRequest("Email is required", "INVALID_INPUT");
@@ -281,11 +273,11 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
281
273
  // Use provided password or auto-generate one
282
274
  const clearPassword = password || generateSecurePassword();
283
275
 
284
- const validation = validatePasswordStrength(clearPassword);
276
+ const validation = ops.validatePasswordStrength(clearPassword);
285
277
  if (!validation.valid) {
286
278
  throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
287
279
  }
288
- const passwordHash = await hashPassword(clearPassword);
280
+ const passwordHash = await ops.hashPassword(clearPassword);
289
281
 
290
282
  const user = await authRepo.createUser({
291
283
  email: email.toLowerCase(),
@@ -401,14 +393,14 @@ displayName: existing.displayName }, appName);
401
393
  console.error("Failed to send reset email:", emailError instanceof Error ? emailError.message : emailError);
402
394
  // Fall back to returning the temporary password
403
395
  const clearPassword = generateSecurePassword();
404
- const passwordHash = await hashPassword(clearPassword);
396
+ const passwordHash = await ops.hashPassword(clearPassword);
405
397
  await authRepo.updatePassword(existing.id, passwordHash);
406
398
  temporaryPassword = clearPassword;
407
399
  }
408
400
  } else {
409
401
  // No email service — generate password, set it, and return one-time
410
402
  const clearPassword = generateSecurePassword();
411
- const passwordHash = await hashPassword(clearPassword);
403
+ const passwordHash = await ops.hashPassword(clearPassword);
412
404
  await authRepo.updatePassword(existing.id, passwordHash);
413
405
  temporaryPassword = clearPassword;
414
406
  }
@@ -430,7 +422,8 @@ displayName: existing.displayName }, appName);
430
422
  router.put("/users/:userId", requireAdmin, async (c) => {
431
423
  const userId = c.req.param("userId");
432
424
  const body = await c.req.json();
433
- let { email, displayName, password, roles } = body;
425
+ const { password } = body;
426
+ let { email, displayName, roles } = body;
434
427
 
435
428
  const existing = await authRepo.getUserById(userId);
436
429
  if (!existing) {
@@ -451,11 +444,11 @@ displayName: existing.displayName }, appName);
451
444
  if (displayName !== undefined) updates.displayName = displayName;
452
445
 
453
446
  if (password) {
454
- const validation = validatePasswordStrength(password);
447
+ const validation = ops.validatePasswordStrength(password);
455
448
  if (!validation.valid) {
456
449
  throw ApiError.badRequest(validation.errors.join(". "), "WEAK_PASSWORD");
457
450
  }
458
- updates.passwordHash = await hashPassword(password);
451
+ updates.passwordHash = await ops.hashPassword(password);
459
452
  }
460
453
 
461
454
  if (Object.keys(updates).length > 0) {
@@ -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
+ }