@fedify/webfinger 2.4.5-dev.2453 → 2.4.5

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.
@@ -26,9 +26,9 @@ let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
26
26
  let _logtape_logtape = require("@logtape/logtape");
27
27
  let es_toolkit = require("es-toolkit");
28
28
  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, 1);
31
29
  let _opentelemetry_api = require("@opentelemetry/api");
30
+ let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
31
+ let node_test = require("node:test");
32
32
  //#endregion
33
33
  //#region ../../node_modules/.pnpm/regexparam@3.0.0/node_modules/regexparam/dist/index.mjs
34
34
  var import_glob_to_regexp = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -1237,7 +1237,7 @@ var esm_default = new class FetchMock {
1237
1237
  //#endregion
1238
1238
  //#region deno.json
1239
1239
  var name = "@fedify/webfinger";
1240
- var version = "2.4.5-dev.2453+1304e862";
1240
+ var version = "2.4.5";
1241
1241
  //#endregion
1242
1242
  //#region src/body.ts
1243
1243
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1313,13 +1313,223 @@ async function readBoundedText(message, maxBytes, url) {
1313
1313
  }
1314
1314
  }
1315
1315
  //#endregion
1316
- //#region src/lookup.ts
1316
+ //#region src/lookup-impl.ts
1317
1317
  const logger = (0, _logtape_logtape.getLogger)([
1318
1318
  "fedify",
1319
1319
  "webfinger",
1320
1320
  "lookup"
1321
1321
  ]);
1322
1322
  const DEFAULT_MAX_REDIRECTION = 5;
