Skip to content

feat(plugins): api.serverInfo — the server's own origin (#601)#605

Merged
melvincarvalho merged 2 commits into
gh-pagesfrom
feat/601-serverinfo
Jul 11, 2026
Merged

feat(plugins): api.serverInfo — the server's own origin (#601)#605
melvincarvalho merged 2 commits into
gh-pagesfrom
feat/601-serverinfo

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Closes #601 — the first (broadest, cheapest) of the four plugin-API seams from the 33-plugin exercise.

What

export async function activate(api) {
  api.fastify.get('/webfinger', async (req) => {
    const { baseUrl } = api.serverInfo();   // no more config-supplied origin
    return { subject: `acct:me@…`, links: [{ href: `${baseUrl}/profile/card` }] };
  });
}

api.serverInfo(){ baseUrl, protocol, host, port, listening }.

Design notes

  • A function, not a snapshot. activate() runs before listen(), so with port: 0 the real port doesn't exist yet. Called lazily (per request or in an onListen hook via api.fastify) it reports the resolved bind; called during activate it reports the configured values honestly — an explicit port: 0 stays 0 (?? not || in the ctx wiring) instead of being masked by the default, and listening: false says why.
  • 0.0.0.0 reports localhost as the callable host and an explicit idpIssuer wins as baseUrl — both mirror the existing pod-seeding logic in server.js, so a plugin's idea of the origin can't drift from the host's.
  • The live bind details (port, listening) remain available alongside an idpIssuer override for loopback use.

Tests

test/plugin-serverinfo.test.js: port-0 resolution (live value has the bound port, activate-time capture has 0/listening: false), 0.0.0.0localhost, idpIssuer override. Full suite 1034/1034. Documented in docs/configuration.md and the loader header.

A plugin minting absolute URLs or calling the host over loopback had to
be told its origin via config, and a wrong value fails quietly (nip05
serving an empty identity map). ~16 of the 33 out-of-tree plugins
repeat baseUrl/loopbackUrl in config for exactly this.

api.serverInfo() -> { baseUrl, protocol, host, port, listening }.
A function, not a snapshot: with port 0 the real port exists only once
the server is listening, so the seam is honest at both moments —
before listen it reports the configured values (an explicit port 0
stays 0 rather than being masked by the default), after listen the
resolved bind. 0.0.0.0 reports localhost as the callable host and an
explicit idpIssuer wins as the canonical public baseUrl, both
mirroring the existing pod-seeding logic.

Three tests: port-0 resolution live vs activate-time, 0.0.0.0 ->
localhost, idpIssuer override. Full suite 1034/1034.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new plugin API seam (api.serverInfo()) so plugins can reliably discover the server’s own origin (including port-0 resolution once listening), reducing the need for config-supplied baseUrl values that can silently drift.

Changes:

  • Wire origin inputs into the plugin loader context from createServer() options.
  • Add api.serverInfo() to the plugin API surface and document it.
  • Add a focused test suite covering port-0 behavior, 0.0.0.0localhost, and idpIssuer override semantics.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/server.js Passes normalized origin inputs (ssl/host/port/baseUrl override) into plugin-loader context for api.serverInfo().
src/plugins.js Introduces api.serverInfo() on the plugin API object.
test/plugin-serverinfo.test.js Adds tests exercising activate-time vs listening-time serverInfo values and override behavior.
docs/configuration.md Documents the new api.serverInfo() plugin API surface and its lazy usage expectations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/plugins.js Outdated
Comment on lines +166 to +172
const o = ctx.origin ?? {};
const addr = fastify.server?.listening ? fastify.server.address() : null;
const port = (typeof addr === 'object' && addr?.port) || (o.port ?? null);
const host = !o.host || o.host === '0.0.0.0' ? 'localhost' : o.host;
const protocol = o.ssl ? 'https' : 'http';
const baseUrl = o.baseUrl || `${protocol}://${host}:${port}`;
return { baseUrl, protocol, host, port, listening: !!addr };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both confirmed, fixed together in 8f6dad8 — once listening, server.address() wins for host AND port (unspecified binds still map to localhost; IPv6 literals bracketed in baseUrl), and the test harness now binds the host under test — the 0.0.0.0 case was indeed binding 127.0.0.1 and masking exactly this. New test pins configured-vs-live divergence. 1035/1035.

Comment thread test/plugin-serverinfo.test.js Outdated
Comment on lines +45 to +47
await server.listen({ port: 0, host: '127.0.0.1' });
baseUrl = `http://127.0.0.1:${server.server.address().port}`;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both confirmed, fixed together in 8f6dad8 — once listening, server.address() wins for host AND port (unspecified binds still map to localhost; IPv6 literals bracketed in baseUrl), and the test harness now binds the host under test — the 0.0.0.0 case was indeed binding 127.0.0.1 and masking exactly this. New test pins configured-vs-live divergence. 1035/1035.

listen() may be called with a different host than createServer() was
given, so once listening the address from server.address() wins over
options for both host and port. Unspecified binds (0.0.0.0, ::) still
map to localhost; IPv6 literals get bracketed in baseUrl. The test
harness now binds the host under test (the 0.0.0.0 case previously
bound 127.0.0.1, masking exactly this gap) and a new test pins
configured-vs-live divergence.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@melvincarvalho
melvincarvalho merged commit 538aa07 into gh-pages Jul 11, 2026
2 checks passed
@melvincarvalho
melvincarvalho deleted the feat/601-serverinfo branch July 11, 2026 09:43
melvincarvalho added a commit that referenced this pull request Jul 11, 2026
Four plugin-API seams land, all surfaced by the out-of-tree plugin
experiment (github.com/JavaScriptSolidServer/plugins), each with a
consumer proving the need:

- api.mountApp(handler, { prefix }) (#583, #590): mount a wrapped
  node-style (req, res) app — a reverse proxy or framework adapter —
  under the plugin's prefix. Bundles the appPaths WAC exemption, a
  scoped pass-through parser that hands the app an undrained body
  stream, and reply.hijack(). The raw-body-mode answer to #583.

- api.serverInfo() -> { baseUrl, protocol, host, port, listening }
  (#601, #605): a plugin learns the server's own origin instead of
  repeating baseUrl/loopbackUrl in config, where a wrong value fails
  quietly. A function, not a snapshot — with port 0 the real port
  exists only once listening; an explicit idpIssuer wins as the
  canonical baseUrl.

- api.reservePath(path, opts?) (#602, #607): claim and WAC-exempt a
  route outside the plugin's prefix. Literal paths ('/xrpc') own their
  subtree; parameterized paths ('/:user/did.json') match an exact
  shape (read-only by default) for pinned documents inside pod
  namespaces. Two plugins reserving the same path fail the boot with
  both names — loud beats the silent-loser outcome.

- api.plugins -> [{ id, prefix, module }] (#610, #612): a read-only,
  frozen boot-time roster of every loaded entry, so a plugin
  describing the deployment (a status dashboard, an admin console)
  enumerates its co-loaded siblings instead of being hand-fed a
  duplicate of the operator's plugins array.

All documented in docs/configuration.md App Plugins. Remaining plugin
seams tracked upstream: api.events.onResourceChange (#603),
api.authorize (#604).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin API seam: api.serverInfo — a plugin can't learn its own origin at activate time

2 participants