ActivityPub references lead to requests for objects, contexts, and inboxes. Today, validatePublicUrl() returns Promise<void>, so a new request path can omit validation without a type error. I propose an AllowedUrl branded type for Fedify 3.0, required by the functions that perform outbound HTTP requests.
An AllowedUrl means that a particular request policy approved that exact URL. It does not establish the remote object's authenticity or make DNS results permanently safe. Vocabulary IDs and other stored references would remain ordinary URLs.
Preserve runtime URL compatibility
Use a URL subclass for approved values, with a branded, readonly public type. Only the approval factory should construct it. All URL setters should throw TypeError; overriding a setter must preserve its getter too.
searchParams should return one cached ImmutableUrlSearchParams instance, built from a detached copy of the approved query. Its append(), delete(), set(), and sort() methods should throw.
Illustrative API: client owns the request policy; method names are provisional.
const reference = new URL("https://example.org/actors/alice");
await client.fetch(reference); // Type error: URL is not AllowedUrl.
const allowed = await client.allowUrl(reference);
await client.fetch(allowed);
allowed.searchParams.set("page", "2"); // Throws TypeError.
const url: URL = allowed;
url.pathname = "/other"; // Throws TypeError despite the ordinary URL type.
Subclass overrides can be bypassed by calling native prototype setters or the native searchParams getter directly. Keep the approved href and issuing policy in private state, check both at the request boundary, and construct the request from that recorded string. A URL approved under allowPrivateAddress must not carry that approval into a stricter client. Copies and serialized values lose approval; queue consumers must approve destinations again under their current policy.
Own the document loader interface
Change Fedify's DocumentLoader to accept URL, with an adapter for jsonld.js's string-based interface. Keep document resolution separate from network fetching: the loader checks preloaded contexts and caches first, then obtains an AllowedUrl if it needs the network. The network fetcher requires AllowedUrl.
type DocumentLoader = (
url: URL,
options?: DocumentLoaderOptions,
) => Promise<RemoteDocument>;
type DocumentFetcher = (
url: AllowedUrl,
options?: DocumentLoaderOptions,
) => Promise<RemoteDocument>;
Approve only when the loader needs the network, so offline contexts and local identifiers avoid network-policy checks. Bind the policy when creating the loader so recursive loads use the same policy. Offer a network-fetcher extension point that receives approved URLs; replacing the whole resolver still gives application code responsibility for any requests it makes.
Do not automatically normalize JSON-LD identifiers as part of this change. The adapter must preserve the input IRI string where existing identity comparisons or cache keys use it. Whether RemoteDocument.documentUrl and contextUrl should also become URLs needs separate review; a fetched document's approval does not approve its context reference.
Cover every actual request
Redirect destinations and references discovered through HTML or Link headers need their own approval. Signed requests and retries must use the same request boundary; converting an AllowedUrl to Request must not discard the check. Verify the final request URL against the approved destination, re-sign redirected requests, and define which credentials may cross origins. Restrict native fetch() calls in remote-data handling code to the audited request implementation.
Request-time DNS checks remain necessary. Separate DNS validation followed by portable fetch() still leaves a validation-to-connection race; connection pinning or egress controls need a separate runtime-specific design.
Prototype the subclass and loader adapter before settling the public API. Check readonly type behavior, native-setter bypass detection, policy isolation, queue restoration, offline contexts, and signed redirect/retry paths on Deno, Node.js, Bun, and Cloudflare Workers. The API changes target 3.0; internal request consolidation can begin in 2.x.
ActivityPub references lead to requests for objects, contexts, and inboxes. Today,
validatePublicUrl()returnsPromise<void>, so a new request path can omit validation without a type error. I propose anAllowedUrlbranded type for Fedify 3.0, required by the functions that perform outbound HTTP requests.An
AllowedUrlmeans that a particular request policy approved that exact URL. It does not establish the remote object's authenticity or make DNS results permanently safe. Vocabulary IDs and other stored references would remain ordinary URLs.Preserve runtime URL compatibility
Use a
URLsubclass for approved values, with a branded, readonly public type. Only the approval factory should construct it. All URL setters should throwTypeError; overriding a setter must preserve its getter too.searchParamsshould return one cachedImmutableUrlSearchParamsinstance, built from a detached copy of the approved query. Itsappend(),delete(),set(), andsort()methods should throw.Illustrative API:
clientowns the request policy; method names are provisional.Subclass overrides can be bypassed by calling native prototype setters or the native
searchParamsgetter directly. Keep the approvedhrefand issuing policy in private state, check both at the request boundary, and construct the request from that recorded string. A URL approved underallowPrivateAddressmust not carry that approval into a stricter client. Copies and serialized values lose approval; queue consumers must approve destinations again under their current policy.Own the document loader interface
Change Fedify's
DocumentLoaderto acceptURL, with an adapter for jsonld.js's string-based interface. Keep document resolution separate from network fetching: the loader checks preloaded contexts and caches first, then obtains anAllowedUrlif it needs the network. The network fetcher requiresAllowedUrl.Approve only when the loader needs the network, so offline contexts and local identifiers avoid network-policy checks. Bind the policy when creating the loader so recursive loads use the same policy. Offer a network-fetcher extension point that receives approved URLs; replacing the whole resolver still gives application code responsibility for any requests it makes.
Do not automatically normalize JSON-LD identifiers as part of this change. The adapter must preserve the input IRI string where existing identity comparisons or cache keys use it. Whether
RemoteDocument.documentUrlandcontextUrlshould also become URLs needs separate review; a fetched document's approval does not approve its context reference.Cover every actual request
Redirect destinations and references discovered through HTML or
Linkheaders need their own approval. Signed requests and retries must use the same request boundary; converting anAllowedUrltoRequestmust not discard the check. Verify the final request URL against the approved destination, re-sign redirected requests, and define which credentials may cross origins. Restrict nativefetch()calls in remote-data handling code to the audited request implementation.Request-time DNS checks remain necessary. Separate DNS validation followed by portable
fetch()still leaves a validation-to-connection race; connection pinning or egress controls need a separate runtime-specific design.Prototype the subclass and loader adapter before settling the public API. Check readonly type behavior, native-setter bypass detection, policy isolation, queue restoration, offline contexts, and signed redirect/retry paths on Deno, Node.js, Bun, and Cloudflare Workers. The API changes target 3.0; internal request consolidation can begin in 2.x.