@fedify/webfinger 2.5.0-dev.2446 → 2.5.0-dev.2492

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.5.0-dev.2446+2014cb03";
1240
+ var version = "2.5.0-dev.2492+4648c04e";
1241
1241
  //#endregion
1242
1242
  //#region src/body.ts
1243
1243
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1313,13 +1313,230 @@ 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
+ const parsed = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url));
1492
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { outcome: {
1493
+ resource: null,
1494
+ result: "invalid",
1495
+ statusCode: response.status,
1496
+ remoteHost: url.host
1497
+ } };
1498
+ return { outcome: {
1499
+ resource: parsed,
1500
+ result: "found",
1501
+ statusCode: response.status,
1502
+ remoteHost: url.host
1503
+ } };
1504
+ } catch (e) {
1505
+ if (e instanceof BodyTooLargeError) return { outcome: {
1506
+ resource: null,
1507
+ result: "invalid",
1508
+ statusCode: response.status,
1509
+ remoteHost: url.host
1510
+ } };
1511
+ if (e instanceof SyntaxError) {
1512
+ logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
1513
+ return { outcome: {
1514
+ resource: null,
1515
+ result: "invalid",
1516
+ statusCode: response.status,
1517
+ remoteHost: url.host
1518
+ } };
1519
+ }
1520
+ throw e;
1521
+ }
1522
+ }
1523
+ /**
1524
+ * Starts cancelling a response body that is not read, without waiting for it
1525
+ * to settle. The cancellation of a branch of a tee (e.g., a body cloned by
1526
+ * a fetch mock) settles only after the other branch is cancelled too, per
1527
+ * the Streams standard, so awaiting it could block forever. A failure is
1528
+ * only logged.
1529
+ */
1530
+ function cancelBody(response, url) {
1531
+ response.body?.cancel().catch((error) => {
1532
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
1533
+ url: url.href,
1534
+ error
1535
+ });
1536
+ });
1537
+ }
1538
+ //#endregion
1539
+ //#region src/lookup.ts
1323
1540
  const WEBFINGER_HISTOGRAM_BUCKETS = [
1324
1541
  5,
1325
1542
  10,
@@ -1373,6 +1590,9 @@ function getMetricResourceScheme(scheme) {
1373
1590
  return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
1374
1591
  }
1375
1592
  async function lookupWebFinger(resource, options = {}) {
1593
+ return await lookupWebFingerWithDependencies(resource, options);
1594
+ }
1595
+ async function lookupWebFingerWithDependencies(resource, options = {}, deps = {}) {
1376
1596
  const tracer = (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version);
1377
1597
  const scheme = getResourceScheme(resource);
1378
1598
  return await tracer.startActiveSpan("webfinger.lookup", {
@@ -1389,7 +1609,7 @@ async function lookupWebFinger(resource, options = {}) {
1389
1609
  result: "error"
1390
1610
  };
1391
1611
  try {
1392
- outcome = await lookupWebFingerInternal(resource, options);
1612
+ outcome = await lookupWebFingerInternal(resource, options, deps);
1393
1613
  span.setStatus({ code: outcome.resource === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
1394
1614
  return options.withDiagnostics ? outcome : outcome.resource;
1395
1615
  } catch (error) {
@@ -1415,188 +1635,56 @@ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
1415
1635
  instruments.lookup.add(1, attributes);
1416
1636
  instruments.lookupDuration.record(durationMs, attributes);
1417
1637
  }
1418
- async function lookupWebFingerInternal(resource, options = {}) {
1419
- if (typeof resource === "string") resource = new URL(resource);
1420
- let protocol = "https:";
1421
- let server;
1422
- if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
1423
- const atPos = resource.pathname.lastIndexOf("@");
1424
- if (atPos < 0) return {
1425
- resource: null,
1426
- result: "invalid"
1427
- };
1428
- server = resource.pathname.substring(atPos + 1);
1429
- if (server === "" || /[/?#]/.test(server)) return {
1430
- resource: null,
1431
- result: "invalid"
1432
- };
1433
- if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
1434
- resource: null,
1435
- result: "invalid"
1436
- };
1437
- } else {
1438
- protocol = resource.protocol;
1439
- server = resource.host;
1440
- }
1441
- let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
1442
- url.searchParams.set("resource", resource.href);
1443
- let redirected = 0;
1444
- while (true) {
1445
- const remoteHost = url.host;
1446
- logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
1447
- let response;
1448
- if (options.allowPrivateAddress !== true) try {
1449
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url.href);
1450
- } catch (e) {
1451
- if (e instanceof _fedify_vocab_runtime.UrlError) {
1452
- if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
1453
- url: url.href,
1454
- error: e
1455
- });
1456
- else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
1457
- return {
1458
- resource: null,
1459
- result: "network_error",
1460
- remoteHost
1461
- };
1462
- }
1463
- throw e;
1464
- }
1465
- try {
1466
- response = await fetch(url, {
1467
- headers: {
1468
- Accept: "application/jrd+json",
1469
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
1470
- },
1471
- redirect: "manual",
1472
- signal: options.signal
1473
- });
1474
- } catch (error) {
1475
- logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
1476
- url: url.href,
1477
- error
1478
- });
1479
- return {
1480
- resource: null,
1481
- result: "network_error",
1482
- remoteHost
1483
- };
1484
- }
1485
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1486
- redirected++;
1487
- const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1488
- if (redirected > maxRedirection) {
1489
- logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
1490
- return {
1491
- resource: null,
1492
- result: "invalid",
1493
- statusCode: response.status,
1494
- remoteHost
1495
- };
1496
- }
1497
- let redirectedUrl;
1498
- try {
1499
- redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
1500
- } catch (e) {
1501
- logger.error("Invalid Location header while following WebFinger redirect: {error}", {
1502
- url: url.href,
1503
- error: e
1504
- });
1505
- return {
1506
- resource: null,
1507
- result: "invalid",
1508
- statusCode: response.status,
1509
- remoteHost
1510
- };
1511
- }
1512
- if (redirectedUrl.protocol !== url.protocol) {
1513
- logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
1514
- protocol: url.protocol,
1515
- redirectedProtocol: redirectedUrl.protocol
1516
- });
1517
- return {
1518
- resource: null,
1519
- result: "invalid",
1520
- statusCode: response.status,
1521
- remoteHost
1522
- };
1523
- }
1524
- url = redirectedUrl;
1525
- continue;
1526
- }
1527
- if (!response.ok) {
1528
- logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
1529
- url: url.href,
1530
- status: response.status,
1531
- statusText: response.statusText
1532
- });
1533
- return {
1534
- resource: null,
1535
- result: response.status === 404 || response.status === 410 ? "not_found" : "error",
1536
- statusCode: response.status,
1537
- remoteHost
1538
- };
1539
- }
1540
- try {
1541
- const parsed = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url));
1542
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
1543
- resource: null,
1544
- result: "invalid",
1545
- statusCode: response.status,
1546
- remoteHost
1547
- };
1548
- return {
1549
- resource: parsed,
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
1638
  //#endregion
1575
1639
  //#region src/lookup.test.ts
1640
+ function lookupMock(resource, options = {}) {
1641
+ if (resource.toString().includes("fedify-test.internal")) return lookupWebFingerWithDependencies(resource, options, { openHop: (url) => Promise.resolve({
1642
+ url: new URL(url),
1643
+ fetch: () => Promise.reject(/* @__PURE__ */ new TypeError("Connection refused")),
1644
+ close: () => Promise.resolve()
1645
+ }) });
1646
+ return lookupWebFingerWithDependencies(resource, options, { openHop: options.allowPrivateAddress === true ? _fedify_vocab_runtime_internal.openDirectFetch : openExampleDirectly });
1647
+ }
1648
+ function lookupDirect(resource, options) {
1649
+ return lookupWebFingerWithDependencies(resource, options, { openHop: _fedify_vocab_runtime_internal.openDirectFetch });
1650
+ }
1651
+ function lookupResolving(addresses) {
1652
+ return () => Promise.resolve(addresses);
1653
+ }
1654
+ function openExampleDirectly(url) {
1655
+ return new URL(url).hostname === "example.com" ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(url) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup: lookupResolving([{
1656
+ address: "93.184.215.14",
1657
+ family: 4
1658
+ }]) });
1659
+ }
1660
+ async function lookupResourceWithDependencies(resource, options, deps) {
1661
+ return (await lookupWebFingerInternal(resource, options, deps)).resource;
1662
+ }
1576
1663
  (0, _fedify_fixture.test)({
1577
1664
  name: "lookupWebFinger()",
1578
1665
  sanitizeOps: false,
1579
1666
  sanitizeResources: false,
1580
1667
  async fn(t) {
1668
+ const responseTimers = [];
1581
1669
  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);
1670
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe"), null);
1671
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock(new URL("acct:johndoe")), null);
1672
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@"), null);
1673
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock(new URL("acct:johndoe@")), null);
1674
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com/exploit"), null);
1675
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com?x=1"), null);
1676
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com#frag"), null);
1589
1677
  });