1323
+ /**
1324
+ * Looks up a WebFinger resource, opening a fetch session per hop. This is
1325
+ * the implementation of {@link lookupWebFinger}, which is not exported from
1326
+ * the package entry point.
1327
+ * @param resource The resource URL to look up.
1328
+ * @param options Extra options for looking up the resource.
1329
+ * @param deps The dependencies.
1330
+ * @returns The resource descriptor and terminal lookup classification.
1331
+ * @internal
1332
+ */
1333
+ async function lookupWebFingerInternal(resource, options = {}, deps = {}) {
1334
+ if (typeof resource === "string") resource = new URL(resource);
1335
+ let protocol = "https:";
1336
+ let server;
1337
+ if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
1338
+ const atPos = resource.pathname.lastIndexOf("@");
1339
+ if (atPos < 0) return {
1340
+ resource: null,
1341
+ result: "invalid"
1342
+ };
1343
+ server = resource.pathname.substring(atPos + 1);
1344
+ if (server === "" || /[/?#]/.test(server)) return {
1345
+ resource: null,
1346
+ result: "invalid"
1347
+ };
1348
+ if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
1349
+ resource: null,
1350
+ result: "invalid"
1351
+ };
1352
+ } else {
1353
+ protocol = resource.protocol;
1354
+ server = resource.host;
1355
+ }
1356
+ let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
1357
+ url.searchParams.set("resource", resource.href);
1358
+ 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 }));
1359
+ let redirected = 0;
1360
+ while (true) {
1361
+ logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
1362
+ const remoteHost = url.host;
1363
+ const session = await openSession(url, openHop, options.signal);
1364
+ if (session == null) return {
1365
+ resource: null,
1366
+ result: "network_error",
1367
+ remoteHost
1368
+ };
1369
+ let hop;
1370
+ try {
1371
+ hop = await fetchHop(session, url, options);
1372
+ } finally {
1373
+ await session.close();
1374
+ }
1375
+ if ("outcome" in hop) return hop.outcome;
1376
+ redirected++;
1377
+ const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1378
+ if (redirected > maxRedirection) {
1379
+ logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
1380
+ return {
1381
+ resource: null,
1382
+ result: "invalid",
1383
+ statusCode: hop.statusCode,
1384
+ remoteHost
1385
+ };
1386
+ }
1387
+ let redirectedUrl;
1388
+ try {
1389
+ redirectedUrl = new URL(hop.redirect, url);
1390
+ } catch (error) {
1391
+ logger.error("Invalid Location header while following WebFinger redirect: {error}", {
1392
+ url: url.href,
1393
+ error
1394
+ });
1395
+ return {
1396
+ resource: null,
1397
+ result: "invalid",
1398
+ statusCode: hop.statusCode,
1399
+ remoteHost
1400
+ };
1401
+ }
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 {
1408
+ resource: null,
1409
+ result: "invalid",
1410
+ statusCode: hop.statusCode,
1411
+ remoteHost
1412
+ };
1413
+ }
1414
+ url = redirectedUrl;
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Opens a fetch session for a hop. A disallowed URL, a failed DNS lookup,
1419
+ * or an aborted lookup yields `null` instead of an error.
1420
+ */
1421
+ async function openSession(url, openHop, signal) {
1422
+ try {
1423
+ return await openHop(url.href);
1424
+ } catch (e) {
1425
+ if (e instanceof _fedify_vocab_runtime.UrlError) {
1426
+ if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
1427
+ url: url.href,
1428
+ error: e
1429
+ });
1430
+ else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
1431
+ return null;
1432
+ }
1433
+ if (signal?.aborted) {
1434
+ logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
1435
+ url: url.href,
1436
+ error: e
1437
+ });
1438
+ return null;
1439
+ }
1440
+ throw e;
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Fetches a hop with the given session, and consumes the response body or
1445
+ * starts cancelling it before returning. The caller closes the session.
1446
+ */
1447
+ async function fetchHop(session, url, options) {
1448
+ let response;
1449
+ try {
1450
+ response = await session.fetch(url, {
1451
+ headers: {
1452
+ Accept: "application/jrd+json",
1453
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
1454
+ },
1455
+ redirect: "manual",
1456
+ signal: options.signal
1457
+ });
1458
+ } catch (error) {
1459
+ logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
1460
+ url: url.href,
1461
+ error
1462
+ });
1463
+ return { outcome: {
1464
+ resource: null,
1465
+ result: "network_error",
1466
+ remoteHost: url.host
1467
+ } };
1468
+ }
1469
+ if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1470
+ cancelBody(response, url);
1471
+ return {
1472
+ redirect: response.headers.get("Location"),
1473
+ statusCode: response.status
1474
+ };
1475
+ }
1476
+ if (!response.ok) {
1477
+ logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
1478
+ url: url.href,
1479
+ status: response.status,
1480
+ statusText: response.statusText
1481
+ });
1482
+ cancelBody(response, url);
1483
+ return { outcome: {
1484
+ resource: null,
1485
+ result: response.status === 404 || response.status === 410 ? "not_found" : "error",
1486
+ statusCode: response.status,
1487
+ remoteHost: url.host
1488
+ } };
1489
+ }
1490
+ try {
1491
+ return { outcome: {
1492
+ resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
1493
+ result: "found",
1494
+ statusCode: response.status,
1495
+ remoteHost: url.host
1496
+ } };
1497
+ } catch (e) {
1498
+ if (e instanceof BodyTooLargeError) return { outcome: {
1499
+ resource: null,
1500
+ result: "invalid",
1501
+ statusCode: response.status,
1502
+ remoteHost: url.host
1503
+ } };
1504
+ if (e instanceof SyntaxError) {
1505
+ logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
1506
+ return { outcome: {
1507
+ resource: null,
1508
+ result: "invalid",
1509
+ statusCode: response.status,
1510
+ remoteHost: url.host
1511
+ } };
1512
+ }
1513
+ throw e;
1514
+ }
1515
+ }
1516
+ /**
1517
+ * Starts cancelling a response body that is not read, without waiting for it
1518
+ * to settle. The cancellation of a branch of a tee (e.g., a body cloned by
1519
+ * a fetch mock) settles only after the other branch is cancelled too, per
1520
+ * the Streams standard, so awaiting it could block forever. A failure is
1521
+ * only logged.
1522
+ */
1523
+ function cancelBody(response, url) {
1524
+ response.body?.cancel().catch((error) => {
1525
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
1526
+ url: url.href,
1527
+ error
1528
+ });
1529
+ });
1530
+ }
1531
+ //#endregion
1532
+ //#region src/lookup.ts
1323
1533
  const WEBFINGER_HISTOGRAM_BUCKETS = [
1324
1534
  5,
1325
1535
  10,
@@ -1373,13 +1583,11 @@ function getMetricResourceScheme(scheme) {
1373
1583
  return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
1374
1584
  }
1375
1585
  /**
1376
- * Looks up a WebFinger resource.
1377
- * @param resource The resource URL to look up.
1378
- * @param options Extra options for looking up the resource.
1379
- * @returns The resource descriptor, or `null` if not found.
1380
- * @since 0.2.0
1586
+ * The lookup implementation with injectable hop sessions for tests.
1587
+ * Not exported from the package entry point.
1588
+ * @internal
1381
1589
  */
1382
- async function lookupWebFinger(resource, options = {}) {
1590
+ async function lookupWebFingerWithDependencies(resource, options = {}, deps = {}) {
1383
1591
  const tracer = (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version);
1384
1592
  const scheme = getResourceScheme(resource);
1385
1593
  return await tracer.startActiveSpan("webfinger.lookup", {
@@ -1396,7 +1604,7 @@ async function lookupWebFinger(resource, options = {}) {
1396
1604
  result: "error"
1397
1605
  };
1398
1606
  try {
1399
- outcome = await lookupWebFingerInternal(resource, options);
1607
+ outcome = await lookupWebFingerInternal(resource, options, deps);
1400
1608
  span.setStatus({ code: outcome.resource === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
1401
1609
  return outcome.resource;
1402
1610
  } catch (error) {
@@ -1422,181 +1630,56 @@ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
1422
1630
  instruments.lookup.add(1, attributes);
1423
1631
  instruments.lookupDuration.record(durationMs, attributes);
1424
1632
  }
1425
- async function lookupWebFingerInternal(resource, options = {}) {
1426
- if (typeof resource === "string") resource = new URL(resource);
1427
- let protocol = "https:";
1428
- let server;
1429
- if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
1430
- const atPos = resource.pathname.lastIndexOf("@");
1431
- if (atPos < 0) return {
1432
- resource: null,
1433
- result: "invalid"
1434
- };
1435
- server = resource.pathname.substring(atPos + 1);
1436
- if (server === "" || /[/?#]/.test(server)) return {
1437
- resource: null,
1438
- result: "invalid"
1439
- };
1440
- if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
1441
- resource: null,
1442
- result: "invalid"
1443
- };
1444
- } else {
1445
- protocol = resource.protocol;
1446
- server = resource.host;
1447
- }
1448
- let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
1449
- url.searchParams.set("resource", resource.href);
1450
- let redirected = 0;
1451
- while (true) {
1452
- const remoteHost = url.host;
1453
- logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
1454
- let response;
1455
- if (options.allowPrivateAddress !== true) try {
1456
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url.href);
1457
- } catch (e) {
1458
- if (e instanceof _fedify_vocab_runtime.UrlError) {
1459
- if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
1460
- url: url.href,
1461
- error: e
1462
- });
1463
- else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
1464
- return {
1465
- resource: null,
1466
- result: "network_error",
1467
- remoteHost
1468
- };
1469
- }
1470
- throw e;
1471
- }
1472
- try {
1473
- response = await fetch(url, {
1474
- headers: {
1475
- Accept: "application/jrd+json",
1476
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
1477
- },
1478
- redirect: "manual",
1479
- signal: options.signal
1480
- });
1481
- } catch (error) {
1482
- logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
1483
- url: url.href,
1484
- error
1485
- });
1486
- return {
1487
- resource: null,
1488
- result: "network_error",
1489
- remoteHost
1490
- };
1491
- }
1492
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1493
- redirected++;
1494
- const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1495
- if (redirected > maxRedirection) {
1496
- logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
1497
- return {
1498
- resource: null,
1499
- result: "invalid",
1500
- statusCode: response.status,
1501
- remoteHost
1502
- };
1503
- }
1504
- let redirectedUrl;
1505
- try {
1506
- redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
1507
- } catch (e) {
1508
- logger.error("Invalid Location header while following WebFinger redirect: {error}", {
1509
- url: url.href,
1510
- error: e
1511
- });
1512
- return {
1513
- resource: null,
1514
- result: "invalid",
1515
- statusCode: response.status,
1516
- remoteHost
1517
- };
1518
- }
1519
- if (redirectedUrl.protocol !== url.protocol) {
1520
- logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
1521
- protocol: url.protocol,
1522
- redirectedProtocol: redirectedUrl.protocol
1523
- });
1524
- return {
1525
- resource: null,
1526
- result: "invalid",
1527
- statusCode: response.status,
1528
- remoteHost
1529
- };
1530
- }
1531
- url = redirectedUrl;
1532
- continue;
1533
- }
1534
- if (!response.ok) {
1535
- logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
1536
- url: url.href,
1537
- status: response.status,
1538
- statusText: response.statusText
1539
- });
1540
- return {
1541
- resource: null,
1542
- result: response.status === 404 || response.status === 410 ? "not_found" : "error",
1543
- statusCode: response.status,
1544
- remoteHost
1545
- };
1546
- }
1547
- try {
1548
- return {
1549
- resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
1550
- result: "found",
1551
- statusCode: response.status,
1552
- remoteHost
1553
- };
1554
- } catch (e) {
1555
- if (e instanceof BodyTooLargeError) return {
1556
- resource: null,
1557
- result: "invalid",
1558
- statusCode: response.status,
1559
- remoteHost
1560
- };
1561
- if (e instanceof SyntaxError) {
1562
- logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
1563
- return {
1564
- resource: null,
1565
- result: "invalid",
1566
- statusCode: response.status,
1567
- remoteHost
1568
- };
1569
- }
1570
- throw e;
1571
- }
1572
- }
1573
- }
1574
1633
  //#endregion
