@rebasepro/server-core 0.2.3 → 0.3.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.
- package/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
- package/dist/index-Cr1D21av.js.map +1 -0
- package/dist/index.es.js +2166 -208
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2155 -193
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/logs-routes.d.ts +37 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
- package/dist/server-core/src/api/types.d.ts +6 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
- package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
- package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
- package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
- package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
- package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
- package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
- package/dist/server-core/src/auth/index.d.ts +5 -3
- package/dist/server-core/src/auth/interfaces.d.ts +93 -3
- package/dist/server-core/src/auth/jwt.d.ts +3 -1
- package/dist/server-core/src/auth/mfa.d.ts +49 -0
- package/dist/server-core/src/auth/middleware.d.ts +7 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
- package/dist/server-core/src/auth/routes.d.ts +3 -3
- package/dist/server-core/src/env.d.ts +6 -0
- package/dist/server-core/src/index.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +3 -3
- package/dist/server-core/src/services/webhook-service.d.ts +29 -0
- package/dist/server-core/src/storage/image-transform.d.ts +48 -0
- package/dist/server-core/src/storage/index.d.ts +3 -0
- package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +8 -6
- package/src/api/ast-schema-editor.ts +4 -4
- package/src/api/errors.ts +16 -7
- package/src/api/logs-routes.ts +129 -0
- package/src/api/rest/api-generator.ts +42 -2
- package/src/api/rest/query-parser.ts +37 -1
- package/src/api/types.ts +6 -1
- package/src/auth/adapter-middleware.ts +20 -4
- package/src/auth/admin-routes.ts +39 -14
- package/src/auth/api-keys/api-key-middleware.ts +126 -0
- package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
- package/src/auth/api-keys/api-key-routes.ts +183 -0
- package/src/auth/api-keys/api-key-store.ts +317 -0
- package/src/auth/api-keys/api-key-types.ts +94 -0
- package/src/auth/api-keys/index.ts +37 -0
- package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
- package/src/auth/builtin-auth-adapter.ts +31 -19
- package/src/auth/index.ts +7 -3
- package/src/auth/interfaces.ts +111 -3
- package/src/auth/jwt.ts +19 -5
- package/src/auth/mfa.ts +160 -0
- package/src/auth/middleware.ts +20 -1
- package/src/auth/rate-limiter.ts +92 -0
- package/src/auth/routes.ts +455 -24
- package/src/cron/cron-loader.ts +5 -10
- package/src/cron/cron-scheduler.ts +11 -12
- package/src/cron/cron-store.ts +8 -7
- package/src/env.ts +2 -0
- package/src/functions/function-loader.ts +6 -9
- package/src/index.ts +1 -2
- package/src/init.ts +37 -7
- package/src/serve-spa.ts +5 -4
- package/src/services/webhook-service.ts +155 -0
- package/src/storage/image-transform.ts +202 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/routes.ts +56 -3
- package/src/storage/tus-handler.ts +315 -0
- package/src/utils/dev-port.ts +14 -0
- package/src/utils/logging.ts +9 -7
- package/test/admin-routes.test.ts +74 -7
- package/test/api-generator.test.ts +0 -1
- package/test/api-key-permission-guard.test.ts +132 -0
- package/test/ast-schema-editor.test.ts +26 -0
- package/test/auth-routes.test.ts +1 -2
- package/test/backend-hooks-admin.test.ts +3 -4
- package/test/email-templates.test.ts +169 -0
- package/test/function-loader.test.ts +124 -0
- package/test/jwt.test.ts +4 -2
- package/test/mfa.test.ts +197 -0
- package/test/middleware.test.ts +10 -5
- package/test/webhook-service.test.ts +249 -0
- package/vite.config.ts +3 -2
- package/dist/index-BZoAtuqi.js.map +0 -1
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/src/bootstrappers/index.ts +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
546
|
+
logger.error(`[cron] Failed to persist log for "${job.id}"`, { error: persistErr });
|
|
548
547
|
});
|
|
549
548
|
}
|
|
550
549
|
|
|
551
550
|
if (success) {
|
|
552
|
-
|
|
551
|
+
logger.info(`✅ [cron] "${job.id}" completed in ${durationMs}ms`);
|
|
553
552
|
} else {
|
|
554
|
-
|
|
553
|
+
logger.error(`❌ [cron] "${job.id}" failed in ${durationMs}ms: ${error}`);
|
|
555
554
|
}
|
|
556
555
|
|
|
557
556
|
return logEntry;
|
package/src/cron/cron-store.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
72
|
+
logger.info("✅ Cron logs table ready");
|
|
72
73
|
} catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/src/init.ts
CHANGED
|
@@ -18,12 +18,16 @@ 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";
|
|
22
26
|
import { createHistoryRoutes } from "./history";
|
|
23
27
|
import { EmailConfig, createEmailService } from "./email";
|
|
24
28
|
import type { EmailService } from "./email";
|
|
25
29
|
import type { OAuthProvider } from "./auth/interfaces";
|
|
26
|
-
import type {
|
|
30
|
+
import type { AuthHooks } from "./auth/auth-hooks";
|
|
27
31
|
import { _initRebase } from "./singleton";
|
|
28
32
|
|
|
29
33
|
export interface RebaseAuthConfig {
|
|
@@ -72,13 +76,13 @@ export interface RebaseAuthConfig {
|
|
|
72
76
|
* ```ts
|
|
73
77
|
* import bcrypt from "bcrypt";
|
|
74
78
|
*
|
|
75
|
-
*
|
|
79
|
+
* hooks: {
|
|
76
80
|
* hashPassword: (pw) => bcrypt.hash(pw, 12),
|
|
77
81
|
* verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
|
|
78
82
|
* }
|
|
79
83
|
* ```
|
|
80
84
|
*/
|
|
81
|
-
|
|
85
|
+
hooks?: AuthHooks;
|
|
82
86
|
[key: string]: unknown;
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -417,7 +421,7 @@ collectionRegistry });
|
|
|
417
421
|
oauthProviders,
|
|
418
422
|
serviceKey,
|
|
419
423
|
hooks: config.hooks,
|
|
420
|
-
|
|
424
|
+
authHooks: safeAuthConfig.hooks,
|
|
421
425
|
});
|
|
422
426
|
}
|
|
423
427
|
|
|
@@ -579,7 +583,7 @@ collectionRegistry });
|
|
|
579
583
|
oauthProviders,
|
|
580
584
|
serviceKey,
|
|
581
585
|
hooks: config.hooks,
|
|
582
|
-
|
|
586
|
+
authHooks: safeAuthConfig.hooks,
|
|
583
587
|
});
|
|
584
588
|
}
|
|
585
589
|
|
|
@@ -601,6 +605,23 @@ collectionRegistry });
|
|
|
601
605
|
}
|
|
602
606
|
}
|
|
603
607
|
|
|
608
|
+
// ─── API Key Store Bootstrap ──────────────────────────────────────────
|
|
609
|
+
let apiKeyStore: ApiKeyStore | undefined;
|
|
610
|
+
const apiKeyStoreResult = createApiKeyStore(defaultDriver);
|
|
611
|
+
if (apiKeyStoreResult) {
|
|
612
|
+
apiKeyStore = apiKeyStoreResult;
|
|
613
|
+
await apiKeyStore.ensureTable();
|
|
614
|
+
logger.info("Service API Keys initialized");
|
|
615
|
+
|
|
616
|
+
// Mount API key admin routes
|
|
617
|
+
const apiKeyRoutes = createApiKeyRoutes({
|
|
618
|
+
store: apiKeyStore,
|
|
619
|
+
serviceKey,
|
|
620
|
+
});
|
|
621
|
+
config.app.route(`${basePath}/admin/api-keys`, apiKeyRoutes);
|
|
622
|
+
logger.info("API key admin routes mounted", { path: `${basePath}/admin/api-keys` });
|
|
623
|
+
}
|
|
624
|
+
|
|
604
625
|
if (config.collectionsDir) {
|
|
605
626
|
if (process.env.NODE_ENV !== "production") {
|
|
606
627
|
const { createSchemaEditorRoutes } = await import("./api/schema-editor-routes");
|
|
@@ -676,15 +697,22 @@ collectionRegistry });
|
|
|
676
697
|
adapter: authAdapter,
|
|
677
698
|
driver: defaultDriver,
|
|
678
699
|
requireAuth: dataRequireAuth,
|
|
700
|
+
apiKeyStore,
|
|
679
701
|
}));
|
|
680
702
|
} else {
|
|
681
703
|
dataRouter.use("/*", createAuthMiddleware({
|
|
682
704
|
driver: defaultDriver,
|
|
683
705
|
requireAuth: dataRequireAuth,
|
|
684
|
-
serviceKey
|
|
706
|
+
serviceKey,
|
|
707
|
+
apiKeyStore,
|
|
685
708
|
}));
|
|
686
709
|
}
|
|
687
710
|
|
|
711
|
+
// Per-API-key rate limiting (no-op for non-API-key requests)
|
|
712
|
+
if (apiKeyStore) {
|
|
713
|
+
dataRouter.use("/*", createApiKeyRateLimiter());
|
|
714
|
+
}
|
|
715
|
+
|
|
688
716
|
// Mount history routes BEFORE the REST API subcollection catch-all so
|
|
689
717
|
// that /:slug/:entityId/history is matched by the dedicated handler first.
|
|
690
718
|
if (historyConfigResult && historyConfigResult.historyService) {
|
|
@@ -800,12 +828,14 @@ collectionRegistry });
|
|
|
800
828
|
adapter: authAdapter,
|
|
801
829
|
driver: defaultDriver,
|
|
802
830
|
requireAuth: functionsRequireAuth,
|
|
831
|
+
apiKeyStore,
|
|
803
832
|
}));
|
|
804
833
|
} else {
|
|
805
834
|
functionsRouter.use("/*", createAuthMiddleware({
|
|
806
835
|
driver: defaultDriver,
|
|
807
836
|
requireAuth: functionsRequireAuth,
|
|
808
|
-
serviceKey
|
|
837
|
+
serviceKey,
|
|
838
|
+
apiKeyStore,
|
|
809
839
|
}));
|
|
810
840
|
}
|
|
811
841
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|