@@ -23,6 +23,12 @@ are candidates, each with a consumer in this repo attached.
2323 client's own credentials. Slower than an internal check but definitionally
2424 correct. This removes a whole class of would-be seams (` api.wac.check ` )
2525 from the * necessary* list, leaving them merely * nice* .
26+ - ** Conditional writes pass through loopback intact** (remotestorage/,
27+ measured): ` If-Match ` /` If-None-Match ` forwarded verbatim are honored by
28+ the host end-to-end — stale ` If-Match ` PUT/DELETE → 412,
29+ ` If-None-Match: * ` on an existing resource → 412 with the body proven
30+ not to land, ` If-None-Match ` GET → 304. Atomic at the host; no
31+ plugin-side conditional logic needed.
2632
2733## Candidate seams (ranked by how many independent plugins demanded them)
2834
@@ -38,8 +44,10 @@ ports reached for it without coordinating.
3844 #382 ), or a capability exercising the * issuer's* authority
3945 (capability #506 ). This is the most-requested seam and the one that
4046 moves the most backlog issues from "plugin-approximation" to "faithful".
41- 2 . ** ` api.events.onResourceChange(cb) ` ** — ** five consumers now:
42- notifications/, sparql/, search/, matrix/, backup/** , in rising
47+ 2 . ** ` api.events.onResourceChange(cb) ` ** — ** seven consumers now:
48+ notifications/, sparql/, search/, matrix/, backup/, jmap/,
49+ remotestorage/** (plus oembed/ as a soft eighth: a write-time image-
50+ dimension cache would replace per-unfurl byte parsing), in rising
4351 sharpness: notifications (a miss is a * late* notification), sparql (a
4452 miss is a * wrong* query result), search (a miss is * stale results* , the
4553 property users most expect to be fresh — the most user-visible
@@ -48,15 +56,19 @@ ports reached for it without coordinating.
4856 — so a stateless bridge can only do full-state ` /sync ` at all), and
4957 backup (with no change hook * and* no plugin-owned read authority,
5058 incremental and scheduled/server-initiated backup are both unbuildable —
51- every backup is a caller-driven full crawl). Core already has the
59+ every backup is a caller-driven full crawl), jmap (JMAP push
60+ (` eventSourceUrl ` ) and delta sync (` */changes ` , ` Email/queryChanges ` )
61+ are omitted/refused in-protocol), and remotestorage (rS's
62+ descendant-version propagation at depth ≥ 2 needs a write-time index).
63+ Core already has the
5264 emitter internally (` src/notifications/events.js ` ); this is the seam
5365 every "react to pod writes" app (webhooks, indexing, sync, search, live
5466 chat) will want, and it's now clearly the #2 most-demanded after
5567 ` api.authorize ` .
56683 . ** ` api.serverInfo ` (` { baseUrl, port } ` at listen)** — ** a dozen+
5769 consumers: notifications/, webdav/, carddav/, caldav/, sparql/, rss/,
5870 nip05/, webfinger/, mastodon/, bluesky/, activitypub/, micropub/,
59- backup/** — essentially
71+ backup/, dashboard/, oembed/, jmap/, remotestorage/ ** — essentially
6072 every plugin that mints absolute URLs or reaches the host over loopback.
6173 All repeat the origin in config today; a wrong value fails quietly (nip05
6274 serves an empty identity map). The single most * broadly* needed seam
@@ -65,7 +77,9 @@ ports reached for it without coordinating.
6577 gitscratch/ sharpened it; micropub/ adds a blocked one. tunnel/ needed
6678 the raw * buffer* ; git needs the raw * stream* piped to a subprocess
6779 gzip-and-all; micropub's ** media endpoint** (multipart file upload) is
68- simply not implemented until a plugin can pipe an un-drained body.
80+ simply not implemented until a plugin can pipe an un-drained body;
81+ jmap/ likewise omits blobs/attachments (` uploadUrl ` absent,
82+ ` maxSizeUpload: 0 ` advertised honestly).
6983 Whatever ` api.mountApp ` / raw-body mode ships must hand back the
7084 un-drained stream, not just a buffered body. (This is exactly what the
7185 merged loader's scoped pass-through parser does — the finding is to
@@ -97,10 +111,18 @@ ports reached for it without coordinating.
97111 an API shim, did: web can't escape to a fake root (the DID id is fixed
98112 by the method). It works only where the pod grants public Read. This is
99113 the sharpest form and needs a ** parameterized** ` api.reservePath ` .
100- - none of it has conflict detection: a future core route at a
101- plugin-claimed path throws ` FST_ERR_DUPLICATED_ROUTE ` at boot (and a
102- link registry — ` api.webfinger.addLink ` — is missing, so two plugins
103- contributing ` .well-known/webfinger ` links would collide silently).
114+ - conflict detection: ** the collision is now WITNESSED, both ways**
115+ (remotestorage/ + webfinger/ both own ` /.well-known/webfinger ` ).
116+ Unguarded claim second → boot fails: `plugin remotestorage:
117+ activate() failed: Method 'GET' already declared for route
118+ '/.well-known/webfinger'` (Fastify's ` FST_ERR_DUPLICATED_ROUTE`, but
119+ the loader's error wrap ** drops ` err.code ` ** — only the message
120+ identifies it). Reverse order → boot * succeeds* and webfinger/'s
121+ try/catch-guarded claim ** silently loses** : its profile/actor/issuer
122+ links vanish with no error. Loud failure or silent loss — both wrong,
123+ because both plugins legitimately own * parts* of one discovery
124+ document. The missing seam is a link/JRD registry
125+ (` api.webfinger.addLink ` ).
104126 The seam: ` api.reservePath('/xrpc') ` / ` api.reservePath('/:user/did.json') `
105127 (or ` paths: [...] ` in the entry) — the loader exempts * and* claims each
106128 deliberately and reports collisions. The seam ** every API-shim plugin**
@@ -118,11 +140,16 @@ ports reached for it without coordinating.
118140 (` src/nostr/event.js ` NIP-01 verify), pay/ (` src/mrc20.js ` ). Pure crypto.
119141 Export like auth.js (` javascript-solid-server/nostr.js ` ) or bless
120142 vendoring.
121- 8 . ** Response-header injection on core routes** — ** two consumers:
122- notifications/** (` Updates-Via ` discovery) and ** micropub/** (clients
143+ 8 . ** Response-header injection on core routes** — ** three consumers:
144+ notifications/** (` Updates-Via ` discovery), ** micropub/** (clients
123145 find the endpoint via ` <link rel="micropub"> ` on the user's homepage —
124146 a core-owned resource the plugin can't decorate; the operator must
125- advertise it by hand). A plugin can't add headers to routes it doesn't
147+ advertise it by hand), and ** oembed/** — the sharpest form: oEmbed
148+ discovery wants a per-resource ` <link> ` in * every HTML resource's
149+ head* , which even a gated header hook couldn't provide (in-HTML
150+ injection is content rewriting — core's side of the #564 line; the
151+ ` Link: ` header variant would cover the rest). A plugin can't add
152+ headers to routes it doesn't
126153 own. NOT a default-on hook (bigger grant than route ownership); gate
127154 behind ` capabilities: ['hooks'] ` if ever.
1281559 . ** Plugin-to-plugin isolation is ZERO (measured — metrics/).** All
@@ -173,6 +200,7 @@ MODIFIES the request pipeline of routes it doesn't own.**
173200
174201| Feature | Shape | Verdict | Evidence |
175202| ---| ---| ---| ---|
203+ | remoteStorage | owns ` /storage/:user/* ` | ✅ plugin | remotestorage/ — 7th port; core's bundled copy could move out-of-tree |
176204| nostr relay | owns ` /relay ` ws | ✅ plugin | relay/ — parity + persistence |
177205| webrtc | owns ` /webrtc ` ws | ✅ plugin | webrtc/ — full parity, zero imports |
178206| terminal | owns a ws | ✅ plugin | terminal/ — hardened beyond core |
@@ -205,6 +233,11 @@ this repo is its proof.
205233 candidate: when the basename is generic (` plugin ` , ` index ` ), derive from
206234 the parent directory (` relay/plugin.js ` → ` relay ` ). Small, backward-
207235 compatible, removes the most common footgun. ** Filed-worthy.**
236+ - ** The loader's activate-failure wrap drops ` err.code ` **
237+ (remotestorage/): a route collision inside ` activate ` surfaces as
238+ ` plugin <id>: activate() failed: <message> ` with the original
239+ ` FST_ERR_DUPLICATED_ROUTE ` code stripped — callers can only string-match
240+ the message. Preserve ` code ` (or ` cause ` ) when re-wrapping.
208241- ** ` logger: false ` silently kills every plugin ` onResponse ` hook**
209242 (metrics/): core's access-log hook calls
210243 ` request.log.isLevelEnabled('info') ` , which doesn't exist on Fastify's
0 commit comments