@rebasepro/server-core 0.2.3 → 0.2.5

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 (116) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +9 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
  5. package/dist/index-Cr1D21av.js.map +1 -0
  6. package/dist/index.es.js +2309 -370
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +2298 -355
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-core/src/api/logs-routes.d.ts +37 -0
  11. package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
  12. package/dist/server-core/src/api/types.d.ts +6 -1
  13. package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
  14. package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
  15. package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
  16. package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
  17. package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
  18. package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
  19. package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
  20. package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
  21. package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
  22. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
  23. package/dist/server-core/src/auth/index.d.ts +5 -3
  24. package/dist/server-core/src/auth/interfaces.d.ts +93 -3
  25. package/dist/server-core/src/auth/jwt.d.ts +3 -1
  26. package/dist/server-core/src/auth/mfa.d.ts +49 -0
  27. package/dist/server-core/src/auth/middleware.d.ts +7 -0
  28. package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
  29. package/dist/server-core/src/auth/routes.d.ts +3 -3
  30. package/dist/server-core/src/env.d.ts +6 -0
  31. package/dist/server-core/src/index.d.ts +1 -0
  32. package/dist/server-core/src/init.d.ts +3 -3
  33. package/dist/server-core/src/services/webhook-service.d.ts +29 -0
  34. package/dist/server-core/src/storage/image-transform.d.ts +48 -0
  35. package/dist/server-core/src/storage/index.d.ts +3 -0
  36. package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
  37. package/dist/types/src/controllers/auth.d.ts +4 -26
  38. package/dist/types/src/controllers/client.d.ts +25 -43
  39. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  40. package/dist/types/src/controllers/data.d.ts +4 -0
  41. package/dist/types/src/controllers/data_driver.d.ts +23 -0
  42. package/dist/types/src/controllers/registry.d.ts +5 -4
  43. package/dist/types/src/rebase_context.d.ts +1 -1
  44. package/dist/types/src/types/auth_adapter.d.ts +5 -60
  45. package/dist/types/src/types/backend.d.ts +2 -2
  46. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  47. package/dist/types/src/types/collections.d.ts +0 -4
  48. package/dist/types/src/types/component_ref.d.ts +1 -1
  49. package/dist/types/src/types/cron.d.ts +1 -1
  50. package/dist/types/src/types/entity_views.d.ts +1 -0
  51. package/dist/types/src/types/export_import.d.ts +1 -1
  52. package/dist/types/src/types/formex.d.ts +2 -2
  53. package/dist/types/src/types/properties.d.ts +9 -7
  54. package/dist/types/src/types/translations.d.ts +28 -12
  55. package/dist/types/src/types/user_management_delegate.d.ts +22 -57
  56. package/dist/types/src/users/index.d.ts +0 -1
  57. package/dist/types/src/users/user.d.ts +0 -1
  58. package/package.json +8 -6
  59. package/src/api/ast-schema-editor.ts +4 -4
  60. package/src/api/errors.ts +16 -7
  61. package/src/api/logs-routes.ts +129 -0
  62. package/src/api/rest/api-generator.ts +42 -2
  63. package/src/api/rest/query-parser.ts +37 -1
  64. package/src/api/types.ts +6 -1
  65. package/src/auth/adapter-middleware.ts +20 -4
  66. package/src/auth/admin-routes.ts +36 -100
  67. package/src/auth/api-keys/api-key-middleware.ts +126 -0
  68. package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
  69. package/src/auth/api-keys/api-key-routes.ts +183 -0
  70. package/src/auth/api-keys/api-key-store.ts +317 -0
  71. package/src/auth/api-keys/api-key-types.ts +94 -0
  72. package/src/auth/api-keys/index.ts +37 -0
  73. package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
  74. package/src/auth/builtin-auth-adapter.ts +35 -73
  75. package/src/auth/custom-auth-adapter.ts +1 -1
  76. package/src/auth/index.ts +7 -3
  77. package/src/auth/interfaces.ts +111 -3
  78. package/src/auth/jwt.ts +19 -5
  79. package/src/auth/mfa.ts +160 -0
  80. package/src/auth/middleware.ts +20 -1
  81. package/src/auth/rate-limiter.ts +92 -0
  82. package/src/auth/routes.ts +455 -24
  83. package/src/cron/cron-loader.ts +5 -10
  84. package/src/cron/cron-scheduler.ts +11 -12
  85. package/src/cron/cron-store.ts +8 -7
  86. package/src/env.ts +2 -0
  87. package/src/functions/function-loader.ts +6 -9
  88. package/src/index.ts +1 -2
  89. package/src/init.ts +56 -8
  90. package/src/serve-spa.ts +5 -4
  91. package/src/services/webhook-service.ts +155 -0
  92. package/src/storage/image-transform.ts +202 -0
  93. package/src/storage/index.ts +3 -0
  94. package/src/storage/routes.ts +56 -3
  95. package/src/storage/tus-handler.ts +315 -0
  96. package/src/utils/dev-port.ts +14 -0
  97. package/src/utils/logging.ts +9 -7
  98. package/test/admin-routes.test.ts +62 -164
  99. package/test/api-generator.test.ts +0 -1
  100. package/test/api-key-permission-guard.test.ts +132 -0
  101. package/test/ast-schema-editor.test.ts +26 -0
  102. package/test/auth-routes.test.ts +1 -2
  103. package/test/backend-hooks-admin.test.ts +3 -29
  104. package/test/custom-auth-adapter.test.ts +2 -10
  105. package/test/email-templates.test.ts +169 -0
  106. package/test/function-loader.test.ts +124 -0
  107. package/test/jwt.test.ts +4 -2
  108. package/test/mfa.test.ts +197 -0
  109. package/test/middleware.test.ts +10 -5
  110. package/test/webhook-service.test.ts +249 -0
  111. package/vite.config.ts +3 -2
  112. package/dist/index-BZoAtuqi.js.map +0 -1
  113. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  114. package/dist/types/src/users/roles.d.ts +0 -22
  115. package/src/bootstrappers/index.ts +0 -1
  116. package/src/singleton.test.ts +0 -28
