Skip to content

feat(web-next): implement remote follow UIRemote follow#214

Merged
dahlia merged 20 commits intohackers-pub:mainfrom
dodok8:remote-follow
Mar 2, 2026
Merged

feat(web-next): implement remote follow UIRemote follow#214
dahlia merged 20 commits intohackers-pub:mainfrom
dodok8:remote-follow

Conversation

@dodok8
Copy link
Contributor

@dodok8 dodok8 commented Mar 2, 2026

Summary

Implements remote follow functionality for the web-next stack (closes #173).

  • Add lookupWebFinger GraphQL query for WebFinger + OStatus subscribe template lookup
  • Add ViewerContext to provide auth state across the app
  • Add RemoteFollowButton component with Fediverse ID input dialog and actor preview
  • Update FollowButton to show remote follow fallback for unauthenticated viewers, and support onFollowed callback
  • Add /authorize_interaction page for OStatus subscribe redirects with post-follow navigation
  • Add translations for all 5 locales (en-US, ja-JP, ko-KR, zh-CN, zh-TW)

Flow

  1. Unauthenticated user sees "Remote Follow" button on actor profiles
  2. Clicking opens a dialog to enter their Fediverse ID (e.g. @user@mastodon.social)
  3. After lookup, shows actor preview and redirects to their instance's follow page
  4. When another instance redirects to /authorize_interaction?uri=..., authenticates the user and shows a follow confirmation card
  5. After successful follow, navigates to the actor's profile page

Validation

  • deno task check

Summary by CodeRabbit

  • New Features

    • Remote follow dialog to look up Fediverse users and initiate remote follows.
    • WebFinger lookup GraphQL endpoint returning rich actor metadata.
    • Authorization page to follow users via external URIs.
  • Enhancements

    • Follow button now supports auth-aware rendering and an onFollowed callback.
    • Centralized viewer authentication context (ViewerProvider/useViewer) for auth-aware UI.
  • Localization

    • Added translations for remote follow flows in en-US, ja-JP, ko-KR, zh-CN, zh-TW.

dodok8 and others added 5 commits March 2, 2026 15:32
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show remote follow dialog for unauthenticated viewers,
and support onFollowed callback for post-follow navigation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handles OStatus subscribe redirects, shows actor card
with follow button, and navigates to profile on success.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between b942ddd and 460bb6d.

📒 Files selected for processing (7)
  • graphql/webfinger.ts
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/zh-TW/messages.po
  • web-next/src/routes/(root)/authorize_interaction.tsx
 ___________________________________________________________________________________________________
< All problems in computer science can be solved with another level of indirection. - David Wheeler >
 ---------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.

Tip

CodeRabbit can use oxc to improve the quality of JavaScript and TypeScript code reviews.

Add a configuration file to your project to customize how CodeRabbit runs oxc.

📝 Walkthrough

Walkthrough

Adds WebFinger-based remote-follow support: new GraphQL WebFingerResult and lookup resolver, web-next RemoteFollowButton UI and authorize_interaction route, Viewer authentication context and root wiring, FollowButton adjustments, and i18n entries across locales.

Changes

Cohort / File(s) Summary
GraphQL WebFinger integration
graphql/mod.ts, graphql/schema.graphql, graphql/webfinger.ts
Imports WebFinger module, adds WebFingerResult type and lookupRemoteFollower query, implements WebFinger lookup/resolution, nodeinfo/icon/emoji extraction, remoteFollowUrl detection, and fallbacks with logging.
Remote follow UI
web-next/src/components/RemoteFollowButton.tsx
New Solid component that validates Fediverse handles, queries GraphQL lookupRemoteFollower, shows actor metadata, and initiates remote follow (opens remoteFollowUrl). Includes loading/error states and i18n.
Follow button & fragments
web-next/src/components/FollowButton.tsx
Adds optional onFollowed?: () => void, exposes actor handle and rawName in fragment, and integrates viewer-aware rendering with RemoteFollowButton fallback for unauthenticated/remote flows.
Viewer context & root wiring
web-next/src/contexts/ViewerContext.tsx, web-next/src/routes/(root).tsx
Introduces ViewerProvider and useViewer() exposing isAuthenticated() and isLoaded(), and wraps the root layout with the provider to centralize auth state.
Authorize interaction route
web-next/src/routes/(root)/authorize_interaction.tsx
New route to preload/resolve actor by URI, render actor card, handle signed-in vs signed-out flows, and wire FollowButton for authorization follow flow.
Localization
web-next/src/locales/*/messages.po
en-US, ja-JP, ko-KR, zh-CN, zh-TW
Adds translation keys and messages for Fediverse handle input, validation, lookup states, errors, remote follow prompts, and authorize-interaction UI across locales.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant RemoteFollowButton
    participant GraphQL_API
    participant WebFinger_Resolver
    participant Nodeinfo_Service

    User->>RemoteFollowButton: Submit Fediverse handle
    RemoteFollowButton->>GraphQL_API: lookupRemoteFollower(followerHandle, actorId)
    GraphQL_API->>WebFinger_Resolver: resolve webfinger (acct:user@host)
    WebFinger_Resolver->>WebFinger_Resolver: normalize & validate handle
    WebFinger_Resolver->>WebFinger_Resolver: fetch actor (ActivityPub)
    WebFinger_Resolver->>Nodeinfo_Service: fetch nodeinfo (software)
    Nodeinfo_Service-->>WebFinger_Resolver: nodeinfo response
    WebFinger_Resolver->>WebFinger_Resolver: extract icons, emojis, remoteFollowUrl
    WebFinger_Resolver-->>GraphQL_API: WebFingerResult
    GraphQL_API-->>RemoteFollowButton: actor info
    User->>RemoteFollowButton: Confirm remote follow
    RemoteFollowButton->>User: Open remoteFollowUrl (new tab)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • dahlia

Poem

🐰 I hopped to a handle far away,
I sniffed WebFinger to learn their day,
Icons and emojis in a tidy row,
A click, a tab — a new friend to follow,
Hooray, connections stitch the meadow.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'feat(web-next): implement remote follow UIRemote follow' is partially related but contains a duplicated/malformed segment ('UIRemote follow') making it unclear and unprofessional. Correct the title to remove duplication, e.g., 'feat(web-next): implement remote follow UI' or similar, ensuring clear and professional phrasing.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All coding requirements from issue #173 are comprehensively implemented: WebFinger/OStatus lookup support via GraphQL, ViewerContext for auth state, RemoteFollowButton component, FollowButton updates with remote fallback, authorize_interaction page, and localization across five locales.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #173 requirements: GraphQL WebFinger/OStatus integration, ViewerContext implementation, RemoteFollowButton UI, FollowButton enhancements, authorize_interaction flow, and localization support. No extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's interoperability with the Fediverse by integrating remote follow capabilities. It provides a seamless experience for users to follow accounts hosted on other instances, whether they are authenticated or not, by introducing a new GraphQL API for WebFinger lookups, dedicated UI components for interaction, and a robust page to manage the authorization flow.

Highlights

  • Remote Follow Functionality: Implemented the ability for users to remotely follow Fediverse accounts from other instances, including a UI for entering Fediverse IDs, previewing actors, and handling follow redirects.
  • WebFinger GraphQL API: Added a new GraphQL query lookupWebFinger and a corresponding WebFingerResult type to facilitate looking up Fediverse actor information and OStatus subscribe templates.
  • Viewer Context for Authentication: Introduced a ViewerContext to provide application-wide access to the viewer's authentication status, enabling conditional rendering of follow buttons.
  • Dynamic Follow Button: Modified the existing FollowButton component to display a 'Remote Follow' option for unauthenticated users, leveraging the new RemoteFollowButton component.
  • Authorization Interaction Page: Created a new page /authorize_interaction to handle redirects from external Fediverse instances for OStatus subscribe, displaying a follow confirmation card and redirecting after a successful follow.
  • Internationalization Updates: Added new translation strings across all five supported locales (en-US, ja-JP, ko-KR, zh-CN, zh-TW) to support the new remote follow features.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • graphql/mod.ts
    • Imported the new WebFinger GraphQL module to integrate it into the main schema.
  • graphql/schema.graphql
    • Added a lookupWebFinger query to the GraphQL schema, allowing clients to query Fediverse actor information.
    • Defined the WebFingerResult GraphQL type to structure the data returned by WebFinger lookups, including details like preferred username, name, summary, icon URL, and remote follow URL.
  • graphql/webfinger.ts
    • Added a new module to implement the backend logic for WebFinger lookups.
    • Implemented buildWebFingerResult to construct detailed actor information, including fetching NodeInfo for software detection and extracting custom emojis.
    • Implemented lookupWebFingerImpl to parse Fediverse IDs, perform WebFinger lookups, extract ActivityPub and OStatus subscribe links, and handle ActivityPub actor object lookups with fallback mechanisms.
  • web-next/src/components/FollowButton.tsx
    • Imported useViewer and RemoteFollowButton to support conditional rendering based on authentication status.
    • Added an optional onFollowed callback prop to allow external components to react to successful follow actions.
    • Updated the component to display RemoteFollowButton for unauthenticated viewers, otherwise rendering the standard follow/unfollow button.
  • web-next/src/components/RemoteFollowButton.tsx
    • Added a new component to provide the user interface for initiating a remote follow.
    • Implemented state management for the dialog, Fediverse ID input, loading status, and error messages.
    • Integrated client-side validation for Fediverse ID format using a regex.
    • Defined and utilized a GraphQL query RemoteFollowButton_lookupWebFingerQuery to fetch WebFinger results.
    • Provided functionality to display a preview of the remote actor and redirect to their instance's follow page.
  • web-next/src/contexts/ViewerContext.tsx
    • Added a new context and provider to manage and expose the viewer's authentication status across the application.
    • Created a useViewer hook for convenient access to the authentication state within components.
  • web-next/src/locales/en-US/messages.po
    • Added new translation keys for remote follow UI elements and interaction messages.
    • Updated references for existing translation keys to reflect changes in FollowButton.tsx.
  • web-next/src/locales/ja-JP/messages.po
    • Added new translation keys for remote follow UI elements and interaction messages.
    • Updated references for existing translation keys to reflect changes in FollowButton.tsx.
  • web-next/src/locales/ko-KR/messages.po
    • Added new translation keys for remote follow UI elements and interaction messages.
    • Updated references for existing translation keys to reflect changes in FollowButton.tsx.
  • web-next/src/locales/zh-CN/messages.po
    • Added new translation keys for remote follow UI elements and interaction messages.
    • Updated references for existing translation keys to reflect changes in FollowButton.tsx.
  • web-next/src/locales/zh-TW/messages.po
    • Added new translation keys for remote follow UI elements and interaction messages.
    • Updated references for existing translation keys to reflect changes in FollowButton.tsx.
  • web-next/src/routes/(root).tsx
    • Integrated the ViewerProvider to make the authentication status available globally within the application's root layout.
  • web-next/src/routes/(root)/authorize_interaction.tsx
    • Added a new route to handle OStatus subscribe redirects and display a follow confirmation.
    • Implemented preload logic to fetch actor data based on a provided URI.
    • Included an effect to redirect unauthenticated users to the sign-in page while preserving the interaction context.
    • Designed a UI to confirm the follow action, displaying actor details and providing a FollowButton for local instance interaction.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements the remote follow functionality for the web-next stack, which is a great addition for federation. The changes include a new GraphQL query for WebFinger lookups, a RemoteFollowButton component, and a new page to handle OStatus subscribe redirects. The overall implementation is solid, but I've identified a couple of critical Cross-Site Scripting (XSS) vulnerabilities where unsanitized remote data is rendered as HTML. I've also suggested a minor improvement for URL encoding on the backend. Please address the security issues with high priority.

@dodok8 dodok8 marked this pull request as draft March 2, 2026 06:46
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
web-next/src/contexts/ViewerContext.tsx (1)

9-11: Use a named props interface for ViewerProvider.

The component works, but defining props inline here diverges from the repo’s TS props convention and reduces reuse/readability.

♻️ Proposed refactor
+interface ViewerProviderProps {
+  isAuthenticated: () => boolean;
+}
+
-export const ViewerProvider: ParentComponent<{
-  isAuthenticated: () => boolean;
-}> = (props) => {
+export const ViewerProvider: ParentComponent<ViewerProviderProps> = (props) => {
As per coding guidelines "Use interfaces for component props (e.g., ButtonProps) in TypeScript".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-next/src/contexts/ViewerContext.tsx` around lines 9 - 11, Replace the
inline props type on ViewerProvider with a named interface: create an exported
interface (e.g., ViewerProviderProps) that declares isAuthenticated: () =>
boolean, then update the component signature to
ParentComponent<ViewerProviderProps> and use that interface where the props type
is referenced (i.e., the ViewerProvider function definition and any related type
imports/exports) to follow the repo TS props convention and improve
reuse/readability.
graphql/schema.graphql (1)

1377-1383: Use stronger scalar types for URL-shaped fields in WebFingerResult.

iconUrl and url are URL values but currently typed as String, which weakens schema validation and client contract clarity.

♻️ Proposed schema refinement
 type WebFingerResult {
   domain: String
   emojis: JSON
   handle: String
-  iconUrl: String
+  iconUrl: URL
   name: String
   preferredUsername: String
   remoteFollowUrl: String
   software: String
   summary: String
-  url: String
+  url: URL
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/schema.graphql` around lines 1377 - 1383, The WebFingerResult type
uses plain String for URL-shaped fields; change the iconUrl and url field types
to a URL scalar (e.g., replace "iconUrl: String" and "url: String" with
"iconUrl: URL" and "url: URL"), ensure the URL scalar is declared/imported in
the schema (or add a GraphQL scalar type named URL that validates URLs), and
update any resolvers/type mappings that construct WebFingerResult (functions or
classes that return iconUrl or url) to return/validate a proper URL value or
string that passes the URL scalar validation.
web-next/src/components/RemoteFollowButton.tsx (1)

63-65: Destructure component props at the function boundary.

This keeps the component aligned with repo conventions and simplifies access across handlers.

♻️ Proposed refactor
-export function RemoteFollowButton(props: RemoteFollowButtonProps) {
+export function RemoteFollowButton(
+  { actorHandle, actorName }: RemoteFollowButtonProps,
+) {
@@
-          actorHandle: props.actorHandle,
+          actorHandle,
@@
-  const displayName = () => props.actorName || props.actorHandle;
+  const displayName = () => actorName || actorHandle;
As per coding guidelines "Files with components use PascalCase naming (e.g., Button.tsx) Use functional components with props destructuring".

Also applies to: 115-116, 144-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-next/src/components/RemoteFollowButton.tsx` around lines 63 - 65, The
component currently receives props via a single identifier; change
RemoteFollowButton to destructure its RemoteFollowButtonProps at the function
boundary (e.g., function RemoteFollowButton({ ... }: RemoteFollowButtonProps))
so handlers can use the props directly, update any internal references that
access props.* to the new local names, and apply the same props-destructuring
refactor to the other component declarations in this file that follow the same
pattern (the other component functions that currently accept a props
identifier).
graphql/webfinger.ts (1)

121-125: Add an explicit return type to lookupWebFingerImpl.

This function returns a complex object-or-null shape; please annotate it explicitly (ideally via a shared result type used by both buildWebFingerResult and fallback returns).

As per coding guidelines: **/*.{ts,tsx}: Use explicit typing for complex return types in TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/webfinger.ts` around lines 121 - 125, Add an explicit TypeScript
return type for lookupWebFingerImpl that describes the complex object-or-null
shape it returns; define a shared interface or type alias (e.g., WebFingerResult
or IWebFingerResult) that matches the return shape produced by
buildWebFingerResult and use that type on lookupWebFingerImpl's signature and on
any fallback return values (including null if appropriate) so both
buildWebFingerResult and lookupWebFingerImpl reference the same result type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@graphql/webfinger.ts`:
- Around line 148-155: remoteFollowLink.template is untrusted; before building
remoteFollowUrl ensure the template contains the literal "{uri}", replace using
encodeURIComponent(actorHandle) (not encodeURI), then parse the resulting URL
with the URL API and only accept it if its protocol is "http:" or "https:"; if
the template is missing "{uri}" or the parsed URL has a non-http(s) scheme or
fails to parse, do not return remoteFollowUrl (leave undefined) and skip
including the link in responses. Apply this validation around the
remoteFollowLink -> remoteFollowUrl logic (symbols: webfingerResult.links,
remoteFollowLink, remoteFollowUrl, actorHandle).

In `@web-next/src/components/RemoteFollowButton.tsx`:
- Around line 149-150: The fallback username parsing using
info.handle?.split("@")[0] can return an empty string for handles like
"@user@host"; update the fallback so it first strips leading "@" characters from
info.handle (e.g. remove any leading @ symbols), then split on "@" and take the
first non-empty segment, and keep the existing fallbacks info.name and
info.preferredUsername; modify the expression that currently uses
info.handle?.split("@")[0] (in RemoteFollowButton.tsx) to perform this sanitized
extraction and return the sanitized result or "" if nothing valid remains.
- Around line 230-233: The h4 in RemoteFollowButton.tsx is using innerHTML with
actorDisplayName(), which injects remote metadata and opens an XSS vector;
change it to render the display name as plain text (remove
innerHTML/dangerouslySetInnerHTML and pass actorDisplayName() as the element's
children or use textContent), and ensure the actorDisplayName() helper returns a
plain string (no HTML) or is sanitized before rendering.
- Around line 135-141: Validate info.remoteFollowUrl before opening it: in
RemoteFollowButton.tsx, replace the direct window.open call with a guard that
constructs a URL object from info.remoteFollowUrl (inside a try/catch) and
checks that url.protocol is "http:" or "https:"; if invalid or construction
fails call setError(t`This service does not support remote follow.`) and return,
otherwise call window.open(url.toString(), "_blank", "noopener,noreferrer") and
then handleOpenChange(false). Ensure you reference info.remoteFollowUrl,
setError, and handleOpenChange in the fix.

In `@web-next/src/routes/`(root)/authorize_interaction.tsx:
- Around line 72-80: The code always calls loadPageQuery("") via
createPreloadedQuery even when uri() is falsy; change it to avoid invoking
loadPageQuery when there's no URI by only calling createPreloadedQuery (and thus
loadPageQuery) if uri() returns a truthy value—e.g., compute const u = uri(); if
(!u) set data to null/undefined and let the component render the “No user URI
provided.” fallback; otherwise call
createPreloadedQuery(authorizeInteractionPageQuery, () => loadPageQuery(u)).
Update any downstream logic that reads data to handle the null/undefined case.
- Around line 23-35: The identifier authorizeInteractionPageQuery is declared
twice (once in the type import and once as the GraphQL document constant),
causing a redeclare lint error; rename one of them (e.g., alias the imported
type to AuthorizeInteractionPageQueryType or rename the const to
authorizeInteractionPageDocument) and update any usages accordingly—look for the
import line importing authorizeInteractionPageQuery and the const declaration
using graphql as well as related usages in preload, loadPageQuery, and any route
typing to ensure the new name is used consistently.

