@fedify/webfinger 2.0.35-dev.2480 → 2.0.35

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.
@@ -23,11 +23,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  let _fedify_fixture = require("@fedify/fixture");
25
25
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
26
+ let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
26
27
  let _logtape_logtape = require("@logtape/logtape");
27
28
  let es_toolkit = require("es-toolkit");
28
29
  let node_assert_strict = require("node:assert/strict");
29
- let node_dns_promises = require("node:dns/promises");
30
- node_dns_promises = __toESM(node_dns_promises);
31
30
  let _opentelemetry_api = require("@opentelemetry/api");
32
31
  //#endregion
33
32
  //#region ../../node_modules/.pnpm/regexparam@3.0.0/node_modules/regexparam/dist/index.mjs
@@ -1230,7 +1229,7 @@ var esm_default = new class FetchMock {
1230
1229
  //#endregion
1231
1230
  //#region deno.json
1232
1231
  var name = "@fedify/webfinger";
1233
- var version = "2.0.35-dev.2480+2330a2e7";
1232
+ var version = "2.0.35";
1234
1233
  //#endregion
1235
1234
  //#region src/body.ts
1236
1235
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1306,7 +1305,7 @@ async function readBoundedText(message, maxBytes, url) {
1306
1305
  }
1307
1306
  }
1308
1307
  //#endregion