1590
1678
  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);
1679
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@fedify-test.internal"), null);
1680
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("https://fedify-test.internal/foo"), null);
1593
1681
  });
1594
1682
  esm_default.spyGlobal();
1595
1683
  try {
1596
1684
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1597
1685
  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);
1686
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1687
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("https://example.com/foo"), null);
1600
1688
  });
1601
1689
  const expected = {
1602
1690
  subject: "acct:johndoe@example.com",
@@ -1605,7 +1693,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1605
1693
  esm_default.removeRoutes();
1606
1694
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1607
1695
  await t.step("acct", async () => {
1608
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1696
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
1609
1697
  });
1610
1698
  const expected2 = {
1611
1699
  subject: "https://example.com/foo",
@@ -1614,7 +1702,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1614
1702
  esm_default.removeRoutes();
1615
1703
  esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
1616
1704
  await t.step("https", async () => {
1617
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), expected2);
1705
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("https://example.com/foo"), expected2);
1618
1706
  });
1619
1707
  const mailtoExpected = {
1620
1708
  subject: "mailto:juliet@example.com",
@@ -1623,7 +1711,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1623
1711
  esm_default.removeRoutes();
1624
1712
  esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com", { body: mailtoExpected });
1625
1713
  await t.step("mailto", async () => {
1626
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("mailto:juliet@example.com"), mailtoExpected);
1714
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("mailto:juliet@example.com"), mailtoExpected);
1627
1715
  });