1575
1634
  //#region src/lookup.test.ts
1635
+ function lookupMock(resource, options = {}) {
1636
+ if (resource.toString().includes("fedify-test.internal")) return lookupWebFingerWithDependencies(resource, options, { openHop: (url) => Promise.resolve({
1637
+ url: new URL(url),
1638
+ fetch: () => Promise.reject(/* @__PURE__ */ new TypeError("Connection refused")),
1639
+ close: () => Promise.resolve()
1640
+ }) });
1641
+ return lookupWebFingerWithDependencies(resource, options, { openHop: options.allowPrivateAddress === true ? _fedify_vocab_runtime_internal.openDirectFetch : openExampleDirectly });
1642
+ }
1643
+ function lookupDirect(resource, options) {
1644
+ return lookupWebFingerWithDependencies(resource, options, { openHop: _fedify_vocab_runtime_internal.openDirectFetch });
1645
+ }
1646
+ function lookupResolving(addresses) {
1647
+ return () => Promise.resolve(addresses);
1648
+ }
1649
+ function openExampleDirectly(url) {
1650
+ return new URL(url).hostname === "example.com" ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(url) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup: lookupResolving([{
1651
+ address: "93.184.215.14",
1652
+ family: 4
1653
+ }]) });
1654
+ }
1655
+ async function lookupResourceWithDependencies(resource, options, deps) {
1656
+ return (await lookupWebFingerInternal(resource, options, deps)).resource;
1657
+ }
1576
1658
  (0, _fedify_fixture.test)({
1577
1659
  name: "lookupWebFinger()",
1578
1660
  sanitizeOps: false,
1579
1661
  sanitizeResources: false,
1580
1662
  async fn(t) {
1663
+ const responseTimers = [];
1581
1664
  await t.step("invalid resource", async () => {
1582
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe"), null);
1583
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger(new URL("acct:johndoe")), null);
1584
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@"), null);
1585
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger(new URL("acct:johndoe@")), null);
1586
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com/exploit"), null);
1587
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com?x=1"), null);
1588
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com#frag"), null);
1665
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe"), null);
1666
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock(new URL("acct:johndoe")), null);
1667
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@"), null);
1668
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock(new URL("acct:johndoe@")), null);
1669
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com/exploit"), null);
1670
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com?x=1"), null);
1671
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com#frag"), null);
1589
1672
  });
