// feed.js — the bridge's public face as a FEED, not a log of arrivals.
//
// WHY IT IS A PRODUCT SURFACE AND A DIAGNOSTIC AT ONCE. The existing /timeline shows only what the
// fediverse SENT us, as a flat list of activity types. That answers "did anything arrive" and cannot
// answer the question we actually keep getting wrong: are interactions being handled in BOTH
// directions? A like we received and a like we sent are the same shape on this page, so a missing
// one is visible rather than inferred from a log.
//
// ⛔ PUBLIC ONLY, AND THE FILTER IS THE SAFETY PROPERTY. There is no login in front of this. Mastodon
// delivers DMs to an inbox exactly as it delivers replies, through the same handler into the same
// store, so anything that cannot prove it was public is withheld — including everything stored
// before `public` was recorded.
import test from 'brittle'
import { buildFeed, handleOf } from '../lib/feed.js'
const REMOTE = 'https://mastodon.social/users/alice'
const OURS = 'https://p2p.7rnx.net/ap/objects/3kmine'
const note = (rkey, content, published) => ({
id: `https://p2p.7rnx.net/ap/objects/${rkey}`,
type: 'Note',
content,
published,
attributedTo: 'https://p2p.7rnx.net/ap/actor'
})
const inbound = (over) => ({
id: 'act-' + Math.random(), type: 'Create', actor: REMOTE, objectId: `${REMOTE}/notes/x`,
content: '<p>nice post</p>', published: '2026-09-15T10:00:00Z', public: true,
recipient: 'p2p', receivedAt: Date.parse('2026-09-15T10:00:00Z'), ...over
})
test('a remote actor URI reads as the handle a fediverse user recognises', (t) => {
t.is(handleOf(REMOTE), '@alice@mastodon.social')
t.is(handleOf('https://prsm.space/users/fleeky'), '@fleeky@prsm.space')
t.is(handleOf('https://example.org/@bob'), '@bob@example.org', 'the /@name form too')
t.is(handleOf('not a url'), 'not a url', 'and anything unparseable is shown as-is, never invented')
})
test('own posts and inbound activity land in ONE feed, newest first', (t) => {
const rows = buildFeed({
self: { handle: '@p2p@p2p.7rnx.net', name: 'Fleeky' },
notes: [note('a', '<p>my post</p>', '2026-09-15T09:00:00Z')],
inbound: [inbound({ published: '2026-09-15T11:00:00Z' })]
})
t.is(rows.length, 2)
t.is(rows[0].at, '2026-09-15T11:00:00Z', 'newest first, like every feed anyone has used')
t.is(rows[1].kind, 'post')
t.is(rows[1].author.handle, '@p2p@p2p.7rnx.net', 'our own post is attributed to us, not to a URL')
t.is(rows[1].text, 'my post', "and the remote server's markup is not ours to render")
})
test('★ every interaction verb is visible and labelled, which is the point of the page', (t) => {
const rows = buildFeed({
self: { handle: '@p2p@p2p.7rnx.net' },
notes: [],
inbound: [
inbound({ type: 'Like', objectId: OURS, content: null, published: '2026-09-15T10:00:00Z' }),
inbound({ type: 'Announce', objectId: OURS, content: null, published: '2026-09-15T10:01:00Z' }),
inbound({ type: 'Follow', objectId: null, content: null, published: '2026-09-15T10:02:00Z' }),
inbound({ type: 'Create', inReplyTo: OURS, published: '2026-09-15T10:03:00Z' })
]
})
t.alike(rows.map((r) => r.kind), ['reply', 'follow', 'boost', 'like'], 'all four, newest first')
t.is(rows[3].verb, 'liked your post', 'a like says so in words')
t.is(rows[2].verb, 'boosted your post')
t.is(rows[1].verb, 'followed you')
t.is(rows[0].kind, 'reply', 'a reply is content, not a bare verb')
t.is(rows[0].text, 'nice post')
})
test('⛔ anything that cannot prove it was public is withheld', (t) => {
const rows = buildFeed({
self: {},
notes: [],
inbound: [
inbound({ id: 'dm', public: false, content: '<p>secret</p>' }),
inbound({ id: 'legacy', content: '<p>from before the field existed</p>' })
].map((i) => (i.id === 'legacy' ? (delete i.public, i) : i))
})
t.is(rows.length, 0, 'a DM and an unattributable item are both absent')
})
test('⛔ a row for somebody else never appears on this peer\'s feed', (t) => {
const rows = buildFeed({
self: {},
recipient: 'ashatron',
notes: [],
inbound: [inbound({ recipient: 'p2p' }), inbound({ id: 'mine', recipient: 'ashatron' })]
})
t.is(rows.length, 1)
t.is(rows[0].id, 'mine')
})
test('an unknown activity type is dropped, not shown as the nearest familiar one', (t) => {
const rows = buildFeed({ self: {}, notes: [], inbound: [inbound({ type: 'Flag' })] })
t.is(rows.length, 0, 'inventing a verb would tell the reader something that did not happen')
})
// ── the outbound half ─────────────────────────────────────────────────────────────────────────────
//
// ⛔ THE REASON THE PAGE IS WORTH LOOKING AT. Without these the feed shows our posts and other
// people's reactions to them, which proves INBOUND handling and says nothing about outbound. A like
// we sent and a like we received now sit in the same stream, so a broken direction is visible instead
// of being inferred from a delivery log.
const ownLike = (rkey, at, object = 'https://mastodon.social/notes/1') => ({
kind: 'like', rkey, key: 'like:' + rkey, at,
activity: { id: 'https://p2p.7rnx.net/ap/activities/like/' + rkey, type: 'Like', object }
})
test('★★ our own likes and boosts appear, attributed to us', (t) => {
const rows = buildFeed({
self: { handle: '@p2p@p2p.7rnx.net', name: 'Fleeky' },
own: [
ownLike('l1', '2026-09-15T12:00:00Z'),
{ kind: 'announce', rkey: 'b1', key: 'announce:b1', at: '2026-09-15T12:01:00Z', activity: { type: 'Announce', object: 'https://mastodon.social/notes/2' } }
]
})
t.alike(rows.map((r) => r.kind), ['boost', 'like'], 'newest first')
t.is(rows[1].verb, 'you liked a post', 'in words, and in the first person — it was us')
t.is(rows[0].verb, 'you boosted a post')
t.is(rows[1].author.handle, '@p2p@p2p.7rnx.net', 'attributed to us, not to whoever we acted on')
t.is(rows[1].source, 'own')
t.is(rows[1].subject, 'https://mastodon.social/notes/1', 'and it links to what we acted on')
})
test('★ both directions interleave by time in ONE stream', (t) => {
const rows = buildFeed({
self: { handle: '@p2p@p2p.7rnx.net' },
notes: [note('a', '<p>my post</p>', '2026-09-15T09:00:00Z')],
own: [ownLike('l1', '2026-09-15T11:00:00Z')],
inbound: [inbound({ type: 'Like', objectId: OURS, content: null, published: '2026-09-15T10:00:00Z' })]
})
t.alike(rows.map((r) => r.verb), ['you liked a post', 'liked your post', null],
'ours, theirs, and a post — one timeline, which is the whole diagnostic')
})
test('⛔ an outbound interaction with no time still shows, sorted last', (t) => {
const rows = buildFeed({ self: {}, own: [ownLike('l1', null)] })
t.is(rows.length, 1, '"we do not know when" is not "it did not happen"')
})
// ── images ────────────────────────────────────────────────────────────────────────────────────────
test('a post carries its pictures through to the row', (t) => {
const withImg = {
...note('img', '<p>look</p>', '2026-09-15T09:00:00Z'),
attachment: [{ type: 'Document', mediaType: 'image/png', url: 'https://p2p.7rnx.net/ap/blobs/bafk1', name: 'a screenshot' }]
}
const rows = buildFeed({ self: {}, notes: [withImg] })
t.is(rows[0].attachments.length, 1)
t.is(rows[0].attachments[0].url, 'https://p2p.7rnx.net/ap/blobs/bafk1')
t.is(rows[0].attachments[0].name, 'a screenshot', 'alt text travels — a picture with no description is unreadable to some readers')
})
test('★ an inbound reply keeps its pictures too', (t) => {
const rows = buildFeed({
self: {},
inbound: [inbound({
inReplyTo: OURS,
attachment: [{ type: 'Document', mediaType: 'image/jpeg', url: 'https://mastodon.social/media/1.jpg', name: 'their photo' }]
})]
})
t.is(rows[0].attachments.length, 1, 'a reply from the fediverse can have a picture in it')
t.is(rows[0].attachments[0].mediaType, 'image/jpeg')
})
test('⛔ an attachment with no url is dropped, and the count is bounded', (t) => {
const many = Array.from({ length: 20 }, (_, i) => ({ type: 'Document', mediaType: 'image/png', url: 'u' + i }))
const rows = buildFeed({ self: {}, notes: [{ ...note('m', '<p>x</p>', 'z'), attachment: [{ type: 'Document' }, ...many] }] })
t.absent(rows[0].attachments.some((a) => !a.url), 'nothing without a url')
t.ok(rows[0].attachments.length <= 8, 'a remote server does not get to decide how tall our page is')
})
// ── recovering an unattributed item ───────────────────────────────────────────────────────────────
//
// ⛔ THE SCOPE HID THE ONE THING WE WANTED TO SEE. Items stored before `recipient` existed belong to
// nobody, which is right when nothing can say whose they were. But the OBJECT URI can: `/ap/objects/x`
// is the primary's flat path and `/ap/users/<name>/objects/x` is that user's. Reading it is recovering
// addressing that is still present, not guessing at addressing that was lost.
test('★★ an unattributed item is recovered from the object path when it names our host', (t) => {
const rows = buildFeed({
self: {}, recipient: 'p2p', host: 'ap.7rnx.net',
inbound: [{ id: 'l', type: 'Like', actor: 'https://prsm.space/users/fleeky', objectId: 'https://ap.7rnx.net/ap/objects/3mvks7khp3u6w', public: false, published: '2026-09-14T00:00:00Z' }]
})
t.is(rows.length, 1, 'the flat object path is the primary, so this is the primary\'s')
t.is(rows[0].verb, 'liked your post')
})
test('★★ and it goes to the RIGHT user, not just the primary', (t) => {
const item = { id: 'l', type: 'Like', actor: 'https://prsm.space/users/x', objectId: 'https://ap.7rnx.net/ap/users/ashatron/objects/9', public: false, published: '2026-09-14T00:00:00Z' }
t.is(buildFeed({ self: {}, recipient: 'ashatron', host: 'ap.7rnx.net', inbound: [item] }).length, 1, "ashatron's")
t.is(buildFeed({ self: {}, recipient: 'p2p', host: 'ap.7rnx.net', inbound: [item] }).length, 0, 'and not the primary\'s')
})
test('⛔ an object on somebody else\'s host recovers nothing and stays unattributed', (t) => {
const rows = buildFeed({
self: {}, recipient: 'p2p', host: 'ap.7rnx.net',
inbound: [{ id: 'l', type: 'Like', actor: 'https://m.example/users/x', objectId: 'https://elsewhere.net/ap/objects/1', public: true, published: 'z' }]
})
t.is(rows.length, 0, 'we cannot read another server\'s paths, and will not pretend to')
})
// ── interactions we cannot federate, but can still show ────────────────────────────────────────────
//
// ⛔ MEASURED, NOT ASSUMED. All 6 of the live bridge's unaddressable interactions are likes of BLUESKY
// posts: did:plc:4732wedq…, did:plc:e2jevhuu…, did:plc:y5caotnr…. None is on our host, none is fronted
// by us, and Bluesky content has no ActivityPub identity at all — so `projectLike` is right to refuse
// and minting a URI on a host we do not control is what hyper-social #71 §9 explicitly declines.
//
// ⚠ THE AP PROJECTION IS UNCHANGED. Nothing new goes on the wire. This is OUR page showing OUR act,
// linked to where the post actually lives, which is the only truthful way to render it.
test('★★ an unprojectable like still appears, linked to where the post really is', (t) => {
const rows = buildFeed({
self: { handle: '@p2p@ap.7rnx.net' },
unprojectable: [{ kind: 'like', rkey: 'k1', at: '2026-09-15T10:00:00Z', subject: 'at://did:plc:y5caotnrle4kxlfez4azaf33/app.bsky.feed.post/3mrx7jvraws23' }]
})
t.is(rows.length, 1)
t.is(rows[0].verb, 'you liked a post')
t.is(rows[0].subject, 'https://bsky.app/profile/did:plc:y5caotnrle4kxlfez4azaf33/post/3mrx7jvraws23',
'named where it lives, not at a URI we would have had to invent')
t.is(rows[0].network, 'bsky', 'and the row says which network, so the link is not a surprise')
})
test('⛔ a subject we cannot even locate is still not invented', (t) => {
const rows = buildFeed({ self: {}, unprojectable: [{ kind: 'like', rkey: 'k2', at: 'z', subject: 'garbage' }] })
t.is(rows.length, 1, 'the act happened and is shown')
t.is(rows[0].subject, null, 'but nothing is fabricated to point at')
})