@fedify/webfinger 2.3.0-dev.994 → 2.3.0-pr.809.36

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.
@@ -5,7 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
8
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
9
  var __copyProps = (to, from, except, desc) => {
10
10
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
11
  key = keys[i];
@@ -1228,7 +1228,7 @@ var esm_default = new class FetchMock {
1228
1228
  //#endregion
1229
1229
  //#region deno.json
1230
1230
  var name = "@fedify/webfinger";
1231
- var version = "2.3.0-dev.994+9071ca0a";
1231
+ var version = "2.3.0-pr.809.36+c592d116";
1232
1232
  //#endregion
1233
1233
  //#region src/lookup.ts
1234
1234
  const logger = (0, _logtape_logtape.getLogger)([
@@ -1237,6 +1237,58 @@ const logger = (0, _logtape_logtape.getLogger)([
1237
1237
  "lookup"
1238
1238
  ]);
1239
1239
  const DEFAULT_MAX_REDIRECTION = 5;
1240
+ const WEBFINGER_HISTOGRAM_BUCKETS = [
1241
+ 5,
1242
+ 10,
1243
+ 25,
1244
+ 50,
1245
+ 75,
1246
+ 100,
1247
+ 250,
1248
+ 500,
1249
+ 750,
1250
+ 1e3,
1251
+ 2500,
1252
+ 5e3,
1253
+ 7500,
1254
+ 1e4
1255
+ ];
1256
+ const webFingerInstruments = /* @__PURE__ */ new WeakMap();
1257
+ function getWebFingerInstruments(meterProvider) {
1258
+ let instruments = webFingerInstruments.get(meterProvider);
1259
+ if (instruments == null) {
1260
+ const meter = meterProvider.getMeter(name, version);
1261
+ instruments = {
1262
+ lookup: meter.createCounter("webfinger.lookup", {
1263
+ description: "Outgoing WebFinger lookup attempts.",
1264
+ unit: "{lookup}"
1265
+ }),
1266
+ lookupDuration: meter.createHistogram("webfinger.lookup.duration", {
1267
+ description: "Duration of outgoing WebFinger lookups.",
1268
+ unit: "ms",
1269
+ advice: { explicitBucketBoundaries: [...WEBFINGER_HISTOGRAM_BUCKETS] }
1270
+ })
1271
+ };
1272
+ webFingerInstruments.set(meterProvider, instruments);
1273
+ }
1274
+ return instruments;
1275
+ }
1276
+ function getResourceScheme(resource) {
1277
+ if (typeof resource === "string") {
1278
+ const colon = resource.indexOf(":");
1279
+ return colon > 0 ? resource.substring(0, colon).toLowerCase() : "";
1280
+ }
1281
+ return resource.protocol.replace(/:$/, "").toLowerCase();
1282
+ }
1283
+ const WEBFINGER_LOOKUP_SCHEME_WHITELIST = new Set([
1284
+ "acct",
1285
+ "http",
1286
+ "https",
1287
+ "mailto"
1288
+ ]);
1289
+ function getMetricResourceScheme(scheme) {
1290
+ return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
1291
+ }
1240
1292
  /**
1241
1293
  * Looks up a WebFinger resource.
1242
1294
  * @param resource The resource URL to look up.
@@ -1245,17 +1297,25 @@ const DEFAULT_MAX_REDIRECTION = 5;
1245
1297
  * @since 0.2.0
1246
1298
  */
1247
1299
  async function lookupWebFinger(resource, options = {}) {
1248
- return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("webfinger.lookup", {
1300
+ const tracer = (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version);
1301
+ const scheme = getResourceScheme(resource);
1302
+ return await tracer.startActiveSpan("webfinger.lookup", {
1249
1303
  kind: _opentelemetry_api.SpanKind.CLIENT,
1250
1304
  attributes: {
1251
1305
  "webfinger.resource": resource.toString(),
1252
- "webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
1306
+ "webfinger.resource.scheme": scheme
1253
1307
  }
1254
1308
  }, async (span) => {
1309
+ const meterProvider = options.meterProvider;
1310
+ const start = meterProvider == null ? 0 : performance.now();
1311
+ let outcome = {
1312
+ resource: null,
1313
+ result: "error"
1314
+ };
1255
1315
  try {
1256
- const result = await lookupWebFingerInternal(resource, options);
1257
- span.setStatus({ code: result === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
1258
- return result;
1316
+ outcome = await lookupWebFingerInternal(resource, options);
1317
+ span.setStatus({ code: outcome.resource === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
1318
+ return outcome.resource;
1259
1319
  } catch (error) {
1260
1320
  span.setStatus({
1261
1321
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -1263,19 +1323,41 @@ async function lookupWebFinger(resource, options = {}) {
1263
1323
  });
1264
1324
  throw error;
1265
1325
  } finally {
1326
+ if (meterProvider != null) recordWebFingerLookup(meterProvider, Math.max(0, performance.now() - start), scheme, outcome);
1266
1327
  span.end();
1267
1328
  }
1268
1329
  });
1269
1330
  }
1331
+ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
1332
+ const attributes = {
1333
+ "webfinger.lookup.result": outcome.result,
1334
+ "webfinger.resource.scheme": getMetricResourceScheme(scheme)
1335
+ };
1336
+ if (outcome.remoteHost != null) attributes["activitypub.remote.host"] = outcome.remoteHost;
1337
+ if (outcome.statusCode != null) attributes["http.response.status_code"] = outcome.statusCode;
1338
+ const instruments = getWebFingerInstruments(meterProvider);
1339
+ instruments.lookup.add(1, attributes);
1340
+ instruments.lookupDuration.record(durationMs, attributes);
1341
+ }
1270
1342
  async function lookupWebFingerInternal(resource, options = {}) {
1271
1343
  if (typeof resource === "string") resource = new URL(resource);
1272
1344
  let protocol = "https:";
1273
1345
  let server;
1274
- if (resource.protocol === "acct:") {
1346
+ if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
1275
1347
  const atPos = resource.pathname.lastIndexOf("@");
1276
- if (atPos < 0) return null;
1348
+ if (atPos < 0) return {
1349
+ resource: null,
1350
+ result: "invalid"
1351
+ };
1277
1352
  server = resource.pathname.substring(atPos + 1);
1278
- if (server === "") return null;
1353
+ if (server === "" || /[/?#]/.test(server)) return {
1354
+ resource: null,
1355
+ result: "invalid"
1356
+ };
1357
+ if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
1358
+ resource: null,
1359
+ result: "invalid"
1360
+ };
1279
1361
  } else {
1280
1362
  protocol = resource.protocol;
1281
1363
  server = resource.host;
@@ -1284,6 +1366,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
1284
1366
  url.searchParams.set("resource", resource.href);
1285
1367
  let redirected = 0;
1286
1368
  while (true) {
1369
+ const remoteHost = url.host;
1287
1370
  logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
1288
1371
  let response;
1289
1372
  if (options.allowPrivateAddress !== true) try {
@@ -1291,7 +1374,11 @@ async function lookupWebFingerInternal(resource, options = {}) {
1291
1374
  } catch (e) {
1292
1375
  if (e instanceof _fedify_vocab_runtime.UrlError) {
1293
1376
  logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
1294
- return null;
1377
+ return {
1378
+ resource: null,
1379
+ result: "network_error",
1380
+ remoteHost
1381
+ };
1295
1382
  }
1296
1383
  throw e;
1297
1384
  }
@@ -1309,22 +1396,50 @@ async function lookupWebFingerInternal(resource, options = {}) {
1309
1396
  url: url.href,
1310
1397
  error
1311
1398
  });
1312
- return null;
1399
+ return {
1400
+ resource: null,
1401
+ result: "network_error",
1402
+ remoteHost
1403
+ };
1313
1404
  }
1314
1405
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
1315
1406
  redirected++;
1316
1407
  const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
1317
- if (redirected >= maxRedirection) {
1408
+ if (redirected > maxRedirection) {
1318
1409
  logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
1319
- return null;
1410
+ return {
1411
+ resource: null,
1412
+ result: "invalid",
1413
+ statusCode: response.status,
1414
+ remoteHost
1415
+ };
1416
+ }
1417
+ let redirectedUrl;
1418
+ try {
1419
+ redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
1420
+ } catch (e) {
1421
+ logger.error("Invalid Location header while following WebFinger redirect: {error}", {
1422
+ url: url.href,
1423
+ error: e
1424
+ });
1425
+ return {
1426
+ resource: null,
1427
+ result: "invalid",
1428
+ statusCode: response.status,
1429
+ remoteHost
1430
+ };
1320
1431
  }
1321
- const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
1322
1432
  if (redirectedUrl.protocol !== url.protocol) {
1323
1433
  logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
1324
1434
  protocol: url.protocol,
1325
1435
  redirectedProtocol: redirectedUrl.protocol
1326
1436
  });
1327
- return null;
1437
+ return {
1438
+ resource: null,
1439
+ result: "invalid",
1440
+ statusCode: response.status,
1441
+ remoteHost
1442
+ };
1328
1443
  }
1329
1444
  url = redirectedUrl;
1330
1445
  continue;
@@ -1335,14 +1450,29 @@ async function lookupWebFingerInternal(resource, options = {}) {
1335
1450
  status: response.status,
1336
1451
  statusText: response.statusText
1337
1452
  });
1338
- return null;
1453
+ return {
1454
+ resource: null,
1455
+ result: response.status === 404 || response.status === 410 ? "not_found" : "error",
1456
+ statusCode: response.status,
1457
+ remoteHost
1458
+ };
1339
1459
  }
1340
1460
  try {
1341
- return await response.json();
1461
+ return {
1462
+ resource: await response.json(),
1463
+ result: "found",
1464
+ statusCode: response.status,
1465
+ remoteHost
1466
+ };
1342
1467
  } catch (e) {
1343
1468
  if (e instanceof SyntaxError) {
1344
1469
  logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
1345
- return null;
1470
+ return {
1471
+ resource: null,
1472
+ result: "invalid",
1473
+ statusCode: response.status,
1474
+ remoteHost
1475
+ };
1346
1476
  }
1347
1477
  throw e;
1348
1478
  }
@@ -1360,52 +1490,64 @@ async function lookupWebFingerInternal(resource, options = {}) {
1360
1490
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger(new URL("acct:johndoe")), null);
1361
1491
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@"), null);
1362
1492
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger(new URL("acct:johndoe@")), null);
1493
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com/exploit"), null);
1494
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com?x=1"), null);
1495
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com#frag"), null);
1363
1496
  });
1364
1497
  await t.step("connection refused", async () => {
1365
1498
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@fedify-test.internal"), null);
1366
1499
  (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://fedify-test.internal/foo"), null);
1367
1500
  });
1368
1501
  esm_default.spyGlobal();
1369
- esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1370
- await t.step("not found", async () => {
1371
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1372
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), null);
1373
- });
1374
- const expected = {
1375
- subject: "acct:johndoe@example.com",
1376
- links: []
1377
- };
1378
- esm_default.removeRoutes();
1379
- esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1380
- await t.step("acct", async () => {
1381
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1382
- });
1383
- const expected2 = {
1384
- subject: "https://example.com/foo",
1385
- links: []
1386
- };
1387
- esm_default.removeRoutes();
1388
- esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
1389
- await t.step("https", async () => {
1390
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), expected2);
1391
- });
1392
- esm_default.removeRoutes();
1393
- esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1394
- await t.step("invalid response", async () => {
1395
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1396
- });
1397
- esm_default.removeRoutes();
1398
- esm_default.get("begin:https://localhost/.well-known/webfinger?", {
1399
- subject: "acct:test@localhost",
1400
- links: [{
1401
- rel: "self",
1402
- type: "application/activity+json",
1403
- href: "https://localhost/actor"
1404
- }]
1405
- });
1406
- await t.step("private address", async () => {
1407
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:test@localhost"), null);
1408
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:test@localhost", { allowPrivateAddress: true }), {
1502
+ try {
1503
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1504
+ await t.step("not found", async () => {
1505
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1506
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), null);
1507
+ });
1508
+ const expected = {
1509
+ subject: "acct:johndoe@example.com",
1510
+ links: []
1511
+ };
1512
+ esm_default.removeRoutes();
1513
+ esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1514
+ await t.step("acct", async () => {
1515
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1516
+ });
1517
+ const expected2 = {
1518
+ subject: "https://example.com/foo",
1519
+ links: []
1520
+ };
1521
+ esm_default.removeRoutes();
1522
+ esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
1523
+ await t.step("https", async () => {
1524
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://example.com/foo"), expected2);
1525
+ });
1526
+ const mailtoExpected = {
1527
+ subject: "mailto:juliet@example.com",
1528
+ links: []
1529
+ };
1530
+ esm_default.removeRoutes();
1531
+ esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com", { body: mailtoExpected });
1532
+ await t.step("mailto", async () => {
1533
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("mailto:juliet@example.com"), mailtoExpected);
1534
+ });
1535
+ const mailtoQueryExpected = {
1536
+ subject: "mailto:juliet@example.com?subject=Hi",
1537
+ links: []
1538
+ };
1539
+ esm_default.removeRoutes();
1540
+ esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com%3Fsubject%3DHi", { body: mailtoQueryExpected });
1541
+ await t.step("mailto with hfields", async () => {
1542
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
1543
+ });
1544
+ esm_default.removeRoutes();
1545
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1546
+ await t.step("invalid response", async () => {
1547
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1548
+ });
1549
+ esm_default.removeRoutes();
1550
+ esm_default.get("begin:https://localhost/.well-known/webfinger?", {
1409
1551
  subject: "acct:test@localhost",
1410
1552
  links: [{
1411
1553
  rel: "self",
@@ -1413,109 +1555,342 @@ async function lookupWebFingerInternal(resource, options = {}) {
1413
1555
  href: "https://localhost/actor"
1414
1556
  }]
1415
1557
  });
1558
+ await t.step("private address", async () => {
1559
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:test@localhost"), null);
1560
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:test@localhost", { allowPrivateAddress: true }), {
1561
+ subject: "acct:test@localhost",
1562
+ links: [{
1563
+ rel: "self",
1564
+ type: "application/activity+json",
1565
+ href: "https://localhost/actor"
1566
+ }]
1567
+ });
1568
+ });
1569
+ esm_default.removeRoutes();
1570
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1571
+ status: 302,
1572
+ headers: { Location: "/.well-known/webfinger2" }
1573
+ });
1574
+ esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
1575
+ await t.step("redirection", async () => {
1576
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1577
+ });
1578
+ esm_default.removeRoutes();
1579
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1580
+ status: 302,
1581
+ headers: { Location: "/.well-known/webfinger" }
1582
+ });
1583
+ await t.step("infinite redirection", async () => {
1584
+ (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com"), 2e3), null);
1585
+ });
1586
+ esm_default.removeRoutes();
1587
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1588
+ status: 302,
1589
+ headers: { Location: "ftp://example.com/" }
1590
+ });
1591
+ await t.step("redirection to different protocol", async () => {
1592
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1593
+ });
1594
+ esm_default.removeRoutes();
1595
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1596
+ status: 302,
1597
+ headers: { Location: "https://localhost/" }
1598
+ });
1599
+ await t.step("redirection to private address", async () => {
1600
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1601
+ });
1602
+ esm_default.removeRoutes();
1603
+ let redirectCount = 0;
1604
+ esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
1605
+ redirectCount++;
1606
+ if (redirectCount < 3) return {
1607
+ status: 302,
1608
+ headers: { Location: `/.well-known/webfinger?redirect=${redirectCount}` }
1609
+ };
1610
+ return { body: expected };
1611
+ });
1612
+ await t.step("custom maxRedirection", async () => {
1613
+ redirectCount = 0;
1614
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1615
+ redirectCount = 0;
1616
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
1617
+ redirectCount = 0;
1618
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1619
+ redirectCount = 0;
1620
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1621
+ });
1622
+ await t.step("maxRedirection: 1 follows exactly one redirect", async () => {
1623
+ esm_default.removeRoutes();
1624
+ let count = 0;
1625
+ esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
1626
+ count++;
1627
+ return count < 2 ? {
1628
+ status: 302,
1629
+ headers: { Location: "/.well-known/webfinger?after=1" }
1630
+ } : { body: expected };
1631
+ });
1632
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
1633
+ esm_default.removeRoutes();
1634
+ count = 0;
1635
+ esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
1636
+ count++;
1637
+ return count < 3 ? {
1638
+ status: 302,
1639
+ headers: { Location: `/.well-known/webfinger?after=${count}` }
1640
+ } : { body: expected };
1641
+ });
1642
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 1 }), null);
1643
+ });
1644
+ esm_default.removeRoutes();
1645
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
1646
+ const timeoutId = setTimeout(() => {
1647
+ resolve({ body: expected });
1648
+ }, 1e3);
1649
+ return () => clearTimeout(timeoutId);
1650
+ }));
1651
+ await t.step("request cancellation", async () => {
1652
+ const controller = new AbortController();
1653
+ const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1654
+ controller.abort();
1655
+ (0, node_assert_strict.deepStrictEqual)(await promise, null);
1656
+ });
1657
+ esm_default.removeRoutes();
1658
+ let redirectCount2 = 0;
1659
+ esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
1660
+ redirectCount2++;
1661
+ if (redirectCount2 === 1) return {
1662
+ status: 302,
1663
+ headers: { Location: "/.well-known/webfinger2" }
1664
+ };
1665
+ return new Promise((resolve) => {
1666
+ const timeoutId = setTimeout(() => {
1667
+ resolve({ body: expected });
1668
+ }, 1e3);
1669
+ return () => clearTimeout(timeoutId);
1670
+ });
1671
+ });
1672
+ await t.step("cancellation during redirection", async () => {
1673
+ const controller = new AbortController();
1674
+ const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1675
+ setTimeout(() => controller.abort(), 100);
1676
+ (0, node_assert_strict.deepStrictEqual)(await promise, null);
1677
+ });
1678
+ esm_default.removeRoutes();
1679
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
1680
+ const timeoutId = setTimeout(() => {
1681
+ resolve({ body: expected });
1682
+ }, 500);
1683
+ return () => clearTimeout(timeoutId);
1684
+ }));
1685
+ await t.step("cancellation with immediate abort", async () => {
1686
+ const controller = new AbortController();
1687
+ controller.abort();
1688
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal }), null);
1689
+ });
1690
+ esm_default.removeRoutes();
1691
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
1692
+ await t.step("successful request with signal", async () => {
1693
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
1694
+ });
1695
+ } finally {
1696
+ esm_default.removeRoutes();
1697
+ esm_default.hardReset();
1698
+ }
1699
+ }
1700
+ });
1701
+ (0, _fedify_fixture.test)("lookupWebFinger() records webfinger.lookup counter and duration", {
1702
+ sanitizeOps: false,
1703
+ sanitizeResources: false
1704
+ }, async (t) => {
1705
+ esm_default.spyGlobal();
1706
+ try {
1707
+ const expected = {
1708
+ subject: "acct:johndoe@example.com",
1709
+ links: []
1710
+ };
1711
+ await t.step("records result=found for a successful acct lookup", async () => {
1712
+ esm_default.removeRoutes();
1713
+ esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1714
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1715
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { meterProvider }), expected);
1716
+ const counters = recorder.getMeasurements("webfinger.lookup");
1717
+ (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1718
+ (0, node_assert_strict.deepStrictEqual)(counters[0].type, "counter");
1719
+ (0, node_assert_strict.deepStrictEqual)(counters[0].value, 1);
1720
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.lookup.result"], "found");
1721
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.resource.scheme"], "acct");
1722
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["activitypub.remote.host"], "example.com");
1723
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["http.response.status_code"], 200);
1724
+ const durations = recorder.getMeasurements("webfinger.lookup.duration");
1725
+ (0, node_assert_strict.deepStrictEqual)(durations.length, 1);
1726
+ (0, node_assert_strict.deepStrictEqual)(durations[0].type, "histogram");
1727
+ (0, node_assert_strict.deepStrictEqual)(durations[0].attributes["webfinger.lookup.result"], "found");
1728
+ (0, node_assert_strict.deepStrictEqual)(durations[0].attributes["webfinger.resource.scheme"], "acct");
1729
+ (0, node_assert_strict.ok)(typeof durations[0].value === "number" && durations[0].value >= 0);
1416
1730
  });