1309
- //#region src/lookup.ts
1308
+ //#region src/lookup-impl.ts
1310
1309
  const logger = (0, _logtape_logtape.getLogger)([
1311
1310
  "fedify",
1312
1311
  "webfinger",
@@ -1314,36 +1313,16 @@ const logger = (0, _logtape_logtape.getLogger)([
1314
1313
  ]);
1315
1314
  const DEFAULT_MAX_REDIRECTION = 5;
1316
1315
  /**
1317
- * Looks up a WebFinger resource.
1316
+ * Looks up a WebFinger resource, opening a fetch session per hop. This is
1317
+ * the implementation of {@link lookupWebFinger}, which is not exported from
1318
+ * the package entry point.
1318
1319
  * @param resource The resource URL to look up.
1319
1320
  * @param options Extra options for looking up the resource.
1321
+ * @param deps The dependencies.
1320
1322
  * @returns The resource descriptor, or `null` if not found.
1321
- * @since 0.2.0
1323
+ * @internal
1322
1324
  */
1323
- async function lookupWebFinger(resource, options = {}) {
1324
- return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("webfinger.lookup", {
1325
- kind: _opentelemetry_api.SpanKind.CLIENT,
1326
- attributes: {
1327
- "webfinger.resource": resource.toString(),
1328
- "webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
1329
- }
1330
- }, async (span) => {
1331
- try {
1332
- const result = await lookupWebFingerInternal(resource, options);
1333
- span.setStatus({ code: result === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
1334
- return result;
1335
- } catch (error) {
1336
- span.setStatus({
1337
- code: _opentelemetry_api.SpanStatusCode.ERROR,
1338
- message: String(error)
1339
- });
1340
- throw error;
1341
- } finally {
1342
- span.end();
1343
- }
1344
- });
1345
- }
1346
- async function lookupWebFingerInternal(resource, options = {}) {
1325
+ async function lookupWebFingerInternal(resource, options = {}, deps = {}) {
1347
1326
  if (typeof resource === "string") resource = new URL(resource);
1348
1327
  let protocol = "https:";
1349
1328
  let server;
@@ -1358,79 +1337,169 @@ async function lookupWebFingerInternal(resource, options = {}) {
1358
1337
  }
1359
1338
  let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
1360
1339
  url.searchParams.set("resource", resource.href);
1340
+ const openHop = deps.openHop ?? ((hopUrl) => options.allowPrivateAddress === true ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(hopUrl) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(hopUrl, { signal: options.signal }));
1361
1341
  let redirected = 0;
1362
1342
  while (true) {
1363
1343
  logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
1364
- let response;
1365
- if (options.allowPrivateAddress !== true) try {
1366
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url.href);
1367
- } catch (e) {
1368
- if (e instanceof _fedify_vocab_runtime.UrlError) {
1369
- if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
1370
- url: url.href,
1371
- error: e
1372
- });
1373
- else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
1374
- return null;
1375
- }
1376
- throw e;
1377
- }
1344
+ const session = await openSession(url, openHop, options.signal);
1345
+ if (session == null) return null;
1346
+ let hop;
1378
1347
  try {
1379
- response = await fetch(url, {
1380
- headers: {
1381
- Accept: "application/jrd+json",
1382
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
1383
- },
1384
- redirect: "manual",
1385
- signal: options.signal
1348
+ hop = await fetchHop(session, url, options);
1349
+ } finally {
1350
+ await session.close();
1351
+ }
1352
+ if ("result" in hop) return hop.result;
1353
+ redirected++;
1354
+ const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1355
+ if (redirected >= maxRedirection) {
1356
+ logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
1357
+ return null;
1358
+ }
1359
+ const redirectedUrl = new URL(hop.redirect, url);
1360
+ if (redirectedUrl.protocol !== url.protocol) {
1361
+ logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
1362
+ protocol: url.protocol,
1363
+ redirectedProtocol: redirectedUrl.protocol
1386
1364
  });
1387
- } catch (error) {
1388
- logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
1365
+ return null;
1366
+ }
1367
+ url = redirectedUrl;
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Opens a fetch session for a hop. A disallowed URL, a failed DNS lookup,
1372
+ * or an aborted lookup yields `null` instead of an error.
1373
+ */
1374
+ async function openSession(url, openHop, signal) {
1375
+ try {
1376
+ return await openHop(url.href);
1377
+ } catch (e) {
1378
+ if (e instanceof _fedify_vocab_runtime.UrlError) {
1379
+ if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
1389
1380
  url: url.href,
1390
- error
1381
+ error: e
1391
1382
  });
1383
+ else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
1392
1384
  return null;
1393
1385
  }
1394
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1395
- redirected++;
1396
- const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1397
- if (redirected >= maxRedirection) {
1398
- logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
1399
- return null;
1400
- }
1401
- const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
1402
- if (redirectedUrl.protocol !== url.protocol) {
1403
- logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
1404
- protocol: url.protocol,
1405
- redirectedProtocol: redirectedUrl.protocol
1406
- });
1407
- return null;
1408
- }
1409
- url = redirectedUrl;
1410
- continue;
1411
- }
1412
- if (!response.ok) {
1413
- logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
1386
+ if (signal?.aborted) {
1387
+ logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
1414
1388
  url: url.href,
1415
- status: response.status,
1416
- statusText: response.statusText
1389
+ error: e
1417
1390
  });
1418
1391
  return null;
1419
1392
  }
1420
- try {
1421
- return JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url));
1422
- } catch (e) {
1423
- if (e instanceof BodyTooLargeError) return null;
1424
- if (e instanceof SyntaxError) {
1425
- logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
1426
- return null;
1427
- }
1428
- throw e;
1393
+ throw e;
1394
+ }
1395
+ }
1396
+ /**
1397
+ * Fetches a hop with the given session, and consumes the response body or
1398
+ * starts cancelling it before returning. The caller closes the session.
1399
+ */
1400
+ async function fetchHop(session, url, options) {
1401
+ let response;
1402
+ try {
1403
+ response = await session.fetch(url, {
1404
+ headers: {
1405
+ Accept: "application/jrd+json",
1406
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
1407
+ },
1408
+ redirect: "manual",
1409
+ signal: options.signal
1410
+ });
1411
+ } catch (error) {
1412
+ logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
1413
+ url: url.href,
1414
+ error
1415
+ });
1416
+ return { result: null };
1417
+ }
1418
+ if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1419
+ cancelBody(response, url);
1420
+ return { redirect: response.headers.get("Location") };
1421
+ }
1422
+ if (!response.ok) {
1423
+ logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
1424
+ url: url.href,
1425
+ status: response.status,
1426
+ statusText: response.statusText
1427
+ });
1428
+ cancelBody(response, url);
1429
+ return { result: null };
1430
+ }
1431
+ try {
1432
+ return { result: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)) };
1433
+ } catch (e) {
1434
+ if (e instanceof BodyTooLargeError) return { result: null };
1435
+ if (e instanceof SyntaxError) {
1436
+ logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
1437
+ return { result: null };
1429
1438
  }
1439
+ throw e;
1430
1440
  }
