Skip to content

Add Mastodon-compatible API endpoints - #159

Merged
melvincarvalho merged 3 commits into
gh-pagesfrom
feature/mastodon-api
Mar 10, 2026
Merged

Add Mastodon-compatible API endpoints#159
melvincarvalho merged 3 commits into
gh-pagesfrom
feature/mastodon-api

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

  • Adds POST /api/v1/apps — dynamic client registration for Mastodon clients
  • Adds GET /api/v1/accounts/verify_credentials — returns WebID profile as Mastodon Account
  • Adds GET /api/v1/instance — instance metadata for client discovery

This is Step 1 of Mastodon API compatibility. After this, clients like Elk, Phanpy, and Ice Cubes can connect to a JSS instance and identify the user.

Motivation

The fediverse has ~10 million active users. JSS already speaks ActivityPub. Adding the Mastodon-compatible API surface lets existing Mastodon clients connect to JSS without users installing anything new — 10 million potential users, zero onboarding friction.

OIDC already provides the OAuth 2.0 layer. These endpoints add the Mastodon-specific API surface on top.

Next steps

  • Step 2: GET /api/v1/timelines/home — pod content as a Mastodon feed
  • Step 3: POST /api/v1/statuses — write to pod, federate via ActivityPub

Closes #158

Test plan

  • POST /api/v1/apps returns client_id and client_secret
  • GET /api/v1/instance returns valid instance metadata
  • GET /api/v1/accounts/verify_credentials returns account object
  • Elk or Phanpy can discover the instance via /api/v1/instance

Adds /api/v1/apps (dynamic client registration),
/api/v1/accounts/verify_credentials, and /api/v1/instance
so Mastodon clients can connect to JSS.

Refs #158

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a minimal set of Mastodon-compatible API endpoints under the existing ActivityPub Fastify plugin so Mastodon clients can attempt to connect to JSS.

Changes:

  • Introduces new Mastodon route handlers for /api/v1/apps, /api/v1/accounts/verify_credentials, and /api/v1/instance.
  • Registers these new routes in the ActivityPub plugin.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/ap/routes/mastodon.js Implements Mastodon-compatible handlers (app registration, verify credentials, instance info) plus an in-memory client store.