@@ -8,6 +8,7 @@ import type {
8
8
  import type { RebaseClient } from "@rebasepro/client";
9
9
  import type { LoadedCronJob } from "./cron-loader";
10
10
  import type { CronStore } from "./cron-store";
11
+ import { logger } from "../utils/logger.js";
11
12
 
12
13
  // ─── Cron expression parser (minimal, no external dependency) ────────
13
14
  // Supports standard 5-field cron (minute hour dom month dow).
@@ -207,15 +208,13 @@ export class CronScheduler {
207
208
  // Validate schedule up-front — reject invalid schedules
208
209
  const validation = validateCronExpression(loaded.definition.schedule);
209
210
  if (!validation.valid) {
210
- console.error(
211
- `[cron] Rejecting job "${loaded.id}": invalid schedule "${loaded.definition.schedule}" — ${validation.reason}`
212
- );
211
+ logger.error(`[cron] Rejecting job "${loaded.id}": invalid schedule "${loaded.definition.schedule}" — ${validation.reason}`);
213
212
  continue;
214
213
  }
215
214
 
216
215
  const existing = this.jobs.get(loaded.id);
217
216
  if (existing) {
218
- console.warn(`[cron] Duplicate cron job id: "${loaded.id}". Overwriting.`);
217
+ logger.warn(`[cron] Duplicate cron job id: "${loaded.id}". Overwriting.`);
219
218
  this.stopJob(loaded.id);
220
219
  }
221
220
 
@@ -260,7 +259,7 @@ export class CronScheduler {
260
259
  }
261
260
  }
262
261
  }).catch((err) => {
263
- console.warn("[cron] Failed to seed job stats from database:", err);
262
+ logger.warn("[cron] Failed to seed job stats from database", { error: err });
264
263
  });
