Skip to content

feat(auth): public getAgent accessor — request → WebID (#584)#586

Merged
melvincarvalho merged 6 commits into
gh-pagesfrom
get-agent-584
Jul 10, 2026
Merged

feat(auth): public getAgent accessor — request → WebID (#584)#586
melvincarvalho merged 6 commits into
gh-pagesfrom
get-agent-584

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Closes #584. Second seam from the plugin-zero work (#206), following the pattern #582/#585 set: smallest change, contract test, real consumer waiting.

What

auth.js at the package root — the stable public import for applications that authenticate their own traffic:

import { getAgent } from 'javascript-solid-server/auth.js';

const webId = await getAgent(request); // string | null

Thin wrapper over getWebIdFromRequestAsync, so it uniformly covers every token scheme the server itself accepts (IdP Bearer, Solid-OIDC DPoP, Nostr NIP-98, LWS10-CID) and never throws on bad credentials. Authentication only, deliberately not authorization — apps under an appPaths prefix (#582) own their own permissioning.

Why a root file

  • main (src/index.js) starts a server on import — unusable as an export surface
  • Adding an exports map would lock down every existing deep import — too invasive for this
  • files is unset, so the root file ships in the npm tarball, and javascript-solid-server/auth.js resolves everywhere

Everything under src/ stays internal and free to refactor; auth.js is the contract. When the #206 loader lands, api.auth.getAgent hands plugins this same function.

Tests

test/auth-agent.test.js pins the contract: anonymous → null, malformed credentials → null (never a throw), real IdP Bearer token → the account's WebID. Scheme-specific coverage stays with the existing auth suites. Full suite: 1006 pass.

Consumer

The Tideholm plugin-zero adapter migrates onto this import the moment it's published (per #584's acceptance list) — removing the last internal-path import in the reference plugin.

auth.js at the package root is the stable import path for apps that
authenticate their own traffic (appPaths mounts from #582, external
compositions):

    import { getAgent } from 'javascript-solid-server/auth.js';
    const webId = await getAgent(request); // string | null

Thin wrapper over getWebIdFromRequestAsync, so it covers every token
scheme the server accepts (IdP Bearer, Solid-OIDC DPoP, Nostr NIP-98,
LWS10-CID) and never throws on bad credentials. Authentication only,
deliberately not authorization — apps under an app path own their own
permissioning.

Everything in src/ stays internal and free to move; this file is the
contract, pinned by test/auth-agent.test.js. Pre-loader shape of the
seam: when #206's loader lands, api.auth.getAgent is this function.

Docs: the appPaths section now shows the public import instead of the
internal path. Full suite 1006 green.

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

This PR introduces a stable, root-level public authentication seam (javascript-solid-server/auth.js) so external apps/plugins can map an incoming request to an authenticated WebID without importing internal src/auth/* modules.

Changes:

  • Adds auth.js at the package root exporting getAgent(request) -> Promise<string|null> as a thin wrapper over getWebIdFromRequestAsync.
  • Adds a contract test (test/auth-agent.test.js) to pin behavior (anonymous/malformed → null, valid IdP Bearer → WebID).
  • Updates docs/configuration.md to recommend getAgent for identity resolution under appPaths mounts.

Reviewed changes

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

File Description
auth.js New public export surface providing getAgent(request) wrapper around internal auth resolution.
test/auth-agent.test.js New contract/integration test for the public getAgent behavior.
docs/configuration.md Docs updated to point app-path handlers at the new public accessor instead of internal imports.

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

Comment thread test/auth-agent.test.js
Comment on lines +56 to +67
it('resolves a real IdP Bearer token to the account WebID', async () => {
await fs.emptyDir(TEST_DATA_DIR);
server = createServer({
logger: false,
forceCloseConnections: true,
root: TEST_DATA_DIR,
idp: true,
idpIssuer: 'http://127.0.0.1:0', // patched after listen below
});
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.

Fixed — port reserved up front and passed as the real idpIssuer (adopted the well-known-did-nostr pattern you pointed at). The token/WebID pair the test asserts is now issued against the actual listening origin.

Comment thread auth.js Outdated
Comment on lines +27 to +29
* Resolve the authenticated agent of a request.
* @param {object} request - Fastify request (or any object with `headers`)
* @returns {Promise<string|null>} verified WebID, or null when anonymous /

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.

Agreed, the doc oversold it. JSDoc now states the split explicitly: Bearer reads only headers; DPoP/NIP-98/LWS-CID verification also reads method/url/protocol/hostname — so pass the real Fastify request.

The idpIssuer was a bogus http://127.0.0.1:0 with a stale 'patched
later' comment — now a port is reserved up front and the IdP gets its
real issuer (the well-known-did-nostr test's pattern). getAgent's
JSDoc no longer claims 'any object with headers': Bearer only reads
headers, but DPoP/NIP-98/LWS-CID verification also reads method, url,
protocol and hostname — pass the real request.

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 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread auth.js
Comment on lines +35 to +42
export async function getAgent(request) {
try {
const { webId } = await getWebIdFromRequestAsync(request);
return webId || null;
} catch {
return null;
}
}

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.

Right diagnosis, deliberate behavior: did:nostr agents are first-class in JSS, so filtering them to null would silently lock Nostr-authenticated users out of every app built on this seam — the plugin-zero consumer (Tideholm) explicitly wants them as players. What was wrong was the doc claiming 'WebID'. The contract now states it plainly: getAgent returns the verified agent identifier — an HTTP(S) WebID, or a did:nostr DID for NIP-98 agents without a WebID mapping — which is why it's named getAgent rather than getWebId. Callers who need strictly-HTTP WebIDs can filter on the prefix; the seam won't decide that for them.

Copilot correctly spotted that NIP-98 can yield did:nostr identifiers
where no WebID mapping exists. That's intended, not filtered: DID
agents are first-class in JSS, and locking them out would silently
exclude Nostr-authenticated users from every app. The contract docs
now say what the function actually promises — an HTTP(S) WebID or a
did:nostr DID — which is why the seam is named getAgent, not
getWebId. Apps key users on the identifier string either way.

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 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread auth.js Outdated
Comment on lines +13 to +16
* Covers every token scheme the server itself accepts, uniformly:
* IdP-issued Bearer tokens, Solid-OIDC DPoP, Nostr NIP-98 signatures,
* and LWS10-CID. The returned identifier is usually an HTTP(S) WebID;
* for NIP-98 it can be a `did:nostr:...` DID when no WebID mapping

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.

Added — the list now includes WebID-TLS client certificates (and says 'credential scheme' rather than 'token scheme', since TLS isn't a token). Verified against token.js: the async path does try webIdTlsAuth when a client certificate is present.

Comment thread test/auth-agent.test.js

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 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread test/auth-agent.test.js Outdated
@@ -0,0 +1,97 @@
/**
* getAgent — public request → WebID accessor (#584).

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.

Updated — the test header now says agent-identifier and spells out the WebID-or-did:nostr contract, matching auth.js and the PR description.

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 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread test/auth-agent.test.js Outdated
Comment on lines +63 to +69
const port = await new Promise((resolve) => {
const probe = net.createServer();
probe.listen(0, '127.0.0.1', () => {
const p = probe.address().port;
probe.close(() => resolve(p));
});
});

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.

Fixed — the probe now rejects on 'error' so a failed listen fails the test with the actual error instead of hanging. Also ported the same fix to the Tideholm demo where this pattern originated.

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 3 out of 3 changed files in this pull request and generated no new comments.

@melvincarvalho
melvincarvalho merged commit 327bb01 into gh-pages Jul 10, 2026
2 checks passed
@melvincarvalho
melvincarvalho deleted the get-agent-584 branch July 10, 2026 15:40
melvincarvalho added a commit that referenced this pull request Jul 10, 2026
Public auth seam (#584, #586): import { getAgent } from
'javascript-solid-server/auth.js' resolves a request to its verified
agent identifier — an HTTP(S) WebID or a did:nostr DID — across every
credential scheme (IdP Bearer, Solid-OIDC DPoP, Nostr NIP-98,
LWS10-CID, WebID-TLS). The stable contract for apps that authenticate
their own traffic under appPaths mounts (#582, shipped in 0.0.213);
everything in src/ remains internal. Second plugin-system seam from
#206; remaining: #583 raw-body mode, the #206/#564 loader.
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: bless getWebIdFromRequestAsync as api.auth.getAgent (request → WebID)

2 participants