1417
- esm_default.removeRoutes();
1418
- esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1419
- status: 302,
1420
- headers: { Location: "/.well-known/webfinger2" }
1731
+ await t.step("records scheme=https for an https resource lookup", async () => {
1732
+ esm_default.removeRoutes();
1733
+ esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: {
1734
+ subject: "https://example.com/foo",
1735
+ links: []
1736
+ } });
1737
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1738
+ await lookupWebFinger("https://example.com/foo", { meterProvider });
1739
+ const counters = recorder.getMeasurements("webfinger.lookup");
1740
+ (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1741
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.resource.scheme"], "https");
1742
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.lookup.result"], "found");
1421
1743
  });
1422
- esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
1423
- await t.step("redirection", async () => {
1424
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1744
+ await t.step("records non-default ports for URL resources", async () => {
1745
+ esm_default.removeRoutes();
1746
+ esm_default.get("https://example.com:8443/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%3A8443%2Ffoo", { body: {
1747
+ subject: "https://example.com:8443/foo",
1748
+ links: []
1749
+ } });
1750
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1751
+ await lookupWebFinger("https://example.com:8443/foo", { meterProvider });
1752
+ const counter = recorder.getMeasurement("webfinger.lookup");
1753
+ (0, node_assert_strict.ok)(counter != null);
1754
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "example.com:8443");
1425
1755
  });
1426
- esm_default.removeRoutes();
1427
- esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1428
- status: 302,
1429
- headers: { Location: "/.well-known/webfinger" }
1756
+ await t.step("records result=not_found with status 404", async () => {
1757
+ esm_default.removeRoutes();
1758
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
1759
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1760
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { meterProvider }), null);
1761
+ const counters = recorder.getMeasurements("webfinger.lookup");
1762
+ (0, node_assert_strict.deepStrictEqual)(counters.length, 1);
1763
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.lookup.result"], "not_found");
1764
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["http.response.status_code"], 404);
1765
+ (0, node_assert_strict.deepStrictEqual)(counters[0].attributes["activitypub.remote.host"], "example.com");
1766
+ const durations = recorder.getMeasurements("webfinger.lookup.duration");
1767
+ (0, node_assert_strict.deepStrictEqual)(durations.length, 1);
1768
+ (0, node_assert_strict.deepStrictEqual)(durations[0].attributes["webfinger.lookup.result"], "not_found");
1430
1769
  });
