@rebasepro/server-core 0.1.0 → 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 (148) 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 +15909 -16083
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +15847 -16017
  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/app/frontend/node_modules/esbuild/LICENSE.md +0 -21
  72. package/app/frontend/node_modules/esbuild/README.md +0 -3
  73. package/app/frontend/node_modules/esbuild/bin/esbuild +0 -220
  74. package/app/frontend/node_modules/esbuild/install.js +0 -285
  75. package/app/frontend/node_modules/esbuild/lib/main.d.ts +0 -705
  76. package/app/frontend/node_modules/esbuild/lib/main.js +0 -2239
  77. package/app/frontend/node_modules/esbuild/package.json +0 -46
  78. package/dist/index-DXVBFp5V.js.map +0 -1
  79. package/examples/firebase/node_modules/esbuild/LICENSE.md +0 -21
  80. package/examples/firebase/node_modules/esbuild/README.md +0 -3
  81. package/examples/firebase/node_modules/esbuild/bin/esbuild +0 -220
  82. package/examples/firebase/node_modules/esbuild/install.js +0 -285
  83. package/examples/firebase/node_modules/esbuild/lib/main.d.ts +0 -705
  84. package/examples/firebase/node_modules/esbuild/lib/main.js +0 -2239
  85. package/examples/firebase/node_modules/esbuild/package.json +0 -46
  86. package/examples/medmot-staging/frontend/node_modules/esbuild/LICENSE.md +0 -21
  87. package/examples/medmot-staging/frontend/node_modules/esbuild/README.md +0 -3
  88. package/examples/medmot-staging/frontend/node_modules/esbuild/bin/esbuild +0 -220
  89. package/examples/medmot-staging/frontend/node_modules/esbuild/install.js +0 -285
  90. package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.d.ts +0 -705
  91. package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.js +0 -2239
  92. package/examples/medmot-staging/frontend/node_modules/esbuild/package.json +0 -46
  93. package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +0 -21
  94. package/examples/sdk-demo/node_modules/esbuild/README.md +0 -3
  95. package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +0 -223
  96. package/examples/sdk-demo/node_modules/esbuild/install.js +0 -289
  97. package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +0 -716
  98. package/examples/sdk-demo/node_modules/esbuild/lib/main.js +0 -2242
  99. package/examples/sdk-demo/node_modules/esbuild/package.json +0 -49
  100. package/packages/client/node_modules/esbuild/LICENSE.md +0 -21
  101. package/packages/client/node_modules/esbuild/README.md +0 -3
  102. package/packages/client/node_modules/esbuild/bin/esbuild +0 -220
  103. package/packages/client/node_modules/esbuild/install.js +0 -285
  104. package/packages/client/node_modules/esbuild/lib/main.d.ts +0 -705
  105. package/packages/client/node_modules/esbuild/lib/main.js +0 -2239
  106. package/packages/client/node_modules/esbuild/package.json +0 -46
  107. package/packages/client-postgresql/node_modules/esbuild/LICENSE.md +0 -21
  108. package/packages/client-postgresql/node_modules/esbuild/README.md +0 -3
  109. package/packages/client-postgresql/node_modules/esbuild/bin/esbuild +0 -220
  110. package/packages/client-postgresql/node_modules/esbuild/install.js +0 -285
  111. package/packages/client-postgresql/node_modules/esbuild/lib/main.d.ts +0 -705
  112. package/packages/client-postgresql/node_modules/esbuild/lib/main.js +0 -2239
  113. package/packages/client-postgresql/node_modules/esbuild/package.json +0 -46
  114. package/packages/common/node_modules/esbuild/LICENSE.md +0 -21
  115. package/packages/common/node_modules/esbuild/README.md +0 -3
  116. package/packages/common/node_modules/esbuild/bin/esbuild +0 -220
  117. package/packages/common/node_modules/esbuild/install.js +0 -285
  118. package/packages/common/node_modules/esbuild/lib/main.d.ts +0 -705
  119. package/packages/common/node_modules/esbuild/lib/main.js +0 -2239
  120. package/packages/common/node_modules/esbuild/package.json +0 -46
  121. package/packages/server-mongodb/node_modules/esbuild/LICENSE.md +0 -21
  122. package/packages/server-mongodb/node_modules/esbuild/README.md +0 -3
  123. package/packages/server-mongodb/node_modules/esbuild/bin/esbuild +0 -220
  124. package/packages/server-mongodb/node_modules/esbuild/install.js +0 -285
  125. package/packages/server-mongodb/node_modules/esbuild/lib/main.d.ts +0 -705
  126. package/packages/server-mongodb/node_modules/esbuild/lib/main.js +0 -2239
  127. package/packages/server-mongodb/node_modules/esbuild/package.json +0 -46
  128. package/packages/server-postgresql/node_modules/esbuild/LICENSE.md +0 -21
  129. package/packages/server-postgresql/node_modules/esbuild/README.md +0 -3
  130. package/packages/server-postgresql/node_modules/esbuild/bin/esbuild +0 -220
  131. package/packages/server-postgresql/node_modules/esbuild/install.js +0 -285
  132. package/packages/server-postgresql/node_modules/esbuild/lib/main.d.ts +0 -705
  133. package/packages/server-postgresql/node_modules/esbuild/lib/main.js +0 -2239
  134. package/packages/server-postgresql/node_modules/esbuild/package.json +0 -46
  135. package/packages/types/node_modules/esbuild/LICENSE.md +0 -21
  136. package/packages/types/node_modules/esbuild/README.md +0 -3
  137. package/packages/types/node_modules/esbuild/bin/esbuild +0 -220
  138. package/packages/types/node_modules/esbuild/install.js +0 -285
  139. package/packages/types/node_modules/esbuild/lib/main.d.ts +0 -705
  140. package/packages/types/node_modules/esbuild/lib/main.js +0 -2239
  141. package/packages/types/node_modules/esbuild/package.json +0 -46
  142. package/packages/utils/node_modules/esbuild/LICENSE.md +0 -21
  143. package/packages/utils/node_modules/esbuild/README.md +0 -3
  144. package/packages/utils/node_modules/esbuild/bin/esbuild +0 -220
  145. package/packages/utils/node_modules/esbuild/install.js +0 -285
  146. package/packages/utils/node_modules/esbuild/lib/main.d.ts +0 -705
  147. package/packages/utils/node_modules/esbuild/lib/main.js +0 -2239
  148. package/packages/utils/node_modules/esbuild/package.json +0 -46
