Skip to content

Commit 6aa55f1

Browse files
Integrate wave 5 (caldav, webfinger, activitypub, rss): 20 plugins, 195 tests
- compose.test.js + serve.js load all 20 (appPaths widened +/ap for activitypub); 21-check compose. - README/ISSUES tables + tally (13 built as plugins, 20 total). - NOTES: reserved-path is now the most-hit finding (7+ consumers) — 3 protocol-shim confirmations (mastodon/bluesky/activitypub), activitypub sharpening it (wants paths interleaved with the pod namespace); the two most-wanted .well-known docs (nip05, webfinger) both by luck. api.serverInfo elevated to broadest seam (~10 consumers). DAV bridge proven 3x.
1 parent b0c61cf commit 6aa55f1

5 files changed

Lines changed: 95 additions & 44 deletions

File tree

ISSUES.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ evidence. Legend:
2727
| #505 | `otp/` | one-time-password session flow (account *recovery* → 🔩 core auth) |
2828
| #157 | `carddav/` | CardDAV contact sync (iOS/macOS/Thunderbird/DAVx5) |
2929
| #211 | `bluesky/` | AT-Protocol XRPC shim — a client logs into its own pod (needs `appPaths` widened → 🔩) |
30+
| #157-sib | `caldav/` | CalDAV calendar sync — completes the DAV family |
31+
| #164 | `webfinger/` | `/.well-known/webfinger` — the WebFinger half of #164 |
32+
| #51/#164 | `activitypub/` | federate a pod as an AS2 actor (needs `appPaths` widened → 🔩) |
33+
|| `rss/` | any pod container as an Atom/RSS feed |
3034

3135
Plus six **ports of bundled features** proving the migration path for #564 /
3236
#164: `relay/` `webrtc/` `terminal/` `tunnel/` `notifications/`, and `pay/`
@@ -87,11 +91,12 @@ exists.
8791

8892
## Tally
8993