1431
1441
  }
1442
+ /**
1443
+ * Starts cancelling a response body that is not read, without waiting for it
1444
+ * to settle. The cancellation of a branch of a tee (e.g., a body cloned by
1445
+ * a fetch mock) settles only after the other branch is cancelled too, per
1446
+ * the Streams standard, so awaiting it could block forever. A failure is
1447
+ * only logged.
1448
+ */
1449
+ function cancelBody(response, url) {
1450
+ response.body?.cancel().catch((error) => {
1451
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
1452
+ url: url.href,
1453
+ error
1454
+ });
1455
+ });
1456
+ }
1457
+ //#endregion
1458
+ //#region src/lookup.ts
1459
+ /**
1460
+ * Looks up a WebFinger resource.
1461
+ * @param resource The resource URL to look up.
1462
+ * @param options Extra options for looking up the resource.
1463
+ * @returns The resource descriptor, or `null` if not found.
1464
+ * @since 0.2.0
1465
+ */
1466
+ async function lookupWebFinger(resource, options = {}) {
1467
+ return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("webfinger.lookup", {
1468
+ kind: _opentelemetry_api.SpanKind.CLIENT,
1469
+ attributes: {
1470
+ "webfinger.resource": resource.toString(),
1471
+ "webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
1472
+ }
1473
+ }, async (span) => {
1474
+ try {
1475
+ const result = await lookupWebFingerInternal(resource, options);
1476
+ span.setStatus({ code: result === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
1477
+ return result;
1478
+ } catch (error) {
1479
+ span.setStatus({
1480
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
1481
+ message: String(error)
1482
+ });
1483
+ throw error;
1484
+ } finally {
1485
+ span.end();
1486
+ }
1487
+ });
1488
+ }
1432
1489
  //#endregion
1433
1490
  //#region src/lookup.test.ts
1491
+ function lookupDirect(resource, options) {
1492
+ return lookupWebFingerInternal(resource, options, { openHop: _fedify_vocab_runtime_internal.openDirectFetch });
1493
+ }
1494
+ function lookupResolving(addresses) {
1495
+ return () => Promise.resolve(addresses);
1496
+ }
1497
+ function openExampleDirectly(url) {
1498
+ return new URL(url).hostname === "example.com" ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(url) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup: lookupResolving([{
1499
+ address: "93.184.215.14",
1500
+ family: 4
1501
+ }]) });
1502
+ }
1434
1503
  (0, _fedify_fixture.test)({
1435
1504
  name: "lookupWebFinger()",
1436
1505
  sanitizeOps: false,
@@ -1446,11 +1515,31 @@ async function lookupWebFingerInternal(resource, options = {}) {
1446
1515
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@fedify-test.internal"), null);
1447
1516
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://fedify-test.internal/foo"), null);
1448
1517
  });
1518
+ await t.step("fedify-test.internal resolving to a private address", async () => {
1519
+ esm_default.mockGlobal();
1520
+ esm_default.catch(500);
1521
+ try {
1522
+ const hosts = [];
1523
+ const openHop = (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup: (hostname) => {
1524
+ hosts.push(hostname);
1525
+ return Promise.resolve([{
1526
+ address: "10.0.0.1",
1527
+ family: 4
1528
+ }]);
1529
+ } });
1530
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@fedify-test.internal", {}, { openHop }), null);
1531
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("https://fedify-test.internal/foo", {}, { openHop }), null);
1532
+ (0, node_assert_strict.deepStrictEqual)(hosts, ["fedify-test.internal", "fedify-test.internal"]);
1533
+ (0, node_assert_strict.deepStrictEqual)(esm_default.callHistory.calls().length, 0);
1534
+ } finally {
1535
+ esm_default.hardReset();
1536
+ }
1537
+ });
1449
1538
  esm_default.spyGlobal();
