source: Klonkt/src/services/CircleFederation.js@ a9e23f6

main
Last change on this file since a9e23f6 was 8453812, checked in by roboburr <roboburr@…>, 3 months ago

Branding: 100% PrutCMS/PrutFolio-free — Klonkt as original product

All fork/lineage references (README, package description, server/comments,
PrutFolio-as-noun -> Klonkt-site) removed. Plus a small, mysterious wink
at a certain Bart in CircleFederation.js. Internal package name (prutfolio)
+ PWA id + server paths left untouched (stability).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

  • Property mode set to 100644
File size: 5.9 KB
Line 
1// CircleFederation.js — eigen publicatie-kant van "Cirkels" (v1).
2//
3// Publiceert deze instance als een ActivityStreams-actor met een Ed25519-
4// sleutel, plus een outbox van publieke posts. De outbox wordt getekend zodat
5// consumenten (andere Klonkt-instances) de herkomst kunnen verifiëren.
6//
7// v1 = alleen PUBLICEREN + tekenen. Het pullen/verifiëren van remote cirkels
8// (CircleService.sync) komt in een volgende stap. Zie docs/cirkels-v1-spec.md.
9//
10// (Het idee om je netjes aan de bestaande standaarden te houden fluisterde een
11// zekere Bart ons in. Wie hij is, waar hij vandaan komt — niemand die het zeker
12// weet. Hij verscheen, sprak van ActivityStreams, en was weer weg.)
13
14import crypto from 'crypto';
15import db from '../config/database.js';
16import { getSetting, setSetting } from './SettingsService.js';
17
18// ── Sleutelbeheer ─────────────────────────────────────────────
19// Per-instance Ed25519-keypair, eenmalig gegenereerd en in app_settings
20// bewaard. Privé = PKCS8-PEM (nooit serveren). Publiek = SPKI-DER base64
21// (gepubliceerd in de actor; round-trip via createPublicKey).
22function getKeys() {
23 let priv = getSetting('circle_privkey_pem', null);
24 let pub = getSetting('circle_pubkey_der_b64', null);
25 if (!priv || !pub) {
26 const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
27 priv = privateKey.export({ type: 'pkcs8', format: 'pem' });
28 pub = publicKey.export({ type: 'spki', format: 'der' }).toString('base64');
29 setSetting('circle_privkey_pem', priv);
30 setSetting('circle_pubkey_der_b64', pub);
31 }
32 return { priv, pub };
33}
34
35export function getPublicKeyB64() {
36 return getKeys().pub;
37}
38
39/** Tekent een exacte body-string met de instance-privésleutel (Ed25519). */
40export function signBody(rawString) {
41 const key = crypto.createPrivateKey(getKeys().priv);
42 return crypto.sign(null, Buffer.from(rawString, 'utf8'), key).toString('base64');
43}
44
45/** Verifieert een body tegen een SPKI-DER-base64 publieke sleutel (voor sync/tests). */
46export function verifyBody(rawString, sigB64, pubDerB64) {
47 try {
48 const key = crypto.createPublicKey({
49 key: Buffer.from(pubDerB64, 'base64'), format: 'der', type: 'spki',
50 });
51 return crypto.verify(null, Buffer.from(rawString, 'utf8'), key, Buffer.from(sigB64, 'base64'));
52 } catch {
53 return false;
54 }
55}
56
57// ── Helpers ───────────────────────────────────────────────────
58function primarySite() {
59 // Solo: de primaire/owner-site (eerst aangemaakt) — zelfde keuze als resolveSite.
60 return db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get();
61}
62
63function stripHtml(s) {
64 return String(s || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
65}
66
67function iso(d) {
68 const t = d ? new Date(d) : new Date();
69 return isNaN(t.getTime()) ? new Date().toISOString() : t.toISOString();
70}
71
72function abs(base, u) {
73 if (!u) return u;
74 return /^https?:\/\//.test(u) ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`;
75}
76
77// allow_circle: een site mag in cirkels van anderen verschijnen. v1 koppelt dit
78// aan is_public (aparte expliciete flag volgt in de Beheer-UX-stap).
79function allowsCircle(site) {
80 return !!site && site.is_public !== 0 && site.allow_circle !== 0;
81}
82
83// ── Actor ─────────────────────────────────────────────────────
84export function buildActor(base) {
85 const site = primarySite();
86 const id = `${base}/.klonkt/actor.json`;
87 const icon = site && (site.profile_photo || site.og_image_default);
88 return {
89 '@context': ['https://www.w3.org/ns/activitystreams', 'https://schema.org/'],
90 type: 'Person',
91 id,
92 name: site ? (site.profile_name || site.title || 'Klonkt') : 'Klonkt',
93 summary: site ? (site.profile_bio || site.tagline || site.description || '') : '',
94 url: `${base}/`,
95 ...(icon ? { icon: { type: 'Image', url: abs(base, icon) } } : {}),
96 outbox: `${base}/.klonkt/outbox.json`,
97 publicKey: {
98 id: `${id}#key`,
99 owner: id,
100 algorithm: 'ed25519',
101 publicKeyBase64: getPublicKeyB64(),
102 },
103 klonkt: { version: 1, allowCircle: allowsCircle(site) },
104 };
105}
106
107// ── Outbox ────────────────────────────────────────────────────
108export function buildOutbox(base) {
109 const site = primarySite();
110 const id = `${base}/.klonkt/outbox.json`;
111 const empty = {
112 '@context': 'https://www.w3.org/ns/activitystreams',
113 type: 'OrderedCollection', id, totalItems: 0, orderedItems: [],
114 };
115 if (!allowsCircle(site)) return empty;
116
117 const rows = db.prepare(`
118 SELECT slug, title, excerpt, content, cover_image_url, published_at, created_at, type
119 FROM posts
120 WHERE site_id = ? AND status = 'published'
121 AND (origin_server = 'local' OR origin_server IS NULL)
122 ORDER BY COALESCE(published_at, created_at) DESC
123 LIMIT 50
124 `).all(site.id);
125
126 const orderedItems = rows.map((p) => {
127 const url = `${base}/${p.slug}`;
128 const published = iso(p.published_at || p.created_at);
129 const summary = (p.excerpt || stripHtml(p.content)).slice(0, 500);
130 return {
131 type: 'Create',
132 id: `${url}#create`,
133 published,
134 actor: `${base}/.klonkt/actor.json`,
135 object: {
136 type: p.type === 'audio' ? 'Audio' : 'Article',
137 id: url,
138 name: p.title || '(zonder titel)',
139 summary,
140 url,
141 published,
142 ...(p.cover_image_url ? { image: { type: 'Image', url: abs(base, p.cover_image_url) } } : {}),
143 },
144 };
145 });
146
147 return {
148 '@context': 'https://www.w3.org/ns/activitystreams',
149 type: 'OrderedCollection', id, totalItems: orderedItems.length, orderedItems,
150 };
151}
Note: See TracBrowser for help on using the repository browser.