1590
1673
  await t.step("connection refused", async () => {
1591
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@fedify-test.internal"), null);
1592
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://fedify-test.internal/foo"), null);
1674
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@fedify-test.internal"), null);
1675
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("https://fedify-test.internal/foo"), null);
1593
1676
  });
1594
1677
  esm_default.spyGlobal();
1595
1678
  try {
1596
1679
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1597
1680
  await t.step("not found", async () => {
1598
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1599
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), null);
1681
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1682
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("https://example.com/foo"), null);
1600
1683
  });
1601
1684
  const expected = {
1602
1685
  subject: "acct:johndoe@example.com",
@@ -1605,7 +1688,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1605
1688
  esm_default.removeRoutes();
1606
1689
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1607
1690
  await t.step("acct", async () => {
1608
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1691
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
1609
1692
  });
1610
1693
  const expected2 = {
1611
1694
  subject: "https://example.com/foo",
@@ -1614,7 +1697,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1614
1697
  esm_default.removeRoutes();
1615
1698
  esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
1616
1699
  await t.step("https", async () => {
1617
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), expected2);
1700
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("https://example.com/foo"), expected2);
1618
1701
  });
1619
1702
  const mailtoExpected = {
1620
1703
  subject: "mailto:juliet@example.com",
@@ -1623,7 +1706,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1623
1706
  esm_default.removeRoutes();
1624
1707
  esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com", { body: mailtoExpected });
1625
1708
  await t.step("mailto", async () => {
1626
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("mailto:juliet@example.com"), mailtoExpected);
1709
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("mailto:juliet@example.com"), mailtoExpected);
1627
1710
  });
1628
1711
  const mailtoQueryExpected = {
1629
1712
  subject: "mailto:juliet@example.com?subject=Hi",
@@ -1632,12 +1715,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
1632
1715
  esm_default.removeRoutes();
1633
1716
  esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com%3Fsubject%3DHi", { body: mailtoQueryExpected });
1634
1717
  await t.step("mailto with hfields", async () => {
1635
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
1718
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
1636
1719
  });
1637
1720
  esm_default.removeRoutes();
1638
1721
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1639
1722
  await t.step("invalid response", async () => {
1640
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1723
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1641
1724
  });
1642
1725
  esm_default.removeRoutes();
1643
1726
  esm_default.get("begin:https://localhost/.well-known/webfinger?", {
@@ -1649,8 +1732,8 @@ async function lookupWebFingerInternal(resource, options = {}) {
1649
1732
  }]
1650
1733
  });
1651
1734
  await t.step("private address", async () => {
1652
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:test@localhost"), null);
1653
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:test@localhost", { allowPrivateAddress: true }), {
1735
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:test@localhost"), null);
1736
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:test@localhost", { allowPrivateAddress: true }), {
1654
1737
  subject: "acct:test@localhost",
1655
1738
  links: [{
1656
1739
  rel: "self",
@@ -1666,7 +1749,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1666
1749
  });
1667
1750
  esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
1668
1751
  await t.step("redirection", async () => {
1669
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1752
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
1670
1753
  });
1671
1754
  esm_default.removeRoutes();
1672
1755
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1674,7 +1757,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1674
1757
  headers: { Location: "/.well-known/webfinger" }
1675
1758
  });
1676
1759
  await t.step("infinite redirection", async () => {
1677
- (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com"), 2e3), null);
1760
+ (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupMock("acct:johndoe@example.com"), 2e3), null);
1678
1761
  });
1679
1762
  esm_default.removeRoutes();
1680
1763
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1682,7 +1765,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1682
1765
  headers: { Location: "ftp://example.com/" }
1683
1766
  });
1684
1767
  await t.step("redirection to different protocol", async () => {
1685
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1768
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1686
1769
  });
1687
1770
  esm_default.removeRoutes();
1688
1771
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1690,7 +1773,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1690
1773
  headers: { Location: "https://localhost/" }
1691
1774
  });
1692
1775
  await t.step("redirection to private address", async () => {
1693
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1776
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1694
1777
  });
1695
1778
  esm_default.removeRoutes();
1696
1779
  let redirectCount = 0;
@@ -1704,13 +1787,13 @@ async function lookupWebFingerInternal(resource, options = {}) {
1704
1787
  });
1705
1788
  await t.step("custom maxRedirection", async () => {
1706
1789
  redirectCount = 0;
1707
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1790
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1708
1791
  redirectCount = 0;
1709
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
1792
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
1710
1793
  redirectCount = 0;
1711
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1794
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1712
1795
  redirectCount = 0;
1713
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1796
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
1714
1797
  });
