Files
indiekit-endpoint-activitypub/lib/mastodon/router.js
Ricardo 12454749ad fix: comprehensive security, performance, and architecture audit fixes
27 issues fixed from multi-dimensional code review (4 Critical, 6 High, 11 Medium, 6 Low):

Security (Critical):
- Escape HTML in OAuth authorization page to prevent XSS (C1)
- Add CSRF protection to OAuth authorize flow (C2)
- Replace bypassable regex sanitizer with sanitize-html library (C3)
- Enforce OAuth scopes on all Mastodon API routes (C4)

Security (Medium/Low):
- Fix SSRF via DNS resolution before private IP check (M1)
- Add rate limiting to API, auth, and app registration endpoints (M2)
- Validate redirect_uri on POST /oauth/authorize (M4)
- Fix custom emoji URL injection with scheme validation + escaping (M5)
- Remove data: scheme from allowed image sources (L6)
- Add access token expiry (1hr) and refresh token rotation (90d) (M3)
- Hash client secrets before storage (L3)

Architecture:
- Extract batch-broadcast.js — shared delivery logic (H1a)
- Extract init-indexes.js — MongoDB index creation (H1b)
- Extract syndicator.js — syndication logic (H1c)
- Create federation-actions.js facade for controllers (M6)
- index.js reduced from 1810 to ~1169 lines (35%)

Performance:
- Cache moderation data with 30s TTL + write invalidation (H6)
- Increase inbox queue throughput to 10 items/sec (H5)
- Make account enrichment non-blocking with fire-and-forget (H4)
- Remove ephemeral getReplies/getLikes/getShares from ingest (M11)
- Fix LRU caches to use true LRU eviction (L1)
- Fix N+1 backfill queries with batch $in lookup (L2)

UI/UX:
- Split 3441-line reader.css into 15 feature-scoped files (H2)
- Extract inline Alpine.js interaction component (H3)
- Reduce sidebar navigation from 7 to 3 items (M7)
- Add ARIA live regions for dynamic content updates (M8)
- Extract shared CW/non-CW content partial (M9)
- Document form handling pattern convention (M10)
- Add accessible labels to functional emoji icons (L4)
- Convert profile editor to Alpine.js (L5)

Audit: documentation-central/audits/2026-03-24-activitypub-code-review.md
Plan: documentation-central/plans/2026-03-24-activitypub-audit-fixes.md
2026-03-25 07:41:20 +01:00

128 lines
5.6 KiB
JavaScript

/**
* Mastodon Client API — main router.
*
* Combines all sub-routers, applies CORS and error handling middleware.
* Mounted at "/" via Indiekit.addEndpoint() so Mastodon clients can access
* /api/v1/*, /api/v2/*, /oauth/* at the domain root.
*/
import express from "express";
import rateLimit from "express-rate-limit";
import { corsMiddleware } from "./middleware/cors.js";
import { tokenRequired, optionalToken } from "./middleware/token-required.js";
import { errorHandler, notImplementedHandler } from "./middleware/error-handler.js";
// Route modules
import oauthRouter from "./routes/oauth.js";
import instanceRouter from "./routes/instance.js";
import accountsRouter from "./routes/accounts.js";
import statusesRouter from "./routes/statuses.js";
import timelinesRouter from "./routes/timelines.js";
import notificationsRouter from "./routes/notifications.js";
import searchRouter from "./routes/search.js";
import mediaRouter from "./routes/media.js";
import stubsRouter from "./routes/stubs.js";
// Rate limiters for different endpoint categories
const apiLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 300,
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many requests, please try again later" },
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 30,
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many authentication attempts" },
});
const appRegistrationLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 25,
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many app registrations" },
});
/**
* Create the combined Mastodon API router.
*
* @param {object} options
* @param {object} options.collections - MongoDB collections object
* @param {object} [options.pluginOptions] - Plugin options (handle, etc.)
* @returns {import("express").Router} Express router
*/
export function createMastodonRouter({ collections, pluginOptions = {} }) {
const router = express.Router(); // eslint-disable-line new-cap
// ─── Body parsers ───────────────────────────────────────────────────────
// Mastodon clients send JSON, form-urlencoded, and occasionally text/plain.
// These must be applied before route handlers.
router.use("/api", express.json());
router.use("/api", express.urlencoded({ extended: true }));
router.use("/oauth", express.json());
router.use("/oauth", express.urlencoded({ extended: true }));
// ─── CORS ───────────────────────────────────────────────────────────────
router.use("/api", corsMiddleware);
router.use("/oauth/token", corsMiddleware);
router.use("/oauth/revoke", corsMiddleware);
router.use("/.well-known/oauth-authorization-server", corsMiddleware);
// ─── Rate limiting ─────────────────────────────────────────────────────
router.use("/api", apiLimiter);
router.use("/oauth/token", authLimiter);
router.use("/api/v1/apps", appRegistrationLimiter);
// ─── Inject collections + plugin options into req ───────────────────────
router.use("/api", (req, res, next) => {
req.app.locals.mastodonCollections = collections;
req.app.locals.mastodonPluginOptions = pluginOptions;
next();
});
router.use("/oauth", (req, res, next) => {
req.app.locals.mastodonCollections = collections;
req.app.locals.mastodonPluginOptions = pluginOptions;
next();
});
router.use("/.well-known/oauth-authorization-server", (req, res, next) => {
req.app.locals.mastodonCollections = collections;
req.app.locals.mastodonPluginOptions = pluginOptions;
next();
});
// ─── Token resolution ───────────────────────────────────────────────────
// Apply optional token resolution to all API routes so handlers can check
// req.mastodonToken. Specific routes that require auth use tokenRequired.
router.use("/api", optionalToken);
// ─── OAuth routes (no token required for most) ──────────────────────────
router.use(oauthRouter);
// ─── Public API routes (no auth required) ───────────────────────────────
router.use(instanceRouter);
// ─── Authenticated API routes ───────────────────────────────────────────
router.use(accountsRouter);
router.use(statusesRouter);
router.use(timelinesRouter);
router.use(notificationsRouter);
router.use(searchRouter);
router.use(mediaRouter);
router.use(stubsRouter);
// ─── Catch-all for unimplemented endpoints ──────────────────────────────
// Express 5 path-to-regexp v8: use {*name} for wildcard
router.all("/api/v1/{*rest}", notImplementedHandler);
router.all("/api/v2/{*rest}", notImplementedHandler);
// ─── Error handler ──────────────────────────────────────────────────────
router.use("/api", errorHandler);
router.use("/oauth", errorHandler);
return router;
}