@fedify/webfinger 2.3.15-dev.2482 → 2.3.15

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