1715
1798
  await t.step("maxRedirection: 1 follows exactly one redirect", async () => {
1716
1799
  esm_default.removeRoutes();
@@ -1722,7 +1805,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1722
1805
  headers: { Location: "/.well-known/webfinger?after=1" }
1723
1806
  } : { body: expected };
1724
1807
  });
1725
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
1808
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
1726
1809
  esm_default.removeRoutes();
1727
1810
  count = 0;
1728
1811
  esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
@@ -1732,18 +1815,18 @@ async function lookupWebFingerInternal(resource, options = {}) {
1732
1815
  headers: { Location: `/.well-known/webfinger?after=${count}` }
1733
1816
  } : { body: expected };
1734
1817
  });
1735
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1818
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1736
1819
  });
1737
1820
  esm_default.removeRoutes();
1738
1821
  esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
1739
1822
  const timeoutId = setTimeout(() => {
1740
1823
  resolve({ body: expected });
1741
1824
  }, 1e3);
1742
- return () => clearTimeout(timeoutId);
1825
+ responseTimers.push(timeoutId);
1743
1826
  }));
1744
1827
  await t.step("request cancellation", async () => {
1745
1828
  const controller = new AbortController();
1746
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1829
+ const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
1747
1830
  controller.abort();
1748
1831
  (0, node_assert_strict.deepStrictEqual)(await promise, null);
1749
1832
  });
@@ -1759,12 +1842,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
1759
1842
  const timeoutId = setTimeout(() => {
1760
1843
  resolve({ body: expected });
1761
1844
  }, 1e3);
1762
- return () => clearTimeout(timeoutId);
1845
+ responseTimers.push(timeoutId);
1763
1846
  });
1764
1847
  });
1765
1848
  await t.step("cancellation during redirection", async () => {
1766
1849
  const controller = new AbortController();
1767
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1850
+ const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
1768
1851
  setTimeout(() => controller.abort(), 100);
1769
1852
  (0, node_assert_strict.deepStrictEqual)(await promise, null);
1770
1853
  });
@@ -1773,19 +1856,20 @@ async function lookupWebFingerInternal(resource, options = {}) {
1773
1856
  const timeoutId = setTimeout(() => {
1774
1857
  resolve({ body: expected });
1775
1858
  }, 500);
1776
- return () => clearTimeout(timeoutId);
1859
+ responseTimers.push(timeoutId);
1777
1860
  }));
1778
1861
  await t.step("cancellation with immediate abort", async () => {
1779
1862
  const controller = new AbortController();
1780
1863
  controller.abort();
1781
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal }), null);
1864
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { signal: controller.signal }), null);
1782
1865
  });
1783
1866
  esm_default.removeRoutes();
1784
1867
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
1785
1868
  await t.step("successful request with signal", async () => {
1786
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
1869
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
1787
1870
  });
1788
1871
  } finally {
1872
+ for (const timer of responseTimers) clearTimeout(timer);
1789
1873
  esm_default.removeRoutes();
1790
1874
  esm_default.hardReset();
1791
1875
  }
@@ -1805,7 +1889,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1805
1889
  esm_default.removeRoutes();
1806
1890
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1807
1891
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1808
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { meterProvider }), expected);
1892
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { meterProvider }), expected);
1809
1893
  const counters = recorder.getMeasurements("webfinger.lookup");
1810
1894
  (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1811
1895
  (0, node_assert_strict.deepStrictEqual)(counters[0].type, "counter");
@@ -1828,7 +1912,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1828
1912
  links: []
1829
1913
  } });
1830
1914
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1831
- await lookupWebFinger("https://example.com/foo", { meterProvider });
1915
+ await lookupMock("https://example.com/foo", { meterProvider });
1832
1916
  const counters = recorder.getMeasurements("webfinger.lookup");
1833
1917
  (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1834
1918
  (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.resource.scheme"], "https");
@@ -1841,7 +1925,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1841
1925
  links: []
1842
1926
  } });
1843
1927
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1844
- await lookupWebFinger("https://example.com:8443/foo", { meterProvider });
1928
+ await lookupMock("https://example.com:8443/foo", { meterProvider });
1845
1929
  const counter = recorder.getMeasurement("webfinger.lookup");
1846
1930
  (0, node_assert_strict.ok)(counter != null);
1847
1931
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "example.com:8443");
@@ -1850,7 +1934,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1850
1934
  esm_default.removeRoutes();
1851
1935
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1852
1936
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1853
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { meterProvider }), null);
1937
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { meterProvider }), null);
1854
1938
  const counters = recorder.getMeasurements("webfinger.lookup");
1855
1939
  (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1856
1940
  (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.lookup.result"], "not_found");
@@ -1864,7 +1948,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1864
1948
  esm_default.removeRoutes();
1865
1949
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 410 });
1866
1950
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1867
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1951
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1868
1952
  const counter = recorder.getMeasurement("webfinger.lookup");
1869
1953
  (0, node_assert_strict.ok)(counter != null);
1870
1954
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "not_found");
@@ -1874,7 +1958,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1874
1958
  esm_default.removeRoutes();
