Simplify dashboard to kind-only colors; add comment sync from 4 origins
Dashboard: removed the separate "source"/"replicated" cell colors and
legend entries. Cells now always color by event kind (commit, issue,
patch, comment) — the fact that a side holds content without a
graft-logged push of its own (Radicle gossip, an Authorized Integration,
or simply being the origin) is now a tooltip note only, never a visual
category of its own.
New "comment" kind, tracked from four origins, each recorded in a new
activity_log.origin column and shown as a tooltip note (never a color):
- Forgejo <-> Radicle, native: internal/sync/comments.go polls both
sides' comments each pass and cross-mirrors new ones, prefixed
"**via Forgejo:**" / "**via Radicle:**". Dedup via a new comment_seen
table (content hash, scoped per repo_pair+kind+item) plus a prefix
guard against re-mirroring graft's own output — verified live: a
native Forgejo comment mirrored to Radicl<e exactly once, confirmed
stable (no duplicate) across multiple subsequent sync passes.
- ActivityPub: handleReply now logs its existing comment bridge as a
dashboard activity (previously invisible on the dashboard).
- AT Proto: new inbound reply polling (pollBlueskyReplies in
cmd/sync/bluesky_replies.go), using app.bsky.notification.listNotifications.
NOT verified against a live account — none is configured in this
deployment; built directly from AT Proto's published lexicon. Required
extending atproto.Client.Post and sync.BlueskyPoster.Post to return
the new post's AT-URI (new atproto_post table) so a later reply's
parent can be traced back to the activity it was about, and
state.Store.LogActivity to return the new row's id.
Bug caught by a unit test before deploy: the three new comment/reply
truncation helpers sliced on byte index, which both overshot the byte
bound (a 3-byte "…" past a 200-byte cutoff) and could cut a multi-byte
UTF-8 rune in half — fixed to slice on runes, matching the pattern
atproto.Client.Post already used correctly.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
v2 M2: inbox, follows, and signed delivery to followers
- httpsig.go: draft-cavage HTTP Signatures (sign outbound, verify inbound)
— the fiddliest part of ActivityPub federation, so it ships with a
round-trip test (sign -> receive -> verify) plus a tamper-detection
test, rather than trusting it by inspection.
- Inbox accepts Follow (verifies the signature against the follower's own
published key, records them, replies with a signed Accept) and
Undo{Follow} (removes them). Anything else (replies included) is
accepted and ignored until the comment bridge, M3.
- After each sync pass, DeliverNewActivity signs and POSTs a Create{Note}
to every follower's inbox for whatever landed in activity_log since
that series' last delivery — tracked per-series so a restart never
redelivers history.
- /social now shows each series' live follower count.
Verified live against real malformed/unreachable-actor requests (400 on
bad JSON, 502 on an actor that fails to resolve) with no crash — a real
follow from an actual fediverse account is still the outstanding
end-to-end check, not yet done.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.
Security audit fixes: SSRF hardening, replay protection, resource limits
Addresses every finding from the full-codebase security audit:
- CRITICAL: persistent SSRF via ActivityPub actor/inbox URLs. New
internal/activitypub/ssrf.go validates every externally-supplied URL
(https only, rejects loopback/private/link-local/metadata addresses)
and re-validates the resolved IP at dial time to close the DNS-rebinding
gap a resolve-then-connect check would leave open. Wired into
FetchActor, NewHandler's client, and DeliverNewActivity's per-follower
delivery loop.
- CRITICAL: state.db (holds every series' ActivityPub private key in
plaintext) was world-readable (644) on the live deployment. Open() now
hard-fails if it can't chmod the file to 600.
- HIGH: no replay protection on inbound HTTP Signatures. VerifyRequest
now rejects requests whose Date header is more than 5 minutes stale.
- MEDIUM: unbounded upstream response bodies embedded whole into error
messages/logs (Forgejo, AT Proto, ActivityPub clients) — capped at
4 MiB/1 MiB read limits with logged bodies truncated to 500 bytes.
- MEDIUM: subprocess calls (git, rad) had no timeout, so one hung command
could wedge the whole serial sync loop — bounded to 5 minutes via
context.WithTimeout/exec.CommandContext.
- MEDIUM: Authorized Integrations detection silently disables graft's own
push forever once triggered, with no bound — now falls back to pushing
directly after 10 consecutive skipped passes.
Also fixes a dashboard accuracy issue: a mirror side with zero
graft-logged events read as "nothing synced" even when it plainly held
the content — the Forgejo side changes originate on (graft only logs the
pushes it makes, never the pushes it reads from), or a side that received
content via a path graft doesn't log itself (Radicle's own peer-to-peer
gossip, or a Forgejo-to-Forgejo Authorized Integration). Those sides now
show a distinct "source" or "replicated" badge instead, with a legend
entry for each; "nothing synced" is reserved for a series with truly zero
activity anywhere. Also: renamed the dashboard subtitle to "Forgejos &
Radicles syncs", dropped the redundant "Activity by repo" heading, moved
the "last 3 days" window label to the Recent Activity header, and renamed
"Social bridges" to "Social IDs" throughout.