@@ -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
+ }
@@ -3,7 +3,8 @@ import { ApiError, errorHandler } from "../api/errors";
3
3
  import { randomBytes, createHash } from "crypto";
4
4
  import type { AuthRepository, OAuthProvider } from "./interfaces";
5
5
  import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
6
- import { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
6
+ import type { AuthOverrides } from "./auth-overrides";
7
+ import { resolveAuthOverrides } from "./auth-overrides";
7
8
  import { requireAuth } from "./middleware";
8
9
  import { EmailService, EmailConfig } from "../email";
9
10
  import { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmailTemplate } from "../email/templates";
@@ -23,9 +24,15 @@ export interface AuthModuleConfig {
23
24
  /** Default role ID to assign to new users (default: none). Must NOT be "admin". */
24
25
  defaultRole?: string;
25
26
  /** Optional array of OAuth providers */
26
- oauthProviders?: OAuthProvider[];
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ oauthProviders?: OAuthProvider<any>[];
27
29
  /** When true, blocks all self-registration regardless of `allowRegistration`. */
28
30
  disableSelfRegistration?: boolean;
31
+ /**
32
+ * Auth overrides for customizing password hashing, credential
33
+ * verification, lifecycle hooks, etc.
34
+ */
35
+ overrides?: AuthOverrides;
29
36
  /**
30
37
  * Callback that checks if bootstrap has already been completed.
31
38
  * Used by GET /auth/config to report `needsSetup` status.
@@ -38,7 +45,7 @@ export interface AuthModuleConfig {
38
45
  * Helper to build standard auth response output
39
46
  */
40
47
  function buildAuthResponse(
41
- user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null },
48
+ user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
42
49
  roleIds: string[],
43
50
  accessToken: string,
44
51
  refreshToken: string
@@ -49,7 +56,8 @@ function buildAuthResponse(
49
56
  email: user.email,
50
57
  displayName: user.displayName ?? null,
51
58
  photoURL: user.photoUrl ?? null,
52
- roles: roleIds
59
+ roles: roleIds,
60
+ metadata: user.metadata ?? {}
53
61
  },
54
62
  tokens: {
55
63
  accessToken,
@@ -93,7 +101,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
93
101
  router.onError(errorHandler);
94
102
 
95
103
  const authRepo = config.authRepo;
96
- const { emailService, emailConfig, allowRegistration = false } = config;
104
+ const { emailService, emailConfig, allowRegistration = false, overrides } = config;
105
+ const ops = resolveAuthOverrides(overrides);
97
106
 
98
107
  // ── Zod input schemas ──────────────────────────────────────────────
99
108
  const registerSchema = z.object({
@@ -214,7 +223,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
214
223
  }
215
224
 
216
225
  // Validate password strength
217
- const passwordValidation = validatePasswordStrength(password);
226
+ const passwordValidation = ops.validatePasswordStrength(password);
218
227
  if (!passwordValidation.valid) {
219
228
  throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
220
229
  }
@@ -226,12 +235,16 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
226
235
  }
227
236
 
228
237
  // Create user
229
- const passwordHash = await hashPassword(password);
230
- const user = await authRepo.createUser({
238
+ const passwordHash = await ops.hashPassword(password);
239
+ let createData: import("./interfaces").CreateUserData = {
231
240
  email: email.toLowerCase(),
232
241
  passwordHash,
233
242
  displayName: displayName || undefined
234
- });
243
+ };
244
+ if (overrides?.beforeUserCreate) {
245
+ createData = await overrides.beforeUserCreate(createData);
246
+ }
247
+ const user = await authRepo.createUser(createData);
235
248
 
236
249
  // Auto-bootstrap: if this is the very first user in the system, promote to admin.
237
250
  // This avoids the chicken-and-egg problem where the first user has no permissions
@@ -256,6 +269,20 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
256
269
  sendWelcomeEmail({ email: user.email,
257
270
  displayName: user.displayName });
258
271
 
272
+ // Fire afterUserCreate hook (fire-and-forget)
273
+ if (overrides?.afterUserCreate) {
274
+ overrides.afterUserCreate(user).catch(err => {
275
+ console.error("[AuthOverrides] afterUserCreate error:", err instanceof Error ? err.message : err);
276
+ });
277
+ }
278
+
279
+ // Fire onAuthenticated hook (fire-and-forget)
280
+ if (overrides?.onAuthenticated) {
281
+ overrides.onAuthenticated(user, "register").catch(err => {
282
+ console.error("[AuthOverrides] onAuthenticated error:", err instanceof Error ? err.message : err);
283
+ });
284
+ }
285
+
259
286
  return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
260
287
  });
261
288
 
@@ -266,18 +293,29 @@ displayName: user.displayName });
266
293
  router.post("/login", defaultAuthLimiter, async (c) => {
267
294
  const { email, password } = parseBody(loginSchema, await c.req.json());
268
295
 
269
- const user = await authRepo.getUserByEmail(email);
270
- if (!user) {
271
- throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
272
- }
296
+ let user;
273
297
 
274
- if (!user.passwordHash) {
275
- throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
276
- }
298
+ if (overrides?.verifyCredentials) {
299
+ // Full credential verification override
300
+ user = await overrides.verifyCredentials(email, password, authRepo);
301
+ if (!user) {
302
+ throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
303
+ }
304
+ } else {
305
+ // Default: email lookup + password hash verification
306
+ user = await authRepo.getUserByEmail(email);
307
+ if (!user) {
308
+ throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
309
+ }
277
310
 
278
- const isValidPassword = await verifyPassword(password, user.passwordHash);
279
- if (!isValidPassword) {
280
- throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
311
+ if (!user.passwordHash) {
312
+ throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
313
+ }
314
+
315
+ const isValidPassword = await ops.verifyPassword(password, user.passwordHash);
316
+ if (!isValidPassword) {
317
+ throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
318
+ }
281
319
  }
282
320
 
283
321
  const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
@@ -286,6 +324,13 @@ displayName: user.displayName });
286
324
  c.req.header("x-forwarded-for") || "unknown"
287
325
  );