1431
- await t.step("infinite redirection", async () => {
1432
- (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com"), 2e3), null);
1770
+ await t.step("records result=not_found with status 410", async () => {
1771
+ esm_default.removeRoutes();
1772
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 410 });
1773
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1774
+ await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1775
+ const counter = recorder.getMeasurement("webfinger.lookup");
1776
+ (0, node_assert_strict.ok)(counter != null);
1777
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "not_found");
1778
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["http.response.status_code"], 410);
1433
1779
  });
1434
- esm_default.removeRoutes();
1435
- esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1436
- status: 302,
1437
- headers: { Location: "ftp://example.com/" }
1780
+ await t.step("records result=error for non-2xx, non-404/410 HTTP responses", async () => {
1781
+ esm_default.removeRoutes();
1782
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
1783
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1784
+ await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1785
+ const counter = recorder.getMeasurement("webfinger.lookup");
1786
+ (0, node_assert_strict.ok)(counter != null);
1787
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "error");
1788
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["http.response.status_code"], 500);
1438
1789
  });
1439
- await t.step("redirection to different protocol", async () => {
1440
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1790
+ await t.step("records result=invalid for malformed JSON bodies", async () => {
1791
+ esm_default.removeRoutes();
1792
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
1793
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1794
+ await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1795
+ const counter = recorder.getMeasurement("webfinger.lookup");
1796
+ (0, node_assert_strict.ok)(counter != null);
1797
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
1798
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["http.response.status_code"], 200);
1441
1799
  });
1442
- esm_default.removeRoutes();
1443
- esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1444
- status: 302,
1445
- headers: { Location: "https://localhost/" }
1800
+ await t.step("records result=network_error when fetch never reaches the remote", async () => {
1801
+ esm_default.removeRoutes();
1802
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1803
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@fedify-test.internal", { meterProvider }), null);
1804
+ const counter = recorder.getMeasurement("webfinger.lookup");
1805
+ (0, node_assert_strict.ok)(counter != null);
1806
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
1807
+ (0, node_assert_strict.deepStrictEqual)("http.response.status_code" in counter.attributes, false, "no HTTP response means no status code attribute");
1808
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "fedify-test.internal");
1446
1809
  });
