hookdash 0.1.1 → 0.1.2
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.
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-d-rI7pO3.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-BDjBwO-x.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
16
16
|
</body>
|
|
17
17
|
</html>
|
|
18
|
-
```
|
package/dist/index.js
CHANGED
|
@@ -1171,7 +1171,6 @@ var init_stats = __esm({
|
|
|
1171
1171
|
successfulDeliveries = row.count;
|
|
1172
1172
|
} else if (row.status === "failed" || row.status === "retrying") {
|
|
1173
1173
|
failedDeliveries += row.count;
|
|
1174
|
-
if (row.status === "pending") pendingDeliveries = row.count;
|
|
1175
1174
|
} else if (row.status === "dead") {
|
|
1176
1175
|
deadDeliveries = row.count;
|
|
1177
1176
|
} else if (row.status === "pending") {
|
|
@@ -1404,7 +1403,7 @@ async function createServer(config) {
|
|
|
1404
1403
|
});
|
|
1405
1404
|
await app.register(fastifyRawBody, {
|
|
1406
1405
|
field: "rawBody",
|
|
1407
|
-
global:
|
|
1406
|
+
global: true,
|
|
1408
1407
|
encoding: false,
|
|
1409
1408
|
// Return Buffer
|
|
1410
1409
|
runFirst: true
|
|
@@ -1436,7 +1435,7 @@ async function createServer(config) {
|
|
|
1436
1435
|
await app.register(fastifyStatic, {
|
|
1437
1436
|
root: dashboardPath,
|
|
1438
1437
|
prefix: "/",
|
|
1439
|
-
wildcard:
|
|
1438
|
+
wildcard: true
|
|
1440
1439
|
});
|
|
1441
1440
|
app.setNotFoundHandler((request, reply) => {
|
|
1442
1441
|
if (request.url.startsWith("/api/") || request.url.startsWith("/webhook/")) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db/schema.ts","../src/db/connection.ts","../src/providers/stripe.ts","../src/providers/github.ts","../src/providers/twilio.ts","../src/providers/shopify.ts","../src/providers/generic.ts","../src/providers/registry.ts","../src/ingestion/parser.ts","../src/delivery/circuit-breaker.ts","../src/delivery/retry.ts","../src/delivery/worker.ts","../src/api/sse.ts","../src/ingestion/routes.ts","../src/api/events.ts","../src/api/endpoints.ts","../src/api/stats.ts","../src/api/routes.ts","../src/index.ts","../src/config/loader.ts","../src/config/schema.ts","../src/server.ts"],"sourcesContent":["/**\n * SQLite schema for hookdash.\n * All tables are created idempotently (IF NOT EXISTS).\n */\nexport const SCHEMA_SQL = `\n-- Webhook sources / providers\nCREATE TABLE IF NOT EXISTS sources (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n provider TEXT NOT NULL DEFAULT 'generic',\n signing_secret TEXT,\n active INTEGER DEFAULT 1,\n created_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Forwarding endpoints\nCREATE TABLE IF NOT EXISTS endpoints (\n id TEXT PRIMARY KEY,\n url TEXT NOT NULL,\n source_id TEXT REFERENCES sources(id) ON DELETE SET NULL,\n event_filter TEXT,\n headers TEXT,\n timeout_ms INTEGER DEFAULT 30000,\n max_retries INTEGER DEFAULT 8,\n active INTEGER DEFAULT 1,\n circuit_state TEXT DEFAULT 'closed',\n circuit_failure_count INTEGER DEFAULT 0,\n circuit_last_failure_at TEXT,\n created_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Incoming webhook events\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n source_id TEXT NOT NULL REFERENCES sources(id),\n event_type TEXT,\n headers TEXT NOT NULL,\n body TEXT NOT NULL,\n content_type TEXT,\n signature_valid INTEGER,\n received_at TEXT DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_received ON events(received_at);\nCREATE INDEX IF NOT EXISTS idx_events_source ON events(source_id);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);\n\n-- Delivery attempts\nCREATE TABLE IF NOT EXISTS deliveries (\n id TEXT PRIMARY KEY,\n event_id TEXT NOT NULL REFERENCES events(id) ON DELETE CASCADE,\n endpoint_id TEXT NOT NULL REFERENCES endpoints(id) ON DELETE CASCADE,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n next_retry_at TEXT,\n last_status_code INTEGER,\n last_response_body TEXT,\n last_error TEXT,\n created_at TEXT DEFAULT (datetime('now')),\n completed_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_deliveries_status ON deliveries(status);\nCREATE INDEX IF NOT EXISTS idx_deliveries_next_retry ON deliveries(next_retry_at);\nCREATE INDEX IF NOT EXISTS idx_deliveries_event ON deliveries(event_id);\n\n-- Schema version tracking\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at TEXT DEFAULT (datetime('now'))\n);\n`;\n\nexport const CURRENT_SCHEMA_VERSION = 1;\n","import Database from 'better-sqlite3';\nimport { resolve } from 'node:path';\nimport { mkdirSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { SCHEMA_SQL, CURRENT_SCHEMA_VERSION } from './schema.js';\nimport type { HookdashConfig } from '../types/index.js';\n\nlet db: Database.Database | null = null;\n\n/**\n * Get or create the SQLite database connection.\n * Applies WAL mode and creates schema if needed.\n */\nexport function getDatabase(config?: HookdashConfig): Database.Database {\n if (db) return db;\n\n const dbPath = config?.database.path\n ? resolve(config.database.path)\n : resolve('./hookdash.db');\n\n // Ensure directory exists\n mkdirSync(dirname(dbPath), { recursive: true });\n\n db = new Database(dbPath);\n\n // Performance pragmas\n db.pragma('journal_mode = WAL');\n db.pragma('synchronous = NORMAL');\n db.pragma('foreign_keys = ON');\n db.pragma('busy_timeout = 5000');\n db.pragma('cache_size = -20000'); // 20MB\n\n // Apply schema\n applySchema(db);\n\n console.log(`[hookdash] Database ready at ${dbPath}`);\n return db;\n}\n\n/**\n * Apply schema and run migrations.\n */\nfunction applySchema(database: Database.Database): void {\n database.exec(SCHEMA_SQL);\n\n // Check current version\n const row = database.prepare(\n 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'\n ).get() as { version: number } | undefined;\n\n const currentVersion = row?.version ?? 0;\n\n if (currentVersion < CURRENT_SCHEMA_VERSION) {\n // Run migrations for each version\n runMigrations(database, currentVersion, CURRENT_SCHEMA_VERSION);\n\n // Update version\n database.prepare(\n 'INSERT OR REPLACE INTO schema_version (version) VALUES (?)'\n ).run(CURRENT_SCHEMA_VERSION);\n }\n}\n\n/**\n * Run incremental migrations between versions.\n */\nfunction runMigrations(\n database: Database.Database,\n _fromVersion: number,\n _toVersion: number\n): void {\n // v1 is the initial schema — no migrations needed yet.\n // Future migrations go here:\n //\n // if (fromVersion < 2) {\n // database.exec(`ALTER TABLE events ADD COLUMN size_bytes INTEGER`);\n // }\n}\n\n/**\n * Close the database connection.\n */\nexport function closeDatabase(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Seed the database with sources and endpoints from config.\n */\nexport function seedFromConfig(config: HookdashConfig): void {\n const database = getDatabase(config);\n\n const upsertSource = database.prepare(`\n INSERT INTO sources (id, name, provider, signing_secret, active)\n VALUES (lower(hex(randomblob(8))), ?, ?, ?, 1)\n ON CONFLICT(name) DO UPDATE SET\n provider = excluded.provider,\n signing_secret = excluded.signing_secret,\n active = 1\n `);\n\n const getSource = database.prepare('SELECT id FROM sources WHERE name = ?');\n\n const upsertEndpoint = database.prepare(`\n INSERT INTO endpoints (id, url, source_id, event_filter, headers, timeout_ms, max_retries)\n VALUES (lower(hex(randomblob(8))), ?, ?, ?, ?, ?, ?)\n ON CONFLICT DO NOTHING\n `);\n\n const checkEndpoint = database.prepare(\n 'SELECT id FROM endpoints WHERE url = ? AND source_id = ?'\n );\n\n const seedTransaction = database.transaction(() => {\n for (const source of config.sources) {\n upsertSource.run(source.name, source.provider, source.signing_secret ?? null);\n const sourceRow = getSource.get(source.name) as { id: string };\n\n for (const endpoint of source.endpoints) {\n const existing = checkEndpoint.get(endpoint.url, sourceRow.id);\n if (!existing) {\n upsertEndpoint.run(\n endpoint.url,\n sourceRow.id,\n endpoint.events ? JSON.stringify(endpoint.events) : null,\n endpoint.headers ? JSON.stringify(endpoint.headers) : null,\n endpoint.timeout_ms ?? config.delivery.default_timeout,\n endpoint.max_retries ?? config.delivery.default_max_retries,\n );\n }\n }\n }\n });\n\n seedTransaction();\n console.log(`[hookdash] Seeded ${config.sources.length} source(s) from config`);\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nconst TIMESTAMP_TOLERANCE_SEC = 300; // 5 minutes\n\nexport const stripeProvider: WebhookProvider = {\n name: 'stripe',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['stripe-signature'];\n if (!sigHeader) return false;\n\n // Parse \"t=timestamp,v1=signature\" format\n const parts = new Map<string, string>();\n for (const pair of sigHeader.split(',')) {\n const idx = pair.indexOf('=');\n if (idx === -1) continue;\n parts.set(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());\n }\n\n const timestamp = parts.get('t');\n const signature = parts.get('v1');\n if (!timestamp || !signature) return false;\n\n // Validate timestamp within tolerance\n const ts = parseInt(timestamp, 10);\n if (Number.isNaN(ts)) return false;\n\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - ts) > TIMESTAMP_TOLERANCE_SEC) return false;\n\n // Signed payload: \"${timestamp}.${rawBody}\"\n const signedPayload = `${timestamp}.${rawBody.toString('utf8')}`;\n const expectedSig = createHmac('sha256', secret)\n .update(signedPayload)\n .digest('hex');\n\n // Constant-time comparison\n const sigBuf = Buffer.from(signature, 'hex');\n const expectedBuf = Buffer.from(expectedSig, 'hex');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(_headers: Record<string, string>, body: unknown): string | null {\n if (body && typeof body === 'object' && 'type' in body) {\n const t = (body as Record<string, unknown>).type;\n return typeof t === 'string' ? t : null;\n }\n return null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const githubProvider: WebhookProvider = {\n name: 'github',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['x-hub-signature-256'];\n if (!sigHeader) return false;\n\n // Format: \"sha256=hexsignature\"\n const prefix = 'sha256=';\n if (!sigHeader.startsWith(prefix)) return false;\n\n const signature = sigHeader.slice(prefix.length);\n\n const expectedSig = createHmac('sha256', secret)\n .update(rawBody)\n .digest('hex');\n\n // Constant-time comparison\n const sigBuf = Buffer.from(signature, 'hex');\n const expectedBuf = Buffer.from(expectedSig, 'hex');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(headers: Record<string, string>, _body: unknown): string | null {\n return headers['x-github-event'] ?? null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const twilioProvider: WebhookProvider = {\n name: 'twilio',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['x-twilio-signature'];\n if (!sigHeader) return false;\n\n // HMAC-SHA1, base64 encoded\n const expectedSig = createHmac('sha1', secret)\n .update(rawBody)\n .digest('base64');\n\n // Constant-time comparison (compare as raw bytes from base64)\n const sigBuf = Buffer.from(sigHeader, 'base64');\n const expectedBuf = Buffer.from(expectedSig, 'base64');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(_headers: Record<string, string>, body: unknown): string | null {\n // Twilio doesn't have a standard event type header — try to extract from body\n if (body && typeof body === 'object') {\n const b = body as Record<string, unknown>;\n // Twilio status callbacks include an EventType or similar field\n if (typeof b.EventType === 'string') return b.EventType;\n if (typeof b.event_type === 'string') return b.event_type;\n if (typeof b.type === 'string') return b.type;\n }\n return null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const shopifyProvider: WebhookProvider = {\n name: 'shopify',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['x-shopify-hmac-sha256'];\n if (!sigHeader) return false;\n\n // HMAC-SHA256, base64 encoded\n const expectedSig = createHmac('sha256', secret)\n .update(rawBody)\n .digest('base64');\n\n // Constant-time comparison (compare raw bytes from base64)\n const sigBuf = Buffer.from(sigHeader, 'base64');\n const expectedBuf = Buffer.from(expectedSig, 'base64');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(headers: Record<string, string>, _body: unknown): string | null {\n return headers['x-shopify-topic'] ?? null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const genericProvider: WebhookProvider = {\n name: 'generic',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n // Check both common signature headers\n const sigHeader = headers['x-webhook-signature'] ?? headers['x-signature'];\n if (!sigHeader) return false;\n\n // HMAC-SHA256, hex encoded\n const expectedSig = createHmac('sha256', secret)\n .update(rawBody)\n .digest('hex');\n\n // Constant-time comparison\n const sigBuf = Buffer.from(sigHeader, 'hex');\n const expectedBuf = Buffer.from(expectedSig, 'hex');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(_headers: Record<string, string>, body: unknown): string | null {\n if (body && typeof body === 'object') {\n const b = body as Record<string, unknown>;\n // Try common event type field names\n if (typeof b.event === 'string') return b.event;\n if (typeof b.type === 'string') return b.type;\n }\n return null;\n },\n};\n","import type { WebhookProvider } from '../types/index.js';\nimport { stripeProvider } from './stripe.js';\nimport { githubProvider } from './github.js';\nimport { twilioProvider } from './twilio.js';\nimport { shopifyProvider } from './shopify.js';\nimport { genericProvider } from './generic.js';\n\nconst providers = new Map<string, WebhookProvider>();\n\nfunction register(provider: WebhookProvider): void {\n providers.set(provider.name, provider);\n}\n\n// Register all built-in providers\nregister(stripeProvider);\nregister(githubProvider);\nregister(twilioProvider);\nregister(shopifyProvider);\nregister(genericProvider);\n\n/**\n * Retrieve a webhook provider by name.\n * @throws Error if the provider is not registered.\n */\nexport function getProvider(name: string): WebhookProvider {\n const provider = providers.get(name);\n if (!provider) {\n const available = [...providers.keys()].join(', ');\n throw new Error(\n `Unknown webhook provider \"${name}\". Available providers: ${available}`,\n );\n }\n return provider;\n}\n","/**\n * Normalize headers to lowercase keys.\n * HTTP headers are case-insensitive, so we standardize for consistent lookup.\n */\nexport function normalizeHeaders(\n headers: Record<string, string | string[] | undefined>,\n): Record<string, string> {\n const normalized: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return normalized;\n}\n\n/**\n * Safely parse a raw body as JSON and attempt to extract the event type.\n * Returns the parsed body and event type (if found).\n */\nexport function parseBodyEventType(rawBody: Buffer): {\n parsed: unknown;\n eventType: string | null;\n} {\n let parsed: unknown = null;\n let eventType: string | null = null;\n\n try {\n parsed = JSON.parse(rawBody.toString('utf8'));\n } catch {\n // Body is not JSON — that's fine, return null\n return { parsed: null, eventType: null };\n }\n\n if (parsed && typeof parsed === 'object') {\n const obj = parsed as Record<string, unknown>;\n // Try common event type field names in priority order\n for (const field of ['event', 'type', 'event_type', 'eventType', 'action']) {\n if (typeof obj[field] === 'string') {\n eventType = obj[field] as string;\n break;\n }\n }\n }\n\n return { parsed, eventType };\n}\n\n/**\n * Match an event type against a list of glob patterns.\n * Supported patterns:\n * - `*` → matches everything\n * - `order.*` → matches `order.created`, `order.updated`, etc.\n * - `push` → exact match\n * - `issues.*` → matches `issues.opened`, `issues.closed`\n *\n * Returns true if eventType matches ANY of the patterns, or if the\n * filter list is empty/null (meaning \"accept all\").\n */\nexport function matchesEventFilter(\n eventType: string | null,\n filterPatterns: string[] | null,\n): boolean {\n // No filter = accept everything\n if (!filterPatterns || filterPatterns.length === 0) return true;\n\n // No event type but filters exist — can't match\n if (!eventType) return false;\n\n return filterPatterns.some((pattern) => globMatch(pattern, eventType));\n}\n\n/**\n * Simple glob matching for webhook event types.\n * Converts glob pattern to regex:\n * `*` → `[^.]*` (single segment)\n * `**` → `.*` (any number of segments)\n * `.` → `\\.` (literal dot)\n */\nfunction globMatch(pattern: string, value: string): boolean {\n // Shortcut: universal wildcard\n if (pattern === '*' || pattern === '**') return true;\n\n // Escape regex specials except * and ?\n let regexStr = '';\n let i = 0;\n while (i < pattern.length) {\n const char = pattern[i];\n\n if (char === '*') {\n if (pattern[i + 1] === '*') {\n // ** matches across dots\n regexStr += '.*';\n i += 2;\n } else {\n // * matches within a single segment (no dots)\n regexStr += '[^.]*';\n i += 1;\n }\n } else if (char === '?') {\n regexStr += '[^.]';\n i += 1;\n } else if ('.+^${}()|[]\\\\'.includes(char)) {\n regexStr += `\\\\${char}`;\n i += 1;\n } else {\n regexStr += char;\n i += 1;\n }\n }\n\n const regex = new RegExp(`^${regexStr}$`);\n return regex.test(value);\n}\n","import type { Database } from 'better-sqlite3';\nimport type { CircuitState } from '../types/index.js';\n\nexport class CircuitBreaker {\n private db: Database;\n private failureThreshold = 5;\n private cooldownMs = 60000; // 60 seconds\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Check if we can deliver to this endpoint.\n * If state is open, checks if cooldown has expired to transition to half-open.\n */\n canDeliver(endpointId: string): boolean {\n const row = this.db.prepare(\n 'SELECT circuit_state, circuit_last_failure_at FROM endpoints WHERE id = ?'\n ).get(endpointId) as { circuit_state: CircuitState; circuit_last_failure_at: string | null } | undefined;\n\n if (!row) return false;\n\n if (row.circuit_state === 'closed' || row.circuit_state === 'half-open') {\n return true;\n }\n\n if (row.circuit_state === 'open' && row.circuit_last_failure_at) {\n const lastFailure = new Date(row.circuit_last_failure_at).getTime();\n const elapsed = Date.now() - lastFailure;\n\n if (elapsed >= this.cooldownMs) {\n // Transition to half-open to allow a probe request\n this.db.prepare(\n \"UPDATE endpoints SET circuit_state = 'half-open' WHERE id = ?\"\n ).run(endpointId);\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Reset circuit to closed on successful delivery.\n */\n recordSuccess(endpointId: string): void {\n this.db.prepare(`\n UPDATE endpoints\n SET circuit_state = 'closed',\n circuit_failure_count = 0,\n circuit_last_failure_at = NULL\n WHERE id = ?\n `).run(endpointId);\n }\n\n /**\n * Record failure. If consecutive failures exceed threshold, open the circuit.\n */\n recordFailure(endpointId: string): void {\n const row = this.db.prepare(\n 'SELECT circuit_failure_count, circuit_state FROM endpoints WHERE id = ?'\n ).get(endpointId) as { circuit_failure_count: number; circuit_state: CircuitState } | undefined;\n\n if (!row) return;\n\n const newFailureCount = row.circuit_failure_count + 1;\n const nowStr = new Date().toISOString();\n\n if (newFailureCount >= this.failureThreshold || row.circuit_state === 'half-open') {\n // Open the circuit\n this.db.prepare(`\n UPDATE endpoints\n SET circuit_state = 'open',\n circuit_failure_count = ?,\n circuit_last_failure_at = ?\n WHERE id = ?\n `).run(newFailureCount, nowStr, endpointId);\n } else {\n // Just increment failure count\n this.db.prepare(`\n UPDATE endpoints\n SET circuit_failure_count = ?,\n circuit_last_failure_at = ?\n WHERE id = ?\n `).run(newFailureCount, nowStr, endpointId);\n }\n }\n}\n","/**\n * Calculates exponential backoff delay with jitter.\n * Formula: min(BASE * 2^attempt + jitter, MAX_DELAY)\n */\nexport function getNextRetryDelay(attempt: number): number {\n const BASE_DELAY = 1000; // 1 second\n const MAX_DELAY = 3600_000; // 1 hour\n const JITTER_FACTOR = 0.25;\n\n const exponential = BASE_DELAY * Math.pow(2, attempt);\n const capped = Math.min(exponential, MAX_DELAY);\n const jitter = capped * JITTER_FACTOR * Math.random();\n\n return capped + jitter;\n}\n\n/**\n * Determines if a request should be retried based on status code.\n * Retry on 5xx server errors and network errors (e.g. status code 0 or -1).\n * Do NOT retry on 4xx client errors (unauthorized, bad request, etc.), except for 429 Too Many Requests.\n */\nexport function shouldRetry(statusCode: number): boolean {\n if (statusCode === 0 || statusCode === -1) return true; // network errors, timeouts\n if (statusCode === 429) return true; // rate limited, retry later\n if (statusCode >= 500 && statusCode < 600) return true; // server errors\n return false;\n}\n","import { EventEmitter } from 'node:events';\nimport type { Database } from 'better-sqlite3';\nimport type { HookdashConfig, Delivery, Endpoint, WebhookEvent } from '../types/index.js';\nimport { getDatabase } from '../db/connection.js';\nimport { CircuitBreaker } from './circuit-breaker.js';\nimport { getNextRetryDelay, shouldRetry } from './retry.js';\n\nexport const deliveryEvents = new EventEmitter();\n\nexport class DeliveryWorker {\n private db: Database;\n private intervalId: NodeJS.Timeout | null = null;\n private pollInterval: number;\n private defaultTimeout: number;\n private circuitBreaker: CircuitBreaker;\n private isProcessing = false;\n\n constructor(config: HookdashConfig) {\n this.db = getDatabase(config);\n this.pollInterval = config.delivery.poll_interval;\n this.defaultTimeout = config.delivery.default_timeout;\n this.circuitBreaker = new CircuitBreaker(this.db);\n }\n\n start(): void {\n if (this.intervalId) return;\n\n this.intervalId = setInterval(async () => {\n if (this.isProcessing) return;\n this.isProcessing = true;\n try {\n await this.processQueue();\n } catch (err) {\n console.error('[hookdash-worker] Error processing queue:', err);\n } finally {\n this.isProcessing = false;\n }\n }, this.pollInterval);\n\n console.log(`[hookdash-worker] Started queue worker (poll: ${this.pollInterval}ms)`);\n }\n\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n console.log('[hookdash-worker] Stopped queue worker');\n }\n }\n\n private async processQueue(): Promise<void> {\n const now = new Date().toISOString();\n\n // Select pending and retrying deliveries that are due\n const dueDeliveries = this.db.prepare(`\n SELECT d.*, e.url as endpoint_url, e.headers as endpoint_headers, e.timeout_ms as endpoint_timeout,\n e.max_retries as endpoint_max_retries, ev.body as event_body, ev.headers as event_headers,\n ev.content_type as event_content_type\n FROM deliveries d\n JOIN endpoints e ON d.endpoint_id = e.id\n JOIN events ev ON d.event_id = ev.id\n WHERE (d.status = 'pending' OR (d.status = 'retrying' AND d.next_retry_at <= ?))\n LIMIT 10\n `).all(now) as any[];\n\n if (dueDeliveries.length === 0) return;\n\n for (const d of dueDeliveries) {\n // 1. Check Circuit Breaker\n if (!this.circuitBreaker.canDeliver(d.endpoint_id)) {\n // Skip for now, check next time\n continue;\n }\n\n await this.deliver(d);\n }\n }\n\n private async deliver(d: any): Promise<void> {\n const endpointHeaders = d.endpoint_headers ? JSON.parse(d.endpoint_headers) : {};\n const eventHeaders = d.event_headers ? JSON.parse(d.event_headers) : {};\n\n // Build headers to send\n const headers = {\n 'content-type': d.event_content_type || 'application/json',\n ...eventHeaders,\n ...endpointHeaders,\n 'x-hookdash-delivery-id': d.id,\n 'x-hookdash-event-id': d.event_id,\n 'x-hookdash-attempt': (d.attempts + 1).toString(),\n };\n\n // Remove hop-by-hop or signature verification headers that don't apply,\n // but keep custom signing headers if needed.\n delete headers['connection'];\n delete headers['host'];\n delete headers['content-length'];\n\n const timeout = d.endpoint_timeout || this.defaultTimeout;\n const controller = new AbortController();\n const id = setTimeout(() => controller.abort(), timeout);\n\n const startTime = Date.now();\n let status: Delivery['status'] = 'failed';\n let statusCode: number | null = null;\n let responseBody = '';\n let errorMsg: string | null = null;\n\n try {\n const response = await fetch(d.endpoint_url, {\n method: 'POST',\n headers,\n body: d.event_body,\n signal: controller.signal,\n });\n\n statusCode = response.status;\n responseBody = await response.text();\n\n if (response.ok) {\n status = 'success';\n this.circuitBreaker.recordSuccess(d.endpoint_id);\n } else {\n status = 'failed';\n this.circuitBreaker.recordFailure(d.endpoint_id);\n }\n } catch (err: any) {\n this.circuitBreaker.recordFailure(d.endpoint_id);\n if (err.name === 'AbortError') {\n errorMsg = `Timeout after ${timeout}ms`;\n } else {\n errorMsg = err.message || 'Network error';\n }\n } finally {\n clearTimeout(id);\n }\n\n const attempts = d.attempts + 1;\n const completedAt = new Date().toISOString();\n let nextRetryAt: string | null = null;\n\n if (status === 'failed') {\n const maxRetries = d.endpoint_max_retries ?? 8;\n const shouldRetryRequest = shouldRetry(statusCode ?? -1);\n\n if (attempts < maxRetries && shouldRetryRequest) {\n status = 'retrying';\n const delay = getNextRetryDelay(attempts);\n nextRetryAt = new Date(Date.now() + delay).toISOString();\n } else {\n status = 'dead'; // Move to DLQ\n }\n }\n\n // Update database\n this.db.prepare(`\n UPDATE deliveries\n SET status = ?,\n attempts = ?,\n next_retry_at = ?,\n last_status_code = ?,\n last_response_body = ?,\n last_error = ?,\n completed_at = ?\n WHERE id = ?\n `).run(\n status,\n attempts,\n nextRetryAt,\n statusCode,\n responseBody.substring(0, 10000), // Cap payload response length\n errorMsg,\n completedAt,\n d.id\n );\n\n // Emit event for real-time dashboard updates\n deliveryEvents.emit('delivery:update', {\n id: d.id,\n event_id: d.event_id,\n endpoint_id: d.endpoint_id,\n endpoint_url: d.endpoint_url,\n status,\n attempts,\n next_retry_at: nextRetryAt,\n last_status_code: statusCode,\n last_error: errorMsg,\n completed_at: completedAt,\n });\n }\n}\n","import type { FastifyPluginAsync } from 'fastify';\nimport { deliveryEvents } from '../delivery/worker.js';\n\n// Global registry of active SSE connections\nconst activeClients = new Set<(event: string, data: any) => void>();\n\n/**\n * Broadcast an event to all active dashboard SSE connections.\n */\nexport function broadcastSSE(event: string, data: any): void {\n for (const send of activeClients) {\n send(event, data);\n }\n}\n\nexport const sseRoutes: FastifyPluginAsync = async (fastify) => {\n fastify.get('/stream', async (request, reply) => {\n // SSE headers\n reply.raw.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const send = (event: string, data: any) => {\n reply.raw.write(`event: ${event}\\n`);\n reply.raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n };\n\n activeClients.add(send);\n\n // Heartbeat every 15 seconds to keep connection alive\n const heartbeatId = setInterval(() => {\n reply.raw.write(': heartbeat\\n\\n');\n }, 15000);\n\n // Initial message\n send('connected', { timestamp: new Date().toISOString() });\n\n // Listen to delivery worker events\n const onDeliveryUpdate = (data: any) => {\n send('delivery:update', data);\n };\n\n deliveryEvents.on('delivery:update', onDeliveryUpdate);\n\n // Cleanup on disconnect\n request.raw.on('close', () => {\n clearInterval(heartbeatId);\n activeClients.delete(send);\n deliveryEvents.off('delivery:update', onDeliveryUpdate);\n });\n\n // Tell Fastify not to send standard response\n reply.hijack();\n });\n};\n","import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';\nimport { nanoid } from 'nanoid';\nimport { getDatabase } from '../db/connection.js';\nimport { getProvider } from '../providers/registry.js';\nimport { normalizeHeaders, parseBodyEventType, matchesEventFilter } from './parser.js';\nimport type { Source, Endpoint, SSEEvent } from '../types/index.js';\n\nimport { broadcastSSE } from '../api/sse.js';\n// ─── Route Params / Types ───────────────────────────────────────\ninterface WebhookParams {\n source: string;\n}\n\n// ─── Fastify Plugin ─────────────────────────────────────────────\nexport default async function ingestionRoutes(fastify: FastifyInstance): Promise<void> {\n fastify.post<{ Params: WebhookParams }>(\n '/webhook/:source',\n async (request: FastifyRequest<{ Params: WebhookParams }>, reply: FastifyReply) => {\n const { source: sourceName } = request.params;\n\n // ── 1. Get raw body ───────────────────────────────────────\n const rawBody = (request as FastifyRequest & { rawBody?: Buffer }).rawBody;\n if (!rawBody) {\n return reply.status(400).send({\n error: 'Missing raw body. Ensure @fastify/raw-body is registered.',\n });\n }\n\n // ── 2. Look up source by name from DB ─────────────────────\n const db = getDatabase();\n const source = db\n .prepare('SELECT * FROM sources WHERE name = ? AND active = 1')\n .get(sourceName) as Source | undefined;\n\n if (!source) {\n return reply.status(404).send({\n error: `Unknown webhook source \"${sourceName}\".`,\n });\n }\n\n // ── 3. Normalize headers ──────────────────────────────────\n const headers = normalizeHeaders(\n request.headers as Record<string, string | string[] | undefined>,\n );\n\n // ── 4. Parse body and extract event type ──────────────────\n const { parsed: parsedBody } = parseBodyEventType(rawBody);\n\n // ── 5. Verify signature ───────────────────────────────────\n let signatureValid: boolean | null = null;\n\n if (source.signing_secret) {\n try {\n const provider = getProvider(source.provider);\n signatureValid = provider.verify(source.signing_secret, headers, rawBody);\n\n if (!signatureValid) {\n return reply.status(401).send({ error: 'Invalid webhook signature.' });\n }\n } catch (err) {\n fastify.log.error(err, `Provider verification error for source \"${sourceName}\"`);\n return reply.status(500).send({ error: 'Signature verification failed.' });\n }\n }\n\n // ── 6. Extract event type via provider ────────────────────\n let eventType: string | null = null;\n try {\n const provider = getProvider(source.provider);\n eventType = provider.extractEventType(headers, parsedBody ?? rawBody.toString('utf8'));\n } catch {\n // Non-fatal — event type is optional\n }\n\n // ── 7. Store event ────────────────────────────────────────\n const eventId = nanoid();\n const contentType = headers['content-type'] ?? null;\n\n db.prepare(\n `INSERT INTO events (id, source_id, event_type, headers, body, content_type, signature_valid)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n eventId,\n source.id,\n eventType,\n JSON.stringify(headers),\n rawBody.toString('utf8'),\n contentType,\n signatureValid === null ? null : signatureValid ? 1 : 0,\n );\n\n // ── 8. Find matching endpoints and create deliveries ──────\n const endpoints = db\n .prepare(\n 'SELECT * FROM endpoints WHERE (source_id = ? OR source_id IS NULL) AND active = 1',\n )\n .all(source.id) as Endpoint[];\n\n const deliveryIds: string[] = [];\n\n const insertDelivery = db.prepare(\n `INSERT INTO deliveries (id, event_id, endpoint_id, status)\n VALUES (?, ?, ?, 'pending')`,\n );\n\n const createDeliveries = db.transaction(() => {\n for (const endpoint of endpoints) {\n // Check event filter\n let filterPatterns: string[] | null = null;\n if (endpoint.event_filter) {\n try {\n filterPatterns = JSON.parse(endpoint.event_filter) as string[];\n } catch {\n // Invalid filter JSON — skip filtering\n }\n }\n\n if (!matchesEventFilter(eventType, filterPatterns)) {\n continue;\n }\n\n const deliveryId = nanoid();\n insertDelivery.run(deliveryId, eventId, endpoint.id);\n deliveryIds.push(deliveryId);\n }\n });\n\n createDeliveries();\n\n // ── 9. Emit SSE event for real-time dashboard ─────────────\n broadcastSSE('event:new', {\n id: eventId,\n source_id: source.id,\n source_name: source.name,\n source_provider: source.provider,\n event_type: eventType,\n delivery_count: deliveryIds.length,\n received_at: new Date().toISOString(),\n });\n\n // ── 10. Return 202 Accepted ───────────────────────────────\n return reply.status(202).send({\n event_id: eventId,\n event_type: eventType,\n deliveries: deliveryIds.length,\n });\n },\n );\n}\n","import type { FastifyPluginAsync } from 'fastify';\nimport { nanoid } from 'nanoid';\nimport type { EventWithSource, PaginatedResponse, Delivery } from '../types/index.js';\n\nexport const eventRoutes: FastifyPluginAsync = async (fastify) => {\n const { db } = fastify;\n\n // GET /api/events\n fastify.get('/', async (request, reply) => {\n const query = request.query as any;\n const page = Math.max(1, parseInt(query.page) || 1);\n const perPage = Math.min(100, Math.max(1, parseInt(query.per_page) || 20));\n const offset = (page - 1) * perPage;\n\n const conditions: string[] = [];\n const params: any[] = [];\n\n if (query.source) {\n conditions.push('s.name = ?');\n params.push(query.source);\n }\n\n if (query.event_type) {\n conditions.push('e.event_type = ?');\n params.push(query.event_type);\n }\n\n if (query.status) {\n conditions.push(`EXISTS (\n SELECT 1 FROM deliveries d\n WHERE d.event_id = e.id AND d.status = ?\n )`);\n params.push(query.status);\n }\n\n if (query.from) {\n conditions.push('e.received_at >= ?');\n params.push(query.from);\n }\n\n if (query.to) {\n conditions.push('e.received_at <= ?');\n params.push(query.to);\n }\n\n if (query.search) {\n conditions.push('(e.body LIKE ? OR e.event_type LIKE ? OR e.id LIKE ?)');\n const wild = `%${query.search}%`;\n params.push(wild, wild, wild);\n }\n\n const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n\n // Get total count\n const totalRow = db.prepare(`\n SELECT COUNT(DISTINCT e.id) as count\n FROM events e\n JOIN sources s ON e.source_id = s.id\n ${whereClause}\n `).get(...params) as { count: number };\n\n const total = totalRow?.count || 0;\n\n // Get paginated events with delivery statistics\n const eventsQuery = `\n SELECT e.id, e.event_type, e.received_at, e.signature_valid,\n s.name as source_name, s.provider as source_provider,\n COUNT(d.id) as delivery_count,\n SUM(CASE WHEN d.status = 'success' THEN 1 ELSE 0 END) as success_count,\n SUM(CASE WHEN d.status IN ('failed', 'retrying', 'dead') THEN 1 ELSE 0 END) as failed_count\n FROM events e\n JOIN sources s ON e.source_id = s.id\n LEFT JOIN deliveries d ON d.event_id = e.id\n ${whereClause}\n GROUP BY e.id\n ORDER BY e.received_at DESC\n LIMIT ? OFFSET ?\n `;\n\n const events = db.prepare(eventsQuery).all(...params, perPage, offset) as EventWithSource[];\n\n const response: PaginatedResponse<EventWithSource> = {\n data: events,\n total,\n page,\n per_page: perPage,\n total_pages: Math.ceil(total / perPage),\n };\n\n return response;\n });\n\n // GET /api/events/:id\n fastify.get('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const event = db.prepare(`\n SELECT e.*, s.name as source_name, s.provider as source_provider\n FROM events e\n JOIN sources s ON e.source_id = s.id\n WHERE e.id = ?\n `).get(id) as any;\n\n if (!event) {\n return reply.status(404).send({ error: 'Event not found' });\n }\n\n const deliveries = db.prepare(`\n SELECT d.*, e.url as endpoint_url\n FROM deliveries d\n JOIN endpoints e ON d.endpoint_id = e.id\n WHERE d.event_id = ?\n ORDER BY d.created_at ASC\n `).all(id) as Delivery[];\n\n return {\n ...event,\n deliveries,\n };\n });\n\n // POST /api/events/:id/replay\n fastify.post('/:id/replay', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const event = db.prepare('SELECT id FROM events WHERE id = ?').get(id);\n if (!event) {\n return reply.status(404).send({ error: 'Event not found' });\n }\n\n // Get all endpoints configured for the source of this event\n const endpoints = db.prepare(`\n SELECT ep.id\n FROM endpoints ep\n JOIN events ev ON ev.source_id = ep.source_id\n WHERE ev.id = ? AND ep.active = 1\n `).all(id) as { id: string }[];\n\n const newDeliveryIds: string[] = [];\n\n const insertDelivery = db.prepare(`\n INSERT INTO deliveries (id, event_id, endpoint_id, status, attempts)\n VALUES (?, ?, ?, 'pending', 0)\n `);\n\n db.transaction(() => {\n for (const ep of endpoints) {\n const deliveryId = `del_${nanoid(12)}`;\n insertDelivery.run(deliveryId, id, ep.id);\n newDeliveryIds.push(deliveryId);\n }\n })();\n\n return {\n success: true,\n message: `Requeued ${newDeliveryIds.length} deliveries`,\n delivery_ids: newDeliveryIds,\n };\n });\n\n // DELETE /api/events/:id\n fastify.delete('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const result = db.prepare('DELETE FROM events WHERE id = ?').run(id);\n\n if (result.changes === 0) {\n return reply.status(404).send({ error: 'Event not found' });\n }\n\n return { success: true };\n });\n};\n","import type { FastifyPluginAsync } from 'fastify';\nimport { nanoid } from 'nanoid';\n\nexport const endpointRoutes: FastifyPluginAsync = async (fastify) => {\n const { db } = fastify;\n\n // GET /api/endpoints\n fastify.get('/', async (request, reply) => {\n const endpoints = db.prepare(`\n SELECT e.*, s.name as source_name, s.provider as source_provider\n FROM endpoints e\n LEFT JOIN sources s ON e.source_id = s.id\n ORDER BY e.created_at DESC\n `).all();\n\n return endpoints.map((ep: any) => ({\n ...ep,\n event_filter: ep.event_filter ? JSON.parse(ep.event_filter) : null,\n headers: ep.headers ? JSON.parse(ep.headers) : null,\n }));\n });\n\n // POST /api/endpoints\n fastify.post('/', async (request, reply) => {\n const body = request.body as any;\n\n if (!body.url) {\n return reply.status(400).send({ error: 'url is required' });\n }\n\n const id = `ep_${nanoid(12)}`;\n const eventFilter = body.event_filter ? JSON.stringify(body.event_filter) : null;\n const customHeaders = body.headers ? JSON.stringify(body.headers) : null;\n\n db.prepare(`\n INSERT INTO endpoints (\n id, url, source_id, event_filter, headers, timeout_ms, max_retries, active\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n id,\n body.url,\n body.source_id || null,\n eventFilter,\n customHeaders,\n body.timeout_ms ?? 30000,\n body.max_retries ?? 8,\n body.active !== undefined ? (body.active ? 1 : 0) : 1\n );\n\n const created = db.prepare('SELECT * FROM endpoints WHERE id = ?').get(id) as any;\n return {\n ...created,\n event_filter: created.event_filter ? JSON.parse(created.event_filter) : null,\n headers: created.headers ? JSON.parse(created.headers) : null,\n };\n });\n\n // PUT /api/endpoints/:id\n fastify.put('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const body = request.body as any;\n\n const existing = db.prepare('SELECT id FROM endpoints WHERE id = ?').get(id);\n if (!existing) {\n return reply.status(404).send({ error: 'Endpoint not found' });\n }\n\n const eventFilter = body.event_filter ? JSON.stringify(body.event_filter) : null;\n const customHeaders = body.headers ? JSON.stringify(body.headers) : null;\n\n db.prepare(`\n UPDATE endpoints\n SET url = COALESCE(?, url),\n source_id = COALESCE(?, source_id),\n event_filter = ?,\n headers = ?,\n timeout_ms = COALESCE(?, timeout_ms),\n max_retries = COALESCE(?, max_retries),\n active = COALESCE(?, active)\n WHERE id = ?\n `).run(\n body.url,\n body.source_id,\n eventFilter,\n customHeaders,\n body.timeout_ms,\n body.max_retries,\n body.active !== undefined ? (body.active ? 1 : 0) : null,\n id\n );\n\n const updated = db.prepare('SELECT * FROM endpoints WHERE id = ?').get(id) as any;\n return {\n ...updated,\n event_filter: updated.event_filter ? JSON.parse(updated.event_filter) : null,\n headers: updated.headers ? JSON.parse(updated.headers) : null,\n };\n });\n\n // DELETE /api/endpoints/:id\n fastify.delete('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const result = db.prepare('DELETE FROM endpoints WHERE id = ?').run(id);\n\n if (result.changes === 0) {\n return reply.status(404).send({ error: 'Endpoint not found' });\n }\n\n return { success: true };\n });\n\n // POST /api/endpoints/:id/test\n fastify.post('/:id/test', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const ep = db.prepare('SELECT url, headers, timeout_ms FROM endpoints WHERE id = ?').get(id) as any;\n if (!ep) {\n return reply.status(404).send({ error: 'Endpoint not found' });\n }\n\n const customHeaders = ep.headers ? JSON.parse(ep.headers) : {};\n const timeout = ep.timeout_ms || 30000;\n\n const testPayload = {\n event: 'hookdash.test',\n timestamp: new Date().toISOString(),\n test: true,\n message: 'This is a test webhook from hookdash dashboard',\n };\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(ep.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-hookdash-test': 'true',\n ...customHeaders,\n },\n body: JSON.stringify(testPayload),\n signal: controller.signal,\n });\n\n const responseText = await response.text();\n\n return {\n success: response.ok,\n status_code: response.status,\n response: responseText.substring(0, 1000),\n };\n } catch (err: any) {\n return {\n success: false,\n error: err.name === 'AbortError' ? `Timeout after ${timeout}ms` : err.message,\n };\n } finally {\n clearTimeout(timer);\n }\n });\n};\n","import type { FastifyPluginAsync } from 'fastify';\nimport type { Stats } from '../types/index.js';\n\nexport const statsRoutes: FastifyPluginAsync = async (fastify) => {\n const { db } = fastify;\n\n // GET /api/stats\n fastify.get('/', async (request, reply) => {\n const todayStr = new Date().toISOString().split('T')[0] + 'T00:00:00.000Z';\n\n // 1. Total & Today event count\n const eventCounts = db.prepare(`\n SELECT\n COUNT(id) as total,\n SUM(CASE WHEN received_at >= ? THEN 1 ELSE 0 END) as today\n FROM events\n `).get(todayStr) as { total: number; today: number | null };\n\n const totalEvents = eventCounts?.total || 0;\n const eventsToday = eventCounts?.today || 0;\n\n // 2. Deliveries by status\n const deliveryStats = db.prepare(`\n SELECT\n status,\n COUNT(id) as count\n FROM deliveries\n GROUP BY status\n `).all() as { status: string; count: number }[];\n\n let totalDeliveries = 0;\n let successfulDeliveries = 0;\n let failedDeliveries = 0;\n let deadDeliveries = 0;\n let pendingDeliveries = 0;\n\n for (const row of deliveryStats) {\n totalDeliveries += row.count;\n if (row.status === 'success') {\n successfulDeliveries = row.count;\n } else if (row.status === 'failed' || row.status === 'retrying') {\n failedDeliveries += row.count;\n if (row.status === 'pending') pendingDeliveries = row.count;\n } else if (row.status === 'dead') {\n deadDeliveries = row.count;\n } else if (row.status === 'pending') {\n pendingDeliveries = row.count;\n }\n }\n\n const successRate = totalDeliveries > 0\n ? Math.round((successfulDeliveries / (successfulDeliveries + deadDeliveries + failedDeliveries || 1)) * 1000) / 10\n : 100.0;\n\n // 3. Events per hour (last 24 hours)\n const activeHours: { hour: string; count: number }[] = [];\n for (let i = 23; i >= 0; i--) {\n const d = new Date(Date.now() - i * 3600 * 1000);\n d.setMinutes(0, 0, 0);\n const isoStr = d.toISOString();\n // Format to YYYY-MM-DD HH:00\n const formatted = isoStr.replace('T', ' ').substring(0, 16);\n activeHours.push({ hour: formatted, count: 0 });\n }\n\n // Query events in last 24 hours\n const last24h = new Date(Date.now() - 24 * 3600 * 1000).toISOString();\n const dbHours = db.prepare(`\n SELECT strftime('%Y-%m-%d %H:00', received_at) as hour_bucket, COUNT(id) as count\n FROM events\n WHERE received_at >= ?\n GROUP BY hour_bucket\n ORDER BY hour_bucket ASC\n `).all(last24h) as { hour_bucket: string; count: number }[];\n\n for (const dbHour of dbHours) {\n const match = activeHours.find(h => h.hour === dbHour.hour_bucket);\n if (match) {\n match.count = dbHour.count;\n }\n }\n\n // 4. Top sources\n const topSources = db.prepare(`\n SELECT s.name, COUNT(e.id) as count\n FROM events e\n JOIN sources s ON e.source_id = s.id\n GROUP BY s.name\n ORDER BY count DESC\n LIMIT 5\n `).all() as { name: string; count: number }[];\n\n // 5. Top event types\n const topEventTypes = db.prepare(`\n SELECT event_type as type, COUNT(id) as count\n FROM events\n WHERE event_type IS NOT NULL\n GROUP BY event_type\n ORDER BY count DESC\n LIMIT 10\n `).all() as { type: string; count: number }[];\n\n const stats: Stats = {\n total_events: totalEvents,\n events_today: eventsToday,\n total_deliveries: totalDeliveries,\n successful_deliveries: successfulDeliveries,\n failed_deliveries: failedDeliveries,\n dead_deliveries: deadDeliveries,\n pending_deliveries: pendingDeliveries,\n success_rate: successRate,\n events_per_hour: activeHours,\n top_sources: topSources,\n top_event_types: topEventTypes,\n };\n\n return stats;\n });\n};\n","import type { FastifyPluginAsync } from 'fastify';\nimport { eventRoutes } from './events.js';\nimport { endpointRoutes } from './endpoints.js';\nimport { statsRoutes } from './stats.js';\nimport { sseRoutes } from './sse.js';\n\nexport const apiRoutes: FastifyPluginAsync = async (fastify) => {\n // Register sub-routes\n await fastify.register(eventRoutes, { prefix: '/events' });\n await fastify.register(endpointRoutes, { prefix: '/endpoints' });\n await fastify.register(statsRoutes, { prefix: '/stats' });\n await fastify.register(sseRoutes);\n\n // Health check route\n fastify.get('/health', async () => {\n return {\n status: 'ok',\n version: '0.1.0',\n uptime: process.uptime(),\n timestamp: new Date().toISOString(),\n };\n });\n};\n","import { Command } from 'commander';\nimport { loadConfig } from './config/loader.js';\nimport { createServer } from './server.js';\nimport { DeliveryWorker } from './delivery/worker.js';\nimport { closeDatabase } from './db/connection.js';\n\nconst program = new Command();\n\nprogram\n .name('hookdash')\n .description('Zero-config, self-hosted webhook gateway with a beautiful dashboard')\n .version('0.1.0');\n\nprogram\n .command('start')\n .description('Start the hookdash server')\n .option('-p, --port <port>', 'Server port', parseInt)\n .option('-h, --host <host>', 'Server host')\n .option('-c, --config <path>', 'Path to config file')\n .option('-d, --db <path>', 'Database file path')\n .action(async (options) => {\n try {\n console.log('');\n console.log(' ∿ hookdash v0.1.0');\n console.log(' ─────────────────────');\n console.log('');\n\n // Load config\n const config = loadConfig({\n configPath: options.config,\n port: options.port,\n host: options.host,\n dbPath: options.db,\n });\n\n // Create and start server\n const app = await createServer(config);\n\n // Start delivery worker\n const worker = new DeliveryWorker(config);\n worker.start();\n\n // Graceful shutdown\n const shutdown = async (signal: string) => {\n console.log(`\\n[hookdash] ${signal} received, shutting down...`);\n worker.stop();\n await app.close();\n closeDatabase();\n console.log('[hookdash] Goodbye! 👋');\n process.exit(0);\n };\n\n process.on('SIGINT', () => shutdown('SIGINT'));\n process.on('SIGTERM', () => shutdown('SIGTERM'));\n\n // Start listening\n await app.listen({\n port: config.server.port,\n host: config.server.host,\n });\n\n console.log('');\n console.log(` 🪝 Webhook endpoint: http://${config.server.host === '0.0.0.0' ? 'localhost' : config.server.host}:${config.server.port}/webhook/:source`);\n console.log(` 📊 Dashboard: http://${config.server.host === '0.0.0.0' ? 'localhost' : config.server.host}:${config.server.port}`);\n console.log(` 📡 API: http://${config.server.host === '0.0.0.0' ? 'localhost' : config.server.host}:${config.server.port}/api/health`);\n console.log('');\n\n if (config.sources.length === 0) {\n console.log(' ℹ No sources configured. Create a hookdash.config.yml to get started.');\n console.log(' See: https://github.com/hookdash/hookdash#configuration');\n console.log('');\n }\n } catch (err) {\n console.error('[hookdash] Failed to start:', (err as Error).message);\n process.exit(1);\n }\n });\n\nprogram\n .command('init')\n .description('Create an example hookdash.config.yml in the current directory')\n .action(async () => {\n const { writeFileSync, existsSync } = await import('node:fs');\n const configPath = 'hookdash.config.yml';\n\n if (existsSync(configPath)) {\n console.log(`[hookdash] ${configPath} already exists. Skipping.`);\n return;\n }\n\n const exampleConfig = `# hookdash configuration\n# Docs: https://github.com/hookdash/hookdash#configuration\n\nserver:\n port: 9090\n host: 0.0.0.0\n\ndatabase:\n path: ./hookdash.db\n\ndelivery:\n poll_interval: 1000 # ms between delivery polls\n default_timeout: 30000 # ms per delivery attempt\n default_max_retries: 8 # max retries before dead letter\n\nsources:\n # Stripe webhooks\n # - name: stripe\n # provider: stripe\n # signing_secret: \\${STRIPE_WEBHOOK_SECRET}\n # endpoints:\n # - url: http://localhost:3000/api/webhooks/stripe\n # events: [\"payment_intent.*\", \"charge.*\"]\n\n # GitHub webhooks\n # - name: github\n # provider: github\n # signing_secret: \\${GITHUB_WEBHOOK_SECRET}\n # endpoints:\n # - url: http://localhost:3000/api/webhooks/github\n\n # Generic webhooks (HMAC-SHA256)\n - name: my-service\n provider: generic\n signing_secret: my-secret-key\n endpoints:\n - url: http://localhost:3000/hooks\n`;\n\n writeFileSync(configPath, exampleConfig);\n console.log(`[hookdash] Created ${configPath}`);\n console.log('[hookdash] Edit it and run: hookdash start');\n });\n\n// Parse CLI args\nprogram.parse();\n","import { readFileSync, existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { configSchema } from './schema.js';\nimport type { HookdashConfig } from '../types/index.js';\n\n/**\n * Interpolate ${ENV_VAR} references in strings with actual env values.\n */\nfunction interpolateEnvVars(value: unknown): unknown {\n if (typeof value === 'string') {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envKey: string) => {\n const envValue = process.env[envKey.trim()];\n if (envValue === undefined) {\n console.warn(`[hookdash] Warning: Environment variable \"${envKey}\" is not set`);\n return '';\n }\n return envValue;\n });\n }\n if (Array.isArray(value)) {\n return value.map(interpolateEnvVars);\n }\n if (value !== null && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n result[k] = interpolateEnvVars(v);\n }\n return result;\n }\n return value;\n}\n\n/**\n * Search for config file in standard locations.\n */\nfunction findConfigFile(customPath?: string): string | null {\n if (customPath) {\n const resolved = resolve(customPath);\n if (existsSync(resolved)) return resolved;\n throw new Error(`Config file not found: ${resolved}`);\n }\n\n const candidates = [\n resolve('hookdash.config.yml'),\n resolve('hookdash.config.yaml'),\n resolve('.hookdash.yml'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) return candidate;\n }\n\n return null;\n}\n\n/**\n * Load and validate the hookdash configuration.\n * Priority: CLI args → config file → defaults\n */\nexport function loadConfig(options: {\n configPath?: string;\n port?: number;\n host?: string;\n dbPath?: string;\n} = {}): HookdashConfig {\n let rawConfig: Record<string, unknown> = {};\n\n // Load from config file if found\n const configFile = findConfigFile(options.configPath);\n if (configFile) {\n const content = readFileSync(configFile, 'utf-8');\n const parsed = parseYaml(content);\n if (parsed && typeof parsed === 'object') {\n rawConfig = interpolateEnvVars(parsed) as Record<string, unknown>;\n }\n console.log(`[hookdash] Loaded config from ${configFile}`);\n }\n\n // CLI overrides\n if (options.port) {\n rawConfig.server = { ...(rawConfig.server as object || {}), port: options.port };\n }\n if (options.host) {\n rawConfig.server = { ...(rawConfig.server as object || {}), host: options.host };\n }\n if (options.dbPath) {\n rawConfig.database = { ...(rawConfig.database as object || {}), path: options.dbPath };\n }\n\n // Validate\n const result = configSchema.safeParse(rawConfig);\n if (!result.success) {\n const errors = result.error.errors.map(e => ` - ${e.path.join('.')}: ${e.message}`).join('\\n');\n throw new Error(`Invalid configuration:\\n${errors}`);\n }\n\n return result.data as HookdashConfig;\n}\n","import { z } from 'zod';\n\nconst endpointConfigSchema = z.object({\n url: z.string().url(),\n events: z.array(z.string()).optional(),\n headers: z.record(z.string()).optional(),\n timeout_ms: z.number().int().positive().optional(),\n max_retries: z.number().int().min(0).max(50).optional(),\n});\n\nconst sourceConfigSchema = z.object({\n name: z.string().min(1).regex(/^[a-z0-9-]+$/, 'Source name must be lowercase alphanumeric with hyphens'),\n provider: z.enum(['stripe', 'github', 'twilio', 'shopify', 'generic']).default('generic'),\n signing_secret: z.string().optional(),\n endpoints: z.array(endpointConfigSchema).min(1),\n});\n\nexport const configSchema = z.object({\n server: z.object({\n port: z.number().int().min(1).max(65535).default(9090),\n host: z.string().default('0.0.0.0'),\n }).default({}),\n\n database: z.object({\n path: z.string().default('./hookdash.db'),\n }).default({}),\n\n delivery: z.object({\n poll_interval: z.number().int().min(100).max(60000).default(1000),\n default_timeout: z.number().int().min(1000).max(120000).default(30000),\n default_max_retries: z.number().int().min(0).max(50).default(8),\n }).default({}),\n\n sources: z.array(sourceConfigSchema).default([]),\n});\n\nexport type ValidatedConfig = z.infer<typeof configSchema>;\n","import Fastify from 'fastify';\nimport fastifyCors from '@fastify/cors';\nimport fastifyStatic from '@fastify/static';\nimport fastifyRawBody from 'fastify-raw-body';\nimport { resolve, dirname } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { HookdashConfig } from './types/index.js';\nimport { getDatabase, seedFromConfig } from './db/connection.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport async function createServer(config: HookdashConfig) {\n const app = Fastify({\n logger: {\n transport: {\n target: 'pino-pretty',\n options: {\n translateTime: 'HH:MM:ss',\n ignore: 'pid,hostname',\n },\n },\n },\n bodyLimit: 10 * 1024 * 1024, // 10MB max webhook payload\n });\n\n // ─── Plugins ──────────────────────────────────────────\n await app.register(fastifyCors, {\n origin: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE'],\n });\n\n await app.register(fastifyRawBody, {\n field: 'rawBody',\n global: false,\n encoding: false, // Return Buffer\n runFirst: true,\n });\n\n // ─── Database ─────────────────────────────────────────\n const db = getDatabase(config);\n seedFromConfig(config);\n\n // Make config and db accessible via decorators\n app.decorate('hookdashConfig', config);\n app.decorate('db', db);\n\n // ─── API Routes ───────────────────────────────────────\n // These are registered as Fastify plugins\n const ingestionRoutes = (await import('./ingestion/routes.js')).default;\n await app.register(ingestionRoutes);\n\n const { apiRoutes } = await import('./api/routes.js');\n await app.register(apiRoutes, { prefix: '/api' });\n\n // ─── Dashboard Static Files ───────────────────────────\n // Try multiple possible dashboard locations\n const dashboardPaths = [\n resolve(__dirname, '../dashboard/dist'), // Dev: source tree\n resolve(__dirname, '../../dashboard/dist'), // Built: dist/\n resolve(dirname(__dirname), 'dashboard/dist'), // npm package\n ];\n\n let dashboardPath: string | null = null;\n for (const p of dashboardPaths) {\n if (existsSync(p)) {\n dashboardPath = p;\n break;\n }\n }\n\n if (dashboardPath) {\n await app.register(fastifyStatic, {\n root: dashboardPath,\n prefix: '/',\n wildcard: false,\n });\n\n // SPA fallback: serve index.html for all non-API, non-webhook routes\n app.setNotFoundHandler((request, reply) => {\n if (request.url.startsWith('/api/') || request.url.startsWith('/webhook/')) {\n return reply.status(404).send({ error: 'Not found' });\n }\n return reply.sendFile('index.html');\n });\n\n console.log(`[hookdash] Dashboard serving from ${dashboardPath}`);\n } else {\n console.log('[hookdash] Dashboard not found (run `npm run build:dashboard` to enable)');\n\n // Basic fallback page\n app.get('/', async (_request, reply) => {\n return reply.type('text/html').send(`\n <!DOCTYPE html>\n <html>\n <head><title>hookdash</title></head>\n <body style=\"font-family:system-ui;background:#0a0a0f;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0\">\n <div style=\"text-align:center\">\n <h1>∿ hookdash</h1>\n <p style=\"color:#888\">Dashboard not built. Run <code style=\"background:#1e1e2e;padding:2px 8px;border-radius:4px\">npm run build:dashboard</code></p>\n <p style=\"color:#888;margin-top:20px\">API available at <a href=\"/api/health\" style=\"color:#3b82f6\">/api/health</a></p>\n </div>\n </body>\n </html>\n `);\n });\n }\n\n return app;\n}\n\n// ─── Fastify Type Augmentation ──────────────────────────\ndeclare module 'fastify' {\n interface FastifyInstance {\n hookdashConfig: HookdashConfig;\n db: import('better-sqlite3').Database;\n }\n\n interface FastifyRequest {\n rawBody?: Buffer;\n }\n}\n"],"mappings":"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","names":["resolve","createHmac","timingSafeEqual","createHmac","timingSafeEqual","createHmac","timingSafeEqual","createHmac","timingSafeEqual","db","db","nanoid","db","nanoid","db","db","routes_exports","init_routes","resolve","dirname","existsSync","db","ingestionRoutes","apiRoutes","existsSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/db/schema.ts","../src/db/connection.ts","../src/providers/stripe.ts","../src/providers/github.ts","../src/providers/twilio.ts","../src/providers/shopify.ts","../src/providers/generic.ts","../src/providers/registry.ts","../src/ingestion/parser.ts","../src/delivery/circuit-breaker.ts","../src/delivery/retry.ts","../src/delivery/worker.ts","../src/api/sse.ts","../src/ingestion/routes.ts","../src/api/events.ts","../src/api/endpoints.ts","../src/api/stats.ts","../src/api/routes.ts","../src/index.ts","../src/config/loader.ts","../src/config/schema.ts","../src/server.ts"],"sourcesContent":["/**\n * SQLite schema for hookdash.\n * All tables are created idempotently (IF NOT EXISTS).\n */\nexport const SCHEMA_SQL = `\n-- Webhook sources / providers\nCREATE TABLE IF NOT EXISTS sources (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n provider TEXT NOT NULL DEFAULT 'generic',\n signing_secret TEXT,\n active INTEGER DEFAULT 1,\n created_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Forwarding endpoints\nCREATE TABLE IF NOT EXISTS endpoints (\n id TEXT PRIMARY KEY,\n url TEXT NOT NULL,\n source_id TEXT REFERENCES sources(id) ON DELETE SET NULL,\n event_filter TEXT,\n headers TEXT,\n timeout_ms INTEGER DEFAULT 30000,\n max_retries INTEGER DEFAULT 8,\n active INTEGER DEFAULT 1,\n circuit_state TEXT DEFAULT 'closed',\n circuit_failure_count INTEGER DEFAULT 0,\n circuit_last_failure_at TEXT,\n created_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Incoming webhook events\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n source_id TEXT NOT NULL REFERENCES sources(id),\n event_type TEXT,\n headers TEXT NOT NULL,\n body TEXT NOT NULL,\n content_type TEXT,\n signature_valid INTEGER,\n received_at TEXT DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_received ON events(received_at);\nCREATE INDEX IF NOT EXISTS idx_events_source ON events(source_id);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);\n\n-- Delivery attempts\nCREATE TABLE IF NOT EXISTS deliveries (\n id TEXT PRIMARY KEY,\n event_id TEXT NOT NULL REFERENCES events(id) ON DELETE CASCADE,\n endpoint_id TEXT NOT NULL REFERENCES endpoints(id) ON DELETE CASCADE,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n next_retry_at TEXT,\n last_status_code INTEGER,\n last_response_body TEXT,\n last_error TEXT,\n created_at TEXT DEFAULT (datetime('now')),\n completed_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_deliveries_status ON deliveries(status);\nCREATE INDEX IF NOT EXISTS idx_deliveries_next_retry ON deliveries(next_retry_at);\nCREATE INDEX IF NOT EXISTS idx_deliveries_event ON deliveries(event_id);\n\n-- Schema version tracking\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at TEXT DEFAULT (datetime('now'))\n);\n`;\n\nexport const CURRENT_SCHEMA_VERSION = 1;\n","import Database from 'better-sqlite3';\nimport { resolve } from 'node:path';\nimport { mkdirSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { SCHEMA_SQL, CURRENT_SCHEMA_VERSION } from './schema.js';\nimport type { HookdashConfig } from '../types/index.js';\n\nlet db: Database.Database | null = null;\n\n/**\n * Get or create the SQLite database connection.\n * Applies WAL mode and creates schema if needed.\n */\nexport function getDatabase(config?: HookdashConfig): Database.Database {\n if (db) return db;\n\n const dbPath = config?.database.path\n ? resolve(config.database.path)\n : resolve('./hookdash.db');\n\n // Ensure directory exists\n mkdirSync(dirname(dbPath), { recursive: true });\n\n db = new Database(dbPath);\n\n // Performance pragmas\n db.pragma('journal_mode = WAL');\n db.pragma('synchronous = NORMAL');\n db.pragma('foreign_keys = ON');\n db.pragma('busy_timeout = 5000');\n db.pragma('cache_size = -20000'); // 20MB\n\n // Apply schema\n applySchema(db);\n\n console.log(`[hookdash] Database ready at ${dbPath}`);\n return db;\n}\n\n/**\n * Apply schema and run migrations.\n */\nfunction applySchema(database: Database.Database): void {\n database.exec(SCHEMA_SQL);\n\n // Check current version\n const row = database.prepare(\n 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'\n ).get() as { version: number } | undefined;\n\n const currentVersion = row?.version ?? 0;\n\n if (currentVersion < CURRENT_SCHEMA_VERSION) {\n // Run migrations for each version\n runMigrations(database, currentVersion, CURRENT_SCHEMA_VERSION);\n\n // Update version\n database.prepare(\n 'INSERT OR REPLACE INTO schema_version (version) VALUES (?)'\n ).run(CURRENT_SCHEMA_VERSION);\n }\n}\n\n/**\n * Run incremental migrations between versions.\n */\nfunction runMigrations(\n database: Database.Database,\n _fromVersion: number,\n _toVersion: number\n): void {\n // v1 is the initial schema — no migrations needed yet.\n // Future migrations go here:\n //\n // if (fromVersion < 2) {\n // database.exec(`ALTER TABLE events ADD COLUMN size_bytes INTEGER`);\n // }\n}\n\n/**\n * Close the database connection.\n */\nexport function closeDatabase(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Seed the database with sources and endpoints from config.\n */\nexport function seedFromConfig(config: HookdashConfig): void {\n const database = getDatabase(config);\n\n const upsertSource = database.prepare(`\n INSERT INTO sources (id, name, provider, signing_secret, active)\n VALUES (lower(hex(randomblob(8))), ?, ?, ?, 1)\n ON CONFLICT(name) DO UPDATE SET\n provider = excluded.provider,\n signing_secret = excluded.signing_secret,\n active = 1\n `);\n\n const getSource = database.prepare('SELECT id FROM sources WHERE name = ?');\n\n const upsertEndpoint = database.prepare(`\n INSERT INTO endpoints (id, url, source_id, event_filter, headers, timeout_ms, max_retries)\n VALUES (lower(hex(randomblob(8))), ?, ?, ?, ?, ?, ?)\n ON CONFLICT DO NOTHING\n `);\n\n const checkEndpoint = database.prepare(\n 'SELECT id FROM endpoints WHERE url = ? AND source_id = ?'\n );\n\n const seedTransaction = database.transaction(() => {\n for (const source of config.sources) {\n upsertSource.run(source.name, source.provider, source.signing_secret ?? null);\n const sourceRow = getSource.get(source.name) as { id: string };\n\n for (const endpoint of source.endpoints) {\n const existing = checkEndpoint.get(endpoint.url, sourceRow.id);\n if (!existing) {\n upsertEndpoint.run(\n endpoint.url,\n sourceRow.id,\n endpoint.events ? JSON.stringify(endpoint.events) : null,\n endpoint.headers ? JSON.stringify(endpoint.headers) : null,\n endpoint.timeout_ms ?? config.delivery.default_timeout,\n endpoint.max_retries ?? config.delivery.default_max_retries,\n );\n }\n }\n }\n });\n\n seedTransaction();\n console.log(`[hookdash] Seeded ${config.sources.length} source(s) from config`);\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nconst TIMESTAMP_TOLERANCE_SEC = 300; // 5 minutes\n\nexport const stripeProvider: WebhookProvider = {\n name: 'stripe',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['stripe-signature'];\n if (!sigHeader) return false;\n\n // Parse \"t=timestamp,v1=signature\" format\n const parts = new Map<string, string>();\n for (const pair of sigHeader.split(',')) {\n const idx = pair.indexOf('=');\n if (idx === -1) continue;\n parts.set(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());\n }\n\n const timestamp = parts.get('t');\n const signature = parts.get('v1');\n if (!timestamp || !signature) return false;\n\n // Validate timestamp within tolerance\n const ts = parseInt(timestamp, 10);\n if (Number.isNaN(ts)) return false;\n\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - ts) > TIMESTAMP_TOLERANCE_SEC) return false;\n\n // Signed payload: \"${timestamp}.${rawBody}\"\n const signedPayload = `${timestamp}.${rawBody.toString('utf8')}`;\n const expectedSig = createHmac('sha256', secret)\n .update(signedPayload)\n .digest('hex');\n\n // Constant-time comparison\n const sigBuf = Buffer.from(signature, 'hex');\n const expectedBuf = Buffer.from(expectedSig, 'hex');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(_headers: Record<string, string>, body: unknown): string | null {\n if (body && typeof body === 'object' && 'type' in body) {\n const t = (body as Record<string, unknown>).type;\n return typeof t === 'string' ? t : null;\n }\n return null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const githubProvider: WebhookProvider = {\n name: 'github',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['x-hub-signature-256'];\n if (!sigHeader) return false;\n\n // Format: \"sha256=hexsignature\"\n const prefix = 'sha256=';\n if (!sigHeader.startsWith(prefix)) return false;\n\n const signature = sigHeader.slice(prefix.length);\n\n const expectedSig = createHmac('sha256', secret)\n .update(rawBody)\n .digest('hex');\n\n // Constant-time comparison\n const sigBuf = Buffer.from(signature, 'hex');\n const expectedBuf = Buffer.from(expectedSig, 'hex');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(headers: Record<string, string>, _body: unknown): string | null {\n return headers['x-github-event'] ?? null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const twilioProvider: WebhookProvider = {\n name: 'twilio',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['x-twilio-signature'];\n if (!sigHeader) return false;\n\n // HMAC-SHA1, base64 encoded\n const expectedSig = createHmac('sha1', secret)\n .update(rawBody)\n .digest('base64');\n\n // Constant-time comparison (compare as raw bytes from base64)\n const sigBuf = Buffer.from(sigHeader, 'base64');\n const expectedBuf = Buffer.from(expectedSig, 'base64');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(_headers: Record<string, string>, body: unknown): string | null {\n // Twilio doesn't have a standard event type header — try to extract from body\n if (body && typeof body === 'object') {\n const b = body as Record<string, unknown>;\n // Twilio status callbacks include an EventType or similar field\n if (typeof b.EventType === 'string') return b.EventType;\n if (typeof b.event_type === 'string') return b.event_type;\n if (typeof b.type === 'string') return b.type;\n }\n return null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const shopifyProvider: WebhookProvider = {\n name: 'shopify',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n const sigHeader = headers['x-shopify-hmac-sha256'];\n if (!sigHeader) return false;\n\n // HMAC-SHA256, base64 encoded\n const expectedSig = createHmac('sha256', secret)\n .update(rawBody)\n .digest('base64');\n\n // Constant-time comparison (compare raw bytes from base64)\n const sigBuf = Buffer.from(sigHeader, 'base64');\n const expectedBuf = Buffer.from(expectedSig, 'base64');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(headers: Record<string, string>, _body: unknown): string | null {\n return headers['x-shopify-topic'] ?? null;\n },\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { WebhookProvider } from '../types/index.js';\n\nexport const genericProvider: WebhookProvider = {\n name: 'generic',\n\n verify(secret: string, headers: Record<string, string>, rawBody: Buffer): boolean {\n // Check both common signature headers\n const sigHeader = headers['x-webhook-signature'] ?? headers['x-signature'];\n if (!sigHeader) return false;\n\n // HMAC-SHA256, hex encoded\n const expectedSig = createHmac('sha256', secret)\n .update(rawBody)\n .digest('hex');\n\n // Constant-time comparison\n const sigBuf = Buffer.from(sigHeader, 'hex');\n const expectedBuf = Buffer.from(expectedSig, 'hex');\n\n if (sigBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(sigBuf, expectedBuf);\n },\n\n extractEventType(_headers: Record<string, string>, body: unknown): string | null {\n if (body && typeof body === 'object') {\n const b = body as Record<string, unknown>;\n // Try common event type field names\n if (typeof b.event === 'string') return b.event;\n if (typeof b.type === 'string') return b.type;\n }\n return null;\n },\n};\n","import type { WebhookProvider } from '../types/index.js';\nimport { stripeProvider } from './stripe.js';\nimport { githubProvider } from './github.js';\nimport { twilioProvider } from './twilio.js';\nimport { shopifyProvider } from './shopify.js';\nimport { genericProvider } from './generic.js';\n\nconst providers = new Map<string, WebhookProvider>();\n\nfunction register(provider: WebhookProvider): void {\n providers.set(provider.name, provider);\n}\n\n// Register all built-in providers\nregister(stripeProvider);\nregister(githubProvider);\nregister(twilioProvider);\nregister(shopifyProvider);\nregister(genericProvider);\n\n/**\n * Retrieve a webhook provider by name.\n * @throws Error if the provider is not registered.\n */\nexport function getProvider(name: string): WebhookProvider {\n const provider = providers.get(name);\n if (!provider) {\n const available = [...providers.keys()].join(', ');\n throw new Error(\n `Unknown webhook provider \"${name}\". Available providers: ${available}`,\n );\n }\n return provider;\n}\n","/**\n * Normalize headers to lowercase keys.\n * HTTP headers are case-insensitive, so we standardize for consistent lookup.\n */\nexport function normalizeHeaders(\n headers: Record<string, string | string[] | undefined>,\n): Record<string, string> {\n const normalized: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return normalized;\n}\n\n/**\n * Safely parse a raw body as JSON and attempt to extract the event type.\n * Returns the parsed body and event type (if found).\n */\nexport function parseBodyEventType(rawBody: Buffer): {\n parsed: unknown;\n eventType: string | null;\n} {\n let parsed: unknown = null;\n let eventType: string | null = null;\n\n try {\n parsed = JSON.parse(rawBody.toString('utf8'));\n } catch {\n // Body is not JSON — that's fine, return null\n return { parsed: null, eventType: null };\n }\n\n if (parsed && typeof parsed === 'object') {\n const obj = parsed as Record<string, unknown>;\n // Try common event type field names in priority order\n for (const field of ['event', 'type', 'event_type', 'eventType', 'action']) {\n if (typeof obj[field] === 'string') {\n eventType = obj[field] as string;\n break;\n }\n }\n }\n\n return { parsed, eventType };\n}\n\n/**\n * Match an event type against a list of glob patterns.\n * Supported patterns:\n * - `*` → matches everything\n * - `order.*` → matches `order.created`, `order.updated`, etc.\n * - `push` → exact match\n * - `issues.*` → matches `issues.opened`, `issues.closed`\n *\n * Returns true if eventType matches ANY of the patterns, or if the\n * filter list is empty/null (meaning \"accept all\").\n */\nexport function matchesEventFilter(\n eventType: string | null,\n filterPatterns: string[] | null,\n): boolean {\n // No filter = accept everything\n if (!filterPatterns || filterPatterns.length === 0) return true;\n\n // No event type but filters exist — can't match\n if (!eventType) return false;\n\n return filterPatterns.some((pattern) => globMatch(pattern, eventType));\n}\n\n/**\n * Simple glob matching for webhook event types.\n * Converts glob pattern to regex:\n * `*` → `[^.]*` (single segment)\n * `**` → `.*` (any number of segments)\n * `.` → `\\.` (literal dot)\n */\nfunction globMatch(pattern: string, value: string): boolean {\n // Shortcut: universal wildcard\n if (pattern === '*' || pattern === '**') return true;\n\n // Escape regex specials except * and ?\n let regexStr = '';\n let i = 0;\n while (i < pattern.length) {\n const char = pattern[i];\n\n if (char === '*') {\n if (pattern[i + 1] === '*') {\n // ** matches across dots\n regexStr += '.*';\n i += 2;\n } else {\n // * matches within a single segment (no dots)\n regexStr += '[^.]*';\n i += 1;\n }\n } else if (char === '?') {\n regexStr += '[^.]';\n i += 1;\n } else if ('.+^${}()|[]\\\\'.includes(char)) {\n regexStr += `\\\\${char}`;\n i += 1;\n } else {\n regexStr += char;\n i += 1;\n }\n }\n\n const regex = new RegExp(`^${regexStr}$`);\n return regex.test(value);\n}\n","import type { Database } from 'better-sqlite3';\nimport type { CircuitState } from '../types/index.js';\n\nexport class CircuitBreaker {\n private db: Database;\n private failureThreshold = 5;\n private cooldownMs = 60000; // 60 seconds\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Check if we can deliver to this endpoint.\n * If state is open, checks if cooldown has expired to transition to half-open.\n */\n canDeliver(endpointId: string): boolean {\n const row = this.db.prepare(\n 'SELECT circuit_state, circuit_last_failure_at FROM endpoints WHERE id = ?'\n ).get(endpointId) as { circuit_state: CircuitState; circuit_last_failure_at: string | null } | undefined;\n\n if (!row) return false;\n\n if (row.circuit_state === 'closed' || row.circuit_state === 'half-open') {\n return true;\n }\n\n if (row.circuit_state === 'open' && row.circuit_last_failure_at) {\n const lastFailure = new Date(row.circuit_last_failure_at).getTime();\n const elapsed = Date.now() - lastFailure;\n\n if (elapsed >= this.cooldownMs) {\n // Transition to half-open to allow a probe request\n this.db.prepare(\n \"UPDATE endpoints SET circuit_state = 'half-open' WHERE id = ?\"\n ).run(endpointId);\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Reset circuit to closed on successful delivery.\n */\n recordSuccess(endpointId: string): void {\n this.db.prepare(`\n UPDATE endpoints\n SET circuit_state = 'closed',\n circuit_failure_count = 0,\n circuit_last_failure_at = NULL\n WHERE id = ?\n `).run(endpointId);\n }\n\n /**\n * Record failure. If consecutive failures exceed threshold, open the circuit.\n */\n recordFailure(endpointId: string): void {\n const row = this.db.prepare(\n 'SELECT circuit_failure_count, circuit_state FROM endpoints WHERE id = ?'\n ).get(endpointId) as { circuit_failure_count: number; circuit_state: CircuitState } | undefined;\n\n if (!row) return;\n\n const newFailureCount = row.circuit_failure_count + 1;\n const nowStr = new Date().toISOString();\n\n if (newFailureCount >= this.failureThreshold || row.circuit_state === 'half-open') {\n // Open the circuit\n this.db.prepare(`\n UPDATE endpoints\n SET circuit_state = 'open',\n circuit_failure_count = ?,\n circuit_last_failure_at = ?\n WHERE id = ?\n `).run(newFailureCount, nowStr, endpointId);\n } else {\n // Just increment failure count\n this.db.prepare(`\n UPDATE endpoints\n SET circuit_failure_count = ?,\n circuit_last_failure_at = ?\n WHERE id = ?\n `).run(newFailureCount, nowStr, endpointId);\n }\n }\n}\n","/**\n * Calculates exponential backoff delay with jitter.\n * Formula: min(BASE * 2^attempt + jitter, MAX_DELAY)\n */\nexport function getNextRetryDelay(attempt: number): number {\n const BASE_DELAY = 1000; // 1 second\n const MAX_DELAY = 3600_000; // 1 hour\n const JITTER_FACTOR = 0.25;\n\n const exponential = BASE_DELAY * Math.pow(2, attempt);\n const capped = Math.min(exponential, MAX_DELAY);\n const jitter = capped * JITTER_FACTOR * Math.random();\n\n return capped + jitter;\n}\n\n/**\n * Determines if a request should be retried based on status code.\n * Retry on 5xx server errors and network errors (e.g. status code 0 or -1).\n * Do NOT retry on 4xx client errors (unauthorized, bad request, etc.), except for 429 Too Many Requests.\n */\nexport function shouldRetry(statusCode: number): boolean {\n if (statusCode === 0 || statusCode === -1) return true; // network errors, timeouts\n if (statusCode === 429) return true; // rate limited, retry later\n if (statusCode >= 500 && statusCode < 600) return true; // server errors\n return false;\n}\n","import { EventEmitter } from 'node:events';\nimport type { Database } from 'better-sqlite3';\nimport type { HookdashConfig, Delivery, Endpoint, WebhookEvent } from '../types/index.js';\nimport { getDatabase } from '../db/connection.js';\nimport { CircuitBreaker } from './circuit-breaker.js';\nimport { getNextRetryDelay, shouldRetry } from './retry.js';\n\nexport const deliveryEvents = new EventEmitter();\n\nexport class DeliveryWorker {\n private db: Database;\n private intervalId: NodeJS.Timeout | null = null;\n private pollInterval: number;\n private defaultTimeout: number;\n private circuitBreaker: CircuitBreaker;\n private isProcessing = false;\n\n constructor(config: HookdashConfig) {\n this.db = getDatabase(config);\n this.pollInterval = config.delivery.poll_interval;\n this.defaultTimeout = config.delivery.default_timeout;\n this.circuitBreaker = new CircuitBreaker(this.db);\n }\n\n start(): void {\n if (this.intervalId) return;\n\n this.intervalId = setInterval(async () => {\n if (this.isProcessing) return;\n this.isProcessing = true;\n try {\n await this.processQueue();\n } catch (err) {\n console.error('[hookdash-worker] Error processing queue:', err);\n } finally {\n this.isProcessing = false;\n }\n }, this.pollInterval);\n\n console.log(`[hookdash-worker] Started queue worker (poll: ${this.pollInterval}ms)`);\n }\n\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n console.log('[hookdash-worker] Stopped queue worker');\n }\n }\n\n private async processQueue(): Promise<void> {\n const now = new Date().toISOString();\n\n // Select pending and retrying deliveries that are due\n const dueDeliveries = this.db.prepare(`\n SELECT d.*, e.url as endpoint_url, e.headers as endpoint_headers, e.timeout_ms as endpoint_timeout,\n e.max_retries as endpoint_max_retries, ev.body as event_body, ev.headers as event_headers,\n ev.content_type as event_content_type\n FROM deliveries d\n JOIN endpoints e ON d.endpoint_id = e.id\n JOIN events ev ON d.event_id = ev.id\n WHERE (d.status = 'pending' OR (d.status = 'retrying' AND d.next_retry_at <= ?))\n LIMIT 10\n `).all(now) as any[];\n\n if (dueDeliveries.length === 0) return;\n\n for (const d of dueDeliveries) {\n // 1. Check Circuit Breaker\n if (!this.circuitBreaker.canDeliver(d.endpoint_id)) {\n // Skip for now, check next time\n continue;\n }\n\n await this.deliver(d);\n }\n }\n\n private async deliver(d: any): Promise<void> {\n const endpointHeaders = d.endpoint_headers ? JSON.parse(d.endpoint_headers) : {};\n const eventHeaders = d.event_headers ? JSON.parse(d.event_headers) : {};\n\n // Build headers to send\n const headers = {\n 'content-type': d.event_content_type || 'application/json',\n ...eventHeaders,\n ...endpointHeaders,\n 'x-hookdash-delivery-id': d.id,\n 'x-hookdash-event-id': d.event_id,\n 'x-hookdash-attempt': (d.attempts + 1).toString(),\n };\n\n // Remove hop-by-hop or signature verification headers that don't apply,\n // but keep custom signing headers if needed.\n delete headers['connection'];\n delete headers['host'];\n delete headers['content-length'];\n\n const timeout = d.endpoint_timeout || this.defaultTimeout;\n const controller = new AbortController();\n const id = setTimeout(() => controller.abort(), timeout);\n\n const startTime = Date.now();\n let status: Delivery['status'] = 'failed';\n let statusCode: number | null = null;\n let responseBody = '';\n let errorMsg: string | null = null;\n\n try {\n const response = await fetch(d.endpoint_url, {\n method: 'POST',\n headers,\n body: d.event_body,\n signal: controller.signal,\n });\n\n statusCode = response.status;\n responseBody = await response.text();\n\n if (response.ok) {\n status = 'success';\n this.circuitBreaker.recordSuccess(d.endpoint_id);\n } else {\n status = 'failed';\n this.circuitBreaker.recordFailure(d.endpoint_id);\n }\n } catch (err: any) {\n this.circuitBreaker.recordFailure(d.endpoint_id);\n if (err.name === 'AbortError') {\n errorMsg = `Timeout after ${timeout}ms`;\n } else {\n errorMsg = err.message || 'Network error';\n }\n } finally {\n clearTimeout(id);\n }\n\n const attempts = d.attempts + 1;\n const completedAt = new Date().toISOString();\n let nextRetryAt: string | null = null;\n\n if (status === 'failed') {\n const maxRetries = d.endpoint_max_retries ?? 8;\n const shouldRetryRequest = shouldRetry(statusCode ?? -1);\n\n if (attempts < maxRetries && shouldRetryRequest) {\n status = 'retrying';\n const delay = getNextRetryDelay(attempts);\n nextRetryAt = new Date(Date.now() + delay).toISOString();\n } else {\n status = 'dead'; // Move to DLQ\n }\n }\n\n // Update database\n this.db.prepare(`\n UPDATE deliveries\n SET status = ?,\n attempts = ?,\n next_retry_at = ?,\n last_status_code = ?,\n last_response_body = ?,\n last_error = ?,\n completed_at = ?\n WHERE id = ?\n `).run(\n status,\n attempts,\n nextRetryAt,\n statusCode,\n responseBody.substring(0, 10000), // Cap payload response length\n errorMsg,\n completedAt,\n d.id\n );\n\n // Emit event for real-time dashboard updates\n deliveryEvents.emit('delivery:update', {\n id: d.id,\n event_id: d.event_id,\n endpoint_id: d.endpoint_id,\n endpoint_url: d.endpoint_url,\n status,\n attempts,\n next_retry_at: nextRetryAt,\n last_status_code: statusCode,\n last_error: errorMsg,\n completed_at: completedAt,\n });\n }\n}\n","import type { FastifyPluginAsync } from 'fastify';\nimport { deliveryEvents } from '../delivery/worker.js';\n\n// Global registry of active SSE connections\nconst activeClients = new Set<(event: string, data: any) => void>();\n\n/**\n * Broadcast an event to all active dashboard SSE connections.\n */\nexport function broadcastSSE(event: string, data: any): void {\n for (const send of activeClients) {\n send(event, data);\n }\n}\n\nexport const sseRoutes: FastifyPluginAsync = async (fastify) => {\n fastify.get('/stream', async (request, reply) => {\n // SSE headers\n reply.raw.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n });\n\n const send = (event: string, data: any) => {\n reply.raw.write(`event: ${event}\\n`);\n reply.raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n };\n\n activeClients.add(send);\n\n // Heartbeat every 15 seconds to keep connection alive\n const heartbeatId = setInterval(() => {\n reply.raw.write(': heartbeat\\n\\n');\n }, 15000);\n\n // Initial message\n send('connected', { timestamp: new Date().toISOString() });\n\n // Listen to delivery worker events\n const onDeliveryUpdate = (data: any) => {\n send('delivery:update', data);\n };\n\n deliveryEvents.on('delivery:update', onDeliveryUpdate);\n\n // Cleanup on disconnect\n request.raw.on('close', () => {\n clearInterval(heartbeatId);\n activeClients.delete(send);\n deliveryEvents.off('delivery:update', onDeliveryUpdate);\n });\n\n // Tell Fastify not to send standard response\n reply.hijack();\n });\n};\n","import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';\nimport { nanoid } from 'nanoid';\nimport { getDatabase } from '../db/connection.js';\nimport { getProvider } from '../providers/registry.js';\nimport { normalizeHeaders, parseBodyEventType, matchesEventFilter } from './parser.js';\nimport type { Source, Endpoint, SSEEvent } from '../types/index.js';\n\nimport { broadcastSSE } from '../api/sse.js';\n// ─── Route Params / Types ───────────────────────────────────────\ninterface WebhookParams {\n source: string;\n}\n\n// ─── Fastify Plugin ─────────────────────────────────────────────\nexport default async function ingestionRoutes(fastify: FastifyInstance): Promise<void> {\n fastify.post<{ Params: WebhookParams }>(\n '/webhook/:source',\n async (request: FastifyRequest<{ Params: WebhookParams }>, reply: FastifyReply) => {\n const { source: sourceName } = request.params;\n\n // ── 1. Get raw body ───────────────────────────────────────\n const rawBody = (request as FastifyRequest & { rawBody?: Buffer }).rawBody;\n if (!rawBody) {\n return reply.status(400).send({\n error: 'Missing raw body. Ensure @fastify/raw-body is registered.',\n });\n }\n\n // ── 2. Look up source by name from DB ─────────────────────\n const db = getDatabase();\n const source = db\n .prepare('SELECT * FROM sources WHERE name = ? AND active = 1')\n .get(sourceName) as Source | undefined;\n\n if (!source) {\n return reply.status(404).send({\n error: `Unknown webhook source \"${sourceName}\".`,\n });\n }\n\n // ── 3. Normalize headers ──────────────────────────────────\n const headers = normalizeHeaders(\n request.headers as Record<string, string | string[] | undefined>,\n );\n\n // ── 4. Parse body and extract event type ──────────────────\n const { parsed: parsedBody } = parseBodyEventType(rawBody);\n\n // ── 5. Verify signature ───────────────────────────────────\n let signatureValid: boolean | null = null;\n\n if (source.signing_secret) {\n try {\n const provider = getProvider(source.provider);\n signatureValid = provider.verify(source.signing_secret, headers, rawBody);\n\n if (!signatureValid) {\n return reply.status(401).send({ error: 'Invalid webhook signature.' });\n }\n } catch (err) {\n fastify.log.error(err, `Provider verification error for source \"${sourceName}\"`);\n return reply.status(500).send({ error: 'Signature verification failed.' });\n }\n }\n\n // ── 6. Extract event type via provider ────────────────────\n let eventType: string | null = null;\n try {\n const provider = getProvider(source.provider);\n eventType = provider.extractEventType(headers, parsedBody ?? rawBody.toString('utf8'));\n } catch {\n // Non-fatal — event type is optional\n }\n\n // ── 7. Store event ────────────────────────────────────────\n const eventId = nanoid();\n const contentType = headers['content-type'] ?? null;\n\n db.prepare(\n `INSERT INTO events (id, source_id, event_type, headers, body, content_type, signature_valid)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n eventId,\n source.id,\n eventType,\n JSON.stringify(headers),\n rawBody.toString('utf8'),\n contentType,\n signatureValid === null ? null : signatureValid ? 1 : 0,\n );\n\n // ── 8. Find matching endpoints and create deliveries ──────\n const endpoints = db\n .prepare(\n 'SELECT * FROM endpoints WHERE (source_id = ? OR source_id IS NULL) AND active = 1',\n )\n .all(source.id) as Endpoint[];\n\n const deliveryIds: string[] = [];\n\n const insertDelivery = db.prepare(\n `INSERT INTO deliveries (id, event_id, endpoint_id, status)\n VALUES (?, ?, ?, 'pending')`,\n );\n\n const createDeliveries = db.transaction(() => {\n for (const endpoint of endpoints) {\n // Check event filter\n let filterPatterns: string[] | null = null;\n if (endpoint.event_filter) {\n try {\n filterPatterns = JSON.parse(endpoint.event_filter) as string[];\n } catch {\n // Invalid filter JSON — skip filtering\n }\n }\n\n if (!matchesEventFilter(eventType, filterPatterns)) {\n continue;\n }\n\n const deliveryId = nanoid();\n insertDelivery.run(deliveryId, eventId, endpoint.id);\n deliveryIds.push(deliveryId);\n }\n });\n\n createDeliveries();\n\n // ── 9. Emit SSE event for real-time dashboard ─────────────\n broadcastSSE('event:new', {\n id: eventId,\n source_id: source.id,\n source_name: source.name,\n source_provider: source.provider,\n event_type: eventType,\n delivery_count: deliveryIds.length,\n received_at: new Date().toISOString(),\n });\n\n // ── 10. Return 202 Accepted ───────────────────────────────\n return reply.status(202).send({\n event_id: eventId,\n event_type: eventType,\n deliveries: deliveryIds.length,\n });\n },\n );\n}\n","import type { FastifyPluginAsync } from 'fastify';\nimport { nanoid } from 'nanoid';\nimport type { EventWithSource, PaginatedResponse, Delivery } from '../types/index.js';\n\nexport const eventRoutes: FastifyPluginAsync = async (fastify) => {\n const { db } = fastify;\n\n // GET /api/events\n fastify.get('/', async (request, reply) => {\n const query = request.query as any;\n const page = Math.max(1, parseInt(query.page) || 1);\n const perPage = Math.min(100, Math.max(1, parseInt(query.per_page) || 20));\n const offset = (page - 1) * perPage;\n\n const conditions: string[] = [];\n const params: any[] = [];\n\n if (query.source) {\n conditions.push('s.name = ?');\n params.push(query.source);\n }\n\n if (query.event_type) {\n conditions.push('e.event_type = ?');\n params.push(query.event_type);\n }\n\n if (query.status) {\n conditions.push(`EXISTS (\n SELECT 1 FROM deliveries d\n WHERE d.event_id = e.id AND d.status = ?\n )`);\n params.push(query.status);\n }\n\n if (query.from) {\n conditions.push('e.received_at >= ?');\n params.push(query.from);\n }\n\n if (query.to) {\n conditions.push('e.received_at <= ?');\n params.push(query.to);\n }\n\n if (query.search) {\n conditions.push('(e.body LIKE ? OR e.event_type LIKE ? OR e.id LIKE ?)');\n const wild = `%${query.search}%`;\n params.push(wild, wild, wild);\n }\n\n const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n\n // Get total count\n const totalRow = db.prepare(`\n SELECT COUNT(DISTINCT e.id) as count\n FROM events e\n JOIN sources s ON e.source_id = s.id\n ${whereClause}\n `).get(...params) as { count: number };\n\n const total = totalRow?.count || 0;\n\n // Get paginated events with delivery statistics\n const eventsQuery = `\n SELECT e.id, e.event_type, e.received_at, e.signature_valid,\n s.name as source_name, s.provider as source_provider,\n COUNT(d.id) as delivery_count,\n SUM(CASE WHEN d.status = 'success' THEN 1 ELSE 0 END) as success_count,\n SUM(CASE WHEN d.status IN ('failed', 'retrying', 'dead') THEN 1 ELSE 0 END) as failed_count\n FROM events e\n JOIN sources s ON e.source_id = s.id\n LEFT JOIN deliveries d ON d.event_id = e.id\n ${whereClause}\n GROUP BY e.id\n ORDER BY e.received_at DESC\n LIMIT ? OFFSET ?\n `;\n\n const events = db.prepare(eventsQuery).all(...params, perPage, offset) as EventWithSource[];\n\n const response: PaginatedResponse<EventWithSource> = {\n data: events,\n total,\n page,\n per_page: perPage,\n total_pages: Math.ceil(total / perPage),\n };\n\n return response;\n });\n\n // GET /api/events/:id\n fastify.get('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const event = db.prepare(`\n SELECT e.*, s.name as source_name, s.provider as source_provider\n FROM events e\n JOIN sources s ON e.source_id = s.id\n WHERE e.id = ?\n `).get(id) as any;\n\n if (!event) {\n return reply.status(404).send({ error: 'Event not found' });\n }\n\n const deliveries = db.prepare(`\n SELECT d.*, e.url as endpoint_url\n FROM deliveries d\n JOIN endpoints e ON d.endpoint_id = e.id\n WHERE d.event_id = ?\n ORDER BY d.created_at ASC\n `).all(id) as Delivery[];\n\n return {\n ...event,\n deliveries,\n };\n });\n\n // POST /api/events/:id/replay\n fastify.post('/:id/replay', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const event = db.prepare('SELECT id FROM events WHERE id = ?').get(id);\n if (!event) {\n return reply.status(404).send({ error: 'Event not found' });\n }\n\n // Get all endpoints configured for the source of this event\n const endpoints = db.prepare(`\n SELECT ep.id\n FROM endpoints ep\n JOIN events ev ON ev.source_id = ep.source_id\n WHERE ev.id = ? AND ep.active = 1\n `).all(id) as { id: string }[];\n\n const newDeliveryIds: string[] = [];\n\n const insertDelivery = db.prepare(`\n INSERT INTO deliveries (id, event_id, endpoint_id, status, attempts)\n VALUES (?, ?, ?, 'pending', 0)\n `);\n\n db.transaction(() => {\n for (const ep of endpoints) {\n const deliveryId = `del_${nanoid(12)}`;\n insertDelivery.run(deliveryId, id, ep.id);\n newDeliveryIds.push(deliveryId);\n }\n })();\n\n return {\n success: true,\n message: `Requeued ${newDeliveryIds.length} deliveries`,\n delivery_ids: newDeliveryIds,\n };\n });\n\n // DELETE /api/events/:id\n fastify.delete('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const result = db.prepare('DELETE FROM events WHERE id = ?').run(id);\n\n if (result.changes === 0) {\n return reply.status(404).send({ error: 'Event not found' });\n }\n\n return { success: true };\n });\n};\n","import type { FastifyPluginAsync } from 'fastify';\nimport { nanoid } from 'nanoid';\n\nexport const endpointRoutes: FastifyPluginAsync = async (fastify) => {\n const { db } = fastify;\n\n // GET /api/endpoints\n fastify.get('/', async (request, reply) => {\n const endpoints = db.prepare(`\n SELECT e.*, s.name as source_name, s.provider as source_provider\n FROM endpoints e\n LEFT JOIN sources s ON e.source_id = s.id\n ORDER BY e.created_at DESC\n `).all();\n\n return endpoints.map((ep: any) => ({\n ...ep,\n event_filter: ep.event_filter ? JSON.parse(ep.event_filter) : null,\n headers: ep.headers ? JSON.parse(ep.headers) : null,\n }));\n });\n\n // POST /api/endpoints\n fastify.post('/', async (request, reply) => {\n const body = request.body as any;\n\n if (!body.url) {\n return reply.status(400).send({ error: 'url is required' });\n }\n\n const id = `ep_${nanoid(12)}`;\n const eventFilter = body.event_filter ? JSON.stringify(body.event_filter) : null;\n const customHeaders = body.headers ? JSON.stringify(body.headers) : null;\n\n db.prepare(`\n INSERT INTO endpoints (\n id, url, source_id, event_filter, headers, timeout_ms, max_retries, active\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n id,\n body.url,\n body.source_id || null,\n eventFilter,\n customHeaders,\n body.timeout_ms ?? 30000,\n body.max_retries ?? 8,\n body.active !== undefined ? (body.active ? 1 : 0) : 1\n );\n\n const created = db.prepare('SELECT * FROM endpoints WHERE id = ?').get(id) as any;\n return {\n ...created,\n event_filter: created.event_filter ? JSON.parse(created.event_filter) : null,\n headers: created.headers ? JSON.parse(created.headers) : null,\n };\n });\n\n // PUT /api/endpoints/:id\n fastify.put('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const body = request.body as any;\n\n const existing = db.prepare('SELECT id FROM endpoints WHERE id = ?').get(id);\n if (!existing) {\n return reply.status(404).send({ error: 'Endpoint not found' });\n }\n\n const eventFilter = body.event_filter ? JSON.stringify(body.event_filter) : null;\n const customHeaders = body.headers ? JSON.stringify(body.headers) : null;\n\n db.prepare(`\n UPDATE endpoints\n SET url = COALESCE(?, url),\n source_id = COALESCE(?, source_id),\n event_filter = ?,\n headers = ?,\n timeout_ms = COALESCE(?, timeout_ms),\n max_retries = COALESCE(?, max_retries),\n active = COALESCE(?, active)\n WHERE id = ?\n `).run(\n body.url,\n body.source_id,\n eventFilter,\n customHeaders,\n body.timeout_ms,\n body.max_retries,\n body.active !== undefined ? (body.active ? 1 : 0) : null,\n id\n );\n\n const updated = db.prepare('SELECT * FROM endpoints WHERE id = ?').get(id) as any;\n return {\n ...updated,\n event_filter: updated.event_filter ? JSON.parse(updated.event_filter) : null,\n headers: updated.headers ? JSON.parse(updated.headers) : null,\n };\n });\n\n // DELETE /api/endpoints/:id\n fastify.delete('/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const result = db.prepare('DELETE FROM endpoints WHERE id = ?').run(id);\n\n if (result.changes === 0) {\n return reply.status(404).send({ error: 'Endpoint not found' });\n }\n\n return { success: true };\n });\n\n // POST /api/endpoints/:id/test\n fastify.post('/:id/test', async (request, reply) => {\n const { id } = request.params as { id: string };\n\n const ep = db.prepare('SELECT url, headers, timeout_ms FROM endpoints WHERE id = ?').get(id) as any;\n if (!ep) {\n return reply.status(404).send({ error: 'Endpoint not found' });\n }\n\n const customHeaders = ep.headers ? JSON.parse(ep.headers) : {};\n const timeout = ep.timeout_ms || 30000;\n\n const testPayload = {\n event: 'hookdash.test',\n timestamp: new Date().toISOString(),\n test: true,\n message: 'This is a test webhook from hookdash dashboard',\n };\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(ep.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-hookdash-test': 'true',\n ...customHeaders,\n },\n body: JSON.stringify(testPayload),\n signal: controller.signal,\n });\n\n const responseText = await response.text();\n\n return {\n success: response.ok,\n status_code: response.status,\n response: responseText.substring(0, 1000),\n };\n } catch (err: any) {\n return {\n success: false,\n error: err.name === 'AbortError' ? `Timeout after ${timeout}ms` : err.message,\n };\n } finally {\n clearTimeout(timer);\n }\n });\n};\n","import type { FastifyPluginAsync } from 'fastify';\nimport type { Stats } from '../types/index.js';\n\nexport const statsRoutes: FastifyPluginAsync = async (fastify) => {\n const { db } = fastify;\n\n // GET /api/stats\n fastify.get('/', async (request, reply) => {\n const todayStr = new Date().toISOString().split('T')[0] + 'T00:00:00.000Z';\n\n // 1. Total & Today event count\n const eventCounts = db.prepare(`\n SELECT\n COUNT(id) as total,\n SUM(CASE WHEN received_at >= ? THEN 1 ELSE 0 END) as today\n FROM events\n `).get(todayStr) as { total: number; today: number | null };\n\n const totalEvents = eventCounts?.total || 0;\n const eventsToday = eventCounts?.today || 0;\n\n // 2. Deliveries by status\n const deliveryStats = db.prepare(`\n SELECT\n status,\n COUNT(id) as count\n FROM deliveries\n GROUP BY status\n `).all() as { status: string; count: number }[];\n\n let totalDeliveries = 0;\n let successfulDeliveries = 0;\n let failedDeliveries = 0;\n let deadDeliveries = 0;\n let pendingDeliveries = 0;\n\n for (const row of deliveryStats) {\n totalDeliveries += row.count;\n if (row.status === 'success') {\n successfulDeliveries = row.count;\n } else if (row.status === 'failed' || row.status === 'retrying') {\n failedDeliveries += row.count;\n } else if (row.status === 'dead') {\n deadDeliveries = row.count;\n } else if (row.status === 'pending') {\n pendingDeliveries = row.count;\n }\n }\n\n const successRate = totalDeliveries > 0\n ? Math.round((successfulDeliveries / (successfulDeliveries + deadDeliveries + failedDeliveries || 1)) * 1000) / 10\n : 100.0;\n\n // 3. Events per hour (last 24 hours)\n const activeHours: { hour: string; count: number }[] = [];\n for (let i = 23; i >= 0; i--) {\n const d = new Date(Date.now() - i * 3600 * 1000);\n d.setMinutes(0, 0, 0);\n const isoStr = d.toISOString();\n // Format to YYYY-MM-DD HH:00\n const formatted = isoStr.replace('T', ' ').substring(0, 16);\n activeHours.push({ hour: formatted, count: 0 });\n }\n\n // Query events in last 24 hours\n const last24h = new Date(Date.now() - 24 * 3600 * 1000).toISOString();\n const dbHours = db.prepare(`\n SELECT strftime('%Y-%m-%d %H:00', received_at) as hour_bucket, COUNT(id) as count\n FROM events\n WHERE received_at >= ?\n GROUP BY hour_bucket\n ORDER BY hour_bucket ASC\n `).all(last24h) as { hour_bucket: string; count: number }[];\n\n for (const dbHour of dbHours) {\n const match = activeHours.find(h => h.hour === dbHour.hour_bucket);\n if (match) {\n match.count = dbHour.count;\n }\n }\n\n // 4. Top sources\n const topSources = db.prepare(`\n SELECT s.name, COUNT(e.id) as count\n FROM events e\n JOIN sources s ON e.source_id = s.id\n GROUP BY s.name\n ORDER BY count DESC\n LIMIT 5\n `).all() as { name: string; count: number }[];\n\n // 5. Top event types\n const topEventTypes = db.prepare(`\n SELECT event_type as type, COUNT(id) as count\n FROM events\n WHERE event_type IS NOT NULL\n GROUP BY event_type\n ORDER BY count DESC\n LIMIT 10\n `).all() as { type: string; count: number }[];\n\n const stats: Stats = {\n total_events: totalEvents,\n events_today: eventsToday,\n total_deliveries: totalDeliveries,\n successful_deliveries: successfulDeliveries,\n failed_deliveries: failedDeliveries,\n dead_deliveries: deadDeliveries,\n pending_deliveries: pendingDeliveries,\n success_rate: successRate,\n events_per_hour: activeHours,\n top_sources: topSources,\n top_event_types: topEventTypes,\n };\n\n return stats;\n });\n};\n","import type { FastifyPluginAsync } from 'fastify';\nimport { eventRoutes } from './events.js';\nimport { endpointRoutes } from './endpoints.js';\nimport { statsRoutes } from './stats.js';\nimport { sseRoutes } from './sse.js';\n\nexport const apiRoutes: FastifyPluginAsync = async (fastify) => {\n // Register sub-routes\n await fastify.register(eventRoutes, { prefix: '/events' });\n await fastify.register(endpointRoutes, { prefix: '/endpoints' });\n await fastify.register(statsRoutes, { prefix: '/stats' });\n await fastify.register(sseRoutes);\n\n // Health check route\n fastify.get('/health', async () => {\n return {\n status: 'ok',\n version: '0.1.0',\n uptime: process.uptime(),\n timestamp: new Date().toISOString(),\n };\n });\n};\n","import { Command } from 'commander';\nimport { loadConfig } from './config/loader.js';\nimport { createServer } from './server.js';\nimport { DeliveryWorker } from './delivery/worker.js';\nimport { closeDatabase } from './db/connection.js';\n\nconst program = new Command();\n\nprogram\n .name('hookdash')\n .description('Zero-config, self-hosted webhook gateway with a beautiful dashboard')\n .version('0.1.0');\n\nprogram\n .command('start')\n .description('Start the hookdash server')\n .option('-p, --port <port>', 'Server port', parseInt)\n .option('-h, --host <host>', 'Server host')\n .option('-c, --config <path>', 'Path to config file')\n .option('-d, --db <path>', 'Database file path')\n .action(async (options) => {\n try {\n console.log('');\n console.log(' ∿ hookdash v0.1.0');\n console.log(' ─────────────────────');\n console.log('');\n\n // Load config\n const config = loadConfig({\n configPath: options.config,\n port: options.port,\n host: options.host,\n dbPath: options.db,\n });\n\n // Create and start server\n const app = await createServer(config);\n\n // Start delivery worker\n const worker = new DeliveryWorker(config);\n worker.start();\n\n // Graceful shutdown\n const shutdown = async (signal: string) => {\n console.log(`\\n[hookdash] ${signal} received, shutting down...`);\n worker.stop();\n await app.close();\n closeDatabase();\n console.log('[hookdash] Goodbye! 👋');\n process.exit(0);\n };\n\n process.on('SIGINT', () => shutdown('SIGINT'));\n process.on('SIGTERM', () => shutdown('SIGTERM'));\n\n // Start listening\n await app.listen({\n port: config.server.port,\n host: config.server.host,\n });\n\n console.log('');\n console.log(` 🪝 Webhook endpoint: http://${config.server.host === '0.0.0.0' ? 'localhost' : config.server.host}:${config.server.port}/webhook/:source`);\n console.log(` 📊 Dashboard: http://${config.server.host === '0.0.0.0' ? 'localhost' : config.server.host}:${config.server.port}`);\n console.log(` 📡 API: http://${config.server.host === '0.0.0.0' ? 'localhost' : config.server.host}:${config.server.port}/api/health`);\n console.log('');\n\n if (config.sources.length === 0) {\n console.log(' ℹ No sources configured. Create a hookdash.config.yml to get started.');\n console.log(' See: https://github.com/hookdash/hookdash#configuration');\n console.log('');\n }\n } catch (err) {\n console.error('[hookdash] Failed to start:', (err as Error).message);\n process.exit(1);\n }\n });\n\nprogram\n .command('init')\n .description('Create an example hookdash.config.yml in the current directory')\n .action(async () => {\n const { writeFileSync, existsSync } = await import('node:fs');\n const configPath = 'hookdash.config.yml';\n\n if (existsSync(configPath)) {\n console.log(`[hookdash] ${configPath} already exists. Skipping.`);\n return;\n }\n\n const exampleConfig = `# hookdash configuration\n# Docs: https://github.com/hookdash/hookdash#configuration\n\nserver:\n port: 9090\n host: 0.0.0.0\n\ndatabase:\n path: ./hookdash.db\n\ndelivery:\n poll_interval: 1000 # ms between delivery polls\n default_timeout: 30000 # ms per delivery attempt\n default_max_retries: 8 # max retries before dead letter\n\nsources:\n # Stripe webhooks\n # - name: stripe\n # provider: stripe\n # signing_secret: \\${STRIPE_WEBHOOK_SECRET}\n # endpoints:\n # - url: http://localhost:3000/api/webhooks/stripe\n # events: [\"payment_intent.*\", \"charge.*\"]\n\n # GitHub webhooks\n # - name: github\n # provider: github\n # signing_secret: \\${GITHUB_WEBHOOK_SECRET}\n # endpoints:\n # - url: http://localhost:3000/api/webhooks/github\n\n # Generic webhooks (HMAC-SHA256)\n - name: my-service\n provider: generic\n signing_secret: my-secret-key\n endpoints:\n - url: http://localhost:3000/hooks\n`;\n\n writeFileSync(configPath, exampleConfig);\n console.log(`[hookdash] Created ${configPath}`);\n console.log('[hookdash] Edit it and run: hookdash start');\n });\n\n// Parse CLI args\nprogram.parse();\n","import { readFileSync, existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { configSchema } from './schema.js';\nimport type { HookdashConfig } from '../types/index.js';\n\n/**\n * Interpolate ${ENV_VAR} references in strings with actual env values.\n */\nfunction interpolateEnvVars(value: unknown): unknown {\n if (typeof value === 'string') {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envKey: string) => {\n const envValue = process.env[envKey.trim()];\n if (envValue === undefined) {\n console.warn(`[hookdash] Warning: Environment variable \"${envKey}\" is not set`);\n return '';\n }\n return envValue;\n });\n }\n if (Array.isArray(value)) {\n return value.map(interpolateEnvVars);\n }\n if (value !== null && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n result[k] = interpolateEnvVars(v);\n }\n return result;\n }\n return value;\n}\n\n/**\n * Search for config file in standard locations.\n */\nfunction findConfigFile(customPath?: string): string | null {\n if (customPath) {\n const resolved = resolve(customPath);\n if (existsSync(resolved)) return resolved;\n throw new Error(`Config file not found: ${resolved}`);\n }\n\n const candidates = [\n resolve('hookdash.config.yml'),\n resolve('hookdash.config.yaml'),\n resolve('.hookdash.yml'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) return candidate;\n }\n\n return null;\n}\n\n/**\n * Load and validate the hookdash configuration.\n * Priority: CLI args → config file → defaults\n */\nexport function loadConfig(options: {\n configPath?: string;\n port?: number;\n host?: string;\n dbPath?: string;\n} = {}): HookdashConfig {\n let rawConfig: Record<string, unknown> = {};\n\n // Load from config file if found\n const configFile = findConfigFile(options.configPath);\n if (configFile) {\n const content = readFileSync(configFile, 'utf-8');\n const parsed = parseYaml(content);\n if (parsed && typeof parsed === 'object') {\n rawConfig = interpolateEnvVars(parsed) as Record<string, unknown>;\n }\n console.log(`[hookdash] Loaded config from ${configFile}`);\n }\n\n // CLI overrides\n if (options.port) {\n rawConfig.server = { ...(rawConfig.server as object || {}), port: options.port };\n }\n if (options.host) {\n rawConfig.server = { ...(rawConfig.server as object || {}), host: options.host };\n }\n if (options.dbPath) {\n rawConfig.database = { ...(rawConfig.database as object || {}), path: options.dbPath };\n }\n\n // Validate\n const result = configSchema.safeParse(rawConfig);\n if (!result.success) {\n const errors = result.error.errors.map(e => ` - ${e.path.join('.')}: ${e.message}`).join('\\n');\n throw new Error(`Invalid configuration:\\n${errors}`);\n }\n\n return result.data as HookdashConfig;\n}\n","import { z } from 'zod';\n\nconst endpointConfigSchema = z.object({\n url: z.string().url(),\n events: z.array(z.string()).optional(),\n headers: z.record(z.string()).optional(),\n timeout_ms: z.number().int().positive().optional(),\n max_retries: z.number().int().min(0).max(50).optional(),\n});\n\nconst sourceConfigSchema = z.object({\n name: z.string().min(1).regex(/^[a-z0-9-]+$/, 'Source name must be lowercase alphanumeric with hyphens'),\n provider: z.enum(['stripe', 'github', 'twilio', 'shopify', 'generic']).default('generic'),\n signing_secret: z.string().optional(),\n endpoints: z.array(endpointConfigSchema).min(1),\n});\n\nexport const configSchema = z.object({\n server: z.object({\n port: z.number().int().min(1).max(65535).default(9090),\n host: z.string().default('0.0.0.0'),\n }).default({}),\n\n database: z.object({\n path: z.string().default('./hookdash.db'),\n }).default({}),\n\n delivery: z.object({\n poll_interval: z.number().int().min(100).max(60000).default(1000),\n default_timeout: z.number().int().min(1000).max(120000).default(30000),\n default_max_retries: z.number().int().min(0).max(50).default(8),\n }).default({}),\n\n sources: z.array(sourceConfigSchema).default([]),\n});\n\nexport type ValidatedConfig = z.infer<typeof configSchema>;\n","import Fastify from 'fastify';\nimport fastifyCors from '@fastify/cors';\nimport fastifyStatic from '@fastify/static';\nimport fastifyRawBody from 'fastify-raw-body';\nimport { resolve, dirname } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { HookdashConfig } from './types/index.js';\nimport { getDatabase, seedFromConfig } from './db/connection.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport async function createServer(config: HookdashConfig) {\n const app = Fastify({\n logger: {\n transport: {\n target: 'pino-pretty',\n options: {\n translateTime: 'HH:MM:ss',\n ignore: 'pid,hostname',\n },\n },\n },\n bodyLimit: 10 * 1024 * 1024, // 10MB max webhook payload\n });\n\n // ─── Plugins ──────────────────────────────────────────\n await app.register(fastifyCors, {\n origin: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE'],\n });\n\n await app.register(fastifyRawBody, {\n field: 'rawBody',\n global: true,\n encoding: false, // Return Buffer\n runFirst: true,\n });\n\n // ─── Database ─────────────────────────────────────────\n const db = getDatabase(config);\n seedFromConfig(config);\n\n // Make config and db accessible via decorators\n app.decorate('hookdashConfig', config);\n app.decorate('db', db);\n\n // ─── API Routes ───────────────────────────────────────\n // These are registered as Fastify plugins\n const ingestionRoutes = (await import('./ingestion/routes.js')).default;\n await app.register(ingestionRoutes);\n\n const { apiRoutes } = await import('./api/routes.js');\n await app.register(apiRoutes, { prefix: '/api' });\n\n // ─── Dashboard Static Files ───────────────────────────\n // Try multiple possible dashboard locations\n const dashboardPaths = [\n resolve(__dirname, '../dashboard/dist'), // Dev: source tree\n resolve(__dirname, '../../dashboard/dist'), // Built: dist/\n resolve(dirname(__dirname), 'dashboard/dist'), // npm package\n ];\n\n let dashboardPath: string | null = null;\n for (const p of dashboardPaths) {\n if (existsSync(p)) {\n dashboardPath = p;\n break;\n }\n }\n\n if (dashboardPath) {\n await app.register(fastifyStatic, {\n root: dashboardPath,\n prefix: '/',\n wildcard: true,\n });\n\n // SPA fallback: serve index.html for all non-API, non-webhook routes\n app.setNotFoundHandler((request, reply) => {\n if (request.url.startsWith('/api/') || request.url.startsWith('/webhook/')) {\n return reply.status(404).send({ error: 'Not found' });\n }\n return reply.sendFile('index.html');\n });\n\n console.log(`[hookdash] Dashboard serving from ${dashboardPath}`);\n } else {\n console.log('[hookdash] Dashboard not found (run `npm run build:dashboard` to enable)');\n\n // Basic fallback page\n app.get('/', async (_request, reply) => {\n return reply.type('text/html').send(`\n <!DOCTYPE html>\n <html>\n <head><title>hookdash</title></head>\n <body style=\"font-family:system-ui;background:#0a0a0f;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0\">\n <div style=\"text-align:center\">\n <h1>∿ hookdash</h1>\n <p style=\"color:#888\">Dashboard not built. Run <code style=\"background:#1e1e2e;padding:2px 8px;border-radius:4px\">npm run build:dashboard</code></p>\n <p style=\"color:#888;margin-top:20px\">API available at <a href=\"/api/health\" style=\"color:#3b82f6\">/api/health</a></p>\n </div>\n </body>\n </html>\n `);\n });\n }\n\n return app;\n}\n\n// ─── Fastify Type Augmentation ──────────────────────────\ndeclare module 'fastify' {\n interface FastifyInstance {\n hookdashConfig: HookdashConfig;\n db: import('better-sqlite3').Database;\n }\n}\n"],"mappings":"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","names":["resolve","createHmac","timingSafeEqual","createHmac","timingSafeEqual","createHmac","timingSafeEqual","createHmac","timingSafeEqual","db","db","nanoid","db","nanoid","db","db","routes_exports","init_routes","resolve","dirname","existsSync","db","ingestionRoutes","apiRoutes","existsSync"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hookdash",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Zero-config, self-hosted webhook gateway with a beautiful dashboard. Receive, queue, retry, and monitor webhooks.",
|
|
5
5
|
"keywords": ["webhook", "gateway", "self-hosted", "dashboard", "retry", "queue", "stripe", "github"],
|
|
6
6
|
"license": "MIT",
|