@@ -22,15 +22,9 @@ plugins: [{
2222}]
2323```
2424
25- …** and** — the load-bearing caveat — the operator must WAC-exempt the fixed
26- ` /xrpc ` root, because a plugin cannot do it itself (see [ Findings] ( #findings ) ):
27-
28- ```
29- createServer({
30- appPaths: ['/xrpc'], // REQUIRED: or WAC 401s every client call
31- plugins: [ … ],
32- })
33- ```
25+ No ` appPaths ` needed: since JSS 0.0.219 the plugin claims and WAC-exempts
26+ its one fixed root itself at activate time via ` api.reservePath('/xrpc') `
27+ (#602 ) — see [ Findings] ( #findings ) for the gap this closed.
3428
3529## The vertical slice
3630
@@ -123,39 +117,53 @@ mints no token of its own, keeps no session store:
123117
124118### 1. SECOND independent confirmation of the reserved-path / multi-prefix seam
125119
126- ** Headline.** This is the same wall [ ` mastodon/ ` ] ( ../mastodon/ ) hit, reached
120+ ** Headline.** This was the same wall [ ` mastodon/ ` ] ( ../mastodon/ ) hit, reached
127121from a completely different external protocol — which is exactly what makes it
128122a * confirmation* rather than a repeat. An AT-Protocol client hits ** fixed
129123absolute paths** under one root, ` /xrpc/<nsid> ` , that no client will let you
130- relocate under a plugin ` prefix ` . Two things follow , identically to mastodon:
124+ relocate under a plugin ` prefix ` . Two things followed , identically to mastodon:
131125
132126- ** Routing works.** Like mastodon's ` /api ` + ` /oauth ` and nip05's
133127 ` /.well-known/nostr.json ` , the loader does not confine a plugin's routes to
134128 its prefix (` api.fastify ` is the real scoped instance), and each
135129 ` /xrpc/<nsid> ` static route outranks core's LDP ` GET /* ` wildcard on
136130 Fastify's specificity ordering. Registration is fine.
137- - ** Authorization does not.** ` /xrpc ` is an ordinary pod path as far as WAC is
131+ - ** Authorization did not.** ` /xrpc ` is an ordinary pod path as far as WAC is
138132 concerned. The WAC hook skips only paths in ` appPaths ` (` server.js ` ), and
139133 the loader pushes a plugin's ** single ` prefix ` ** there (` plugins.js ` — `if
140134 (prefix) ctx.appPaths.push(prefix)`). This shim's surface lives at a ** fixed
141- protocol root that is not its prefix** , and a plugin has no
135+ protocol root that is not its prefix** , and a plugin had no
142136 ` api.appPaths.add() ` / ` api.reservePath() ` to push it. So the plugin
143- ** cannot self-exempt its own surface** , and every unexempted ` /xrpc ` call is
144- 401'd by WAC before the handler runs .
145-
146- The honest consequence, verbatim from mastodon: this shim is only usable if
147- the ** operator** widens ` appPaths ` by hand (` appPaths: ['/xrpc'] ` ). Note the
148- sharpening over mastodon: mastodon needed * two* roots and blamed the
149- "one-prefix" model; bluesky needs only * one* root and ** still can 't reach it ** ,
150- because the exempt path is tied to the plugin's * own* prefix, not to the paths
151- it actually serves. The seam is not "a plugin should get more than one
152- prefix" — it is "** a plugin must be able to declare the paths it owns** ,
137+ ** could not self-exempt its own surface** , and every unexempted ` /xrpc ` call
138+ was 401'd by WAC before the handler ran .
139+
140+ The honest consequence * was * , verbatim from mastodon: this shim was only
141+ usable if the ** operator** widened ` appPaths ` by hand (` appPaths: ['/xrpc'] ` ).
142+ Note the sharpening over mastodon: mastodon needed * two* roots and blamed the
143+ "one-prefix" model; bluesky needed only * one* root and ** still couldn 't reach
144+ it ** , because the exempt path was tied to the plugin's * own* prefix, not to
145+ the paths it actually serves. The seam is not "a plugin should get more than
146+ one prefix" — it is "** a plugin must be able to declare the paths it owns** ,
153147independently of its mount prefix" (` paths: ['/xrpc'] ` on the entry, or an
154148` api.reservePath() ` surface, with collision detection). This is the seam ** all
155149API-shim plugins** (mastodon, bluesky, and any future ActivityPub / gateway)
156- structurally require; it now has ** two independent consumers** and belongs
150+ structurally require; it got ** two independent consumers** and belongs
157151above mastodon-alone in NOTES.md's ranking of the reserved-path seam.
158152
153+ ** Closed — JSS 0.0.219 shipped ` api.reservePath(path, opts) ` (#602 ) and this
154+ plugin consumes it.** At activate time it reserves the literal root ` /xrpc ` ;
155+ a literal reservation WAC-exempts the whole subtree, and a second plugin
156+ claiming the same root fails the boot loudly instead of silently losing.
157+ Reservations are ** read-only by default** (GET/HEAD/OPTIONS), so the root is
158+ widened with ` { methods: ['GET', 'HEAD', 'OPTIONS', 'POST'] } ` — POST is the
159+ only write verb the shim implements (` POST createSession ` ,
160+ ` POST createRecord ` : XRPC procedures are POSTs). PUT/DELETE/PATCH are
161+ deliberately ** not** exempted: no route implements them, and an exemption on
162+ an unimplemented verb would fall through to core's LDP write wildcards as an
163+ unauthenticated storage write. Registering the routes is still the plugin's
164+ job; the reservation only settles claim + WAC. The operator config shrinks to
165+ just ` plugins: ` — the test suite passing without any ` appPaths ` is the proof.
166+
159167### 2. The loopback → ` /idp/credentials ` token bridge generalizes cleanly
160168
161169The mastodon finding held with zero changes: two OAuth-shaped worlds (AT's
0 commit comments