Repository navigation
Outbound webhooks on @profullstack/webhooks 0.2.0 (0.4.0) - #142
Merged
Merged
Conversation
Domain and project webhooks now go through the shared hub: CloudEvents bodies, Standard Webhooks signatures, a durable queue with retries and a delivery log, drained by hooks.start() at server boot (instrumentation.ts). - lib/webhook-store.ts: a WebhookStore on the app's libSQL-shaped client (Postgres via libsql-pg in prod, file: in tests), hub_ tables from the package's DDL. - Existing endpoints are copied once into the hub with their ids, URLs, subscriptions and on/off state. The old sender keyed the HMAC with the whole secret string; the stored secret becomes whsec_ + base64 of it, the same key, so receivers verify unchanged (tested). - Domain events waitlist.signup, bid.placed/won/accepted and inbound.* relays are emitted via hooks.emit; the x-moshcoding-hop header is kept. - /api/v1/webhooks?dn= mounts webhooksApi behind session or API key, scoped to a domain the account owns. - Dashboard Webhooks tab: event filter on add, per-target test, rotate, delivery log. Project webhook routes answer as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
| VALUES (?, ?, ?, ?, ?, '', ?, 0, NULL, ?, ?) ON CONFLICT (id) DO NOTHING`, | ||
| args: [String(r.id), src.owner(r), String(r.url), legacySecretToSpec(String(r.secret)), events, Number(r.active) === 1 ? 1 : 0, at, at], | ||
| }); | ||
| await d.execute({ sql: `UPDATE ${src.table} SET hub_migrated_at = ? WHERE id = ?`, args: [sqlTime(), String(r.id)] }); |
| const keys = Object.keys(patch).filter((k) => fields[k] && patch[k] !== undefined); | ||
| if (!keys.length) return; | ||
| await run( | ||
| `UPDATE ${table} SET ${keys.map((k) => `${fields[k]} = ?`).join(", ")} WHERE id = ?`, |
|
|
||
| return { | ||
| async insertEndpoint(e) { | ||
| await run(`INSERT INTO ${HUB_ENDPOINTS} (${ENDPOINT_COLS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ |
| ]); | ||
| }, | ||
| async getEndpoint(id) { | ||
| return toEndpoint((await all(`SELECT ${ENDPOINT_COLS} FROM ${HUB_ENDPOINTS} WHERE id = ?`, [id]))[0]); |
| return toEndpoint((await all(`SELECT ${ENDPOINT_COLS} FROM ${HUB_ENDPOINTS} WHERE id = ?`, [id]))[0]); | ||
| }, | ||
| async listEndpoints(owner) { | ||
| return (await all(`SELECT ${ENDPOINT_COLS} FROM ${HUB_ENDPOINTS} WHERE owner = ? ORDER BY created_at, id`, [owner])) |
| } | ||
| }, | ||
| async getDelivery(id) { | ||
| return toDelivery((await all(`SELECT ${DELIVERY_COLS} FROM ${HUB_DELIVERIES} WHERE id = ?`, [id]))[0]); |
| async listDeliveries({ owner, endpointId, limit = 50 }) { | ||
| const where = endpointId ? "owner = ? AND endpoint_id = ?" : "owner = ?"; | ||
| const args = endpointId ? [owner, endpointId] : [owner]; | ||
| return (await all(`SELECT ${DELIVERY_COLS} FROM ${HUB_DELIVERIES} WHERE ${where} ORDER BY created_at DESC, id DESC LIMIT ?`, [...args, limit])) |
| */ | ||
| async claimDue(now, limit, leaseMs) { | ||
| const rows = await all( | ||
| `UPDATE ${HUB_DELIVERIES} SET next_at = ? |
| return rows.map((r) => toDelivery(r)!).sort((a, b) => a.createdAt - b.createdAt); | ||
| }, | ||
| async prune(before) { | ||
| await run(`DELETE FROM ${HUB_DELIVERIES} WHERE status <> 'pending' AND updated_at < ?`, [before]); |
| await ensureSchema(); | ||
| const placeholders = projectIds.map(() => "?").join(","); | ||
| const r = await db().execute({ | ||
| sql: `SELECT url, MAX(created_at) AS latest FROM ${HUB_ENDPOINTS} WHERE owner IN (${placeholders}) GROUP BY url ORDER BY latest DESC LIMIT 25`, |
ThreatCrush Security Scan30 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 26 | LOW: 2
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Moves moshcoding's outbound webhooks onto the shared
@profullstack/webhookshub.What changes
lib/webhook-store.tsimplements the package'sWebhookStoreon the app's own libSQL-shaped client (db(): Postgres through libsql-pg in production, afile:DB locally and in tests). Tables are the package DDL with ahub_prefix, created ininitSchema.lib/webhooks.tsbuilds oneWebhooks({ store, source, eventTypes });instrumentation.tscallshooks().start()once at boot.dn:<domain>, subject = domain):waitlist.signup,bid.placed,bid.won,bid.accepted, andinbound.*relays from/api/webhooks/<dn>. Thex-moshcoding-hoprelay header andmoshcoding-webhooks/1user agent are kept./api/v1/webhooks?dn=<domain>mountswebhooksApibehind the session or an API key (resolveAccountIdOrToken); the account must own the domain (401 without auth, 403 for someone else's domain)./api/projects/:id/webhooks*answer with the same shapes as before; the test route sendswebhook.testto every enabled endpoint.Existing endpoints
Copied once into the hub tables (old rows stamped
hub_migrated_at, so a target deleted later is not resurrected), keeping their ids, URLs, subscriptions and active state. The old sender used the wholewhsec_...string as the HMAC key, while the package base64-decodes awhsec_secret, so each stored secret becomeswhsec_+ base64(old string): the same key bytes. A receiver holding the old secret verifies unchanged (tests/webhook-hub-legacy.test.mjs), and the newly shown form also verifies with any Standard Webhooks library.Body shape changes from
{id,type,dn,data,created_at}to a CloudEvent ({specversion,id,type,source,subject,time,data}). Production has 2 project and 6 domain endpoints, all probes or test URLs.Tests
bun test tests/: 283 pass. The hub tests also pass on Postgres 16 viaTEST_PG_URL.tsc --noEmitandnext buildare clean.🤖 Generated with Claude Code