1628
1716
  const mailtoQueryExpected = {
1629
1717
  subject: "mailto:juliet@example.com?subject=Hi",
@@ -1632,12 +1720,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
1632
1720
  esm_default.removeRoutes();
1633
1721
  esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com%3Fsubject%3DHi", { body: mailtoQueryExpected });
1634
1722
  await t.step("mailto with hfields", async () => {
1635
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
1723
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
1636
1724
  });
1637
1725
  esm_default.removeRoutes();
1638
1726
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1639
1727
  await t.step("invalid response", async () => {
1640
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1728
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1641
1729
  });
1642
1730
  esm_default.removeRoutes();
1643
1731
  esm_default.get("begin:https://localhost/.well-known/webfinger?", {
@@ -1649,8 +1737,8 @@ async function lookupWebFingerInternal(resource, options = {}) {
1649
1737
  }]
1650
1738
  });
1651
1739
  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 }), {
1740
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:test@localhost"), null);
1741
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:test@localhost", { allowPrivateAddress: true }), {
1654
1742
  subject: "acct:test@localhost",
1655
1743
  links: [{
1656
1744
  rel: "self",
@@ -1666,7 +1754,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1666
1754
  });
1667
1755
  esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
1668
1756
  await t.step("redirection", async () => {
1669
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1757
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
1670
1758
  });
1671
1759
  esm_default.removeRoutes();
1672
1760
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1674,7 +1762,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1674
1762
  headers: { Location: "/.well-known/webfinger" }
1675
1763
  });
1676
1764
  await t.step("infinite redirection", async () => {
1677
- (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com"), 2e3), null);
1765
+ (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupMock("acct:johndoe@example.com"), 2e3), null);
1678
1766
  });
1679
1767
  esm_default.removeRoutes();
1680
1768
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1682,7 +1770,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1682
1770
  headers: { Location: "ftp://example.com/" }
1683
1771
  });
1684
1772
  await t.step("redirection to different protocol", async () => {
1685
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1773
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1686
1774
  });
1687
1775
  esm_default.removeRoutes();
1688
1776
  esm_default.get("begin:https://example.com/.well-known/webfinger?", {
@@ -1690,7 +1778,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1690
1778
  headers: { Location: "https://localhost/" }
1691
1779
  });
1692
1780
  await t.step("redirection to private address", async () => {
1693
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1781
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
1694
1782
  });
1695
1783
  esm_default.removeRoutes();
1696
1784
  let redirectCount = 0;
@@ -1704,13 +1792,13 @@ async function lookupWebFingerInternal(resource, options = {}) {
1704
1792
  });
1705
1793
  await t.step("custom maxRedirection", async () => {
1706
1794
  redirectCount = 0;
1707
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1795
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1708
1796
  redirectCount = 0;
1709
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
1797
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
1710
1798
  redirectCount = 0;
1711
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1799
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1712
1800
  redirectCount = 0;
1713
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1801
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
1714
1802
  });
1715
1803
  await t.step("maxRedirection: 1 follows exactly one redirect", async () => {
1716
1804
  esm_default.removeRoutes();
@@ -1722,7 +1810,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1722
1810
  headers: { Location: "/.well-known/webfinger?after=1" }
1723
1811
  } : { body: expected };
1724
1812
  });
1725
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
1813
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
1726
1814
  esm_default.removeRoutes();
1727
1815
  count = 0;
1728
1816
  esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
@@ -1732,18 +1820,18 @@ async function lookupWebFingerInternal(resource, options = {}) {
1732
1820
  headers: { Location: `/.well-known/webfinger?after=${count}` }
1733
1821
  } : { body: expected };
1734
1822
  });
1735
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1823
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1736
1824
  });
1737
1825
  esm_default.removeRoutes();
1738
1826
  esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
1739
1827
  const timeoutId = setTimeout(() => {
1740
1828
  resolve({ body: expected });
1741
1829
  }, 1e3);
1742
- return () => clearTimeout(timeoutId);
1830
+ responseTimers.push(timeoutId);
1743
1831
  }));
