Skip to content

Outbound webhooks on @profullstack/webhooks 0.2.0 (0.4.0) - #142

Merged
ralyodio merged 1 commit into
masterfrom
feat/profullstack-webhooks
Oct 28, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/profullstack-webhooks

Conversation

@ralyodio

Copy link
Copy Markdown
Collaborator

Moves moshcoding's outbound webhooks onto the shared @profullstack/webhooks hub.

What changes

  • Store: lib/webhook-store.ts implements the package's WebhookStore on the app's own libSQL-shaped client (db(): Postgres through libsql-pg in production, a file: DB locally and in tests). Tables are the package DDL with a hub_ prefix, created in initSchema.
  • Hub: lib/webhooks.ts builds one Webhooks({ store, source, eventTypes }); instrumentation.ts calls hooks().start() once at boot.
  • Events emitted (owner dn:<domain>, subject = domain): waitlist.signup, bid.placed, bid.won, bid.accepted, and inbound.* relays from /api/webhooks/<dn>. The x-moshcoding-hop relay header and moshcoding-webhooks/1 user agent are kept.
  • API: /api/v1/webhooks?dn=<domain> mounts webhooksApi behind the session or an API key (resolveAccountIdOrToken); the account must own the domain (401 without auth, 403 for someone else's domain).
  • Dashboard: Webhooks tab gains an event filter on add, a per-target Test, Rotate (new secret shown once), and a recent-deliveries log.
  • Project webhooks: /api/projects/:id/webhooks* answer with the same shapes as before; the test route sends webhook.test to 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 whole whsec_... string as the HMAC key, while the package base64-decodes a whsec_ secret, so each stored secret becomes whsec_ + 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 via TEST_PG_URL. tsc --noEmit and next build are clean.

🤖 Generated with Claude Code

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>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​webhooks@​0.2.07610010090100

View full report

Comment thread lib/db.ts
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)] });
Comment thread lib/webhook-store.ts
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 = ?`,
Comment thread lib/webhook-store.ts

return {
async insertEndpoint(e) {
await run(`INSERT INTO ${HUB_ENDPOINTS} (${ENDPOINT_COLS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
Comment thread lib/webhook-store.ts
]);
},
async getEndpoint(id) {
return toEndpoint((await all(`SELECT ${ENDPOINT_COLS} FROM ${HUB_ENDPOINTS} WHERE id = ?`, [id]))[0]);
Comment thread lib/webhook-store.ts
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]))
Comment thread lib/webhook-store.ts
}
},
async getDelivery(id) {
return toDelivery((await all(`SELECT ${DELIVERY_COLS} FROM ${HUB_DELIVERIES} WHERE id = ?`, [id]))[0]);
Comment thread lib/webhook-store.ts
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]))
Comment thread lib/webhook-store.ts
*/
async claimDue(now, limit, leaseMs) {
const rows = await all(
`UPDATE ${HUB_DELIVERIES} SET next_at = ?
Comment thread lib/webhook-store.ts
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]);
Comment thread lib/webhooks.ts
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`,
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

30 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 26 | LOW: 2

Severity Rule Location
HIGH sh-remote-script-execution public/install.sh:302
HIGH sh-remote-script-execution public/install.sh:305
MEDIUM js-open-redirect app/dashboard/[[...tab]]/page.tsx:67
MEDIUM js-open-redirect app/signup/page.tsx:46
MEDIUM js-unescaped-html-sink components/PitFooter.tsx:30
MEDIUM js-open-redirect components/PitSearch.tsx:118
MEDIUM js-unescaped-html-sink components/Tenant.tsx:29
MEDIUM js-unescaped-html-sink components/Tenant.tsx:30
MEDIUM js-unescaped-html-sink components/Tenant.tsx:78
MEDIUM js-unescaped-html-sink components/Tenant.tsx:150
MEDIUM sql-template-interpolation lib/db.ts:590
MEDIUM sql-template-interpolation lib/db.ts:1232
MEDIUM redos-nested-quantifier lib/markdown.ts:109
MEDIUM sql-template-interpolation lib/webhook-store.ts:95
MEDIUM sql-template-interpolation lib/webhook-store.ts:102
MEDIUM sql-template-interpolation lib/webhook-store.ts:108
MEDIUM sql-template-interpolation lib/webhook-store.ts:111
MEDIUM sql-template-interpolation lib/webhook-store.ts:116
MEDIUM sql-template-interpolation lib/webhook-store.ts:117
MEDIUM sql-template-interpolation lib/webhook-store.ts:121
MEDIUM sql-template-interpolation lib/webhook-store.ts:128
MEDIUM sql-template-interpolation lib/webhook-store.ts:133
MEDIUM sql-template-interpolation lib/webhook-store.ts:144
MEDIUM sql-template-interpolation lib/webhook-store.ts:154
MEDIUM sql-template-interpolation lib/webhooks.ts:207
MEDIUM sh-remote-script-execution public/install.sh:123
MEDIUM sh-remote-script-execution public/install.sh:127
MEDIUM sh-remote-script-execution public/install.sh:146
LOW secret-generic-credential tests/signup-profile.test.mjs:28
LOW secret-generic-credential tests/signup-profile.test.mjs:39

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 7cfefd1 into master Oct 28, 2026
5 checks passed
@ralyodio
ralyodio deleted the feat/profullstack-webhooks branch October 28, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants