de08cd2796
- @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>
142 lines
6.5 KiB
JavaScript
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;
|
|
}
|