265
264
  }
266
265
 
@@ -269,7 +268,7 @@ export class CronScheduler {
269
268
  this.scheduleNext(id);
270
269
  }
271
270
  }
272
- console.log(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);
271
+ logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);
273
272
  }
274
273
 
275
274
  /**
@@ -356,7 +355,7 @@ export class CronScheduler {
356
355
 
357
356
  // Concurrency guard — don't run two instances simultaneously
358
357
  if (job.executing) {
359
- console.warn(`[cron] Skipping manual trigger of "${id}" — already executing`);
358
+ logger.warn(`[cron] Skipping manual trigger of "${id}" — already executing`);
360
359
  const logEntry: CronJobLogEntry = {
361
360
  jobId: id,
362
361
  startedAt: new Date().toISOString(),
@@ -411,7 +410,7 @@ export class CronScheduler {
411
410
 
412
411
  // Concurrency guard: if somehow we're already executing, skip
413
412
  if (job.executing) {
414
- console.warn(`[cron] Skipping scheduled run of "${id}" — still executing from previous run`);
413
+ logger.warn(`[cron] Skipping scheduled run of "${id}" — still executing from previous run`);
415
414
  // Re-schedule to try again later
416
415
  this.scheduleNext(id);
417
416
  return;
@@ -433,7 +432,7 @@ export class CronScheduler {
433
432
 
434
433
  job.timerId = timer;
435
434
  } catch (err: unknown) {
436
- console.error(`[cron] Failed to schedule "${id}":`, err);
435
+ logger.error(`[cron] Failed to schedule "${id}"`, { error: err });
437
436
  job.state = "error";
438
437
  job.lastError = err instanceof Error ? err.message : String(err);
439
438
  }
@@ -544,14 +543,14 @@ export class CronScheduler {
544
543
  // Persist to database (non-blocking)
545
544
  if (this.store) {
546
545
  this.store.insertLog(logEntry).catch((persistErr) => {
547
- console.error(`[cron] Failed to persist log for "${job.id}":`, persistErr);
546
+ logger.error(`[cron] Failed to persist log for "${job.id}"`, { error: persistErr });
548
547
  });
549
548
  }
550
549
 
551
550
  if (success) {
552
- console.log(`✅ [cron] "${job.id}" completed in ${durationMs}ms`);
551
+ logger.info(`✅ [cron] "${job.id}" completed in ${durationMs}ms`);
553
552
  } else {
554
- console.error(`❌ [cron] "${job.id}" failed in ${durationMs}ms: ${error}`);
553
+ logger.error(`❌ [cron] "${job.id}" failed in ${durationMs}ms: ${error}`);
555
554
  }
556
555
 
557
556
  return logEntry;
@@ -1,6 +1,7 @@
1
1
  import type { CronJobLogEntry } from "@rebasepro/types";
2
2
  import type { DataDriver } from "@rebasepro/types";
3
3
  import { isSQLAdmin } from "@rebasepro/types";
4
+ import { logger } from "../utils/logger.js";
4
5
 
5
6
  /**
6
7
  * Persistence layer for cron job execution logs.
@@ -38,7 +39,7 @@ const TABLE = "rebase.cron_logs";
38
39
  export function createCronStore(driver: DataDriver): CronStore | undefined {
39
40
  const admin = driver.admin;
40
41
  if (!isSQLAdmin(admin)) {
41
- console.warn("⚠️ [cron-store] DataDriver does not support SQL admin — cron logs will not be persisted.");
42
+ logger.warn("⚠️ [cron-store] DataDriver does not support SQL admin — cron logs will not be persisted.");
42
43
  return undefined;
43
44
  }
44
45
 
@@ -68,10 +69,10 @@ export function createCronStore(driver: DataDriver): CronStore | undefined {
68
69
  ON ${TABLE}(job_id, started_at DESC)
69
70
  `);
70
71
 
71
- console.log("✅ Cron logs table ready");
72
+ logger.info("✅ Cron logs table ready");
72
73
  } catch (err) {
73
- console.error("❌ Failed to create cron logs table:", err);
74
- console.warn("⚠️ Continuing without cron log persistence.");
74
+ logger.error("❌ Failed to create cron logs table", { error: err });
75
+ logger.warn("⚠️ Continuing without cron log persistence.");
75
76
  }
76
77
  },
77
78
 
@@ -97,7 +98,7 @@ export function createCronStore(driver: DataDriver): CronStore | undefined {
97
98
  `);
98
99
  } catch (err) {
99
100
  // Non-blocking — log persistence should never crash the scheduler
100
- console.error(`[cron-store] Failed to persist log for "${entry.jobId}":`, err);
101
+ logger.error(`[cron-store] Failed to persist log for "${entry.jobId}"`, { error: err });
101
102
  }
102
103
  },
103
104
 
@@ -113,7 +114,7 @@ export function createCronStore(driver: DataDriver): CronStore | undefined {
113
114
 
114
115
  return rows.map(rowToLogEntry);
115
116
  } catch (err) {
116
- console.error(`[cron-store] Failed to fetch logs for "${jobId}":`, err);
117
+ logger.error(`[cron-store] Failed to fetch logs for "${jobId}"`, { error: err });
117
118
  return [];
118
119
  }
119
120
  },
@@ -139,7 +140,7 @@ export function createCronStore(driver: DataDriver): CronStore | undefined {
139
140
  });
140
141
  }
141
142
  } catch (err) {
142
- console.error("[cron-store] Failed to fetch job stats:", err);
143
+ logger.error("[cron-store] Failed to fetch job stats", { error: err });
143
144
  }
144
145
  return stats;
145
146
  }
package/src/env.ts CHANGED
@@ -102,6 +102,8 @@ const rebaseEnvSchema = z.object({
102
102
  DB_POOL_MAX: z.string().default("20").transform(Number),
103
103
  DB_POOL_IDLE_TIMEOUT: z.string().default("30000").transform(Number),
104
104
  DB_POOL_CONNECT_TIMEOUT: z.string().default("10000").transform(Number),
105
+ DATABASE_DIRECT_URL: z.string().url().optional(),
106
+ DATABASE_READ_URL: z.string().url().optional(),
105
107
  FORCE_LOCAL_STORAGE: optionalBoolString,
106
108
  STORAGE_TYPE: z.enum(["local", "s3"]).default("local"),
107
109
  STORAGE_PATH: z.string().optional(),
@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { pathToFileURL } from "url";
4
4
  import { Hono } from "hono";
5
+ import { logger } from "../utils/logger.js";
5
6
 
6
7
  export interface LoadedFunction {
7
8
  /** Endpoint name derived from filename (e.g., "send-invoice") */
