Changeset d3b9f68 in Klonkt


Ignore:
Timestamp:
06/30/2026 02:38:47 AM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
432831c
Parents:
e0a1ec1
Message:

feat(federation): full JSON-LD @context on all AP objects (valid AS2)

Every object now carries a complete @context (AS2 + security/v1 + term definitions for the
Mastodon/toot + schema.org extensions we emit: sensitive, Hashtag, manuallyApprovesFollowers,
discoverable, featured, PropertyValue, embedUrl). Previously these were emitted under only the
bare AS2 context, so a strict JSON-LD processor dropped them — Mastodon tolerated it but it
wasn't valid AS2/JSON-LD. It is the same context shape Mastodon publishes, so Mastodon sees no
change while strict consumers now resolve every term.

  • src/services/ActivityPubService.js — shared AP_CONTEXT on every object/activity; removed the now-dead local AS2 string const
  • src/routes/activitypub.js — served objects (note/replies/notes route) use AP.AP_CONTEXT

Co-Authored-By: Claude <noreply@…>

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    re0a1ec1 rd3b9f68  
    132132      return res.redirect(302, src);
    133133    }
    134     return AP.sendAP(res, { '@context': 'https://www.w3.org/ns/activitystreams', ...note });
     134    return AP.sendAP(res, { '@context': AP.AP_CONTEXT, ...note });
    135135  }
    136136  const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(post.site_id);
     
    142142    return res.redirect(302, note.url || (baseUrl(req) + '/'));
    143143  }
    144   AP.sendAP(res, { '@context': 'https://www.w3.org/ns/activitystreams', ...note });
     144  AP.sendAP(res, { '@context': AP.AP_CONTEXT, ...note });
    145145});
    146146
     
    150150  const items = AP.getReplyUris(base, req.params.id);
    151151  AP.sendAP(res, {
    152     '@context': 'https://www.w3.org/ns/activitystreams',
     152    '@context': AP.AP_CONTEXT,
    153153    id: `${base}/ap/notes/${req.params.id}/replies`,
    154154    type: 'OrderedCollection',
  • src/services/ActivityPubService.js

    re0a1ec1 rd3b9f68  
    2323
    2424const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
     25// Full JSON-LD context for every AP object we emit: AS2 core + security (publicKey) + the
     26// extension terms we actually use (Mastodon/toot + schema.org), each with a term definition
     27// so a strict JSON-LD processor resolves them instead of dropping them → valid AS2/JSON-LD.
     28// This is the same context shape Mastodon publishes, so Mastodon sees no change.
     29const AP_CONTEXT = [
     30  'https://www.w3.org/ns/activitystreams',
     31  'https://w3id.org/security/v1',
     32  {
     33    toot: 'http://joinmastodon.org/ns#',
     34    schema: 'http://schema.org#',
     35    sensitive: 'as:sensitive',
     36    Hashtag: 'as:Hashtag',
     37    manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
     38    discoverable: 'toot:discoverable',
     39    featured: { '@id': 'toot:featured', '@type': '@id' },
     40    PropertyValue: 'schema:PropertyValue',
     41    value: 'schema:value',
     42    embedUrl: { '@id': 'schema:embedUrl', '@type': '@id' },
     43  },
     44];
    2545
    2646// Short random suffix so two activity ids minted in the same millisecond (e.g.
     
    125145  const keys = getOrCreateKeys(site.slug);
    126146  const actor = {
    127     '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
     147    '@context': AP_CONTEXT,
    128148    id,
    129149    type: 'Person',
     
    351371  const note = buildNote(base, site, post);
    352372  return {
    353     '@context': 'https://www.w3.org/ns/activitystreams',
     373    '@context': AP_CONTEXT,
    354374    id: note.id + '#create',
    355375    type: 'Create',
     
    366386  const items = (posts || []).slice(0, MAX_OUTBOX).map((p) => buildCreate(base, site, p));
    367387  return {
    368     '@context': 'https://www.w3.org/ns/activitystreams',
     388    '@context': AP_CONTEXT,
    369389    id,
    370390    type: 'OrderedCollection',
     
    377397  const id = `${actorId(base, site.slug)}/followers`;
    378398  return {
    379     '@context': 'https://www.w3.org/ns/activitystreams',
     399    '@context': AP_CONTEXT,
    380400    id,
    381401    type: 'OrderedCollection',
     
    390410  const id = `${actorId(base, site.slug)}/following`;
    391411  return {
    392     '@context': 'https://www.w3.org/ns/activitystreams',
     412    '@context': AP_CONTEXT,
    393413    id,
    394414    type: 'OrderedCollection',
     
    405425  const items = (posts || []).map((p) => buildNote(base, site, p));
    406426  return {
    407     '@context': 'https://www.w3.org/ns/activitystreams',
     427    '@context': AP_CONTEXT,
    408428    id,
    409429    type: 'OrderedCollection',
     
    721741    const me = actorId(base, slug);
    722742    const keys = getOrCreateKeys(slug);
    723     const accept = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}#accept-${Date.now()}-${rid()}`, type: 'Accept', actor: me, object: act };
     743    const accept = { '@context': AP_CONTEXT, id: `${me}#accept-${Date.now()}-${rid()}`, type: 'Accept', actor: me, object: act };
    724744    deliver(remote.inbox, accept, `${me}#main-key`, keys.private_pem).catch((e) => console.warn('[AP] Accept delivery failed:', e.message));
    725745    // Auto-backfill: send our recent posts as Create so the instance has our history
     
    909929  const nid = noteId(base, post.id);
    910930  const del = {
    911     '@context': 'https://www.w3.org/ns/activitystreams',
     931    '@context': AP_CONTEXT,
    912932    id: `${nid}#delete-${Date.now()}-${rid()}`,
    913933    type: 'Delete',
     
    932952  note.updated = new Date().toISOString();
    933953  const update = {
    934     '@context': 'https://www.w3.org/ns/activitystreams',
     954    '@context': AP_CONTEXT,
    935955    id: `${noteId(base, post.id)}#update-${Date.now()}-${rid()}`,
    936956    type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`],
     
    952972  const me = actorId(base, site.slug);
    953973  const update = {
    954     '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
     974    '@context': AP_CONTEXT,
    955975    id: `${me}#update-${Date.now()}-${rid()}`,
    956976    type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`],
     
    976996  const keyId = `${me}#main-key`;
    977997  const featured = `${me}/featured`;
    978   const AS = 'https://www.w3.org/ns/activitystreams';
    979998  const note = (id) => noteId(base, id);
    980999  const pinned = db.prepare(
     
    9861005  // 1. Remove every current pin so Mastodon can recreate them in order.
    9871006  for (const id of removeIds) {
    988     const rm = { '@context': AS, id: `${me}#rm-${id}-${Date.now()}-${rid()}`, type: 'Remove', actor: me, object: note(id), target: featured, to: [PUBLIC] };
     1007    const rm = { '@context': AP_CONTEXT, id: `${me}#rm-${id}-${Date.now()}-${rid()}`, type: 'Remove', actor: me, object: note(id), target: featured, to: [PUBLIC] };
    9891008    for (const inbox of inboxes) deliver(inbox, rm, keyId, keys.private_pem).catch(() => { /* best-effort */ });
    9901009  }
     
    9931012  // 2. Add in rank-DESC order, gaps so each StatusPin gets an increasing created_at.
    9941013  for (const p of pinned) {
    995     const add = { '@context': AS, id: `${me}#add-${p.id}-${Date.now()}-${rid()}`, type: 'Add', actor: me, object: note(p.id), target: featured, to: [PUBLIC], cc: [`${me}/followers`] };
     1014    const add = { '@context': AP_CONTEXT, id: `${me}#add-${p.id}-${Date.now()}-${rid()}`, type: 'Add', actor: me, object: note(p.id), target: featured, to: [PUBLIC], cc: [`${me}/followers`] };
    9961015    for (const inbox of inboxes) deliver(inbox, add, keyId, keys.private_pem).catch(() => { /* best-effort */ });
    9971016    await new Promise((r) => setTimeout(r, 2000));
     
    11461165  const note = buildReplyNote(base, site, row);
    11471166  const create = {
    1148     '@context': 'https://www.w3.org/ns/activitystreams',
     1167    '@context': AP_CONTEXT,
    11491168    id: note.id + '#create', type: 'Create', actor: me,
    11501169    published: note.published, to: note.to, cc: note.cc, object: note,
     
    12671286    const me = actorId(base, site.slug);
    12681287    const nid = noteId(base, row.id);
    1269     const del = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${nid}#delete-${Date.now()}-${rid()}`, type: 'Delete', actor: me, to: [PUBLIC], object: { id: nid, type: 'Tombstone' } };
     1288    const del = { '@context': AP_CONTEXT, id: `${nid}#delete-${Date.now()}-${rid()}`, type: 'Delete', actor: me, to: [PUBLIC], object: { id: nid, type: 'Tombstone' } };
    12701289    const keys = getOrCreateKeys(site.slug);
    12711290    const inboxes = new Set();
     
    13001319  note.updated = new Date().toISOString();
    13011320  const update = {
    1302     '@context': 'https://www.w3.org/ns/activitystreams',
     1321    '@context': AP_CONTEXT,
    13031322    id: `${note.id}#update-${Date.now()}-${rid()}`, type: 'Update', actor: me,
    13041323    published: note.published, updated: note.updated, to: note.to, cc: note.cc, object: note,
     
    15161535  const followId = `${me}#follow-${Date.now()}-${rid()}`;
    15171536  fwStmts().ins.run(site.slug, actor.id, ai.handle, ai.name, ai.icon, ai.url, actor.inbox, followId, 'pending', autoBoost ? 1 : 0);
    1518   const follow = { '@context': 'https://www.w3.org/ns/activitystreams', id: followId, type: 'Follow', actor: me, object: actor.id };
     1537  const follow = { '@context': AP_CONTEXT, id: followId, type: 'Follow', actor: me, object: actor.id };
    15191538  try { await deliver(actor.inbox, follow, `${me}#main-key`, keys.private_pem); }
    15201539  catch (e) { console.warn('[AP] follow deliver failed:', e.message); }
     
    15461565  // rather than send an unmatchable one. Deliver durably via the retry queue.
    15471566  if (row && row.inbox && row.follow_id) {
    1548     const undo = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}/undo/${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: { id: row.follow_id, type: 'Follow', actor: me, object: actorUri } };
     1567    const undo = { '@context': AP_CONTEXT, id: `${me}/undo/${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: { id: row.follow_id, type: 'Follow', actor: me, object: actorUri } };
    15491568    deliverWithRetry(site.slug, row.inbox, undo, `${me}#main-key`, keys.private_pem);
    15501569  } else if (row && row.inbox) {
     
    15761595    const inner = kind === 'unboost' ? 'Announce' : 'Like';
    15771596    act = {
    1578       '@context': 'https://www.w3.org/ns/activitystreams',
     1597      '@context': AP_CONTEXT,
    15791598      id: `${me}/undo/${Date.now()}-${rid()}`, type: 'Undo', actor: me,
    15801599      object: { id: `${me}/${inner.toLowerCase()}/${Date.now()}-${rid()}`, type: inner, actor: me, object: targetNoteId },
     
    15841603    const type = kind === 'boost' ? 'Announce' : 'Like';
    15851604    act = {
    1586       '@context': 'https://www.w3.org/ns/activitystreams',
     1605      '@context': AP_CONTEXT,
    15871606      id: `${me}/${type.toLowerCase()}/${Date.now()}-${rid()}`,
    15881607      type, actor: me, object: targetNoteId,
     
    16841703
    16851704export default {
    1686   getOrCreateKeys, apWants, sendAP, actorId, noteId,
     1705  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId,
    16871706  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
    16881707  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
Note: See TracChangeset for help on using the changeset viewer.