@rebasepro/server-core 0.5.0 → 0.6.0

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 (279) hide show
  1. package/dist/{server-core/src/api → api}/errors.d.ts +5 -2
  2. package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
  3. package/dist/{server-core/src/api → api}/types.d.ts +2 -0
  4. package/dist/auth/admin-user-ops.d.ts +79 -0
  5. package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
  6. package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
  7. package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
  8. package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
  9. package/dist/auth/mfa-routes.d.ts +6 -0
  10. package/dist/auth/reset-password-admin.d.ts +29 -0
  11. package/dist/auth/session-routes.d.ts +25 -0
  12. package/dist/backend-CIxN4FVm.js +15 -0
  13. package/dist/backend-CIxN4FVm.js.map +1 -0
  14. package/dist/chunk-Dze3rakg.js +42 -0
  15. package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
  16. package/dist/dist-CZKP-Xz4.js +832 -0
  17. package/dist/dist-CZKP-Xz4.js.map +1 -0
  18. package/dist/env.d.ts +102 -0
  19. package/dist/from-VbwD7xRf.js +3849 -0
  20. package/dist/from-VbwD7xRf.js.map +1 -0
  21. package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
  22. package/dist/index.es.js +60867 -50462
  23. package/dist/index.es.js.map +1 -1
  24. package/dist/index.umd.js +73151 -51298
  25. package/dist/index.umd.js.map +1 -1
  26. package/dist/init/docs.d.ts +4 -0
  27. package/dist/init/health.d.ts +2 -0
  28. package/dist/init/middlewares.d.ts +10 -0
  29. package/dist/init/shutdown.d.ts +11 -0
  30. package/dist/init/storage.d.ts +5 -0
  31. package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
  32. package/dist/jwt-DHcQRGC3.js +4168 -0
  33. package/dist/jwt-DHcQRGC3.js.map +1 -0
  34. package/dist/logger-BYU66ENZ.js +94 -0
  35. package/dist/logger-BYU66ENZ.js.map +1 -0
  36. package/dist/ms-BeBSuOXN.js +125 -0
  37. package/dist/ms-BeBSuOXN.js.map +1 -0
  38. package/dist/multipart-parser-CedBDOeC.js +299 -0
  39. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  40. package/dist/src-COaj0G3P.js +1182 -0
  41. package/dist/src-COaj0G3P.js.map +1 -0
  42. package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
  43. package/dist/utils/request-id.d.ts +4 -0
  44. package/package.json +29 -29
  45. package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
  46. package/src/api/errors.ts +75 -19
  47. package/src/api/graphql/graphql-schema-generator.ts +15 -10
  48. package/src/api/logs-routes.ts +5 -2
  49. package/src/api/rest/api-generator-count.test.ts +21 -10
  50. package/src/api/rest/api-generator.ts +83 -24
  51. package/src/api/rest/query-parser.ts +18 -12
  52. package/src/api/server.ts +12 -1
  53. package/src/api/types.ts +2 -0
  54. package/src/auth/adapter-middleware.ts +17 -10
  55. package/src/auth/admin-user-ops.ts +236 -0
  56. package/src/auth/api-keys/api-key-middleware.ts +15 -9
  57. package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
  58. package/src/auth/api-keys/api-key-routes.ts +3 -3
  59. package/src/auth/api-keys/api-key-store.ts +14 -8
  60. package/src/auth/api-keys/index.ts +2 -2
  61. package/src/auth/apple-oauth.ts +4 -3
  62. package/src/auth/auth-hooks.ts +50 -10
  63. package/src/auth/bitbucket-oauth.ts +5 -4
  64. package/src/auth/builtin-auth-adapter.ts +60 -39
  65. package/src/auth/custom-auth-adapter.ts +4 -5
  66. package/src/auth/discord-oauth.ts +5 -4
  67. package/src/auth/facebook-oauth.ts +5 -4
  68. package/src/auth/github-oauth.ts +6 -5
  69. package/src/auth/gitlab-oauth.ts +5 -4
  70. package/src/auth/google-oauth.ts +2 -1
  71. package/src/auth/index.ts +6 -2
  72. package/src/auth/jwt.ts +5 -4
  73. package/src/auth/linkedin-oauth.ts +4 -3
  74. package/src/auth/mfa-routes.ts +299 -0
  75. package/src/auth/mfa.ts +2 -1
  76. package/src/auth/microsoft-oauth.ts +5 -4
  77. package/src/auth/middleware.ts +5 -4
  78. package/src/auth/rate-limiter.ts +1 -1
  79. package/src/auth/reset-password-admin.ts +144 -0
  80. package/src/auth/rls-scope.ts +1 -1
  81. package/src/auth/routes.ts +52 -596
  82. package/src/auth/session-routes.ts +341 -0
  83. package/src/auth/slack-oauth.ts +6 -5
  84. package/src/auth/spotify-oauth.ts +5 -4
  85. package/src/auth/twitter-oauth.ts +4 -3
  86. package/src/collections/loader.ts +5 -4
  87. package/src/cron/cron-scheduler.test.ts +23 -11
  88. package/src/cron/cron-scheduler.ts +14 -8
  89. package/src/cron/cron-store.ts +1 -1
  90. package/src/email/smtp-email-service.ts +3 -2
  91. package/src/env.ts +11 -10
  92. package/src/index.ts +1 -0
  93. package/src/init/docs.ts +47 -0
  94. package/src/init/health.ts +37 -0
  95. package/src/init/middlewares.ts +61 -0
  96. package/src/init/shutdown.ts +56 -0
  97. package/src/init/storage.ts +57 -0
  98. package/src/init.ts +85 -279
  99. package/src/serve-spa.ts +12 -5
  100. package/src/services/driver-registry.ts +4 -3
  101. package/src/storage/S3StorageController.ts +1 -2
  102. package/src/storage/image-transform.ts +25 -10
  103. package/src/storage/routes.ts +30 -11
  104. package/src/storage/storage-registry.ts +4 -3
  105. package/src/storage/tus-handler.ts +12 -12
  106. package/src/utils/request-id.ts +40 -0
  107. package/src/utils/request-logger.ts +6 -0
  108. package/test/api-generator.test.ts +90 -2
  109. package/test/api-key-permission-guard.test.ts +24 -12
  110. package/test/auth-routes.test.ts +5 -3
  111. package/test/backend-hooks-data.test.ts +99 -30
  112. package/test/custom-auth-adapter.test.ts +16 -15
  113. package/test/email-templates.test.ts +10 -5
  114. package/test/env.test.ts +10 -10
  115. package/test/function-loader.test.ts +7 -4
  116. package/test/graphql-schema-generator.test.ts +554 -0
  117. package/test/jwt-security.test.ts +1 -1
  118. package/test/query-parser.test.ts +0 -1
  119. package/test/reset-password-admin.test.ts +113 -0
  120. package/test/singleton.test.ts +5 -5
  121. package/test/smtp-email-service.test.ts +21 -21
  122. package/test/webhook-service.test.ts +22 -11
  123. package/tsconfig.json +2 -0
  124. package/tsconfig.prod.json +3 -0
  125. package/vite.config.ts +5 -5
  126. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  127. package/dist/common/src/collections/default-collections.d.ts +0 -9
  128. package/dist/common/src/collections/index.d.ts +0 -2
  129. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  130. package/dist/common/src/data/query_builder.d.ts +0 -55
  131. package/dist/common/src/index.d.ts +0 -4
  132. package/dist/common/src/util/builders.d.ts +0 -57
  133. package/dist/common/src/util/callbacks.d.ts +0 -6
  134. package/dist/common/src/util/collections.d.ts +0 -11
  135. package/dist/common/src/util/common.d.ts +0 -2
  136. package/dist/common/src/util/conditions.d.ts +0 -26
  137. package/dist/common/src/util/entities.d.ts +0 -58
  138. package/dist/common/src/util/enums.d.ts +0 -3
  139. package/dist/common/src/util/index.d.ts +0 -16
  140. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  141. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  142. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  143. package/dist/common/src/util/paths.d.ts +0 -14
  144. package/dist/common/src/util/permissions.d.ts +0 -14
  145. package/dist/common/src/util/references.d.ts +0 -2
  146. package/dist/common/src/util/relations.d.ts +0 -22
  147. package/dist/common/src/util/resolutions.d.ts +0 -72
  148. package/dist/common/src/util/storage.d.ts +0 -24
  149. package/dist/index-Cr1D21av.js +0 -49
  150. package/dist/index-Cr1D21av.js.map +0 -1
  151. package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
  152. package/dist/server-core/src/env.d.ts +0 -137
  153. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  154. package/dist/types/src/controllers/auth.d.ts +0 -104
  155. package/dist/types/src/controllers/client.d.ts +0 -168
  156. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  157. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  158. package/dist/types/src/controllers/data.d.ts +0 -207
  159. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  160. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  161. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  162. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  163. package/dist/types/src/controllers/email.d.ts +0 -36
  164. package/dist/types/src/controllers/index.d.ts +0 -18
  165. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  166. package/dist/types/src/controllers/navigation.d.ts +0 -225
  167. package/dist/types/src/controllers/registry.d.ts +0 -63
  168. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  169. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  170. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  171. package/dist/types/src/controllers/storage.d.ts +0 -171
  172. package/dist/types/src/index.d.ts +0 -4
  173. package/dist/types/src/rebase_context.d.ts +0 -122
  174. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  175. package/dist/types/src/types/backend.d.ts +0 -571
  176. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  177. package/dist/types/src/types/builders.d.ts +0 -15
  178. package/dist/types/src/types/chips.d.ts +0 -5
  179. package/dist/types/src/types/collections.d.ts +0 -961
  180. package/dist/types/src/types/component_ref.d.ts +0 -47
  181. package/dist/types/src/types/cron.d.ts +0 -102
  182. package/dist/types/src/types/data_source.d.ts +0 -64
  183. package/dist/types/src/types/database_adapter.d.ts +0 -94
  184. package/dist/types/src/types/entities.d.ts +0 -145
  185. package/dist/types/src/types/entity_actions.d.ts +0 -104
  186. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  187. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  188. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  189. package/dist/types/src/types/entity_views.d.ts +0 -87
  190. package/dist/types/src/types/export_import.d.ts +0 -21
  191. package/dist/types/src/types/formex.d.ts +0 -40
  192. package/dist/types/src/types/index.d.ts +0 -28
  193. package/dist/types/src/types/locales.d.ts +0 -4
  194. package/dist/types/src/types/modify_collections.d.ts +0 -5
  195. package/dist/types/src/types/plugins.d.ts +0 -282
  196. package/dist/types/src/types/properties.d.ts +0 -1173
  197. package/dist/types/src/types/property_config.d.ts +0 -74
  198. package/dist/types/src/types/relations.d.ts +0 -336
  199. package/dist/types/src/types/slots.d.ts +0 -262
  200. package/dist/types/src/types/translations.d.ts +0 -900
  201. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  202. package/dist/types/src/types/websockets.d.ts +0 -78
  203. package/dist/types/src/users/index.d.ts +0 -1
  204. package/dist/types/src/users/user.d.ts +0 -50
  205. package/src/auth/admin-routes.ts +0 -534
  206. package/test/admin-routes.test.ts +0 -544
  207. package/test/backend-hooks-admin.test.ts +0 -388
  208. /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
  209. /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
  210. /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
  211. /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
  212. /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
  213. /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
  214. /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
  215. /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
  216. /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
  217. /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
  218. /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
  219. /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
  220. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
  221. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
  222. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
  223. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
  224. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
  225. /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
  226. /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
  227. /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
  228. /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
  229. /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
  230. /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
  231. /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
  232. /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
  233. /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
  234. /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
  235. /package/dist/{server-core/src/auth → auth}/jwt.d.ts +0 -0
  236. /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
  237. /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
  238. /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
  239. /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
  240. /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
  241. /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
  242. /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
  243. /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
  244. /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
  245. /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
  246. /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
  247. /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
  248. /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
  249. /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
  250. /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
  251. /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
  252. /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
  253. /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
  254. /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
  255. /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
  256. /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
  257. /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
  258. /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
  259. /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
  260. /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
  261. /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
  262. /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
  263. /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
  264. /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
  265. /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
  266. /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
  267. /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
  268. /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
  269. /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
  270. /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
  271. /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
  272. /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
  273. /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
  274. /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
  275. /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
  276. /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
  277. /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
  278. /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
  279. /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