288
326
 
327
+ // Fire onAuthenticated hook (fire-and-forget)
328
+ if (overrides?.onAuthenticated) {
329
+ overrides.onAuthenticated(user, "login").catch(err => {
330
+ console.error("[AuthOverrides] onAuthenticated error:", err instanceof Error ? err.message : err);
331
+ });
332
+ }
333
+
289
334
  return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken));
290
335
  });
291
336
 
@@ -434,7 +479,7 @@ displayName: user.displayName }, appName);
434
479
  const { token, password } = parseBody(resetPasswordSchema, await c.req.json());
435
480
 
436
481
  // Validate password strength
437
- const passwordValidation = validatePasswordStrength(password);
482
+ const passwordValidation = ops.validatePasswordStrength(password);
438
483
  if (!passwordValidation.valid) {
439
484
  throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
440
485
  }
@@ -448,7 +493,7 @@ displayName: user.displayName }, appName);
448
493
  }
449
494
 
450
495
  // Update password
451
- const passwordHash = await hashPassword(password);
496
+ const passwordHash = await ops.hashPassword(password);
452
497
  await authRepo.updatePassword(storedToken.userId, passwordHash);
453
498
 
454
499
  // Mark token as used
@@ -480,19 +525,19 @@ message: "Password has been reset successfully" });
480
525
  }
