Skip to content

Commit 719fe5d

Browse files
bluesky: self-reserve the /xrpc root via api.reservePath (#602)
The shim that sharpened the ask ('declare owned paths, not more prefixes') now declares its own: /xrpc claimed and WAC-exempted at activate, widened to POST only (createSession, createRecord). 11/11 tests green with the operator appPaths removed.
1 parent eada353 commit 719fe5d

3 files changed

Lines changed: 53 additions & 39 deletions

File tree

bluesky/README.md

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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
127121
from a completely different external protocol — which is exactly what makes it
128122
a *confirmation* rather than a repeat. An AT-Protocol client hits **fixed
129123
absolute 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**,
153147
independently of its mount prefix" (`paths: ['/xrpc']` on the entry, or an
154148
`api.reservePath()` surface, with collision detection). This is the seam **all
155149
API-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
157151
above 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

161169
The mastodon finding held with zero changes: two OAuth-shaped worlds (AT's

bluesky/plugin.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
// routes to its prefix, and each `/xrpc/<nsid>` static route outranks core's
2222
// LDP `GET /*` wildcard on Fastify's route-specificity ordering. That the
2323
// whole XRPC surface lives at ONE fixed root OUTSIDE the plugin's own prefix
24-
// — which the plugin cannot WAC-exempt itself — is the headline finding
24+
// — which the plugin could not WAC-exempt itself — was the headline finding
2525
// (README "Findings"): a SECOND independent confirmation of mastodon's
26-
// reserved-path/multi-prefix seam.
26+
// reserved-path/multi-prefix seam. As of JSS 0.0.219 the plugin claims +
27+
// WAC-exempts that root itself via api.reservePath (#602), so the operator
28+
// no longer touches appPaths.
2729
//
2830
// --------------------------------------------------------- the token bridge
2931
//
@@ -295,6 +297,15 @@ export async function activate(api) {
295297

296298
// =================================================================== routes
297299

300+
// Claim + WAC-exempt the one fixed XRPC root (#602, JSS 0.0.219). A
301+
// literal reservation exempts the whole /xrpc subtree but is READ-ONLY
302+
// by default, so widen to POST — the only write verb any route below
303+
// implements (createSession, createRecord: XRPC procedures are POSTs).
304+
// PUT/DELETE/PATCH stay gated on purpose: exempting a verb no route
305+
// implements would let it fall through to core's LDP write wildcards
306+
// as an unauthenticated storage write.
307+
api.reservePath('/xrpc', { methods: ['GET', 'HEAD', 'OPTIONS', 'POST'] });
308+
298309
// Preflight for the whole XRPC surface.
299310
api.fastify.options('/xrpc/*', (request, reply) => cors(reply).code(204).send());
300311

@@ -420,12 +431,7 @@ export async function activate(api) {
420431
});
421432

422433
api.log.info(`bluesky: XRPC shim at /xrpc/* → pods via ${loopback} (issue #211 Phase 1)`);
423-
// The load-bearing caveat (see README "Findings"): these absolute /xrpc
424-
// routes register fine, but they are only reachable if the operator has
425-
// WAC-exempted /xrpc. A plugin gets ONE prefix pushed to appPaths and this
426-
// fixed protocol root is not it, so the plugin cannot self-exempt — the
427-
// operator must pass `appPaths: ['/xrpc']` to createServer. This is the
428-
// SECOND independent confirmation of mastodon's reserved-path seam.
429-
api.log.warn('bluesky: /xrpc must be in appPaths or WAC will 401 every client '
430-
+ 'request — the one-prefix plugin model cannot self-exempt a fixed protocol root');
434+
// The former load-bearing caveat is closed: /xrpc is self-reserved above
435+
// via api.reservePath (#602), so no operator appPaths are needed since
436+
// JSS 0.0.219 (see README "Findings").
431437
}

bluesky/test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ describe('bluesky plugin', () => {
5050
jss = await startJss({
5151
port,
5252
idp: true,
53-
// The finding in action: AT-Protocol's fixed `/xrpc/*` root is an
54-
// absolute path no single plugin `prefix` can own, so the plugin can't
55-
// self-exempt it from WAC — the operator widens appPaths by hand. This
56-
// re-proves mastodon's multi-prefix seam (a SECOND time). See README.
57-
appPaths: ['/xrpc'],
53+
// The finding, closed: AT-Protocol's fixed `/xrpc/*` root — one root,
54+
// once unreachable without the operator widening appPaths by hand —
55+
// is self-reserved via api.reservePath (#602), consumed as of JSS
56+
// 0.0.219. No appPaths here, and the suite passing without them is
57+
// the proof. See README "Findings".
5858
plugins: [{ module: module_, config: { baseUrl: base } }],
5959
});
6060
const reg = await fetch(`${base}/idp/register`, {

0 commit comments

Comments
 (0)