Comment endpoint for Indiekit — IndieAuth/RelMeAuth-based comment system with admin dashboard and JF2 API
  • JavaScript 87.7%
  • Nunjucks 7.9%
  • CSS 4.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ricardo f172e94599 fix: refuse to fetch visitor-supplied URLs at blocked hosts
The sign-in form decided where this server sent requests. A visitor
submits a URL, discoverEndpoints and fetchProfile fetch it, fetchMetadata
fetches what its rels point at, and exchangeCode POSTs an authorization
code to the authorization endpoint that same page declared. None of those
restricted scheme or host, so all four were blind SSRF against loopback,
private ranges and 169.254.169.254 — reachable by anyone who can submit
the form. Demonstrated with a live loopback server: before this change it
was contacted and its declared endpoint adopted.

This predates the IndieAuth work; discoverEndpoints fetched unrestricted
URLs in 1.0.17 too. It is the same class of issue as
harden-client-discovery.patch, sent upstream for endpoint-auth on
2026-08-16, and isFetchableUrl is ported from it rather than re-derived so
the two behave identically. A profile URL's host must be a domain name, so
every IP literal is refused rather than sorting private ranges from
public, and the numeric encodings the resolver accepts but net.isIP does
not (2130706433, 0x7f000001, 0177.0.0.1) are refused explicitly.

The patch guards one URL. That is not enough here: fetches followed
redirects, so a permitted domain answering 302 to 169.254.169.254 walked
straight through. safeFetch follows redirects by hand and re-checks every
hop, caps the chain, applies a 5s timeout, and never follows a redirect
for a non-GET request — replaying the POST body would hand the
authorization code to whoever set the Location header.

A domain name is still trusted without resolving it, so one pointing at an
internal address passes. Closing that needs a resolving dispatcher.

Verified unchanged against live sites: rmendes.net and aaronparecki.com
resolve to their own endpoints, tantek.com to indieauth.com, profiles
still parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:53:15 +02:00
assets fix: override --flow-inset to defeat s-flow list padding specificity 2026-03-14 13:13:55 +01:00
lib fix: refuse to fetch visitor-supplied URLs at blocked hosts 2026-08-22 13:53:15 +02:00
locales i18n: add translations for all 14 supported locales 2026-03-21 12:46:14 +01:00
tests fix: refuse to fetch visitor-supplied URLs at blocked hosts 2026-08-22 13:53:15 +02:00
views fix: polish dashboard layout and improve avatar fetching 2026-03-14 11:11:38 +01:00
.gitignore feat: verify profile URL claims per IndieAuth 5.4 2026-08-21 08:27:49 +02:00
index.js feat: declare recent-comments v2 block(s) via get blocks() (Phase 7b) 2026-06-22 07:59:35 +02:00
package.json fix: refuse to fetch visitor-supplied URLs at blocked hosts 2026-08-22 13:53:15 +02:00
README.md docs: add README with API reference and reply architecture 2026-03-15 14:07:25 +01:00

@rmdes/indiekit-endpoint-comments

Comment endpoint for Indiekit. Allows visitors to authenticate via IndieAuth and post comments on blog posts. Site owners can reply to comments from the admin session.

Features

  • IndieAuth authentication — visitors sign in with their personal URL via IndieAuth/PKCE
  • Comment submission — authenticated visitors can post comments on any page
  • Owner replies — site owner can reply to native comments from the admin session
  • Rate limiting — per-user rate limits (configurable per hour/per day)
  • Content sanitization — HTML sanitization with configurable max length
  • Admin moderation — hide (soft delete), restore, and purge comments
  • JF2 API — comments served in JF2 feed format for frontend consumption

Installation

npm install @rmdes/indiekit-endpoint-comments
// indiekit.config.js
import CommentsEndpoint from "@rmdes/indiekit-endpoint-comments";

export default {
  plugins: [
    new CommentsEndpoint({
      mountPath: "/comments",
      maxLength: 2000,
      rateLimit: { perHour: 5, perDay: 20 },
    }),
  ],
};

API

Public Routes (no authentication required)

Method Path Description
GET /comments/api/comments?target={url} Get comments for a target URL (JF2 feed)
GET /comments/api/session Check current IndieAuth session
GET /comments/api/is-owner Check if visitor is the site owner (admin session)
POST /comments/api/submit Submit a comment (requires IndieAuth session)
POST /comments/api/reply Submit an owner reply to a comment (requires admin session)
POST /comments/api/auth Start IndieAuth flow
GET /comments/auth/callback IndieAuth callback handler

Admin Routes (require Indiekit authentication)

Method Path Description
GET /comments Admin dashboard with stats and moderation
POST /comments/hide Soft-delete a comment
POST /comments/purge Permanently delete a comment

Owner Reply (POST /comments/api/reply)

Allows the site owner to reply to native comments. Requires an active Indiekit admin session (not an IndieAuth comment session).

Request:

{
  "parent_id": "comment-id-123",
  "content": "Thanks for the comment!",
  "target": "https://example.com/posts/hello/"
}

Response:

{
  "success": true,
  "comment": {
    "type": "entry",
    "author": {
      "name": "Site Owner",
      "url": "https://example.com",
      "photo": "https://..."
    },
    "content": {
      "text": "Thanks for the comment!",
      "html": "<p>Thanks for the comment!</p>"
    },
    "published": "2026-03-15T12:00:00.000Z"
  }
}

Owner Detection (GET /comments/api/is-owner)

Returns owner status and available syndication targets for the reply-to-interactions feature. Used by the frontend to show reply buttons on interactions.

Response (when owner):

{
  "isOwner": true,
  "name": "Ricardo Mendes",
  "url": "https://rmendes.net",
  "photo": "https://...",
  "syndicationTargets": {
    "bluesky": "https://bsky.social",
    "mastodon": "https://indieweb.social"
  }
}

The syndicationTargets map is auto-detected from Indiekit's registered syndicators. The frontend uses these to route replies to the correct platform via Micropub.

Architecture

Comment Types

This plugin handles two distinct reply flows:

Flow Trigger Route Session
Visitor comment Visitor submits via comment form POST /api/submit IndieAuth comment session
Owner reply to comment Owner replies to a native comment POST /api/reply Indiekit admin session

Reply-to-Interactions (Micropub flow)

When the site owner replies to platform interactions (Mastodon replies, Bluesky mentions, IndieWeb webmentions), the reply goes through Micropub — not through this plugin. The frontend posts to /micropub with in-reply-to set to the interaction's URL and optional mp-syndicate-to for platform-specific threading.

This separation exists because:

  • Native comments are stored in the comments collection and managed by this plugin
  • Platform interactions are stored in the conversation_items collection by @rmdes/indiekit-endpoint-conversations
  • Owner replies to interactions are Micropub posts stored in the posts collection

Collections

Collection Purpose
comments Native visitor comments
comment_sessions IndieAuth session state (PKCE, tokens)

Dependencies

  • indiekit-eleventy-theme — The theme's comments.js provides the Alpine.js comment form, IndieAuth flow, and inline reply UI. webmentions.js provides reply buttons on platform interactions.
  • @rmdes/indiekit-endpoint-conversations — Handles platform interactions (Mastodon/Bluesky/AP). Owner replies to platform interactions go through Micropub, and conversations enriches its API with those replies for frontend threading.

License

MIT