1450
1539
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1451
1540
  await t.step("not found", async () => {
1452
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1453
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), null);
1541
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), null);
1542
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("https://example.com/foo"), null);
1454
1543
  });
1455
1544
  const expected = {
1456
1545
  subject: "acct:johndoe@example.com",
@@ -1459,7 +1548,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1459
1548
  esm_default.removeRoutes();
1460
1549
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1461
1550
  await t.step("acct", async () => {
1462
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1551
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), expected);
1463
1552
  });
1464
1553
  const expected2 = {
1465
1554
  subject: "https://example.com/foo",
@@ -1468,12 +1557,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
1468
1557
  esm_default.removeRoutes();
1469
1558
  esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
1470
1559
  await t.step("https", async () => {
1471
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), expected2);
1560
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("https://example.com/foo"), expected2);
1472
1561
  });
1473
1562
  esm_default.removeRoutes();
1474
1563
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1475
1564
  await t.step("invalid response", async () => {
1476
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1565
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), null);
1477
1566
  });
1478
1567
  esm_default.removeRoutes();
1479
1568
  esm_default.get("begin:https://localhost/.well-known/webfinger?", {
@@ -1502,7 +1591,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1502
1591
  });
1503
1592
  esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
1504
1593
  await t.step("redirection", async () => {
1505
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1594
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), expected);
1506
1595
  });
1507
1596
  esm_default.removeRoutes();
1508
1597
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1510,7 +1599,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1510
1599
  headers: { Location: "/.well-known/webfinger" }
1511
1600
  });
1512
1601
  await t.step("infinite redirection", async () => {
1513
- (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com"), 2e3), null);
1602
+ (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupDirect("acct:johndoe@example.com"), 2e3), null);
1514
1603
  });
1515
1604
  esm_default.removeRoutes();
1516
1605
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1518,7 +1607,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1518
1607
  headers: { Location: "ftp://example.com/" }
1519
1608
  });
1520
1609
  await t.step("redirection to different protocol", async () => {
1521
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1610
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), null);
1522
1611
  });
1523
1612
  esm_default.removeRoutes();
1524
1613
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1526,7 +1615,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1526
1615
  headers: { Location: "https://localhost/" }
1527
1616
  });
1528
1617
  await t.step("redirection to private address", async () => {
1529
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1618
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: openExampleDirectly }), null);
1530
1619
  });
1531
1620
  esm_default.removeRoutes();
1532
1621
  let redirectCount = 0;
@@ -1540,11 +1629,11 @@ async function lookupWebFingerInternal(resource, options = {}) {
1540
1629
  });
1541
1630
  await t.step("custom maxRedirection", async () => {
1542
1631
  redirectCount = 0;
1543
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 2 }), null);
1632
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { maxRedirection: 2 }), null);
1544
1633
  redirectCount = 0;
1545
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1634
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1546
1635
  redirectCount = 0;
1547
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1636
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), expected);
1548
1637
  });
1549
1638
  esm_default.removeRoutes();
1550
1639
  esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
@@ -1555,7 +1644,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1555
1644
  }));
1556
1645
  await t.step("request cancellation", async () => {
1557
1646
  const controller = new AbortController();
1558
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1647
+ const promise = lookupDirect("acct:johndoe@example.com", { signal: controller.signal });
1559
1648
  controller.abort();
1560
1649
  (0, node_assert_strict.deepStrictEqual)(await promise, null);
1561
1650
  });
@@ -1576,7 +1665,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1576
1665
  });