1875
1959
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
1876
1960
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1877
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1961
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1878
1962
  const counter = recorder.getMeasurement("webfinger.lookup");
1879
1963
  (0, node_assert_strict.ok)(counter != null);
1880
1964
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "error");
@@ -1884,7 +1968,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1884
1968
  esm_default.removeRoutes();
1885
1969
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1886
1970
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1887
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1971
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1888
1972
  const counter = recorder.getMeasurement("webfinger.lookup");
1889
1973
  (0, node_assert_strict.ok)(counter != null);
1890
1974
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1893,7 +1977,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1893
1977
  await t.step("records result=network_error when fetch never reaches the remote", async () => {
1894
1978
  esm_default.removeRoutes();
1895
1979
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1896
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@fedify-test.internal", { meterProvider }), null);
1980
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@fedify-test.internal", { meterProvider }), null);
1897
1981
  const counter = recorder.getMeasurement("webfinger.lookup");
1898
1982
  (0, node_assert_strict.ok)(counter != null);
1899
1983
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
@@ -1902,7 +1986,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1902
1986
  });
1903
1987
  await t.step("records result=invalid for malformed acct: resources", async () => {
1904
1988
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1905
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe", { meterProvider }), null);
1989
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe", { meterProvider }), null);
1906
1990
  const counter = recorder.getMeasurement("webfinger.lookup");
1907
1991
  (0, node_assert_strict.ok)(counter != null);
1908
1992
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1916,7 +2000,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1916
2000
  headers: { Location: "/.well-known/webfinger" }
1917
2001
  });
1918
2002
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1919
- (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com", {
2003
+ (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupMock("acct:johndoe@example.com", {
1920
2004
  meterProvider,
1921
2005
  maxRedirection: 3
1922
2006
  }), 2e3), null);
@@ -1933,7 +2017,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1933
2017
  headers: { Location: "ftp://example.com/" }
1934
2018
  });
1935
2019
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1936
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
2020
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1937
2021
  const counter = recorder.getMeasurement("webfinger.lookup");
1938
2022
  (0, node_assert_strict.ok)(counter != null);
1939
2023
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1946,7 +2030,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1946
2030
  headers: { Location: "https://localhost/" }
1947
2031
  });
1948
2032
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1949
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
2033
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1950
2034
  const counter = recorder.getMeasurement("webfinger.lookup");
1951
2035
  (0, node_assert_strict.ok)(counter != null);
1952
2036
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
@@ -1959,7 +2043,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1959
2043
  headers: { Location: "http://[bad" }
1960
2044
  });
1961
2045
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1962
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
2046
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1963
2047
  const counter = recorder.getMeasurement("webfinger.lookup");
1964
2048
  (0, node_assert_strict.ok)(counter != null);
1965
2049
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1969,7 +2053,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1969
2053
  await t.step("buckets unknown resource schemes as 'other' to keep metric cardinality bounded", async () => {
1970
2054
  esm_default.removeRoutes();
1971
2055
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1972
- await lookupWebFinger("ssh://example.com/foo", { meterProvider });
2056
+ await lookupMock("ssh://example.com/foo", { meterProvider });
1973
2057
  const counter = recorder.getMeasurement("webfinger.lookup");
1974
2058
  (0, node_assert_strict.ok)(counter != null);
1975
2059
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.resource.scheme"], "other");
@@ -1978,7 +2062,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1978
2062
  esm_default.removeRoutes();
1979
2063
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1980
2064
  const [_unused, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1981
- await lookupWebFinger("acct:johndoe@example.com");
2065
+ await lookupMock("acct:johndoe@example.com");
1982
2066
  (0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup").length, 0);
1983
2067
  (0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup.duration").length, 0);
1984
2068
  });
@@ -1995,25 +2079,23 @@ async function lookupWebFingerInternal(resource, options = {}) {
1995
2079
  "Content-Type": "application/jrd+json",
1996
2080
  "Content-Length": "1"
1997
2081
  } }));
1998
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:alice@example.com", { allowPrivateAddress: true }), null);
2082
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), null);
1999
2083
  oversized = false;
2000
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:alice@example.com", { allowPrivateAddress: true }), { subject: "acct:alice@example.com" });
2084
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), { subject: "acct:alice@example.com" });
2001
2085
  } finally {
2002
2086
  esm_default.hardReset();
2003
2087
  }
2004
2088
  });
2005
2089
  (0, _fedify_fixture.test)("lookupWebFinger() logs DNS failures as such", async (t) => {
2006
- if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") return;
2007
2090
  for (const result of [
2008
2091
  "throws",
2009
2092
  "empty",
2010
2093
  "private"
2011
2094
  ]) await t.step(result, async () => {
2012
- const originalLookup = node_dns_promises.default.lookup;
2013
- node_dns_promises.default.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
2095
+ const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
2014
2096
  address: "127.0.0.1",
2015
2097
  family: 4
2016
- }]));
2098
+ }]);
2017
2099
  const records = [];
2018
2100
  await (0, _logtape_logtape.configure)({
2019
2101
  sinks: { buffer: (record) => records.push(record) },
@@ -2028,7 +2110,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2028
2110
  reset: true
2029
2111
  });
