2c0cfffd54
Implement the Mastodon Client REST API (/api/v1/*, /api/v2/*) and OAuth2 server within the ActivityPub plugin, enabling Mastodon-compatible clients to connect to the Fedify-based server. Core features: - OAuth2 with PKCE (S256) — app registration, authorization, token exchange - Instance info + nodeinfo for client discovery - Account lookup, verification, relationships, follow/unfollow/mute/block - Home/public/hashtag timelines with cursor-based pagination - Status viewing, creation, deletion, thread context - Favourite, boost, bookmark interactions with AP federation - Notifications with type filtering and pagination - Search across accounts, statuses, and hashtags - Markers for read position tracking - Bookmarks and favourites collection lists - 25+ stub endpoints preventing client errors on unimplemented features Architecture: - 24 new files under lib/mastodon/ (entities, helpers, middleware, routes) - Virtual endpoint at "/" via Indiekit.addEndpoint() for domain-root access - CORS + JSON error handling for browser-based clients - Six-layer mute/block filtering reusing existing moderation infrastructure BREAKING CHANGE: bumps to v3.0.0 — adds new MongoDB collections (ap_oauth_apps, ap_oauth_tokens, ap_markers) and new route registrations Confab-Link: http://localhost:8080/sessions/5360e3f5-b3cc-4bf3-8c31-5448e2b23947
87 lines
2.3 KiB
JavaScript
87 lines
2.3 KiB
JavaScript
/**
|
|
* Scope enforcement middleware for Mastodon Client API.
|
|
*
|
|
* Supports scope hierarchy: parent scope covers all children.
|
|
* "read" grants "read:accounts", "read:statuses", etc.
|
|
* "write" grants "write:statuses", "write:favourites", etc.
|
|
*
|
|
* Legacy "follow" scope maps to read/write for blocks, follows, and mutes.
|
|
*/
|
|
|
|
/**
|
|
* Scopes that the legacy "follow" scope grants access to.
|
|
*/
|
|
const FOLLOW_SCOPE_EXPANSION = [
|
|
"read:blocks",
|
|
"write:blocks",
|
|
"read:follows",
|
|
"write:follows",
|
|
"read:mutes",
|
|
"write:mutes",
|
|
];
|
|
|
|
/**
|
|
* Create middleware that checks if the token has the required scope.
|
|
*
|
|
* @param {...string} requiredScopes - One or more scopes (any match = pass)
|
|
* @returns {Function} Express middleware
|
|
*/
|
|
export function scopeRequired(...requiredScopes) {
|
|
return (req, res, next) => {
|
|
const token = req.mastodonToken;
|
|
if (!token) {
|
|
return res.status(401).json({
|
|
error: "The access token is invalid",
|
|
});
|
|
}
|
|
|
|
const grantedScopes = token.scopes || [];
|
|
|
|
const hasScope = requiredScopes.some((required) =>
|
|
checkScope(grantedScopes, required),
|
|
);
|
|
|
|
if (!hasScope) {
|
|
return res.status(403).json({
|
|
error: `This action is outside the authorized scopes. Required: ${requiredScopes.join(" or ")}`,
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if granted scopes satisfy a required scope.
|
|
*
|
|
* Rules:
|
|
* - Exact match: "read:accounts" satisfies "read:accounts"
|
|
* - Parent match: "read" satisfies "read:accounts"
|
|
* - "follow" expands to read/write for blocks, follows, mutes
|
|
* - "profile" satisfies "read:accounts" (for verify_credentials)
|
|
*
|
|
* @param {string[]} granted - Scopes on the token
|
|
* @param {string} required - Scope being checked
|
|
* @returns {boolean}
|
|
*/
|
|
function checkScope(granted, required) {
|
|
// Exact match
|
|
if (granted.includes(required)) return true;
|
|
|
|
// Parent scope: "read" covers "read:*", "write" covers "write:*"
|
|
const [parent] = required.split(":");
|
|
if (parent && granted.includes(parent)) return true;
|
|
|
|
// Legacy "follow" scope expansion
|
|
if (granted.includes("follow") && FOLLOW_SCOPE_EXPANSION.includes(required)) {
|
|
return true;
|
|
}
|
|
|
|
// "profile" scope can satisfy "read:accounts"
|
|
if (required === "read:accounts" && granted.includes("profile")) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|