@@ -49,9 +50,7 @@ export async function loadFunctionsFromDirectory(
49
50
  const exported = mod.default;
50
51
 
51
52
  if (!exported) {
52
- console.warn(
53
- `[functions] ${file}: no default export. Skipping.`
54
- );
53
+ logger.warn(`[functions] ${file}: no default export. Skipping.`);
55
54
  continue;
56
55
  }
57
56
 
@@ -61,7 +60,7 @@ export async function loadFunctionsFromDirectory(
61
60
  const name = path.basename(file, path.extname(file));
62
61
  functions.push({ name,
63
62
  app: exported as Hono });
64
- console.log(`⚡ Loaded function route: ${name}`);
63
+ logger.info(`⚡ Loaded function route: ${name}`);
65
64
  continue;
66
65
  }
67
66
 
@@ -72,7 +71,7 @@ app: exported as Hono });
72
71
  const name = path.basename(file, path.extname(file));
73
72
  functions.push({ name,
74
73
  app: result as Hono });
75
- console.log(`⚡ Loaded function route: ${name}`);
74
+ logger.info(`⚡ Loaded function route: ${name}`);
76
75
  continue;
77
76
  }
78
77
  }
@@ -82,7 +81,7 @@ app: result as Hono });
82
81
  const keys = exported && typeof exported === "object"
