Serve the backend under a base path: one URL per environment, the backend under /api
The test deployment keeps one base URL per environment, as the sibling ActivityPods apps do, so a pull-request preview lives on a single host: the app at the root, the backend under /api on the same host, its /.well-known routes at the origin root, the proxy passing paths through unchanged. SemApps already works that way — it mounts every route it adds under the base URL's path and keeps /.well-known at the root — but Festipod's own pieces were written for a backend at the root of its own origin: its routes (/app, /as/actor/app, /catalog) were mounted at fixed paths, the public-surface allow-list matched fixed paths (so under /api every request would have been refused), and the Solid-OIDC client_id joined the raw request path to the base URL, which would have repeated the base path (/api/api/...). base-path.js derives the base path from SEMAPPS_HOME_URL; the routes are mounted under it; the allow-list matches paths relative to it, the two /.well-known endpoints against the full path; the client_id is the requested URI rebuilt from the base URL as written, equal byte for byte to the actor URI and to what the front end derives. At the root every gate decision is identical to before (compared over ~8M method/path cases). The rights matrix probes /.well-known at the backend's origin root. Verified: bun test (424 pass); the full gate at the root; and the built images behind a local proxy routing /api and /.well-known unstripped with the Host header kept — login flow, rights matrix 98/98, revocation.
This commit is contained in:
@@ -25,6 +25,10 @@
|
||||
// POST /.webhooks/<id> the pods' notifications to the app's watcher (see emitted-activity.js)
|
||||
// HEAD follows GET; a CORS preflight (OPTIONS) is judged on the method it announces.
|
||||
//
|
||||
// WHERE. The paths above are relative to the backend's base path (`/` locally, `/api/` on Coolify —
|
||||
// see base-path.js), except the two `/.well-known` ones, which SemApps mounts at the origin root
|
||||
// whatever the base path. A path outside the base path is refused like any other.
|
||||
//
|
||||
// Everything else is refused, notably: `/key` (private keys), `/` and every other container listing,
|
||||
// `/interop/*-registration|*-grant`, `/sparql` (also unmounted, `core.service.js`), `/proxy` (the
|
||||
// signing proxy — the app uses it internally only), `/.well-known/webfinger` (unused), the app
|
||||
@@ -46,21 +50,29 @@ const PUBLIC_ENDPOINTS = [
|
||||
{ methods: READ, path: new RegExp(`^/public-key/${ID}$`) },
|
||||
{ methods: READ, path: new RegExp(`^/interop/access-need-group/${ID}$`) },
|
||||
{ methods: READ, path: new RegExp(`^/interop/access-need/${ID}$`) },
|
||||
{ methods: READ, path: /^\/\.well-known\/context\.jsonld$/ },
|
||||
{ methods: READ, path: /^\/\.well-known\/nodeinfo$/ },
|
||||
{ methods: READ, path: /^\/nodeinfo\/2\.1$/ },
|
||||
{ methods: ['POST'], path: /^\/as\/actor\/app\/inbox$/ },
|
||||
{ methods: ['POST'], path: new RegExp(`^/\\.webhooks/${ID}$`) }
|
||||
];
|
||||
|
||||
/** The endpoints at the origin root, whatever the base path. */
|
||||
const ROOT_ENDPOINTS = [
|
||||
{ methods: READ, path: /^\/\.well-known\/context\.jsonld$/ },
|
||||
{ methods: READ, path: /^\/\.well-known\/nodeinfo$/ }
|
||||
];
|
||||
|
||||
/**
|
||||
* isPublicRequest(method, path) → boolean. True only when `method` (upper case) on `path` (the
|
||||
* request path without its query string, as the gateway routes it) is in `PUBLIC_ENDPOINTS`.
|
||||
* isPublicRequest(method, path, basePath = '/') → boolean. True only when `method` (upper case) on
|
||||
* `path` (the request path without its query string, as the gateway routes it) is in `ROOT_ENDPOINTS`,
|
||||
* or lies under `basePath` (as `basePathOf` returns it) and, relative to it, is in `PUBLIC_ENDPOINTS`.
|
||||
*/
|
||||
function isPublicRequest(method, path) {
|
||||
function isPublicRequest(method, path, basePath = '/') {
|
||||
if (typeof method !== 'string' || typeof path !== 'string') return false;
|
||||
const verb = method.toUpperCase();
|
||||
return PUBLIC_ENDPOINTS.some(endpoint => endpoint.methods.includes(verb) && endpoint.path.test(path));
|
||||
const listed = (endpoints, p) => endpoints.some(endpoint => endpoint.methods.includes(verb) && endpoint.path.test(p));
|
||||
if (listed(ROOT_ENDPOINTS, path)) return true;
|
||||
if (!path.startsWith(basePath)) return false;
|
||||
return listed(PUBLIC_ENDPOINTS, path.slice(basePath.length - 1));
|
||||
}
|
||||
|
||||
/** The path the gateway routes on: moleculer-web's `parsedUrl` (query stripped, trailing `/` trimmed). */
|
||||
@@ -71,14 +83,17 @@ function routedPathOf(req) {
|
||||
}
|
||||
|
||||
/**
|
||||
* publicSurfaceGate(req, res, next) — a moleculer-web global middleware (`api.use`, which the gateway
|
||||
* puts in front of every route, including the ones services add at runtime). Passes a request in
|
||||
* `PUBLIC_ENDPOINTS` through; answers any other with 404 before authentication or any action runs.
|
||||
* createPublicSurfaceGate(basePath = '/') → publicSurfaceGate(req, res, next), a moleculer-web global
|
||||
* middleware (`api.use`, which the gateway puts in front of every route, including the ones services
|
||||
* add at runtime). Passes a public request (`isPublicRequest` under `basePath`) through; answers any
|
||||
* other with 404 before authentication or any action runs.
|
||||
*/
|
||||
function publicSurfaceGate(req, res, next) {
|
||||
const method = req.method === 'OPTIONS' ? req.headers && req.headers['access-control-request-method'] : req.method;
|
||||
if (isPublicRequest(method, routedPathOf(req))) return next();
|
||||
return next(new MoleculerClientError('Not found', 404, 'NOT_FOUND'));
|
||||
function createPublicSurfaceGate(basePath = '/') {
|
||||
return function publicSurfaceGate(req, res, next) {
|
||||
const method = req.method === 'OPTIONS' ? req.headers && req.headers['access-control-request-method'] : req.method;
|
||||
if (isPublicRequest(method, routedPathOf(req), basePath)) return next();
|
||||
return next(new MoleculerClientError('Not found', 404, 'NOT_FOUND'));
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { isPublicRequest, publicSurfaceGate, PUBLIC_ENDPOINTS };
|
||||
module.exports = { isPublicRequest, createPublicSurfaceGate, PUBLIC_ENDPOINTS, ROOT_ENDPOINTS };
|
||||
|
||||
Reference in New Issue
Block a user