Skip to content

Commit 61cf85f

Browse files
Integrate the six new plugins: compose all twelve, rank seams by demand
- compose.test.js + serve.js load all 12 plugins on one server from pure config; 13-check composition (each surface probed), 103 tests total. - README: two tables (bundled-feature ports + new-feature plugins from the issue backlog), 97→103 green. - NOTES: candidate seams re-ranked by how many independent plugins demanded each — api.authorize now #1 (3 consumers: notifications, corsproxy, capability), api.events #2 (notifications + sparql, the latter stronger: a missed write = wrong results). Added the unconsumed-body-stream primitive (#583, gitscratch), reserved-path declaration (nip05, gitscratch), and the DATA_ROOT / ~/.gitconfig test-harness footguns every multi-boot suite rediscovered.
1 parent 87d3c1c commit 61cf85f

4 files changed

Lines changed: 138 additions & 23 deletions

File tree

NOTES.md

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,72 @@ are candidates, each with a consumer in this repo attached.
2424
correct. This removes a whole class of would-be seams (`api.wac.check`)
2525
from the *necessary* list, leaving them merely *nice*.
2626

27-
## Candidate seams (in value order, consumers attached)
28-
29-
1. **`api.events.onResourceChange(cb)`** — consumer: notifications/.
30-
Core has the emitter internally (`src/notifications/events.js`); today a
31-
plugin must fs.watch a config-supplied path, which drifts and misses
32-
non-fs backends. This is also the seam any future "react to pod writes"
33-
app (webhooks, indexing, sync) wants — likely the most demanded seam of
34-
the next wave of real apps.
35-
2. **`api.serverInfo` (`{ baseUrl, port }` resolved at listen)** — consumers:
36-
notifications/ (pub URLs, origin checks, loopback), any plugin minting
37-
absolute URLs. Today the operator repeats the origin in every plugin's
38-
config.
39-
3. **Internal utility modules plugins re-vendor** — consumers: relay/
40-
(`src/nostr/event.js` NIP-01 verify) and potentially pay/ (`src/mrc20.js`).
41-
Both are pure, dependency-light crypto. Candidate: export like auth.js
42-
(`javascript-solid-server/nostr.js`), or bless vendoring as the answer.
43-
4. **Response-header injection on core routes** — consumer: notifications/
44-
(`Updates-Via` discovery). Explicitly NOT proposing a default-on hook:
45-
a plugin rewriting every response is a bigger grant than route ownership.
46-
If it ships, gate it (`capabilities: ['hooks']`).
27+
## Candidate seams (ranked by how many independent plugins demanded them)
28+
29+
Twelve plugins in, the ranking is now empirical — a seam's rank is how many
30+
ports reached for it without coordinating.
31+
32+
1. **`api.authorize(request, path, mode)`** — "would the host's WAC allow
33+
this?" **Three independent consumers: notifications/, corsproxy/,
34+
capability/.** The loopback trick (below) covers the case where the
35+
*requester's own* credentials should decide (notifications, webdav,
36+
sparql all use it), but it can't cover authorization the requester
37+
doesn't drive: a proxy governed by a *pod owner's* `.acl` (corsproxy
38+
#382), or a capability exercising the *issuer's* authority
39+
(capability #506). This is the most-requested seam and the one that
40+
moves the most backlog issues from "plugin-approximation" to "faithful".
41+
2. **`api.events.onResourceChange(cb)`****two consumers: notifications/,
42+
sparql/**, and sparql is the *stronger* one: without a write hook a
43+
plugin index returns **wrong** query results, not merely late
44+
notifications, and `pluginDir` caching is uninvalidatable. Core already
45+
has the emitter internally (`src/notifications/events.js`). The seam
46+
every "react to pod writes" app (webhooks, indexing, sync, full-text)
47+
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.
53+
4. **The unconsumed-body-**stream** primitive (#583)** — consumer:
54+
gitscratch/ sharpened it. tunnel/ needed the raw *buffer*; git needs the
55+
raw *stream* piped to a subprocess gzip-and-all. Whatever `api.mountApp`
56+
/ raw-body mode ships must hand back the un-drained stream, not just a
57+
buffered body. (This is exactly what the merged loader's scoped
58+
pass-through parser does — the finding is to keep it that way.)
59+
5. **Reserved-path declaration** — consumers: nip05/ (`/.well-known/
60+
nostr.json`), gitscratch/ (coexistence with core `git: true`). A plugin
61+
can register absolute/exact paths outside its prefix, but only WAC-exempt
62+
*by luck* (core blanket-exempts `/.well-known/*`) and with no conflict
63+
detection — a future core route at the same path throws
64+
`FST_ERR_DUPLICATED_ROUTE` at boot. Let entries declare reserved paths so
65+
the loader claims them deliberately and reports collisions.
66+
6. **Can't set fastify server options** — consumer: capability/ hit
67+
`maxParamLength` (100) silently 404ing long tokens in named params;
68+
workaround is a wildcard route. A plugin has no way to raise per-route
69+
limits. Minor, but sharp when it bites.
70+
7. **Internal utility modules plugins re-vendor** — consumer: relay/
71+
(`src/nostr/event.js` NIP-01 verify), pay/ (`src/mrc20.js`). Pure crypto.
72+
Export like auth.js (`javascript-solid-server/nostr.js`) or bless
73+
vendoring.
74+
8. **Response-header injection on core routes** — consumer: notifications/
75+
(`Updates-Via` discovery). A plugin can't add headers to routes it
76+
doesn't own. NOT a default-on hook (bigger grant than route ownership);
77+
gate behind `capabilities: ['hooks']` if ever.
78+
79+
## Test-harness footguns (host quirks, not plugin api)
80+
81+
Every multi-boot suite independently rediscovered these; worth a line in
82+
the plugin-author docs.
83+
84+
- **Module-global `DATA_ROOT`**: JSS keeps the storage root (and IdP key
85+
resolution) in a process-global env var that *every* `createServer`
86+
repoints — a second boot in one process, **even a deliberately-failing
87+
one**, poisons the first. Order validation-failure tests *before* the
88+
long-lived boot. (notifications/, webdav/, sparql/ all hit this.)
89+
- **Ambient `~/.gitconfig`**: git-shelling plugins inherit the operator's
90+
config — `init.defaultBranch = gh-pages` leaked dangling HEADs into
91+
server-created bare repos (empty clones). Spawn git with
92+
`GIT_CONFIG_NOSYSTEM=1` and no `HOME`. (gitscratch/.)
4793

4894
## The core/plugin line — answering #564 empirically
4995

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ README and NOTES.md, ship the closest honest approximation.
4040

4141
## What's here
4242

43+
**Ports of bundled features** (does the public api reproduce core?):
44+
4345
| Plugin | Ports | Status | Notable |
4446
|---|---|---|---|
4547
| `relay/` | `src/nostr/relay.js` | ✅ parity + | pluginDir persistence core lacks; vendored NIP-01 verify |
@@ -49,8 +51,20 @@ README and NOTES.md, ship the closest honest approximation.
4951
| `notifications/` | `src/notifications/` | ✅ parity | **the seam-forcer** — WAC via loopback; forces `api.events`, `api.serverInfo` |
5052
| `pay/` | pay mode | 📋 wall-report | pipeline-modifying → **stays core**; draws the #564 line |
5153

52-
39 tests, all green (`npm test`), including `compose.test.js` — every plugin
53-
on one server from pure config. Findings consolidated in [NOTES.md](./NOTES.md).
54+
**New features built straight onto the api** (from the `plugin`-tagged issue backlog):
55+
56+
| Plugin | Issue | Notable |
57+
|---|---|---|
58+
| `nip05/` | #445 | `/.well-known/nostr.json` from pods' public keys; well-known-path ownership is *accidental* today |
59+
| `corsproxy/` | #382/#379 | forward proxy with fail-closed SSRF defense; per-pod ACL needs `api.authorize` |
60+
| `capability/` | #506 | macaroon-lite scoped/time-bound/revocable capability URLs |
61+
| `webdav/` | #507 | mount a pod in Finder/Nautilus/Windows; WebDAV↔LDP over loopback, Basic→Bearer |
62+
| `sparql/` | #509 | read-time SPARQL SELECT over pod JSON-LD; write-index needs `api.events` |
63+
| `gitscratch/` | #322 | ephemeral Solid-authed git remotes via the `git-http-backend` CGI |
64+
65+
**97 tests, all green** (`npm test`), including `compose.test.js` — all twelve
66+
plugins on one server from pure config, pods + WAC intact beside them.
67+
Findings consolidated in [NOTES.md](./NOTES.md).
5468

5569
```
5670
<name>/plugin.js the port <name>/test.js real-JSS tests <name>/README.md findings

compose.test.js

Lines changed: 44 additions & 1 deletion
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 + six plugins from config', async () => {
49+
it('boots pods + idp + twelve 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}`;
@@ -70,6 +70,12 @@ describe('composition: every plugin on one server', () => {
7070
config: { podsRoot: root, baseUrl: base },
7171
},
7272
{ id: 'pay', module: at('pay/plugin.js'), prefix: '/paid', config: { cost: 2, address: 'x' } },
73+
{ id: 'nip05', module: at('nip05/plugin.js'), prefix: '/nip05', config: { podsRoot: root } },
74+
{ id: 'corsproxy', module: at('corsproxy/plugin.js'), prefix: '/proxy', config: {} },
75+
{ id: 'capability', module: at('capability/plugin.js'), prefix: '/cap', config: {} },
76+
{ id: 'webdav', module: at('webdav/plugin.js'), prefix: '/webdav', config: { baseUrl: base, loopbackUrl: base } },
77+
{ id: 'gitscratch', module: at('gitscratch/plugin.js'), prefix: '/git', config: {} },
78+
{ id: 'sparql', module: at('sparql/plugin.js'), prefix: '/sparql', config: { baseUrl: base, loopbackUrl: base } },
7379
],
7480
});
7581
assert.ok(jss.base);
@@ -149,6 +155,43 @@ describe('composition: every plugin on one server', () => {
149155
assert.strictEqual(res.status, 200);
150156
});
151157

158+
it('nip05: serves the discovery document', async () => {
159+
const res = await fetch(`${base}/nip05/nostr.json`);
160+
assert.strictEqual(res.status, 200);
161+
assert.ok('names' in (await res.json()));
162+
});
163+
164+
it('corsproxy: refuses a missing/blocked target but is alive', async () => {
165+
const res = await fetch(`${base}/proxy`); // no ?url
166+
assert.strictEqual(res.status, 400);
167+
});
168+
169+
it('capability: minting requires identity (401 anon)', async () => {
170+
const res = await fetch(`${base}/cap/issue`, {
171+
method: 'POST', headers: { 'content-type': 'application/json' },
172+
body: JSON.stringify({ resource: '/cap/x', modes: ['read'], ttl: 60 }),
173+
});
174+
assert.strictEqual(res.status, 401);
175+
});
176+
177+
it('webdav: OPTIONS advertises DAV class 1', async () => {
178+
const res = await fetch(`${base}/webdav/`, { method: 'OPTIONS' });
179+
assert.ok(res.status < 500);
180+
assert.match(res.headers.get('dav') || '', /1/);
181+
});
182+
183+
it('gitscratch: an anonymous push is refused', async () => {
184+
const res = await fetch(`${base}/git/probe.git/info/refs?service=git-receive-pack`);
185+
assert.strictEqual(res.status, 401);
186+
});
187+
188+
it('sparql: rejects a non-sparql content type (415)', async () => {
189+
const res = await fetch(`${base}/sparql`, {
190+
method: 'POST', headers: { 'content-type': 'text/plain' }, body: 'nope',
191+
});
192+
assert.ok([400, 415].includes(res.status), `got ${res.status}`);
193+
});
194+
152195
it('pods still work beside all of it (idp register + WAC)', async () => {
153196
let res = await fetch(`${base}/idp/register`, {
154197
method: 'POST',

serve.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ const fastify = createServer({
4141
config: { podsRoot: PODS, baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` },
4242
},
4343
{ id: 'pay', module: at('pay/plugin.js'), prefix: '/paid', config: { cost: 1, address: 'demo' } },
44+
{ id: 'nip05', module: at('nip05/plugin.js'), prefix: '/nip05', config: { podsRoot: PODS, relayUrl: `${PUBLIC_URL.replace(/^http/, 'ws')}/relay` } },
45+
{ id: 'corsproxy', module: at('corsproxy/plugin.js'), prefix: '/proxy', config: {} },
46+
{ id: 'capability', module: at('capability/plugin.js'), prefix: '/cap', config: {} },
47+
{ id: 'webdav', module: at('webdav/plugin.js'), prefix: '/webdav', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
48+
{ id: 'gitscratch', module: at('gitscratch/plugin.js'), prefix: '/git', config: {} },
49+
{ id: 'sparql', module: at('sparql/plugin.js'), prefix: '/sparql', config: { baseUrl: PUBLIC_URL, loopbackUrl: `http://127.0.0.1:${PORT}` } },
4450
],
4551
});
4652

@@ -60,3 +66,9 @@ console.log(` terminal: ws ${PUBLIC_URL}/terminal?token=${terminalToken}
6066
console.log(` tunnel: ws ${PUBLIC_URL}/tunnel`);
6167
console.log(` notifications: ws ${PUBLIC_URL}/.notifications`);
6268
console.log(` paid demo: GET ${PUBLIC_URL}/paid/demo`);
69+
console.log(` nip05: GET ${PUBLIC_URL}/.well-known/nostr.json`);
70+
console.log(` cors-proxy: GET ${PUBLIC_URL}/proxy?url=<url>`);
71+
console.log(` capability: POST ${PUBLIC_URL}/cap/issue (auth)`);
72+
console.log(` webdav: ${PUBLIC_URL}/webdav/ (mount with a pod Bearer)`);
73+
console.log(` git scratch: git clone ${PUBLIC_URL}/git/<name>.git`);
74+
console.log(` sparql: POST ${PUBLIC_URL}/sparql (auth)`);

0 commit comments

Comments
 (0)