481
526
 
482
527
  // Verify old password
483
- const isValidOldPassword = await verifyPassword(oldPassword, user.passwordHash);
528
+ const isValidOldPassword = await ops.verifyPassword(oldPassword, user.passwordHash);
484
529
  if (!isValidOldPassword) {
485
530
  throw ApiError.unauthorized("Current password is incorrect", "INVALID_CREDENTIALS");
486
531
  }
487
532
 
488
533
  // Validate new password strength
489
- const passwordValidation = validatePasswordStrength(newPassword);
534
+ const passwordValidation = ops.validatePasswordStrength(newPassword);
490
535
  if (!passwordValidation.valid) {
491
536
  throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
492
537
  }
493
538
 
494
539
  // Update password
495
- const passwordHash = await hashPassword(newPassword);
540
+ const passwordHash = await ops.hashPassword(newPassword);
496
541
  await authRepo.updatePassword(user.id, passwordHash);
497
542
 
498
543
  // Invalidate all refresh tokens (security: log out all sessions)
@@ -727,7 +772,8 @@ message: "Session revoked successfully" });
727
772
  displayName: result.user.displayName,
728
773
  photoURL: result.user.photoUrl,
729
774
  emailVerified: result.user.emailVerified,
730
- roles: result.roles.map(r => r.id)
775
+ roles: result.roles.map(r => r.id),
776
+ metadata: result.user.metadata ?? {}
731
777
  }
732
778
  });
733
779
  });
@@ -765,7 +811,8 @@ message: "Session revoked successfully" });
765
811
  displayName: result.user.displayName,
766
812
  photoURL: result.user.photoUrl,
767
813
  emailVerified: result.user.emailVerified,
768
- roles: result.roles.map(r => r.id)
814
+ roles: result.roles.map(r => r.id),
815
+ metadata: result.user.metadata ?? {}
769
816
  }
770
817
  });
771
818
  });
@@ -788,18 +835,13 @@ message: "Session revoked successfully" });
788
835
  // Registration is allowed when explicitly enabled OR during initial setup
789
836
  const registrationAllowed = needsSetup || !!allowRegistration;
790
837
 
791
- // Build a dynamic map of enabled providers for frontend discovery.
792
- // Also maintain legacy boolean fields for backward compatibility.
838
+ // Build the list of enabled OAuth providers for frontend discovery.
793
839
  const enabledProviders = (config.oauthProviders || []).map(p => p.id);
