// A page with no login in front of it, rendering what strangers sent us.
//
// ⛔ THE FILTER IS THE SAFETY PROPERTY. Mastodon delivers DIRECT MESSAGES to an inbox exactly as it
// delivers replies — same handler, same store — so a public page over that store publishes somebody's
// DM unless addressing is checked. normalize() drops `to`/`cc`, and the remote object is only
// refetchable while its server still serves it, so the answer has to be recorded AT ARRIVAL.
import '../lib/bare-globals.js'
import test from 'brittle'
import { normalize, isPublicActivity, isInteraction } from '../lib/inbound-store.js'
import { publicTimeline } from '../lib/public-timeline.js'
import { buildFeed } from '../lib/feed.js'
const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'
const note = (extra = {}, objExtra = {}) => ({
id: 'https://m.example/a/1',
type: 'Create',
actor: 'https://m.example/users/someone',
object: { id: 'https://m.example/o/1', type: 'Note', content: '<p>hi</p>', ...objExtra },
...extra
})
test('★★★ a direct message is NOT public', (t) => {
// Addressed to one actor and nobody else — this is a DM, and it arrives at the same inbox.
const dm = note({ to: ['https://ap.7rnx.net/ap/users/fleeky/actor'] })
t.absent(isPublicActivity(dm), 'a DM is not public')
t.is(normalize(dm, 1).public, false, 'and the store records that at arrival')
})
test('★★★ followers-only is NOT public', (t) => {
const followers = note({ to: ['https://m.example/users/someone/followers'] })
t.absent(isPublicActivity(followers))
t.is(normalize(followers, 1).public, false)
})
test('★★★ public is recognised in all three spellings seen in the wild', (t) => {
// ⚠ The full URI is canonical, `as:Public` is the compacted JSON-LD form, bare `Public` comes from
// older implementations. Matching only the first fails safely but hides half the fediverse.
for (const form of [PUBLIC, 'as:Public', 'Public']) {
t.ok(isPublicActivity(note({ to: [form] })), form + ' in to')
t.ok(isPublicActivity(note({ cc: [form] })), form + ' in cc')
}
// And on the OBJECT rather than the activity, which is where Mastodon often puts it.
t.ok(isPublicActivity(note({}, { to: [PUBLIC] })), 'public on the object counts')
})
test('★★★ an item with no addressing at all is withheld', (t) => {
// ⛔ FAIL CLOSED. Records stored before `public` existed have `undefined`, and the route requires
// `=== true`. An operator sees a shorter page after upgrading; the alternative is publishing a DM
// because the record predates the question being asked.
t.absent(isPublicActivity(note({})), 'no to/cc means not public')
const legacy = { type: 'Create', actor: 'https://m.example/u/1', content: 'old' }
t.not(legacy.public, true, 'a legacy record is not `=== true`, which is what the route tests')
})
test('★★★ remote markup never reaches the page intact', (t) => {
const rows = buildFeed({
self: {},
inbound: [{ id: 'x', actor: 'https://m.example/u/1', type: 'Create', inReplyTo: 'https://ap.7rnx.net/ap/objects/1', public: true, receivedAt: 1788800000000, content: '<img src=x onerror=alert(1)>hello <b>there</b>' }]
})
const body = publicTimeline({ host: 'ap.7rnx.net', total: 1, rows })
t.absent(/<img/.test(body), 'no remote tag survives')
t.absent(/onerror/.test(body), 'no remote attribute survives')
t.ok(/hello there/.test(body), 'the words do')
})
test('★★ it says what it does not show, and asks not to be indexed', (t) => {
const body = publicTimeline({ host: 'ap.7rnx.net', rows: [], total: 0 })
t.ok(/Direct and followers-only messages are never shown/.test(body),
'a reader should not have to infer the policy from an absence')
// ⚠ Other people's words republished on our domain — letting a crawler take a second permanent copy
// is a decision nobody here has made.
t.ok(/noindex/.test(body))
})
test('★★ paging offers an older link only when there is more', (t) => {
const rows = Array.from({ length: 50 }, (_, i) => ({ kind: 'post', author: { handle: '@a@b' }, at: String(i), text: 'x', source: 'own' }))
t.ok(/50 older/.test(publicTimeline({ rows, total: 120, offset: 0 })), 'more to come')
t.absent(/older/.test(publicTimeline({ rows, total: 50, offset: 0 })), 'nothing more, no link')
t.ok(/newer/.test(publicTimeline({ rows, total: 120, offset: 50 })), 'and a way back')
})
test('★★★ an interaction is visible in words, which is why this page exists', (t) => {
const rows = buildFeed({
self: {},
inbound: [
{ id: 'l', type: 'Like', actor: 'https://m.example/users/alice', objectId: 'https://ap.7rnx.net/ap/objects/1', public: true, published: '2026-09-15T10:00:00Z' },
{ id: 'b', type: 'Announce', actor: 'https://m.example/users/bob', objectId: 'https://ap.7rnx.net/ap/objects/1', public: true, published: '2026-09-15T10:01:00Z' }
]
})
const body = publicTimeline({ host: 'ap.7rnx.net', rows, total: 2 })
t.ok(/liked your post/.test(body), 'a like reads as a like, not as the string "Like"')
t.ok(/boosted your post/.test(body), 'and a boost as a boost')
t.ok(/@alice@m.example/.test(body), 'attributed to a handle a fediverse user recognises')
t.absent(/https:\/\/m\.example\/users\/alice<\/span>/.test(body), 'not to a raw URL')
})
test('★★ our own picture renders; a remote one is LINKED, not hotlinked', (t) => {
const rows = buildFeed({
self: { handle: '@p2p@ap.7rnx.net' },
notes: [{ id: 'https://ap.7rnx.net/ap/objects/1', type: 'Note', content: '<p>look</p>', published: '2026-09-15T09:00:00Z', attachment: [{ type: 'Document', mediaType: 'image/png', url: 'https://ap.7rnx.net/ap/blobs/bafk1', name: 'a screenshot' }] }],
inbound: [{ id: 'r', type: 'Create', actor: 'https://m.example/users/alice', inReplyTo: 'https://ap.7rnx.net/ap/objects/1', public: true, published: '2026-09-15T10:00:00Z', content: '<p>nice</p>', attachment: [{ type: 'Document', mediaType: 'image/jpeg', url: 'https://m.example/media/1.jpg', name: 'their photo' }] }]
})
const body = publicTimeline({ host: 'ap.7rnx.net', rows, total: 2 })
t.ok(/<img src="https:\/\/ap.7rnx.net\/ap\/blobs\/bafk1"/.test(body), 'ours is shown — we serve those bytes')
t.ok(/alt="a screenshot"/.test(body), 'with its alt text')
// ⛔ A public page with no login hotlinking a remote image hands every viewer's IP and page view to
// a server we do not run. Mastodon proxies media for exactly this reason; we link instead.
t.absent(/<img src="https:\/\/m.example/.test(body), "a stranger's server does not get our readers' IPs")
t.ok(/their photo/.test(body), 'but the reader is told a picture is there, and can open it')
})
// ── interactions carry no audience, and absence is not evidence of privacy ─────────────────────────
//
// ⛔ THE RULE THAT WAS WRONG. `isPublicActivity` looks for to/cc on the activity or its object. A Like
// has neither — its `object` is a bare URI string and Mastodon sends no audience at all — so "no
// addressing" read as "not public" and EVERY inbound like, boost and follow was invisible by
// construction. Measured on the live bridge: one Like from @fleeky@prsm.space, stored `public: false`,
// withheld from a page built to show exactly that.
//
// ⚠ THE FIX IS NARROW. For CONTENT the old rule is right and unchanged — a Create with no addressing
// is still withheld, because a DM arrives at the same inbox through the same handler. What changes is
// that an interaction is judged by the thing it acted on, which on this bridge is a public post.
test('★★★ a Create with no addressing is STILL withheld — the DM guard is untouched', (t) => {
t.absent(isPublicActivity(note({})), 'content with no audience stays private')
t.is(normalize(note({}), 1).public, false)
t.is(normalize(note({ to: ['https://ap.7rnx.net/ap/users/fleeky/actor'] }), 1).public, false, 'and a DM stays a DM')
})
test('★★★ a Like is showable — it has no audience because the protocol supplies none', (t) => {
const like = { id: 'l', type: 'Like', actor: 'https://m.example/users/a', object: 'https://ap.7rnx.net/ap/objects/1' }
t.ok(isInteraction(like), 'a Like is an interaction, not content')
t.is(normalize(like, 1).public, true, 'so it is recorded as showable, matching Mastodon')
})
test('★★ a Follow and a Boost too, and an explicitly private Boost is still refused', (t) => {
t.is(normalize({ type: 'Follow', actor: 'https://m.example/users/a', object: 'https://ap.7rnx.net/ap/actor' }, 1).public, true)
t.is(normalize({ type: 'Announce', actor: 'https://m.example/users/a', object: 'https://ap.7rnx.net/ap/objects/1' }, 1).public, true)
// ⚠ An Announce CAN carry addressing, unlike a Like. When it does, it is honoured.
const privateBoost = { type: 'Announce', actor: 'https://m.example/users/a', object: 'x', to: ['https://m.example/users/a/followers'] }
t.is(normalize(privateBoost, 1).public, false, 'stated privacy beats the default')
})
test('★★ the feed now shows a like that arrived', (t) => {
const rows = buildFeed({
self: {},
inbound: [normalize({ type: 'Like', actor: 'https://prsm.space/users/fleeky', object: 'https://ap.7rnx.net/ap/objects/1' }, Date.parse('2026-09-14T00:00:00Z'))]
})
t.is(rows.length, 1, 'the one measured on the live bridge would appear')
t.is(rows[0].verb, 'liked your post')
})
test('★★ a handle is a link — ours to their page here, a stranger\'s to their own server', (t) => {
const rows = buildFeed({
self: { handle: '@fleeky@ap.7rnx.net' },
notes: [{ id: 'https://ap.7rnx.net/ap/users/fleeky/objects/1', type: 'Note', content: '<p>mine</p>', published: '2026-09-15T09:00:00Z' }],
inbound: [{ id: 'r', type: 'Create', actor: 'https://m.example/users/alice', inReplyTo: 'https://ap.7rnx.net/ap/users/fleeky/objects/1', public: true, published: '2026-09-15T10:00:00Z', content: '<p>hi</p>' }]
})
const body = publicTimeline({ host: 'ap.7rnx.net', rows, total: 2 })
// ⛔ A COMBINED PAGE WITH NO WAY INTO A PEER'S OWN IS HALF A FEATURE. You had to already know the
// URL to read one account, which is the same as it not being there.
t.ok(/href="\/ap\/users\/fleeky\/timeline"/.test(body), 'ours goes to their page on this bridge')
t.ok(/href="https:\/\/m.example\/users\/alice"/.test(body), "a stranger's goes to their own server")
t.ok(/rel="nofollow noopener ugc"/.test(body), 'and a link we did not author is marked as such')
})
test('⛔ a handle on another host is never rewritten into a local path', (t) => {
const rows = buildFeed({
self: {},
inbound: [{ id: 'x', type: 'Like', actor: 'https://evil.example/users/ap.7rnx.net', objectId: 'https://ap.7rnx.net/ap/objects/1', public: true, published: 'z' }]
})
const body = publicTimeline({ host: 'ap.7rnx.net', rows, total: 1 })
t.absent(/href="\/ap\/users\/ap.7rnx.net\/timeline"/.test(body),
'a name that merely looks like ours does not get one of our pages')
})