OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / 16f3dca

0.9.89: select a level for the fediverse too
The held-feed adapter reads the pointer straight out of the feed — which is the whole reason it is a record rather than a local index: this bridge holds somebody elses feed and nothing else, and can still choose. The ceiling here is OURS rather than quoted. bskys is fixed in a lexicon; Mastodons is an instance setting and it re-encodes on receipt, so there is nothing authoritative to quote and the base level is almost always right. 238/238. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16f3dca73cd4a131f4388a218ad99b69db630881
by plankatron <fleeky@gmail.com> · 2026-09-10T00:03:19.000Z

Changed files (3) +38 -3

lib/favela-feed.js +24 -0
……@@ -19,11 +19,14 @@
1919// there, and a bridge that could write would be authoring under their name.
2020
2121import b4a from 'b4a'
22// ⛔ ONE ladder, owned by the vocabulary that defines it (record-vocabulary §3.5).
23import { selectLevel } from 'favela-markdown/lib/levels.js'
2224
2325const REACH = 'reach'
2426const SELF = 'self'
2527const POST = 'post'
2628const PROFILE = 'profile'
29const BLOB = 'blob'
2730
2831export class FavelaFeed {
2932 /**
……@@ -101,6 +104,27 @@ export class FavelaFeed {
101104 if (!this.files || typeof this.files.get !== 'function') return null
102105 return this.files.get(args && args.oid)
103106 }
107 /**
108 * ⛔ SELECT A LEVEL — the bridge's half of the mip pyramid (SOCIAL-MODEL §6.6.1). The pointer
109 * travels IN the feed, which is the whole reason it is a record rather than a local index: this
110 * adapter holds somebody else's feed and nothing else, and can still choose.
111 *
112 * ⚠ A blob with no pointer has one level, which is every blob written before the pyramid
113 * existed. Absence is the single-level case, never an error.
114 */
115 if (cmd === 'blob-level') {
116 const oid = args && args.oid
117 const maxBytes = (args && args.maxBytes) || Infinity
118 await this.feed.update()
119 const rec = await this.feed.get(BLOB, oid)
120 const fm = (rec && rec.frontmatter) || null
121 if (fm && Array.isArray(fm.levels)) return selectLevel(fm, { maxBytes })
122 const info = this.files && typeof this.files.get === 'function' ? await this.files.get(oid) : null
123 if (!info) return { level: null, reason: 'no such blob' }
124 return (info.size || 0) <= maxBytes
125 ? { level: { oid, w: 0, h: 0, size: info.size }, reason: 'single level' }
126 : { level: null, reason: 'the only level is ' + info.size + ' bytes against a ceiling of ' + maxBytes }
127 }
104128 throw new Error('FavelaFeed: unsupported command "' + cmd + '" — this adapter is read-only')
105129 }
106130}
lib/favela-source.js +13 -2
……@@ -23,6 +23,11 @@ import { render, embeds } from 'favela-markdown/lib/render.js'
2323import { maxReach } from 'favela-markdown/lib/reach.js'
2424import { projectNote, objectIdFor, blobUrl, standaloneNote } from './as2-project.js'
2525
26// ⚠ OURS, not quoted. bsky's ceiling is in a lexicon and is fixed; Mastodon's is an instance
27// setting and it re-encodes on receipt, so there is nothing authoritative to quote. Generous
28// because the base level is almost always the right answer for the fediverse.
29const AP_MAX_IMAGE_BYTES = 8000000
30
2631export class FavelaSource {
2732 /**
2833 * @param {object} opts
……@@ -177,10 +182,16 @@ export class FavelaSource {
177182 const out = []
178183 for (const ref of refs) {
179184 if (!ref || !ref.oid || ref.kind !== 'image') continue
185 // ⛔ SELECT, never downsample. Mastodon resizes on receipt and its ceiling is instance-
186 // configurable, so this is OUR conservative bound rather than a quoted limit — but choosing a
187 // level is still the bridge's job and doing it here keeps both projections on one ladder.
188 let chosen = null
189 try { chosen = await this.peer.call('blob-level', { oid: ref.oid, maxBytes: AP_MAX_IMAGE_BYTES }) } catch { chosen = null }
190 const oid = (chosen && chosen.level && chosen.level.oid) || ref.oid
180191 let info = null
181 try { info = await this.peer.call('file-get', { oid: ref.oid }) } catch { info = null }
192 try { info = await this.peer.call('file-get', { oid }) } catch { info = null }
182193 if (!info || !info.mimeType) continue
183 const doc = { type: 'Document', mediaType: info.mimeType, url: blobUrl({ host: this.host, base: this.base, cid: ref.oid }) }
194 const doc = { type: 'Document', mediaType: info.mimeType, url: blobUrl({ host: this.host, base: this.base, cid: oid }) }
184195 // ⛔ Alt text is an accessibility obligation, not a nicety — carried whenever the author wrote one.
185196 if (ref.alt) doc.name = ref.alt
186197 out.push(doc)
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.88",
3 "version": "0.9.89",
44 "description": "ActivityPub membrane \u2014 a second projection of the self-custody atproto peer",
55 "type": "module",
66 "main": "index.js",