1447
- await t.step("redirection to private address", async () => {
1448
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), null);
1810
+ await t.step("records result=invalid for malformed acct: resources", async () => {
1811
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1812
+ (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe", { meterProvider }), null);
1813
+ const counter = recorder.getMeasurement("webfinger.lookup");
1814
+ (0, node_assert_strict.ok)(counter != null);
1815
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
1816
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.resource.scheme"], "acct");
1817
+ (0, node_assert_strict.deepStrictEqual)("activitypub.remote.host" in counter.attributes, false, "a malformed acct resource has no usable remote host");
1449
1818
  });
1450
- esm_default.removeRoutes();
1451
- let redirectCount = 0;
1452
- esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
1453
- redirectCount++;
1454
- if (redirectCount < 3) return {
1819
+ await t.step("records result=invalid when the redirect chain exceeds maxRedirection", async () => {
1820
+ esm_default.removeRoutes();
1821
+ esm_default.get("begin:https://example.com/.well-known/webfinger", {
1455
1822
  status: 302,
1456
- headers: { Location: `/.well-known/webfinger?redirect=${redirectCount}` }
1457
- };
1458
- return { body: expected };
1823
+ headers: { Location: "/.well-known/webfinger" }
1824
+ });
1825
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1826
+ (0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupWebFinger("acct:johndoe@example.com", {
1827
+ meterProvider,
1828
+ maxRedirection: 3
1829
+ }), 2e3), null);
1830
+ const counter = recorder.getMeasurement("webfinger.lookup");
1831
+ (0, node_assert_strict.ok)(counter != null);
1832
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
1833
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["http.response.status_code"], 302);
1834
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "example.com");
1459
1835
  });
