Skip to content

Commit f6e752e

Browse files
docs(features): plugins & application mounts — status and how-to (#29)
New Features page covering the plugin system as of JSS v0.0.213: - status table: appPaths shipped (#582/#585), plugin zero running, raw-body (#583) and getAgent (#584) proposed, loader designed (#206/#564) - how-to: mounting an app with appPaths, the WAC-exemption rules, pod identity as the app's login (getWebIdFromRequestAsync + the browser /idp/credentials bridge), and the scoped raw-body pattern for wrapping node-style handlers - case study: Tideholm as plugin zero (WebID -> player, dual-mode, runnable demo) - roadmap: loader -> bundled-feature migration -> richer seams, with the never-load-from-pods security rule stated Registered in the Features sidebar after MCP. Site builds clean.
1 parent 9787f45 commit f6e752e

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

docs/features/plugins.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
---
2+
sidebar_position: 17
3+
title: Plugins & Application Mounts
4+
description: Extend JSS with whole applications — where the plugin system stands and how to use it today
5+
---
6+
7+
# Plugins & Application Mounts
8+
9+
JSS can host entire applications beside your pod — same origin, same server,
10+
with the pod's identity system as the app's login. This page covers what
11+
works **today** (v0.0.213+), how to build on it, and where the full plugin
12+
system is headed.
13+
14+
:::tip The one-line version
15+
`createServer({ appPaths: ['/myapp'] })` mounts an application under a URL
16+
prefix. The app owns everything below its prefix; the pod keeps everything
17+
else. Your WebID can be the app's account — no separate passwords.
18+
:::
19+
20+
## Status at a glance
21+
22+
| Piece | Status | Where |
23+
|---|---|---|
24+
| Application mount points (`appPaths`) |**Shipped in v0.0.213** | [#582](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/582), [#585](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/585) |
25+
| Reference plugin ("plugin zero") | ✅ Running — [Tideholm](https://github.com/melvincarvalho/tideholm/tree/gh-pages/jss-plugin), a multiplayer game where pod WebIDs are player accounts | [#206 discussion](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206) |
26+
| Raw-body mode for wrapped apps | 📋 Pattern documented below; helper proposed | [#583](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/583) |
27+
| Public identity accessor (`api.auth.getAgent`) | 📋 Works via internal import; public blessing proposed | [#584](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/584) |
28+
| Plugin loader (manifest, discovery, policy) | 🔭 Designed, not yet built | [#206](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206), [#564](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564) |
29+
30+
## Why plugins?
31+
32+
JSS already *is* a plugin system internally: notifications, the IdP,
33+
ActivityPub, the Nostr relay, MCP, remoteStorage, the tunnel — each is an
34+
encapsulated Fastify plugin toggled by a `createServer` flag. The plugin
35+
effort (#206) is about opening that same power to third parties **without
36+
editing server.js**: games, dashboards, CardDAV servers, custom APIs —
37+
anything that wants to live on your pod's origin and speak to your pod's
38+
identity.
39+
40+
The strategy is deliberate: ship the smallest seam, prove it with a real
41+
consumer, bless the API afterward. The first consumer — *plugin zero* — is
42+
[Tideholm](https://github.com/melvincarvalho/tideholm), a zero-dependency
43+
island-strategy game. Mounting it surfaced exactly three missing seams, of
44+
which only one required a core change. That one shipped in v0.0.213.
45+
46+
## What shipped: `appPaths`
47+
48+
JSS authorizes every request against pod ACLs (WAC) in a global hook, and
49+
rejects before routes run. Built-in features escape via a hardcoded list;
50+
until v0.0.213, third-party routes couldn't. Now:
51+
52+
```js
53+
import { createServer } from 'javascript-solid-server/src/server.js';
54+
55+
const fastify = createServer({
56+
root: './data/pods',
57+
idp: true,
58+
idpIssuer: 'https://pod.example',
59+
appPaths: ['/myapp'], // ← the seam
60+
});
61+
62+
// Register both forms — fastify wildcards don't match the bare prefix.
63+
fastify.all('/myapp', myAppHandler);
64+
fastify.all('/myapp/*', myAppHandler);
65+
66+
await fastify.listen({ port: 4443 });
67+
```
68+
69+
Rules of the road:
70+
71+
- Requests at or below an app path **skip the WAC hook** — the app owns
72+
authentication *and* authorization under its prefix, exactly the deal
73+
`/storage/` and `/db/` have always had. Everything else keeps full WAC.
74+
- Matching is per **path segment**: `/myapp` does not exempt
75+
`/myapplication`. Trailing slashes are normalized; malformed entries
76+
(no leading `/`, bare `/`, whitespace) are dropped, never widened.
77+
- `request.webId` is **never set** under an app path — the WAC hook is what
78+
populates it. Resolve identity yourself (next section).
79+
- Default off. No `appPaths`, no change.
80+
81+
## Pod identity as the app's login
82+
83+
The best part: your app doesn't need accounts. JSS's token verification
84+
already handles every supported scheme uniformly — IdP Bearer tokens,
85+
Solid-OIDC DPoP, Nostr NIP-98 signatures, LWS10-CID:
86+
87+
```js
88+
import { getWebIdFromRequestAsync } from 'javascript-solid-server/src/auth/token.js';
89+
90+
async function myAppHandler(request, reply) {
91+
const { webId } = await getWebIdFromRequestAsync(request);
92+
if (!webId) return reply.code(401).send({ error: 'sign in with your pod' });
93+
// webId is a verified identity — key your app's users on it
94+
}
95+
```
96+
97+
:::caution
98+
`getWebIdFromRequestAsync` is currently an internal import — it works, but
99+
its path isn't a stable contract yet.
100+
[#584](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/584)
101+
tracks blessing it as public API (`api.auth.getAgent`).
102+
:::
103+
104+
**Browser users** don't send `Authorization` headers by themselves. The
105+
pattern proven in Tideholm: the app's login screen POSTs pod credentials to
106+
JSS's documented [`/idp/credentials`](/docs/features/authentication)
107+
endpoint, stores the returned Bearer token, and attaches it to the app's
108+
API calls. Tokens expire after 3600s — handle the 401 by returning to the
109+
login screen (or watch #584 for improvements here).
110+
111+
## Wrapping an existing app (raw bodies)
112+
113+
If your "plugin" is an existing node-style HTTP app (`(req, res)` handler),
114+
two gotchas — both solved with one pattern:
115+
116+
1. Fastify's content parsers **consume request bodies** before handlers
117+
run, so your wrapped app hangs waiting for a stream that's been drained.
118+
2. Fastify wants to own the response unless you tell it otherwise.
119+
120+
```js
121+
await fastify.register(async (scope) => {
122+
// Pass bodies through untouched, scoped so the host is unaffected.
123+
scope.removeAllContentTypeParsers();
124+
scope.addContentTypeParser('*', (req, payload, done) => done(null, payload));
125+
126+
const handler = async (request, reply) => {
127+
const { webId } = await getWebIdFromRequestAsync(request);
128+
request.raw.myAppWebId = webId; // hand identity to the raw handler
129+
reply.hijack(); // fastify lets go of the response
130+
myNodeApp.handle(request.raw, reply.raw);
131+
};
132+
scope.all('/myapp', handler);
133+
scope.all('/myapp/*', handler);
134+
});
135+
```
136+
137+
[#583](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/583)
138+
proposes packaging this as `api.mountApp(prefix, nodeHandler)` so nobody
139+
rediscovers it the hard way.
140+
141+
## Case study: plugin zero
142+
143+
[Tideholm](https://github.com/melvincarvalho/tideholm) is the working
144+
reference for everything above — a multiplayer browser strategy game whose
145+
core is zero-dependency and transport-agnostic, with a ~100-line adapter
146+
([`jss-plugin/`](https://github.com/melvincarvalho/tideholm/tree/gh-pages/jss-plugin))
147+
that mounts it at `/tideholm`:
148+
149+
- **WebID → player**: first authenticated request auto-provisions a game
150+
account keyed to the pod identity. Two pods, two players. No passwords.
151+
- **Same origin, both worlds**: `/alice/profile/card.jsonld` (LDP + WAC)
152+
and `/tideholm/api/state` (game auth) serve side by side.
153+
- **Dual mode**: the same game runs standalone (`node server.js`, its own
154+
password accounts) or mounted (pod identity), switching via a tiny
155+
`GET /api/meta` the client reads at boot.
156+
- **Try it**: `JSS_PATH=... node jss-plugin/demo.js` runs a 12-check
157+
end-to-end proof; `jss-plugin/serve.js` is a persistent composed server.
158+
159+
The adapter also ships `jss.plugin.json` and an `activate(api)` entry —
160+
dead code today, but shaped exactly like the coming loader contract, so it
161+
becomes a drop-in plugin the day the loader lands.
162+
163+
## The road ahead
164+
165+
From [#206](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206)
166+
/ [#564](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564),
167+
in order:
168+
169+
1. **Loader**`jss.plugin.json` manifests (`{ id, version, entry }`),
170+
`activate(api)` entry points, allow/deny policy, per-plugin config.
171+
Plugins load **only from operator config paths — never from pod
172+
storage** (pods are user-writable; a loader that reads them is RCE).
173+
2. **Migrate the bundled features** onto the loader (#564) — eight
174+
battle-tested consumers from day one.
175+
3. **Richer seams as consumers demand them**`api.auth.getAgent` (#584),
176+
`api.mountApp` (#583), `registerMcpTool`, `registerPane`, with the
177+
[pane store](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/184)
178+
as the eventual marketplace layer.
179+
180+
The pattern for contributing a seam is established: build a real thing
181+
against JSS, hit a wall, file the smallest issue that removes it, prove it
182+
with your consumer. Plugin zero took the `appPaths` route from idea to npm
183+
in a day — the door is open.

sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const sidebars: SidebarsConfig = {
4444
'features/git-integration',
4545
'features/app-install',
4646
'features/mcp',
47+
'features/plugins',
4748
'features/charlie',
4849
'features/activitypub',
4950
'features/nostr',

0 commit comments

Comments
 (0)