@sdxc/well-known 2026.10.24 → 2026.10.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,11 @@
7
7
  * @copyright Sergio Xalambrí 2026
8
8
  */
9
9
  import type { WellKnownParseError } from "../parse-error.js";
10
- /** Every RFC 8414 member, by camelCase field. */
10
+ /**
11
+ * Every RFC 8414 registry member, by camelCase field. `request_uri_parameter_supported`
12
+ * reads an absent member as `true`, the OpenID Connect Discovery definition the
13
+ * registry points to, so a server without by-reference requests must publish `false`.
14
+ */
11
15
  export declare const AUTHORIZATION_SERVER_FIELDS: {
12
16
  issuer: {
13
17
  wire: string;
@@ -46,6 +50,27 @@ export declare const AUTHORIZATION_SERVER_FIELDS: {
46
50
  wire: string;
47
51
  kind: "url";
48
52
  };
53
+ requirePushedAuthorizationRequests: {
54
+ wire: string;
55
+ kind: "boolean";
56
+ };
57
+ requestParameterSupported: {
58
+ wire: string;
59
+ kind: "boolean";
60
+ };
61
+ requestUriParameterSupported: {
62
+ wire: string;
63
+ kind: "boolean";
64
+ default: true;
65
+ };
66
+ requestObjectSigningAlgValuesSupported: {
67
+ wire: string;
68
+ kind: "strings";
69
+ };
70
+ requireSignedRequestObject: {
71
+ wire: string;
72
+ kind: "boolean";
73
+ };
49
74
  scopesSupported: {
50
75
  wire: string;
51
76
  kind: "strings";
@@ -107,6 +132,14 @@ export declare const AUTHORIZATION_SERVER_FIELDS: {
107
132
  wire: string;
108
133
  kind: "urls";
109
134
  };
135
+ introspectionSigningAlgValuesSupported: {
136
+ wire: string;
137
+ kind: "strings";
138
+ };
139
+ dpopSigningAlgValuesSupported: {
140
+ wire: string;
141
+ kind: "strings";
142
+ };
110
143
  signedMetadata: {
111
144
  wire: string;
112
145
  kind: "text";
@@ -121,8 +154,7 @@ export declare const AUTHORIZATION_SERVER_FIELDS: {
121
154
  export declare function issuerCheck(expected: URL | string | undefined): (fields: Record<string, unknown>, issues: WellKnownParseError.Issue[]) => void;
122
155
  /**
123
156
  * OpenID Connect Discovery §3 members over the RFC 8414 ones. `authorization_endpoint`
124
- * and `jwks_uri` become required, and `request_uri_parameter_supported` defaults to
125
- * `true`, as §3 states for an absent member.
157
+ * and `jwks_uri` become required.
126
158
  */
127
159
  export declare const OPENID_PROVIDER_FIELDS: {
128
160
  authorizationEndpoint: {
@@ -169,15 +201,6 @@ export declare const OPENID_PROVIDER_FIELDS: {
169
201
  wire: string;
170
202
  kind: "strings";
171
203
  };
172
- requestParameterSupported: {
173
- wire: string;
174
- kind: "boolean";
175
- };
176
- requestUriParameterSupported: {
177
- wire: string;
178
- kind: "boolean";
179
- default: true;
180
- };
181
204
  frontchannelLogoutSupported: {
182
205
  wire: string;
183
206
  kind: "boolean";
@@ -223,6 +246,27 @@ export declare const OPENID_PROVIDER_FIELDS: {
223
246
  wire: string;
224
247
  kind: "url";
225
248
  };
249
+ requirePushedAuthorizationRequests: {
250
+ wire: string;
251
+ kind: "boolean";
252
+ };
253
+ requestParameterSupported: {
254
+ wire: string;
255
+ kind: "boolean";
256
+ };
257
+ requestUriParameterSupported: {
258
+ wire: string;
259
+ kind: "boolean";
260
+ default: true;
261
+ };
262
+ requestObjectSigningAlgValuesSupported: {
263
+ wire: string;
264
+ kind: "strings";
265
+ };
266
+ requireSignedRequestObject: {
267
+ wire: string;
268
+ kind: "boolean";
269
+ };
226
270
  scopesSupported: {
227
271
  wire: string;
228
272
  kind: "strings";
@@ -284,6 +328,14 @@ export declare const OPENID_PROVIDER_FIELDS: {
284
328
  wire: string;
285
329
  kind: "urls";
286
330
  };
331
+ introspectionSigningAlgValuesSupported: {
332
+ wire: string;
333
+ kind: "strings";
334
+ };
335
+ dpopSigningAlgValuesSupported: {
336
+ wire: string;
337
+ kind: "strings";
338
+ };
287
339
  signedMetadata: {
288
340
  wire: string;
289
341
  kind: "text";
@@ -1,5 +1,9 @@
1
1
  import { sameIdentifier } from "./identifier.js";
2
- /** Every RFC 8414 member, by camelCase field. */
2
+ /**
3
+ * Every RFC 8414 registry member, by camelCase field. `request_uri_parameter_supported`
4
+ * reads an absent member as `true`, the OpenID Connect Discovery definition the
5
+ * registry points to, so a server without by-reference requests must publish `false`.
6
+ */
3
7
  export const AUTHORIZATION_SERVER_FIELDS = {
4
8
  issuer: { wire: "issuer", kind: "text", required: true },
5
9
  authorizationEndpoint: { wire: "authorization_endpoint", kind: "url" },
@@ -13,6 +17,21 @@ export const AUTHORIZATION_SERVER_FIELDS = {
13
17
  wire: "pushed_authorization_request_endpoint",
14
18
  kind: "url",
15
19
  },
20
+ requirePushedAuthorizationRequests: {
21
+ wire: "require_pushed_authorization_requests",
22
+ kind: "boolean",
23
+ },
24
+ requestParameterSupported: { wire: "request_parameter_supported", kind: "boolean" },
25
+ requestUriParameterSupported: {
26
+ wire: "request_uri_parameter_supported",
27
+ kind: "boolean",
28
+ default: true,
29
+ },
30
+ requestObjectSigningAlgValuesSupported: {
31
+ wire: "request_object_signing_alg_values_supported",
32
+ kind: "strings",
33
+ },
34
+ requireSignedRequestObject: { wire: "require_signed_request_object", kind: "boolean" },
16
35
  scopesSupported: { wire: "scopes_supported", kind: "strings" },
17
36
  responseTypesSupported: { wire: "response_types_supported", kind: "strings", required: true },
18
37
  responseModesSupported: { wire: "response_modes_supported", kind: "strings" },
@@ -43,6 +62,11 @@ export const AUTHORIZATION_SERVER_FIELDS = {
43
62
  kind: "boolean",
44
63
  },
45
64
  protectedResources: { wire: "protected_resources", kind: "urls" },
65
+ introspectionSigningAlgValuesSupported: {
66
+ wire: "introspection_signing_alg_values_supported",
67
+ kind: "strings",
68
+ },
69
+ dpopSigningAlgValuesSupported: { wire: "dpop_signing_alg_values_supported", kind: "strings" },
46
70
  signedMetadata: { wire: "signed_metadata", kind: "text" },
47
71
  };
48
72
  /**
@@ -65,8 +89,7 @@ export function issuerCheck(expected) {
65
89
  }
66
90
  /**
67
91
  * OpenID Connect Discovery §3 members over the RFC 8414 ones. `authorization_endpoint`
68
- * and `jwks_uri` become required, and `request_uri_parameter_supported` defaults to
69
- * `true`, as §3 states for an absent member.
92
+ * and `jwks_uri` become required.
70
93
  */
71
94
  export const OPENID_PROVIDER_FIELDS = {
72
95
  ...AUTHORIZATION_SERVER_FIELDS,
@@ -84,12 +107,6 @@ export const OPENID_PROVIDER_FIELDS = {
84
107
  claimsSupported: { wire: "claims_supported", kind: "strings" },
85
108
  promptValuesSupported: { wire: "prompt_values_supported", kind: "strings" },
86
109
  acrValuesSupported: { wire: "acr_values_supported", kind: "strings" },
87
- requestParameterSupported: { wire: "request_parameter_supported", kind: "boolean" },
88
- requestUriParameterSupported: {
89
- wire: "request_uri_parameter_supported",
90
- kind: "boolean",
91
- default: true,
92
- },
93
110
  frontchannelLogoutSupported: { wire: "frontchannel_logout_supported", kind: "boolean" },
94
111
  frontchannelLogoutSessionSupported: {
95
112
  wire: "frontchannel_logout_session_supported",
@@ -30,6 +30,16 @@ export interface AuthorizationServerMetadata<Extensions extends object = {}> {
30
30
  deviceAuthorizationEndpoint: URL | null;
31
31
  /** RFC 9126. */
32
32
  pushedAuthorizationRequestEndpoint: URL | null;
33
+ /** RFC 9126 §5: authorization request data is accepted only through the PAR endpoint. */
34
+ requirePushedAuthorizationRequests: boolean;
35
+ /** Whether the `request` parameter (RFC 9101) is accepted. */
36
+ requestParameterSupported: boolean;
37
+ /** Whether a `request_uri` the client hosts is fetched; absent in the document means `true`. */
38
+ requestUriParameterSupported: boolean;
39
+ /** The JWS algorithms a request object (RFC 9101) may be signed with. */
40
+ requestObjectSigningAlgValuesSupported: string[];
41
+ /** RFC 9101 §10.5: every authorization request must arrive as a signed request object. */
42
+ requireSignedRequestObject: boolean;
33
43
  scopesSupported: string[];
34
44
  responseTypesSupported: string[];
35
45
  responseModesSupported: string[];
@@ -47,6 +57,10 @@ export interface AuthorizationServerMetadata<Extensions extends object = {}> {
47
57
  authorizationResponseIssParameterSupported: boolean;
48
58
  /** RFC 9728 §4. */
49
59
  protectedResources: URL[];
60
+ /** RFC 9701 §7: the JWS algorithms a JWT introspection response is signed with. */
61
+ introspectionSigningAlgValuesSupported: string[];
62
+ /** RFC 9449 §5.1: the JWS algorithms a DPoP proof may be signed with. */
63
+ dpopSigningAlgValuesSupported: string[];
50
64
  /** A JWT carrying the metadata, kept opaque. */
51
65
  signedMetadata: string | null;
52
66
  extensions: Extensions;
@@ -31,9 +31,6 @@ export interface OpenIdProviderMetadata<Extensions extends object = {}> extends
31
31
  claimsSupported: string[];
32
32
  promptValuesSupported: string[];
33
33
  acrValuesSupported: string[];
34
- requestParameterSupported: boolean;
35
- /** Absent in the document means `true` (§3). */
36
- requestUriParameterSupported: boolean;
37
34
  frontchannelLogoutSupported: boolean;
38
35
  frontchannelLogoutSessionSupported: boolean;
39
36
  backchannelLogoutSupported: boolean;
@@ -16,11 +16,21 @@ export interface RespondOptions {
16
16
  cache?: PolicyOptions;
17
17
  /** Extra headers, applied last so they override the defaults. */
18
18
  headers?: HeadersInit;
19
+ /**
20
+ * Rewrites the serialized body before its `ETag` is computed, so a body wrapped in a
21
+ * signature (a cleartext-signed security.txt) is what the tag and a 304 describe.
22
+ */
23
+ transform?: (body: string) => string | Promise<string>;
24
+ /**
25
+ * Runs over the full 200 response before a 304 or `HEAD` drops its body, so a message
26
+ * signature can cover the body's digest, as a signed key directory does.
27
+ */
28
+ sign?: (response: Response, request: Request | undefined) => Promise<Response>;
19
29
  }
20
30
  /**
21
31
  * The document as a `Response`: its media type, a `Cache-Control` from `policy()`, an
22
- * `ETag` from `etag()` over the serialized body, CORS where the format requires it, and a
23
- * 304 through `conditional()` when `request` is given.
32
+ * `ETag` from `etag()` over the serialized body, CORS and the format's own headers, and a
33
+ * 304 through `conditional()` when `request` is given, after any `sign` step.
24
34
  *
25
35
  * @param format - The document's format.
26
36
  * @param document - The document to serve.
package/dist/response.js CHANGED
@@ -3,8 +3,8 @@ import { conditional, etag, policy } from "@sdxc/http/cache";
3
3
  const DEFAULT_CACHE = { visibility: "public", maxAge: "1 hour" };
4
4
  /**
5
5
  * The document as a `Response`: its media type, a `Cache-Control` from `policy()`, an
6
- * `ETag` from `etag()` over the serialized body, CORS where the format requires it, and a
7
- * 304 through `conditional()` when `request` is given.
6
+ * `ETag` from `etag()` over the serialized body, CORS and the format's own headers, and a
7
+ * 304 through `conditional()` when `request` is given, after any `sign` step.
8
8
  *
9
9
  * @param format - The document's format.
10
10
  * @param document - The document to serve.
@@ -15,6 +15,8 @@ const DEFAULT_CACHE = { visibility: "public", maxAge: "1 hour" };
15
15
  */
16
16
  export async function respond(format, document, options = {}) {
17
17
  let body = format.stringify(document);
18
+ if (options.transform)
19
+ body = await options.transform(body);
18
20
  let headers = new Headers({
19
21
  "Content-Type": format.mediaType,
20
22
  "Cache-Control": policy(options.cache ?? DEFAULT_CACHE).toString(),
@@ -24,11 +26,16 @@ export async function respond(format, document, options = {}) {
24
26
  headers.set("ETag", tag.data);
25
27
  if (format.cors)
26
28
  headers.set("Access-Control-Allow-Origin", "*");
29
+ for (let [name, value] of Object.entries(format.headers ?? {}))
30
+ headers.set(name, value);
27
31
  for (let [name, value] of new Headers(options.headers))
28
32
  headers.set(name, value);
29
33
  let request = options.request;
30
- let isHead = request?.method.toUpperCase() === "HEAD";
31
- let response = new Response(isHead ? null : body, { status: 200, headers });
34
+ let response = new Response(body, { status: 200, headers });
35
+ if (options.sign)
36
+ response = await options.sign(response, request);
37
+ if (request?.method.toUpperCase() === "HEAD")
38
+ response = new Response(null, response);
32
39
  if (request === undefined)
33
40
  return response;
34
41
  return await conditional(request, response);
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * security.txt (RFC 9116): a line-based reader enforcing the §2.5 cardinalities and
3
- * `https` rules, reading a cleartext-signed file from its signed body, and a writer,
4
- * so every app publishes where a researcher reports a vulnerability.
3
+ * `https` rules, a writer, and an OpenPGP cleartext signer, so every app publishes where
4
+ * a researcher reports a vulnerability in a file they can authenticate.
5
5
  *
6
6
  * @author [Sergio Xalambrí](https://sergiodxa.com)
7
7
  * @copyright Sergio Xalambrí 2026
8
8
  */
9
+ import type { OpenPgpError } from "@sdxc/openpgp";
9
10
  import type { Result } from "@sdxc/result";
10
11
  import type { WellKnownFormat } from "./format.js";
11
12
  import { WellKnownParseError } from "./parse-error.js";
@@ -62,3 +63,28 @@ export declare function stringify(document: SecurityTxt, options?: StringifyOpti
62
63
  export declare function isExpired(document: SecurityTxt, now?: Date): boolean;
63
64
  /** security.txt as a servable format. */
64
65
  export declare const securityTxt: WellKnownFormat<SecurityTxt>;
66
+ /** Where a cleartext signer reads its key and reports a key it cannot use. */
67
+ export interface CleartextSignerOptions {
68
+ /**
69
+ * The armored OpenPGP private key, read on each call so it can come from a Worker
70
+ * secret; `undefined` or an empty string serves the file unsigned.
71
+ */
72
+ key: () => string | undefined;
73
+ /** The key's passphrase, when it is protected. */
74
+ passphrase?: () => string | undefined;
75
+ /** Called when the key cannot be read, unlocked or used; the file is then served unsigned. */
76
+ onError?: (error: OpenPgpError) => void;
77
+ }
78
+ /**
79
+ * A body transform that wraps security.txt text in an OpenPGP cleartext signature, as RFC
80
+ * 9116 §2.3 recommends. The key is read once per distinct armored value and each body is
81
+ * signed once, so repeated answers share one `ETag`. Without a usable key the text is
82
+ * served unsigned, after `onError` reports why.
83
+ *
84
+ * @param options - Where the key comes from and where failures go.
85
+ * @example
86
+ * serve(securityTxt, () => SECURITY_TXT, {
87
+ * transform: cleartextSigner({ key: () => env.SECURITY_TXT_SIGNING_KEY }),
88
+ * });
89
+ */
90
+ export declare function cleartextSigner(options: CleartextSignerOptions): (body: string) => Promise<string>;
@@ -1,4 +1,5 @@
1
- import { failure, success } from "@sdxc/result";
1
+ import { readPrivateKey, signCleartext } from "@sdxc/openpgp";
2
+ import { failure, isFailure, success } from "@sdxc/result";
2
3
  import { WellKnownParseError } from "./parse-error.js";
3
4
  export const NAME = "security.txt";
4
5
  export const MEDIA_TYPE = "text/plain; charset=utf-8";
@@ -226,3 +227,43 @@ export const securityTxt = {
226
227
  stringify: (document) => stringify(document),
227
228
  parse,
228
229
  };
230
+ /** How many distinct bodies a signer keeps signed copies of. */
231
+ const SIGNED_CACHE_SIZE = 16;
232
+ /**
233
+ * A body transform that wraps security.txt text in an OpenPGP cleartext signature, as RFC
234
+ * 9116 §2.3 recommends. The key is read once per distinct armored value and each body is
235
+ * signed once, so repeated answers share one `ETag`. Without a usable key the text is
236
+ * served unsigned, after `onError` reports why.
237
+ *
238
+ * @param options - Where the key comes from and where failures go.
239
+ * @example
240
+ * serve(securityTxt, () => SECURITY_TXT, {
241
+ * transform: cleartextSigner({ key: () => env.SECURITY_TXT_SIGNING_KEY }),
242
+ * });
243
+ */
244
+ export function cleartextSigner(options) {
245
+ let loaded;
246
+ let signed = new Map();
247
+ return async (body) => {
248
+ let armored = options.key()?.trim() ?? "";
249
+ if (armored === "")
250
+ return body;
251
+ if (loaded?.armored !== armored) {
252
+ loaded = { armored, key: readPrivateKey(armored, { passphrase: options.passphrase?.() }) };
253
+ signed.clear();
254
+ }
255
+ let cached = signed.get(body);
256
+ if (cached !== undefined)
257
+ return cached;
258
+ let key = await loaded.key;
259
+ let result = isFailure(key) ? key : await signCleartext(body, key.data);
260
+ if (isFailure(result)) {
261
+ options.onError?.(result.error);
262
+ return body;
263
+ }
264
+ if (signed.size >= SIGNED_CACHE_SIZE)
265
+ signed.clear();
266
+ signed.set(body, result.data);
267
+ return result.data;
268
+ };
269
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdxc/well-known",
3
- "version": "2026.10.24",
3
+ "version": "2026.10.26",
4
4
  "description": "Typed documents for well-known URIs: security.txt, WebFinger, NodeInfo, OAuth and OIDC metadata, JWKS and more",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,8 +12,11 @@
12
12
  "./openid-configuration": "./dist/openid-configuration.js",
13
13
  "./oauth-protected-resource": "./dist/oauth-protected-resource.js",
14
14
  "./jwks": "./dist/jwks.js",
15
+ "./client-id-metadata-document": "./dist/client-id-metadata-document.js",
16
+ "./http-message-signatures-directory": "./dist/http-message-signatures-directory.js",
15
17
  "./change-password": "./dist/change-password.js",
16
18
  "./passkey-endpoints": "./dist/passkey-endpoints.js",
19
+ "./api-catalog": "./dist/api-catalog.js",
17
20
  "./nodeinfo": "./dist/nodeinfo.js",
18
21
  "./response": "./dist/response.js",
19
22
  "./middleware": "./dist/middleware.js"
@@ -21,11 +24,13 @@
21
24
  "dependencies": {
22
25
  "@remix-run/data-schema": "^1.0.0",
23
26
  "@sdxc/json-pointer": "2026.10.14",
27
+ "@sdxc/link-header": "2026.10.25",
28
+ "@sdxc/openpgp": "2026.10.25",
24
29
  "@sdxc/result": "2026.10.13",
25
30
  "@standard-schema/spec": "^1.1.0"
26
31
  },
27
32
  "peerDependencies": {
28
- "@sdxc/http": "2026.10.24",
33
+ "@sdxc/http": "2026.10.26",
29
34
  "remix": "3.1.0"
30
35
  },
31
36
  "peerDependenciesMeta": {
@@ -36,7 +41,7 @@
36
41
  "optional": true
37
42
  }
38
43
  },
39
- "gitHead": "f9db0f8f1f51a256e74b94b0f4b1db4be9880370",
44
+ "gitHead": "ff18988bd00de6200766dbc77367257277979566",
40
45
  "publishConfig": {
41
46
  "access": "public"
42
47
  },