1744
1832
  await t.step("request cancellation", async () => {
1745
1833
  const controller = new AbortController();
1746
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1834
+ const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
1747
1835
  controller.abort();
1748
1836
  (0, node_assert_strict.deepStrictEqual)(await promise, null);
1749
1837
  });
@@ -1759,12 +1847,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
1759
1847
  const timeoutId = setTimeout(() => {
1760
1848
  resolve({ body: expected });
1761
1849
  }, 1e3);
1762
- return () => clearTimeout(timeoutId);
1850
+ responseTimers.push(timeoutId);
1763
1851
  });
1764
1852
  });
1765
1853
  await t.step("cancellation during redirection", async () => {
1766
1854
  const controller = new AbortController();
1767
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1855
+ const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
1768
1856
  setTimeout(() => controller.abort(), 100);
1769
1857
  (0, node_assert_strict.deepStrictEqual)(await promise, null);
1770
1858
  });
@@ -1773,19 +1861,20 @@ async function lookupWebFingerInternal(resource, options = {}) {
1773
1861
  const timeoutId = setTimeout(() => {
1774
1862
  resolve({ body: expected });
1775
1863
  }, 500);
1776
- return () => clearTimeout(timeoutId);
1864
+ responseTimers.push(timeoutId);
1777
1865
  }));
1778
1866
  await t.step("cancellation with immediate abort", async () => {
1779
1867
  const controller = new AbortController();
1780
1868
  controller.abort();
1781
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal }), null);
1869
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { signal: controller.signal }), null);
1782
1870
  });
1783
1871
  esm_default.removeRoutes();
1784
1872
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
1785
1873
  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);
1874
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
1787
1875
  });
1788
1876
  } finally {
1877
+ for (const timer of responseTimers) clearTimeout(timer);
1789
1878
  esm_default.removeRoutes();
1790
1879
  esm_default.hardReset();
1791
1880
  }
@@ -1805,7 +1894,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1805
1894
  esm_default.removeRoutes();
1806
1895
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1807
1896
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1808
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { meterProvider }), expected);
1897
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { meterProvider }), expected);
1809
1898
  const counters = recorder.getMeasurements("webfinger.lookup");
1810
1899
  (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1811
1900
  (0, node_assert_strict.deepStrictEqual)(counters[0].type, "counter");
@@ -1828,7 +1917,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1828
1917
  links: []
1829
1918
  } });
1830
1919
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1831
- await lookupWebFinger("https://example.com/foo", { meterProvider });
1920
+ await lookupMock("https://example.com/foo", { meterProvider });
1832
1921
  const counters = recorder.getMeasurements("webfinger.lookup");
1833
1922
  (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1834
1923
  (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.resource.scheme"], "https");
@@ -1841,7 +1930,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1841
1930
  links: []
1842
1931
  } });
1843
1932
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1844
- await lookupWebFinger("https://example.com:8443/foo", { meterProvider });
1933
+ await lookupMock("https://example.com:8443/foo", { meterProvider });
1845
1934
  const counter = recorder.getMeasurement("webfinger.lookup");
1846
1935
  (0, node_assert_strict.ok)(counter != null);
1847
1936
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "example.com:8443");
@@ -1850,7 +1939,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1850
1939
  esm_default.removeRoutes();
1851
1940
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1852
1941
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1853
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { meterProvider }), null);
1942
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { meterProvider }), null);
1854
1943
  const counters = recorder.getMeasurements("webfinger.lookup");
1855
1944
  (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1856
1945
  (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.lookup.result"], "not_found");
@@ -1864,7 +1953,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1864
1953
  esm_default.removeRoutes();
1865
1954
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 410 });
1866
1955
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1867
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1956
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1868
1957
  const counter = recorder.getMeasurement("webfinger.lookup");
1869
1958
  (0, node_assert_strict.ok)(counter != null);
1870
1959
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "not_found");
@@ -1874,7 +1963,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1874
1963
  esm_default.removeRoutes();
1875
1964
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
1876
1965
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1877
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1966
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1878
1967
  const counter = recorder.getMeasurement("webfinger.lookup");
1879
1968
  (0, node_assert_strict.ok)(counter != null);
1880
1969
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "error");
@@ -1884,7 +1973,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1884
1973
  esm_default.removeRoutes();
1885
1974
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1886
1975
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1887
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1976
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1888
1977
  const counter = recorder.getMeasurement("webfinger.lookup");
1889
1978
  (0, node_assert_strict.ok)(counter != null);
1890
1979
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1893,7 +1982,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1893
1982
  await t.step("records result=network_error when fetch never reaches the remote", async () => {
1894
1983
  esm_default.removeRoutes();
1895
1984
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1896
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@fedify-test.internal", { meterProvider }), null);
1985
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@fedify-test.internal", { meterProvider }), null);
1897
1986
  const counter = recorder.getMeasurement("webfinger.lookup");
1898
1987
  (0, node_assert_strict.ok)(counter != null);
1899
1988
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
@@ -1902,7 +1991,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1902
1991
  });
