Skip to content

Commit fc64b4e

Browse files
Integrate wave 11 (admin): 33 plugins, 374 tests — the capstone
compose.test.js boots all 33; serve.js's front door is now /admin/, fed by ONE shared INVENTORY array beside the real plugins list — the hand-copied-registry workaround living in the demo itself (#463/#464). ADMIN_AGENTS env gates the operator home; without it the plugin warns loudly that the surface is open. Docs folded: NOTES gains seams 11 (api.isOperator — terminal, metrics and admin hold three incompatible answers to 'who is the operator?') and 12 (the admin-surface gaps: runtime lifecycle, install, settings panels, log tailing, storage introspection); api.plugins is now a two-consumer seam. REPORT.md gains 'The wp-admin test' — the capstone gap table: the read side of a WordPress-style admin works today but only by the operator re-telling the plugin what the host already knows three ways; the write side's absences each name their seam.
1 parent 5fec215 commit fc64b4e

8 files changed

Lines changed: 166 additions & 52 deletions

File tree

AGENT.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Out-of-tree plugins for [JavaScript Solid
1111
Server](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer),
1212
built on its **#206 loader** (`createServer({ plugins })`, JSS ≥ 0.0.215).
1313
It is an *experiment*: prove the plugin api by using it, and treat every
14-
wall you hit as a finding, not a blocker. **32 plugins, 363 tests today.**
14+
wall you hit as a finding, not a blocker. **33 plugins, 374 tests today.**
1515

1616
### The one rule that makes the experiment valid
1717