1460
- await t.step("custom maxRedirection", async () => {
1461
- redirectCount = 0;
1462
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 2 }), null);
1463
- redirectCount = 0;
1464
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
1465
- redirectCount = 0;
1466
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com"), expected);
1836
+ await t.step("records result=invalid for cross-protocol redirects", async () => {
1837
+ esm_default.removeRoutes();
1838
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1839
+ status: 302,
1840
+ headers: { Location: "ftp://example.com/" }
1841
+ });
1842
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1843
+ await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1844
+ const counter = recorder.getMeasurement("webfinger.lookup");
1845
+ (0, node_assert_strict.ok)(counter != null);
1846
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
1847
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["http.response.status_code"], 302);
1467
1848
  });
1468
- esm_default.removeRoutes();
1469
- esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
1470
- const timeoutId = setTimeout(() => {
1471
- resolve({ body: expected });
1472
- }, 1e3);
1473
- return () => clearTimeout(timeoutId);
1474
- }));
1475
- await t.step("request cancellation", async () => {
1476
- const controller = new AbortController();
1477
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1478
- controller.abort();
1479
- (0, node_assert_strict.deepStrictEqual)(await promise, null);
1849
+ await t.step("records result=network_error when a redirect points to a private address", async () => {
1850
+ esm_default.removeRoutes();
1851
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1852
+ status: 302,
1853
+ headers: { Location: "https://localhost/" }
1854
+ });
1855
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1856
+ await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1857
+ const counter = recorder.getMeasurement("webfinger.lookup");
1858
+ (0, node_assert_strict.ok)(counter != null);
1859
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
1860
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "localhost", "remote.host reflects the latest URL we attempted, even after a redirect");
1480
1861
  });