1577
1666
  await t.step("cancellation during redirection", async () => {
1578
1667
  const controller = new AbortController();
1579
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1668
+ const promise = lookupDirect("acct:johndoe@example.com", { signal: controller.signal });
1580
1669
  setTimeout(() => controller.abort(), 100);
1581
1670
  (0, node_assert_strict.deepStrictEqual)(await promise, null);
1582
1671
  });
@@ -1590,12 +1679,13 @@ async function lookupWebFingerInternal(resource, options = {}) {
1590
1679
  await t.step("cancellation with immediate abort", async () => {
1591
1680
  const controller = new AbortController();
1592
1681
  controller.abort();
1682
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { signal: controller.signal }), null);
1593
1683
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal }), null);
1594
1684
  });
1595
1685
  esm_default.removeRoutes();
1596
1686
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
1597
1687
  await t.step("successful request with signal", async () => {
1598
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
1688
+ (0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
1599
1689
  });
1600
1690
  esm_default.hardReset();
1601
1691
  }
@@ -1616,17 +1706,15 @@ async function lookupWebFingerInternal(resource, options = {}) {
1616
1706
  }
1617
1707
  });
1618
1708
  (0, _fedify_fixture.test)("lookupWebFinger() logs DNS failures as such", async (t) => {
1619
- if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") return;
1620
1709
  for (const result of [
1621
1710
  "throws",
1622
1711
  "empty",
1623
1712
  "private"
1624
1713
  ]) await t.step(result, async () => {
1625
- const originalLookup = node_dns_promises.default.lookup;
1626
- node_dns_promises.default.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
1714
+ const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
1627
1715
  address: "127.0.0.1",
1628
1716
  family: 4
1629
- }]));
1717
+ }]);
1630
1718
  const records = [];
1631
1719
  await (0, _logtape_logtape.configure)({
1632
1720
  sinks: { buffer: (record) => records.push(record) },
@@ -1641,7 +1729,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1641
1729
  reset: true
1642
1730
  });
1643
1731
  try {
1644
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:alice@dns-failure.invalid"), null);
1732
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:alice@dns-failure.invalid", {}, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
1645
1733
  const failures = records.filter((r) => r.rawMessage !== "Fetching WebFinger resource descriptor from {url}...");
1646
1734
  (0, node_assert_strict.deepStrictEqual)(failures.map((r) => [r.level, r.rawMessage]), [result === "private" ? ["error", "Invalid URL for WebFinger resource descriptor: {error}"] : ["debug", "DNS lookup failed for {url}"]]);
1647
1735
  const { error } = failures[0].properties;
@@ -1649,8 +1737,248 @@ async function lookupWebFingerInternal(resource, options = {}) {
1649
1737
  (0, node_assert_strict.deepStrictEqual)(error.reason, result === "private" ? "disallowed" : "dns");
1650
1738
  } finally {
1651
1739
  await (0, _logtape_logtape.reset)();
1652
- node_dns_promises.default.lookup = originalLookup;
1653
1740
  }
1654
1741
  });
1655
1742
  });