794
840
 
795
841
  return c.json({
796
842
  needsSetup,
797
843
  registrationEnabled: registrationAllowed,
798
- // Legacy fields (kept for backward compat)
799
- googleEnabled: enabledProviders.includes("google"),
800
- linkedinEnabled: enabledProviders.includes("linkedin"),
801
844
  emailServiceEnabled: isEmailConfigured(),
802
- // New: complete list of available OAuth providers
803
845
  enabledProviders
804
846
  });
805
847
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
2
2
  import { CronScheduler, validateCronExpression } from "./cron-scheduler";
3
- import type { CronJobDefinition } from "@rebasepro/types";
3
+ import type { CronJobDefinition, CronJobLogEntry } from "@rebasepro/types";
4
4
  import type { LoadedCronJob } from "./cron-loader";
5
5
 
6
6
  // ─── Helpers ────────────────────────────────────────────────────────
@@ -475,12 +475,12 @@ describe("CronScheduler", () => {
475
475
  beforeEach(() => { jest.useRealTimers(); });
476
476
 
477
477
  it("persists logs to store after execution", async () => {
478
- const insertLog = jest.fn<(entry: any) => Promise<void>>().mockResolvedValue(undefined);
478
+ const insertLog = jest.fn<(entry: CronJobLogEntry) => Promise<void>>().mockResolvedValue(undefined);
479
479
  const mockStore = {
480
480
  ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
481
481
  insertLog,
482
- fetchLogs: jest.fn<() => Promise<any[]>>().mockResolvedValue([]),
483
- fetchJobStats: jest.fn<() => Promise<Map<string, any>>>().mockResolvedValue(new Map()),
482
+ fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
483
+ fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map()),
484
484
  };
485
485
  scheduler.setStore(mockStore);
486
486
  scheduler.registerJobs([makeJob("persisted")]);
@@ -493,8 +493,8 @@ describe("CronScheduler", () => {
493
493
  const mockStore = {
494
494
  ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
495
495
  insertLog: jest.fn<() => Promise<void>>().mockRejectedValue(new Error("DB down")),
496
- fetchLogs: jest.fn<() => Promise<any[]>>().mockResolvedValue([]),
497
- fetchJobStats: jest.fn<() => Promise<Map<string, any>>>().mockResolvedValue(new Map()),
496
+ fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
497
+ fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map()),
498
498
  };
499
499
  scheduler.setStore(mockStore);
500
500
  scheduler.registerJobs([makeJob("resilient")]);
@@ -504,13 +504,13 @@ describe("CronScheduler", () => {
504
504
  });
505
505
 
506
506
  it("seeds counters from store on start", async () => {
507
- const stats = new Map<string, any>();
507
+ const stats = new Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>();
508
508
  stats.set("seeded", { totalRuns: 42, totalFailures: 3, lastRunAt: "2026-01-01T00:00:00Z" });
509
509
  const mockStore = {
510
510
  ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
511
511
  insertLog: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
512
- fetchLogs: jest.fn<() => Promise<any[]>>().mockResolvedValue([]),
513
- fetchJobStats: jest.fn<() => Promise<Map<string, any>>>().mockResolvedValue(stats),
512
+ fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
513
+ fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(stats),
514
514
  };
515
515
  scheduler.setStore(mockStore);
516
516
  scheduler.registerJobs([makeJob("seeded")]);
@@ -15,7 +15,7 @@ import type { CronStore } from "./cron-store";
15
15
 
16
16
  /**
17
17
  * Expand a single cron field into an ordered array of allowed values.
18
- * Supports: `*`, `N`, `N-M`, `N/S`, `N-M/S`, `*​/S`, and comma-separated combinations.
18
+ * Supports: `*`, `N`, `N-M`, `N/S`, `N-M/S`, `*\/S`, and comma-separated combinations.
19
19
  */
20
20
  function expandCronField(field: string, min: number, max: number): number[] {
21
21
  const results = new Set<number>();