| [834bcc3] | 1 | // routes/federation.js — public Cirkels endpoints (v1, publication side).
|
|---|
| [b300682] | 2 | //
|
|---|
| [834bcc3] | 3 | // GET /.klonkt/actor.json — ActivityStreams actor + Ed25519 public key
|
|---|
| 4 | // GET /.klonkt/outbox.json — public posts as AS Create objects,
|
|---|
| 5 | // signed via the Klonkt-Signature header
|
|---|
| [b300682] | 6 | //
|
|---|
| [834bcc3] | 7 | // Site-agnostic and unauthenticated — read-only. See docs/cirkels-v1-spec.md.
|
|---|
| [b300682] | 8 |
|
|---|
| 9 | import express from 'express';
|
|---|
| [f63cbc2] | 10 | import { buildActor, buildOutbox, signBody, KLONKT_PROTO, MIN_PROTO } from '../services/CircleFederation.js';
|
|---|
| [efdde37] | 11 | import { getTenancy } from '../services/SettingsService.js';
|
|---|
| [b300682] | 12 |
|
|---|
| 13 | const router = express.Router();
|
|---|
| 14 |
|
|---|
| 15 | function baseUrl(req) {
|
|---|
| 16 | const b = process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`;
|
|---|
| 17 | return b.replace(/\/+$/, '');
|
|---|
| 18 | }
|
|---|
| 19 |
|
|---|
| [834bcc3] | 20 | // The proto the consumer claims to be running (from their request header), or 0.
|
|---|
| [f63cbc2] | 21 | function consumerProto(req) {
|
|---|
| 22 | return parseInt(req.get('Klonkt-Proto') || '0', 10) || 0;
|
|---|
| 23 | }
|
|---|
| 24 |
|
|---|
| [b300682] | 25 | router.get('/.klonkt/actor.json', (req, res) => {
|
|---|
| [834bcc3] | 26 | // Circles = solo-to-solo; hubs do not publish a federation actor.
|
|---|
| [efdde37] | 27 | if (getTenancy() === 'hub') return res.status(404).type('text/plain').send('Niet beschikbaar in hub-modus');
|
|---|
| [834bcc3] | 28 | // We ALWAYS serve the actor (including to older consumers) so they can read our
|
|---|
| 29 | // proto and show a clean "update required" message.
|
|---|
| [b300682] | 30 | const body = JSON.stringify(buildActor(baseUrl(req)), null, 2);
|
|---|
| 31 | res.type('application/activity+json; charset=utf-8');
|
|---|
| [f63cbc2] | 32 | res.set('Klonkt-Proto', String(KLONKT_PROTO));
|
|---|
| [b300682] | 33 | res.set('Cache-Control', 'public, max-age=300');
|
|---|
| 34 | res.send(body);
|
|---|
| 35 | });
|
|---|
| 36 |
|
|---|
| 37 | router.get('/.klonkt/outbox.json', (req, res) => {
|
|---|
| [efdde37] | 38 | if (getTenancy() === 'hub') return res.status(404).type('text/plain').send('Niet beschikbaar in hub-modus');
|
|---|
| [f63cbc2] | 39 | res.set('Klonkt-Proto', String(KLONKT_PROTO));
|
|---|
| [834bcc3] | 40 | // Consumer too old? Reject with 426 Upgrade Required (the crypto binding already
|
|---|
| 41 | // excludes them; this gives an explicit, readable signal). proto 0 = no header
|
|---|
| 42 | // (e.g. a browser/curl) → allow, they won't verify anyway.
|
|---|
| [f63cbc2] | 43 | const cp = consumerProto(req);
|
|---|
| 44 | if (cp && cp < MIN_PROTO) {
|
|---|
| 45 | return res.status(426).type('text/plain')
|
|---|
| 46 | .send(`Upgrade Required: deze cirkel draait proto ${KLONKT_PROTO}; jouw Klonkt (proto ${cp}) is te oud.`);
|
|---|
| 47 | }
|
|---|
| [b300682] | 48 | const body = JSON.stringify(buildOutbox(baseUrl(req)), null, 2);
|
|---|
| 49 | res.type('application/activity+json; charset=utf-8');
|
|---|
| 50 | res.set('Cache-Control', 'public, max-age=300');
|
|---|
| 51 | res.set('Klonkt-Signature', `ed25519=${signBody(body)}`);
|
|---|
| 52 | res.send(body);
|
|---|
| 53 | });
|
|---|
| 54 |
|
|---|
| 55 | export default router;
|
|---|