-
Notifications
You must be signed in to change notification settings - Fork 9
Add Mastodon-compatible API endpoints #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| /** | ||
| * Mastodon-compatible API endpoints | ||
| * Allows Mastodon clients (Elk, Phanpy, Ice Cubes) to connect to JSS | ||
| * | ||
| * Step 1: Dynamic client registration + account verification | ||
| * Refs: https://docs.joinmastodon.org/methods/apps/ | ||
| * https://docs.joinmastodon.org/methods/accounts/#verify_credentials | ||
| */ | ||
|
|
||
| // In-memory client store (replace with persistent storage later) | ||
| const clients = new Map() | ||
|
|
||
|
Comment on lines
+10
to
+12
|
||
| // Stable instance start time (used for created_at) | ||
| const startedAt = new Date().toISOString() | ||
|
Comment on lines
+10
to
+14
|
||
|
|
||
| /** | ||
| * Parse request body — handles both JSON and form-urlencoded | ||
| * (JSS uses raw buffer parser for all content types) | ||
| */ | ||
| function parseBody (request) { | ||
| if (request.body && typeof request.body === 'object' && !Buffer.isBuffer(request.body)) { | ||
| return request.body | ||
| } | ||
| const raw = Buffer.isBuffer(request.body) ? request.body.toString() : String(request.body || '') | ||
| const ct = request.headers['content-type'] || '' | ||
| if (ct.includes('application/json')) { | ||
| try { return JSON.parse(raw) } catch { return {} } | ||
| } | ||
| // Default: parse as form-urlencoded | ||
| return Object.fromEntries(new URLSearchParams(raw)) | ||
| } | ||
|
|
||
| /** | ||
| * POST /api/v1/apps — Dynamic client registration | ||
| * Mastodon clients call this to register before OAuth | ||
| */ | ||
| export function createAppsHandler () { | ||
| return async (request, reply) => { | ||
| const body = parseBody(request) | ||
| const { client_name, redirect_uris, scopes, website } = body | ||
|
|
||
| if (!client_name || !redirect_uris) { | ||
| return reply.code(422).send({ error: 'client_name and redirect_uris are required' }) | ||
| } | ||
|
Comment on lines
+38
to
+44
|
||
|
|
||
| const clientId = crypto.randomUUID() | ||
| const clientSecret = crypto.randomUUID() | ||
|
|
||
|
Comment on lines
+45
to
+48
|
||
| const client = { | ||
| id: clientId, | ||
| name: client_name, | ||
| redirect_uri: redirect_uris, | ||
| client_id: clientId, | ||
| client_secret: clientSecret, | ||
| scopes: scopes || 'read', | ||
| website: website || null | ||
| } | ||
|
|
||
| clients.set(clientId, client) | ||
|
|
||
| return reply.send(client) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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}` | ||
|
|
||
|
Comment on lines
+66
to
+74
|
||
| 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`, | ||
|
Comment on lines
+79
to
+81
|
||
| uri: `${baseUrl}/profile/card#me`, | ||
| avatar: `${baseUrl}/profile/avatar.png`, | ||
| header: '', | ||
| locked: false, | ||
| bot: false, | ||
| created_at: startedAt, | ||
| followers_count: 0, | ||
|
Comment on lines
+86
to
+88
|
||
| following_count: 0, | ||
| statuses_count: 0, | ||
| source: { | ||
| privacy: 'public', | ||
| sensitive: false, | ||
| language: 'en', | ||
| note: config.summary || '' | ||
| } | ||
| } | ||
|
|
||
| return reply.send(account) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/v1/instance — Instance information | ||
| * Required by most Mastodon clients before login | ||
| */ | ||
| export function createInstanceHandler (config) { | ||
| return async (request, reply) => { | ||
| const protocol = request.headers['x-forwarded-proto'] || request.protocol | ||
| const host = request.headers['x-forwarded-host'] || request.hostname | ||
| const wsProtocol = protocol === 'https' ? 'wss' : 'ws' | ||
|
|
||
| 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: `${wsProtocol}://${host}` | ||
| }, | ||
|
Comment on lines
+109
to
+121
|
||
| stats: { | ||
| user_count: 1, | ||
| status_count: 0, | ||
| domain_count: 1 | ||
| }, | ||
| languages: ['en'], | ||
| registrations: false, | ||
| approval_required: false, | ||
| configuration: { | ||
| statuses: { max_characters: 5000 }, | ||
| media_attachments: { supported_mime_types: [] } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Look up a registered client | ||
| */ | ||
| export function getClient (clientId) { | ||
| return clients.get(clientId) || null | ||
| } | ||
|
|
||
| function escapeHtml (str) { | ||
| return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | ||
| } | ||
|
|
||
| export default { | ||
| createAppsHandler, | ||
| createVerifyCredentialsHandler, | ||
| createInstanceHandler, | ||
| getClient | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -353,7 +353,8 @@ export function createServer(options = {}) { | |||||
| fastify.addHook('preHandler', async (request, reply) => { | ||||||
| // Skip auth for pod creation, OPTIONS, IdP routes, mashlib, solidos-ui, well-known, notifications, nostr, git, and AP | ||||||
| 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']; | ||||||
|
||||||
| '/api/v1/apps', '/api/v1/instance', '/api/v1/accounts/verify_credentials']; | |
| '/api/v1/apps', '/api/v1/instance']; |
There was a problem hiding this comment.
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
preHandlerauthorization 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/instanceand/api/v1/apps(both need to be public for Mastodon login/registration). Consider marking these routes as public (e.g., routeconfig: { public: true }) or adding them to the auth-skip list.