@@ -0,0 +1,341 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import { randomBytes } from "crypto";
4
+ import { ApiError } from "../api/errors";
5
+ import { HonoEnv } from "../api/types";
6
+ import { requireAuth } from "./middleware";
7
+ import { logger } from "../utils/logger";
8
+ import { strictAuthLimiter, defaultAuthLimiter } from "./rate-limiter";
9
+ import { hashRefreshToken } from "./jwt";
10
+ import type { AuthModuleConfig } from "./routes";
11
+ import { resolveAuthHooks } from "./auth-hooks";
12
+ import type { CreateUserData } from "./interfaces";
13
+
14
+ interface SessionRoutesConfig {
15
+ router: Hono<HonoEnv>;
16
+ config: AuthModuleConfig;
17
+ ops: ReturnType<typeof resolveAuthHooks>;
18
+ parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;
19
+ buildAuthResponse: (
20
+ user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
21
+ roleIds: string[],
22
+ accessToken: string,
23
+ refreshToken: string
24
+ ) => unknown;
25
+ createSessionAndTokens: (userId: string, userAgent: string, ipAddress: string) => Promise<{
26
+ roleIds: string[];
27
+ accessToken: string;
28
+ refreshToken: string;
29
+ }>;
30
+ }
31
+
32
+ export function mountSessionRoutes(opts: SessionRoutesConfig): void {
33
+ const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens } = opts;
34
+ const authRepo = config.authRepo;
35
+
36
+ const logoutSchema = z.object({
37
+ refreshToken: z.string().optional()
38
+ });
39
+
40
+ const linkSchema = z.object({
41
+ email: z.string().email("Invalid email address").max(255),
42
+ password: z.string().min(1, "Password is required").max(128)
43
+ });
44
+
45
+ const updateProfileSchema = z.object({
46
+ displayName: z.string().max(255).optional(),
47
+ photoURL: z.string().url().max(2048).optional()
48
+ });
49
+
50
+ const isEmailConfigured = () => !!(config.emailService && config.emailService.isConfigured());
51
+
52
+ /**
53
+ * POST /auth/logout
54
+ */
55
+ router.post("/logout", async (c) => {
56
+ const { refreshToken } = parseBody(logoutSchema, await c.req.json());
57
+
58
+ if (refreshToken) {
59
+ const tokenHash = hashRefreshToken(refreshToken);
60
+ await authRepo.deleteRefreshToken(tokenHash);
61
+ }
62
+
63
+ // Call afterLogout hook (fire-and-forget)
64
+ // Extract userId from the access token if present
65
+ const authHeader = c.req.header("authorization");
66
+ if (ops.afterLogout && authHeader?.startsWith("Bearer ")) {
67
+ const { verifyAccessToken } = await import("./jwt");
68
+ const payload = verifyAccessToken(authHeader.substring(7));
69
+ if (payload) {
70
+ ops.afterLogout(payload.userId).catch((err) => {
71
+ logger.error("[AuthHooks] afterLogout error", {
72
+ error: err instanceof Error ? err.message : err
73
+ });
74
+ });
75
+ }
76
+ }
77
+
78
+ return c.json({ success: true });
79
+ });
80
+
81
+ /**
82
+ * GET /auth/sessions
83
+ * Get active refresh tokens (sessions) for the current user
84
+ */
85
+ router.get("/sessions", requireAuth, async (c) => {
86
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
87
+ if (!userCtx) {
88
+ throw ApiError.unauthorized("Not authenticated");
89
+ }
90
+
91
+ const currentRefreshToken = c.req.header("x-refresh-token") as string;
92
+ const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
93
+
94
+ const sessions = await authRepo.listRefreshTokensForUser(userCtx.userId);
95
+
96
+ const mappedSessions = sessions.map((s) => ({
97
+ id: s.id,
98
+ userAgent: s.userAgent,
99
+ ipAddress: s.ipAddress,
100
+ createdAt: s.createdAt,
101
+ isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
102
+ }));
103
+
104
+ return c.json({ sessions: mappedSessions });
105
+ });
106
+
107
+ /**
108
+ * DELETE /auth/sessions
109
+ * Delete all refresh tokens for the current user (remote logout every device)
110
+ */
111
+ router.delete("/sessions", requireAuth, async (c) => {
112
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
113
+ if (!userCtx) {
114
+ throw ApiError.unauthorized("Not authenticated");
115
+ }
116
+
117
+ await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
118
+ return c.json({
119
+ success: true,
120
+ message: "All sessions revoked successfully"
121
+ });
122
+ });
123
+
124
+ /**
125
+ * DELETE /auth/sessions/:id
126
+ * Delete a specific refresh token (remote logout)
127
+ */
128
+ router.delete("/sessions/:id", requireAuth, async (c) => {
129
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
130
+ if (!userCtx) {
131
+ throw ApiError.unauthorized("Not authenticated");
132
+ }
133
+
134
+ const id = c.req.param("id");
135
+ if (!id) {
136
+ throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
137
+ }
138
+
139
+ await authRepo.deleteRefreshTokenById(id, userCtx.userId);
140
+ return c.json({
141
+ success: true,
142
+ message: "Session revoked successfully"
143
+ });
144
+ });
145
+
146
+ /**
147
+ * GET /auth/me
148
+ * Get current authenticated user
149
+ */
150
+ router.get("/me", requireAuth, async (c) => {
151
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
152
+ if (!userCtx) {
153
+ throw ApiError.unauthorized("Not authenticated");
154
+ }
155
+
156
+ const result = await authRepo.getUserWithRoles(userCtx.userId);
157
+ if (!result) {
158
+ throw ApiError.notFound("User not found");
159
+ }
160
+
161
+ return c.json({
162
+ user: {
163
+ uid: result.user.id,
164
+ email: result.user.email,
165
+ displayName: result.user.displayName,
166
+ photoURL: result.user.photoUrl,
167
+ emailVerified: result.user.emailVerified,
168
+ roles: result.roles.map((r) => r.id),
169
+ metadata: result.user.metadata ?? {}
170
+ }
171
+ });
172
+ });
173
+
174
+ /**
175
+ * PATCH /auth/me
176
+ * Update current authenticated user profile
177
+ */
178
+ router.patch("/me", requireAuth, async (c) => {
179
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
180
+ if (!userCtx) {
181
+ throw ApiError.unauthorized("Not authenticated");
182
+ }
183
+
184
+ const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
185
+
186
+ const updatedUser = await authRepo.updateUser(userCtx.userId, {
187
+ displayName: displayName !== undefined ? displayName : undefined,
188
+ photoUrl: photoURL !== undefined ? photoURL : undefined
189
+ });
190
+
191
+ if (!updatedUser) {
192
+ throw ApiError.notFound("User not found");
193
+ }
194
+
195
+ const result = await authRepo.getUserWithRoles(userCtx.userId);
196
+ if (!result) {
197
+ throw ApiError.notFound("User not found");
198
+ }
199
+
200
+ return c.json({
201
+ user: {
202
+ uid: result.user.id,
203
+ email: result.user.email,
204
+ displayName: result.user.displayName,
205
+ photoURL: result.user.photoUrl,
206
+ emailVerified: result.user.emailVerified,
207
+ roles: result.roles.map((r) => r.id),
208
+ metadata: result.user.metadata ?? {}
209
+ }
210
+ });
211
+ });
212
+
213
+ /**
214
+ * GET /auth/config
215
+ * Get public auth configuration
216
+ */
217
+ router.get("/config", defaultAuthLimiter, async (c) => {
218
+ let needsSetup: boolean;
219
+ if (config.isBootstrapCompleted) {
220
+ needsSetup = !(await config.isBootstrapCompleted());
221
+ } else {
222
+ const allUsers = await authRepo.listUsers();
223
+ needsSetup = allUsers.length === 0;
224
+ }
225
+
226
+ const registrationAllowed = needsSetup || !!config.allowRegistration;
227
+ const enabledProviders = (config.oauthProviders || []).map((p) => p.id);
228
+
229
+ return c.json({
230
+ needsSetup,
231
+ registrationEnabled: registrationAllowed,
232
+ emailServiceEnabled: isEmailConfigured(),
233
+ enabledProviders
234
+ });
235
+ });
236
+
237
+ /**
238
+ * POST /auth/anonymous
239
+ * Create an anonymous user with temporary credentials
240
+ */
241
+ router.post("/anonymous", strictAuthLimiter, async (c) => {
242
+ const anonId = randomBytes(16).toString("hex");
243
+ const anonEmail = `anon_${anonId.slice(0, 8)}@anonymous.local`;
244
+
245
+ let createData: CreateUserData = {
246
+ email: anonEmail,
247
+ emailVerified: false,
248
+ isAnonymous: true
249
+ };
250
+
251
+ if (ops.beforeUserCreate) {
252
+ createData = await ops.beforeUserCreate(createData);
253
+ }
254
+
255
+ const user = await authRepo.createUser(createData);
256
+
257
+ // Assign default role (follow register route pattern, but never auto-admin)
258
+ if (config.defaultRole) {
259
+ await authRepo.assignDefaultRole(user.id, config.defaultRole);
260
+ }
261
+
262
+ const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
263
+ user.id,
264
+ c.req.header("user-agent") || "unknown",
265
+ c.req.header("x-forwarded-for") || "unknown"
266
+ );
267
+
268
+ // Fire afterUserCreate hook
269
+ if (ops.afterUserCreate) {
270
+ ops.afterUserCreate(user).catch((err) => {
271
+ logger.error("[AuthHooks] afterUserCreate error", {
272
+ error: err instanceof Error ? err.message : err
273
+ });
274
+ });
275
+ }
276
+
277
+ // Fire onAuthenticated hook
278
+ if (ops.onAuthenticated) {
279
+ ops.onAuthenticated(user, "anonymous").catch((err) => {
280
+ logger.error("[AuthHooks] onAuthenticated error", {
281
+ error: err instanceof Error ? err.message : err
282
+ });
283
+ });
284
+ }
285
+
286
+ return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
287
+ });
288
+
289
+ /**
290
+ * POST /auth/anonymous/link
291
+ * Upgrade an anonymous user to a permanent account with email/password
292
+ */
293
+ router.post("/anonymous/link", requireAuth, async (c) => {
294
+ const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
295
+ if (!userCtx) {
296
+ throw ApiError.unauthorized("Not authenticated");
297
+ }
298
+
299
+ const user = await authRepo.getUserById(userCtx.userId);
300
+ if (!user?.isAnonymous) {
301
+ throw ApiError.badRequest("User is not anonymous", "NOT_ANONYMOUS");
302
+ }
303
+
304
+ const { email, password } = parseBody(linkSchema, await c.req.json());
305
+
306
+ // Validate password strength
307
+ const passwordValidation = ops.validatePasswordStrength(password);
308
+ if (!passwordValidation.valid) {
309
+ throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
310
+ }
311
+
312
+ // Check if email is already taken
313
+ const existingUser = await authRepo.getUserByEmail(email.toLowerCase());
314
+ if (existingUser) {
315
+ throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
316
+ }
317
+
318
+ // Hash password
319
+ const passwordHash = await ops.hashPassword(password);
320
+
321
+ // Update user: set email, password, remove anonymous flag
322
+ const updatedUser = await authRepo.updateUser(user.id, {
323
+ email: email.toLowerCase(),
324
+ passwordHash,
325
+ isAnonymous: false
326
+ });
327
+
328
+ if (!updatedUser) {
329
+ throw ApiError.notFound("User not found");
330
+ }
331
+
332
+ // Generate new tokens with updated identity
333
+ const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
334
+ user.id,
335
+ c.req.header("user-agent") || "unknown",
336
+ c.req.header("x-forwarded-for") || "unknown"
337
+ );
338
+
339
+ return c.json(buildAuthResponse(updatedUser, roleIds, accessToken, refreshToken));
340
+ });
341
+ }
@@ -1,5 +1,6 @@
1
1
  import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
