@fedify/fedify 2.5.0-dev.2448 → 2.5.0-dev.2494

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.
Files changed (126) hide show
  1. package/README.md +3 -0
  2. package/dist/{builder-DbC8bn6k.mjs → builder-Y3pokaG2.mjs} +2 -2
  3. package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.cts} +0 -1
  4. package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.ts} +0 -1
  5. package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
  6. package/dist/compat/mod.d.cts +1 -1
  7. package/dist/compat/mod.d.ts +1 -1
  8. package/dist/compat/transformers.test.mjs +1 -1
  9. package/dist/{compound-proof-heeXW2_A.mjs → compound-proof-BM7AINht.mjs} +2 -2
  10. package/dist/{context-GlHrzdSP.d.ts → context-DQJE9YfH.d.ts} +29 -3
  11. package/dist/{context-BazwQrHn.d.cts → context-DSuL3YWx.d.cts} +29 -3
  12. package/dist/{deno-Dpewt_jJ.mjs → deno-D7blYLbb.mjs} +1 -1
  13. package/dist/docloader-DbBbDaY-.mjs +136 -0
  14. package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
  15. package/dist/federation/builder.test.mjs +1 -1
  16. package/dist/federation/compound-fanout.test.mjs +6 -3
  17. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  18. package/dist/federation/handler.test.mjs +5 -5
  19. package/dist/federation/hashlink-media.test.mjs +2 -2
  20. package/dist/federation/idempotency.test.mjs +2 -2
  21. package/dist/federation/inbox-actorless.test.mjs +5 -5
  22. package/dist/federation/inbox-report.test.mjs +5 -5
  23. package/dist/federation/inbox-view.test.mjs +1 -1
  24. package/dist/federation/inbox.test.mjs +1 -1
  25. package/dist/federation/metrics.test.mjs +1 -1
  26. package/dist/federation/middleware.test.mjs +468 -33
  27. package/dist/federation/mod.cjs +1 -1
  28. package/dist/federation/mod.d.cts +3 -3
  29. package/dist/federation/mod.d.ts +3 -3
  30. package/dist/federation/mod.js +1 -1
  31. package/dist/federation/mq.test.mjs +1 -1
  32. package/dist/federation/outgoing-proof.test.mjs +9 -6
  33. package/dist/federation/portable-actor.test.mjs +3 -3
  34. package/dist/federation/portable-chain.test.mjs +3 -3
  35. package/dist/federation/portable-collection.test.mjs +4 -4
  36. package/dist/federation/portable-delivery.test.mjs +2 -2
  37. package/dist/federation/portable-gateway-keys.test.mjs +11 -5
  38. package/dist/federation/portable-inbox.test.mjs +4 -4
  39. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  40. package/dist/federation/portable-route-activity.test.mjs +2 -2
  41. package/dist/federation/portable.test.mjs +3 -3
  42. package/dist/federation/send.test.mjs +502 -93
  43. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  44. package/dist/federation/tasks/tasks.test.mjs +2 -2
  45. package/dist/federation/temporal.test.mjs +1 -1
  46. package/dist/federation/webfinger.test.mjs +1 -1
  47. package/dist/{federation-D8YexkP1.cjs → federation-CFaTsSMN.cjs} +246 -108
  48. package/dist/{http-CAl7Num5.cjs → http-2SwIR4b-.cjs} +179 -110
  49. package/dist/{http-BBr-VLil.js → http-CrYxBKqG.js} +178 -109
  50. package/dist/{http-BZkyQIkb.mjs → http-CxKd10Lq.mjs} +204 -106
  51. package/dist/{inbox-wDuMKaFW.mjs → inbox-CM0UktG8.mjs} +2 -2
  52. package/dist/{inbox-view-NE9wXdLo.mjs → inbox-view-BJ5DBPFo.mjs} +1 -1
  53. package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
  54. package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
  55. package/dist/{kv-cache-CTQSfkmp.mjs → kv-cache-CIdGATck.mjs} +1 -1
  56. package/dist/{kv-cache-CjgCOz3i.cjs → kv-cache-Cd8xKv-x.cjs} +72 -27
  57. package/dist/{kv-cache-U7uz5nx4.js → kv-cache-DbgFe35y.js} +73 -28
  58. package/dist/{ld-Cel1RRZ5.d.cts → ld-CWVpo_NM.d.cts} +1 -1
  59. package/dist/{ld-CQFuiah9.mjs → ld-DUeBHFWH.mjs} +2 -2
  60. package/dist/{ld-CcuZ4NZa.d.ts → ld-Inagww4R.d.ts} +1 -1
  61. package/dist/{metrics-B-QxRSSR.mjs → metrics-B7TvoAP7.mjs} +1 -1
  62. package/dist/{middleware-BdlNa8BD.mjs → middleware-BE4JXx-U.mjs} +1 -1
  63. package/dist/{middleware-CuVa0UuS.mjs → middleware-BEaUojg7.mjs} +89 -49
  64. package/dist/{middleware-P-AZEQMn.js → middleware-CId6efsJ.js} +247 -109
  65. package/dist/{mod-DvWOv2Ho.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
  66. package/dist/{mod-BHQE-7WQ.d.ts → mod-B4kBw1-8.d.ts} +1 -1
  67. package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
  68. package/dist/{mod-B2783_3v.d.cts → mod-C1nT74IG.d.cts} +1 -1
  69. package/dist/{mod-DjZfJj9R.d.ts → mod-CIeBgsOC.d.ts} +1 -1
  70. package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
  71. package/dist/mod.cjs +5 -5
  72. package/dist/mod.d.cts +7 -7
  73. package/dist/mod.d.ts +7 -7
  74. package/dist/mod.js +5 -5
  75. package/dist/nodeinfo/client.test.mjs +337 -19
  76. package/dist/nodeinfo/handler.test.mjs +1 -1
  77. package/dist/nodeinfo/mod.cjs +1 -1
  78. package/dist/nodeinfo/mod.d.cts +1 -1
  79. package/dist/nodeinfo/mod.d.ts +1 -1
  80. package/dist/nodeinfo/mod.js +1 -1
  81. package/dist/otel/exporter.test.mjs +257 -1
  82. package/dist/otel/mod.cjs +36 -30
  83. package/dist/otel/mod.d.cts +13 -0
  84. package/dist/otel/mod.d.ts +13 -0
  85. package/dist/otel/mod.js +36 -30
  86. package/dist/{owner-ccKkQOAf.mjs → owner-CwijV0c5.mjs} +3 -3
  87. package/dist/{portable-collection-DUlQ3s2M.js → portable-collection-2jn_4V7Y.js} +1 -1
  88. package/dist/{portable-collection-CndnpXfo.mjs → portable-collection-B023xqhZ.mjs} +2 -2
  89. package/dist/{portable-collection-TC-kvkVI.cjs → portable-collection-BeS88JJp.cjs} +1 -1
  90. package/dist/{portable-key-D5tFOoV-.mjs → portable-key-BpBucRfb.mjs} +1 -1
  91. package/dist/{proof-DPoF06nz.mjs → proof-DQh2z9fx.mjs} +3 -3
  92. package/dist/{send-CF44O1Df.mjs → send-DN5HNBv-.mjs} +174 -76
  93. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  94. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  95. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  96. package/dist/sig/compound-proof.test.mjs +2 -2
  97. package/dist/sig/http.test.mjs +553 -2
  98. package/dist/sig/key.test.mjs +1 -1
  99. package/dist/sig/ld.test.mjs +1 -1
  100. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  101. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  102. package/dist/sig/mod.cjs +2 -2
  103. package/dist/sig/mod.d.cts +3 -3
  104. package/dist/sig/mod.d.ts +3 -3
  105. package/dist/sig/mod.js +2 -2
  106. package/dist/sig/owner.test.mjs +1 -1
  107. package/dist/sig/portable-collection.test.mjs +1 -1
  108. package/dist/sig/portable-interop.test.mjs +3 -3
  109. package/dist/sig/portable-key.test.mjs +5 -5
  110. package/dist/sig/proof.test.mjs +4 -4
  111. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  112. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  113. package/dist/sig/verification.test.mjs +3 -3
  114. package/dist/{temporal-XlZ4Wb94.mjs → temporal-yl7Z7A6y.mjs} +1 -1
  115. package/dist/testing/kv.test.mjs +1 -1
  116. package/dist/testing/mod.d.mts +26 -0
  117. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  118. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  119. package/dist/utils/docloader.test.mjs +344 -36
  120. package/dist/utils/kv-cache.test.mjs +1 -1
  121. package/dist/utils/mod.cjs +1 -1
  122. package/dist/utils/mod.d.cts +1 -1
  123. package/dist/utils/mod.d.ts +1 -1
  124. package/dist/utils/mod.js +1 -1
  125. package/package.json +8 -8
  126. package/dist/docloader-CpsDyqVN.mjs +0 -91
@@ -16,14 +16,14 @@ import "../std__assert-DvMDRPes.mjs";
16
16
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
17
17
  import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
18
18
  import { t as assert } from "../assert-D1Eafmd_.mjs";
19
- import { g as recordInboxActivity } from "../metrics-B-QxRSSR.mjs";
20
- import { _ as verifyJsonLd, a as compactJsonLd, m as signJsonLd, s as detachSignature } from "../ld-CQFuiah9.mjs";
21
- import { l as verifyObject, s as signObject } from "../proof-DPoF06nz.mjs";
22
- import { f as verifyRequest, l as signRequest } from "../http-BZkyQIkb.mjs";
19
+ import { g as recordInboxActivity } from "../metrics-B7TvoAP7.mjs";
20
+ import { _ as verifyJsonLd, a as compactJsonLd, m as signJsonLd, s as detachSignature } from "../ld-DUeBHFWH.mjs";
21
+ import { l as verifyObject, s as signObject } from "../proof-DQh2z9fx.mjs";
22
+ import { p as verifyRequest, u as signRequest } from "../http-CxKd10Lq.mjs";
23
23
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
24
- import { t as getAuthenticatedDocumentLoader } from "../docloader-CpsDyqVN.mjs";
25
- import { T as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-CuVa0UuS.mjs";
26
- import { t as doesActorOwnKey } from "../owner-ccKkQOAf.mjs";
24
+ import { n as getAuthenticatedDocumentLoader } from "../docloader-DbBbDaY-.mjs";
25
+ import { T as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-BEaUojg7.mjs";
26
+ import { t as doesActorOwnKey } from "../owner-CwijV0c5.mjs";
27
27
  import { t as CircuitBreaker } from "../circuit-breaker-XJ53md4S.mjs";
28
28
  import { t as TaskCodec } from "../codec-C6rTrpIp.mjs";
29
29
  import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-DW6YC_Pe.mjs";
@@ -34,11 +34,11 @@ import { Create, CryptographicKey, Note, Offer, Person, getTypeId, lookupObject
34
34
  import { SpanStatusCode, metrics } from "@opentelemetry/api";
35
35
  import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
36
36
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
37
- import dns from "node:dns/promises";
38
37
  import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
39
38
  import serialize from "json-canon";
40
39
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
41
40
  import { AsyncLocalStorage } from "node:async_hooks";
41
+ import dns from "node:dns/promises";
42
42
  //#region ../fixture/src/fixtures/example.com/create.json
43
43
  var create_default = {
44
44
  "@context": "https://www.w3.org/ns/activitystreams",
@@ -102,7 +102,7 @@ var person2_default = {
102
102
  };
103
103
  //#endregion
104
104
  //#region src/federation/middleware.test.ts
105
- const documentLoader = getDocumentLoader();
105
+ const documentLoader = getDocumentLoader({ allowPrivateAddress: true });
106
106
  let logtapeLock = Promise.resolve();
107
107
  async function withLogtapeLock(fn) {
108
108
  const run = logtapeLock.then(fn, fn);
@@ -657,7 +657,8 @@ test({
657
657
  const federation = createFederation({
658
658
  kv,
659
659
  documentLoaderFactory: () => rejectingLoader,
660
- contextLoaderFactory: () => mockDocumentLoader
660
+ contextLoaderFactory: () => mockDocumentLoader,
661
+ authenticatedDocumentLoaderFactory: (identity) => getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true })
661
662
  });
662
663
  let ctx = federation.createContext(new URL("https://example.com:1234/"), 123);
663
664
  assertEquals(ctx.data, 123);
@@ -768,7 +769,7 @@ test({
768
769
  documentUrl: "https://example.com/auth-check",
769
770
  document: true
770
771
  });
771
- assertEquals(await ctx.lookupObject("https://example.com/object"), null);
772
+ assertEquals(await withDnsLookup(() => Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")), () => ctx.lookupObject("https://example.com/object")), null);
772
773
  await assertRejects(() => ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({})), TypeError, "The activity to send must have at least one actor property.");
773
774
  await ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({ actor: new URL("https://example.com/users/handle") }));
774
775
  esm_default.get("https://example.com/object", () => new Response(JSON.stringify({
@@ -1017,7 +1018,7 @@ test({
1017
1018
  assertStrictEquals(clone.contextLoader, ctx.contextLoader);
1018
1019
  assertStrictEquals(clone.federation, ctx.federation);
1019
1020
  });
1020
- esm_default.get("https://example.com/.well-known/nodeinfo", (cl) => {
1021
+ esm_default.get("path:/.well-known/nodeinfo", (cl) => {
1021
1022
  const headers = cl.options.headers ?? {};
1022
1023
  assertEquals(new Headers(headers).get("User-Agent"), "CustomUserAgent/1.2.3");
1023
1024
  return new Response(JSON.stringify({ links: [{
@@ -1025,7 +1026,7 @@ test({
1025
1026
  href: "https://example.com/nodeinfo/2.1"
1026
1027
  }] }));
1027
1028
  });
1028
- esm_default.get("https://example.com/nodeinfo/2.1", (cl) => {
1029
+ esm_default.get("path:/nodeinfo/2.1", (cl) => {
1029
1030
  const headers = cl.options.headers ?? {};
1030
1031
  assertEquals(new Headers(headers).get("User-Agent"), "CustomUserAgent/1.2.3");
1031
1032
  return new Response(JSON.stringify({
@@ -1046,7 +1047,11 @@ test({
1046
1047
  kv,
1047
1048
  userAgent: "CustomUserAgent/1.2.3"
1048
1049
  }).createContext(new URL("https://example.com/"), 123);
1049
- assertEquals(await ctx.lookupNodeInfo("https://example.com/"), {
1050
+ const lookupExample = () => Promise.resolve([{
1051
+ address: "93.184.215.14",
1052
+ family: 4
1053
+ }]);
1054
+ assertEquals(await withDnsLookup(lookupExample, () => ctx.lookupNodeInfo("https://example.com/")), {
1050
1055
  software: {
1051
1056
  name: "foo",
1052
1057
  version: "1.2.3"
@@ -1058,7 +1063,7 @@ test({
1058
1063
  localComments: 456
1059
1064
  }
1060
1065
  });
1061
- assertEquals(await ctx.lookupNodeInfo("https://example.com/", { parse: "none" }), {
1066
+ assertEquals(await withDnsLookup(lookupExample, () => ctx.lookupNodeInfo("https://example.com/", { parse: "none" })), {
1062
1067
  software: {
1063
1068
  name: "foo",
1064
1069
  version: "1.2.3"
@@ -1258,6 +1263,408 @@ test({
1258
1263
  esm_default.hardReset();
1259
1264
  }
1260
1265
  });
1266
+ test("Federation.fetch() accepts unencoded path colons", async () => {
1267
+ const identifier = "https://pod.example/alice";
1268
+ const federation = createFederation({
1269
+ kv: new MemoryKvStore(),
1270
+ contextLoaderFactory: () => mockDocumentLoader
1271
+ });
1272
+ const dispatched = [];
1273
+ federation.setActorDispatcher("/users/{identifier}", (ctx, id) => {
1274
+ dispatched.push(id);
1275
+ assertEquals(ctx.url.pathname, "/users/https:%2F%2Fpod.example%2Falice");
1276
+ assertEquals(ctx.url.search, "?page=1");
1277
+ assertEquals(ctx.request.url, ctx.url.href);
1278
+ return new vocab.Person({ id: ctx.getActorUri(id) });
1279
+ });
1280
+ const request = new Request("https://local.example/users/https:%2F%2Fpod.example%2Falice?page=1", { headers: { Accept: "application/activity+json" } });
1281
+ const response = await federation.fetch(request, { contextData: void 0 });
1282
+ assertEquals(response.status, 200);
1283
+ assertEquals(dispatched, [identifier]);
1284
+ const canonical = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice";
1285
+ const body = await response.json();
1286
+ assert(body != null && typeof body === "object" && "id" in body);
1287
+ assertEquals(body.id, canonical);
1288
+ const ctx = federation.createContext(request, void 0);
1289
+ assertEquals(ctx.getActorUri(identifier).href, canonical);
1290
+ assertEquals(ctx.parseUri(new URL(canonical)), {
1291
+ type: "actor",
1292
+ identifier
1293
+ });
1294
+ assertEquals(ctx.parseUri(new URL(request.url)), null);
1295
+ assertEquals(ctx.parseUri(new URL(canonical.replace("local.example", "other.example"))), null);
1296
+ });
1297
+ test("Federation.fetch() verifies signatures over unencoded path colons", async (t) => {
1298
+ const identifier = "https://pod.example/alice";
1299
+ const canonical = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice/inbox";
1300
+ const encoded = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice/inbox?delivery=1";
1301
+ const normalized = encoded.replace("%3A", ":");
1302
+ for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) for (const target of [
1303
+ "encoded",
1304
+ "normalized",
1305
+ "changed after signing"
1306
+ ]) await t.step(`${spec}: ${target}`, async () => {
1307
+ const recipients = [];
1308
+ const federation = createFederation({
1309
+ kv: new MemoryKvStore(),
1310
+ documentLoaderFactory: () => mockDocumentLoader,
1311
+ contextLoaderFactory: () => mockDocumentLoader
1312
+ });
1313
+ federation.setActorDispatcher("/users/{identifier}", (ctx, id) => new vocab.Person({ id: ctx.getActorUri(id) })).setKeyPairsDispatcher(() => []);
1314
+ federation.setInboxListeners("/users/{identifier}/inbox").on(vocab.Create, (ctx) => {
1315
+ recipients.push(ctx.recipient);
1316
+ });
1317
+ const signed = await signRequest(new Request(target === "normalized" ? normalized : encoded, {
1318
+ method: "POST",
1319
+ headers: { "Content-Type": "application/activity+json" },
1320
+ body: JSON.stringify(create_default)
1321
+ }), rsaPrivateKey2, rsaPublicKey2.id, { spec });
1322
+ const request = target === "changed after signing" ? new Request(normalized, signed) : signed;
1323
+ const response = await federation.fetch(request, { contextData: void 0 });
1324
+ assertEquals(request.url, target === "encoded" ? encoded : normalized);
1325
+ const valid = target !== "changed after signing";
1326
+ assertEquals(response.status, valid ? 202 : 401);
1327
+ assertEquals(recipients, valid ? [identifier] : []);
1328
+ assertEquals(federation.createContext(new URL(canonical), void 0).getInboxUri(identifier).href, canonical);
1329
+ });
1330
+ });
1331
+ test("RequestContext signature checks", async (t) => {
1332
+ const [tracerProvider, exporter] = createTestTracerProvider();
1333
+ const keyId = rsaPublicKey3.id.href;
1334
+ const ownerId = rsaPublicKey3.ownerId.href;
1335
+ const request = await signRequest(new Request("https://example.com/"), rsaPrivateKey3, rsaPublicKey3.id);
1336
+ function setup(options = {}) {
1337
+ const loads = {
1338
+ key: 0,
1339
+ owner: 0
1340
+ };
1341
+ const loader = async (url, loaderOptions) => {
1342
+ if (url === keyId) loads.key++;
1343
+ else if (url === ownerId) loads.owner++;
1344
+ await options.gate;
1345
+ return await (options.load ?? mockDocumentLoader)(url, loaderOptions);
1346
+ };
1347
+ const federation = createFederation({
1348
+ kv: new MemoryKvStore(),
1349
+ documentLoaderFactory: () => loader,
1350
+ contextLoaderFactory: () => mockDocumentLoader,
1351
+ tracerProvider: options.tracerProvider ?? tracerProvider
1352
+ });
1353
+ return {
1354
+ ctx: federation.createContext(options.request ?? new Request(request), void 0),
1355
+ loads,
1356
+ loader,
1357
+ federation
1358
+ };
1359
+ }
1360
+ try {
1361
+ for (const mode of [
1362
+ "keys",
1363
+ "owners",
1364
+ "warm owners",
1365
+ "mixed"
1366
+ ]) await t.step(`coalesces ${mode}`, async () => {
1367
+ exporter.clear();
1368
+ let release;
1369
+ const { ctx, loads, loader } = setup({ gate: new Promise((resolve) => release = resolve) });
1370
+ if (mode === "warm owners") {
1371
+ release();
1372
+ assert(await ctx.getSignedKey());
1373
+ }
1374
+ const results = Promise.all(Array.from({ length: 8 }, (_, i) => {
1375
+ const options = i % 2 === 0 ? {} : {
1376
+ documentLoader: loader,
1377
+ contextLoader: ctx.contextLoader,
1378
+ tracerProvider
1379
+ };
1380
+ return mode === "keys" || mode === "mixed" && i % 2 === 0 ? ctx.getSignedKey(options) : ctx.getSignedKeyOwner(options);
1381
+ }));
1382
+ release();
1383
+ for (const result of await results) assert(result != null);
1384
+ assertEquals(loads, {
1385
+ key: 1,
1386
+ owner: mode === "keys" ? 0 : 1
1387
+ });
1388
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1389
+ assert(await ctx.getSignedKey());
1390
+ if (mode !== "keys") assert(await ctx.getSignedKeyOwner());
1391
+ assertEquals(loads, {
1392
+ key: 1,
1393
+ owner: mode === "keys" ? 0 : 1
1394
+ });
1395
+ });
1396
+ await t.step("memoizes null keys", async () => {
1397
+ exporter.clear();
1398
+ const { ctx, loads } = setup({ load: () => Promise.reject(/* @__PURE__ */ new Error("unavailable")) });
1399
+ for (let i = 0; i < 3; i++) {
1400
+ assertEquals(await ctx.getSignedKey(), null);
1401
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1402
+ }
1403
+ assertEquals(loads, {
1404
+ key: 1,
1405
+ owner: 0
1406
+ });
1407
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1408
+ assert(await ctx.getSignedKey({ documentLoader: mockDocumentLoader }));
1409
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1410
+ });
1411
+ await t.step("coalesces key-owner FetchError results", async () => {
1412
+ const { ctx, loads } = setup({ load: async (url, options) => {
1413
+ if (url === ownerId) throw new FetchError(new URL(url), "HTTP 401: Unauthorized");
1414
+ return await mockDocumentLoader(url, options);
1415
+ } });
1416
+ assert(await ctx.getSignedKey());
1417
+ assertEquals(await Promise.all(Array.from({ length: 8 }, () => ctx.getSignedKeyOwner())), Array(8).fill(null));
1418
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1419
+ assertEquals(loads, {
1420
+ key: 1,
1421
+ owner: 1
1422
+ });
1423
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1424
+ });
1425
+ await t.step("memoizes unsigned requests", async () => {
1426
+ exporter.clear();
1427
+ const { ctx, loads } = setup({ request: new Request("https://example.com/") });
1428
+ for (let i = 0; i < 3; i++) {
1429
+ assertEquals(await ctx.getSignedKey(), null);
1430
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1431
+ }
1432
+ assertEquals(loads, {
1433
+ key: 0,
1434
+ owner: 0
1435
+ });
1436
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1437
+ });
1438
+ await t.step("memoizes null owners without trusting a key's owner claim", async () => {
1439
+ const { ctx, loads } = setup({ load: async (url, options) => url === ownerId ? {
1440
+ documentUrl: url,
1441
+ contextUrl: null,
1442
+ document: {
1443
+ ...person2_default,
1444
+ publicKey: []
1445
+ }
1446
+ } : await mockDocumentLoader(url, options) });
1447
+ assert(await ctx.getSignedKey());
1448
+ assertEquals(await Promise.all(Array.from({ length: 8 }, () => ctx.getSignedKeyOwner())), Array(8).fill(null));
1449
+ for (let i = 0; i < 3; i++) assertEquals(await ctx.getSignedKeyOwner(), null);
1450
+ assertEquals(loads, {
1451
+ key: 1,
1452
+ owner: 1
1453
+ });
1454
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1455
+ assert(await ctx.getSignedKeyOwner());
1456
+ });
1457
+ await t.step("isolates concurrent document loaders before success", async () => {
1458
+ const { ctx, loads } = setup();
1459
+ let otherLoads = 0;
1460
+ const otherLoader = async (url, options) => {
1461
+ otherLoads++;
1462
+ if (url === ownerId) return {
1463
+ documentUrl: url,
1464
+ contextUrl: null,
1465
+ document: {
1466
+ ...person2_default,
1467
+ name: "Other view"
1468
+ }
1469
+ };
1470
+ return await mockDocumentLoader(url, options);
1471
+ };
1472
+ const [first, second] = await Promise.all([ctx.getSignedKeyOwner(), ctx.getSignedKeyOwner({ documentLoader: otherLoader })]);
1473
+ assertEquals(first?.name, "Jane Doe");
1474
+ assertEquals(second?.name, "Other view");
1475
+ const cached = await ctx.getSignedKeyOwner();
1476
+ assert(cached === first || cached === second);
1477
+ assertStrictEquals(await ctx.getSignedKeyOwner({ documentLoader: otherLoader }), cached);
1478
+ assertEquals(loads, {
1479
+ key: 1,
1480
+ owner: 1
1481
+ });
1482
+ assertEquals(otherLoads, 2);
1483
+ });
1484
+ await t.step("preserves in-flight null while another loader succeeds", async () => {
1485
+ exporter.clear();
1486
+ let release;
1487
+ const gate = new Promise((resolve) => release = resolve);
1488
+ const { ctx, loads } = setup({ load: async () => {
1489
+ await gate;
1490
+ throw new TypeError("fetch failed");
1491
+ } });
1492
+ const failed = Promise.all([ctx.getSignedKey(), ctx.getSignedKeyOwner()]);
1493
+ const owner = await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader });
1494
+ assert(owner);
1495
+ release();
1496
+ assertEquals(await failed, [null, null]);
1497
+ assertStrictEquals(await ctx.getSignedKeyOwner(), owner);
1498
+ assert(await ctx.getSignedKey());
1499
+ assertEquals(loads, {
1500
+ key: 1,
1501
+ owner: 0
1502
+ });
1503
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1504
+ });
1505
+ await t.step("isolates context loaders", async () => {
1506
+ exporter.clear();
1507
+ const { ctx, loads } = setup();
1508
+ const otherContextLoader = (url, options) => mockDocumentLoader(url, options);
1509
+ await Promise.all([ctx.getSignedKeyOwner(), ctx.getSignedKeyOwner({ contextLoader: otherContextLoader })]);
1510
+ assertEquals(loads, {
1511
+ key: 2,
1512
+ owner: 2
1513
+ });
1514
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1515
+ });
1516
+ await t.step("shares work across tracer providers", async () => {
1517
+ exporter.clear();
1518
+ const [otherProvider, otherExporter] = createTestTracerProvider();
1519
+ try {
1520
+ const { ctx, loads } = setup();
1521
+ await Promise.all([
1522
+ ctx.getSignedKeyOwner(),
1523
+ ctx.getSignedKeyOwner({ tracerProvider: otherProvider }),
1524
+ ctx.getSignedKey(),
1525
+ ctx.getSignedKey({ tracerProvider: otherProvider })
1526
+ ]);
1527
+ assert(await ctx.getSignedKeyOwner({ tracerProvider: otherProvider }));
1528
+ assertEquals(loads, {
1529
+ key: 1,
1530
+ owner: 1
1531
+ });
1532
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1533
+ assertEquals(otherExporter.getSpans("http_signatures.verify").length, 0);
1534
+ } finally {
1535
+ await otherProvider.shutdown();
1536
+ }
1537
+ });
1538
+ await t.step("reuses success with a fresh authorization loader", async () => {
1539
+ exporter.clear();
1540
+ const { ctx, loads } = setup();
1541
+ let freshLoads = 0;
1542
+ const freshLoader = (url, options) => {
1543
+ freshLoads++;
1544
+ return mockDocumentLoader(url, options);
1545
+ };
1546
+ const key = await ctx.getSignedKey({ documentLoader: freshLoader });
1547
+ assert(key);
1548
+ assertStrictEquals(await ctx.getSignedKey(), key);
1549
+ const owner = await ctx.getSignedKeyOwner();
1550
+ assert(owner);
1551
+ assertStrictEquals(await ctx.getSignedKeyOwner({ documentLoader: freshLoader }), owner);
1552
+ assertEquals(loads, {
1553
+ key: 0,
1554
+ owner: 1
1555
+ });
1556
+ assertEquals(freshLoads, 1);
1557
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1558
+ });
1559
+ await t.step("retains a transient owner lookup failure as null", async () => {
1560
+ let fail = true;
1561
+ const { ctx, loads } = setup({ load: async (url, options) => {
1562
+ if (url === ownerId && fail) throw new TypeError("fetch failed");
1563
+ return await mockDocumentLoader(url, options);
1564
+ } });
1565
+ assert(await ctx.getSignedKey());
1566
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1567
+ fail = false;
1568
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1569
+ assertEquals(loads, {
1570
+ key: 1,
1571
+ owner: 1
1572
+ });
1573
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1574
+ });
1575
+ await t.step("rechecks an owner after another loader verifies the key", async () => {
1576
+ let fail = true;
1577
+ const { ctx, loads } = setup({ load: async (url, options) => {
1578
+ if (url === keyId && fail) throw new TypeError("fetch failed");
1579
+ return await mockDocumentLoader(url, options);
1580
+ } });
1581
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1582
+ fail = false;
1583
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1584
+ assertEquals(loads, {
1585
+ key: 1,
1586
+ owner: 0
1587
+ });
1588
+ assert(await ctx.getSignedKey({ documentLoader: mockDocumentLoader }));
1589
+ assertEquals((await ctx.getSignedKeyOwner())?.id?.href, ownerId);
1590
+ assertEquals(loads, {
1591
+ key: 1,
1592
+ owner: 1
1593
+ });
1594
+ });
1595
+ await t.step("retries unexpected key rejections from a rejection handler", async () => {
1596
+ exporter.clear();
1597
+ const failure = /* @__PURE__ */ new Error("transient context loader failure");
1598
+ let rejectContext = true;
1599
+ const contextLoader = async (url, options) => {
1600
+ if (rejectContext) throw failure;
1601
+ return await mockDocumentLoader(url, options);
1602
+ };
1603
+ const { ctx, loads } = setup();
1604
+ const options = { contextLoader };
1605
+ const failedKeys = Promise.allSettled([
1606
+ ctx.getSignedKey(options),
1607
+ ctx.getSignedKeyOwner(options),
1608
+ ctx.getSignedKey(options)
1609
+ ]);
1610
+ const recovery = ctx.getSignedKey(options).catch(() => {
1611
+ rejectContext = false;
1612
+ return ctx.getSignedKey(options);
1613
+ });
1614
+ for (const result of await failedKeys) assertEquals(result.status, "rejected");
1615
+ assert(await recovery);
1616
+ assertEquals(loads, {
1617
+ key: 2,
1618
+ owner: 0
1619
+ });
1620
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1621
+ });
1622
+ await t.step("retries owner rejections while retaining the verified key", async () => {
1623
+ exporter.clear();
1624
+ let rejectContext = false;
1625
+ const contextLoader = async (url, options) => {
1626
+ if (rejectContext) throw new Error("transient owner context failure");
1627
+ return await mockDocumentLoader(url, options);
1628
+ };
1629
+ const { ctx, loads } = setup();
1630
+ const options = { contextLoader };
1631
+ assert(await ctx.getSignedKey(options));
1632
+ assertEquals(loads, {
1633
+ key: 1,
1634
+ owner: 0
1635
+ });
1636
+ rejectContext = true;
1637
+ const failures = await Promise.allSettled([ctx.getSignedKeyOwner(options), ctx.getSignedKeyOwner(options)]);
1638
+ for (const result of failures) assertEquals(result.status, "rejected");
1639
+ assertEquals(loads, {
1640
+ key: 1,
1641
+ owner: 1
1642
+ });
1643
+ rejectContext = false;
1644
+ assert(await ctx.getSignedKeyOwner(options));
1645
+ assertEquals(loads, {
1646
+ key: 1,
1647
+ owner: 2
1648
+ });
1649
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1650
+ });
1651
+ await t.step("keeps clones and separate requests independent", async () => {
1652
+ exporter.clear();
1653
+ const { ctx, loads, federation } = setup();
1654
+ assert(await ctx.getSignedKeyOwner());
1655
+ const clone = ctx.clone(void 0);
1656
+ const separate = federation.createContext(new Request(request), void 0);
1657
+ await Promise.all([clone.getSignedKeyOwner(), separate.getSignedKeyOwner()]);
1658
+ assertEquals(loads, {
1659
+ key: 1,
1660
+ owner: 3
1661
+ });
1662
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 3);
1663
+ });
1664
+ } finally {
1665
+ await tracerProvider.shutdown();
1666
+ }
1667
+ });
1261
1668
  test("Federation.fetch() [rfc9421] multiple POST signatures", async (t) => {
1262
1669
  for (const [name, validSecond, maxHttpSignatures] of [
1263
1670
  [
@@ -1360,7 +1767,7 @@ test("Federation.fetch()", async (t) => {
1360
1767
  kv,
1361
1768
  documentLoaderFactory: () => mockDocumentLoader,
1362
1769
  authenticatedDocumentLoaderFactory(identity) {
1363
- const docLoader = getAuthenticatedDocumentLoader(identity);
1770
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
1364
1771
  return (url) => {
1365
1772
  if (new URL(url).host === "example.com") return docLoader(url);
1366
1773
  return mockDocumentLoader(url);
@@ -1811,7 +2218,7 @@ test("Federation.setInboxListeners()", async (t) => {
1811
2218
  kv,
1812
2219
  documentLoaderFactory: () => mockDocumentLoader,
1813
2220
  authenticatedDocumentLoaderFactory(identity) {
1814
- const docLoader = getAuthenticatedDocumentLoader(identity);
2221
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
1815
2222
  return (url) => {
1816
2223
  const urlObj = new URL(url);
1817
2224
  authenticatedRequests.push([url, identity.keyId.href]);
@@ -2134,7 +2541,7 @@ test("Federation.setInboxListeners()", async (t) => {
2134
2541
  kv,
2135
2542
  documentLoaderFactory: () => mockDocumentLoader,
2136
2543
  authenticatedDocumentLoaderFactory(identity) {
2137
- const docLoader = getAuthenticatedDocumentLoader(identity);
2544
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
2138
2545
  return (url) => {
2139
2546
  if (new URL(url).host === "example.com") return docLoader(url);
2140
2547
  return mockDocumentLoader(url);
@@ -2347,10 +2754,11 @@ test("Federation.setOutboxListeners()", async (t) => {
2347
2754
  sinks: ["buffer"]
2348
2755
  }]
2349
2756
  });
2350
- const federation = createFederation({
2757
+ const federation = new FederationImpl({
2351
2758
  kv,
2352
2759
  documentLoaderFactory: () => mockDocumentLoader
2353
2760
  });
2761
+ federation.allowPrivateAddress = true;
2354
2762
  federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
2355
2763
  privateKey: rsaPrivateKey2,
2356
2764
  publicKey: rsaPublicKey2.publicKey
@@ -2453,10 +2861,11 @@ test("Federation.setOutboxListeners()", async (t) => {
2453
2861
  sinks: ["buffer"]
2454
2862
  }]
2455
2863
  });
2456
- const federation = createFederation({
2864
+ const federation = new FederationImpl({
2457
2865
  kv,
2458
2866
  documentLoaderFactory: () => mockDocumentLoader
2459
2867
  });
2868
+ federation.allowPrivateAddress = true;
2460
2869
  federation.setActorDispatcher("/{identifier}", (_ctx, identifier) => identifier === "person2" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
2461
2870
  privateKey: rsaPrivateKey2,
2462
2871
  publicKey: rsaPublicKey2.publicKey
@@ -3136,7 +3545,7 @@ test("FederationImpl.sendActivity()", async (t) => {
3136
3545
  });
3137
3546
  const federation = new FederationImpl({
3138
3547
  kv: new MemoryKvStore(),
3139
- contextLoaderFactory: () => mockDocumentLoader
3548
+ allowPrivateAddress: true
3140
3549
  });
3141
3550
  const context = federation.createContext(new URL("https://example.com/"));
3142
3551
  await t.step("success", async () => {
@@ -6863,7 +7272,7 @@ test("ContextImpl.sendActivity()", async (t) => {
6863
7272
  const kv = new MemoryKvStore();
6864
7273
  const federation = new FederationImpl({
6865
7274
  kv,
6866
- contextLoaderFactory: () => mockDocumentLoader
7275
+ allowPrivateAddress: true
6867
7276
  });
6868
7277
  federation.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
6869
7278
  if (identifier !== "1") return null;
@@ -6986,11 +7395,13 @@ test("ContextImpl.sendActivity()", async (t) => {
6986
7395
  });
6987
7396
  await t.step("records recipient span attributes correctly", async () => {
6988
7397
  const [tracerProvider, exporter] = createTestTracerProvider();
6989
- const ctx = new FederationImpl({
7398
+ const federation3 = new FederationImpl({
6990
7399
  kv,
6991
7400
  contextLoaderFactory: () => mockDocumentLoader,
6992
7401
  tracerProvider
6993
- }).createContext(new URL("https://example.com/"), void 0);
7402
+ });
7403
+ federation3.allowPrivateAddress = true;
7404
+ const ctx = federation3.createContext(new URL("https://example.com/"), void 0);
6994
7405
  const activity = new vocab.Create({
6995
7406
  id: new URL("https://example.com/activity/telemetry"),
6996
7407
  actor: new URL("https://example.com/person"),
@@ -7024,7 +7435,7 @@ test("ContextImpl.sendActivity()", async (t) => {
7024
7435
  };
7025
7436
  const federation2 = new FederationImpl({
7026
7437
  kv,
7027
- contextLoaderFactory: () => mockDocumentLoader,
7438
+ allowPrivateAddress: true,
7028
7439
  queue
7029
7440
  });
7030
7441
  federation2.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
@@ -7195,7 +7606,7 @@ test("ContextImpl.sendActivity()", async (t) => {
7195
7606
  const asyncEnqueued = [];
7196
7607
  const fed = new FederationImpl({
7197
7608
  kv,
7198
- contextLoaderFactory: () => mockDocumentLoader,
7609
+ allowPrivateAddress: true,
7199
7610
  queue: {
7200
7611
  enqueue(message) {
7201
7612
  return new Promise((resolve) => {
@@ -7422,10 +7833,10 @@ test({
7422
7833
  const signedInvite = await signObject(new vocab.Invite({ actor: new URL("https://example.com/person2") }), ed25519PrivateKey, ed25519Multikey.id);
7423
7834
  assert(await ctx.routeActivity("id", signedInvite));
7424
7835
  assertEquals(activities, [[null, signedOffer], ["id", signedInvite]]);
7425
- assertFalse(await ctx.routeActivity(null, new vocab.Create({
7836
+ assertFalse(await withDnsLookup(() => Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")), () => ctx.routeActivity(null, new vocab.Create({
7426
7837
  id: new URL("https://example.com/not-found"),
7427
7838
  actor: new URL("https://example.com/person")
7428
- })));
7839
+ }))));
7429
7840
  assertEquals(activities, [[null, signedOffer], ["id", signedInvite]]);
7430
7841
  assertFalse(await ctx.routeActivity(null, new vocab.Create({
7431
7842
  id: new URL("https://example.com/person"),
@@ -7932,7 +8343,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
7932
8343
  const kv = new MemoryKvStore();
7933
8344
  const federation = new FederationImpl({
7934
8345
  kv,
7935
- contextLoaderFactory: () => mockDocumentLoader
8346
+ allowPrivateAddress: true
7936
8347
  });
7937
8348
  await t.step("skip", async () => {
7938
8349
  await new InboxContextImpl(null, {
@@ -8055,6 +8466,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
8055
8466
  contextLoaderFactory: () => mockDocumentLoader,
8056
8467
  tracerProvider
8057
8468
  });
8469
+ federationWithTracing.allowPrivateAddress = true;
8058
8470
  await new InboxContextImpl(null, await signJsonLd({
8059
8471
  "@context": "https://www.w3.org/ns/activitystreams",
8060
8472
  "type": "Create",
@@ -8178,12 +8590,14 @@ test("createFederation() applies httpMessageSignaturesSpecTtl to remembered spec
8178
8590
  return new Response(null, { status: key == null ? 401 : 202 });
8179
8591
  });
8180
8592
  const kv = new TtlRecordingKvStore();
8181
- const ctx = createFederation({
8593
+ const federation = new FederationImpl({
8182
8594
  kv,
8183
8595
  documentLoaderFactory: () => mockDocumentLoader,
8184
8596
  contextLoaderFactory: () => mockDocumentLoader,
8185
8597
  httpMessageSignaturesSpecTtl: { days: 14 }
8186
- }).createContext(new URL("https://example.com/"), void 0);
8598
+ });
8599
+ federation.allowPrivateAddress = true;
8600
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
8187
8601
  const specKey = [
8188
8602
  "_fedify",
8189
8603
  "httpMessageSignaturesSpec",
@@ -8230,12 +8644,17 @@ test("createFederation() applies publicKeyTtl to cached public keys", async () =
8230
8644
  });
8231
8645
  const keyId = "https://example.com/person2#key3";
8232
8646
  const kv = new TtlRecordingKvStore();
8233
- const federation = createFederation({
8647
+ const federation = new FederationImpl({
8234
8648
  kv,
8235
8649
  documentLoaderFactory: () => mockDocumentLoader,
8236
8650
  contextLoaderFactory: () => mockDocumentLoader,
8651
+ authenticatedDocumentLoaderFactory: (identity, options) => getAuthenticatedDocumentLoader(identity, {
8652
+ ...options,
8653
+ allowPrivateAddress: true
8654
+ }),
8237
8655
  publicKeyTtl: { days: 7 }
8238
8656
  });
8657
+ federation.allowPrivateAddress = true;
8239
8658
  const inbox = [];
8240
8659
  federation.setActorDispatcher("/users/{identifier}", (_, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
8241
8660
  privateKey: rsaPrivateKey2,
@@ -8441,7 +8860,9 @@ test("ContextImpl.lookupWebFinger()", async (t) => {
8441
8860
  links: []
8442
8861
  };
8443
8862
  esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Aalice%40example.com", { body: expected });
8444
- deepStrictEqual(await createFederation(mockOptions()).createContext(new URL("https://example.com/"), void 0).lookupWebFinger("acct:alice@example.com", { withDiagnostics: true }), expected);
8863
+ const federation = new FederationImpl(mockOptions());
8864
+ federation.allowPrivateAddress = true;
8865
+ deepStrictEqual(await federation.createContext(new URL("https://example.com/"), void 0).lookupWebFinger("acct:alice@example.com", { withDiagnostics: true }), expected);
8445
8866
  } finally {
8446
8867
  esm_default.hardReset();
8447
8868
  }
@@ -8872,5 +9293,19 @@ test("createFederation() validates documentLoaderTimeout", () => {
8872
9293
  documentLoaderTimeout: Temporal.Duration.from({ seconds: 30 })
8873
9294
  });
8874
9295
  });
9296
+ /**
9297
+ * Runs the given function while `node:dns/promises`' `lookup()`, which the
9298
+ * default public fetch resolves hostnames with, is replaced, so that no test
9299
+ * depends on the real DNS.
9300
+ */
9301
+ async function withDnsLookup(lookup, fn) {
9302
+ const originalLookup = dns.lookup;
9303
+ dns.lookup = lookup;
9304
+ try {
9305
+ return await fn();
9306
+ } finally {
9307
+ dns.lookup = originalLookup;
9308
+ }
9309
+ }
8875
9310
  //#endregion
8876
9311
  export {};