83
82
  ? Object.getOwnPropertyNames(Object.getPrototypeOf(exported)).slice(0, 10).join(", ")
84
83
  : "N/A";
85
- console.warn(
84
+ logger.warn(
86
85
  `[functions] ${file}: default export is not a Hono app or factory. Skipping.\n` +
87
86
  ` export type: ${exportType}${exported?.constructor?.name ? ` (${exported.constructor.name})` : ""}\n` +
88
87
  ` prototype methods: ${keys}\n` +
@@ -92,9 +91,7 @@ app: result as Hono });
92
91
  } catch (err: unknown) {
93
92
  const message =
94
93
  err instanceof Error ? err.message : String(err);
95
- console.error(
96
- `[functions] Failed to load ${file}: ${message}`
97
- );
94
+ logger.error(`[functions] Failed to load ${file}: ${message}`);
98
95
  }
99
96
  }
100
97
  }
package/src/index.ts CHANGED
@@ -65,6 +65,5 @@ export * from "./utils/dev-port";
65
65
  // Environment validation
66
66
  export { loadEnv } from "./env";
67
67
  export type { RebaseEnv } from "./env";
68
-
69
- // Backend bootstrappers (pluggable driver initialization)
68
+ export { z } from "zod";
70
69
 
package/src/init.ts CHANGED
@@ -18,12 +18,17 @@ import { logger } from "./utils/logger";
18
18
  import { requestLogger } from "./utils/request-logger";
19
19
  import { createAdminRoutes, createAuthRoutes, requireAuth, requireAdmin, configureJwt } from "./auth";
20
20
  import { createStorageController, createStorageRoutes, DEFAULT_STORAGE_ID, DefaultStorageRegistry, BackendStorageConfig, StorageController, StorageRegistry } from "./storage";
21
+ import { createApiKeyStore } from "./auth/api-keys/api-key-store";
22
+ import { createApiKeyRoutes } from "./auth/api-keys/api-key-routes";
23
+ import type { ApiKeyStore } from "./auth/api-keys/api-key-store";
24
+ import { createApiKeyRateLimiter } from "./auth/rate-limiter";
21
25
  import { createRebaseClient } from "@rebasepro/client";
26
+ import { defaultUsersCollection } from "@rebasepro/common";
22
27
  import { createHistoryRoutes } from "./history";
23
28
  import { EmailConfig, createEmailService } from "./email";
24
29
  import type { EmailService } from "./email";
25
30
  import type { OAuthProvider } from "./auth/interfaces";
26
- import type { AuthOverrides } from "./auth/auth-overrides";
31
+ import type { AuthHooks } from "./auth/auth-hooks";
27
32
  import { _initRebase } from "./singleton";
28
33
 
29
34
  export interface RebaseAuthConfig {
@@ -72,13 +77,13 @@ export interface RebaseAuthConfig {
72
77
  * ```ts
73
78
  * import bcrypt from "bcrypt";
74
79
  *
75
- * overrides: {
80
+ * hooks: {
76
81
  * hashPassword: (pw) => bcrypt.hash(pw, 12),
77
82
  * verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
78
83
  * }
79
84
  * ```
80
85
  */
81
- overrides?: AuthOverrides;
86
+ hooks?: AuthHooks;
82
87
  [key: string]: unknown;
83
88
  }
84
89
 
@@ -288,6 +293,13 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
288
293
  dir: config.collectionsDir });
289
294
  }
290
295
 
296
+ if (config.auth) {
297
+ // Prepend defaultUsersCollection if not overridden by the developer's collections
298
+ activeCollections = Array.from(
299
+ new Map([defaultUsersCollection, ...activeCollections].map(c => [c.slug, c])).values()
300
+ );
301
+ }
302
+
291
303
  const realtimeServices: Record<string, RealtimeProvider> = {};
