@fedify/vocab-runtime 2.4.2 → 2.4.4

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.
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
package/dist/mod.cjs CHANGED
@@ -16,7 +16,7 @@ let _multiformats_base_x = require("@multiformats/base-x");
16
16
  _multiformats_base_x = require_rolldown_runtime.__toESM(_multiformats_base_x, 1);
17
17
  //#region deno.json
18
18
  var name = "@fedify/vocab-runtime";
19
- var version = "2.4.2";
19
+ var version = "2.4.4";
20
20
  //#endregion
21
21
  //#region src/request.ts
22
22
  /**
@@ -688,7 +688,10 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
688
688
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
689
689
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
690
690
  options?.signal?.throwIfAborted();
691
- const currentUrl = new URL(url).href;
691
+ const parsedUrl = new URL(url);
692
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new require_url.UrlError(`Unsupported protocol: ${parsedUrl.protocol}`);
693
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new require_url.UrlError("URLs with credentials are not allowed");
694
+ const currentUrl = parsedUrl.href;
692
695
  if (!skipPreloadedContexts && currentUrl in require_contexts.preloadedContexts) {
693
696
  logger.debug("Using preloaded context: {url}.", { url: currentUrl });
694
697
  return {
package/dist/mod.js CHANGED
@@ -12,7 +12,7 @@ import { PublicKeyInfo } from "pkijs";
12
12
  import baseX from "@multiformats/base-x";
13
13
  //#region deno.json
14
14
  var name = "@fedify/vocab-runtime";
15
- var version = "2.4.2";
15
+ var version = "2.4.4";
16
16
  //#endregion
17
17
  //#region src/request.ts
18
18
  /**
@@ -684,7 +684,10 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
684
684
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
685
685
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
686
686
  options?.signal?.throwIfAborted();
687
- const currentUrl = new URL(url).href;
687
+ const parsedUrl = new URL(url);
688
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsedUrl.protocol}`);
689
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
690
+ const currentUrl = parsedUrl.href;
688
691
  if (!skipPreloadedContexts && currentUrl in preloadedContexts) {
689
692
  logger.debug("Using preloaded context: {url}.", { url: currentUrl });
690
693
  return {
@@ -1,4 +1,4 @@
1
- import { t as FetchError } from "./request-C4AssP0b.mjs";
1
+ import { t as FetchError } from "./request-Cb7B8WAb.mjs";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,4 +1,4 @@
1
- const require_request = require("./request-CAxz3d9M.cjs");
1
+ const require_request = require("./request-nWurHb32.cjs");
2
2
  let _logtape_logtape = require("@logtape/logtape");
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,4 +1,4 @@
1
- const require_body = require("./body-COsEN5g-.cjs");
1
+ const require_body = require("./body-z_6Hg7ci.cjs");
2
2
  let node_assert_strict = require("node:assert/strict");
3
3
  let node_test = require("node:test");
4
4
  let node_zlib = require("node:zlib");
@@ -1,4 +1,4 @@
1
- import { i as readBoundedText, t as BodyTooLargeError } from "./body-7_djU77X.mjs";
1
+ import { i as readBoundedText, t as BodyTooLargeError } from "./body-CrrDpQuG.mjs";
2
2
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
3
3
  import { test } from "node:test";
4
4
  import { gunzipSync, gzipSync } from "node:zlib";
@@ -1,11 +1,11 @@
1
1
  require("./url-BNakuZ8k.cjs");
2
- require("./docloader-D5WdzhZN.cjs");
2
+ require("./docloader-DUnO3Uvt.cjs");
3
3
  require("./key-C-AYkdJJ.cjs");
4
4
  require("./multibase-B5Mea7Ip.cjs");
5
5
  require("./digest-3FeH2Y-Q.cjs");
6
6
  require("./langtag-Czjvxfzx.cjs");
7
7
  require("./langstr-_PqsL1ae.cjs");
8
- require("./portable-media-Dh0NSS49.cjs");
8
+ require("./portable-media-DBl1-UE5.cjs");
9
9
  let node_test = require("node:test");
10
10
  let node_assert = require("node:assert");
11
11
  //#region src/decimal.ts
@@ -1,11 +1,11 @@
1
1
  import "./url-DMxmp7ZG.mjs";
2
- import "./docloader-gWE7bf9J.mjs";
2
+ import "./docloader-Dh8AenhB.mjs";
3
3
  import "./key-C2Db_TAJ.mjs";
4
4
  import "./multibase-BPnF_L4e.mjs";
5
5
  import "./digest-COC7xDiQ.mjs";
6
6
  import "./langtag-CUppHs_s.mjs";
7
7
  import "./langstr-DYRmJqFn.mjs";
8
- import "./portable-media-DTlmIkgj.mjs";
8
+ import "./portable-media-DG7moKKk.mjs";
9
9
  import { test } from "node:test";
10
10
  import { deepStrictEqual, throws } from "node:assert";
11
11
  //#region src/decimal.ts
@@ -1,5 +1,5 @@
1
- const require_request = require("./request-CAxz3d9M.cjs");
2
- const require_body = require("./body-COsEN5g-.cjs");
1
+ const require_request = require("./request-nWurHb32.cjs");
2
+ const require_body = require("./body-z_6Hg7ci.cjs");
3
3
  const require_contexts = require("./contexts-C9Lj11SC.cjs");
4
4
  const require_link = require("./link-DlKm8bEr.cjs");
5
5
  const require_url = require("./url-BNakuZ8k.cjs");
@@ -291,7 +291,10 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
291
291
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
292
292
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
293
293
  options?.signal?.throwIfAborted();
294
- const currentUrl = new URL(url).href;
294
+ const parsedUrl = new URL(url);
295
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new require_url.UrlError(`Unsupported protocol: ${parsedUrl.protocol}`);
296
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new require_url.UrlError("URLs with credentials are not allowed");
297
+ const currentUrl = parsedUrl.href;
295
298
  if (!skipPreloadedContexts && currentUrl in require_contexts.preloadedContexts) {
296
299
  logger.debug("Using preloaded context: {url}.", { url: currentUrl });
297
300
  return {
@@ -1,5 +1,5 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-C4AssP0b.mjs";
2
- import { i as readBoundedText, n as MAX_BODY_SIZE, r as readBoundedBytes, t as BodyTooLargeError } from "./body-7_djU77X.mjs";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-Cb7B8WAb.mjs";
2
+ import { i as readBoundedText, n as MAX_BODY_SIZE, r as readBoundedBytes, t as BodyTooLargeError } from "./body-CrrDpQuG.mjs";
3
3
  import { t as preloadedContexts } from "./contexts-duRA_dDC.mjs";
4
4
  import { t as HttpHeaderLink } from "./link-Cevmc87v.mjs";
5
5
  import { n as UrlError, x as validatePublicUrl } from "./url-DMxmp7ZG.mjs";
@@ -291,7 +291,10 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
291
291
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
292
292
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
293
293
  options?.signal?.throwIfAborted();
294
- const currentUrl = new URL(url).href;
294
+ const parsedUrl = new URL(url);
295
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsedUrl.protocol}`);
296
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
297
+ const currentUrl = parsedUrl.href;
295
298
  if (!skipPreloadedContexts && currentUrl in preloadedContexts) {
296
299
  logger.debug("Using preloaded context: {url}.", { url: currentUrl });
297
300
  return {
@@ -1,8 +1,8 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_request = require("./request-CAxz3d9M.cjs");
2
+ const require_request = require("./request-nWurHb32.cjs");
3
3
  const require_contexts = require("./contexts-C9Lj11SC.cjs");
4
4
  const require_url = require("./url-BNakuZ8k.cjs");
5
- const require_docloader = require("./docloader-D5WdzhZN.cjs");
5
+ const require_docloader = require("./docloader-DUnO3Uvt.cjs");
6
6
  const require_jsonld = require("./jsonld-CmDhHNTx.cjs");
7
7
  let node_zlib = require("node:zlib");
8
8
  let _logtape_logtape = require("@logtape/logtape");
@@ -2371,4 +2371,98 @@ function isTimeoutFetchError(error, url) {
2371
2371
  })(url), (e) => isTimeoutFetchError(e, url));
2372
2372
  });
2373
2373
  });
2374
+ const unsupportedUrls = [
2375
+ "ftp://example.com/collection",
2376
+ "file:///collection",
2377
+ `data:application/ld+json,${encodeURIComponent(JSON.stringify({ id: "https://example.com/data" }))}`
2378
+ ];
2379
+ function isCredentialUrlError(error) {
2380
+ return error instanceof require_url.UrlError && error.reason === "disallowed" && error.message === "URLs with credentials are not allowed";
2381
+ }
2382
+ (0, _fedify_fixture.test)("getDocumentLoader() rejects URLs with credentials", async (t) => {
2383
+ const start = "https://8.8.8.8/start";
2384
+ const target = "https://user:pass@8.8.8.8/document";
2385
+ for (const allowPrivateAddress of [false, true]) {
2386
+ const loader = require_docloader.getDocumentLoader({ allowPrivateAddress });
2387
+ for (const path of [
2388
+ "direct",
2389
+ "redirect",
2390
+ "Link",
2391
+ "HTML"
2392
+ ]) await t.step(`${path}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
2393
+ await withFetchStub((url) => {
2394
+ if (url !== start) return Response.json({});
2395
+ if (path === "redirect") return Response.redirect(target, 302);
2396
+ if (path === "HTML") return new Response(`<link rel="alternate" type="application/activity+json" href="${target}">`, { headers: { "Content-Type": "text/html" } });
2397
+ return new Response("", { headers: {
2398
+ "Content-Type": "text/plain",
2399
+ Link: `<${target}>; rel="alternate"; type="application/activity+json"`
2400
+ } });
2401
+ }, async (fetched) => {
2402
+ await (0, node_assert.rejects)(loader(path === "direct" ? target : start), isCredentialUrlError);
2403
+ (0, node_assert.deepStrictEqual)(fetched, path === "direct" ? [] : [start]);
2404
+ });
2405
+ });
2406
+ for (const url of [
2407
+ "https://user@8.8.8.8/document",
2408
+ "https://:pass@8.8.8.8/document",
2409
+ "https://%75ser:p%40ss@8.8.8.8/document",
2410
+ "http://user:pass@8.8.8.8/document"
2411
+ ]) await t.step(`${url}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
2412
+ await withFetchStub(() => Response.json({}), async (fetched) => {
2413
+ await (0, node_assert.rejects)(loader(url), isCredentialUrlError);
2414
+ (0, node_assert.deepStrictEqual)(fetched, []);
2415
+ });
2416
+ });
2417
+ for (const url of ["https://:@8.8.8.8/document", "https://8.8.8.8/@user?q=@pass"]) await t.step(`allows ${url}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
2418
+ await withFetchStub(() => Response.json({ done: true }), async (fetched) => {
2419
+ (0, node_assert.deepStrictEqual)((await loader(url)).document, { done: true });
2420
+ (0, node_assert.deepStrictEqual)(fetched, [new URL(url).href]);
2421
+ });
2422
+ });
2423
+ }
2424
+ });
2425
+ function isUnsupportedProtocolError(error, url) {
2426
+ return error instanceof require_url.UrlError && error.message === `Unsupported protocol: ${new URL(url).protocol}` && error.reason === "disallowed";
2427
+ }
2428
+ async function withFetchStub(respond, callback) {
2429
+ const fetched = [];
2430
+ const originalFetch = globalThis.fetch;
2431
+ globalThis.fetch = (input) => {
2432
+ const url = input instanceof Request ? input.url : String(input);
2433
+ fetched.push(url);
2434
+ return Promise.resolve(respond(url));
2435
+ };
2436
+ try {
2437
+ await callback(fetched);
2438
+ } finally {
2439
+ globalThis.fetch = originalFetch;
2440
+ }
2441
+ }
2442
+ for (const url of unsupportedUrls) {
2443
+ const scheme = new URL(url).protocol;
2444
+ (0, _fedify_fixture.test)(`getDocumentLoader() rejects ${scheme} URLs with private addresses allowed before fetching`, async () => {
2445
+ await withFetchStub(() => Response.json({}), async (fetched) => {
2446
+ await (0, node_assert.rejects)(require_docloader.getDocumentLoader({ allowPrivateAddress: true })(url), (e) => isUnsupportedProtocolError(e, url));
2447
+ (0, node_assert.deepStrictEqual)(fetched, []);
2448
+ });
2449
+ });
2450
+ (0, _fedify_fixture.test)(`getDocumentLoader() rejects redirects to ${scheme} URLs with private addresses allowed`, async () => {
2451
+ const start = "https://example.com/redirect-to-unsupported";
2452
+ await withFetchStub(() => Response.redirect(url, 302), async (fetched) => {
2453
+ await (0, node_assert.rejects)(require_docloader.getDocumentLoader({ allowPrivateAddress: true })(start), (e) => isUnsupportedProtocolError(e, url));
2454
+ (0, node_assert.deepStrictEqual)(fetched, [start]);
2455
+ });
2456
+ });
2457
+ for (const html of [false, true]) (0, _fedify_fixture.test)(`getDocumentLoader() rejects ${html ? "HTML" : "Link header"} alternates to ${scheme} URLs with private addresses allowed`, async () => {
2458
+ const start = "https://example.com/alternate-to-unsupported";
2459
+ await withFetchStub(() => html ? new Response(`<link rel="alternate" type="application/activity+json" href="${url}">`, { headers: { "Content-Type": "text/html" } }) : new Response("", { headers: {
2460
+ "Content-Type": "text/plain",
2461
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
2462
+ } }), async (fetched) => {
2463
+ await (0, node_assert.rejects)(require_docloader.getDocumentLoader({ allowPrivateAddress: true })(start), (e) => isUnsupportedProtocolError(e, url));
2464
+ (0, node_assert.deepStrictEqual)(fetched, [start]);
2465
+ });
2466
+ });
2467
+ }
2374
2468
  //#endregion
@@ -1,7 +1,7 @@
1
- import { t as FetchError } from "./request-C4AssP0b.mjs";
1
+ import { t as FetchError } from "./request-Cb7B8WAb.mjs";
2
2
  import { n as cid_v1_default, t as preloadedContexts } from "./contexts-duRA_dDC.mjs";
3
3
  import { n as UrlError } from "./url-DMxmp7ZG.mjs";
4
- import { i as withDocumentLoaderTimeout, n as getRemoteDocument, r as resolveDocumentLoaderTimeout, t as getDocumentLoader } from "./docloader-gWE7bf9J.mjs";
4
+ import { i as withDocumentLoaderTimeout, n as getRemoteDocument, r as resolveDocumentLoaderTimeout, t as getDocumentLoader } from "./docloader-Dh8AenhB.mjs";
5
5
  import { t as jsonld_default } from "./jsonld-DweIJ07b.mjs";
6
6
  import { gzipSync } from "node:zlib";
7
7
  import { configure, reset } from "@logtape/logtape";
@@ -2389,5 +2389,99 @@ test("getDocumentLoader() times out a stalled body of such a status", async () =
2389
2389
  })(url), (e) => isTimeoutFetchError(e, url));
2390
2390
  });
2391
2391
  });
2392
+ const unsupportedUrls = [
2393
+ "ftp://example.com/collection",
2394
+ "file:///collection",
2395
+ `data:application/ld+json,${encodeURIComponent(JSON.stringify({ id: "https://example.com/data" }))}`
2396
+ ];
2397
+ function isCredentialUrlError(error) {
2398
+ return error instanceof UrlError && error.reason === "disallowed" && error.message === "URLs with credentials are not allowed";
2399
+ }
2400
+ test("getDocumentLoader() rejects URLs with credentials", async (t) => {
2401
+ const start = "https://8.8.8.8/start";
2402
+ const target = "https://user:pass@8.8.8.8/document";
2403
+ for (const allowPrivateAddress of [false, true]) {
2404
+ const loader = getDocumentLoader({ allowPrivateAddress });
2405
+ for (const path of [
2406
+ "direct",
2407
+ "redirect",
2408
+ "Link",
2409
+ "HTML"
2410
+ ]) await t.step(`${path}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
2411
+ await withFetchStub((url) => {
2412
+ if (url !== start) return Response.json({});
2413
+ if (path === "redirect") return Response.redirect(target, 302);
2414
+ if (path === "HTML") return new Response(`<link rel="alternate" type="application/activity+json" href="${target}">`, { headers: { "Content-Type": "text/html" } });
2415
+ return new Response("", { headers: {
2416
+ "Content-Type": "text/plain",
2417
+ Link: `<${target}>; rel="alternate"; type="application/activity+json"`
2418
+ } });
2419
+ }, async (fetched) => {
2420
+ await rejects(loader(path === "direct" ? target : start), isCredentialUrlError);
2421
+ deepStrictEqual(fetched, path === "direct" ? [] : [start]);
2422
+ });
2423
+ });
2424
+ for (const url of [
2425
+ "https://user@8.8.8.8/document",
2426
+ "https://:pass@8.8.8.8/document",
2427
+ "https://%75ser:p%40ss@8.8.8.8/document",
2428
+ "http://user:pass@8.8.8.8/document"
2429
+ ]) await t.step(`${url}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
2430
+ await withFetchStub(() => Response.json({}), async (fetched) => {
2431
+ await rejects(loader(url), isCredentialUrlError);
2432
+ deepStrictEqual(fetched, []);
2433
+ });
2434
+ });
2435
+ for (const url of ["https://:@8.8.8.8/document", "https://8.8.8.8/@user?q=@pass"]) await t.step(`allows ${url}, allowPrivateAddress=${allowPrivateAddress}`, async () => {
2436
+ await withFetchStub(() => Response.json({ done: true }), async (fetched) => {
2437
+ deepStrictEqual((await loader(url)).document, { done: true });
2438
+ deepStrictEqual(fetched, [new URL(url).href]);
2439
+ });
2440
+ });
2441
+ }
2442
+ });
2443
+ function isUnsupportedProtocolError(error, url) {
2444
+ return error instanceof UrlError && error.message === `Unsupported protocol: ${new URL(url).protocol}` && error.reason === "disallowed";
2445
+ }
2446
+ async function withFetchStub(respond, callback) {
2447
+ const fetched = [];
2448
+ const originalFetch = globalThis.fetch;
2449
+ globalThis.fetch = (input) => {
2450
+ const url = input instanceof Request ? input.url : String(input);
2451
+ fetched.push(url);
2452
+ return Promise.resolve(respond(url));
2453
+ };
2454
+ try {
2455
+ await callback(fetched);
2456
+ } finally {
2457
+ globalThis.fetch = originalFetch;
2458
+ }
2459
+ }
2460
+ for (const url of unsupportedUrls) {
2461
+ const scheme = new URL(url).protocol;
2462
+ test(`getDocumentLoader() rejects ${scheme} URLs with private addresses allowed before fetching`, async () => {
2463
+ await withFetchStub(() => Response.json({}), async (fetched) => {
2464
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(url), (e) => isUnsupportedProtocolError(e, url));
2465
+ deepStrictEqual(fetched, []);
2466
+ });
2467
+ });
2468
+ test(`getDocumentLoader() rejects redirects to ${scheme} URLs with private addresses allowed`, async () => {
2469
+ const start = "https://example.com/redirect-to-unsupported";
2470
+ await withFetchStub(() => Response.redirect(url, 302), async (fetched) => {
2471
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(start), (e) => isUnsupportedProtocolError(e, url));
2472
+ deepStrictEqual(fetched, [start]);
2473
+ });
2474
+ });
2475
+ for (const html of [false, true]) test(`getDocumentLoader() rejects ${html ? "HTML" : "Link header"} alternates to ${scheme} URLs with private addresses allowed`, async () => {
2476
+ const start = "https://example.com/alternate-to-unsupported";
2477
+ await withFetchStub(() => html ? new Response(`<link rel="alternate" type="application/activity+json" href="${url}">`, { headers: { "Content-Type": "text/html" } }) : new Response("", { headers: {
2478
+ "Content-Type": "text/plain",
2479
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
2480
+ } }), async (fetched) => {
2481
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(start), (e) => isUnsupportedProtocolError(e, url));
2482
+ deepStrictEqual(fetched, [start]);
2483
+ });
2484
+ });
2485
+ }
2392
2486
  //#endregion
2393
2487
  export {};
@@ -1,5 +1,5 @@
1
- const require_request = require("./request-CAxz3d9M.cjs");
2
- const require_body = require("./body-COsEN5g-.cjs");
1
+ const require_request = require("./request-nWurHb32.cjs");
2
+ const require_body = require("./body-z_6Hg7ci.cjs");
3
3
  const require_url = require("./url-BNakuZ8k.cjs");
4
4
  const require_digest = require("./digest-3FeH2Y-Q.cjs");
5
5
  //#region src/portable-media.ts
@@ -1,5 +1,5 @@
1
- import { r as getUserAgent } from "./request-C4AssP0b.mjs";
2
- import { r as readBoundedBytes } from "./body-7_djU77X.mjs";
1
+ import { r as getUserAgent } from "./request-Cb7B8WAb.mjs";
2
+ import { r as readBoundedBytes } from "./body-CrrDpQuG.mjs";
3
3
  import { g as parseGatewayUrl, x as validatePublicUrl } from "./url-DMxmp7ZG.mjs";
4
4
  import { a as verifyDigestMultibase, i as parseHashlink, r as parseDigestMultibase } from "./digest-COC7xDiQ.mjs";
5
5
  //#region src/portable-media.ts
@@ -1,5 +1,5 @@
1
1
  const require_digest = require("./digest-3FeH2Y-Q.cjs");
2
- const require_portable_media = require("./portable-media-Dh0NSS49.cjs");
2
+ const require_portable_media = require("./portable-media-DBl1-UE5.cjs");
3
3
  let node_assert_strict = require("node:assert/strict");
4
4
  let node_test = require("node:test");
5
5
  let node_http = require("node:http");
@@ -1,5 +1,5 @@
1
1
  import { n as createHashlink, t as computeDigestMultibase } from "./digest-COC7xDiQ.mjs";
2
- import { t as fetchPortableMedia } from "./portable-media-DTlmIkgj.mjs";
2
+ import { t as fetchPortableMedia } from "./portable-media-DG7moKKk.mjs";
3
3
  import { deepStrictEqual, equal, rejects } from "node:assert/strict";
4
4
  import { test } from "node:test";
5
5
  import { createServer } from "node:http";
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  //#region deno.json
3
3
  var name = "@fedify/vocab-runtime";
4
- var version = "2.4.2";
4
+ var version = "2.4.4";
5
5
  //#endregion
6
6
  //#region src/request.ts
7
7
  /**
@@ -3,7 +3,7 @@ let node_process = require("node:process");
3
3
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
4
4
  //#region deno.json
5
5
  var name = "@fedify/vocab-runtime";
6
- var version = "2.4.2";
6
+ var version = "2.4.4";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_request = require("./request-CAxz3d9M.cjs");
2
+ const require_request = require("./request-nWurHb32.cjs");
3
3
  let node_test = require("node:test");
4
4
  let node_process = require("node:process");
5
5
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
@@ -1,4 +1,4 @@
1
- import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-C4AssP0b.mjs";
1
+ import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-Cb7B8WAb.mjs";
2
2
  import { test } from "node:test";
3
3
  import process from "node:process";
4
4
  import { deepStrictEqual } from "node:assert";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1612,3 +1612,177 @@ test("getDocumentLoader() times out a stalled body of such a status", async () =
1612
1612
  await rejects(loader(url), (e) => isTimeoutFetchError(e, url));
1613
1613
  });
1614
1614
  });
1615
+
1616
+ const unsupportedUrls = [
1617
+ "ftp://example.com/collection",
1618
+ "file:///collection",
1619
+ `data:application/ld+json,${
1620
+ encodeURIComponent(JSON.stringify({ id: "https://example.com/data" }))
1621
+ }`,
1622
+ ];
1623
+
1624
+ function isCredentialUrlError(error: unknown): boolean {
1625
+ return error instanceof UrlError && error.reason === "disallowed" &&
1626
+ error.message === "URLs with credentials are not allowed";
1627
+ }
1628
+
1629
+ test("getDocumentLoader() rejects URLs with credentials", async (t) => {
1630
+ const start = "https://8.8.8.8/start";
1631
+ const target = "https://user:pass@8.8.8.8/document";
1632
+ for (const allowPrivateAddress of [false, true]) {
1633
+ const loader = getDocumentLoader({ allowPrivateAddress });
1634
+ for (const path of ["direct", "redirect", "Link", "HTML"]) {
1635
+ await t.step(
1636
+ `${path}, allowPrivateAddress=${allowPrivateAddress}`,
1637
+ async () => {
1638
+ await withFetchStub(
1639
+ (url) => {
1640
+ if (url !== start) return Response.json({});
1641
+ if (path === "redirect") return Response.redirect(target, 302);
1642
+ if (path === "HTML") {
1643
+ return new Response(
1644
+ `<link rel="alternate" type="application/activity+json" href="${target}">`,
1645
+ { headers: { "Content-Type": "text/html" } },
1646
+ );
1647
+ }
1648
+ return new Response("", {
1649
+ headers: {
1650
+ "Content-Type": "text/plain",
1651
+ Link:
1652
+ `<${target}>; rel="alternate"; type="application/activity+json"`,
1653
+ },
1654
+ });
1655
+ },
1656
+ async (fetched) => {
1657
+ await rejects(
1658
+ loader(path === "direct" ? target : start),
1659
+ isCredentialUrlError,
1660
+ );
1661
+ deepStrictEqual(fetched, path === "direct" ? [] : [start]);
1662
+ },
1663
+ );
1664
+ },
1665
+ );
1666
+ }
1667
+ for (
1668
+ const url of [
1669
+ "https://user@8.8.8.8/document",
1670
+ "https://:pass@8.8.8.8/document",
1671
+ "https://%75ser:p%40ss@8.8.8.8/document",
1672
+ "http://user:pass@8.8.8.8/document",
1673
+ ]
1674
+ ) {
1675
+ await t.step(
1676
+ `${url}, allowPrivateAddress=${allowPrivateAddress}`,
1677
+ async () => {
1678
+ await withFetchStub(() => Response.json({}), async (fetched) => {
1679
+ await rejects(loader(url), isCredentialUrlError);
1680
+ deepStrictEqual(fetched, []);
1681
+ });
1682
+ },
1683
+ );
1684
+ }
1685
+ for (
1686
+ const url of [
1687
+ "https://:@8.8.8.8/document",
1688
+ "https://8.8.8.8/@user?q=@pass",
1689
+ ]
1690
+ ) {
1691
+ await t.step(
1692
+ `allows ${url}, allowPrivateAddress=${allowPrivateAddress}`,
1693
+ async () => {
1694
+ await withFetchStub(
1695
+ () => Response.json({ done: true }),
1696
+ async (fetched) => {
1697
+ const document = await loader(url);
1698
+ deepStrictEqual(document.document, { done: true });
1699
+ deepStrictEqual(fetched, [new URL(url).href]);
1700
+ },
1701
+ );
1702
+ },
1703
+ );
1704
+ }
1705
+ }
1706
+ });
1707
+
1708
+ function isUnsupportedProtocolError(error: unknown, url: string): boolean {
1709
+ return error instanceof UrlError &&
1710
+ error.message === `Unsupported protocol: ${new URL(url).protocol}` &&
1711
+ error.reason === "disallowed";
1712
+ }
1713
+
1714
+ async function withFetchStub(
1715
+ respond: (url: string) => Response,
1716
+ callback: (fetched: string[]) => Promise<void>,
1717
+ ): Promise<void> {
1718
+ const fetched: string[] = [];
1719
+ const originalFetch = globalThis.fetch;
1720
+ globalThis.fetch = (input: RequestInfo | URL) => {
1721
+ const url = input instanceof Request ? input.url : String(input);
1722
+ fetched.push(url);
1723
+ return Promise.resolve(respond(url));
1724
+ };
1725
+ try {
1726
+ await callback(fetched);
1727
+ } finally {
1728
+ globalThis.fetch = originalFetch;
1729
+ }
1730
+ }
1731
+
1732
+ for (const url of unsupportedUrls) {
1733
+ const scheme = new URL(url).protocol;
1734
+
1735
+ test(`getDocumentLoader() rejects ${scheme} URLs with private addresses allowed before fetching`, async () => {
1736
+ await withFetchStub(() => Response.json({}), async (fetched) => {
1737
+ await rejects(
1738
+ getDocumentLoader({ allowPrivateAddress: true })(url),
1739
+ (e) => isUnsupportedProtocolError(e, url),
1740
+ );
1741
+ deepStrictEqual(fetched, []);
1742
+ });
1743
+ });
1744
+
1745
+ test(`getDocumentLoader() rejects redirects to ${scheme} URLs with private addresses allowed`, async () => {
1746
+ const start = "https://example.com/redirect-to-unsupported";
1747
+ await withFetchStub(() => Response.redirect(url, 302), async (fetched) => {
1748
+ await rejects(
1749
+ getDocumentLoader({ allowPrivateAddress: true })(start),
1750
+ (e) => isUnsupportedProtocolError(e, url),
1751
+ );
1752
+ deepStrictEqual(fetched, [start]);
1753
+ });
1754
+ });
1755
+
1756
+ for (const html of [false, true]) {
1757
+ test(
1758
+ `getDocumentLoader() rejects ${
1759
+ html ? "HTML" : "Link header"
1760
+ } alternates to ${scheme} URLs with private addresses allowed`,
1761
+ async () => {
1762
+ const start = "https://example.com/alternate-to-unsupported";
1763
+ await withFetchStub(
1764
+ () =>
1765
+ html
1766
+ ? new Response(
1767
+ `<link rel="alternate" type="application/activity+json" href="${url}">`,
1768
+ { headers: { "Content-Type": "text/html" } },
1769
+ )
1770
+ : new Response("", {
1771
+ headers: {
1772
+ "Content-Type": "text/plain",
1773
+ Link:
1774
+ `<${url}>; rel="alternate"; type="application/activity+json"`,
1775
+ },
1776
+ }),
1777
+ async (fetched) => {
1778
+ await rejects(
1779
+ getDocumentLoader({ allowPrivateAddress: true })(start),
1780
+ (e) => isUnsupportedProtocolError(e, url),
1781
+ );
1782
+ deepStrictEqual(fetched, [start]);
1783
+ },
1784
+ );
1785
+ },
1786
+ );
1787
+ }
1788
+ }
package/src/docloader.ts CHANGED
@@ -557,7 +557,16 @@ export function getDocumentLoader(
557
557
  visited = new Set<string>(),
558
558
  ): Promise<RemoteDocument> {
559
559
  options?.signal?.throwIfAborted();
560
- const currentUrl = new URL(url).href;
560
+ const parsedUrl = new URL(url);
561
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
562
+ throw new UrlError(`Unsupported protocol: ${parsedUrl.protocol}`);
563
+ }
564
+ // When merging forward, keep this guard independent of allowPrivateAddress:
565
+ // private-address opt-in must not permit credentials on any followed URL.
566
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") {
567
+ throw new UrlError("URLs with credentials are not allowed");
568
+ }
569
+ const currentUrl = parsedUrl.href;
561
570
  if (!skipPreloadedContexts && currentUrl in preloadedContexts) {
562
571
  logger.debug("Using preloaded context: {url}.", { url: currentUrl });
563
572
  return {