Repository navigation
feat(activitypub): double-knock delivery signatures and per-host circuit breaker - #574
Merged
Merged
Conversation
Sign each outbound inbox POST with RFC 9421 first and, when the peer answers 400/401/403, re-sign the same body with draft-cavage and send once more. Hosts that only accept draft-cavage are remembered in the actor DO's SQLite (`sig_pref`) for 7 days so later deliveries skip the first knock. Outbound GETs stay draft-cavage. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj
davidwkeith
commented
Oct 27, 2026
davidwkeith
left a comment
Owner
Author
There was a problem hiding this comment.
Review
CONTRIBUTING.md conformance: title is valid Conventional Commits, the PR body keeps the template headings, the changeset is present (minor, appropriate for a new export), the spec is updated, and tests are colocated. The unchecked catalog item has a reason. I haven't run the local gate (CI is still in progress), so I'm relying on the stated results.
Suggestions (non-blocking):
packages/activitypub/src/delivery.ts:KNOCK_AGAIN_STATUSESincludes403. A peer that blocks our domain or actor also answers 403, so every delivery to that peer is sent twice, and the second knock will also fail. The fallback is probably still worth it for peers that use 403 for signature failures. Consider a short comment saying the doubled traffic to blocking peers is accepted, or record the failure so the next knock is skipped.delivery.ts:deliverActivitynow takes 7 positional parameters, withpreferenceadded aftertimeoutMs. Callers that only want to pass a preference must also supplytimeoutMs. Moving the optional trailing arguments into an options object would be cleaner. This is non-blocking, but a public signature is easier to change before release.packages/activitypub/src/object.ts: theimport type { SignatureProfile } from "@dwk/http-signatures"sits between the./as2.jsimport and the@dwk/logimport. Move it to the other package imports for consistency.object.ts:sig_prefrows are only written when the second knock succeeds andpreference !== "cavage". After the 7-day TTL expires, a draft-cavage-only peer costs one wasted RFC 9421 knock per actor DO, which is acceptable. A test for the expiry path (a row older than 7 days is ignored and pruned) would lock that behaviour in, if one doesn't already exist.
No blocking issues found. Please wait for CI to go green and resolve any threads before merging.
Generated by Claude Code
Move deliverActivity's optional trailing arguments into a DeliveryOptions object, document the accepted 403 double-traffic trade-off, regroup the type import, and test the sig_pref TTL expiry and prune path. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj
Open a host's delivery circuit after 5 consecutive retryable failures and defer its rows (no retry attempt spent) for a cooldown that starts at 10 minutes and doubles per failed half-open probe, capped at 24 h. A success, a non-retryable answer, or a signed inbound activity from the host closes it; after 7 days of failure, rows reaching an open circuit are dropped. State lives in the actor DO's SQLite. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj
4 of 5 tasks
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.
Summary
Two delivery-hardening patterns from SiliconBeest and Fedify. State for both lives in the actor Durable Object's SQLite, not KV, per the consistency rules.
Double-knock signatures
@method @target-uri content-digest content-type,created,rsa-v1_5-sha256), using the newsignRequestRfc9421, which sits on@dwk/http-signatures.400/401/403, the same body is re-signed with draft-cavage and sent once more. Other statuses (404/410/422/5xx) never trigger a second knock. A peer that blocks us with403gets two requests per delivery. This is accepted and documented next toKNOCK_AGAIN_STATUSES.sig_preftable. For 7 days after that, deliveries to the host skip the RFC 9421 knock. The row then expires and is pruned, so a peer that adds RFC 9421 support later gets found again.Outbound
GETs (authorized-fetch key resolution) stay on draft-cavage. API changes:deliverActivity's optional trailingnow/timeoutMsarguments move into aDeliveryOptionsobject, which also carriespreference.DeliveryResultnow includes the acceptedprofileon success.Per-host circuit breaker (
peer_circuit)5xx,408,429), counted across all of its delivery rows, open the circuit for 10 minutes. While it is open, that host's due rows are deferred to the end of the cooldown and are not charged a retry attempt, so one dead peer no longer spends each alarm wake on 10 s timeouts.activitypub.delivery.deferred,activitypub.delivery.circuit_opened,activitypub.delivery.circuit_closed. A drop is logged asdelivery.failedwithreason: "circuit_open".Packages affected
@dwk/activitypub (plus updated assertions in @dwk/server's activitypub integration tests)
Checklist
spec/packages/and updated them ifbehaviour changed:
spec/packages/activitypub.mdS2S section (both features)src/*.test.ts). Double-knock: fallback on 401, skipping the first knock for a known draft-cavage peer, no second knock on 404/410/422/500, a round trip through our own RFC 9421 verifier, and the preference stored, used, then expired. Circuit breaker: opens after 5 failures with the remaining rows deferred and not charged attempts, a failed probe doubles the cooldown, a successful probe delivers the backlog, a 7-day-dead host's rows are dropped, and an inbound signed activity closes the circuit.pnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm test(3284 passed)pnpm changeset) if this touches a publishablepackage (one per feature)
catalog.json/conformance/status.jsonif this adds a newmountable worker or changes conformance status: not applicable, no new worker and no change to conformance status
🤖 Generated with Claude Code
https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj