Skip to content

feat(activitypub): follow a followed account's Mastodon Move - #579

Merged
davidwkeith merged 2 commits into
mainfrom
claude/trusting-hawking-3eawag
Oct 29, 2026
Merged

davidwkeith merged 2 commits into
mainfrom
claude/trusting-hawking-3eawag

Conversation

@davidwkeith

@davidwkeith davidwkeith commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Inbound Move, the follower side of Mastodon account migration. This is the first of the SiliconBeest interop extensions; EmojiReact, quote posts and FEP-8fcf follow in separate PRs.

Before this change, an inbound Move hit the default branch and was ignored, so this actor kept following a dead account. Now:

  • A Move is acted on only when its actor (already bound to the HTTP signer) equals its object, its target is a different, SSRF-safe IRI, and this actor follows the mover. Anything else is accepted with 202 and ignored.
  • It queues a pending_accept row of kind move. The target fetch runs from the alarm, so the inbound POST never holds the DO's input gate on an outbound request. The Follow and Undo(Follow) are built at enqueue time, while the forwarded config is available.
  • The alarm requires the target's alsoKnownAs to list the mover, the same check Mastodon applies. If it does, this actor follows the target (a pending following row plus a Follow, skipped if it already follows the target), drops the mover's row, and sends the mover an Undo(Follow) (Mastodon matches that on actor and object, so the original Follow id isn't needed). If it doesn't, nothing changes and activitypub.move.rejected is logged.
  • An owner unfollow before the alarm runs cancels the queued move. At most one move row is queued per mover. An unreachable target is retried like any other resolution, and it never marks an existing follow of the target as Unknown.
  • #resolveInbox now also returns the actor's alsoKnownAs.

Moving off this actor (movedTo and an outbound Move) is still out of scope, as the spec already states.

Packages affected

@dwk/activitypub

Checklist

  • Read the relevant spec(s) under spec/packages/ and updated them if
    behaviour changed: spec/packages/activitypub.md S2S section
  • Added/updated colocated tests (src/*.test.ts), DO-level in index.test.ts: a verified move follows the target and unfollows the mover; a target without the alias changes nothing; a Move from an account we don't follow is ignored; a Move whose object isn't the mover is ignored; two Moves queue one row and fetch the target once; an owner unfollow before the alarm cancels the move without fetching; an already-followed target only gets the mover's Undo; self, private-network and non-https targets are ignored
  • Ran the local CI gate: pnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm test (3314 passed)
  • Added a changeset (pnpm changeset) if this touches a publishable
    package
  • Updated catalog.json / conformance/status.json if this adds a new
    mountable 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

When an account this actor follows sends a Move, queue a `move` row;
the alarm fetches the target and, only if its alsoKnownAs lists the
mover, follows the target and sends the mover an Undo(Follow).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj

@davidwkeith davidwkeith left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review against CONTRIBUTING.md / CLAUDE.md.

Conforms: Conventional Commits title with a valid scope, PR template headings kept verbatim, a minor changeset for @dwk/activitypub, the spec updated, and the unchecked catalog.json item carries a reason. The design is sound: the target fetch runs from the alarm, the alsoKnownAs check matches Mastodon, and the target goes through the SSRF-safe #resolveInbox.

Suggested fixes (none blocking):

  1. Missing test coverage for behaviour the spec and PR body promise. The four tests cover the happy path, a missing alias, an unfollowed mover and a mismatched object. Nothing covers:

    • an owner unfollow before the alarm cancelling the queued move (#pendingAcceptStillActive, kind === "move");
    • the "at most one move row per mover" dedupe (send the same Move twice with different ids);
    • the "skipped if already following the target" branch (alreadyFollowing: true);
    • an unsafe or self target.
      Please add DO-level tests for at least the cancel and dedupe paths.
  2. Terminal resolve failure side effect (object.ts alarm loop). When the target can't be resolved and attempts are exhausted, the existing code runs UPDATE following SET actor_type='Unknown' WHERE actor = row.actor. For move rows row.actor is the target. If the owner already follows the target, a failed fetch will mark that row Unknown. Skip that update for kind === "move", or confirm it's intended.

  3. MoveRejected reason. Only alias_missing is ever logged, so an unresolvable target is silent apart from the generic DeliveryFailed. Fine to leave, but the log doc comment (log.ts) should say that reason currently has a single value. Consider also documenting both new events wherever the other activitypub.* events are listed (e.g. the observability spec or README), if such a list exists.

  4. Nit, doc formatting. In spec/packages/activitypub.md the added Move makes the first bullet's line exceed the wrapped width. Run pnpm format:check if it hasn't already caught this, since prose-wrap may differ in CI.

I didn't run the test suite locally.


Generated by Claude Code

Don't mark an existing follow of the migration target `Unknown` when
the target can't be resolved; document MoveRejected's single reason;
and add DO tests for move dedupe, owner-unfollow cancellation, an
already-followed target, and self/private/non-https targets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj
@davidwkeith
davidwkeith merged commit 43e7a0f into main Oct 29, 2026
8 checks passed
@davidwkeith
davidwkeith deleted the claude/trusting-hawking-3eawag branch October 29, 2026 15:52
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