feat(server): appPaths — WAC-exempt application mount points (#582)#585
feat(server): appPaths — WAC-exempt application mount points (#582)#585melvincarvalho wants to merge 3 commits into
Conversation
createServer({ appPaths: ['/myapp'] }) declares URL prefixes owned by
registered applications. Requests at or below an app path skip the
WAC preHandler — the app owns authentication and authorization under
its prefix, exactly the deal the bundled pseudo-plugins (/storage/,
/db, /mcp, ...) already get from the hook's hardcoded exempt list.
This is the one hard blocker for third-party app plugins (#206):
without it, an app registering routes on the returned fastify
instance has its writes swallowed by WAC with no way to opt out.
Proven by plugin zero — Tideholm mounted at /tideholm with pod
WebIDs as player accounts (see #206 discussion).
Entries are validated (leading '/', longer than '/'); malformed
entries are dropped rather than becoming accidental holes. Matching
is per path segment, so /myapp does not exempt /myapplication.
Exempt requests never carry request.webId — identity below an app
prefix is the app's business (#584 tracks the public accessor).
Docs in configuration.md; 7 new tests, full suite green (1002).
There was a problem hiding this comment.
Pull request overview
Adds an appPaths option to createServer to let operators declare application-owned URL prefixes that bypass the global WAC preHandler, enabling “plugin-zero” style apps mounted on the returned Fastify instance to handle auth/authz under their own prefix.
Changes:
- Parse and validate
options.appPathsincreateServer, and exempt matching requests from the WAC hook. - Add an end-to-end test suite covering exemption behavior, segment matching, invalid entries, and default-off behavior.
- Document
appPathsusage and its security/behavioral contract in the configuration docs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/server.js |
Introduces appPaths parsing and plugs it into the WAC preHandler exemption list. |
test/app-paths.test.js |
Adds end-to-end tests verifying app-path exemption and safety properties. |
docs/configuration.md |
Documents how to mount an app prefix and what WAC exemption implies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const appPaths = Array.isArray(options.appPaths) | ||
| ? options.appPaths.filter((p) => typeof p === 'string' && p.startsWith('/') && p.length > 1) | ||
| : []; |
There was a problem hiding this comment.
Good catch — entries are now trimmed and trailing-slash-stripped before validation ('/myapp/' matches like '/myapp', '///' and whitespace-only entries drop out). Regression test added ('trailing-slash entries are normalized, children still exempt').
| const fastify = createServer({ appPaths: ['/myapp'] }); | ||
| fastify.all('/myapp/*', myAppHandler); // the app owns auth below its prefix | ||
| await fastify.listen({ port: 4443 }); |
There was a problem hiding this comment.
Right — same split as /db and /db/*. The example now registers both the bare prefix and the wildcard, with a comment saying why.
Trailing-slash entries ('/myapp/') previously failed to exempt child
routes because the matcher built '/myapp//'. Entries are now trimmed
and stripped of trailing slashes before validation, which also drops
oddities like '///' and ' '. Regression test added (8 total).
Docs example registers both '/myapp' and '/myapp/*' — fastify
wildcards don't match the bare prefix, same reason /db and /db/*
are registered separately.
| Requests at or below an app path skip the WAC authorization hook — the | ||
| application authenticates and authorizes its own traffic, exactly like the | ||
| built-in `/storage/` and `/db/` routes. Everything outside the declared | ||
| prefixes keeps full WAC enforcement. Entries must start with `/` and be | ||
| longer than `/`; anything else is ignored. |
There was a problem hiding this comment.
Added — the section now states that request.webId is never set under an app path (the WAC hook is what populates it) and points app authors at getWebIdFromRequestAsync, with #584 tracking the stable public accessor. It was already pinned by the 'never sets request.webId' test; now the docs say it too.
The WAC hook is also what populates request.webId, so app handlers under an appPaths prefix must resolve identity themselves — point at getWebIdFromRequestAsync and the #584 public-accessor issue.
First release with application mount points (#582, #585): pluggable apps are now possible without editing server.js. createServer({ appPaths: ['/myapp'] }) declares URL prefixes owned by registered applications — requests below an app path skip the WAC hook and the app owns its own auth, like /storage/ and /db/ always have. This is step one of the #206 plugin system, proven by plugin zero: Tideholm (github.com/melvincarvalho/tideholm, see jss-plugin/) mounts a full multiplayer game at /tideholm where pod WebIDs are the player accounts. Remaining seams tracked in #583 (raw-body mode for wrapped node handlers) and #584 (public api.auth.getAgent); the loader itself (manifest, discovery, policy) is #206/#564.
…iptSolidServer#582) (JavaScriptSolidServer#585) * feat(server): appPaths — WAC-exempt application mount points (JavaScriptSolidServer#582) createServer({ appPaths: ['/myapp'] }) declares URL prefixes owned by registered applications. Requests at or below an app path skip the WAC preHandler — the app owns authentication and authorization under its prefix, exactly the deal the bundled pseudo-plugins (/storage/, /db, /mcp, ...) already get from the hook's hardcoded exempt list. This is the one hard blocker for third-party app plugins (JavaScriptSolidServer#206): without it, an app registering routes on the returned fastify instance has its writes swallowed by WAC with no way to opt out. Proven by plugin zero — Tideholm mounted at /tideholm with pod WebIDs as player accounts (see JavaScriptSolidServer#206 discussion). Entries are validated (leading '/', longer than '/'); malformed entries are dropped rather than becoming accidental holes. Matching is per path segment, so /myapp does not exempt /myapplication. Exempt requests never carry request.webId — identity below an app prefix is the app's business (JavaScriptSolidServer#584 tracks the public accessor). Docs in configuration.md; 7 new tests, full suite green (1002). * review: normalize appPaths entries; docs example mounts the bare prefix Trailing-slash entries ('/myapp/') previously failed to exempt child routes because the matcher built '/myapp//'. Entries are now trimmed and stripped of trailing slashes before validation, which also drops oddities like '///' and ' '. Regression test added (8 total). Docs example registers both '/myapp' and '/myapp/*' — fastify wildcards don't match the bare prefix, same reason /db and /db/* are registered separately. * review(docs): appPaths requests never carry request.webId The WAC hook is also what populates request.webId, so app handlers under an appPaths prefix must resolve identity themselves — point at getWebIdFromRequestAsync and the JavaScriptSolidServer#584 public-accessor issue.
Closes #582. The first (and only hard-blocking) seam from the plugin-zero work on #206.
What
createServer({ appPaths: ['/myapp'] })— URL prefixes owned by registered applications. Requests at or below an app path skip the WACpreHandler; the app owns authentication and authorization under its prefix, exactly like the built-in/storage/and/db/escapes.Why
The WAC hook authorizes every URL against pod ACLs and rejects before routes run. Every bundled pseudo-plugin needed an escape and got one by editing the hook's hardcoded list; third-party apps registering routes on the returned fastify instance can't. With this seam, the bitmark-explorer/Tideholm class of app-plugin works with zero further core changes — proven end-to-end by Tideholm mounted at
/tideholmwith pod WebIDs as player accounts (12-check demo + a browser playtest, see the #206 comments).Safety notes
/, longer than/); malformed entries dropped, never accidental holes/myappdoes not exempt/myapplicationrequest.webId— identity below an app prefix is the app's business (Plugin api: bless getWebIdFromRequestAsync as api.auth.getAgent (request → WebID) #584 tracks blessinggetWebIdFromRequestAsyncas the public accessor)Tests
7 new tests in
test/app-paths.test.js(reach-the-app, bare/query forms, nowebIdleakage, sibling LDP still WAC'd, segment matching, malformed entries, default-off). Full suite: 1002 pass.Relationship to the loader (#206 / #564)
Standalone option now; when the loader lands, manifest-declared prefixes feed
appPathsautomatically and this becomes an implementation detail ofapi.fastify. Sibling seams #583 (raw-body mode) and #584 (getAgent) are deliberately not in this PR.