src/ap/index.js Wires the new Mastodon endpoints into the ActivityPub Fastify plugin routes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ap/index.js
Comment on lines +178 to +181
// Mastodon-compatible API endpoints
fastify.post('/api/v1/apps', createAppsHandler())
fastify.get('/api/v1/accounts/verify_credentials', createVerifyCredentialsHandler(config))
fastify.get('/api/v1/instance', createInstanceHandler(config))

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Mastodon endpoints will still be subject to the global WAC preHandler authorization hook. In default root ACL generation, public read is not inherited to child paths, so unauthenticated clients are likely to get 401 on /api/v1/instance and /api/v1/apps (both need to be public for Mastodon login/registration). Consider marking these routes as public (e.g., route config: { public: true }) or adding them to the auth-skip list.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +18 to +23
return async (request, reply) => {
const { client_name, redirect_uris, scopes, website } = request.body || {}

if (!client_name || !redirect_uris) {
return reply.code(422).send({ error: 'client_name and redirect_uris are required' })
}

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/v1/apps will reject clients that POST application/x-www-form-urlencoded (common for Mastodon client registration), because request.body won’t be a decoded object without a form-body parser. Consider adding @fastify/formbody (or parsing Buffer bodies for form-encoded requests) so client_name / redirect_uris are actually read.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +10 to +12
// In-memory client store (replace with persistent storage later)
const clients = new Map()

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This in-memory clients Map grows without bounds and has no TTL/eviction. If /api/v1/apps is made publicly accessible (as Mastodon expects), it can be spammed to cause unbounded memory growth. Consider adding a max size + eviction/expiry, persisting to storage, and/or applying per-IP rate limiting on the route.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +58 to +60
display_name: config.displayName,
note: config.summary ? `<p>${config.summary}</p>` : '',
url: `${baseUrl}/profile/card`,

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note is HTML in the Mastodon API, but this interpolates config.summary directly into HTML without escaping/sanitization. If summary can contain user-provided text, Mastodon clients may render it and become vulnerable to XSS/markup injection. Consider HTML-escaping or sanitizing config.summary before wrapping it in <p>…</p>.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +65 to +67
bot: false,
created_at: new Date().toISOString(),
followers_count: 0,

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created_at is being set to the current time on every verify_credentials call, which makes the account appear newly created each request and can confuse clients/caching. Consider using a stable creation timestamp (from config/storage) instead of new Date() per request.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +88 to +100
const protocol = request.headers['x-forwarded-proto'] || request.protocol
const host = request.headers['x-forwarded-host'] || request.hostname
const baseUrl = `${protocol}://${host}`

return reply.send({
uri: host,
title: config.displayName || 'JSS',
description: 'SAND Stack: Solid + ActivityPub + Nostr + DID',
short_description: 'Solid pod with Mastodon-compatible API',
version: '4.0.0 (compatible; JSS 0.0.67)',
urls: {
streaming_api: `wss://${host}`
},

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseUrl is computed but unused, and streaming_api is hard-coded to wss:// regardless of the actual request protocol. Either remove the unused baseUrl or use it to derive streaming_api (e.g., ws vs wss) to avoid incorrect URLs in non-HTTPS/dev setups.

Copilot uses AI. Check for mistakes.
- Skip WAC auth for /api/v1/* routes (clients need public access)
- Parse form-urlencoded bodies (Mastodon clients use form encoding)
- Use stable created_at timestamp instead of per-request new Date()
- Derive ws/wss protocol from request instead of hardcoding wss
- HTML-escape summary in verify_credentials response

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server.js
Comment on lines 365 to 370
request.url.startsWith('/.well-known/') ||
request.url.startsWith('/solidos-ui/') ||
(nostrEnabled && request.url.startsWith(nostrPath)) ||
(gitEnabled && isGitRequest(request.url)) ||
(activitypubEnabled && apPaths.some(p => request.url === p || request.url.startsWith(p + '?'))) ||
(activitypubEnabled && (apPaths.some(p => request.url === p || request.url.startsWith(p + '?')) || request.url.startsWith('/api/v1/'))) ||
isProfileAP ||

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth-skip condition request.url.startsWith('/api/v1/') applies before routing and will also bypass WAC for the catch-all /* resource routes. That makes any Solid resource created under /api/v1/... publicly accessible when ActivityPub is enabled. Restrict the skip to the exact Mastodon endpoints (e.g. /api/v1/apps, /api/v1/instance, /api/v1/accounts/verify_credentials) or key off request.routeOptions.url/request.routerPath so only those Fastify routes bypass auth.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +66 to +74
* GET /api/v1/accounts/verify_credentials — Who am I?
* Returns the authenticated user's profile as a Mastodon Account object
*/
export function createVerifyCredentialsHandler (config) {
return async (request, reply) => {
const protocol = request.headers['x-forwarded-proto'] || request.protocol
const host = request.headers['x-forwarded-host'] || request.hostname
const baseUrl = `${protocol}://${host}`

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verify_credentials currently returns an account for any caller, even without an Authorization header, so it isn't actually verifying credentials (and can confuse Mastodon clients that expect 401 when unauthenticated). Consider requiring a valid token (e.g., via existing auth/token utilities) and returning 401 when no/invalid auth is provided.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +10 to +14
// In-memory client store (replace with persistent storage later)
const clients = new Map()

// Stable instance start time (used for created_at)
const startedAt = new Date().toISOString()

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clients is a module-level Map with no size/TTL limits. Because /api/v1/apps is unauthenticated, this can be abused to grow memory unbounded (and the Map will also be shared across multiple Fastify server instances in the same process). Consider scoping client storage to the Fastify instance and enforcing an eviction policy (TTL/LRU) and/or rate limiting on registration.

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +45 to +48

const clientId = crypto.randomUUID()
const clientSecret = crypto.randomUUID()

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses crypto.randomUUID() but doesn't import crypto. Elsewhere in the repo UUID generation is done via import crypto from 'crypto' or import { randomUUID } from 'crypto'; relying on a global crypto object is less consistent and can break depending on runtime configuration. Please import from Node's crypto module for consistency.

Copilot uses AI. Check for mistakes.
Instead of skipping WAC for all /api/v1/* paths, enumerate the
specific Mastodon endpoints that need public access.
@melvincarvalho
melvincarvalho requested a review from Copilot March 10, 2026 04:37
@melvincarvalho
melvincarvalho merged commit ebc9d95 into gh-pages Mar 10, 2026
4 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server.js
const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js'];
const apPaths = ['/inbox', '/profile/card/inbox', '/profile/card/outbox', '/profile/card/followers', '/profile/card/following'];
const apPaths = ['/inbox', '/profile/card/inbox', '/profile/card/outbox', '/profile/card/followers', '/profile/card/following',
'/api/v1/apps', '/api/v1/instance', '/api/v1/accounts/verify_credentials'];

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/v1/accounts/verify_credentials is included in the auth-skip allowlist, which makes the endpoint callable without any authentication/WAC checks. For a Mastodon-compatible verify_credentials, this should require a valid access token (or at minimum a verified WebID). Suggestion: remove this path from the skip list and/or enforce auth inside the handler so unauthenticated requests get a 401.

Suggested change
'/api/v1/apps', '/api/v1/instance', '/api/v1/accounts/verify_credentials'];
'/api/v1/apps', '/api/v1/instance'];

Copilot uses AI. Check for mistakes.
Comment thread src/ap/routes/mastodon.js
Comment on lines +69 to +81
export function createVerifyCredentialsHandler (config) {
return async (request, reply) => {
const protocol = request.headers['x-forwarded-proto'] || request.protocol
const host = request.headers['x-forwarded-host'] || request.hostname
const baseUrl = `${protocol}://${host}`

const account = {
id: '1',
username: config.username,
acct: config.username,
display_name: config.displayName,
note: config.summary ? `<p>${escapeHtml(config.summary)}</p>` : '',
url: `${baseUrl}/profile/card`,

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET /api/v1/accounts/verify_credentials returns an account object purely from server config and does not check for an access token / authenticated identity. Combined with the server auth bypass, this effectively makes the endpoint public and allows any caller to obtain user metadata. This handler should validate authentication (e.g., require a verified WebID/access token and return 401 when missing/invalid).

Copilot uses AI. Check for mistakes.
melvincarvalho added a commit that referenced this pull request Mar 10, 2026
Document the three new features from PRs #159, #161, #162:
- Mastodon-compatible API endpoints and OAuth 2.0 flow
- remoteStorage protocol (always on, no flag needed)
- Updated project structure with new files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mastodon-compatible OAuth + API endpoints

2 participants