OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge commits / 40ffbe3

bridge 0.9.19: serve /.well-known/did.json with #bsky_fg
did:web:pds.7rnx.net was NOT resolvable — GET /.well-known/did.json returned MethodNotImplemented (verified live). A feed record whose service = did:web:pds.7rnx.net therefore could not resolve on bsky. Now the membrane serves its own did doc; with the lens-export on (favelaWebUpstream), it advertises the #bsky_fg BskyFeedGenerator service → https://<serverDid host>, so bsky resolves the feed service to this membrane and getFeedSkeleton lands on proxyToFavela → favela-web. This is the deploy prerequisite for riding pds.7rnx.net as the feed service host (no new subdomain). - favela-proxy.js: buildMembraneDidDoc(serverDid, {favelaWebUpstream}) - server.js + server-bare.js: GET /.well-known/did.json handler (favelaWebUpstream threaded into server.js for node parity) - test: pure helper (3 cases) + HTTP roundtrip. membrane-only green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
40ffbe32cfaaee9674d9b0c67ef502f6b75a5c91
by plankatron <fleeky@gmail.com> · 2026-07-16T02:49:17.000Z

Changed files (5) +72 -3

package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-bridge",
3 "version": "0.9.18",
3 "version": "0.9.19",
44 "description": "Stateless keyless atproto MEMBRANE — fronts self-custody hyper-proto-peers to bluesky over HTTP/WS, holds no keys and never signs. Bare-runnable. Deploy root / flat closure over the identity+repo+store+pds packages.",
55 "type": "module",
66 "main": "index.js",
pds/lib/favela-proxy.js +13 -0
……@@ -15,6 +15,19 @@ const FAVELA_METHODS = new Set([
1515// Is this a stateless lens-export service read we forward to favela-web?
1616export function isFavelaMethod (nsid) { return FAVELA_METHODS.has(nsid) }
1717
18// The membrane's own did:web document, served at GET /.well-known/did.json — this makes did:web:<host>
19// (serverDid) RESOLVABLE. When the lens-export is on (favelaWebUpstream set), it advertises the #bsky_fg
20// BskyFeedGenerator service pointing back at the membrane, so a feed record whose `service` = this did:web
21// resolves here and bsky calls getFeedSkeleton at this host → proxyToFavela → favela-web. Without the
22// feature it is still a valid (empty-service) did doc. Returns null for a non-did:web serverDid.
23export function buildMembraneDidDoc (serverDid, { favelaWebUpstream = null } = {}) {
24 if (!serverDid || !/^did:web:/.test(serverDid)) return null
25 const host = serverDid.slice('did:web:'.length)
26 const doc = { '@context': ['https://www.w3.org/ns/did/v1'], id: serverDid, service: [] }
27 if (favelaWebUpstream) doc.service.push({ id: '#bsky_fg', type: 'BskyFeedGenerator', serviceEndpoint: `https://${host}` })
28 return doc
29}
30
1831// Proxy one GET to favela-web. upstream = the gateway base URL (config.favelaWebUpstream, e.g.
1932// http://10.22.0.1:4041). doFetch(url,{method,headers}) → { status, contentType, body:Buffer } (the same
2033// injected fetch proxyToAppView uses). 501 when unconfigured; 502 on upstream failure.
pds/lib/server-bare.js +10 -1
……@@ -11,7 +11,7 @@ import https from 'bare-https'
1111import bareWs from 'bare-ws'
1212import { tidNow } from 'atproto-store/lib/tid.js'
1313import { proxyToAppView, isAppViewMethod } from './appview.js'
14import { proxyToFavela, isFavelaMethod } from './favela-proxy.js'
14import { proxyToFavela, isFavelaMethod, buildMembraneDidDoc } from './favela-proxy.js'
1515
1616// The caller's bearer token (access/refresh JWT) — passed to the peer, which is the auth authority.
1717const bearer = (req) => (req.headers.authorization || '').replace(/^Bearer\s+/i, '')
……@@ -147,6 +147,15 @@ export function createPdsServer ({ peers = null, serverDid = null, availableUser
147147 return res.end(body)
148148 }
149149
150 // did:web resolution: GET /.well-known/did.json → the membrane's own did doc. With the lens-export on
151 // (favelaWebUpstream), it advertises #bsky_fg so a feed record whose service = this did:web resolves
152 // here and bsky's getFeedSkeleton lands on proxyToFavela → favela-web.
153 if (req.method === 'GET' && url.pathname === '/.well-known/did.json') {
154 const doc = buildMembraneDidDoc(serverDid, { favelaWebUpstream })
155 if (!doc) return sendErr(res, 404, 'NotFound', 'no did:web served here')
156 return sendJson(res, 200, doc)
157 }
158
150159 if (req.method === 'GET') {
151160 switch (nsid) {
152161 case 'com.atproto.server.describeServer': return sendJson(res, 200, { did: serverDid, availableUserDomains })
pds/lib/server.js +11 -1
……@@ -8,6 +8,7 @@
88import { createServer } from 'node:http'
99import { WebSocketServer } from 'ws'
1010import { proxyToAppView, isAppViewMethod } from './appview.js'
11import { buildMembraneDidDoc } from './favela-proxy.js'
1112
1213// AppView upstream client (node): global fetch → { status, contentType, body:Buffer }.
1314async function doFetch (url, { method, headers, body }) {
……@@ -55,7 +56,7 @@ async function readJson (req) {
5556
5657// The membrane serves ONLY fronted peers. `peers` (PeerRouter) owns the fronted set + live
5758// PeerFronts. serverDid/availableUserDomains answer describeServer (there is no local account).
58export function createPdsServer ({ peers = null, serverDid = null, availableUserDomains = [] } = {}) {
59export function createPdsServer ({ peers = null, serverDid = null, availableUserDomains = [], favelaWebUpstream = null } = {}) {
5960 const resolveHandle = (h) => (peers ? peers.resolveHandle(h) : null)
6061 const resolveDid = async (idOrHandle) => {
6162 if (!idOrHandle) return null
……@@ -95,6 +96,15 @@ export function createPdsServer ({ peers = null, serverDid = null, availableUser
9596 return res.end(body)
9697 }
9798
99 // did:web resolution: GET /.well-known/did.json → the membrane's own did doc. With the lens-export on
100 // (favelaWebUpstream), it advertises #bsky_fg so a feed record whose service = this did:web resolves
101 // here and bsky's getFeedSkeleton lands on the favela proxy → favela-web.
102 if (req.method === 'GET' && url.pathname === '/.well-known/did.json') {
103 const doc = buildMembraneDidDoc(serverDid, { favelaWebUpstream })
104 if (!doc) return sendErr(res, 404, 'NotFound', 'no did:web served here')
105 return sendJson(res, 200, doc)
106 }
107
98108 if (req.method === 'GET') {
99109 switch (nsid) {
100110 case 'com.atproto.server.describeServer': return sendJson(res, 200, { did: serverDid, availableUserDomains })
pds/test/membrane-did-web.test.js +37 -0
……@@ -0,0 +1,37 @@
1// The membrane's did:web document — GET /.well-known/did.json makes did:web:<host> (serverDid) resolvable,
2// and advertises #bsky_fg when the favela lens-export is on so a feed record whose service = this did:web
3// resolves here (getFeedSkeleton → favela-web). Run via brittle-node.
4import test from 'brittle'
5import { buildMembraneDidDoc } from '../lib/favela-proxy.js'
6import { createPdsServer } from '../lib/server.js'
7
8const SERVER_DID = 'did:web:pds.7rnx.net'
9
10test('buildMembraneDidDoc: #bsky_fg present iff the lens-export is on', (t) => {
11 const off = buildMembraneDidDoc(SERVER_DID, {})
12 t.is(off.id, SERVER_DID, 'doc id = serverDid')
13 t.alike(off.service, [], 'no services when favelaWebUpstream is off')
14
15 const on = buildMembraneDidDoc(SERVER_DID, { favelaWebUpstream: 'http://10.22.0.1:4041' })
16 t.is(on.service.length, 1, 'one service when the lens-export is on')
17 t.is(on.service[0].id, '#bsky_fg', 'service id = #bsky_fg')
18 t.is(on.service[0].type, 'BskyFeedGenerator', 'service type = BskyFeedGenerator')
19 t.is(on.service[0].serviceEndpoint, 'https://pds.7rnx.net', 'endpoint = https://<serverDid host>')
20
21 t.is(buildMembraneDidDoc('did:plc:notweb', {}), null, 'non-did:web serverDid → null')
22 t.is(buildMembraneDidDoc(null, {}), null, 'no serverDid → null')
23})
24
25test('GET /.well-known/did.json serves the membrane did doc with #bsky_fg (lens-export on)', async (t) => {
26 const server = createPdsServer({ peers: null, serverDid: SERVER_DID, availableUserDomains: ['.7rnx.net'], favelaWebUpstream: 'http://10.22.0.1:4041' })
27 await new Promise((r) => server.listen(0, '127.0.0.1', r))
28 t.teardown(() => server.close())
29 const base = `http://127.0.0.1:${server.address().port}`
30
31 const res = await fetch(`${base}/.well-known/did.json`)
32 t.is(res.status, 200, 'did.json 200s (was MethodNotImplemented before)')
33 const doc = await res.json()
34 t.is(doc.id, SERVER_DID, 'did doc id = serverDid')
35 const fg = (doc.service || []).find((s) => s.id === '#bsky_fg')
36 t.ok(fg && fg.type === 'BskyFeedGenerator' && fg.serviceEndpoint === 'https://pds.7rnx.net', 'advertises #bsky_fg → https://pds.7rnx.net (bsky resolves the feed service here)')
37})