1743
+ function recordHops(respond) {
1744
+ const events = [];
1745
+ const unsettled = [];
1746
+ let opens = 0;
1747
+ let closes = 0;
1748
+ let responses = 0;
1749
+ return {
1750
+ events,
1751
+ async openHop(url) {
1752
+ opens++;
1753
+ events.push(`open ${url}`);
1754
+ const states = [];
1755
+ let closed = false;
1756
+ return {
1757
+ url: new URL(url),
1758
+ async fetch(input) {
1759
+ if (closed) throw new TypeError("The fetch session is closed.");
1760
+ const target = new URL(input instanceof Request ? input.url : input);
1761
+ const id = ++responses;
1762
+ events.push(`fetch ${target.href} #${id}`);
1763
+ const fake = respond(target);
1764
+ const state = {
1765
+ id,
1766
+ settled: false
1767
+ };
1768
+ states.push(state);
1769
+ let sent = false;
1770
+ const body = new ReadableStream({
1771
+ pull(controller) {
1772
+ if (!sent && fake.body != null && fake.body !== "") {
1773
+ sent = true;
1774
+ controller.enqueue(new TextEncoder().encode(fake.body));
1775
+ return;
1776
+ }
1777
+ state.settled = true;
1778
+ events.push(`consumed #${id}`);
1779
+ controller.close();
1780
+ },
1781
+ cancel() {
1782
+ state.settled = true;
1783
+ events.push(`cancel #${id}`);
1784
+ if (fake.cancelFails) throw new Error("Cancellation failed");
1785
+ if (fake.cancelNeverSettles) return new Promise(() => {});
1786
+ }
1787
+ }, { highWaterMark: 0 });
1788
+ const response = new Response(body, {
1789
+ status: fake.status ?? 200,
1790
+ headers: fake.headers
1791
+ });
1792
+ if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
1793
+ return response;
1794
+ },
1795
+ async close() {
1796
+ if (closed) return;
1797
+ closed = true;
1798
+ closes++;
1799
+ for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
1800
+ events.push(`close ${url}`);
1801
+ }
1802
+ };
1803
+ },
1804
+ assertInvariants() {
1805
+ (0, node_assert_strict.deepStrictEqual)(closes, opens);
1806
+ (0, node_assert_strict.deepStrictEqual)(unsettled, []);
1807
+ }
1808
+ };
1809
+ }
1810
+ (0, _fedify_fixture.test)("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
1811
+ const descriptor = {
1812
+ subject: "acct:johndoe@example.com",
1813
+ links: []
1814
+ };
1815
+ const first = "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
1816
+ const second = "https://example.com/.well-known/webfinger2";
1817
+ await t.step("redirect", async () => {
1818
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
1819
+ status: 302,
1820
+ headers: { Location: "/.well-known/webfinger2" },
1821
+ body: "redirecting",
1822
+ url: url.href.replace("example.com", "93.184.215.14")
1823
+ } : { body: JSON.stringify(descriptor) });
1824
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
1825
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
1826
+ `open ${first}`,
1827
+ `fetch ${first} #1`,
1828
+ "cancel #1",
1829
+ `close ${first}`,
1830
+ `open ${second}`,
1831
+ `fetch ${second} #2`,
1832
+ "consumed #2",
1833
+ `close ${second}`
1834
+ ]);
1835
+ recorder.assertInvariants();
1836
+ });
1837
+ await t.step("non-2xx response", async () => {
1838
+ const recorder = recordHops(() => ({
1839
+ status: 404,
1840
+ body: "not found"
1841
+ }));
1842
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), null);
1843
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
1844
+ `open ${first}`,
1845
+ `fetch ${first} #1`,
1846
+ "cancel #1",
1847
+ `close ${first}`
1848
+ ]);
1849
+ recorder.assertInvariants();
1850
+ });
1851
+ await t.step("too many redirections", async () => {
1852
+ const recorder = recordHops(() => ({
1853
+ status: 302,
1854
+ headers: { Location: "/.well-known/webfinger2" },
1855
+ body: "redirecting"
1856
+ }));
1857
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", { maxRedirection: 2 }, { openHop: recorder.openHop }), null);
1858
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
1859
+ `open ${first}`,
1860
+ `fetch ${first} #1`,
1861
+ "cancel #1",
1862
+ `close ${first}`,
1863
+ `open ${second}`,
1864
+ `fetch ${second} #2`,
1865
+ "cancel #2",
1866
+ `close ${second}`
1867
+ ]);
1868
+ recorder.assertInvariants();
1869
+ });
1870
+ await t.step("failed redirect body cancellation", async () => {
1871
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
1872
+ status: 302,
1873
+ headers: { Location: "/.well-known/webfinger2" },
1874
+ body: "redirecting",
1875
+ cancelFails: true
1876
+ } : { body: JSON.stringify(descriptor) });
1877
+ const records = [];
1878
+ await (0, _logtape_logtape.configure)({
1879
+ sinks: { buffer: (record) => records.push(record) },
1880
+ loggers: [{
1881
+ category: "fedify",
1882
+ sinks: ["buffer"],
1883
+ lowestLevel: "debug"
1884
+ }, {
1885
+ category: ["logtape", "meta"],
1886
+ sinks: []
1887
+ }],
1888
+ reset: true
1889
+ });
1890
+ try {
1891
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
1892
+ const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
1893
+ (0, node_assert_strict.deepStrictEqual)(failures.map((r) => [r.level, r.properties.url]), [["debug", first]]);
1894
+ const { error } = failures[0].properties;
1895
+ (0, node_assert_strict.ok)(error instanceof Error);
1896
+ (0, node_assert_strict.deepStrictEqual)(error.message, "Cancellation failed");
1897
+ } finally {
1898
+ await (0, _logtape_logtape.reset)();
1899
+ }
1900
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
1901
+ `open ${first}`,
1902
+ `fetch ${first} #1`,
1903
+ "cancel #1",
1904
+ `close ${first}`,
1905
+ `open ${second}`,
1906
+ `fetch ${second} #2`,
1907
+ "consumed #2",
1908
+ `close ${second}`
1909
+ ]);
1910
+ recorder.assertInvariants();
1911
+ });
1912
+ await t.step("never-settling body cancellation", async () => {
1913
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
1914
+ status: 302,
1915
+ headers: { Location: "/.well-known/webfinger2" },
1916
+ body: "redirecting",
1917
+ cancelNeverSettles: true
1918
+ } : {
1919
+ status: 404,
1920
+ body: "not found",
1921
+ cancelNeverSettles: true
1922
+ });
1923
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop })), null);
1924
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
1925
+ `open ${first}`,
1926
+ `fetch ${first} #1`,
1927
+ "cancel #1",
1928
+ `close ${first}`,
1929
+ `open ${second}`,
1930
+ `fetch ${second} #2`,
1931
+ "cancel #2",
1932
+ `close ${second}`
1933
+ ]);
1934
+ recorder.assertInvariants();
1935
+ });
1936
+ });
1937
+ /**
1938
+ * Rejects if the given promise does not settle within the timeout, so that
1939
+ * a hang fails the test instead of blocking it.
1940
+ */
1941
+ async function settleWithin(promise, ms = 5e3) {
1942
+ let timer;
1943
+ try {
1944
+ return await Promise.race([promise, new Promise((_, reject) => {
1945
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
1946
+ })]);
1947
+ } finally {
1948
+ clearTimeout(timer);
1949
+ }
1950
+ }
1951
+ function neverCancellableResponse(init) {
1952
+ return new Response(new ReadableStream({
1953
+ start(controller) {
1954
+ controller.enqueue(new TextEncoder().encode("never cancelled"));
1955
+ },
1956
+ cancel: () => new Promise(() => {})
1957
+ }), init);
1958
+ }
1959
+ (0, _fedify_fixture.test)("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
1960
+ const descriptor = {
1961
+ subject: "acct:johndoe@example.com",
1962
+ links: []
1963
+ };
1964
+ esm_default.mockGlobal();
1965
+ try {
1966
+ await t.step("redirect", async () => {
1967
+ esm_default.removeRoutes();
1968
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({
1969
+ status: 302,
1970
+ headers: { Location: "/.well-known/webfinger2" }
1971
+ }));
1972
+ esm_default.get("https://example.com/.well-known/webfinger2", () => new Response(JSON.stringify(descriptor), { headers: { "Content-Type": "application/jrd+json" } }));
1973
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), descriptor);
1974
+ });
1975
+ await t.step("non-2xx response", async () => {
1976
+ esm_default.removeRoutes();
1977
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({ status: 404 }));
1978
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), null);
1979
+ });
1980
+ } finally {
1981
+ esm_default.hardReset();
1982
+ }
1983
+ });
1656
1984
  //#endregion