---

Nitpick comments:
In `@graphql/schema.graphql`:
- Around line 1377-1383: The WebFingerResult type uses plain String for
URL-shaped fields; change the iconUrl and url field types to a URL scalar (e.g.,
replace "iconUrl: String" and "url: String" with "iconUrl: URL" and "url: URL"),
ensure the URL scalar is declared/imported in the schema (or add a GraphQL
scalar type named URL that validates URLs), and update any resolvers/type
mappings that construct WebFingerResult (functions or classes that return
iconUrl or url) to return/validate a proper URL value or string that passes the
URL scalar validation.

In `@graphql/webfinger.ts`:
- Around line 121-125: Add an explicit TypeScript return type for
lookupWebFingerImpl that describes the complex object-or-null shape it returns;
define a shared interface or type alias (e.g., WebFingerResult or
IWebFingerResult) that matches the return shape produced by buildWebFingerResult
and use that type on lookupWebFingerImpl's signature and on any fallback return
values (including null if appropriate) so both buildWebFingerResult and
lookupWebFingerImpl reference the same result type.

In `@web-next/src/components/RemoteFollowButton.tsx`:
- Around line 63-65: The component currently receives props via a single
identifier; change RemoteFollowButton to destructure its RemoteFollowButtonProps
at the function boundary (e.g., function RemoteFollowButton({ ... }:
RemoteFollowButtonProps)) so handlers can use the props directly, update any
internal references that access props.* to the new local names, and apply the
same props-destructuring refactor to the other component declarations in this
file that follow the same pattern (the other component functions that currently
accept a props identifier).