2
2
  import { z } from "zod";
3
+ import { logger } from "../utils/logger";
3
4
 
4
5
  /**
5
6
  * Creates a Slack OAuth Provider integration (OAuth 2.0 / "Sign in with Slack").
@@ -27,13 +28,13 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
27
28
  });
28
29
 
29
30
  if (!tokenResponse.ok) {
30
- console.error("Failed to get Slack access token:", await tokenResponse.text());
31
+ logger.error("Failed to get Slack access token", { detail: await tokenResponse.text() });
31
32
  return null;
32
33
  }
33
34
 
34
35
  const tokenData = await tokenResponse.json() as { ok: boolean; access_token?: string; error?: string };
35
36
  if (!tokenData.ok || !tokenData.access_token) {
36
- console.error("Slack token exchange failed:", tokenData.error);
37
+ logger.error("Slack token exchange failed", { detail: tokenData.error });
37
38
  return null;
38
39
  }
39
40
 
@@ -42,7 +43,7 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
42
43
  });
43
44
 
44
45
  if (!profileResponse.ok) {
45
- console.error("Failed to get Slack user info:", await profileResponse.text());
46
+ logger.error("Failed to get Slack user info", { detail: await profileResponse.text() });
46
47
  return null;
47
48
  }
48
49
 
@@ -52,7 +53,7 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
52
53
  };
53
54
 
54
55
  if (!p.ok || !p.email) {
55
- console.error("Slack user has no email");
56
+ logger.error("Slack user has no email");
56
57
  return null;
57
58
  }
58
59
 
@@ -63,7 +64,7 @@ export function createSlackProvider(config: { clientId: string; clientSecret: st
63
64
  photoUrl: p.picture || null
64
65
  };
65
66
  } catch (error) {
66
- console.error("Slack OAuth error:", error);
67
+ logger.error("Slack OAuth error", { error: error });
67
68
  return null;
68
69
  }
69
70
  }
@@ -1,5 +1,6 @@
1
1
  import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
2
2
  import { z } from "zod";
3
+ import { logger } from "../utils/logger";
3
4
 
4
5
  /**
5
6
  * Creates a Spotify OAuth Provider integration.
@@ -30,7 +31,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
30
31
  });
31
32
 
32
33
  if (!tokenResponse.ok) {
33
- console.error("Failed to get Spotify access token:", await tokenResponse.text());
34
+ logger.error("Failed to get Spotify access token", { detail: await tokenResponse.text() });
34
35
  return null;
35
36
  }
36
37
 
@@ -41,7 +42,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
41
42
  });
42
43
 
43
44
  if (!profileResponse.ok) {
44
- console.error("Failed to get Spotify user info:", await profileResponse.text());
45
+ logger.error("Failed to get Spotify user info", { detail: await profileResponse.text() });
45
46
  return null;
46
47
  }
47
48
 
@@ -50,7 +51,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
50
51
  email?: string; images?: Array<{ url: string }>;
51
52
  };
52
53
 
53
- if (!p.email) { console.error("Spotify user has no email"); return null; }
54
+ if (!p.email) { logger.error("Spotify user has no email"); return null; }
54
55
 
55
56
  return {
56
57
  providerId: p.id,
@@ -59,7 +60,7 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
59
60
  photoUrl: p.images?.[0]?.url || null
60
61
  };
61
62
  } catch (error) {
62
- console.error("Spotify OAuth error:", error);
63
+ logger.error("Spotify OAuth error", { error: error });
63
64
  return null;
64
65
  }
65
66
  }
@@ -1,5 +1,6 @@
1
1
  import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
2
2
  import { z } from "zod";
3
+ import { logger } from "../utils/logger";
3
4
 
4
5
  /**
5
6
  * Creates a Twitter/X OAuth 2.0 Provider integration.
@@ -47,7 +48,7 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
47
48
  });
48
49
 
49
50
  if (!tokenResponse.ok) {
50
- console.error("Failed to get Twitter access token:", await tokenResponse.text());
51
+ logger.error("Failed to get Twitter access token", { detail: await tokenResponse.text() });
51
52
  return null;
52
53
  }
53
54
 
@@ -63,7 +64,7 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
63
64
  );
64
65
 
65
66
  if (!profileResponse.ok) {
66
- console.error("Failed to get Twitter user info:", await profileResponse.text());
67
+ logger.error("Failed to get Twitter user info", { detail: await profileResponse.text() });
67
68
  return null;
68
69
  }
69
70
 
@@ -112,7 +113,7 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
112
113
  photoUrl: profileData.profile_image_url?.replace("_normal", "_400x400") || null
113
114
  };
114
115
  } catch (error) {
115
- console.error("Twitter OAuth error:", error);
116
+ logger.error("Twitter OAuth error", { error: error });
116
117
  return null;
117
118
  }
118
119
  }
@@ -2,6 +2,7 @@ import { EntityCollection } from "@rebasepro/types";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { pathToFileURL } from "url";
5
+ import { logger } from "../utils/logger";
5
6
 
6
7
  /**
7
8
  * Asynchronously load collection files from a directory for backend initialization
@@ -10,7 +11,7 @@ export async function loadCollectionsFromDirectory(directory: string): Promise<E
10
11
  const collections: EntityCollection[] = [];
11
12
  try {
12
13
  if (!fs.existsSync(directory)) {
13
- console.warn(`[loadCollectionsFromDirectory] Collections directory not found: ${directory}`);
14
+ logger.warn(`[loadCollectionsFromDirectory] Collections directory not found: ${directory}`);
14
15
  return collections;
15
16
  }
16
17
 
@@ -34,16 +35,16 @@ export async function loadCollectionsFromDirectory(directory: string): Promise<E
34
35
  if (module && module.default) {
35
36
  collections.push(module.default);
36
37
  } else {
37
- console.warn(`[loadCollectionsFromDirectory] File ${file} does not have a default export. Skipping.`);
38
+ logger.warn(`[loadCollectionsFromDirectory] File ${file} does not have a default export. Skipping.`);
38
39
  }
39
40
  } catch (err: unknown) {
40
41
  const message = err instanceof Error ? err.message : String(err);
41
- console.error(`[loadCollectionsFromDirectory] Failed to load collection from ${file}: ${message}`);
42
+ logger.error(`[loadCollectionsFromDirectory] Failed to load collection from ${file}: ${message}`);
42
43
  }
43
44
  }
44
45
  }
45
46
  } catch (err) {
46
- console.error(`[loadCollectionsFromDirectory] Error reading collections directory: ${err}`);
47
+ logger.error(`[loadCollectionsFromDirectory] Error reading collections directory: ${err}`);
47
48
  }
48
49
  return collections;
49
50
  }
@@ -44,7 +44,7 @@ describe("CronScheduler", () => {
44
44
  describe("validateCronExpression", () => {
45
45
  it.each([
46
46
  "0 * * * *", "*/5 * * * *", "0 0 1 * *", "30 2 * * 1",
