feat(plugins): add the webhooks plugin - #263
Merged
Merged
Conversation
A handler was called with the value and the hook's context and nothing else, so a plugin could filter a view or observe an event in memory but could not record, enqueue or notify. Reacting to board activity — the most obvious thing a plugin author expects to do — was not possible: plugins/dues works around it by declaring no hooks at all. Handlers now take a third argument, a function resolving this plugin's runtime context: the same settings, logger, data, grants, users and notify a task or route is handed, with the same limits. It is a function because hooks are the hot path and a handler that never calls it costs nothing, and because acquiring it can fail on a fixture-mode board, where it rejects rather than pretending. Within one call it resolves once. Additive: a two-argument handler still compiles and runs, so this is a minor rather than a break. Callers driving a handler directly in a test pass unavailableHookRuntime(reason). Extracting the app's runtime builder into plugin-runtime.ts keeps the host off the page-rendering graph, and deferring the notification service's import breaks the notifications -> plugin-view -> plugin-host cycle that reaching for it would otherwise create. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22SRRgkUV7doefmw9NMNf
The first of the seeded marketplace cohort: new threads and replies delivered to an endpoint of your choosing, with a Discord-compatible preset. A thread.created or post.created hook writes one row into the plugin's own queue and returns; a task drains that queue every minute. Nothing is sent from the request, so an unreachable endpoint never delays a member's post. Deliveries retry on a timeout, a refused connection, a 5xx, a 408 or a 429, backing off from 30 seconds to an hour over six attempts, and give up immediately on any other 4xx, which retrying cannot fix. Plain-JSON deliveries are signed HMAC-SHA256 over timestamp and body, so a receiver can verify the delivery came from this board and cannot be handed a captured one twice. A plain-http endpoint is refused rather than sending the board's content over it. The payloads carry no English of ours — a Discord embed uses the thread's own subject and link — so the plugin adds nothing to the translation surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22SRRgkUV7doefmw9NMNf
jouwdan
changed the base branch from
claude/mei-115-xt6308-hook-runtime
to
main
August 26, 2026 21:00
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.
Part of MEI-115 — the first of the approved first-cohort plugins.
What it does
New threads and replies, posted to an endpoint of your choosing. A Discord channel webhook URL works as-is; anything else can take plain, signed JSON.
A
thread.created/post.createdhook writes one row into the plugin's own queue and returns. A task drains that queue every minute. Nothing is sent from the request itself, so a slow or unreachable endpoint never delays a member's post — which is the whole reason the hook needs runtime access rather than doing the HTTP call inline.Decisions worth reviewing
HMAC-SHA256(secret, "<timestamp>.<body>")over the raw body, with the timestamp bound into the signature so a captured delivery cannot be replayed. Verified in constant time. The secret has an env override, so it need not live in the database.http://endpoint is refused rather than sending the board's content over it — surfaced on the status page as its own message, not silently dropped.i18n:checkstays clean without a baseline entry (unlikeplugins/dues, which banked nine).Validated
pnpm verifyfully green: 7997 tests, 457 files — 49 of them new here.fetch: delivered, retried-with-reason, given-up, attempts-exhausted, no-response, and both refusal paths (no endpoint, plain http) assertingfetchis never called and the queue is never touched.pnpm marketplace:genre-run;release:checkpasses with 7 first-party listings. Both docs pages updated.🤖 Generated with Claude Code
https://claude.ai/code/session_01G22SRRgkUV7doefmw9NMNf
Generated by Claude Code