292
304
  const delegates: Record<string, DataDriver> = {};
293
305
 
@@ -374,7 +386,6 @@ collectionRegistry });
374
386
  // (the return type still exposes `auth?: BootstrappedAuth`)
375
387
  authConfigResult = {
376
388
  userService: authAdapter.userManagement ?? {},
377
- roleService: authAdapter.roleManagement ?? {},
378
389
  };
379
390
  } else {
380
391
  // ── RebaseAuthConfig — wrap in built-in adapter ──
@@ -417,7 +428,7 @@ collectionRegistry });
417
428
  oauthProviders,
418
429
  serviceKey,
419
430
  hooks: config.hooks,
420
- overrides: safeAuthConfig.overrides,
431
+ authHooks: safeAuthConfig.hooks,
421
432
  });
422
433
  }
423
434
 
@@ -579,7 +590,7 @@ collectionRegistry });
579
590
  oauthProviders,
580
591
  serviceKey,
581
592
  hooks: config.hooks,
582
- overrides: safeAuthConfig.overrides,
593
+ authHooks: safeAuthConfig.hooks,
583
594
  });
584
595
  }
585
596
 
@@ -601,6 +612,23 @@ collectionRegistry });
601
612
  }
602
613
  }
603
614
 
615
+ // ─── API Key Store Bootstrap ──────────────────────────────────────────
616
+ let apiKeyStore: ApiKeyStore | undefined;
617
+ const apiKeyStoreResult = createApiKeyStore(defaultDriver);
618
+ if (apiKeyStoreResult) {
619
+ apiKeyStore = apiKeyStoreResult;
620
+ await apiKeyStore.ensureTable();
621
+ logger.info("Service API Keys initialized");
622
+
623
+ // Mount API key admin routes
624
+ const apiKeyRoutes = createApiKeyRoutes({
625
+ store: apiKeyStore,
626
+ serviceKey,
627
+ });
628
+ config.app.route(`${basePath}/admin/api-keys`, apiKeyRoutes);
629
+ logger.info("API key admin routes mounted", { path: `${basePath}/admin/api-keys` });
630
+ }
631
+
604
632
  if (config.collectionsDir) {
605
633
  if (process.env.NODE_ENV !== "production") {
606
634
  const { createSchemaEditorRoutes } = await import("./api/schema-editor-routes");
@@ -676,15 +704,22 @@ collectionRegistry });
676
704
  adapter: authAdapter,
677
705
  driver: defaultDriver,
678
706
  requireAuth: dataRequireAuth,
707
+ apiKeyStore,
679
708
  }));
680
709
  } else {
681
710
  dataRouter.use("/*", createAuthMiddleware({
682
711
  driver: defaultDriver,
683
712
  requireAuth: dataRequireAuth,
684
- serviceKey
713
+ serviceKey,
714
+ apiKeyStore,
685
715
  }));
686
716
  }
687
717
 
718
+ // Per-API-key rate limiting (no-op for non-API-key requests)
719
+ if (apiKeyStore) {
720
+ dataRouter.use("/*", createApiKeyRateLimiter());
721
+ }
722
+
688
723
  // Mount history routes BEFORE the REST API subcollection catch-all so
689
724
  // that /:slug/:entityId/history is matched by the dedicated handler first.
690
725
  if (historyConfigResult && historyConfigResult.historyService) {
@@ -764,6 +799,17 @@ collectionRegistry });
764
799
  logger.info("Email service attached to singleton", { configured: emailService.isConfigured() });
765
800
  }
766
801
 
802
+ // Attach raw SQL capability when the driver supports it (Postgres, MySQL).
803
+ // Document databases (MongoDB, Firestore) won't have this.
804
+ const driverAdmin = defaultBootstrapper.getAdmin?.(defaultDriverResult);
805
+ if (isSQLAdmin(driverAdmin)) {
806
+ Object.assign(serverClient, {
807
+ sql: (query: string, options?: { database?: string; role?: string }) =>
808
+ driverAdmin.executeSql(query, options)
809
+ });
810
+ logger.info("SQL capability attached to singleton");
811
+ }
812
+
767
813
  _initRebase(serverClient);
