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:
@@ -1,6 +1,7 @@
|
||||
const urlJoin = require('url-join');
|
||||
const { AppService } = require('@activitypods/app');
|
||||
const CONFIG = require('../config/config');
|
||||
const { requestUri } = require('./base-path');
|
||||
|
||||
// `GET /app` has two personalities, discriminated by the request's `JsonLdContext` header.
|
||||
//
|
||||
@@ -100,12 +101,12 @@ module.exports = {
|
||||
// The client_id must equal the fetched URI exactly. Both `/app` and `/as/actor/app`
|
||||
// serve this document (the configured client_id is the actor uri; `/app` remains
|
||||
// a valid alias), so echo the request path rather than hardcoding one.
|
||||
// `req.url` is rewritten to the alias remainder by moleculer-web; the raw path
|
||||
// survives in `originalUrl`.
|
||||
const requestPath = String((req && (req.originalUrl || req.url)) || '/as/actor/app').split('?')[0];
|
||||
// `req.url` is rewritten to the alias remainder by moleculer-web; the raw path, base
|
||||
// path included, survives in `originalUrl`.
|
||||
const requestPath = String((req && (req.originalUrl || req.url)) || '').split('?')[0];
|
||||
return {
|
||||
'@context': ['https://www.w3.org/ns/solid/oidc-context.jsonld'],
|
||||
client_id: urlJoin(CONFIG.HOME_URL, requestPath),
|
||||
client_id: requestPath ? requestUri(CONFIG.HOME_URL, requestPath) : urlJoin(CONFIG.HOME_URL, 'as/actor/app'),
|
||||
redirect_uris: [urlJoin(CONFIG.FRONT_URL, 'login')],
|
||||
client_name: CONFIG.APP_NAME,
|
||||
client_uri: CONFIG.FRONT_URL
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// The path the backend is served under, read from its base URL (SEMAPPS_HOME_URL): `/` when the
|
||||
// backend owns its origin (http://localhost:3003 locally), `/api/` when it shares the front end's
|
||||
// origin (https://<host>/api/, the Coolify layout, where one base URL per environment keeps pull
|
||||
// request previews on a single host). The proxy in front passes the path through unchanged; SemApps
|
||||
// mounts every route it adds under the base path, except the `/.well-known` ones, which stay at the
|
||||
// origin root. Festipod's own routes and its public-surface allow-list follow the same split.
|
||||
|
||||
const urlJoin = require('url-join');
|
||||
|
||||
/** basePathOf(baseUrl) → the base URL's path, always ending with `/` (`/` when it has none or is unset). */
|
||||
function basePathOf(baseUrl) {
|
||||
if (!baseUrl) return '/';
|
||||
const { pathname } = new URL(baseUrl);
|
||||
return pathname.endsWith('/') ? pathname : `${pathname}/`;
|
||||
}
|
||||
|
||||
/** underBasePath(basePath, path) → the absolute `path` (e.g. `/catalog`) as mounted under `basePath`. */
|
||||
function underBasePath(basePath, path) {
|
||||
return basePath + path.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* requestUri(baseUrl, requestPath) → the absolute URI a request was made to. `requestPath` is the raw
|
||||
* path the gateway received (moleculer-web's `originalUrl`), base path included: the base path is
|
||||
* taken off before joining it to the base URL, which would otherwise repeat it. Joining (rather than
|
||||
* resolving with `new URL`) keeps the base URL exactly as written — host case, explicit port — so the
|
||||
* result matches the URIs the backend mints from the same base URL, its actor's first.
|
||||
*/
|
||||
function requestUri(baseUrl, requestPath) {
|
||||
const basePath = basePathOf(baseUrl);
|
||||
const relative = requestPath.startsWith(basePath) ? requestPath.slice(basePath.length - 1) : requestPath;
|
||||
return urlJoin(baseUrl, relative);
|
||||
}
|
||||
|
||||
module.exports = { basePathOf, underBasePath, requestUri };
|
||||
@@ -0,0 +1,55 @@
|
||||
const { test, expect, describe } = require('bun:test');
|
||||
const { basePathOf, underBasePath, requestUri } = require('./base-path');
|
||||
|
||||
describe('basePathOf — the path the backend is served under', () => {
|
||||
test('a backend owning its origin is served at the root, with or without a trailing slash', () => {
|
||||
expect(basePathOf('http://localhost:3003')).toBe('/');
|
||||
expect(basePathOf('http://localhost:3003/')).toBe('/');
|
||||
});
|
||||
|
||||
test('a backend sharing the front end origin is served under its path, always ending with a slash', () => {
|
||||
expect(basePathOf('https://festipod.dev.reconnexion.coop/api/')).toBe('/api/');
|
||||
expect(basePathOf('https://festipod.dev.reconnexion.coop/api')).toBe('/api/');
|
||||
});
|
||||
|
||||
test('an unset base URL reads as the root', () => {
|
||||
expect(basePathOf(undefined)).toBe('/');
|
||||
expect(basePathOf('')).toBe('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('underBasePath — where a route is mounted', () => {
|
||||
test('at the root, a route keeps its path', () => {
|
||||
expect(underBasePath('/', '/catalog')).toBe('/catalog');
|
||||
expect(underBasePath('/', '/as/actor/app')).toBe('/as/actor/app');
|
||||
});
|
||||
|
||||
test('under a base path, a route is prefixed by it, without a doubled slash', () => {
|
||||
expect(underBasePath('/api/', '/catalog')).toBe('/api/catalog');
|
||||
expect(underBasePath('/api/', 'as/actor/app')).toBe('/api/as/actor/app');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestUri — the URI a request was made to', () => {
|
||||
test('at the root, the request path is appended to the origin', () => {
|
||||
expect(requestUri('http://localhost:3003', '/as/actor/app')).toBe('http://localhost:3003/as/actor/app');
|
||||
expect(requestUri('http://localhost:3003/', '/app')).toBe('http://localhost:3003/app');
|
||||
});
|
||||
|
||||
test('under a base path, the raw request path already carries it and is not prefixed twice', () => {
|
||||
expect(requestUri('https://festipod.dev.reconnexion.coop/api/', '/api/as/actor/app')).toBe(
|
||||
'https://festipod.dev.reconnexion.coop/api/as/actor/app'
|
||||
);
|
||||
expect(requestUri('https://festipod.dev.reconnexion.coop/api', '/api/app')).toBe(
|
||||
'https://festipod.dev.reconnexion.coop/api/app'
|
||||
);
|
||||
});
|
||||
|
||||
test('the base URL is kept exactly as written — the same URI the backend mints its actor under', () => {
|
||||
const urlJoin = require('url-join');
|
||||
for (const baseUrl of ['http://localhost:80', 'https://Festipod.Example/api/', 'https://host:443/api']) {
|
||||
const path = `${basePathOf(baseUrl)}as/actor/app`;
|
||||
expect(requestUri(baseUrl, path)).toBe(urlJoin(baseUrl, 'as/actor/app'));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
const urlJoin = require('url-join');
|
||||
const CONFIG = require('../config/config');
|
||||
const { requestUri } = require('./base-path');
|
||||
|
||||
// Serves the app actor at its canonical uri (`/as/actor/app`) with a Solid-OIDC dual
|
||||
// personality, discriminated by the request's `JsonLdContext` header — same contract as
|
||||
@@ -28,11 +29,12 @@ module.exports = {
|
||||
|
||||
if (isOidcClientIdDocument) {
|
||||
// The client_id must equal the fetched URI exactly. `req.url` is rewritten to
|
||||
// the alias remainder by moleculer-web; the raw path survives in `originalUrl`.
|
||||
const requestPath = String((req && (req.originalUrl || req.url)) || '/as/actor/app').split('?')[0];
|
||||
// the alias remainder by moleculer-web; the raw path, base path included, survives
|
||||
// in `originalUrl`.
|
||||
const requestPath = String((req && (req.originalUrl || req.url)) || '').split('?')[0];
|
||||
return {
|
||||
'@context': ['https://www.w3.org/ns/solid/oidc-context.jsonld'],
|
||||
client_id: urlJoin(CONFIG.HOME_URL, requestPath),
|
||||
client_id: requestPath ? requestUri(CONFIG.HOME_URL, requestPath) : urlJoin(CONFIG.HOME_URL, 'as/actor/app'),
|
||||
redirect_uris: [urlJoin(CONFIG.FRONT_URL, 'login')],
|
||||
client_name: CONFIG.APP_NAME,
|
||||
client_uri: CONFIG.FRONT_URL
|
||||
|
||||
@@ -2,7 +2,11 @@ const path = require('path');
|
||||
const { CoreService } = require('@semapps/core');
|
||||
const { apods, notify, interop, oidc, skos } = require('@semapps/ontologies');
|
||||
const CONFIG = require('../../config/config');
|
||||
const { publicSurfaceGate } = require('../public-surface');
|
||||
const { createPublicSurfaceGate } = require('../public-surface');
|
||||
const { basePathOf, underBasePath } = require('../base-path');
|
||||
|
||||
// Festipod's own routes live under the base path, as SemApps' do (see base-path.js).
|
||||
const BASE_PATH = basePathOf(CONFIG.HOME_URL);
|
||||
|
||||
module.exports = {
|
||||
mixins: [CoreService],
|
||||
@@ -25,10 +29,10 @@ module.exports = {
|
||||
// catch-all among them, which with `webacl: false` would serve the whole `settings` dataset,
|
||||
// the app's private key included): only the allow-listed public endpoints pass, anything else
|
||||
// is a 404 for every caller. See public-surface.js.
|
||||
use: [publicSurfaceGate],
|
||||
use: [createPublicSurfaceGate(BASE_PATH)],
|
||||
routes: [
|
||||
{
|
||||
path: '/app',
|
||||
path: underBasePath(BASE_PATH, '/app'),
|
||||
aliases: {
|
||||
'GET /': {
|
||||
action: 'app.get',
|
||||
@@ -46,7 +50,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/as/actor/app',
|
||||
path: underBasePath(BASE_PATH, '/as/actor/app'),
|
||||
aliases: {
|
||||
'GET /': {
|
||||
// Served by a standalone service (not `app.get`): the Solid-OIDC client_id
|
||||
@@ -64,7 +68,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/catalog',
|
||||
path: underBasePath(BASE_PATH, '/catalog'),
|
||||
aliases: {
|
||||
'GET /': 'catalog.discover'
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
const path = require('path');
|
||||
const { test, expect, describe, afterEach } = require('bun:test');
|
||||
const { ServiceBroker } = require('moleculer');
|
||||
const { isPublicRequest, publicSurfaceGate } = require('./public-surface');
|
||||
const { isPublicRequest, createPublicSurfaceGate } = require('./public-surface');
|
||||
const { basePathOf } = require('./base-path');
|
||||
|
||||
const UUID = 'd9b5f8ca-b2e2-41eb-af83-64e5ef1d4b20';
|
||||
|
||||
describe('the core service mounts the gate in front of every route', () => {
|
||||
test('core.service.js wires publicSurfaceGate as a global gateway middleware, and no SPARQL endpoint', () => {
|
||||
test('core.service.js wires the gate, for its own base path, as a global gateway middleware, and no SPARQL endpoint', () => {
|
||||
const core = require('./core/core.service.js');
|
||||
expect(core.settings.api.use).toContain(publicSurfaceGate);
|
||||
const CONFIG = require('../config/config');
|
||||
const base = basePathOf(CONFIG.HOME_URL);
|
||||
expect(core.settings.api.use).toHaveLength(1);
|
||||
const [gate] = core.settings.api.use;
|
||||
expect(gate.name).toBe('publicSurfaceGate');
|
||||
let passed;
|
||||
gate({ method: 'GET', parsedUrl: `${base}catalog`, headers: {} }, {}, err => (passed = err === undefined));
|
||||
expect(passed).toBe(true);
|
||||
gate({ method: 'GET', parsedUrl: `${base}key`, headers: {} }, {}, err => (passed = err === undefined));
|
||||
expect(passed).toBe(false);
|
||||
expect(core.settings.sparqlEndpoint).toBe(false);
|
||||
});
|
||||
|
||||
test("core.service.js mounts Festipod's own routes under its base path", () => {
|
||||
const core = require('./core/core.service.js');
|
||||
const CONFIG = require('../config/config');
|
||||
const base = basePathOf(CONFIG.HOME_URL);
|
||||
expect(core.settings.api.routes.map(route => route.path)).toEqual([`${base}app`, `${base}as/actor/app`, `${base}catalog`]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPublicRequest — the allow-list', () => {
|
||||
@@ -89,7 +106,56 @@ describe('isPublicRequest — the allow-list', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPublicRequest — under a base path (the Coolify layout: the backend at /api/ on the front end origin)', () => {
|
||||
const BASE = '/api/';
|
||||
|
||||
test('every public endpoint is public under the base path, the /.well-known ones at the origin root', () => {
|
||||
for (const [method, p] of [
|
||||
['GET', '/api/app'],
|
||||
['GET', '/api/as/actor/app'],
|
||||
['GET', '/api/catalog'],
|
||||
['GET', `/api/public-key/${UUID}`],
|
||||
['GET', `/api/interop/access-need-group/${UUID}`],
|
||||
['GET', `/api/interop/access-need/${UUID}`],
|
||||
['GET', '/api/nodeinfo/2.1'],
|
||||
['POST', '/api/as/actor/app/inbox'],
|
||||
['POST', `/api/.webhooks/${UUID}`],
|
||||
['GET', '/.well-known/context.jsonld'],
|
||||
['GET', '/.well-known/nodeinfo']
|
||||
]) {
|
||||
expect(`${method} ${p} ${isPublicRequest(method, p, BASE)}`).toBe(`${method} ${p} true`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the same endpoints outside the base path, and the /.well-known ones under it, are refused', () => {
|
||||
for (const [method, p] of [
|
||||
['GET', '/app'],
|
||||
['GET', '/as/actor/app'],
|
||||
['GET', '/catalog'],
|
||||
['GET', `/public-key/${UUID}`],
|
||||
['POST', '/as/actor/app/inbox'],
|
||||
['POST', `/.webhooks/${UUID}`],
|
||||
['GET', '/api/.well-known/context.jsonld'],
|
||||
['GET', '/api/.well-known/nodeinfo'],
|
||||
['GET', '/api'],
|
||||
['GET', '/apiapp'],
|
||||
['GET', `/apipublic-key/${UUID}`]
|
||||
]) {
|
||||
expect(`${method} ${p} ${isPublicRequest(method, p, BASE)}`).toBe(`${method} ${p} false`);
|
||||
}
|
||||
});
|
||||
|
||||
test('what is refused at the root is refused under the base path', () => {
|
||||
for (const p of ['/', '/key', `/key/${UUID}`, '/sparql', '/proxy', `/interop/access-grant/${UUID}`, '/as/actor/app/outbox', `/public-key/../key/${UUID}`, `//public-key/${UUID}`]) {
|
||||
expect(`GET /api${p} ${isPublicRequest('GET', `/api${p}`, BASE)}`).toBe(`GET /api${p} false`);
|
||||
}
|
||||
expect(isPublicRequest('PUT', '/api/catalog', BASE)).toBe(false);
|
||||
expect(isPublicRequest('POST', '/api/app', BASE)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('publicSurfaceGate — the middleware', () => {
|
||||
const publicSurfaceGate = createPublicSurfaceGate('/');
|
||||
const run = req => {
|
||||
let passed;
|
||||
publicSurfaceGate(req, {}, err => {
|
||||
@@ -163,7 +229,7 @@ describe('the gate covers routes added at runtime — the LDP catch-all that ser
|
||||
});
|
||||
|
||||
test('with the gate: the key container, the root and any write are 404; a public key is served', async () => {
|
||||
const base = await startGateway([publicSurfaceGate]);
|
||||
const base = await startGateway([createPublicSurfaceGate('/')]);
|
||||
expect(await status(base, 'GET', '/key')).toBe(404);
|
||||
expect(await status(base, 'GET', `/key/${UUID}`)).toBe(404);
|
||||
expect(await status(base, 'GET', '/')).toBe(404);
|
||||
|
||||
@@ -774,7 +774,10 @@ async function tracked(): Promise<void> {
|
||||
*/
|
||||
async function publicSurface(): Promise<void> {
|
||||
const G = 'backend public surface (anonymous GET, status codes only)';
|
||||
const status = async (path: string): Promise<number> => (await http('GET', `${BACKEND}${path}`)).status;
|
||||
// A path is under the backend's base URL; a full URL is taken as is.
|
||||
const status = async (target: string): Promise<number> => (await http('GET', /^https?:\/\//.test(target) ? target : `${BACKEND}${target}`)).status;
|
||||
// The /.well-known endpoints sit at the backend ORIGIN's root whatever its base path (`/api` on Coolify, backend/services/base-path.js).
|
||||
const atOriginRoot = (path: string): string => `${new URL(BACKEND).origin}${path}`;
|
||||
const pathOf = (uri: string | undefined, what: string): string => {
|
||||
const u = need(uri, what);
|
||||
if (!u.startsWith(`${BACKEND}/`)) throw new Error(`${what} is not on the backend (${u})`);
|
||||
@@ -794,12 +797,12 @@ async function publicSurface(): Promise<void> {
|
||||
pathOf(linkId(actor?.['publicKey']), 'the app actor\'s public key (GET /as/actor/app)'),
|
||||
groupPath,
|
||||
pathOf(linkId(arrayOf(group?.['interop:hasAccessNeed'])[0]), 'an access need (the access-need group)'),
|
||||
'/.well-known/context.jsonld', '/.well-known/nodeinfo', '/nodeinfo/2.1',
|
||||
atOriginRoot('/.well-known/context.jsonld'), atOriginRoot('/.well-known/nodeinfo'), '/nodeinfo/2.1',
|
||||
];
|
||||
const codes = await Promise.all(paths.map(status));
|
||||
return { ok: codes.every(c => c === 200), actual: paths.map((p, i) => `${shown(p)} ${codes[i]}`).join(', ') };
|
||||
});
|
||||
const webfinger = `/.well-known/webfinger?resource=${encodeURIComponent(`acct:app@${new URL(BACKEND).host}`)}`;
|
||||
const webfinger = atOriginRoot(`/.well-known/webfinger?resource=${encodeURIComponent(`acct:app@${new URL(BACKEND).host}`)}`);
|
||||
const refusedPaths = [
|
||||
'/', '/key', '/sparql', '/sparql?query=ASK%7B%7D', '/settings', '/proxy', '/_acl', '/.webhooks',
|
||||
'/interop/application-registration', '/interop/access-grant', '/interop/data-grant',
|
||||
|
||||
Reference in New Issue
Block a user