@@ -157,6 +157,7 @@ Full ranking in `NOTES.md`. The ones you'll hit:
157157
| a stateless request/response protocol (mail, sync, …) | `jmap/` | loopback CRUD + in-protocol refusal of push/delta |
158158
| a storage protocol with conditional writes | `remotestorage/` | If-Match/If-None-Match pass through loopback intact |
159159
| a redirect / metadata micro-service | `shortlink/` | pluginDir JSON table + wildcard slug routes |
160+
| an operator/admin surface | `admin/` | compose probes + podsRoot scan behind an agent allowlist |
160161
161162
## Footguns (every multi-boot suite rediscovered these)
162163
@@ -178,7 +179,7 @@ Full ranking in `NOTES.md`. The ones you'll hit:
178179
179180
## Current state
180181
181-
32 plugins (7 ports + 25 features), 363 tests, all green (`npm test`).
182+
33 plugins (7 ports + 26 features), 374 tests, all green (`npm test`).
182183
`compose.test.js` runs every one on a single server from pure config. Two
183184
core PRs (#590 `api.mountApp`, #591 `/idp/refresh`) sit upstream, unmerged,
184185
for the maintainer's call. Everything else lives here, by design.

ISSUES.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ evidence. Legend:
4343
|| `jmap/` | JMAP mail over the pod (push/delta → 🔩 `api.events`; blobs → 🔩 #583) |
4444
| #163 | `remotestorage/` | remoteStorage server — 7th port; witnessed the webfinger collision |
4545
|| `shortlink/` | link shortener for pod URLs (pluginDir persistence, 11th witness) |
46+
| #463/#464-adj | `admin/` | operator home — the wp-admin gap analysis; every missing pillar names a seam |
4647

4748
Plus seven **ports of bundled features** proving the migration path for
4849
#564 / #164: `relay/` `webrtc/` `terminal/` `tunnel/` `notifications/`
@@ -106,11 +107,11 @@ exists.
106107

107108
Of ~40 plugin-tagged issues: **14 built as plugins here** (plus `rss/`,
108109
`matrix/`, `search/`, `didweb/`, `s3/`, `micropub/`, `backup/`, `metrics/`,
109-
`dashboard/`, `oembed/`, `jmap/`, `shortlink/` — capability
110+
`dashboard/`, `oembed/`, `jmap/`, `shortlink/`, `admin/` — capability
110111
demonstrations with no single issue), **7 ported**, **5 shipped upstream**,
111112
**2 more plugin-able with no blocker**, **6 clusters blocked on a named
112113
seam** (each with a proof-of-need consumer), the rest core-by-nature or
113-
product-scale. **32 plugins total, 363 tests.** The plugin api reaches most
114+
product-scale. **33 plugins total, 374 tests.** The plugin api reaches most
114115
of the backlog today; ranked by demand, the seams that would unlock the
115116
most next are `api.authorize` (3 blocking consumers), `api.events` (7
116117
consumers — matrix `/sync` needs live push, jmap can't do push or delta

NEXT.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ plugin), `NOTES.md` (the findings/seams — the real deliverable), and
88

99
## Where things stand
1010

11-
- **32 plugins, 363 tests, all green** (`npm test`), all pushed to
11+
- **33 plugins, 374 tests, all green** (`npm test`), all pushed to
1212
`github.com/JavaScriptSolidServer/plugins` (branch `gh-pages`).
13-
- `compose.test.js` boots all 32 on **one** JSS from pure config; `serve.js`
14-
is the runnable demo. Both must be updated when you add a plugin.
13+
- `compose.test.js` boots all 33 on **one** JSS from pure config; `serve.js`
14+
is the runnable demo — its front door is now `admin/` (`/admin/`), the
15+
capstone wp-admin-style operator home fed by a shared `INVENTORY` array.
16+
All three must be updated when you add a plugin.
1517
- Built so far: 7 ports (relay, webrtc, terminal, tunnel, notifications,
16-
remotestorage, pay) + 25 features (nip05, corsproxy, capability, webdav,
18+
remotestorage, pay) + 26 features (nip05, corsproxy, capability, webdav,
1719
sparql — now with UPDATE, gitscratch, otp, carddav, mastodon, bluesky,
1820
caldav — now with free-busy, webfinger,
1921
activitypub, rss, matrix, search, didweb, s3, micropub, backup,
20-
metrics, dashboard, oembed, jmap, shortlink).
22+
metrics, dashboard, oembed, jmap, shortlink, admin).
2123
- Capability classes covered: realtime, WebDAV family, fediverse/social/chat
2224
(5 shims), IndieWeb publishing, identity, data/query/search, object
2325
storage, proxy, dev, pay, data portability, ops/observability, mail,

NOTES.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,33 @@ ports reached for it without coordinating.
179179
the safer default, but it would break the one legitimate consumer found
180180
(metrics/' cross-plugin request counters) — a scoped-vs-shared choice
181181
the loader should make deliberately, not inherit from `register`.
182-
10. **`api.plugins` (the #463/#464 app-registry) — first live consumer:
183-
dashboard/.** The one plugin whose job is describing the deployment
184-
cannot enumerate its co-loaded siblings; the operator hands it a
185-
hand-copied duplicate of the `createServer` plugins list, and the two
186-
silently drift (an added plugin never appears; a removed one keeps
187-
probing). The loader already holds exactly the needed data:
182+
10. **`api.plugins` (the #463/#464 app-registry) — two consumers:
183+
dashboard/ and admin/.** The plugins whose job is describing the
184+
deployment cannot enumerate their co-loaded siblings; the operator
185+
hands each a
186+
hand-copied duplicate of the `createServer` plugins list, and the
187+
copies silently drift (an added plugin never appears; a removed one
188+
keeps probing). serve.js now maintains ONE shared `INVENTORY` array
189+
beside the real plugins array — the workaround that proves the
190+
seam. The loader already holds exactly the needed data:
188191
`api.plugins → [{ id, prefix, module }]`, read-only, plus optional
189192
probe/health hints per entry.
193+
11. **No operator concept — `api.isOperator` (admin/).** Three plugins
194+
now hold three incompatible answers to "who is the operator?":
195+
terminal/ (shared token), metrics/ (optional bearer token), admin/
196+
(`adminAgents` WebID allowlist). The host itself knows nothing of
197+
operators, so every ops-facing plugin invents its own gate. A tiny
198+
seam (`api.isOperator(agentId)` or an `operators: []` server option)
199+
would unify them.
200+
12. **The admin-surface gaps, named (admin/).** The wp-admin pillars the
201+
api cannot express, each measured by building the operator home:
202+
runtime **enable/disable** (`deactivate()` exists but nothing calls
203+
it after boot), **install** (#200 marketplace + post-boot loading),
204+
**settings panels** (no contribute-a-panel affordance — the
205+
`adminPage` link convention is the workaround), **log viewing**
206+
(`api.log` is write-only), and **storage introspection** (pods/disk
207+
stats need an operator-repeated `podsRoot` path — a filesystem
208+
cousin of `api.serverInfo`). Full gap table in admin/README.md.
190209

191210
## Test-harness footguns (host quirks, not plugin api)
192211

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ README and NOTES.md, ship the closest honest approximation.
9090
| `oembed/` || oEmbed provider for pod resources (link unfurling; XSS-refusing) |
9191
| `jmap/` || JMAP mail over pod messages — the stateless/push line, sharpened |
9292
| `shortlink/` || link shortener for pod URLs (deliberately not an open redirector) |
93+
| `admin/` || **the capstone**: wp-admin-style operator home; each missing pillar names its seam |
9394

94-
**363 tests, all green** (`npm test`), including `compose.test.js` — all
95-
thirty-two plugins on one server from pure config, pods + WAC intact
95+
**374 tests, all green** (`npm test`), including `compose.test.js` — all
96+
thirty-three plugins on one server from pure config, pods + WAC intact
9697
beside them. Findings consolidated in [NOTES.md](./NOTES.md); the full
9798
plugin-tagged backlog triaged in [ISSUES.md](./ISSUES.md); the build guide
9899
is [AGENT.md](./AGENT.md); the maintainer-facing summary — what to add

REPORT.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# The plugin api, 32 plugins later — a report for the maintainer
1+
# The plugin api, 33 plugins later — a report for the maintainer
22

33
This document is the actionable summary of the whole experiment: what the
44
#206 plugin api can already do, what it can't, and — ranked with evidence —
@@ -14,7 +14,7 @@ drafted so it could be filed nearly verbatim if wanted.
1414
## Executive summary
1515

1616
- The api as shipped in 0.0.215 (`createServer({ plugins })` + `prefix` +
17-
`getAgent` + `pluginDir` + `ws.route`) is **sufficient for 32 real
17+
`getAgent` + `pluginDir` + `ws.route`) is **sufficient for 33 real
1818
plugins across fifteen capability classes** — realtime, DAV,
1919
fediverse/chat shims, IndieWeb publishing, identity, query/search,
2020
object storage, proxy, dev tooling, pay, data portability,
@@ -187,11 +187,17 @@ probe-port-then-boot dance for the same reason.
187187
property to preserve, not new work.
188188
- **`api.mcp.registerTool`** — blocks the four MCP-tool issues
189189
(#495/#496/#500/#501); no consumer here because it's impossible today.
190-
- **`api.plugins` (the #463/#464 app-registry)** — first live consumer:
191-
`dashboard/`, the plugin whose whole job is describing the deployment,
192-
must be handed a hand-copied duplicate of the `createServer` plugins
193-
list (drift is silent). The loader already holds the needed data:
190+
- **`api.plugins` (the #463/#464 app-registry)** — two consumers:
191+
`dashboard/` and `admin/`, the plugins whose whole job is describing
192+
the deployment, must each be handed a hand-copied duplicate of the
193+
`createServer` plugins list (drift is silent; `serve.js` now maintains
194+
a shared `INVENTORY` array beside the real list — the workaround that
195+
proves the seam). The loader already holds the needed data:
194196
`api.plugins → [{ id, prefix, module }]`, read-only.
197+
- **`api.isOperator` (an operator concept)** — terminal/, metrics/ and
198+
admin/ hold three incompatible answers to "who is the operator?"
199+
(shared token, optional bearer, WebID allowlist). A tiny seam — an
200+
`operators: []` server option surfaced to plugins — would unify them.
195201
- **Export pure utility modules** the way `auth.js` is blessed —
196202
`relay/` re-vendors NIP-01 verify, `pay/` re-vendors mrc20. A
197203
`javascript-solid-server/nostr.js` export (or explicit vendoring
@@ -268,6 +274,30 @@ hooks are ever capability-gated (`capabilities: ['hooks']` /
268274
`['observe']`), the shared scope could become the *granted* behavior and
269275
isolation the default.
270276

277+
## The wp-admin test (the capstone)
278+
279+
`admin/` asks the question every plugin platform eventually answers: can
280+
the ecosystem build its own WordPress-style admin? The read side — server
281+
health, live plugin inventory with probes, pod/user and storage stats,
282+
an agent-allowlist gate — works today, **but only because the operator
283+
re-tells the plugin what the host already knows, three different ways**
284+
(origin, plugins list, data root). The write side is where the api's
285+
edges are exact:
286+
287+
| wp-admin pillar | Today | The seam |
288+
|---|---|---|
289+
| Plugin inventory | hand-copied config, silent drift | `api.plugins` (#463/#464) |
290+
| Install from a store | impossible (boot-time config) | #200 + post-boot loading |
291+
| Enable / disable | impossible — `deactivate()` exists, nothing calls it at runtime | runtime lifecycle on the loader |
292+
| Per-plugin settings panels | `adminPage` link convention | a contribute-a-panel affordance |
293+
| Health | ✅ live probes | registry health hints would refine it |
294+
| Users / storage | ✅ via operator-repeated `podsRoot` | storage introspection (cousin of `serverInfo`) |
295+
| Logs | impossible — `api.log` is write-only | a log-tailing seam |
296+
| Who is admin? | every plugin invents a gate | `api.isOperator` / `operators: []` |
297+
298+
None of these are asks to build an admin panel into core — the plugin
299+
proves the *shell* can live out-of-tree; the seams are the data feeds.
300+
271301
## The core/plugin line (#564, answered empirically)
272302

273303
**A feature is plugin-able iff it OWNS its routes; it stays core iff it

compose.test.js

Lines changed: 30 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 + thirty-two plugins from config', async () => {
49+
it('boots pods + idp + thirty-three 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}`;
@@ -108,6 +108,22 @@ describe('composition: every plugin on one server', () => {
108108
config: { baseUrl: base, loopbackUrl: base, claimWellKnown: false },
109109
},
110110
{ id: 'metrics', module: at('metrics/plugin.js'), prefix: '/metrics', config: { loopbackUrl: base } },
111+
{
112+
id: 'admin',
113+
module: at('admin/plugin.js'),
114+
prefix: '/admin',
115+
// No adminAgents in the composition → open mode (the plugin warns
116+
// loudly at activate; the finding: no operator concept in the api).
117+
config: {
118+
loopbackUrl: base,
119+
baseUrl: base,
120+
podsRoot: root,
121+
plugins: [
122+
{ id: 'nip05', prefix: '/nip05', probe: '/nip05/nostr.json', expect: [200] },
123+
{ id: 'metrics', prefix: '/metrics', probe: '/metrics/healthz', expect: [200] },
124+
],
125+
},
126+
},
111127
{
112128
id: 'dashboard',
113129
module: at('dashboard/plugin.js'),
@@ -349,6 +365,19 @@ describe('composition: every plugin on one server', () => {
349365
assert.strictEqual(res.status, 400); // missing ?resource — endpoint alive
350366
});
351367

368+
it('admin: the operator home renders with plugin inventory and pods stats', async () => {
369+
let res = await fetch(`${base}/admin/`);
370+
assert.strictEqual(res.status, 200);
371+
const page = await res.text();
372+
assert.ok(page.includes('nip05'));
373+
assert.ok(page.includes('metrics'));
374+
res = await fetch(`${base}/admin/status.json`);
375+
assert.strictEqual(res.status, 200);
376+
const status = await res.json();
377+
assert.strictEqual(status.server.alive, true);
378+
assert.ok(status.plugins.every((p) => p.state === 'up'), JSON.stringify(status.plugins));
379+
});
380+
352381
it('metrics: healthz is ok and the exposition names the process gauges', async () => {
353382
let res = await fetch(`${base}/metrics/healthz`);
354383
assert.strictEqual(res.status, 200);

0 commit comments

Comments
 (0)