1481
- esm_default.removeRoutes();
1482
- let redirectCount2 = 0;
1483
- esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
1484
- redirectCount2++;
1485
- if (redirectCount2 === 1) return {
1862
+ await t.step("records result=invalid for malformed Location headers", async () => {
1863
+ esm_default.removeRoutes();
1864
+ esm_default.get("begin:https://example.com/.well-known/webfinger?", {
1486
1865
  status: 302,
1487
- headers: { Location: "/.well-known/webfinger2" }
1488
- };
1489
- return new Promise((resolve) => {
1490
- const timeoutId = setTimeout(() => {
1491
- resolve({ body: expected });
1492
- }, 1e3);
1493
- return () => clearTimeout(timeoutId);
1866
+ headers: { Location: "http://[bad" }
1494
1867
  });
1868
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1869
+ await lookupWebFinger("acct:johndoe@example.com", { meterProvider });
1870
+ const counter = recorder.getMeasurement("webfinger.lookup");
1871
+ (0, node_assert_strict.ok)(counter != null);
1872
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
1873
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["http.response.status_code"], 302);
1874
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "example.com");
1495
1875
  });
1496
- await t.step("cancellation during redirection", async () => {
1497
- const controller = new AbortController();
1498
- const promise = lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal });
1499
- setTimeout(() => controller.abort(), 100);
1500
- (0, node_assert_strict.deepStrictEqual)(await promise, null);
1876
+ await t.step("buckets unknown resource schemes as 'other' to keep metric cardinality bounded", async () => {
1877
+ esm_default.removeRoutes();
1878
+ const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1879
+ await lookupWebFinger("ssh://example.com/foo", { meterProvider });
1880
+ const counter = recorder.getMeasurement("webfinger.lookup");
1881
+ (0, node_assert_strict.ok)(counter != null);
1882
+ (0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.resource.scheme"], "other");
1501
1883
  });
1502
- esm_default.removeRoutes();
1503
- esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
1504
- const timeoutId = setTimeout(() => {
1505
- resolve({ body: expected });
1506
- }, 500);
1507
- return () => clearTimeout(timeoutId);
1508
- }));
1509
- await t.step("cancellation with immediate abort", async () => {
1510
- const controller = new AbortController();
1511
- controller.abort();
1512
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal }), null);
1884
+ await t.step("omits measurements when no meterProvider is provided", async () => {
1885
+ esm_default.removeRoutes();
1886
+ esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
1887
+ const [_unused, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
1888
+ await lookupWebFinger("acct:johndoe@example.com");
1889
+ (0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup").length, 0);
1890
+ (0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup.duration").length, 0);
1513
1891
  });
1892
+ } finally {
1514
1893
  esm_default.removeRoutes();
1515
- esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
1516
- await t.step("successful request with signal", async () => {
1517
- (0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
1518
- });
1519
1894
  esm_default.hardReset();
1520
1895
  }
1521
1896
  });