Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Server: pub http://localhost:4443/alice/public/data.json (on change)
| `--ap-display-name <name>` | ActivityPub display name | (username) |
| `--ap-summary <text>` | ActivityPub bio/summary | - |
| `--ap-nostr-pubkey <hex>` | Nostr pubkey for identity linking | - |
| `--public` | Allow unauthenticated access (skip WAC) | false |
| `--public` | Allow unauthenticated access (skip WAC); never writes into the served directory (no landing-page seed) | false |
| `--read-only` | Disable PUT/DELETE/PATCH methods | false |
| `--live-reload` | Auto-refresh browser on file changes | false |
| `--pay` | Enable HTTP 402 paid access for /pay/* | false |
Expand Down
7 changes: 5 additions & 2 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -887,8 +887,11 @@ export function createServer(options = {}) {
// Server-root landing page: seed /index.html and a public-read /.acl
// on first start (skip-if-exists, so operator-provided files are
// preserved). See #433 / #276. Skipped in read-only deployments so
// startup never mutates DATA_ROOT.
if (!options.readOnly) {
// startup never mutates DATA_ROOT, and in --public mode: WAC is
// bypassed there so the seeded .acl files would never be consulted,
// and public mode's serve-a-directory use case (servejss) must not
// write into the served tree.
if (!options.readOnly && !options.public) {
fastify.addHook('onReady', async () => {
// A missing or unreadable package.json (some production bundles
// omit it) shouldn't block seeding; fall back to "unknown".
Expand Down
50 changes: 50 additions & 0 deletions test/server-root.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,56 @@ describe('Server-root landing — operator override', () => {
});
});

describe('Server-root landing — public mode skips seeding', () => {
let server;
let baseUrl;
let savedDataRoot;
const DATA_DIR = './test-data-server-root-public';

before(async () => {
savedDataRoot = process.env.DATA_ROOT;

await fs.remove(DATA_DIR);
await fs.ensureDir(DATA_DIR);
await fs.writeFile(`${DATA_DIR}/hello.txt`, 'hello');

server = createServer({
logger: false,
root: DATA_DIR,
public: true,
forceCloseConnections: true,
});
await server.listen({ port: 0, host: '127.0.0.1' });
baseUrl = `http://127.0.0.1:${server.server.address().port}`;
});

after(async () => {
await server.close();
await fs.remove(DATA_DIR);
if (savedDataRoot === undefined) delete process.env.DATA_ROOT;
else process.env.DATA_ROOT = savedDataRoot;
});

it('does not seed /index.html, /.acl, or /index.html.acl into the served tree', async () => {
// WAC is bypassed in public mode, so the seeded ACLs would never be
// consulted — and serve-a-directory consumers (servejss) must not
// have startup write into the directory they serve.
assert.strictEqual(await fs.pathExists(`${DATA_DIR}/index.html`), false);
assert.strictEqual(await fs.pathExists(`${DATA_DIR}/.acl`), false);
assert.strictEqual(await fs.pathExists(`${DATA_DIR}/index.html.acl`), false);
});

it('GET / serves the container listing, and files remain readable', async () => {
const res = await fetch(`${baseUrl}/`);
assert.strictEqual(res.status, 200);
assert.match(res.headers.get('content-type'), /application\/ld\+json/);

const file = await fetch(`${baseUrl}/hello.txt`);
assert.strictEqual(file.status, 200);
assert.strictEqual(await file.text(), 'hello');
});
});

describe('renderServerRoot', () => {
// The seeded HTML is fully static — no template substitution, no
// values vary by request. This is deliberate: anything dynamic
Expand Down