@sdxc/well-known 0.0.0-pre.1

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.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * OAuth 2.0 Protected Resource Metadata (RFC 9728): the typed document, its reader
3
+ * with the §3.3 resource check a caller cannot skip, its writer, `define` and the
4
+ * metadata URL, so a client learns which authorization server protects an API.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
11
+ import type { WellKnownFormat } from "./format.js";
12
+ import type { WellKnownParseError } from "./parse-error.js";
13
+ export declare const NAME = "oauth-protected-resource";
14
+ export declare const MEDIA_TYPE = "application/json";
15
+ /** How a resource accepts a bearer token (RFC 6750 §2). */
16
+ export type BearerMethod = "header" | "body" | "query";
17
+ /**
18
+ * RFC 9728 §2, in the order the RFC lists its members.
19
+ *
20
+ * @template Extensions - The members outside this list, validated by a schema on parse.
21
+ */
22
+ export interface ProtectedResourceMetadata<Extensions extends object = {}> {
23
+ resource: URL;
24
+ /** Issuer identifiers of the authorization servers whose tokens the resource accepts. */
25
+ authorizationServers: URL[];
26
+ jwksUri: URL | null;
27
+ scopesSupported: string[];
28
+ bearerMethodsSupported: BearerMethod[];
29
+ resourceSigningAlgValuesSupported: string[];
30
+ resourceName: LocalizedString | null;
31
+ resourceDocumentation: URL | null;
32
+ resourcePolicyUri: URL | null;
33
+ resourceTosUri: URL | null;
34
+ tlsClientCertificateBoundAccessTokens: boolean;
35
+ authorizationDetailsTypesSupported: string[];
36
+ dpopSigningAlgValuesSupported: string[];
37
+ dpopBoundAccessTokensRequired: boolean;
38
+ /** A JWT carrying the metadata, kept opaque. */
39
+ signedMetadata: string | null;
40
+ extensions: Extensions;
41
+ }
42
+ /** A value with its untagged form and its `#`-tagged variants (§2.1), keyed by language tag. */
43
+ export interface LocalizedString {
44
+ value: string;
45
+ translations: Record<string, string>;
46
+ }
47
+ /**
48
+ * How a fetched metadata document is read.
49
+ *
50
+ * @template Extensions - The members the schema produces.
51
+ */
52
+ export interface ParseOptions<Extensions extends object> {
53
+ /** The identifier the metadata URL was built from, or the requested URL (§3.3). */
54
+ resource: URL | string;
55
+ /**
56
+ * Accept a `resource` that is a same-origin, segment-aligned prefix of `resource`,
57
+ * for metadata found through a challenge on a URL below the resource.
58
+ * @default "exact"
59
+ */
60
+ match?: "exact" | "prefix";
61
+ /** Validates the members outside the standard list; without one they stay unchecked. */
62
+ extensions?: StandardSchemaV1<unknown, Extensions>;
63
+ }
64
+ /**
65
+ * Reads protected resource metadata fetched for a resource. The published `resource`
66
+ * must equal the expected one (or, with `match: "prefix"`, cover it); a mismatch, a
67
+ * missing `resource`, or a malformed member fails. Members outside §2 land in
68
+ * `extensions`.
69
+ *
70
+ * @param text - The served JSON.
71
+ * @param options - The expected resource, how to match it, and an extension schema.
72
+ * @template Extensions - The extension members the schema produces.
73
+ * @example
74
+ * let metadata = parse(body, { resource: "https://api.example/v1" });
75
+ */
76
+ export declare function parse<Extensions extends object = Record<string, unknown>>(text: string, options: ParseOptions<Extensions>): Result<ProtectedResourceMetadata<Extensions>, WellKnownParseError>;
77
+ /**
78
+ * Writes the document as its registered member names, leaving out `null` members,
79
+ * empty lists and `false` flags; a localized name writes its tagged variants beside it.
80
+ *
81
+ * @param document - The metadata to publish.
82
+ */
83
+ export declare function stringify(document: ProtectedResourceMetadata<object>): string;
84
+ /**
85
+ * Builds a document from the members an app sets. Lists default to empty, URLs to
86
+ * `null`, booleans to `false`; `bearerMethodsSupported` to `["header"]`, the one method
87
+ * OAuth 2.1 and MCP both allow.
88
+ *
89
+ * @param document - The members the resource publishes.
90
+ * @template Extensions - Members outside §2 the resource also publishes.
91
+ * @example
92
+ * define({ resource: new URL("https://api.example/v1"), scopesSupported: ["read"] });
93
+ */
94
+ export declare function define<Extensions extends object = {}>(document: Pick<ProtectedResourceMetadata<Extensions>, "resource"> & Partial<ProtectedResourceMetadata<Extensions>>): ProtectedResourceMetadata<Extensions>;
95
+ /**
96
+ * Where a resource's metadata is served, with the suffix inserted before its path
97
+ * (§3.1).
98
+ *
99
+ * @param resource - The resource identifier.
100
+ * @example
101
+ * metadataUrl("https://api.example.com/v1");
102
+ * // https://api.example.com/.well-known/oauth-protected-resource/v1
103
+ */
104
+ export declare function metadataUrl(resource: URL | string): URL;
105
+ /**
106
+ * Protected resource metadata as a servable format. Its `parse` reads the structure
107
+ * alone; a client reading a fetched document calls `parse` with the resource it
108
+ * expected.
109
+ */
110
+ export declare const protectedResourceMetadata: WellKnownFormat<ProtectedResourceMetadata<object>>;
@@ -0,0 +1,136 @@
1
+ import { sameIdentifier } from "./lib/identifier.js";
2
+ import { defaults, readMetadata, writeMetadata } from "./lib/metadata.js";
3
+ import { wellKnownUrl } from "./well-known-url.js";
4
+ export const NAME = "oauth-protected-resource";
5
+ export const MEDIA_TYPE = "application/json";
6
+ /** Every RFC 9728 member, by camelCase field. */
7
+ const FIELDS = {
8
+ resource: { wire: "resource", kind: "url", required: true },
9
+ authorizationServers: { wire: "authorization_servers", kind: "urls" },
10
+ jwksUri: { wire: "jwks_uri", kind: "url" },
11
+ scopesSupported: { wire: "scopes_supported", kind: "strings" },
12
+ bearerMethodsSupported: {
13
+ wire: "bearer_methods_supported",
14
+ kind: "strings",
15
+ values: ["header", "body", "query"],
16
+ },
17
+ resourceSigningAlgValuesSupported: {
18
+ wire: "resource_signing_alg_values_supported",
19
+ kind: "strings",
20
+ },
21
+ resourceName: { wire: "resource_name", kind: "localized" },
22
+ resourceDocumentation: { wire: "resource_documentation", kind: "url" },
23
+ resourcePolicyUri: { wire: "resource_policy_uri", kind: "url" },
24
+ resourceTosUri: { wire: "resource_tos_uri", kind: "url" },
25
+ tlsClientCertificateBoundAccessTokens: {
26
+ wire: "tls_client_certificate_bound_access_tokens",
27
+ kind: "boolean",
28
+ },
29
+ authorizationDetailsTypesSupported: {
30
+ wire: "authorization_details_types_supported",
31
+ kind: "strings",
32
+ },
33
+ dpopSigningAlgValuesSupported: { wire: "dpop_signing_alg_values_supported", kind: "strings" },
34
+ dpopBoundAccessTokensRequired: { wire: "dpop_bound_access_tokens_required", kind: "boolean" },
35
+ signedMetadata: { wire: "signed_metadata", kind: "text" },
36
+ };
37
+ /**
38
+ * Whether a published resource is a same-origin, segment-aligned prefix of the one
39
+ * requested: `https://api.example/v1` covers `https://api.example/v1/items`, never
40
+ * `https://api.example/v10`.
41
+ *
42
+ * @param published - The document's `resource`.
43
+ * @param requested - The URL the client asked about.
44
+ */
45
+ function coversResource(published, requested) {
46
+ if (published.origin !== requested.origin)
47
+ return false;
48
+ if (sameIdentifier(published, requested))
49
+ return true;
50
+ let base = published.pathname.endsWith("/") ? published.pathname : `${published.pathname}/`;
51
+ return requested.pathname.startsWith(base);
52
+ }
53
+ /**
54
+ * Reads protected resource metadata fetched for a resource. The published `resource`
55
+ * must equal the expected one (or, with `match: "prefix"`, cover it); a mismatch, a
56
+ * missing `resource`, or a malformed member fails. Members outside §2 land in
57
+ * `extensions`.
58
+ *
59
+ * @param text - The served JSON.
60
+ * @param options - The expected resource, how to match it, and an extension schema.
61
+ * @template Extensions - The extension members the schema produces.
62
+ * @example
63
+ * let metadata = parse(body, { resource: "https://api.example/v1" });
64
+ */
65
+ export function parse(text, options) {
66
+ let expected = new URL(options.resource);
67
+ return readMetadata(text, {
68
+ format: NAME,
69
+ table: FIELDS,
70
+ extensions: options.extensions,
71
+ check(fields, issues) {
72
+ let published = fields.resource;
73
+ let matches = options.match === "prefix"
74
+ ? coversResource(published, expected)
75
+ : sameIdentifier(published, expected);
76
+ if (matches)
77
+ return;
78
+ issues.push({
79
+ at: "/resource",
80
+ message: `The document names resource "${published.href}", not "${expected.href}".`,
81
+ });
82
+ },
83
+ });
84
+ }
85
+ /**
86
+ * Writes the document as its registered member names, leaving out `null` members,
87
+ * empty lists and `false` flags; a localized name writes its tagged variants beside it.
88
+ *
89
+ * @param document - The metadata to publish.
90
+ */
91
+ export function stringify(document) {
92
+ return writeMetadata(document, FIELDS);
93
+ }
94
+ /**
95
+ * Builds a document from the members an app sets. Lists default to empty, URLs to
96
+ * `null`, booleans to `false`; `bearerMethodsSupported` to `["header"]`, the one method
97
+ * OAuth 2.1 and MCP both allow.
98
+ *
99
+ * @param document - The members the resource publishes.
100
+ * @template Extensions - Members outside §2 the resource also publishes.
101
+ * @example
102
+ * define({ resource: new URL("https://api.example/v1"), scopesSupported: ["read"] });
103
+ */
104
+ export function define(document) {
105
+ return {
106
+ ...defaults(FIELDS),
107
+ bearerMethodsSupported: ["header"],
108
+ extensions: {},
109
+ ...document,
110
+ };
111
+ }
112
+ /**
113
+ * Where a resource's metadata is served, with the suffix inserted before its path
114
+ * (§3.1).
115
+ *
116
+ * @param resource - The resource identifier.
117
+ * @example
118
+ * metadataUrl("https://api.example.com/v1");
119
+ * // https://api.example.com/.well-known/oauth-protected-resource/v1
120
+ */
121
+ export function metadataUrl(resource) {
122
+ return wellKnownUrl(resource, NAME, "insert");
123
+ }
124
+ /**
125
+ * Protected resource metadata as a servable format. Its `parse` reads the structure
126
+ * alone; a client reading a fetched document calls `parse` with the resource it
127
+ * expected.
128
+ */
129
+ export const protectedResourceMetadata = {
130
+ name: NAME,
131
+ mediaType: MEDIA_TYPE,
132
+ placement: "insert",
133
+ cors: false,
134
+ stringify,
135
+ parse: (text) => readMetadata(text, { format: NAME, table: FIELDS }),
136
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * OpenID Connect Discovery 1.0 provider metadata: the RFC 8414 document plus the
3
+ * members §3 adds, read with the §4.3 issuer check and served at the appended path
4
+ * §4 fixes, so a provider and its relying parties share one mapping.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { WellKnownFormat } from "./format.js";
11
+ import type { AuthorizationServerMetadata, ParseOptions } from "./oauth-authorization-server.js";
12
+ import type { WellKnownParseError } from "./parse-error.js";
13
+ export type { ParseOptions } from "./oauth-authorization-server.js";
14
+ export declare const NAME = "openid-configuration";
15
+ export declare const MEDIA_TYPE = "application/json";
16
+ /**
17
+ * OpenID Connect Discovery 1.0 §3, which extends the RFC 8414 members. OIDC Discovery
18
+ * requires `RS256` among `idTokenSigningAlgValuesSupported`; the reader accepts a
19
+ * provider publishing without it.
20
+ *
21
+ * @template Extensions - The members outside this list, validated by a schema on parse.
22
+ */
23
+ export interface OpenIdProviderMetadata<Extensions extends object = {}> extends AuthorizationServerMetadata<Extensions> {
24
+ authorizationEndpoint: URL;
25
+ jwksUri: URL;
26
+ userinfoEndpoint: URL | null;
27
+ endSessionEndpoint: URL | null;
28
+ checkSessionIframe: URL | null;
29
+ subjectTypesSupported: string[];
30
+ idTokenSigningAlgValuesSupported: string[];
31
+ claimsSupported: string[];
32
+ promptValuesSupported: string[];
33
+ acrValuesSupported: string[];
34
+ requestParameterSupported: boolean;
35
+ /** Absent in the document means `true` (§3). */
36
+ requestUriParameterSupported: boolean;
37
+ frontchannelLogoutSupported: boolean;
38
+ frontchannelLogoutSessionSupported: boolean;
39
+ backchannelLogoutSupported: boolean;
40
+ backchannelLogoutSessionSupported: boolean;
41
+ }
42
+ /**
43
+ * Reads provider metadata. A missing required member (`issuer`,
44
+ * `authorization_endpoint`, `jwks_uri`, `response_types_supported`,
45
+ * `subject_types_supported`, `id_token_signing_alg_values_supported`) or a different
46
+ * issuer fails; members outside §3 land in `extensions`.
47
+ *
48
+ * @param text - The served JSON.
49
+ * @param options - The expected issuer and an extension schema.
50
+ * @template Extensions - The extension members the schema produces.
51
+ * @example
52
+ * let metadata = parse(body, { issuer: "https://op.example" });
53
+ */
54
+ export declare function parse<Extensions extends object = Record<string, unknown>>(text: string, options?: ParseOptions<Extensions>): Result<OpenIdProviderMetadata<Extensions>, WellKnownParseError>;
55
+ /**
56
+ * Writes the document as its registered member names, leaving out `null` members,
57
+ * optional empty lists and flags at their §3 default. The REQUIRED lists are written
58
+ * even when empty, so the output always passes {@link parse}.
59
+ *
60
+ * @param document - The metadata to publish.
61
+ */
62
+ export declare function stringify(document: OpenIdProviderMetadata<object>): string;
63
+ /**
64
+ * Builds a document from the members an app sets; lists default to empty, URLs to
65
+ * `null` and flags to their §3 default.
66
+ *
67
+ * @param document - The members the provider publishes.
68
+ * @template Extensions - Members outside §3 the provider also publishes.
69
+ */
70
+ export declare function define<Extensions extends object = {}>(document: Pick<OpenIdProviderMetadata<Extensions>, "issuer" | "authorizationEndpoint" | "jwksUri" | "responseTypesSupported" | "subjectTypesSupported" | "idTokenSigningAlgValuesSupported"> & Partial<OpenIdProviderMetadata<Extensions>>): OpenIdProviderMetadata<Extensions>;
71
+ /**
72
+ * Provider metadata as a servable format, appended to the issuer (§4). Its `parse`
73
+ * checks no issuer; a relying party reading a fetched document calls `parse` with one.
74
+ */
75
+ export declare const openIdConfiguration: WellKnownFormat<OpenIdProviderMetadata<object>>;
@@ -0,0 +1,60 @@
1
+ import { defaults, readMetadata, writeMetadata } from "./lib/metadata.js";
2
+ import { issuerCheck, OPENID_PROVIDER_FIELDS } from "./lib/oauth-fields.js";
3
+ export const NAME = "openid-configuration";
4
+ export const MEDIA_TYPE = "application/json";
5
+ /**
6
+ * Reads provider metadata. A missing required member (`issuer`,
7
+ * `authorization_endpoint`, `jwks_uri`, `response_types_supported`,
8
+ * `subject_types_supported`, `id_token_signing_alg_values_supported`) or a different
9
+ * issuer fails; members outside §3 land in `extensions`.
10
+ *
11
+ * @param text - The served JSON.
12
+ * @param options - The expected issuer and an extension schema.
13
+ * @template Extensions - The extension members the schema produces.
14
+ * @example
15
+ * let metadata = parse(body, { issuer: "https://op.example" });
16
+ */
17
+ export function parse(text, options = {}) {
18
+ return readMetadata(text, {
19
+ format: NAME,
20
+ table: OPENID_PROVIDER_FIELDS,
21
+ extensions: options.extensions,
22
+ check: issuerCheck(options.issuer),
23
+ });
24
+ }
25
+ /**
26
+ * Writes the document as its registered member names, leaving out `null` members,
27
+ * optional empty lists and flags at their §3 default. The REQUIRED lists are written
28
+ * even when empty, so the output always passes {@link parse}.
29
+ *
30
+ * @param document - The metadata to publish.
31
+ */
32
+ export function stringify(document) {
33
+ return writeMetadata(document, OPENID_PROVIDER_FIELDS);
34
+ }
35
+ /**
36
+ * Builds a document from the members an app sets; lists default to empty, URLs to
37
+ * `null` and flags to their §3 default.
38
+ *
39
+ * @param document - The members the provider publishes.
40
+ * @template Extensions - Members outside §3 the provider also publishes.
41
+ */
42
+ export function define(document) {
43
+ return {
44
+ ...defaults(OPENID_PROVIDER_FIELDS),
45
+ extensions: {},
46
+ ...document,
47
+ };
48
+ }
49
+ /**
50
+ * Provider metadata as a servable format, appended to the issuer (§4). Its `parse`
51
+ * checks no issuer; a relying party reading a fetched document calls `parse` with one.
52
+ */
53
+ export const openIdConfiguration = {
54
+ name: NAME,
55
+ mediaType: MEDIA_TYPE,
56
+ placement: "append",
57
+ cors: false,
58
+ stringify,
59
+ parse: (text) => parse(text),
60
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The one failure every well-known reader returns, naming the format and each place
3
+ * the text broke its specification, so a caller can report all of them at once
4
+ * instead of fixing a document one error per fetch.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Why a well-known document could not be read (or, for a JWKS, written). Every issue
11
+ * found is listed, and `message` summarizes the first.
12
+ */
13
+ export declare class WellKnownParseError extends Error {
14
+ name: string;
15
+ /** The registered name of the document that failed, such as `security.txt`. */
16
+ readonly format: string;
17
+ readonly issues: WellKnownParseError.Issue[];
18
+ /**
19
+ * @param format - The registered name of the document.
20
+ * @param issues - Every problem found, at least one.
21
+ */
22
+ constructor(format: string, issues: WellKnownParseError.Issue[]);
23
+ }
24
+ export declare namespace WellKnownParseError {
25
+ /** One place a document breaks its specification. */
26
+ interface Issue {
27
+ /**
28
+ * A JSON Pointer for a JSON document (`""` for the whole document), a 1-based line
29
+ * number for a text one (`0` for the file as a whole).
30
+ */
31
+ at: string | number;
32
+ message: string;
33
+ }
34
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The one failure every well-known reader returns, naming the format and each place
3
+ * the text broke its specification, so a caller can report all of them at once
4
+ * instead of fixing a document one error per fetch.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Why a well-known document could not be read (or, for a JWKS, written). Every issue
11
+ * found is listed, and `message` summarizes the first.
12
+ */
13
+ export class WellKnownParseError extends Error {
14
+ name = "WellKnownParseError";
15
+ /** The registered name of the document that failed, such as `security.txt`. */
16
+ format;
17
+ issues;
18
+ /**
19
+ * @param format - The registered name of the document.
20
+ * @param issues - Every problem found, at least one.
21
+ */
22
+ constructor(format, issues) {
23
+ let first = issues[0]?.message ?? "The document is invalid.";
24
+ let more = issues.length > 1 ? ` (and ${issues.length - 1} more)` : "";
25
+ super(`${format}: ${first}${more}`);
26
+ this.format = format;
27
+ this.issues = issues;
28
+ }
29
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The W3C Passkey Endpoints document: where a password manager sends a person to
3
+ * create a passkey or manage existing ones. Every member is optional, so an empty
4
+ * object is a valid document.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { WellKnownFormat } from "./format.js";
11
+ import type { WellKnownParseError } from "./parse-error.js";
12
+ export declare const NAME = "passkey-endpoints";
13
+ export declare const MEDIA_TYPE = "application/json";
14
+ export interface PasskeyEndpoints {
15
+ /** The page where a signed-in person creates a passkey. */
16
+ enroll: URL | null;
17
+ /** The page where a signed-in person lists and removes their passkeys. */
18
+ manage: URL | null;
19
+ /** The page explaining how the site uses the WebAuthn PRF extension. */
20
+ prfUsageDetails: URL | null;
21
+ }
22
+ /**
23
+ * Reads the document; absent members are `null`, a member that is not an absolute URL
24
+ * fails, and unknown members are ignored.
25
+ *
26
+ * @param text - The served JSON.
27
+ */
28
+ export declare function parse(text: string): Result<PasskeyEndpoints, WellKnownParseError>;
29
+ /**
30
+ * Writes the document, leaving out `null` members.
31
+ *
32
+ * @param document - The endpoints to publish.
33
+ */
34
+ export declare function stringify(document: PasskeyEndpoints): string;
35
+ /** The passkey endpoints as a servable format, always answered with the document itself. */
36
+ export declare const passkeyEndpoints: WellKnownFormat<PasskeyEndpoints>;
@@ -0,0 +1,40 @@
1
+ import { success } from "@sdxc/result";
2
+ import { readMetadata, writeMetadata } from "./lib/metadata.js";
3
+ export const NAME = "passkey-endpoints";
4
+ export const MEDIA_TYPE = "application/json";
5
+ /** Every member the specification defines, by camelCase field. */
6
+ const FIELDS = {
7
+ enroll: { wire: "enroll", kind: "url" },
8
+ manage: { wire: "manage", kind: "url" },
9
+ prfUsageDetails: { wire: "prf_usage_details", kind: "url" },
10
+ };
11
+ /**
12
+ * Reads the document; absent members are `null`, a member that is not an absolute URL
13
+ * fails, and unknown members are ignored.
14
+ *
15
+ * @param text - The served JSON.
16
+ */
17
+ export function parse(text) {
18
+ let read = readMetadata(text, { format: NAME, table: FIELDS });
19
+ if (read.status === "failure")
20
+ return read;
21
+ let { enroll, manage, prfUsageDetails } = read.data;
22
+ return success({ enroll, manage, prfUsageDetails });
23
+ }
24
+ /**
25
+ * Writes the document, leaving out `null` members.
26
+ *
27
+ * @param document - The endpoints to publish.
28
+ */
29
+ export function stringify(document) {
30
+ return writeMetadata(document, FIELDS);
31
+ }
32
+ /** The passkey endpoints as a servable format, always answered with the document itself. */
33
+ export const passkeyEndpoints = {
34
+ name: NAME,
35
+ mediaType: MEDIA_TYPE,
36
+ placement: "insert",
37
+ cors: false,
38
+ stringify,
39
+ parse,
40
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Turns a well-known document into a `Response` with the headers every such answer
3
+ * shares: its media type, a cache policy, an `ETag`, CORS where the format requires
4
+ * it, and a 304 for a client whose copy is current.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { PolicyOptions } from "@sdxc/http/cache";
10
+ import type { WellKnownFormat } from "./format.js";
11
+ /** How a document is answered. */
12
+ export interface RespondOptions {
13
+ /** Enables a 304 when the request's `If-None-Match` still matches, and an empty body for `HEAD`. */
14
+ request?: Request;
15
+ /** @default { visibility: "public", maxAge: "1 hour" } */
16
+ cache?: PolicyOptions;
17
+ /** Extra headers, applied last so they override the defaults. */
18
+ headers?: HeadersInit;
19
+ }
20
+ /**
21
+ * 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.
24
+ *
25
+ * @param format - The document's format.
26
+ * @param document - The document to serve.
27
+ * @param options - The request, the cache policy and extra headers.
28
+ * @template Document - The format's document type.
29
+ * @example
30
+ * return respond(openIdConfiguration, WELL_KNOWN, { request: ctx.request });
31
+ */
32
+ export declare function respond<Document>(format: WellKnownFormat<Document>, document: Document, options?: RespondOptions): Promise<Response>;
@@ -0,0 +1,35 @@
1
+ import { conditional, etag, policy } from "@sdxc/http/cache";
2
+ /** The cache policy a well-known document gets unless the caller states one. */
3
+ const DEFAULT_CACHE = { visibility: "public", maxAge: "1 hour" };
4
+ /**
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.
8
+ *
9
+ * @param format - The document's format.
10
+ * @param document - The document to serve.
11
+ * @param options - The request, the cache policy and extra headers.
12
+ * @template Document - The format's document type.
13
+ * @example
14
+ * return respond(openIdConfiguration, WELL_KNOWN, { request: ctx.request });
15
+ */
16
+ export async function respond(format, document, options = {}) {
17
+ let body = format.stringify(document);
18
+ let headers = new Headers({
19
+ "Content-Type": format.mediaType,
20
+ "Cache-Control": policy(options.cache ?? DEFAULT_CACHE).toString(),
21
+ });
22
+ let tag = await etag(body);
23
+ if (tag.status === "success")
24
+ headers.set("ETag", tag.data);
25
+ if (format.cors)
26
+ headers.set("Access-Control-Allow-Origin", "*");
27
+ for (let [name, value] of new Headers(options.headers))
28
+ headers.set(name, value);
29
+ let request = options.request;
30
+ let isHead = request?.method.toUpperCase() === "HEAD";
31
+ let response = new Response(isHead ? null : body, { status: 200, headers });
32
+ if (request === undefined)
33
+ return response;
34
+ return await conditional(request, response);
35
+ }
@@ -0,0 +1,64 @@
1
+ /**
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.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { WellKnownFormat } from "./format.js";
11
+ import { WellKnownParseError } from "./parse-error.js";
12
+ export declare const NAME = "security.txt";
13
+ export declare const MEDIA_TYPE = "text/plain; charset=utf-8";
14
+ /** The fields RFC 9116 §2.5 defines, with unknown ones kept under `extensions`. */
15
+ export interface SecurityTxt {
16
+ /** `mailto:`, `tel:` or `https:` URIs, in the order a researcher should try them. */
17
+ contact: [URL, ...URL[]];
18
+ expires: Date;
19
+ encryption: URL[];
20
+ acknowledgments: URL[];
21
+ /** Language tags, written as one comma-separated field. */
22
+ preferredLanguages: string[];
23
+ canonical: URL[];
24
+ policy: URL[];
25
+ hiring: URL[];
26
+ /** Fields outside RFC 9116 §2.5, keyed by lowercased name, kept in order. */
27
+ extensions: Record<string, string[]>;
28
+ }
29
+ export interface ParsedSecurityTxt extends SecurityTxt {
30
+ /** Whether the text arrived inside an OpenPGP cleartext signature (RFC 9116 §2). */
31
+ signed: boolean;
32
+ }
33
+ export interface StringifyOptions {
34
+ /** `#` comment lines written before the first field. */
35
+ comments?: string[];
36
+ }
37
+ /**
38
+ * Reads a security.txt file. It fails on a missing `Contact`, a missing or repeated
39
+ * `Expires`, a repeated `Preferred-Languages`, an `http` URI (or a non-`https` one where
40
+ * §2.5 asks for a web URI), and a line that is neither a field, a comment nor blank.
41
+ * An expired file parses; `isExpired` answers staleness.
42
+ *
43
+ * @param text - The served text, signed or not; the signature is left unverified.
44
+ */
45
+ export declare function parse(text: string): Result<ParsedSecurityTxt, WellKnownParseError>;
46
+ /**
47
+ * Writes unsigned security.txt text: comments first, then the §2.5 fields, then the
48
+ * extensions, one per line with LF endings. `Expires` is written in UTC to the second.
49
+ *
50
+ * @param document - The fields to publish.
51
+ * @param options - Comment lines to write before the fields.
52
+ * @example
53
+ * stringify({ ...fields, expires: new Date("2027-01-01T00:00:00Z") }, { comments: ["Report issues here"] });
54
+ */
55
+ export declare function stringify(document: SecurityTxt, options?: StringifyOptions): string;
56
+ /**
57
+ * RFC 9116 §2.5.5: a file past its `Expires` is stale and SHOULD be ignored.
58
+ *
59
+ * @param document - The file's fields.
60
+ * @param now - The moment to judge against.
61
+ */
62
+ export declare function isExpired(document: SecurityTxt, now?: Date): boolean;
63
+ /** security.txt as a servable format. */
64
+ export declare const securityTxt: WellKnownFormat<SecurityTxt>;