Skip to content

Commit eada353

Browse files
matrix: self-reserve the /_matrix root via api.reservePath (#602)
The fourth shim consumer of the seam, consumed as of JSS 0.0.219 — no operator appPaths. Widened to POST (login, createRoom) and PUT (room send) only; DELETE/PATCH stay gated so nothing falls through to LDP's write wildcards. 12/12 tests green with the hand-widening removed.
1 parent b4d83cd commit eada353

3 files changed

Lines changed: 60 additions & 41 deletions

File tree

matrix/README.md

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ timeline is the events you appended to it.
1010
This is the **chat-protocol** entry in the API-shim family — after the two
1111
microblog shims (`mastodon/`, `bluesky/`) it confirms the same two seams a
1212
third time, now against a fixed `/_matrix` root: the reserved-path/`appPaths`
13-
gap and the `/idp/credentials` token bridge.
13+
gap (since **closed** by `api.reservePath()`, JSS 0.0.219) and the
14+
`/idp/credentials` token bridge.
1415

1516
```
1617
plugins: [{
@@ -22,15 +23,11 @@ plugins: [{
2223
}]
2324
```
2425

25-
**and** — the load-bearing caveat — the operator must WAC-exempt the fixed
26-
Matrix root, because a plugin cannot do it itself (see [Findings](#findings)):
27-
28-
```
29-
createServer({
30-
appPaths: ['/_matrix'], // REQUIRED: or WAC 401s every client call
31-
plugins: [ … ],
32-
})
33-
```
26+
That's the whole setup. As of JSS 0.0.219 the plugin claims and WAC-exempts
27+
the fixed `/_matrix` root itself via `api.reservePath()` at activate time
28+
(#602) — no `appPaths` widening required. (Before 0.0.219 the operator had to
29+
pass `appPaths: ['/_matrix']` by hand or WAC 401'd every client call — see
30+
[Findings](#findings).)
3431

3532
## Pointing a client at it
3633

@@ -123,7 +120,7 @@ that require the field.
123120

124121
## Findings
125122

126-
### 1. Matrix's fixed `/_matrix` root doesn't fit the one-prefix plugin model — the Nth confirmation, now for a chat protocol
123+
### 1. Matrix's fixed `/_matrix` root doesn't fit the one-prefix plugin model — the Nth confirmation, now for a chat protocol (CLOSED: `api.reservePath`, JSS 0.0.219)
127124

128125
The same seam `mastodon/` (`/api`+`/oauth`) and `bluesky/` (`/xrpc`) found,
129126
confirmed a **third** time and for a **different protocol family** (real-time
@@ -135,20 +132,32 @@ chat, not microblogging). A Matrix client hits **fixed absolute paths** under
135132
prefix (`api.fastify` is the real scoped instance), and these static/param
136133
routes outrank core's LDP `GET /*` wildcard on Fastify's specificity
137134
ordering. Registration is fine.
138-
- **Authorization does not.** `/_matrix` is an ordinary pod path, not
139-
`/.well-known/*` (which core blanket-exempts). The WAC hook skips only paths
140-
in `appPaths`, and the loader pushes a plugin's **single `prefix`** there.
141-
A plugin has no way to push more roots — there is no `api.reservePath()` /
142-
`api.appPaths.add()` — so it **cannot self-exempt its own surface**, and
143-
every unexempted client call is 401'd by WAC before the handler runs.
144-
145-
The honest consequence: this shim is only usable if the **operator** widens
146-
`appPaths` by hand (`appPaths: ['/_matrix']`). That the finding now holds
147-
across **social (`activitypub`) → microblog (`mastodon`, `bluesky`) → chat
148-
(`matrix`)** is the point: the one-prefix model can't express *any* app whose
149-
routes are dictated by an external protocol at a fixed absolute root,
150-
regardless of protocol family. An `api.reservePath()` surface would close it
151-
uniformly.
135+
- **Authorization did not (before 0.0.219).** `/_matrix` is an ordinary pod
136+
path, not `/.well-known/*` (which core blanket-exempts). The WAC hook skips
137+
only paths in `appPaths`, and the loader pushes a plugin's **single
138+
`prefix`** there. A plugin had no way to push more roots — there was no
139+
`api.reservePath()` / `api.appPaths.add()` — so it **could not self-exempt
140+
its own surface**, and every unexempted client call was 401'd by WAC before
141+
the handler ran.
142+
143+
The honest consequence, at the time: the shim was only usable if the
144+
**operator** widened `appPaths` by hand (`appPaths: ['/_matrix']`). That the
145+
finding held across **social (`activitypub`) → microblog (`mastodon`,
146+
`bluesky`) → chat (`matrix`)** was the point: the one-prefix model couldn't
147+
express *any* app whose routes are dictated by an external protocol at a
148+
fixed absolute root, regardless of protocol family — an `api.reservePath()`
149+
surface would close it uniformly.
150+
151+
**Closed — JSS 0.0.219 shipped exactly that seam (#602), consumed here.**
152+
`activate()` now calls `api.reservePath('/_matrix', { methods: ['GET',
153+
'HEAD', 'OPTIONS', 'POST', 'PUT'] })`: the literal reservation WAC-exempts
154+
the whole `/_matrix` subtree, widened to exactly the write verbs the routes
155+
implement (POST login/createRoom, PUT send-event) and no further — exempting
156+
a verb no route implements would let it fall through to core's LDP write
157+
wildcards as an unauthenticated storage write. A second plugin reserving the
158+
same root fails the boot loudly (naming both claimants), and route
159+
registration stays the plugin's job. The operator no longer touches
160+
`appPaths`.
152161

153162
### 2. The `/idp/credentials` token bridge generalizes to a 4th protocol
154163

matrix/plugin.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
// to its prefix, and each `/_matrix/client/...` route outranks core's LDP
2424
// `GET /*` wildcard on Fastify's route-specificity ordering. That the whole
2525
// Matrix Client-Server surface lives at ONE fixed root OUTSIDE the plugin's
26-
// own prefix — which the plugin cannot WAC-exempt itself — is the headline
27-
// finding (README "Findings"): the Nth independent confirmation of the
28-
// mastodon/bluesky reserved-path seam, now for a CHAT protocol.
26+
// own prefix is the headline finding (README "Findings"): the Nth
27+
// independent confirmation of the mastodon/bluesky reserved-path seam, now
28+
// for a CHAT protocol; as of JSS 0.0.219 the plugin claims + WAC-exempts
29+
// that root itself via api.reservePath (#602), so the operator no longer
30+
// touches appPaths.
2931
//
3032
// --------------------------------------------------------- the token bridge
3133
//
@@ -183,6 +185,15 @@ export async function activate(api) {
183185

184186
// =================================================================== routes
185187

188+
// Claim + WAC-exempt the fixed Matrix root (#602, JSS 0.0.219). A literal
189+
// reservation exempts the whole /_matrix subtree but is READ-ONLY by
190+
// default, so widen to exactly the write verbs the routes below implement:
191+
// POST (login, createRoom) and PUT (send-event). DELETE/PATCH stay gated
192+
// on purpose — exempting a verb no route implements would let it fall
193+
// through to core's LDP write wildcards as an unauthenticated storage
194+
// write.
195+
api.reservePath('/_matrix', { methods: ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] });
196+
186197
// Preflight for the whole shim surface (Element is a browser app).
187198
api.fastify.options('/_matrix/*', (request, reply) => cors(reply).code(204).send());
188199

@@ -406,11 +417,10 @@ export async function activate(api) {
406417
});
407418

408419
api.log.info(`matrix: Client-Server shim at /_matrix/client/* → pods via ${loopback} (Phase 1)`);
409-
// The load-bearing caveat (see README "Findings"): these absolute routes
410-
// register fine, but they are only reachable if the operator has WAC-
411-
// exempted the /_matrix root. A plugin gets ONE prefix pushed to appPaths
412-
// and cannot self-exempt a fixed protocol root — so the operator must pass
413-
// `appPaths: ['/_matrix']` to createServer. Same seam as mastodon/bluesky/.
414-
api.log.warn('matrix: /_matrix must be in appPaths or WAC will 401 every client '
415-
+ 'request — the one-prefix plugin model cannot self-exempt a fixed protocol root');
420+
// The formerly load-bearing caveat, now closed (see README "Findings"):
421+
// before JSS 0.0.219 a plugin got ONE prefix pushed to appPaths and could
422+
// not self-exempt a fixed protocol root, so the operator had to pass
423+
// `appPaths: ['/_matrix']` or WAC 401'd every client call. The
424+
// api.reservePath call above (#602) claims the root at activate time —
425+
// same seam, same closure, as mastodon/bluesky/.
416426
}

matrix/test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
// Same probe-port-then-boot dance as mastodon/ and bluesky/: the shim needs
1212
// its server origin in config before listen (finding: api.serverInfo), and
1313
// idp:true gives us the /idp/register + /idp/credentials the token bridge
14-
// rides on. The fixed `/_matrix` root is widened into appPaths by hand (the
15-
// reserved-path finding — the operator must, a plugin can't self-exempt).
14+
// rides on. The fixed `/_matrix` root is self-reserved via api.reservePath
15+
// (#602), consumed as of JSS 0.0.219 — the reserved-path finding, closed:
16+
// the operator no longer widens appPaths by hand.
1617

1718
import { describe, it, after } from 'node:test';
1819
import assert from 'node:assert';
@@ -49,10 +50,9 @@ describe('matrix plugin', () => {
4950
jss = await startJss({
5051
port,
5152
idp: true,
52-
// The finding in action: Matrix's fixed Client-Server root (/_matrix)
53-
// is an absolute path no single plugin `prefix` can own, so the plugin
54-
// can't self-exempt it from WAC — the operator widens appPaths by hand.
55-
appPaths: ['/_matrix'],
53+
// No appPaths here — the closed finding in action: the plugin claims
54+
// + WAC-exempts the fixed /_matrix root itself via api.reservePath
55+
// (#602), consumed as of JSS 0.0.219.
5656
plugins: [{ module: module_, config: { baseUrl: base } }],
5757
});
5858
const reg = await fetch(`${base}/idp/register`, {

0 commit comments

Comments
 (0)