47
- "0 0 * * 0", "0,15,30,45 * * * *", "0 0 1-15 * *",
47
+ "0 0 * * 0", "0,15,30,45 * * * *", "0 0 1-15 * *"
48
48
  ])("accepts valid expression: %s", (expr) => {
49
49
  expect(validateCronExpression(expr)).toEqual({ valid: true });
50
50
  });
@@ -106,7 +106,9 @@ describe("CronScheduler", () => {
106
106
  });
107
107
 
108
108
  it("preserves definition metadata", () => {
109
- scheduler.registerJobs([makeJob("meta", { name: "My Job", description: "Desc", schedule: "30 2 * * 1" })]);
109
+ scheduler.registerJobs([makeJob("meta", { name: "My Job",
110
+ description: "Desc",
111
+ schedule: "30 2 * * 1" })]);
110
112
  const job = scheduler.getJob("meta")!;
111
113
  expect(job.name).toBe("My Job");
112
114
  expect(job.description).toBe("Desc");
@@ -262,7 +264,8 @@ describe("CronScheduler", () => {
262
264
  // Try second trigger while first is running
263
265
  const second = await scheduler.triggerJob("slow");
264
266
 
265
- expect(second!.result).toEqual({ skipped: true, reason: "already_executing" });
267
+ expect(second!.result).toEqual({ skipped: true,
268
+ reason: "already_executing" });
266
269
  expect(second!.logs).toContain("Skipped: job is already running");
267
270
 
268
271
  // Let the first one finish
@@ -480,7 +483,7 @@ describe("CronScheduler", () => {
480
483
  ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
481
484
  insertLog,
482
485
  fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
483
- fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map()),
486
+ fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map())
484
487
  };
485
488
  scheduler.setStore(mockStore);
486
489
  scheduler.registerJobs([makeJob("persisted")]);
@@ -494,7 +497,7 @@ describe("CronScheduler", () => {
494
497
  ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
495
498
  insertLog: jest.fn<() => Promise<void>>().mockRejectedValue(new Error("DB down")),
496
499
  fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
497
- fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map()),
500
+ fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(new Map())
498
501
  };
499
502
  scheduler.setStore(mockStore);
500
503
  scheduler.registerJobs([makeJob("resilient")]);
@@ -505,12 +508,14 @@ describe("CronScheduler", () => {
505
508
 
506
509
  it("seeds counters from store on start", async () => {
507
510
  const stats = new Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>();
508
- stats.set("seeded", { totalRuns: 42, totalFailures: 3, lastRunAt: "2026-01-01T00:00:00Z" });
511
+ stats.set("seeded", { totalRuns: 42,
512
+ totalFailures: 3,
513
+ lastRunAt: "2026-01-01T00:00:00Z" });
509
514
  const mockStore = {
510
515
  ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
511
516
  insertLog: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
512
517
  fetchLogs: jest.fn<() => Promise<CronJobLogEntry[]>>().mockResolvedValue([]),
513
- fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(stats),
518
+ fetchJobStats: jest.fn<() => Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string | Date | null }>>>().mockResolvedValue(stats)
514
519
  };
515
520
  scheduler.setStore(mockStore);
516
521
  scheduler.registerJobs([makeJob("seeded")]);
@@ -527,11 +532,18 @@ describe("CronScheduler", () => {
527
532
 
528
533
  describe("status shape", () => {
529
534
  it("returns all expected fields", () => {
530
- scheduler.registerJobs([makeJob("shape", { name: "Shape Test", description: "Desc", schedule: "15 3 * * *" })]);
535
+ scheduler.registerJobs([makeJob("shape", { name: "Shape Test",
536
+ description: "Desc",
537
+ schedule: "15 3 * * *" })]);
531
538
  expect(scheduler.getJob("shape")).toMatchObject({
532
- id: "shape", name: "Shape Test", description: "Desc",
533
- schedule: "15 3 * * *", enabled: true, state: "idle",
534
- totalRuns: 0, totalFailures: 0
539
+ id: "shape",
540
+ name: "Shape Test",
541
+ description: "Desc",
542
+ schedule: "15 3 * * *",
543
+ enabled: true,
544
+ state: "idle",
545
+ totalRuns: 0,
546
+ totalFailures: 0
535
547
  });
536
548
  });
537
549
 
@@ -5,7 +5,7 @@ import type {
5
5
  CronJobRunState,
6
6
  CronJobContext
7
7
  } from "@rebasepro/types";
8
- import type { RebaseClient } from "@rebasepro/client";
8
+ import type { RebaseClient } from "@rebasepro/types";
9
9
  import type { LoadedCronJob } from "./cron-loader";
10
10
  import type { CronStore } from "./cron-store";
11
11
  import { logger } from "../utils/logger.js";
@@ -62,33 +62,38 @@ function expandCronField(field: string, min: number, max: number): number[] {
62
62
  */
63
63
  export function validateCronExpression(schedule: string): { valid: true } | { valid: false; reason: string } {
64
64
  if (!schedule || typeof schedule !== "string") {
65
- return { valid: false, reason: "Schedule must be a non-empty string" };
65
+ return { valid: false,
66
+ reason: "Schedule must be a non-empty string" };
66
67
  }
67
68
  const parts = schedule.trim().split(/\s+/);
68
69
  if (parts.length !== 5) {
69
- return { valid: false, reason: `Expected 5 fields, got ${parts.length}` };
70
+ return { valid: false,
71
+ reason: `Expected 5 fields, got ${parts.length}` };
70
72
  }
71
73
  const fieldRanges: [string, number, number][] = [
72
74
  ["minute", 0, 59],
73
75
  ["hour", 0, 23],
74
76
  ["day of month", 1, 31],
75
77
  ["month", 1, 12],
76
- ["day of week", 0, 6],
78
+ ["day of week", 0, 6]
77
79
  ];
78
80
  for (let i = 0; i < 5; i++) {
79
81
  const [name, min, max] = fieldRanges[i];
80
82
  try {
81
83
  const values = expandCronField(parts[i], min, max);
82
84
  if (values.length === 0) {
83
- return { valid: false, reason: `${name} field "${parts[i]}" produces no values` };
85
+ return { valid: false,
86
+ reason: `${name} field "${parts[i]}" produces no values` };
84
87
  }
85
88
  for (const v of values) {
86
89
  if (v < min || v > max) {
87
- return { valid: false, reason: `${name} field value ${v} out of range [${min}–${max}]` };
90
+ return { valid: false,
91
+ reason: `${name} field value ${v} out of range [${min}–${max}]` };
88
92
  }
89
93
  }
90
94
  } catch (err) {
91
- return { valid: false, reason: `${name} field: ${err instanceof Error ? err.message : String(err)}` };
95
+ return { valid: false,
96
+ reason: `${name} field: ${err instanceof Error ? err.message : String(err)}` };
92
97
  }
93
98
  }
94
99
  return { valid: true };
@@ -362,7 +367,8 @@ export class CronScheduler {
362
367
  finishedAt: new Date().toISOString(),
363
368
  durationMs: 0,
364
369
  success: true,
365
- result: { skipped: true, reason: "already_executing" },
370
+ result: { skipped: true,
371
+ reason: "already_executing" },
366
372
  logs: ["Skipped: job is already running"],
367
373
  manual: true
368
374
  };
@@ -158,7 +158,7 @@ function rowToLogEntry(row: Record<string, unknown>): CronJobLogEntry {
158
158
  success: row.success as boolean,
159
159
  error: (row.error as string) ?? undefined,
160
160
  result: row.result ?? undefined,
161
- logs: Array.isArray(row.logs) ? row.logs : (row.logs ? JSON.parse(row.logs as string) : []),
161
+ logs: Array.isArray(row.logs) ? row.logs : (row.logs ? (() => { try { return JSON.parse(row.logs as string); } catch { return []; } })() : []),
162
162
  manual: row.manual as boolean
163
163
  };
164
164
  }