2030
2112
  try {
2031
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:alice@dns-failure.invalid"), null);
2113
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:alice@dns-failure.invalid", {}, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
2032
2114
  const failures = records.filter((r) => r.rawMessage !== "Fetching WebFinger resource descriptor from {url}...");
2033
2115
  (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}"]]);
2034
2116
  const { error } = failures[0].properties;
@@ -2036,8 +2118,266 @@ async function lookupWebFingerInternal(resource, options = {}) {
2036
2118
  (0, node_assert_strict.deepStrictEqual)(error.reason, result === "private" ? "disallowed" : "dns");
2037
2119
  } finally {
2038
2120
  await (0, _logtape_logtape.reset)();
2039
- node_dns_promises.default.lookup = originalLookup;
2040
2121
  }
2041
2122
  });
2042
2123
  });
2124
+ function recordHops(respond) {
2125
+ const events = [];
2126
+ const unsettled = [];
2127
+ let opens = 0;
2128
+ let closes = 0;
2129
+ let responses = 0;
2130
+ return {
2131
+ events,
2132
+ async openHop(url) {
2133
+ opens++;
2134
+ events.push(`open ${url}`);
2135
+ const states = [];
2136
+ let closed = false;
2137
+ return {
2138
+ url: new URL(url),
2139
+ async fetch(input) {
2140
+ if (closed) throw new TypeError("The fetch session is closed.");
2141
+ const target = new URL(input instanceof Request ? input.url : input);
2142
+ const id = ++responses;
2143
+ events.push(`fetch ${target.href} #${id}`);
2144
+ const fake = respond(target);
2145
+ const state = {
2146
+ id,
2147
+ settled: false
2148
+ };
2149
+ states.push(state);
2150
+ let sent = false;
2151
+ const body = new ReadableStream({
2152
+ pull(controller) {
2153
+ if (!sent && fake.body != null && fake.body !== "") {
2154
+ sent = true;
2155
+ controller.enqueue(new TextEncoder().encode(fake.body));
2156
+ return;
2157
+ }
2158
+ state.settled = true;
2159
+ events.push(`consumed #${id}`);
2160
+ controller.close();
2161
+ },
2162
+ cancel() {
2163
+ state.settled = true;
2164
+ events.push(`cancel #${id}`);
2165
+ if (fake.cancelFails) throw new Error("Cancellation failed");
2166
+ if (fake.cancelNeverSettles) return new Promise(() => {});
2167
+ }
2168
+ }, { highWaterMark: 0 });
2169
+ const response = new Response(body, {
2170
+ status: fake.status ?? 200,
2171
+ headers: fake.headers
2172
+ });
2173
+ if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
2174
+ return response;
2175
+ },
2176
+ async close() {
2177
+ if (closed) return;
2178
+ closed = true;
2179
+ closes++;
2180
+ for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
2181
+ events.push(`close ${url}`);
2182
+ }
2183
+ };
2184
+ },
2185
+ assertInvariants() {
2186
+ (0, node_assert_strict.deepStrictEqual)(closes, opens);
2187
+ (0, node_assert_strict.deepStrictEqual)(unsettled, []);
2188
+ }
2189
+ };
2190
+ }
2191
+ (0, _fedify_fixture.test)("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
2192
+ const descriptor = {
2193
+ subject: "acct:johndoe@example.com",
2194
+ links: []
2195
+ };
2196
+ const first = "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
2197
+ const second = "https://example.com/.well-known/webfinger2";
2198
+ await t.step("redirect", async () => {
2199
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
2200
+ status: 302,
2201
+ headers: { Location: "/.well-known/webfinger2" },
2202
+ body: "redirecting",
2203
+ url: url.href.replace("example.com", "93.184.215.14")
2204
+ } : { body: JSON.stringify(descriptor) });
2205
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
2206
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2207
+ `open ${first}`,
2208
+ `fetch ${first} #1`,
2209
+ "cancel #1",
2210
+ `close ${first}`,
2211
+ `open ${second}`,
2212
+ `fetch ${second} #2`,
2213
+ "consumed #2",
2214
+ `close ${second}`
2215
+ ]);
2216
+ recorder.assertInvariants();
2217
+ });
2218
+ await t.step("non-2xx response", async () => {
2219
+ const recorder = recordHops(() => ({
2220
+ status: 404,
2221
+ body: "not found"
2222
+ }));
2223
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), null);
2224
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2225
+ `open ${first}`,
2226
+ `fetch ${first} #1`,
2227
+ "cancel #1",
2228
+ `close ${first}`
2229
+ ]);
2230
+ recorder.assertInvariants();
2231
+ });
2232
+ await t.step("too many redirections", async () => {
2233
+ const recorder = recordHops(() => ({
2234
+ status: 302,
2235
+ headers: { Location: "/.well-known/webfinger2" },
2236
+ body: "redirecting"
2237
+ }));
2238
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", { maxRedirection: 1 }, { openHop: recorder.openHop }), null);
2239
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2240
+ `open ${first}`,
2241
+ `fetch ${first} #1`,
2242
+ "cancel #1",
2243
+ `close ${first}`,
2244
+ `open ${second}`,
2245
+ `fetch ${second} #2`,
2246
+ "cancel #2",
2247
+ `close ${second}`
2248
+ ]);
2249
+ recorder.assertInvariants();
2250
+ });
2251
+ await t.step("failed redirect body cancellation", async () => {
2252
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
2253
+ status: 302,
2254
+ headers: { Location: "/.well-known/webfinger2" },
2255
+ body: "redirecting",
2256
+ cancelFails: true
2257
+ } : { body: JSON.stringify(descriptor) });
2258
+ const records = [];
2259
+ await (0, _logtape_logtape.configure)({
2260
+ sinks: { buffer: (record) => records.push(record) },
2261
+ loggers: [{
2262
+ category: "fedify",
2263
+ sinks: ["buffer"],
2264
+ lowestLevel: "debug"
2265
+ }, {
2266
+ category: ["logtape", "meta"],
2267
+ sinks: []
2268
+ }],
2269
+ reset: true
2270
+ });
2271
+ try {
2272
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
2273
+ const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
2274
+ (0, node_assert_strict.deepStrictEqual)(failures.map((r) => [r.level, r.properties.url]), [["debug", first]]);
2275
+ const { error } = failures[0].properties;
2276
+ (0, node_assert_strict.ok)(error instanceof Error);
2277
+ (0, node_assert_strict.deepStrictEqual)(error.message, "Cancellation failed");
2278
+ } finally {
2279
+ await (0, _logtape_logtape.reset)();
2280
+ }
2281
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2282
+ `open ${first}`,
2283
+ `fetch ${first} #1`,
2284
+ "cancel #1",
2285
+ `close ${first}`,
2286
+ `open ${second}`,
2287
+ `fetch ${second} #2`,
2288
+ "consumed #2",
2289
+ `close ${second}`
2290
+ ]);
2291
+ recorder.assertInvariants();
2292
+ });
2293
+ await t.step("never-settling body cancellation", async () => {
2294
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
2295
+ status: 302,
2296
+ headers: { Location: "/.well-known/webfinger2" },
2297
+ body: "redirecting",
2298
+ cancelNeverSettles: true
2299
+ } : {
2300
+ status: 404,
2301
+ body: "not found",
2302
+ cancelNeverSettles: true
2303
+ });
2304
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop })), null);
2305
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2306
+ `open ${first}`,
2307
+ `fetch ${first} #1`,
2308
+ "cancel #1",
2309
+ `close ${first}`,
2310
+ `open ${second}`,
2311
+ `fetch ${second} #2`,
2312
+ "cancel #2",
2313
+ `close ${second}`
2314
+ ]);
2315
+ recorder.assertInvariants();
2316
+ });
2317
+ });
2318
+ /**
2319
+ * Rejects if the given promise does not settle within the timeout, so that
2320
+ * a hang fails the test instead of blocking it.
2321
+ */
2322
+ async function settleWithin(promise, ms = 5e3) {
2323
+ let timer;
2324
+ try {
2325
+ return await Promise.race([promise, new Promise((_, reject) => {
2326
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
2327
+ })]);
2328
+ } finally {
2329
+ clearTimeout(timer);
2330
+ }
2331
+ }
2332
+ function neverCancellableResponse(init) {
2333
+ return new Response(new ReadableStream({
2334
+ start(controller) {
2335
+ controller.enqueue(new TextEncoder().encode("never cancelled"));
2336
+ },
2337
+ cancel: () => new Promise(() => {})
2338
+ }), init);
2339
+ }
2340
+ (0, _fedify_fixture.test)("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
2341
+ const descriptor = {
2342
+ subject: "acct:johndoe@example.com",
2343
+ links: []
2344
+ };
2345
+ esm_default.mockGlobal();
2346
+ try {
2347
+ await t.step("redirect", async () => {
2348
+ esm_default.removeRoutes();
2349
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({
2350
+ status: 302,
2351
+ headers: { Location: "/.well-known/webfinger2" }
2352
+ }));
2353
+ esm_default.get("https://example.com/.well-known/webfinger2", () => new Response(JSON.stringify(descriptor), { headers: { "Content-Type": "application/jrd+json" } }));
2354
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), descriptor);
2355
+ });
2356
+ await t.step("non-2xx response", async () => {
2357
+ esm_default.removeRoutes();
2358
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({ status: 404 }));
2359
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), null);
2360
+ });
2361
+ } finally {
2362
+ esm_default.hardReset();
2363
+ }
2364
+ });
2365
+ (0, node_test.test)("lookupWebFinger() records network_error for rejected hop addresses", async () => {
2366
+ for (const lookup of [
2367
+ () => Promise.reject(/* @__PURE__ */ new Error("DNS unavailable")),
2368
+ () => Promise.resolve([]),
2369
+ () => Promise.resolve([{
2370
+ address: "127.0.0.1",
2371
+ family: 4
2372
+ }])
2373
+ ]) {
2374
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
2375
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerWithDependencies("acct:alice@dns-failure.invalid", { meterProvider }, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
2376
+ const counter = recorder.getMeasurement("webfinger.lookup");
2377
+ (0, node_assert_strict.ok)(counter != null);
2378
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
2379
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "dns-failure.invalid");
2380
+ (0, node_assert_strict.deepStrictEqual)("http.response.status_code" in counter.attributes, false);
2381
+ }
2382
+ });
2043
2383
  //#endregion