1903
1992
  await t.step("records result=invalid for malformed acct: resources", async () => {
1904
1993
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1905
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe", { meterProvider }), null);
1994
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe", { meterProvider }), null);
1906
1995
  const counter = recorder.getMeasurement("webfinger.lookup");
1907
1996
  (0, node_assert_strict.ok)(counter != null);
1908
1997
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1916,7 +2005,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1916
2005
  headers: { Location: "/.well-known/webfinger" }
1917
2006
  });
1918
2007
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1919
- (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com", {
2008
+ (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupMock("acct:johndoe@example.com", {
1920
2009
  meterProvider,
1921
2010
  maxRedirection: 3
1922
2011
  }), 2e3), null);
@@ -1933,7 +2022,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1933
2022
  headers: { Location: "ftp://example.com/" }
1934
2023
  });
1935
2024
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1936
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
2025
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1937
2026
  const counter = recorder.getMeasurement("webfinger.lookup");
1938
2027
  (0, node_assert_strict.ok)(counter != null);
1939
2028
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1946,7 +2035,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1946
2035
  headers: { Location: "https://localhost/" }
1947
2036
  });
1948
2037
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1949
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
2038
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1950
2039
  const counter = recorder.getMeasurement("webfinger.lookup");
1951
2040
  (0, node_assert_strict.ok)(counter != null);
1952
2041
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
@@ -1959,7 +2048,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1959
2048
  headers: { Location: "http://[bad" }
1960
2049
  });
1961
2050
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1962
- await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
2051
+ await lookupMock("acct:johndoe@example.com", { meterProvider });
1963
2052
  const counter = recorder.getMeasurement("webfinger.lookup");
1964
2053
  (0, node_assert_strict.ok)(counter != null);
1965
2054
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
@@ -1969,7 +2058,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1969
2058
  await t.step("buckets unknown resource schemes as 'other' to keep metric cardinality bounded", async () => {
1970
2059
  esm_default.removeRoutes();
1971
2060
  const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1972
- await lookupWebFinger("ssh://example.com/foo", { meterProvider });
2061
+ await lookupMock("ssh://example.com/foo", { meterProvider });
1973
2062
  const counter = recorder.getMeasurement("webfinger.lookup");
1974
2063
  (0, node_assert_strict.ok)(counter != null);
1975
2064
  (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.resource.scheme"], "other");
@@ -1978,7 +2067,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1978
2067
  esm_default.removeRoutes();
1979
2068
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1980
2069
  const [_unused, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1981
- await lookupWebFinger("acct:johndoe@example.com");
2070
+ await lookupMock("acct:johndoe@example.com");
1982
2071
  (0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup").length, 0);
1983
2072
  (0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup.duration").length, 0);
1984
2073
  });
@@ -1995,25 +2084,23 @@ async function lookupWebFingerInternal(resource, options = {}) {
1995
2084
  "Content-Type": "application/jrd+json",
1996
2085
  "Content-Length": "1"
1997
2086
  } }));
1998
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:alice@example.com", { allowPrivateAddress: true }), null);
2087
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), null);
1999
2088
  oversized = false;
2000
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:alice@example.com", { allowPrivateAddress: true }), { subject: "acct:alice@example.com" });
2089
+ (0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), { subject: "acct:alice@example.com" });
2001
2090
  } finally {
2002
2091
  esm_default.hardReset();
2003
2092
  }
2004
2093
  });
2005
2094
  (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
2095
  for (const result of [
2008
2096
  "throws",
2009
2097
  "empty",
2010
2098
  "private"
2011
2099
  ]) 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" ? [] : [{
2100
+ const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
2014
2101
  address: "127.0.0.1",
2015
2102
  family: 4
2016
- }]));
2103
+ }]);
2017
2104
  const records = [];
2018
2105
  await (0, _logtape_logtape.configure)({
2019
2106
  sinks: { buffer: (record) => records.push(record) },
@@ -2028,7 +2115,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2028
2115
  reset: true
2029
2116
  });
2030
2117
  try {
2031
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:alice@dns-failure.invalid"), null);
2118
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:alice@dns-failure.invalid", {}, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
2032
2119
  const failures = records.filter((r) => r.rawMessage !== "Fetching WebFinger resource descriptor from {url}...");
2033
2120
  (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
2121
  const { error } = failures[0].properties;
@@ -2036,7 +2123,6 @@ async function lookupWebFingerInternal(resource, options = {}) {
2036
2123
  (0, node_assert_strict.deepStrictEqual)(error.reason, result === "private" ? "disallowed" : "dns");
2037
2124
  } finally {
2038
2125
  await (0, _logtape_logtape.reset)();
2039
- node_dns_promises.default.lookup = originalLookup;
2040
2126
  }
2041
2127
  });
2042
2128
  });
@@ -2045,7 +2131,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2045
2131
  sanitizeResources: false