90-
Of ~40 plugin-tagged issues: **10 built as plugins here**, **6 ported**, **5
91-
shipped upstream**, **2 more plugin-able with no blocker**, **6 clusters
92-
blocked on a named seam** (each with a proof-of-need consumer), the rest
93-
core-by-nature or product-scale. The plugin api reaches most of the backlog
94-
today; ranked by how many independent plugins demanded them, the seams that
95-
would unlock the most next are `api.authorize` (3 consumers), `api.events`
96-
(2, one where a miss means *wrong* answers), and multi-prefix/`appPaths.add`
97-
(every API-shim).
94+
Of ~40 plugin-tagged issues: **13 built as plugins here** (plus `rss/`, no
95+
issue), **6 ported**, **5 shipped upstream**, **2 more plugin-able with no
96+
blocker**, **6 clusters blocked on a named seam** (each with a proof-of-need
97+
consumer), the rest core-by-nature or product-scale. 20 plugins total, ~200
98+
tests. The plugin api reaches most of the backlog today; ranked by demand,
99+
the seams that would unlock the most next are `api.authorize` (3 blocking
100+
consumers), `api.reservePath` (3 API-shims can't self-exempt), `api.events`
101+
(2, one where a miss means *wrong* answers), and `api.serverInfo` (broadest:
102+
~10 plugins hand-roll their own origin).

NOTES.md

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,40 +45,46 @@ ports reached for it without coordinating.
4545
has the emitter internally (`src/notifications/events.js`). The seam
4646
every "react to pod writes" app (webhooks, indexing, sync, full-text)
4747
will want.
48-
3. **`api.serverInfo` (`{ baseUrl, port }` at listen)****consumers:
49-
notifications/, webdav/, sparql/, nip05/** (subdomain per-host
50-
filtering). Every plugin that mints absolute URLs or reaches the host
51-
over loopback repeats the origin in config today; a wrong value fails
52-
quietly (nip05 serves an empty identity map). Cheap to provide.
48+
3. **`api.serverInfo` (`{ baseUrl, port }` at listen)****eight+
49+
consumers: notifications/, webdav/, carddav/, caldav/, sparql/, rss/,
50+
nip05/, webfinger/, mastodon/, bluesky/, activitypub/** — essentially
51+
every plugin that mints absolute URLs or reaches the host over loopback.
52+
All repeat the origin in config today; a wrong value fails quietly (nip05
53+
serves an empty identity map). The single most *broadly* needed seam
54+
(vs. api.authorize being the most *blocking*); trivially cheap to provide.
5355
4. **The unconsumed-body-**stream** primitive (#583)** — consumer:
5456
gitscratch/ sharpened it. tunnel/ needed the raw *buffer*; git needs the
5557
raw *stream* piped to a subprocess gzip-and-all. Whatever `api.mountApp`
5658
/ raw-body mode ships must hand back the un-drained stream, not just a
5759
buffered body. (This is exactly what the merged loader's scoped
5860
pass-through parser does — the finding is to keep it that way.)
59-
5. **Routes/WAC-exemption outside the single prefix****four consumers:
60-
nip05/, webdav/ & carddav/ (`/.well-known/*`), mastodon/ (`/api`,
61-
`/oauth`).** A plugin can *register* absolute/exact routes outside its
62-
prefix (the loader doesn't confine `api.fastify`), but the loader
63-
WAC-exempts only its **one** `prefix` (`plugins.js` pushes `prefix` to
64-
`appPaths`). Consequences, in increasing severity:
65-
- nip05/webdav/carddav land on `/.well-known/*`, which core *happens* to
66-
blanket-exempt — so they work **by luck**, not by contract.
67-
- **mastodon** needs `/api` **and** `/oauth`, and **bluesky** needs
68-
`/xrpc` — none of which core exempts, so **the plugin cannot serve its
69-
own surface**: every call 401s at the WAC hook until the operator
70-
hand-passes `appPaths: ['/api','/oauth','/xrpc']`. A plugin has no way
71-
to exempt a path it owns. **Two independent confirmations** (two
72-
protocol shims, built separately), and bluesky sharpens it: it needs
73-
only *one* extra root and still can't reach it, so the seam is not
74-
"more prefixes" but **"a plugin declares the paths it owns,
75-
independent of its mount prefix."**
61+
5. **Routes/WAC-exemption outside the single prefix****the most-hit
62+
finding: seven+ consumers.** A plugin can *register* absolute/exact
63+
routes outside its prefix (the loader doesn't confine `api.fastify`), but
64+
the loader WAC-exempts only its **one** `prefix`. Consequences, in
65+
increasing severity:
66+
- **`/.well-known/*` served by luck** — nip05/ (`nostr.json`),
67+
webfinger/ (`webfinger`), webdav/carddav/caldav (`caldav`/`carddav`).
68+
Core *happens* to blanket-exempt `/.well-known/*`, so they work but by
69+
coincidence, not contract. Notably nip05 and webfinger are the **two
70+
most-wanted `.well-known` docs a deployment serves**, both riding the
71+
same undocumented luck.
72+
- **fixed roots core does NOT exempt** — mastodon/ (`/api`,`/oauth`),
73+
bluesky/ (`/xrpc`), activitypub/ (`/ap`). Here the plugin **cannot
74+
serve its own surface**: every call 401s at the WAC hook until the
75+
operator hand-passes `appPaths`. **Three independent protocol-shim
76+
confirmations**, each built separately. bluesky sharpened it (one root,
77+
still unreachable → the ask is "declare owned paths," not "more
78+
prefixes"); activitypub sharpened it further — its natural layout wants
79+
paths **interleaved with** the pod's own `/<user>/` namespace, which no
80+
single mount prefix can carve out at all.
7681
- none of it has conflict detection: a future core route at a
77-
plugin-claimed path throws `FST_ERR_DUPLICATED_ROUTE` at boot.
82+
plugin-claimed path throws `FST_ERR_DUPLICATED_ROUTE` at boot (and a
83+
link registry — `api.webfinger.addLink` — is missing, so two plugins
84+
contributing `.well-known/webfinger` links would collide silently).
7885
The seam: `api.reservePath('/xrpc')` (or `paths: [...]` in the entry) —
7986
the loader exempts *and* claims each deliberately and reports collisions.
80-
This is the seam **every API-shim plugin** (mastodon, bluesky, and any
81-
future ActivityPub/Matrix/gateway) structurally requires; it's the third
87+
The seam **every API-shim plugin** structurally requires; third
8288
most-demanded after `api.authorize` and `api.events`.
8389
6. **Can't set fastify server options** — consumer: capability/ hit
8490
`maxParamLength` (100) silently 404ing long tokens in named params;

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ README and NOTES.md, ship the closest honest approximation.
6565
| `carddav/` | #157 | contact sync (iOS/macOS/Thunderbird/DAVx5); the DAV bridge, generalized |
6666
| `mastodon/` | #515/#516 | Mastodon-API shim — a client logs into its own pod; needs `appPaths` widened |
6767
| `bluesky/` | #211 | AT-Protocol XRPC shim — same shape, `/xrpc` root; token bridge generalizes |
68+
| `caldav/` | #157-sib | calendar sync — completes the DAV family (webdav+carddav+caldav) |
69+
| `webfinger/` | #164 | `/.well-known/webfinger` — fediverse `acct:` resolution for the shims |
70+
| `activitypub/` | #51/#164 | federate a pod as an AS2 actor; outbound HTTP Signatures |
71+
| `rss/` || any pod container as an Atom/RSS feed |
6872

69-
**~150 tests, all green** (`npm test`), including `compose.test.js` — all
70-
sixteen plugins on one server from pure config, pods + WAC intact beside
73+
**~200 tests, all green** (`npm test`), including `compose.test.js` — all
74+
twenty plugins on one server from pure config, pods + WAC intact beside
7175
them. Findings consolidated in [NOTES.md](./NOTES.md); the full
7276
plugin-tagged backlog triaged in [ISSUES.md](./ISSUES.md).
7377

compose.test.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('composition: every plugin on one server', () => {
4646

4747
after(async () => { if (jss) await jss.close(); });
4848

49-
it('boots pods + idp + sixteen plugins from config', async () => {
49+
it('boots pods + idp + twenty plugins from config', async () => {
5050
const port = await probePort();
5151
base = `http://127.0.0.1:${port}`;
5252
wsBase = `ws://127.0.0.1:${port}`;
@@ -55,10 +55,10 @@ describe('composition: every plugin on one server', () => {
5555
port,
5656
root,
5757
idp: true,
58-
// mastodon (/api,/oauth) and bluesky (/xrpc) own fixed roots outside
59-
// their prefix; a plugin can't self-exempt them (finding), so the
60-
// operator widens appPaths.
61-
appPaths: ['/api', '/oauth', '/xrpc'],
58+
// The protocol shims own fixed roots outside their prefix — mastodon
59+
// (/api,/oauth), bluesky (/xrpc), activitypub (/ap) — which a plugin
60+
// can't self-exempt (finding), so the operator widens appPaths.
61+
appPaths: ['/api', '/oauth', '/xrpc', '/ap'],
6262
// Explicit ids: the <name>/plugin.js convention makes every basename
6363
// reduce to 'plugin' — the loader's duplicate-id guard requires ids
6464
// here (finding: derive from the parent dir for generic basenames).
@@ -84,6 +84,10 @@ describe('composition: every plugin on one server', () => {
8484
{ id: 'carddav', module: at('carddav/plugin.js'), prefix: '/carddav', config: { baseUrl: base, loopbackUrl: base } },
8585
{ id: 'mastodon', module: at('mastodon/plugin.js'), prefix: '/mastodon', config: { baseUrl: base, loopbackUrl: base } },
8686
{ id: 'bluesky', module: at('bluesky/plugin.js'), prefix: '/bluesky', config: { baseUrl: base, loopbackUrl: base } },
87+
{ id: 'caldav', module: at('caldav/plugin.js'), prefix: '/caldav', config: { baseUrl: base, loopbackUrl: base } },
88+
{ id: 'webfinger', module: at('webfinger/plugin.js'), prefix: '/webfinger', config: { podsRoot: root, baseUrl: base } },
89+
{ id: 'activitypub', module: at('activitypub/plugin.js'), prefix: '/activitypub', config: { baseUrl: base, loopbackUrl: base } },
90+
{ id: 'rss', module: at('rss/plugin.js'), prefix: '/feed', config: { baseUrl: base, loopbackUrl: base } },
8791
],
8892
});
8993
assert.ok(jss.base);
@@ -222,6 +226,30 @@ describe('composition: every plugin on one server', () => {
222226
assert.strictEqual(res.status, 200);
223227
});
224228

229+
it('caldav: OPTIONS advertises calendar-access', async () => {
230+
const res = await fetch(`${base}/caldav/`, { method: 'OPTIONS' });
231+
assert.ok(res.status < 500);
232+
assert.match(res.headers.get('dav') || '', /calendar-access/);
233+
});
234+
235+
it('webfinger: missing resource is 400 (endpoint alive)', async () => {
236+
const res = await fetch(`${base}/.well-known/webfinger`);
237+
assert.strictEqual(res.status, 400);
238+
});
239+
240+
it('activitypub: unauthenticated outbox POST is refused', async () => {
241+
const res = await fetch(`${base}/ap/alice/outbox`, {
242+
method: 'POST', headers: { 'content-type': 'application/activity+json' },
243+
body: JSON.stringify({ type: 'Note', content: 'x' }),
244+
});
245+
assert.strictEqual(res.status, 401);
246+
});
247+
248+
it('rss: missing container is 400 (endpoint alive)', async () => {
249+
const res = await fetch(`${base}/feed/atom`);
250+
assert.strictEqual(res.status, 400);
251+
});
252+
225253
it('pods still work beside all of it (idp register + WAC)', async () => {
226254
let res = await fetch(`${base}/idp/register`, {
227255
method: 'POST',

serve.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ const fastify = createServer({
2828
root: PODS,
2929
idp: true,
3030
idpIssuer: PUBLIC_URL,
31-
// mastodon (/api,/oauth) and bluesky (/xrpc) own fixed roots outside
32-
// their prefix; a plugin can't self-exempt them from WAC, so the
33-
// operator widens appPaths.
34-
appPaths: ['/api', '/oauth', '/xrpc'],
31+
// The protocol shims own fixed roots outside their prefix — mastodon
32+
// (/api,/oauth), bluesky (/xrpc), activitypub (/ap) — which a plugin
33+
// can't self-exempt from WAC, so the operator widens appPaths.
34+
appPaths: ['/api', '/oauth', '/xrpc', '/ap'],
3535
// Explicit ids — the <name>/plugin.js convention collides on basename.
3636
plugins: [
3737
{ id: 'relay', module: at('relay/plugin.js'), prefix: '/relay' },
@@ -55,6 +55,10 @@ const fastify = createServer({
5555
{ id: 'carddav', module: at('carddav/plugin.js'), prefix: '/carddav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
5656
{ id: 'mastodon', module: at('mastodon/plugin.js'), prefix: '/mastodon', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
5757
{ id: 'bluesky', module: at('bluesky/plugin.js'), prefix: '/bluesky', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
58+
{ id: 'caldav', module: at('caldav/plugin.js'), prefix: '/caldav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
59+
{ id: 'webfinger', module: at('webfinger/plugin.js'), prefix: '/webfinger', config: { podsRoot: PODS, baseUrl: PUBLIC_URL } },
60+
{ id: 'activitypub', module: at('activitypub/plugin.js'), prefix: '/activitypub', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
61+
{ id: 'rss', module: at('rss/plugin.js'), prefix: '/feed', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
5862
],
5963
});
6064

@@ -84,3 +88,7 @@ console.log(` otp: POST ${PUBLIC_URL}/otp/request`);
8488
console.log(` carddav: ${PUBLIC_URL}/carddav/ (contact sync)`);
8589
console.log(` mastodon: GET ${PUBLIC_URL}/api/v1/instance (point a client here)`);
8690
console.log(` bluesky: GET ${PUBLIC_URL}/xrpc/com.atproto.server.describeServer`);
91+
console.log(` caldav: ${PUBLIC_URL}/caldav/ (calendar sync)`);
92+
console.log(` webfinger: GET ${PUBLIC_URL}/.well-known/webfinger?resource=acct:me@host`);
93+
console.log(` activitypub: GET ${PUBLIC_URL}/ap/<user>/actor`);
94+
console.log(` rss/atom: GET ${PUBLIC_URL}/feed/atom?container=<pod-container>`);

0 commit comments

Comments
 (0)