768
814
  logger.info("Rebase singleton initialized");
769
815
 
@@ -800,12 +846,14 @@ collectionRegistry });
800
846
  adapter: authAdapter,
801
847
  driver: defaultDriver,
802
848
  requireAuth: functionsRequireAuth,
849
+ apiKeyStore,
803
850
  }));
804
851
  } else {
805
852
  functionsRouter.use("/*", createAuthMiddleware({
806
853
  driver: defaultDriver,
807
854
  requireAuth: functionsRequireAuth,
808
- serviceKey
855
+ serviceKey,
856
+ apiKeyStore,
809
857
  }));
810
858
  }
811
859
 
package/src/serve-spa.ts CHANGED
@@ -2,6 +2,7 @@ import { Hono } from "hono";
2
2
  import { serveStatic } from "@hono/node-server/serve-static";
3
3
  import * as path from "path";
4
4
  import * as fs from "fs";
5
+ import { logger } from "./utils/logger.js";
5
6
 
6
7
  /**
7
8
  * Configuration for serving a Single Page Application
@@ -45,8 +46,8 @@ export function serveSPA<E extends import("hono").Env>(app: Hono<E>, config: Ser
45
46
 
46
47
  // Validate frontend path exists
47
48
  if (!fs.existsSync(frontendPath)) {
48
- console.warn(`⚠️ Frontend build path does not exist: ${frontendPath}`);
49
- console.warn(" SPA serving is disabled. Build your frontend first.");
49
+ logger.warn(`⚠️ Frontend build path does not exist: ${frontendPath}`);
50
+ logger.warn(" SPA serving is disabled. Build your frontend first.");
50
51
  return;
51
52
  }
52
53
 
@@ -68,7 +69,7 @@ export function serveSPA<E extends import("hono").Env>(app: Hono<E>, config: Ser
68
69
  const indexPath = path.join(frontendPath, indexFile);
69
70
 
70
71
  if (!fs.existsSync(indexPath)) {
71
- console.warn(`⚠️ Index file not found: ${indexPath}`);
72
+ logger.warn(`⚠️ Index file not found: ${indexPath}`);
72
73
  return next();
73
74
  }
74
75
 
@@ -76,6 +77,6 @@ export function serveSPA<E extends import("hono").Env>(app: Hono<E>, config: Ser
76
77
  return c.html(html);
77
78
  });
78
79
 
79
- console.log(`✅ SPA serving enabled from: ${frontendPath}`);
80
+ logger.info(`✅ SPA serving enabled from: ${frontendPath}`);
80
81
  }
81
82
 
@@ -0,0 +1,155 @@
1
+ import { createHmac, randomUUID } from "crypto";
2
+
3
+ export interface WebhookConfig {
4
+ id: string;
5
+ url: string;
6
+ secret?: string;
7
+ headers?: Record<string, string>;
8
+ events: string[];
9
+ table: string;
10
+ enabled: boolean;
11
+ }
12
+
13
+ export interface WebhookDeliveryResult {
14
+ webhookId: string;
15
+ event: string;
16
+ payload: Record<string, unknown>;
17
+ statusCode: number;
18
+ responseBody: string;
19
+ success: boolean;
20
+ attemptNumber: number;
21
+ }
22
+
23
+ export class WebhookDispatcher {
24
+ private webhooks: WebhookConfig[] = [];
25
+ private maxRetries = 3;
26
+ private retryDelays = [1000, 5000, 15000]; // Exponential backoff
27
+
28
+ /** Register webhooks to watch */
29
+ setWebhooks(webhooks: WebhookConfig[]): void {
30
+ this.webhooks = webhooks.filter(w => w.enabled);
31
+ }
32
+
33
+ /** Called when an entity changes — checks if any webhook matches */
34
+ async onEntityChange(
35
+ table: string,
36
+ event: "INSERT" | "UPDATE" | "DELETE",
37
+ entityId: string,
38
+ entity: Record<string, unknown> | null,
39
+ previousEntity?: Record<string, unknown> | null
40
+ ): Promise<WebhookDeliveryResult[]> {
41
+ const matchingWebhooks = this.webhooks.filter(
42
+ w => w.table === table && w.events.includes(event)
43
+ );
44
+
45
+ if (matchingWebhooks.length === 0) return [];
46
+
47
+ const results: WebhookDeliveryResult[] = [];
48
+
49
+ for (const webhook of matchingWebhooks) {
50
+ const payload: Record<string, unknown> = {
51
+ type: event,
52
+ table,
53
+ record: entity,
54
+ old_record: event === "UPDATE" ? previousEntity : undefined,
55
+ schema: "public",
56
+ timestamp: new Date().toISOString()
57
+ };
58
+
59
+ const result = await this.deliverWithRetry(webhook, event, payload);
60
+ results.push(result);
61
+ }
62
+
63
+ return results;
64
+ }
65
+
66
+ private async deliverWithRetry(
67
+ webhook: WebhookConfig,
68
+ event: string,
69
+ payload: Record<string, unknown>
70
+ ): Promise<WebhookDeliveryResult> {
71
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
72
+ const result = await this.deliver(webhook, event, payload, attempt);
73
+ if (result.success) return result;
74
+
75
+ if (attempt < this.maxRetries) {
76
+ await new Promise(r => setTimeout(r, this.retryDelays[attempt - 1]));
77
+ } else {
78
+ return result; // Final failure
79
+ }
80
+ }
81
+
82
+ // Should never reach here, but satisfies TypeScript
83
+ return {
84
+ webhookId: webhook.id,
85
+ event,
86
+ payload,
87
+ statusCode: 0,
88
+ responseBody: "Max retries exceeded",
89
+ success: false,
90
+ attemptNumber: this.maxRetries
91
+ };
92
+ }
93
+
94
+ private async deliver(
95
+ webhook: WebhookConfig,
96
+ event: string,
97
+ payload: Record<string, unknown>,
98
+ attemptNumber: number
99
+ ): Promise<WebhookDeliveryResult> {
100
+ const body = JSON.stringify(payload);
101
+
102
+ const headers: Record<string, string> = {
103
+ "Content-Type": "application/json",
104
+ "X-Webhook-Id": webhook.id,
105
+ "X-Webhook-Event": event,
106
+ "X-Webhook-Delivery": randomUUID(),
107
+ "X-Webhook-Attempt": String(attemptNumber),
108
+ ...(webhook.headers || {})
109
+ };
110
+
111
+ // HMAC signature
112
+ if (webhook.secret) {
113
+ const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
114
+ headers["X-Webhook-Signature"] = `sha256=${signature}`;
115
+ }
116
+
117
+ try {
118
+ const controller = new AbortController();
119
+ const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout
120
+
121
+ const response = await fetch(webhook.url, {
122
+ method: "POST",
123
+ headers,
124
+ body,
125
+ signal: controller.signal
126
+ });
127
+
128
+ clearTimeout(timeout);
129
+
130
+ const responseBody = await response.text().catch(() => "");
131
+ const success = response.status >= 200 && response.status < 300;
132
+
133
+ return {
134
+ webhookId: webhook.id,
135
+ event,
136
+ payload,
137
+ statusCode: response.status,
138
+ responseBody: responseBody.slice(0, 1000), // Truncate
139
+ success,
140
+ attemptNumber
141
+ };
142
+ } catch (error: unknown) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ return {
145
+ webhookId: webhook.id,
146
+ event,
147
+ payload,
148
+ statusCode: 0,
149
+ responseBody: message.slice(0, 1000),
150
+ success: false,
151
+ attemptNumber
152
+ };
153
+ }
154
+ }
155
+ }