Files
svemagie de08cd2796 chore: bump @indiekit peer deps to beta.27, fix duplicate validate keys in rate limiters
- @indiekit/endpoint-micropub, @indiekit/error, @indiekit/frontend: ^1.0.0-beta.25 → ^1.0.0-beta.27
- express-rate-limit: stays at ^7.5.1 (v8 has breaking changes to standardHeaders boolean and validate.trustProxy API)
- Remove duplicate validate: { trustProxy: false } keys in apiLimiter, authLimiter, appRegistrationLimiter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:37:39 +02:00

142 lines
6.5 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 { loadSettingsMiddleware } from "./middleware/load-settings.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 filtersRouter from "./routes/filters.js";
import stubsRouter from "./routes/stubs.js";
// Rate limiters for different endpoint categories.
// validate.trustProxy disabled — Indiekit sets Express trust proxy to true
// (behind Cloudron/nginx), which express-rate-limit v7+ rejects as too
// permissive. The proxy is trusted infrastructure, not user-controlled.
const apiLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 300,
standardHeaders: true,
legacyHeaders: false,
validate: { trustProxy: false }, // behind nginx reverse proxy; trust proxy is intentional
message: { error: "Too many requests, please try again later" },
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 30,
standardHeaders: true,
legacyHeaders: false,
validate: { trustProxy: false },
message: { error: "Too many authentication attempts" },
});
const appRegistrationLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 25,
standardHeaders: true,
legacyHeaders: false,
validate: { trustProxy: 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.
// Note: multipart/form-data is handled globally by express-fileupload
// (configured in Indiekit's express.js), so no multer needed here.
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);
// ─── Settings cache ────────────────────────────────────────────────────
// Loads plugin settings once per minute, available as req.app.locals.apSettings
router.use("/api", loadSettingsMiddleware);
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(filtersRouter);
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;
}