@sdxc/well-known
v2026.10.8
Published
Typed well-known URI documents: security.txt, WebFinger, OIDC metadata, JWKS
Readme
@sdxc/well-known
Typed documents for well-known URIs: security.txt, WebFinger, OAuth and OIDC metadata, JWKS and more.
Installation
npm add @sdxc/well-knownReaders return @sdxc/result values, which
install alongside this package. ./response and ./middleware also need
@sdxc/http and
remix, optional peer dependencies you add when you
serve documents; the document subpaths work without them.
RFC 8615 reserves /.well-known/ for documents a
client finds on any origin without being told where to look. Each registered name is one
subpath, with camelCase fields and URL values; the specification's member names appear only
in the serialized text. Every reader returns a Result whose failure, WellKnownParseError,
lists every issue with a JSON Pointer (or a line number for security.txt).
Usage
Serve security.txt And WebFinger From A Router
import { isFailure } from "@sdxc/result";
import { serve, wellKnown } from "@sdxc/well-known/middleware";
import { securityTxt } from "@sdxc/well-known/security-txt";
import { readQuery, select, webFinger } from "@sdxc/well-known/webfinger";
import type { SecurityTxt } from "@sdxc/well-known/security-txt";
import { createRouter } from "remix/router";
const SECURITY_TXT: SecurityTxt = {
contact: [new URL("mailto:[email protected]")],
expires: new Date("2027-06-30T00:00:00Z"),
encryption: [],
acknowledgments: [],
preferredLanguages: ["en"],
canonical: [new URL("https://example.com/.well-known/security.txt")],
policy: [],
hiring: [],
extensions: {},
};
let router = createRouter({
middleware: [
wellKnown({
"security.txt": serve(securityTxt, () => SECURITY_TXT),
webfinger: serve(webFinger, (ctx) => {
let query = readQuery(ctx.url);
if (isFailure(query)) return null;
return select(profileFor(query.data.resource), query.data.rels);
}),
}),
],
});Publish OpenID Connect Discovery From An Action
import { define, openIdConfiguration } from "@sdxc/well-known/openid-configuration";
import { respond } from "@sdxc/well-known/response";
import { createAction } from "remix/router";
const DISCOVERY = define({
issuer: "https://auth.example.com",
authorizationEndpoint: new URL("https://auth.example.com/authorize"),
jwksUri: new URL("https://auth.example.com/.well-known/jwks.json"),
responseTypesSupported: ["code"],
subjectTypesSupported: ["public"],
idTokenSigningAlgValuesSupported: ["ES256"],
});
export default createAction(routes.discovery, (ctx) =>
respond(openIdConfiguration, DISCOVERY, { request: ctx.request }),
);Read A Provider's Metadata
import { isFailure } from "@sdxc/result";
import { wellKnownUrl } from "@sdxc/well-known";
import { parse } from "@sdxc/well-known/oauth-authorization-server";
let issuer = "https://auth.example.com";
let url = wellKnownUrl(issuer, "oauth-authorization-server");
let metadata = parse(await (await fetch(url)).text(), { issuer });
if (isFailure(metadata)) return metadata;
metadata.data.tokenEndpoint; // URL | nullAPI
@sdxc/well-known
wellKnownUrl(identifier, name, placement = "insert")
The URL a document is served at for an identifier. "insert" (RFC 8414, RFC 9728) puts the
suffix between the host and the path, keeping the query; "append" (OIDC Discovery) adds it
after the path.
wellKnownUrl("https://as.example/tenant", "oauth-authorization-server");
// https://as.example/.well-known/oauth-authorization-server/tenant
wellKnownUrl("https://op.example/tenant", "openid-configuration", "append");
// https://op.example/tenant/.well-known/openid-configurationWellKnownParseError
The failure every reader returns: format (the registered name) and issues, each with at
(a JSON Pointer, "" for the whole document; or a 1-based line number, 0 for the whole
file) and message.
WellKnownFormat<Document> and WellKnownPlacement
The descriptor each document subpath exports: name, mediaType, placement, cors,
stringify and parse. respond and serve take one.
Every Document Subpath
Each exports NAME, MEDIA_TYPE, parse(text) returning a Result, stringify(document)
and a descriptor. The descriptor's parse reads structure only; a client reading a fetched
discovery document calls the subpath's parse with the identifier it expected.
@sdxc/well-known/security-txt (RFC 9116)
SecurityTxthascontact(at least one),expires,encryption,acknowledgments,preferredLanguages,canonical,policy,hiring, andextensionsfor other fields, keyed by lowercased name.parse(text)returns aParsedSecurityTxt, withsignedset when the text arrived inside an OpenPGP cleartext signature (read from its signed body; the signature is left unverified). It fails on a missingContact, a missing or repeatedExpires, a repeatedPreferred-Languages, anhttpURI, and a line that is neither a field, a comment nor blank. An expired file parses.stringify(document, options?)writes unsigned text with LF endings;StringifyOptionstakescommentsto write at the top.isExpired(document, now?)answers staleness (§2.5.5).securityTxtis the descriptor.
@sdxc/well-known/webfinger (RFC 7033)
JrdandJrdLinkare the descriptor and its links; absent members read asnull,[]or{}.readQuery(url)returns aWebFingerQuery({ resource, rels }), or aMissingResourceErrorto answer with a 400.select(document, rels)keeps only the links whoserelwas asked for; an empty list keeps them all.webFingeris the descriptor, withcors: trueas §5 requires.
@sdxc/well-known/oauth-authorization-server (RFC 8414)
AuthorizationServerMetadata<Extensions>has every §2 member plus RFC 8628, RFC 9126, RFC 9207 and RFC 9728 §4 members.issueris a string, compared as published.parse(text, options?)takesParseOptions(issuer,extensions) and fails on a missingissuerorresponse_types_supported, a malformed member, or a different issuer (§3.3). Unknown members land inextensions, validated by a synchronous Standard Schema when one is given.define(document)defaults lists to[], URLs tonulland flags tofalse.stringify(document)leaves outnullmembers, empty lists and flags at their default; the standard member wins over an extension of the same name.authorizationServerMetadatais the descriptor.
@sdxc/well-known/openid-configuration (OpenID Connect Discovery 1.0)
OpenIdProviderMetadata extends the RFC 8414 document with the §3 members, and makes
authorizationEndpoint and jwksUri required. parse, define, stringify and
ParseOptions work as above; requestUriParameterSupported defaults to true, as §3 states.
openIdConfiguration is the descriptor, with placement: "append".
@sdxc/well-known/oauth-protected-resource (RFC 9728)
ProtectedResourceMetadata<Extensions>has every §2 member;resourceNameis aLocalizedString(valueplus#-taggedtranslations), andBearerMethodis"header" | "body" | "query".parse(text, options)requiresParseOptions.resource: the document'sresourcemust equal it, or withmatch: "prefix"be a same-origin, segment-aligned prefix of it (§3.3).define(document)defaultsbearerMethodsSupportedto["header"].metadataUrl(resource)inserts the suffix before the resource's path.protectedResourceMetadatais the descriptor.
@sdxc/well-known/jwks (RFC 7517 §5)
Jwkkeeps the registered member names Web Crypto imports;JwkSetis{ keys }.parse(text)returns aParsedJwkSet, dropping entries that are not objects, lackkty, or lack a member theirEC,RSA,OKPoroctkey type requires, and counting them inskipped.stringify(document)returns aResultand fails on any private member (d,p,q,dp,dq,qi,oth,k).jwksis the descriptor; serving through it publishes each key's public half and leaves symmetric keys out.
@sdxc/well-known/change-password (W3C)
redirect(target) is a 302 to the page where a signed-in person changes their password.
NAME is "change-password".
@sdxc/well-known/passkey-endpoints (W3C)
PasskeyEndpoints has enroll, manage and prfUsageDetails, each URL | null; an empty
object is a valid document. passkeyEndpoints is the descriptor.
@sdxc/well-known/response
respond(format, document, options?)
A Promise<Response> with the document's media type, a Cache-Control (default
public, max-age=3600), an ETag over the body, CORS for a CORS format, a 304 when
options.request's If-None-Match still matches, and an empty body for HEAD.
RespondOptions takes request, cache and headers, which apply last.
@sdxc/well-known/middleware
serve(format, produce, options?)
A WellKnownEntry answering with produce(ctx)'s document through respond; a null
document falls through to the next middleware.
wellKnown(entries)
Answers GET and HEAD on /.well-known/<name> for the names given (and on
/.well-known/<name>/<path> for a served format that inserts its suffix), OPTIONS for a
CORS format, and 405 with Allow for other methods. Every other path goes to next(), so a
name of your own stays an ordinary route. An entry is any (ctx) => Response | null.
Pattern: Metadata For An API Accepting Bearer Tokens
import { serve, wellKnown } from "@sdxc/well-known/middleware";
import {
define,
metadataUrl,
protectedResourceMetadata,
} from "@sdxc/well-known/oauth-protected-resource";
const API_METADATA = define({
resource: new URL("https://api.example.com/v1"),
authorizationServers: [new URL("https://auth.example.com")],
scopesSupported: ["read", "write"],
});
let middleware = wellKnown({
"oauth-protected-resource": serve(protectedResourceMetadata, () => API_METADATA),
});
let challenge = `Bearer resource_metadata="${metadataUrl(API_METADATA.resource)}"`;Pattern: A Change-Password Redirect Beside Documents
import { redirect } from "@sdxc/well-known/change-password";
import { serve, wellKnown } from "@sdxc/well-known/middleware";
import { securityTxt } from "@sdxc/well-known/security-txt";
let middleware = wellKnown({
"security.txt": serve(securityTxt, () => SECURITY_TXT),
"change-password": () => redirect("/account/password"),
});Give security.txt a literal expires date and a test that fails when it is under 30 days
away; a date computed from the clock keeps the file fresh while its contact goes stale.
Pattern: Importing A Fetched Key Set
import { isFailure } from "@sdxc/result";
import { parse } from "@sdxc/well-known/jwks";
let response = await fetch("https://auth.example.com/.well-known/jwks.json");
let set = parse(await response.text());
if (isFailure(set)) return set;
let algorithm = { name: "ECDSA", namedCurve: "P-256" };
for (let key of set.data.keys) {
await crypto.subtle.importKey("jwk", key, algorithm, false, ["verify"]);
}Versioning
Releases are dated rather than semantic. A version is the UTC date it was published,
written YYYY.M.D, so 2026.9.4 is the release from 4 September 2026. At most one
release goes out per day.
Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.
Depend on one exact date, and move it when you are ready to take the change:
{
"dependencies": {
"@sdxc/well-known": "2026.9.4"
}
}A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.
License
MIT
