Skip to content

Commit 265ef2d

Browse files
serve.js: loopback on the issuer host; record the Host-sensitive WAC finding
Standing Phanpy up against serve.js surfaced it: a WebID minted under idpIssuer 'localhost:<port>' fails core's ownership check when the same valid token arrives via '127.0.0.1:<port>' — so every plugin's loopback write 403'd in the demo while the whole suite stayed green (helpers.js uses 127.0.0.1 consistently, hiding it). serve.js loopbacks now use the issuer's host. NOTES gains the seam-shaped ask; NEXT records Phanpy Wave A done and scopes Wave B (real federation).
1 parent fdc1f6a commit 265ef2d

3 files changed

Lines changed: 39 additions & 18 deletions

File tree

NEXT.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ Still genuinely plugin-shaped and distinct:
180180
(capability + shortlink composition), a music/podcast pod (gallery's
181181
Range finding makes byte-range playback free), a turn-based game over
182182
relay + pod state. Demos compose existing plugins; loopback only.
183+
- **Phanpy Wave B — real federation** (Wave A is DONE, 2026-07-12: the
184+
mastodon/ shim is Phanpy-grade for local use — timelines, threads,
185+
media, notifications from the AP inbox, PKCE; serve a Phanpy dist at
186+
any origin and point it at the server). Wave B = inbound HTTP-Signature
187+
verification (SECURITY.md accepted-risk #1), remote actor resolution,
188+
Like/Announce/Follow delivery, ingest of followed actors' posts —
189+
outbound-fetch class, activitypub/'s SSRF gates are the pattern; use
190+
federation-demo/ as the regression harness. Streaming stays blocked on
191+
api.events (#603); Phanpy polls fine.
183192

184193
Prefer plugins that open a **new capability class** or add a **new
185194
independent consumer of an already-named seam** (that strengthens the

NOTES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ are candidates, each with a consumer in this repo attached.
3434

3535
## Candidate seams (ranked by how many independent plugins demanded them)
3636

37+
**New (2026-07-12, found standing up Phanpy against serve.js): agent
38+
resolution is Host-sensitive.** A WebID minted under the idpIssuer host
39+
(`localhost:<port>`) fails core's ownership check when the same request
40+
arrives via `127.0.0.1:<port>` — same server, same valid token, 403.
41+
Every loopback-forwarding plugin is exposed: the loopback bind MUST use
42+
the issuer's host (serve.js now does), and a reverse-proxied deployment
43+
whose loopback carries a different Host header would hit the same wall.
44+
Test suites never see it because helpers.js uses 127.0.0.1 for both.
45+
Seam-shaped ask: either core matches agents host-insensitively for
46+
loopback binds, or `api.serverInfo` should bless one canonical loopback
47+
URL that is guaranteed ownership-safe.
48+
3749
Twelve plugins in, the ranking is now empirical — a seam's rank is how many
3850
ports reached for it without coordinating.
3951

serve.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,39 +75,39 @@ const fastify = createServer({
7575
{
7676
module: at('notifications/plugin.js'),
7777
prefix: '/.notifications',
78-
config: { podsRoot: PODS, baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` },
78+
config: { podsRoot: PODS, baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` },
7979
},
8080
{ module: at('pay/plugin.js'), prefix: '/paid', config: { cost: 1, address: 'demo' } },
8181
{ module: at('nip05/plugin.js'), prefix: '/nip05', config: { podsRoot: PODS, relayUrl: `${PUBLIC_URL.replace(/^http/, 'ws')}/relay` } },
8282
{ module: at('corsproxy/plugin.js'), prefix: '/proxy', config: {} },
8383
{ module: at('capability/plugin.js'), prefix: '/cap', config: {} },
84-
{ module: at('webdav/plugin.js'), prefix: '/webdav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
84+
{ module: at('webdav/plugin.js'), prefix: '/webdav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
8585
{ module: at('gitscratch/plugin.js'), prefix: '/git', config: {} },
86-
{ module: at('sparql/plugin.js'), prefix: '/sparql', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
86+
{ module: at('sparql/plugin.js'), prefix: '/sparql', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
8787
{ module: at('otp/plugin.js'), prefix: '/otp', config: {} },
88-
{ module: at('carddav/plugin.js'), prefix: '/carddav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
89-
{ module: at('mastodon/plugin.js'), prefix: '/mastodon', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
90-
{ module: at('bluesky/plugin.js'), prefix: '/bluesky', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
91-
{ module: at('caldav/plugin.js'), prefix: '/caldav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
88+
{ module: at('carddav/plugin.js'), prefix: '/carddav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
89+
{ module: at('mastodon/plugin.js'), prefix: '/mastodon', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
90+
{ module: at('bluesky/plugin.js'), prefix: '/bluesky', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
91+
{ module: at('caldav/plugin.js'), prefix: '/caldav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
9292
{ module: at('webfinger/plugin.js'), prefix: '/webfinger', config: { podsRoot: PODS, baseUrl: PUBLIC_URL } },
93-
{ module: at('activitypub/plugin.js'), prefix: '/activitypub', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
94-
{ module: at('rss/plugin.js'), prefix: '/feed', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
95-
{ module: at('matrix/plugin.js'), prefix: '/matrix', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
96-
{ module: at('search/plugin.js'), prefix: '/search', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
93+
{ module: at('activitypub/plugin.js'), prefix: '/activitypub', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
94+
{ module: at('rss/plugin.js'), prefix: '/feed', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
95+
{ module: at('matrix/plugin.js'), prefix: '/matrix', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
96+
{ module: at('search/plugin.js'), prefix: '/search', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
9797
{ module: at('didweb/plugin.js'), prefix: '/didweb', config: { podsRoot: PODS, baseUrl: PUBLIC_URL } },
98-
{ module: at('s3/plugin.js'), prefix: '/s3', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
99-
{ module: at('micropub/plugin.js'), prefix: '/micropub', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
100-
{ module: at('backup/plugin.js'), prefix: '/backup', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
98+
{ module: at('s3/plugin.js'), prefix: '/s3', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
99+
{ module: at('micropub/plugin.js'), prefix: '/micropub', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
100+
{ module: at('backup/plugin.js'), prefix: '/backup', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
101101
{ module: at('shortlink/plugin.js'), prefix: '/short', config: { baseUrl: PUBLIC_URL } },
102-
{ module: at('oembed/plugin.js'), prefix: '/oembed', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
103-
{ module: at('jmap/plugin.js'), prefix: '/jmap', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
102+
{ module: at('oembed/plugin.js'), prefix: '/oembed', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
103+
{ module: at('jmap/plugin.js'), prefix: '/jmap', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}` } },
104104
// Zero config on purpose — gallery/ needs none (api.serverInfo origin,
105105
// container defaults to the caller's own pod).
106106
{ module: at('gallery/plugin.js'), prefix: '/gallery' },
107107
// webfinger/ above owns /.well-known/webfinger — the witnessed collision
108108
// (remotestorage/README.md) — so remotestorage stands down here.
109-
{ module: at('remotestorage/plugin.js'), prefix: '/remotestorage', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}`, claimWellKnown: false } },
110-
{ module: at('metrics/plugin.js'), prefix: '/metrics', config: { loopbackUrl: `http://127.0.0.1:${PORT}`, ...(process.env.METRICS_TOKEN ? { token: process.env.METRICS_TOKEN } : {}) } },
109+
{ module: at('remotestorage/plugin.js'), prefix: '/remotestorage', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://localhost:${PORT}`, claimWellKnown: false } },
110+
{ module: at('metrics/plugin.js'), prefix: '/metrics', config: { loopbackUrl: `http://localhost:${PORT}`, ...(process.env.METRICS_TOKEN ? { token: process.env.METRICS_TOKEN } : {}) } },
111111
{
112112
module: at('dashboard/plugin.js'),
113113
prefix: '/dashboard',

0 commit comments

Comments
 (0)