In `@web-next/src/contexts/ViewerContext.tsx`:
- Around line 9-11: Replace the inline props type on ViewerProvider with a named
interface: create an exported interface (e.g., ViewerProviderProps) that
declares isAuthenticated: () => boolean, then update the component signature to
ParentComponent<ViewerProviderProps> and use that interface where the props type
is referenced (i.e., the ViewerProvider function definition and any related type
imports/exports) to follow the repo TS props convention and improve
reuse/readability.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3aeca61 and fdd9f70.

📒 Files selected for processing (13)
  • graphql/mod.ts
  • graphql/schema.graphql
  • graphql/webfinger.ts
  • web-next/src/components/FollowButton.tsx
  • web-next/src/components/RemoteFollowButton.tsx
  • web-next/src/contexts/ViewerContext.tsx
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/zh-TW/messages.po
  • web-next/src/routes/(root).tsx
  • web-next/src/routes/(root)/authorize_interaction.tsx

@dodok8 dodok8 marked this pull request as ready for review March 2, 2026 07:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds “remote follow” support to the web-next Solid/Relay frontend and exposes a new GraphQL WebFinger lookup to power OStatus subscribe redirects (issue #173).

Changes:

  • Introduces lookupWebFinger GraphQL query + WebFingerResult type for WebFinger/ActivityPub lookup and remote-follow URL discovery.
  • Adds viewer auth state via ViewerContext, and updates FollowButton to show a RemoteFollowButton fallback when unauthenticated.
  • Adds /authorize_interaction route to support inbound OStatus-style follow authorization flow and post-follow navigation.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
web-next/src/routes/(root)/authorize_interaction.tsx New authorize-interaction page for inbound remote-follow redirects and follow confirmation UI
web-next/src/routes/(root).tsx Wraps app in ViewerProvider to expose auth state globally
web-next/src/contexts/ViewerContext.tsx Adds viewer auth context (isAuthenticated)
web-next/src/components/FollowButton.tsx Adds unauthenticated remote-follow fallback + onFollowed callback
web-next/src/components/RemoteFollowButton.tsx New dialog flow to look up WebFinger + open remote instance follow URL
graphql/webfinger.ts New GraphQL resolver to perform WebFinger + ActivityPub lookup and build remoteFollowUrl
graphql/schema.graphql Adds lookupWebFinger and WebFingerResult to schema
graphql/mod.ts Registers the new webfinger.ts module
web-next/src/locales/en-US/messages.po Adds translations for remote follow/authorize interaction strings
web-next/src/locales/ja-JP/messages.po Adds translations for remote follow/authorize interaction strings
web-next/src/locales/ko-KR/messages.po Adds translations for remote follow/authorize interaction strings
web-next/src/locales/zh-CN/messages.po Adds translations for remote follow/authorize interaction strings
web-next/src/locales/zh-TW/messages.po Adds translations for remote follow/authorize interaction strings

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
web-next/src/locales/zh-TW/messages.po (1)

1546-1549: Translation adds content not present in source string.

The source text "You are about to follow {0}." is translated as "你即將從你的帳戶關注{0}。" which back-translates to "You are about to follow {0} from your account." The phrase "從你的帳戶" (from your account) was added.

While this may provide helpful context in the UI, it deviates from the source string. Consider aligning with the source:

Suggested fix
 #. placeholder {0}: actor().name ?? actor().handle
 #: src/routes/(root)/authorize_interaction.tsx:127
 msgid "You are about to follow {0}."
-msgstr "你即將從你的帳戶關注{0}。"
+msgstr "你即將關注{0}。"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-next/src/locales/zh-TW/messages.po` around lines 1546 - 1549, The
translated string msgstr for msgid "You are about to follow {0}." adds extra
content ("從你的帳戶") not present in the source; update the msgstr to closely match
the source by removing that added phrase so it reads a direct equivalent of "You
are about to follow {0}." (e.g., "你即將關注{0}。"), preserving the placeholder and
punctuation used in the original msgid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@graphql/webfinger.ts`:
- Around line 76-99: A parsing error on one tag currently escapes the whole
try/catch around the actorObject.getTags() loop and aborts emoji extraction;
update the loop that iterates actorObject.getTags() so each iteration is guarded
with its own try/catch (wrap the body that checks "tag instanceof vocab.Emoji",
calls tag.getIcon(), reads emojiIcon.url, constructs new URL(raw), and assigns
emojis[emojiName]) and on exception continue to the next tag (optionally log the
error); ensure you still skip non-Emoji tags and preserve the existing
validation checks for emojiIcon.url and protocol before assigning into the
emojis map.

---

Nitpick comments:
In `@web-next/src/locales/zh-TW/messages.po`:
- Around line 1546-1549: The translated string msgstr for msgid "You are about
to follow {0}." adds extra content ("從你的帳戶") not present in the source; update
the msgstr to closely match the source by removing that added phrase so it reads
a direct equivalent of "You are about to follow {0}." (e.g., "你即將關注{0}。"),
preserving the placeholder and punctuation used in the original msgid.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fdd9f70 and d798b88.

📒 Files selected for processing (8)
  • graphql/webfinger.ts
  • web-next/src/components/RemoteFollowButton.tsx
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/ja-JP/messages.po
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/locales/zh-CN/messages.po
  • web-next/src/locales/zh-TW/messages.po
  • web-next/src/routes/(root)/authorize_interaction.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • web-next/src/locales/ko-KR/messages.po
  • web-next/src/components/RemoteFollowButton.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
graphql/webfinger.ts (1)

91-97: ⚠️ Potential issue | 🟡 Minor

One malformed emoji URL can abort all emoji extraction.

The new URL(raw) on line 91 is not guarded by its own try-catch. If any single emoji has a malformed URL, the exception propagates to the outer catch block (line 100), terminating the loop and skipping all remaining emojis.

🛠️ Proposed fix
         const emojiName = tag.name.toString();
         const raw = emojiIcon.url instanceof vocab.Link
           ? emojiIcon.url.href!.href
           : emojiIcon.url.href;
-        const u = new URL(raw);
-        if (
-          (u.protocol === "http:" || u.protocol === "https:") &&
-          !/[\'\"]/.test(raw)
-        ) {
-          emojis[emojiName] = u.href;
+        try {
+          const u = new URL(raw);
+          if (
+            (u.protocol === "http:" || u.protocol === "https:") &&
+            !/[\'\"]/.test(raw)
+          ) {
+            emojis[emojiName] = u.href;
+          }
+        } catch {
+          continue;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/webfinger.ts` around lines 91 - 97, The loop that builds the emojis
map uses new URL(raw) unguarded, so a single malformed raw will throw and abort
processing; wrap the URL construction/validation for each emoji (the new
URL(raw) call inside the block that assigns emojis[emojiName]) in its own
try/catch (or pre-validate the string) and only assign emojis[emojiName] =
u.href when URL parsing succeeds and the protocol check passes, ensuring a
malformed emoji URL is skipped without breaking the outer loop or aborting the
whole extraction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@graphql/webfinger.ts`:
- Around line 91-97: The loop that builds the emojis map uses new URL(raw)
unguarded, so a single malformed raw will throw and abort processing; wrap the
URL construction/validation for each emoji (the new URL(raw) call inside the
block that assigns emojis[emojiName]) in its own try/catch (or pre-validate the
string) and only assign emojis[emojiName] = u.href when URL parsing succeeds and
the protocol check passes, ensuring a malformed emoji URL is skipped without
breaking the outer loop or aborting the whole extraction.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d798b88 and abccda9.

📒 Files selected for processing (2)
  • graphql/webfinger.ts
  • web-next/src/components/RemoteFollowButton.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web-next/src/components/RemoteFollowButton.tsx

Copy link
Member

@dahlia dahlia left a comment

Choose a reason for hiding this comment

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

Thanks for implementing remote follow — the overall flow and UX are well thought out. I have some suggestions around the GraphQL query design, security, and a few smaller issues.

dodok8 and others added 8 commits March 2, 2026 16:54
A single malformed emoji URL could abort the entire emoji extraction
loop. Wrap per-emoji processing in its own try/catch so one bad tag
doesn't prevent extracting the rest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…query

Extract AuthorizeInteractionContent so createPreloadedQuery is only
called when a valid URI exists, preventing a wasteful loadPageQuery("")
call when the uri search param is absent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
While auth query is loading, isAuthenticated() returns false, briefly
showing RemoteFollowButton to authenticated users. Add isLoaded state
and gate FollowButton rendering on it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract WebFingerResultData interface shared by buildWebFingerResult
and lookupWebFingerImpl. Change iconUrl, url, remoteFollowUrl fields
from String to URL scalar for stronger schema validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The msgstr for "You are about to follow {0}." contained "從你的帳戶"
(from your account) which is not present in the source string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename fediverseId → followerHandle and actorHandle → actorId (Relay
global ID) so the server can validate it refers to a real Hackers' Pub
actor. Add GraphQL descriptions to the query and its parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Require rel="self" and exact-match the link type against
application/activity+json or the standard LD-JSON ActivityStreams
profile, instead of the overly broad includes("activity") check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (4)
web-next/src/routes/(root)/authorize_interaction.tsx (2)

27-30: ⚠️ Potential issue | 🟡 Minor

Normalize acct: URIs before calling actorByHandle.

If uri arrives as acct:user@host, this goes straight into actorByHandle, causing an avoidable failed lookup path and extra latency.

🔧 Proposed fix
 export const route = {
   preload(args) {
-    const uri = new URLSearchParams(args.location.search).get("uri") ?? "";
-    if (uri) {
-      void loadPageQuery(uri);
+    const rawUri = new URLSearchParams(args.location.search).get("uri") ?? "";
+    const uri = rawUri.replace(/^acct:/i, "");
+    if (uri) {
+      void loadPageQuery(uri);
     }
   },
 } satisfies RouteDefinition;
@@
-  const uri = () => searchParams.uri as string | undefined;
+  const uri = () => (searchParams.uri as string | undefined)?.replace(/^acct:/i, "");

Also applies to: 39-39, 69-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-next/src/routes/`(root)/authorize_interaction.tsx around lines 27 - 30,
The code passes raw "acct:" URIs into actorByHandle causing failed lookups;
before calling loadPageQuery and anywhere actorByHandle is invoked (e.g., where
uri or handle is sourced), detect and normalize strings starting with "acct:" by
stripping the "acct:" prefix (and URL-decoding/trimming) to produce "user@host"
or the expected handle format, then pass the normalized handle into
loadPageQuery/actorByHandle so lookups use the correct handle format and avoid
the extra failed path.

23-35: ⚠️ Potential issue | 🔴 Critical

Fix duplicate authorizeInteractionPageQuery identifier (lint blocker).

The type import and GraphQL document constant share the same name, which triggers noRedeclare and blocks lint/check.

🔧 Proposed fix
-import type { authorizeInteractionPageQuery } from "./__generated__/authorizeInteractionPageQuery.graphql.ts";
+import type {
+  authorizeInteractionPageQuery as AuthorizeInteractionPageQuery,
+} from "./__generated__/authorizeInteractionPageQuery.graphql.ts";
@@
-const authorizeInteractionPageQuery = graphql`
+const authorizeInteractionPageQueryDocument = graphql`
   query authorizeInteractionPageQuery($uri: String!) {
@@
 const loadPageQuery = query(
   (uri: string) =>
-    loadQuery<authorizeInteractionPageQuery>(
+    loadQuery<AuthorizeInteractionPageQuery>(
       useRelayEnvironment()(),
-      authorizeInteractionPageQuery,
+      authorizeInteractionPageQueryDocument,
       { uri },
     ),
@@
-  const data = createPreloadedQuery<authorizeInteractionPageQuery>(
-    authorizeInteractionPageQuery,
+  const data = createPreloadedQuery<AuthorizeInteractionPageQuery>(
+    authorizeInteractionPageQueryDocument,
     () => loadPageQuery(props.uri),
   );
#!/bin/bash
set -euo pipefail
file="$(fd -p 'authorize_interaction.tsx' web-next/src/routes | head -n1)"
rg -nP '\bauthorizeInteractionPageQuery\b' "$file"
sed -n '20,40p' "$file"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-next/src/routes/`(root)/authorize_interaction.tsx around lines 23 - 35,
The file declares a type import authorizeInteractionPageQuery and also a const
graphql document with the same name, causing a redeclare lint error; fix by
renaming one of them (e.g., alias the type import: import type {
authorizeInteractionPageQuery as authorizeInteractionPageQueryType } or rename
the graphql const to authorizeInteractionPageQueryDocument) and update any local
references to the renamed symbol (check the import line and the const
declaration for authorizeInteractionPageQuery and adjust usages accordingly).
graphql/webfinger.ts (2)

227-276: ⚠️ Potential issue | 🟠 Major

Add abuse controls for unauthenticated remote-follow lookups.

This resolver can trigger multiple outbound requests per call and is publicly reachable, so it should be rate-limited (or similarly throttled) to reduce abuse and scanning risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/webfinger.ts` around lines 227 - 276, The public resolver
lookupRemoteFollower (its resolve function) can be abused to trigger many
outbound requests; add throttling by checking an authentication/context rate
limit before calling lookupRemoteFollowerImpl. Specifically, in the resolve for
lookupRemoteFollower, use a per-client key (e.g., ctx.ip or ctx.auth?.userId or
ctx.apiKey) and invoke the existing rate limiter (e.g., ctx.rateLimiter.consume
or a new token-bucket) to allow only a small number of calls per time window; if
the limit is exceeded, log the event and return null/early error instead of
calling lookupRemoteFollowerImpl. Keep the validation of args.followerHandle and
actor lookup intact and apply the limiter before the outbound lookup to protect
lookupRemoteFollower and lookupRemoteFollowerImpl from abuse.

158-163: ⚠️ Potential issue | 🟠 Major

Harden ActivityPub link selection and validate URL scheme before lookup.

Current matching is too permissive (includes("activity")), and href is used for network fetch without explicit http/https validation.

🔧 Proposed hardening
-  const activityPubLink = webfingerResult.links?.find((link) =>
-    link.type === "application/activity+json" ||
-    (link.rel === "self" && link.type?.includes("activity"))
-  ) as WebfingerLink | undefined;
+  const activityPubLink = webfingerResult.links?.find((link) =>
+    link.rel === "self" &&
+    (
+      link.type === "application/activity+json" ||
+      link.type === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+    )
+  ) as WebfingerLink | undefined;
 
   if (!activityPubLink?.href) return null;
+  let activityPubHref: string;
+  try {
+    const parsed = new URL(activityPubLink.href);
+    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
+    activityPubHref = parsed.href;
+  } catch {
+    return null;
+  }
@@
-    const actorObject = await ctx.fedCtx.lookupObject(activityPubLink.href, {
+    const actorObject = await ctx.fedCtx.lookupObject(activityPubHref, {
       documentLoader,
     });
@@
-      url: new URL(activityPubLink.href),
+      url: new URL(activityPubHref),
#!/bin/bash
set -euo pipefail
file="$(fd -p 'webfinger.ts' graphql | head -n1)"
rg -nP 'includes\\("activity"\\)|lookupObject\\(activityPubLink\\.href' "$file"
sed -n '150,200p' "$file"

Also applies to: 190-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/webfinger.ts` around lines 158 - 163, The ActivityPub link selection
is too permissive and uses href for network calls without validating the URL
scheme; tighten the matching and reject non-http(s) hrefs before calling
lookupObject. Update the link find logic over webfingerResult.links to only
accept link.type === "application/activity+json" or (link.rel === "self" &&
link.type?.startsWith("application/activity+json")) (or other precise MIME
strings you expect), then after selecting activityPubLink validate its href by
parsing with the URL constructor and ensuring url.protocol is "http:" or
"https:"; if parsing fails or the protocol is not http/https return null instead
of passing the href to lookupObject(activityPubLink.href). Use the
activityPubLink variable name and the lookupObject call as the places to modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@graphql/webfinger.ts`:
- Around line 227-276: The public resolver lookupRemoteFollower (its resolve
function) can be abused to trigger many outbound requests; add throttling by
checking an authentication/context rate limit before calling
lookupRemoteFollowerImpl. Specifically, in the resolve for lookupRemoteFollower,
use a per-client key (e.g., ctx.ip or ctx.auth?.userId or ctx.apiKey) and invoke
the existing rate limiter (e.g., ctx.rateLimiter.consume or a new token-bucket)
to allow only a small number of calls per time window; if the limit is exceeded,
log the event and return null/early error instead of calling
lookupRemoteFollowerImpl. Keep the validation of args.followerHandle and actor
lookup intact and apply the limiter before the outbound lookup to protect
lookupRemoteFollower and lookupRemoteFollowerImpl from abuse.
- Around line 158-163: The ActivityPub link selection is too permissive and uses
href for network calls without validating the URL scheme; tighten the matching
and reject non-http(s) hrefs before calling lookupObject. Update the link find
logic over webfingerResult.links to only accept link.type ===
"application/activity+json" or (link.rel === "self" &&
link.type?.startsWith("application/activity+json")) (or other precise MIME
strings you expect), then after selecting activityPubLink validate its href by
parsing with the URL constructor and ensuring url.protocol is "http:" or
"https:"; if parsing fails or the protocol is not http/https return null instead
of passing the href to lookupObject(activityPubLink.href). Use the
activityPubLink variable name and the lookupObject call as the places to modify.

In `@web-next/src/routes/`(root)/authorize_interaction.tsx:
- Around line 27-30: The code passes raw "acct:" URIs into actorByHandle causing
failed lookups; before calling loadPageQuery and anywhere actorByHandle is
invoked (e.g., where uri or handle is sourced), detect and normalize strings
starting with "acct:" by stripping the "acct:" prefix (and
URL-decoding/trimming) to produce "user@host" or the expected handle format,
then pass the normalized handle into loadPageQuery/actorByHandle so lookups use
the correct handle format and avoid the extra failed path.
- Around line 23-35: The file declares a type import
authorizeInteractionPageQuery and also a const graphql document with the same
name, causing a redeclare lint error; fix by renaming one of them (e.g., alias
the type import: import type { authorizeInteractionPageQuery as
authorizeInteractionPageQueryType } or rename the graphql const to
authorizeInteractionPageQueryDocument) and update any local references to the
renamed symbol (check the import line and the const declaration for
authorizeInteractionPageQuery and adjust usages accordingly).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abccda9 and b942ddd.

📒 Files selected for processing (8)
  • graphql/schema.graphql
  • graphql/webfinger.ts
  • web-next/src/components/FollowButton.tsx
  • web-next/src/components/RemoteFollowButton.tsx
  • web-next/src/contexts/ViewerContext.tsx
  • web-next/src/locales/zh-TW/messages.po
  • web-next/src/routes/(root).tsx
  • web-next/src/routes/(root)/authorize_interaction.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web-next/src/locales/zh-TW/messages.po

@dahlia
Copy link
Member

dahlia commented Mar 2, 2026

Could you run deno task extract inside web-next/? It would make CI pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dahlia dahlia merged commit d1c3ff5 into hackers-pub:main Mar 2, 2026
4 of 5 checks passed
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.

[Proposal][web/web-next] Remote Follow for Hackerspub on other instance

3 participants