mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
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
128 lines
5.6 KiB
JavaScript
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;
|
|
}
|