2046
2132
  }, async (t) => {
2047
2133
  await t.step("network_error", async () => {
2048
- const outcome = await lookupWebFinger("acct:johndoe@fedify-test.internal", { withDiagnostics: true });
2134
+ const outcome = await lookupMock("acct:johndoe@fedify-test.internal", { withDiagnostics: true });
2049
2135
  (0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
2050
2136
  (0, node_assert_strict.deepStrictEqual)(outcome.result, "network_error");
2051
2137
  (0, node_assert_strict.deepStrictEqual)("statusCode" in outcome, false, "no HTTP response means no status code");
@@ -2059,7 +2145,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2059
2145
  links: []
2060
2146
  };
2061
2147
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
2062
- const outcome = await lookupWebFinger("acct:johndoe@example.com", { withDiagnostics: true });
2148
+ const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
2063
2149
  (0, node_assert_strict.deepStrictEqual)(outcome.resource, expected);
2064
2150
  (0, node_assert_strict.deepStrictEqual)(outcome.result, "found");
2065
2151
  (0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
@@ -2068,7 +2154,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2068
2154
  await t.step("not_found", async () => {
2069
2155
  esm_default.removeRoutes();
2070
2156
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
2071
- const outcome = await lookupWebFinger("acct:johndoe@example.com", { withDiagnostics: true });
2157
+ const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
2072
2158
  (0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
2073
2159
  (0, node_assert_strict.deepStrictEqual)(outcome.result, "not_found");
2074
2160
  (0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 404);
@@ -2077,7 +2163,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2077
2163
  await t.step("invalid", async () => {
2078
2164
  esm_default.removeRoutes();
2079
2165
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
2080
- const outcome = await lookupWebFinger("acct:johndoe@example.com", { withDiagnostics: true });
2166
+ const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
2081
2167
  (0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
2082
2168
  (0, node_assert_strict.deepStrictEqual)(outcome.result, "invalid");
2083
2169
  (0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
@@ -2086,7 +2172,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2086
2172
  await t.step("invalid: a literal JSON null body is not a resource descriptor", async () => {
2087
2173
  esm_default.removeRoutes();
2088
2174
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "null" });
2089
- const outcome = await lookupWebFinger("acct:johndoe@example.com", { withDiagnostics: true });
2175
+ const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
2090
2176
  (0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
2091
2177
  (0, node_assert_strict.deepStrictEqual)(outcome.result, "invalid");
2092
2178
  (0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
@@ -2095,7 +2181,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2095
2181
  await t.step("invalid: a JSON array body is not a resource descriptor", async () => {
2096
2182
  esm_default.removeRoutes();
2097
2183
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "[]" });
2098
- const outcome = await lookupWebFinger("acct:johndoe@example.com", { withDiagnostics: true });
2184
+ const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
2099
2185
  (0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
2100
2186
  (0, node_assert_strict.deepStrictEqual)(outcome.result, "invalid");
2101
2187
  (0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
@@ -2104,7 +2190,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
2104
2190
  await t.step("error", async () => {
2105
2191
  esm_default.removeRoutes();
2106
2192
  esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
2107
- const outcome = await lookupWebFinger("acct:johndoe@example.com", { withDiagnostics: true });
2193
+ const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
2108
2194
  (0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
2109
2195
  (0, node_assert_strict.deepStrictEqual)(outcome.result, "error");
2110
2196
  (0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 500);
@@ -2123,10 +2209,296 @@ async function lookupWebFingerInternal(resource, options = {}) {
2123
2209
  links: []
2124
2210
  };
2125
2211
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
2126
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { userAgent: "test-agent" }), expected);
2212
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", {
2213
+ userAgent: "test-agent",
2214
+ allowPrivateAddress: true
2215
+ }), expected);
2127
2216
  } finally {
2128
2217
  esm_default.removeRoutes();
2129
2218
  esm_default.hardReset();
2130
2219
  }
2131
2220
  });
2221
+ function recordHops(respond) {
2222
+ const events = [];
2223
+ const unsettled = [];
2224
+ let opens = 0;
2225
+ let closes = 0;
2226
+ let responses = 0;
2227
+ return {
2228
+ events,
2229
+ async openHop(url) {
2230
+ opens++;
2231
+ events.push(`open ${url}`);
2232
+ const states = [];
2233
+ let closed = false;
2234
+ return {
2235
+ url: new URL(url),
2236
+ async fetch(input) {
2237
+ if (closed) throw new TypeError("The fetch session is closed.");
2238
+ const target = new URL(input instanceof Request ? input.url : input);
2239
+ const id = ++responses;
2240
+ events.push(`fetch ${target.href} #${id}`);
2241
+ const fake = respond(target);
2242
+ const state = {
2243
+ id,
2244
+ settled: false
2245
+ };
2246
+ states.push(state);
2247
+ let sent = false;
2248
+ const body = new ReadableStream({
2249
+ pull(controller) {
2250
+ if (!sent && fake.body != null && fake.body !== "") {
2251
+ sent = true;
2252
+ controller.enqueue(new TextEncoder().encode(fake.body));
2253
+ return;
2254
+ }
2255
+ state.settled = true;
2256
+ events.push(`consumed #${id}`);
2257
+ controller.close();
2258
+ },
2259
+ cancel() {
2260
+ state.settled = true;
2261
+ events.push(`cancel #${id}`);
2262
+ if (fake.cancelFails) throw new Error("Cancellation failed");
2263
+ if (fake.cancelNeverSettles) return new Promise(() => {});
2264
+ }
2265
+ }, { highWaterMark: 0 });
2266
+ const response = new Response(body, {
2267
+ status: fake.status ?? 200,
2268
+ headers: fake.headers
2269
+ });
2270
+ if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
2271
+ return response;
2272
+ },
2273
+ async close() {
2274
+ if (closed) return;
2275
+ closed = true;
2276
+ closes++;
2277
+ for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
2278
+ events.push(`close ${url}`);
2279
+ }
2280
+ };
2281
+ },
2282
+ assertInvariants() {
2283
+ (0, node_assert_strict.deepStrictEqual)(closes, opens);
2284
+ (0, node_assert_strict.deepStrictEqual)(unsettled, []);
2285
+ }
2286
+ };
2287
+ }
2288
+ (0, _fedify_fixture.test)("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
2289
+ const descriptor = {
2290
+ subject: "acct:johndoe@example.com",
2291
+ links: []
2292
+ };
2293
+ const first = "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
2294
+ const second = "https://example.com/.well-known/webfinger2";
2295
+ await t.step("redirect", async () => {
2296
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
2297
+ status: 302,
2298
+ headers: { Location: "/.well-known/webfinger2" },
2299
+ body: "redirecting",
2300
+ url: url.href.replace("example.com", "93.184.215.14")
2301
+ } : { body: JSON.stringify(descriptor) });
2302
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
2303
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2304
+ `open ${first}`,
2305
+ `fetch ${first} #1`,
2306
+ "cancel #1",
2307
+ `close ${first}`,
2308
+ `open ${second}`,
2309
+ `fetch ${second} #2`,
2310
+ "consumed #2",
2311
+ `close ${second}`
2312
+ ]);
2313
+ recorder.assertInvariants();
2314
+ });
2315
+ await t.step("non-2xx response", async () => {
2316
+ const recorder = recordHops(() => ({
2317
+ status: 404,
2318
+ body: "not found"
2319
+ }));
2320
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), null);
2321
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2322
+ `open ${first}`,
2323
+ `fetch ${first} #1`,
2324
+ "cancel #1",
2325
+ `close ${first}`
2326
+ ]);
2327
+ recorder.assertInvariants();
2328
+ });
2329
+ await t.step("too many redirections", async () => {
2330
+ const recorder = recordHops(() => ({
2331
+ status: 302,
2332
+ headers: { Location: "/.well-known/webfinger2" },
2333
+ body: "redirecting"
2334
+ }));
2335
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", { maxRedirection: 1 }, { openHop: recorder.openHop }), null);
2336
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2337
+ `open ${first}`,
2338
+ `fetch ${first} #1`,
2339
+ "cancel #1",
2340
+ `close ${first}`,
2341
+ `open ${second}`,
2342
+ `fetch ${second} #2`,
2343
+ "cancel #2",
2344
+ `close ${second}`
2345
+ ]);
2346
+ recorder.assertInvariants();
2347
+ });
2348
+ await t.step("failed redirect body cancellation", async () => {
2349
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
2350
+ status: 302,
2351
+ headers: { Location: "/.well-known/webfinger2" },
2352
+ body: "redirecting",
2353
+ cancelFails: true
2354
+ } : { body: JSON.stringify(descriptor) });
2355
+ const records = [];
2356
+ await (0, _logtape_logtape.configure)({
2357
+ sinks: { buffer: (record) => records.push(record) },
2358
+ loggers: [{
2359
+ category: "fedify",
2360
+ sinks: ["buffer"],
2361
+ lowestLevel: "debug"
2362
+ }, {
2363
+ category: ["logtape", "meta"],
2364
+ sinks: []
2365
+ }],
2366
+ reset: true
2367
+ });
2368
+ try {
2369
+ (0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
2370
+ const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
2371
+ (0, node_assert_strict.deepStrictEqual)(failures.map((r) => [r.level, r.properties.url]), [["debug", first]]);
2372
+ const { error } = failures[0].properties;
2373
+ (0, node_assert_strict.ok)(error instanceof Error);
2374
+ (0, node_assert_strict.deepStrictEqual)(error.message, "Cancellation failed");
2375
+ } finally {
2376
+ await (0, _logtape_logtape.reset)();
2377
+ }
2378
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2379
+ `open ${first}`,
2380
+ `fetch ${first} #1`,
2381
+ "cancel #1",
2382
+ `close ${first}`,
2383
+ `open ${second}`,
2384
+ `fetch ${second} #2`,
2385
+ "consumed #2",
2386
+ `close ${second}`
2387
+ ]);
2388
+ recorder.assertInvariants();
2389
+ });
2390
+ await t.step("never-settling body cancellation", async () => {
2391
+ const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
2392
+ status: 302,
2393
+ headers: { Location: "/.well-known/webfinger2" },
2394
+ body: "redirecting",
2395
+ cancelNeverSettles: true
2396
+ } : {
2397
+ status: 404,
2398
+ body: "not found",
2399
+ cancelNeverSettles: true
2400
+ });
2401
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop })), null);
2402
+ (0, node_assert_strict.deepStrictEqual)(recorder.events, [
2403
+ `open ${first}`,
2404
+ `fetch ${first} #1`,
2405
+ "cancel #1",
2406
+ `close ${first}`,
2407
+ `open ${second}`,
2408
+ `fetch ${second} #2`,
2409
+ "cancel #2",
2410
+ `close ${second}`
2411
+ ]);
2412
+ recorder.assertInvariants();
2413
+ });
2414
+ });
2415
+ /**
2416
+ * Rejects if the given promise does not settle within the timeout, so that
2417
+ * a hang fails the test instead of blocking it.
2418
+ */
2419
+ async function settleWithin(promise, ms = 5e3) {
2420
+ let timer;
2421
+ try {
2422
+ return await Promise.race([promise, new Promise((_, reject) => {
2423
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
2424
+ })]);
2425
+ } finally {
2426
+ clearTimeout(timer);
2427
+ }
2428
+ }
2429
+ function neverCancellableResponse(init) {
2430
+ return new Response(new ReadableStream({
2431
+ start(controller) {
2432
+ controller.enqueue(new TextEncoder().encode("never cancelled"));
2433
+ },
2434
+ cancel: () => new Promise(() => {})
2435
+ }), init);
2436
+ }
2437
+ (0, _fedify_fixture.test)("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
2438
+ const descriptor = {
2439
+ subject: "acct:johndoe@example.com",
2440
+ links: []
2441
+ };
2442
+ esm_default.mockGlobal();
2443
+ try {
2444
+ await t.step("redirect", async () => {
2445
+ esm_default.removeRoutes();
2446
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({
2447
+ status: 302,
2448
+ headers: { Location: "/.well-known/webfinger2" }
2449
+ }));
2450
+ esm_default.get("https://example.com/.well-known/webfinger2", () => new Response(JSON.stringify(descriptor), { headers: { "Content-Type": "application/jrd+json" } }));
2451
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), descriptor);
2452
+ });
2453
+ await t.step("non-2xx response", async () => {
2454
+ esm_default.removeRoutes();
2455
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({ status: 404 }));
2456
+ (0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), null);
2457
+ });
2458
+ } finally {
2459
+ esm_default.hardReset();
2460
+ }
2461
+ });
2462
+ (0, node_test.test)("lookupWebFinger() records network_error for rejected hop addresses", async () => {
2463
+ for (const lookup of [
2464
+ () => Promise.reject(/* @__PURE__ */ new Error("DNS unavailable")),
2465
+ () => Promise.resolve([]),
2466
+ () => Promise.resolve([{
2467
+ address: "127.0.0.1",
2468
+ family: 4
2469
+ }])
2470
+ ]) {
2471
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
2472
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerWithDependencies("acct:alice@dns-failure.invalid", { meterProvider }, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
2473
+ const counter = recorder.getMeasurement("webfinger.lookup");
2474
+ (0, node_assert_strict.ok)(counter != null);
2475
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
2476
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "dns-failure.invalid");
2477
+ (0, node_assert_strict.deepStrictEqual)("http.response.status_code" in counter.attributes, false);
2478
+ }
2479
+ });
2480
+ (0, node_test.test)("WebFinger diagnostics classify non-object JSON after closing the hop", async () => {
2481
+ for (const body of [
2482
+ "null",
2483
+ "[]",
2484
+ "42",
2485
+ "\"text\"",
2486
+ "true"
2487
+ ]) {
2488
+ const hops = recordHops(() => ({ body }));
2489
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
2490
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFingerWithDependencies("acct:alice@example.com", {
2491
+ withDiagnostics: true,
2492
+ meterProvider
2493
+ }, { openHop: hops.openHop }), {
2494
+ resource: null,
2495
+ result: "invalid",
2496
+ statusCode: 200,
2497
+ remoteHost: "example.com"
2498
+ });
2499
+ (0, node_assert_strict.deepStrictEqual)(hops.events.at(-1), "close https://example.com/.well-known/webfinger?resource=acct%3Aalice%40example.com");
2500
+ hops.assertInvariants();
2501
+ (0, node_assert_strict.deepStrictEqual)(recorder.getMeasurement("webfinger.lookup")?.attributes["webfinger.lookup.result"], "invalid");
2502
+ }
2503
+ });
2132
2504
  //#endregion