@fedify/fedify 2.4.5-dev.2453 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +3 -0
  2. package/dist/{builder-CS2LjGi-.mjs → builder-CUAVOHg6.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-C6_4hMjm.mjs → compound-proof-BNjXclzO.mjs} +2 -2
  10. package/dist/{context-7NyiqIcg.d.ts → context-CVOOd9pD.d.ts} +28 -2
  11. package/dist/{context-tsYPJ2Ew.d.cts → context-D0gFIgev.d.cts} +28 -2
  12. package/dist/{deno-CTWhU1oT.mjs → deno-DcJISdwu.mjs} +1 -1
  13. package/dist/docloader-CP1UxMLL.mjs +136 -0
  14. package/dist/federation/builder.test.mjs +1 -1
  15. package/dist/federation/compound-fanout.test.mjs +6 -3
  16. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  17. package/dist/federation/handler.test.mjs +4 -4
  18. package/dist/federation/hashlink-media.test.mjs +2 -2
  19. package/dist/federation/idempotency.test.mjs +2 -2
  20. package/dist/federation/inbox-report.test.mjs +5 -5
  21. package/dist/federation/inbox.test.mjs +1 -1
  22. package/dist/federation/metrics.test.mjs +1 -1
  23. package/dist/federation/middleware.test.mjs +465 -32
  24. package/dist/federation/mod.cjs +1 -1
  25. package/dist/federation/mod.d.cts +3 -3
  26. package/dist/federation/mod.d.ts +3 -3
  27. package/dist/federation/mod.js +1 -1
  28. package/dist/federation/outgoing-proof.test.mjs +9 -6
  29. package/dist/federation/portable-actor.test.mjs +3 -3
  30. package/dist/federation/portable-chain.test.mjs +3 -3
  31. package/dist/federation/portable-collection.test.mjs +4 -4
  32. package/dist/federation/portable-delivery.test.mjs +2 -2
  33. package/dist/federation/portable-gateway-keys.test.mjs +11 -5
  34. package/dist/federation/portable-inbox.test.mjs +4 -4
  35. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  36. package/dist/federation/portable-route-activity.test.mjs +2 -2
  37. package/dist/federation/portable.test.mjs +3 -3
  38. package/dist/federation/send.test.mjs +498 -88
  39. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  40. package/dist/federation/tasks/tasks.test.mjs +2 -2
  41. package/dist/federation/temporal.test.mjs +1 -1
  42. package/dist/federation/webfinger.test.mjs +1 -1
  43. package/dist/{federation-DWVfQYX_.cjs → federation-Bkfq2YpS.cjs} +240 -102
  44. package/dist/{http-Dx5s5-pP.cjs → http-DHVH0DqC.cjs} +179 -110
  45. package/dist/{http-rgt1uxns.mjs → http-DuGmr8J8.mjs} +204 -106
  46. package/dist/{http-y9FnJFIe.js → http-R0ty0Udi.js} +178 -109
  47. package/dist/{inbox-4AZHfaO8.mjs → inbox-Bl1Y0s_A.mjs} +2 -2
  48. package/dist/{key-DYxuyd-s.d.cts → key-CyCcut3h.d.ts} +1 -2
  49. package/dist/{key-C1nv7_jD.d.ts → key-VEL1QqHF.d.cts} +1 -2
  50. package/dist/{kv-cache-DmxFPJaR.cjs → kv-cache-BtBSBZi9.cjs} +72 -27
  51. package/dist/{kv-cache-C3QkJF-x.mjs → kv-cache-DAV2_U44.mjs} +1 -1
  52. package/dist/{kv-cache-w4yWxqS1.js → kv-cache-FnLtmATH.js} +73 -28
  53. package/dist/{ld-CT-bAqSC.mjs → ld-Cy5GqbvS.mjs} +2 -2
  54. package/dist/{metrics-DsFjgE5X.mjs → metrics-CW8LoN7n.mjs} +1 -1
  55. package/dist/{middleware-DC8Omn-e.mjs → middleware-CdP-lz8Q.mjs} +1 -1
  56. package/dist/{middleware-Ch9WIaoZ.mjs → middleware-D2fk45uN.mjs} +82 -42
  57. package/dist/{middleware-BCHbutlW.js → middleware-D4rQARvq.js} +241 -103
  58. package/dist/{mod-DEZXwV6J.d.ts → mod-B1Qt5cQQ.d.ts} +2 -9
  59. package/dist/{mod-Xj5P4s4d.d.ts → mod-B5COx5Wd.d.ts} +1 -1
  60. package/dist/{mod-CavhAgLw.d.cts → mod-BTxJG7ZH.d.cts} +1 -1
  61. package/dist/{mod-CtpF2BU5.d.ts → mod-CpyjFA9s.d.ts} +1 -1
  62. package/dist/{mod-BojP2Bw3.d.cts → mod-makPMVMg.d.cts} +1 -1
  63. package/dist/{mod-Dw-nPxQh.d.cts → mod-ss0wBUH1.d.cts} +2 -9
  64. package/dist/mod.cjs +5 -5
  65. package/dist/mod.d.cts +6 -6
  66. package/dist/mod.d.ts +6 -6
  67. package/dist/mod.js +5 -5
  68. package/dist/nodeinfo/client.test.mjs +337 -19
  69. package/dist/nodeinfo/handler.test.mjs +1 -1
  70. package/dist/nodeinfo/mod.cjs +1 -1
  71. package/dist/nodeinfo/mod.d.cts +1 -1
  72. package/dist/nodeinfo/mod.d.ts +1 -1
  73. package/dist/nodeinfo/mod.js +1 -1
  74. package/dist/otel/exporter.test.mjs +292 -30
  75. package/dist/otel/mod.cjs +36 -30
  76. package/dist/otel/mod.d.cts +13 -0
  77. package/dist/otel/mod.d.ts +13 -0
  78. package/dist/otel/mod.js +36 -30
  79. package/dist/{owner-BGYYk-xi.mjs → owner-CbQZqFim.mjs} +3 -3
  80. package/dist/{portable-collection-DzEc4cB2.js → portable-collection-B5Aryi8I.js} +1 -1
  81. package/dist/{portable-collection-Cwn7DM0G.cjs → portable-collection-C_Kmvl3q.cjs} +1 -1
  82. package/dist/{portable-collection-DJSwe8QM.mjs → portable-collection-D2JYDadi.mjs} +2 -2
  83. package/dist/{portable-key-Dc6N1SEB.mjs → portable-key-Cc-ctNU2.mjs} +1 -1
  84. package/dist/{proof-DP93iZiH.mjs → proof-UN4CW55z.mjs} +3 -3
  85. package/dist/{send-c6buRbhI.mjs → send-BzbX2e6v.mjs} +174 -76
  86. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  87. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  88. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  89. package/dist/sig/compound-proof.test.mjs +2 -2
  90. package/dist/sig/http.test.mjs +553 -2
  91. package/dist/sig/key.test.mjs +1 -1
  92. package/dist/sig/ld.test.mjs +1 -1
  93. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  94. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  95. package/dist/sig/mod.cjs +2 -2
  96. package/dist/sig/mod.d.cts +2 -2
  97. package/dist/sig/mod.d.ts +2 -2
  98. package/dist/sig/mod.js +2 -2
  99. package/dist/sig/owner.test.mjs +1 -1
  100. package/dist/sig/portable-collection.test.mjs +1 -1
  101. package/dist/sig/portable-interop.test.mjs +3 -3
  102. package/dist/sig/portable-key.test.mjs +5 -5
  103. package/dist/sig/proof.test.mjs +4 -4
  104. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  105. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  106. package/dist/sig/verification.test.mjs +3 -3
  107. package/dist/{temporal-DfC1NkLg.mjs → temporal-B7Wvxrjs.mjs} +1 -1
  108. package/dist/testing/kv.test.mjs +1 -1
  109. package/dist/testing/mod.d.mts +26 -0
  110. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  111. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  112. package/dist/utils/docloader.test.mjs +344 -36
  113. package/dist/utils/kv-cache.test.mjs +1 -1
  114. package/dist/utils/mod.cjs +1 -1
  115. package/dist/utils/mod.d.cts +1 -1
  116. package/dist/utils/mod.d.ts +1 -1
  117. package/dist/utils/mod.js +1 -1
  118. package/package.json +7 -7
  119. package/dist/docloader-CXFE-n2O.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-DsFjgE5X.mjs";
20
- import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-CT-bAqSC.mjs";
21
- import { a as signObject, s as verifyObject } from "../proof-DP93iZiH.mjs";
22
- import { f as verifyRequest, l as signRequest } from "../http-rgt1uxns.mjs";
19
+ import { g as recordInboxActivity } from "../metrics-CW8LoN7n.mjs";
20
+ import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-Cy5GqbvS.mjs";
21
+ import { a as signObject, s as verifyObject } from "../proof-UN4CW55z.mjs";
22
+ import { p as verifyRequest, u as signRequest } from "../http-DuGmr8J8.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-CXFE-n2O.mjs";
25
- import { T as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-Ch9WIaoZ.mjs";
26
- import { t as doesActorOwnKey } from "../owner-BGYYk-xi.mjs";
24
+ import { n as getAuthenticatedDocumentLoader } from "../docloader-CP1UxMLL.mjs";
25
+ import { T as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-D2fk45uN.mjs";
26
+ import { t as doesActorOwnKey } from "../owner-CbQZqFim.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,10 +34,10 @@ import { Create, Note, Offer, Person, getTypeId, lookupObject } from "@fedify/vo
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";
40
+ import dns from "node:dns/promises";
41
41
  //#region ../fixture/src/fixtures/example.com/create.json
42
42
  var create_default = {
43
43
  "@context": "https://www.w3.org/ns/activitystreams",
@@ -101,7 +101,7 @@ var person2_default = {
101
101
  };
102
102
  //#endregion
103
103
  //#region src/federation/middleware.test.ts
104
- const documentLoader = getDocumentLoader();
104
+ const documentLoader = getDocumentLoader({ allowPrivateAddress: true });
105
105
  let logtapeLock = Promise.resolve();
106
106
  async function withLogtapeLock(fn) {
107
107
  const run = logtapeLock.then(fn, fn);
@@ -656,7 +656,8 @@ test({
656
656
  const federation = createFederation({
657
657
  kv,
658
658
  documentLoaderFactory: () => rejectingLoader,
659
- contextLoaderFactory: () => mockDocumentLoader
659
+ contextLoaderFactory: () => mockDocumentLoader,
660
+ authenticatedDocumentLoaderFactory: (identity) => getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true })
660
661
  });
661
662
  let ctx = federation.createContext(new URL("https://example.com:1234/"), 123);
662
663
  assertEquals(ctx.data, 123);
@@ -767,7 +768,7 @@ test({
767
768
  documentUrl: "https://example.com/auth-check",
768
769
  document: true
769
770
  });
770
- assertEquals(await ctx.lookupObject("https://example.com/object"), null);
771
+ assertEquals(await withDnsLookup(() => Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")), () => ctx.lookupObject("https://example.com/object")), null);
771
772
  await assertRejects(() => ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({})), TypeError, "The activity to send must have at least one actor property.");
772
773
  await ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({ actor: new URL("https://example.com/users/handle") }));
773
774
  esm_default.get("https://example.com/object", () => new Response(JSON.stringify({
@@ -1016,7 +1017,7 @@ test({
1016
1017
  assertStrictEquals(clone.contextLoader, ctx.contextLoader);
1017
1018
  assertStrictEquals(clone.federation, ctx.federation);
1018
1019
  });
1019
- esm_default.get("https://example.com/.well-known/nodeinfo", (cl) => {
1020
+ esm_default.get("path:/.well-known/nodeinfo", (cl) => {
1020
1021
  const headers = cl.options.headers ?? {};
1021
1022
  assertEquals(new Headers(headers).get("User-Agent"), "CustomUserAgent/1.2.3");
1022
1023
  return new Response(JSON.stringify({ links: [{
@@ -1024,7 +1025,7 @@ test({
1024
1025
  href: "https://example.com/nodeinfo/2.1"
1025
1026
  }] }));
1026
1027
  });
1027
- esm_default.get("https://example.com/nodeinfo/2.1", (cl) => {
1028
+ esm_default.get("path:/nodeinfo/2.1", (cl) => {
1028
1029
  const headers = cl.options.headers ?? {};
1029
1030
  assertEquals(new Headers(headers).get("User-Agent"), "CustomUserAgent/1.2.3");
1030
1031
  return new Response(JSON.stringify({
@@ -1045,7 +1046,11 @@ test({
1045
1046
  kv,
1046
1047
  userAgent: "CustomUserAgent/1.2.3"
1047
1048
  }).createContext(new URL("https://example.com/"), 123);
1048
- assertEquals(await ctx.lookupNodeInfo("https://example.com/"), {
1049
+ const lookupExample = () => Promise.resolve([{
1050
+ address: "93.184.215.14",
1051
+ family: 4
1052
+ }]);
1053
+ assertEquals(await withDnsLookup(lookupExample, () => ctx.lookupNodeInfo("https://example.com/")), {
1049
1054
  software: {
1050
1055
  name: "foo",
1051
1056
  version: "1.2.3"
@@ -1057,7 +1062,7 @@ test({
1057
1062
  localComments: 456
1058
1063
  }
1059
1064
  });
1060
- assertEquals(await ctx.lookupNodeInfo("https://example.com/", { parse: "none" }), {
1065
+ assertEquals(await withDnsLookup(lookupExample, () => ctx.lookupNodeInfo("https://example.com/", { parse: "none" })), {
1061
1066
  software: {
1062
1067
  name: "foo",
1063
1068
  version: "1.2.3"
@@ -1176,6 +1181,408 @@ test({
1176
1181
  esm_default.hardReset();
1177
1182
  }
1178
1183
  });
1184
+ test("Federation.fetch() accepts unencoded path colons", async () => {
1185
+ const identifier = "https://pod.example/alice";
1186
+ const federation = createFederation({
1187
+ kv: new MemoryKvStore(),
1188
+ contextLoaderFactory: () => mockDocumentLoader
1189
+ });
1190
+ const dispatched = [];
1191
+ federation.setActorDispatcher("/users/{identifier}", (ctx, id) => {
1192
+ dispatched.push(id);
1193
+ assertEquals(ctx.url.pathname, "/users/https:%2F%2Fpod.example%2Falice");
1194
+ assertEquals(ctx.url.search, "?page=1");
1195
+ assertEquals(ctx.request.url, ctx.url.href);
1196
+ return new vocab.Person({ id: ctx.getActorUri(id) });
1197
+ });
1198
+ const request = new Request("https://local.example/users/https:%2F%2Fpod.example%2Falice?page=1", { headers: { Accept: "application/activity+json" } });
1199
+ const response = await federation.fetch(request, { contextData: void 0 });
1200
+ assertEquals(response.status, 200);
1201
+ assertEquals(dispatched, [identifier]);
1202
+ const canonical = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice";
1203
+ const body = await response.json();
1204
+ assert(body != null && typeof body === "object" && "id" in body);
1205
+ assertEquals(body.id, canonical);
1206
+ const ctx = federation.createContext(request, void 0);
1207
+ assertEquals(ctx.getActorUri(identifier).href, canonical);
1208
+ assertEquals(ctx.parseUri(new URL(canonical)), {
1209
+ type: "actor",
1210
+ identifier
1211
+ });
1212
+ assertEquals(ctx.parseUri(new URL(request.url)), null);
1213
+ assertEquals(ctx.parseUri(new URL(canonical.replace("local.example", "other.example"))), null);
1214
+ });
1215
+ test("Federation.fetch() verifies signatures over unencoded path colons", async (t) => {
1216
+ const identifier = "https://pod.example/alice";
1217
+ const canonical = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice/inbox";
1218
+ const encoded = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice/inbox?delivery=1";
1219
+ const normalized = encoded.replace("%3A", ":");
1220
+ for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) for (const target of [
1221
+ "encoded",
1222
+ "normalized",
1223
+ "changed after signing"
1224
+ ]) await t.step(`${spec}: ${target}`, async () => {
1225
+ const recipients = [];
1226
+ const federation = createFederation({
1227
+ kv: new MemoryKvStore(),
1228
+ documentLoaderFactory: () => mockDocumentLoader,
1229
+ contextLoaderFactory: () => mockDocumentLoader
1230
+ });
1231
+ federation.setActorDispatcher("/users/{identifier}", (ctx, id) => new vocab.Person({ id: ctx.getActorUri(id) })).setKeyPairsDispatcher(() => []);
1232
+ federation.setInboxListeners("/users/{identifier}/inbox").on(vocab.Create, (ctx) => {
1233
+ recipients.push(ctx.recipient);
1234
+ });
1235
+ const signed = await signRequest(new Request(target === "normalized" ? normalized : encoded, {
1236
+ method: "POST",
1237
+ headers: { "Content-Type": "application/activity+json" },
1238
+ body: JSON.stringify(create_default)
1239
+ }), rsaPrivateKey2, rsaPublicKey2.id, { spec });
1240
+ const request = target === "changed after signing" ? new Request(normalized, signed) : signed;
1241
+ const response = await federation.fetch(request, { contextData: void 0 });
1242
+ assertEquals(request.url, target === "encoded" ? encoded : normalized);
1243
+ const valid = target !== "changed after signing";
1244
+ assertEquals(response.status, valid ? 202 : 401);
1245
+ assertEquals(recipients, valid ? [identifier] : []);
1246
+ assertEquals(federation.createContext(new URL(canonical), void 0).getInboxUri(identifier).href, canonical);
1247
+ });
1248
+ });
1249
+ test("RequestContext signature checks", async (t) => {
1250
+ const [tracerProvider, exporter] = createTestTracerProvider();
1251
+ const keyId = rsaPublicKey3.id.href;
1252
+ const ownerId = rsaPublicKey3.ownerId.href;
1253
+ const request = await signRequest(new Request("https://example.com/"), rsaPrivateKey3, rsaPublicKey3.id);
1254
+ function setup(options = {}) {
1255
+ const loads = {
1256
+ key: 0,
1257
+ owner: 0
1258
+ };
1259
+ const loader = async (url, loaderOptions) => {
1260
+ if (url === keyId) loads.key++;
1261
+ else if (url === ownerId) loads.owner++;
1262
+ await options.gate;
1263
+ return await (options.load ?? mockDocumentLoader)(url, loaderOptions);
1264
+ };
1265
+ const federation = createFederation({
1266
+ kv: new MemoryKvStore(),
1267
+ documentLoaderFactory: () => loader,
1268
+ contextLoaderFactory: () => mockDocumentLoader,
1269
+ tracerProvider: options.tracerProvider ?? tracerProvider
1270
+ });
1271
+ return {
1272
+ ctx: federation.createContext(options.request ?? new Request(request), void 0),
1273
+ loads,
1274
+ loader,
1275
+ federation
1276
+ };
1277
+ }
1278
+ try {
1279
+ for (const mode of [
1280
+ "keys",
1281
+ "owners",
1282
+ "warm owners",
1283
+ "mixed"
1284
+ ]) await t.step(`coalesces ${mode}`, async () => {
1285
+ exporter.clear();
1286
+ let release;
1287
+ const { ctx, loads, loader } = setup({ gate: new Promise((resolve) => release = resolve) });
1288
+ if (mode === "warm owners") {
1289
+ release();
1290
+ assert(await ctx.getSignedKey());
1291
+ }
1292
+ const results = Promise.all(Array.from({ length: 8 }, (_, i) => {
1293
+ const options = i % 2 === 0 ? {} : {
1294
+ documentLoader: loader,
1295
+ contextLoader: ctx.contextLoader,
1296
+ tracerProvider
1297
+ };
1298
+ return mode === "keys" || mode === "mixed" && i % 2 === 0 ? ctx.getSignedKey(options) : ctx.getSignedKeyOwner(options);
1299
+ }));
1300
+ release();
1301
+ for (const result of await results) assert(result != null);
1302
+ assertEquals(loads, {
1303
+ key: 1,
1304
+ owner: mode === "keys" ? 0 : 1
1305
+ });
1306
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1307
+ assert(await ctx.getSignedKey());
1308
+ if (mode !== "keys") assert(await ctx.getSignedKeyOwner());
1309
+ assertEquals(loads, {
1310
+ key: 1,
1311
+ owner: mode === "keys" ? 0 : 1
1312
+ });
1313
+ });
1314
+ await t.step("memoizes null keys", async () => {
1315
+ exporter.clear();
1316
+ const { ctx, loads } = setup({ load: () => Promise.reject(/* @__PURE__ */ new Error("unavailable")) });
1317
+ for (let i = 0; i < 3; i++) {
1318
+ assertEquals(await ctx.getSignedKey(), null);
1319
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1320
+ }
1321
+ assertEquals(loads, {
1322
+ key: 1,
1323
+ owner: 0
1324
+ });
1325
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1326
+ assert(await ctx.getSignedKey({ documentLoader: mockDocumentLoader }));
1327
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1328
+ });
1329
+ await t.step("coalesces key-owner FetchError results", async () => {
1330
+ const { ctx, loads } = setup({ load: async (url, options) => {
1331
+ if (url === ownerId) throw new FetchError(new URL(url), "HTTP 401: Unauthorized");
1332
+ return await mockDocumentLoader(url, options);
1333
+ } });
1334
+ assert(await ctx.getSignedKey());
1335
+ assertEquals(await Promise.all(Array.from({ length: 8 }, () => ctx.getSignedKeyOwner())), Array(8).fill(null));
1336
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1337
+ assertEquals(loads, {
1338
+ key: 1,
1339
+ owner: 1
1340
+ });
1341
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1342
+ });
1343
+ await t.step("memoizes unsigned requests", async () => {
1344
+ exporter.clear();
1345
+ const { ctx, loads } = setup({ request: new Request("https://example.com/") });
1346
+ for (let i = 0; i < 3; i++) {
1347
+ assertEquals(await ctx.getSignedKey(), null);
1348
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1349
+ }
1350
+ assertEquals(loads, {
1351
+ key: 0,
1352
+ owner: 0
1353
+ });
1354
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1355
+ });
1356
+ await t.step("memoizes null owners without trusting a key's owner claim", async () => {
1357
+ const { ctx, loads } = setup({ load: async (url, options) => url === ownerId ? {
1358
+ documentUrl: url,
1359
+ contextUrl: null,
1360
+ document: {
1361
+ ...person2_default,
1362
+ publicKey: []
1363
+ }
1364
+ } : await mockDocumentLoader(url, options) });
1365
+ assert(await ctx.getSignedKey());
1366
+ assertEquals(await Promise.all(Array.from({ length: 8 }, () => ctx.getSignedKeyOwner())), Array(8).fill(null));
1367
+ for (let i = 0; i < 3; i++) assertEquals(await ctx.getSignedKeyOwner(), null);
1368
+ assertEquals(loads, {
1369
+ key: 1,
1370
+ owner: 1
1371
+ });
1372
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1373
+ assert(await ctx.getSignedKeyOwner());
1374
+ });
1375
+ await t.step("isolates concurrent document loaders before success", async () => {
1376
+ const { ctx, loads } = setup();
1377
+ let otherLoads = 0;
1378
+ const otherLoader = async (url, options) => {
1379
+ otherLoads++;
1380
+ if (url === ownerId) return {
1381
+ documentUrl: url,
1382
+ contextUrl: null,
1383
+ document: {
1384
+ ...person2_default,
1385
+ name: "Other view"
1386
+ }
1387
+ };
1388
+ return await mockDocumentLoader(url, options);
1389
+ };
1390
+ const [first, second] = await Promise.all([ctx.getSignedKeyOwner(), ctx.getSignedKeyOwner({ documentLoader: otherLoader })]);
1391
+ assertEquals(first?.name, "Jane Doe");
1392
+ assertEquals(second?.name, "Other view");
1393
+ const cached = await ctx.getSignedKeyOwner();
1394
+ assert(cached === first || cached === second);
1395
+ assertStrictEquals(await ctx.getSignedKeyOwner({ documentLoader: otherLoader }), cached);
1396
+ assertEquals(loads, {
1397
+ key: 1,
1398
+ owner: 1
1399
+ });
1400
+ assertEquals(otherLoads, 2);
1401
+ });
1402
+ await t.step("preserves in-flight null while another loader succeeds", async () => {
1403
+ exporter.clear();
1404
+ let release;
1405
+ const gate = new Promise((resolve) => release = resolve);
1406
+ const { ctx, loads } = setup({ load: async () => {
1407
+ await gate;
1408
+ throw new TypeError("fetch failed");
1409
+ } });
1410
+ const failed = Promise.all([ctx.getSignedKey(), ctx.getSignedKeyOwner()]);
1411
+ const owner = await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader });
1412
+ assert(owner);
1413
+ release();
1414
+ assertEquals(await failed, [null, null]);
1415
+ assertStrictEquals(await ctx.getSignedKeyOwner(), owner);
1416
+ assert(await ctx.getSignedKey());
1417
+ assertEquals(loads, {
1418
+ key: 1,
1419
+ owner: 0
1420
+ });
1421
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1422
+ });
1423
+ await t.step("isolates context loaders", async () => {
1424
+ exporter.clear();
1425
+ const { ctx, loads } = setup();
1426
+ const otherContextLoader = (url, options) => mockDocumentLoader(url, options);
1427
+ await Promise.all([ctx.getSignedKeyOwner(), ctx.getSignedKeyOwner({ contextLoader: otherContextLoader })]);
1428
+ assertEquals(loads, {
1429
+ key: 2,
1430
+ owner: 2
1431
+ });
1432
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1433
+ });
1434
+ await t.step("shares work across tracer providers", async () => {
1435
+ exporter.clear();
1436
+ const [otherProvider, otherExporter] = createTestTracerProvider();
1437
+ try {
1438
+ const { ctx, loads } = setup();
1439
+ await Promise.all([
1440
+ ctx.getSignedKeyOwner(),
1441
+ ctx.getSignedKeyOwner({ tracerProvider: otherProvider }),
1442
+ ctx.getSignedKey(),
1443
+ ctx.getSignedKey({ tracerProvider: otherProvider })
1444
+ ]);
1445
+ assert(await ctx.getSignedKeyOwner({ tracerProvider: otherProvider }));
1446
+ assertEquals(loads, {
1447
+ key: 1,
1448
+ owner: 1
1449
+ });
1450
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1451
+ assertEquals(otherExporter.getSpans("http_signatures.verify").length, 0);
1452
+ } finally {
1453
+ await otherProvider.shutdown();
1454
+ }
1455
+ });
1456
+ await t.step("reuses success with a fresh authorization loader", async () => {
1457
+ exporter.clear();
1458
+ const { ctx, loads } = setup();
1459
+ let freshLoads = 0;
1460
+ const freshLoader = (url, options) => {
1461
+ freshLoads++;
1462
+ return mockDocumentLoader(url, options);
1463
+ };
1464
+ const key = await ctx.getSignedKey({ documentLoader: freshLoader });
1465
+ assert(key);
1466
+ assertStrictEquals(await ctx.getSignedKey(), key);
1467
+ const owner = await ctx.getSignedKeyOwner();
1468
+ assert(owner);
1469
+ assertStrictEquals(await ctx.getSignedKeyOwner({ documentLoader: freshLoader }), owner);
1470
+ assertEquals(loads, {
1471
+ key: 0,
1472
+ owner: 1
1473
+ });
1474
+ assertEquals(freshLoads, 1);
1475
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1476
+ });
1477
+ await t.step("retains a transient owner lookup failure as null", async () => {
1478
+ let fail = true;
1479
+ const { ctx, loads } = setup({ load: async (url, options) => {
1480
+ if (url === ownerId && fail) throw new TypeError("fetch failed");
1481
+ return await mockDocumentLoader(url, options);
1482
+ } });
1483
+ assert(await ctx.getSignedKey());
1484
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1485
+ fail = false;
1486
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1487
+ assertEquals(loads, {
1488
+ key: 1,
1489
+ owner: 1
1490
+ });
1491
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1492
+ });
1493
+ await t.step("rechecks an owner after another loader verifies the key", async () => {
1494
+ let fail = true;
1495
+ const { ctx, loads } = setup({ load: async (url, options) => {
1496
+ if (url === keyId && fail) throw new TypeError("fetch failed");
1497
+ return await mockDocumentLoader(url, options);
1498
+ } });
1499
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1500
+ fail = false;
1501
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1502
+ assertEquals(loads, {
1503
+ key: 1,
1504
+ owner: 0
1505
+ });
1506
+ assert(await ctx.getSignedKey({ documentLoader: mockDocumentLoader }));
1507
+ assertEquals((await ctx.getSignedKeyOwner())?.id?.href, ownerId);
1508
+ assertEquals(loads, {
1509
+ key: 1,
1510
+ owner: 1
1511
+ });
1512
+ });
1513
+ await t.step("retries unexpected key rejections from a rejection handler", async () => {
1514
+ exporter.clear();
1515
+ const failure = /* @__PURE__ */ new Error("transient context loader failure");
1516
+ let rejectContext = true;
1517
+ const contextLoader = async (url, options) => {
1518
+ if (rejectContext) throw failure;
1519
+ return await mockDocumentLoader(url, options);
1520
+ };
1521
+ const { ctx, loads } = setup();
1522
+ const options = { contextLoader };
1523
+ const failedKeys = Promise.allSettled([
1524
+ ctx.getSignedKey(options),
1525
+ ctx.getSignedKeyOwner(options),
1526
+ ctx.getSignedKey(options)
1527
+ ]);
1528
+ const recovery = ctx.getSignedKey(options).catch(() => {
1529
+ rejectContext = false;
1530
+ return ctx.getSignedKey(options);
1531
+ });
1532
+ for (const result of await failedKeys) assertEquals(result.status, "rejected");
1533
+ assert(await recovery);
1534
+ assertEquals(loads, {
1535
+ key: 2,
1536
+ owner: 0
1537
+ });
1538
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1539
+ });
1540
+ await t.step("retries owner rejections while retaining the verified key", async () => {
1541
+ exporter.clear();
1542
+ let rejectContext = false;
1543
+ const contextLoader = async (url, options) => {
1544
+ if (rejectContext) throw new Error("transient owner context failure");
1545
+ return await mockDocumentLoader(url, options);
1546
+ };
1547
+ const { ctx, loads } = setup();
1548
+ const options = { contextLoader };
1549
+ assert(await ctx.getSignedKey(options));
1550
+ assertEquals(loads, {
1551
+ key: 1,
1552
+ owner: 0
1553
+ });
1554
+ rejectContext = true;
1555
+ const failures = await Promise.allSettled([ctx.getSignedKeyOwner(options), ctx.getSignedKeyOwner(options)]);
1556
+ for (const result of failures) assertEquals(result.status, "rejected");
1557
+ assertEquals(loads, {
1558
+ key: 1,
1559
+ owner: 1
1560
+ });
1561
+ rejectContext = false;
1562
+ assert(await ctx.getSignedKeyOwner(options));
1563
+ assertEquals(loads, {
1564
+ key: 1,
1565
+ owner: 2
1566
+ });
1567
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1568
+ });
1569
+ await t.step("keeps clones and separate requests independent", async () => {
1570
+ exporter.clear();
1571
+ const { ctx, loads, federation } = setup();
1572
+ assert(await ctx.getSignedKeyOwner());
1573
+ const clone = ctx.clone(void 0);
1574
+ const separate = federation.createContext(new Request(request), void 0);
1575
+ await Promise.all([clone.getSignedKeyOwner(), separate.getSignedKeyOwner()]);
1576
+ assertEquals(loads, {
1577
+ key: 3,
1578
+ owner: 3
1579
+ });
1580
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 3);
1581
+ });
1582
+ } finally {
1583
+ await tracerProvider.shutdown();
1584
+ }
1585
+ });
1179
1586
  test("Federation.fetch() [rfc9421] multiple POST signatures", async (t) => {
1180
1587
  for (const [name, validSecond, maxHttpSignatures] of [
1181
1588
  [
@@ -1278,7 +1685,7 @@ test("Federation.fetch()", async (t) => {
1278
1685
  kv,
1279
1686
  documentLoaderFactory: () => mockDocumentLoader,
1280
1687
  authenticatedDocumentLoaderFactory(identity) {
1281
- const docLoader = getAuthenticatedDocumentLoader(identity);
1688
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
1282
1689
  return (url) => {
1283
1690
  if (new URL(url).host === "example.com") return docLoader(url);
1284
1691
  return mockDocumentLoader(url);
@@ -1729,7 +2136,7 @@ test("Federation.setInboxListeners()", async (t) => {
1729
2136
  kv,
1730
2137
  documentLoaderFactory: () => mockDocumentLoader,
1731
2138
  authenticatedDocumentLoaderFactory(identity) {
1732
- const docLoader = getAuthenticatedDocumentLoader(identity);
2139
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
1733
2140
  return (url) => {
1734
2141
  const urlObj = new URL(url);
1735
2142
  authenticatedRequests.push([url, identity.keyId.href]);
@@ -2052,7 +2459,7 @@ test("Federation.setInboxListeners()", async (t) => {
2052
2459
  kv,
2053
2460
  documentLoaderFactory: () => mockDocumentLoader,
2054
2461
  authenticatedDocumentLoaderFactory(identity) {
2055
- const docLoader = getAuthenticatedDocumentLoader(identity);
2462
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
2056
2463
  return (url) => {
2057
2464
  if (new URL(url).host === "example.com") return docLoader(url);
2058
2465
  return mockDocumentLoader(url);
@@ -2265,10 +2672,11 @@ test("Federation.setOutboxListeners()", async (t) => {
2265
2672
  sinks: ["buffer"]
2266
2673
  }]
2267
2674
  });
2268
- const federation = createFederation({
2675
+ const federation = new FederationImpl({
2269
2676
  kv,
2270
2677
  documentLoaderFactory: () => mockDocumentLoader
2271
2678
  });
2679
+ federation.allowPrivateAddress = true;
2272
2680
  federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
2273
2681
  privateKey: rsaPrivateKey2,
2274
2682
  publicKey: rsaPublicKey2.publicKey
@@ -2371,10 +2779,11 @@ test("Federation.setOutboxListeners()", async (t) => {
2371
2779
  sinks: ["buffer"]
2372
2780
  }]
2373
2781
  });
2374
- const federation = createFederation({
2782
+ const federation = new FederationImpl({
2375
2783
  kv,
2376
2784
  documentLoaderFactory: () => mockDocumentLoader
2377
2785
  });
2786
+ federation.allowPrivateAddress = true;
2378
2787
  federation.setActorDispatcher("/{identifier}", (_ctx, identifier) => identifier === "person2" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
2379
2788
  privateKey: rsaPrivateKey2,
2380
2789
  publicKey: rsaPublicKey2.publicKey
@@ -3054,7 +3463,7 @@ test("FederationImpl.sendActivity()", async (t) => {
3054
3463
  });
3055
3464
  const federation = new FederationImpl({
3056
3465
  kv: new MemoryKvStore(),
3057
- contextLoaderFactory: () => mockDocumentLoader
3466
+ allowPrivateAddress: true
3058
3467
  });
3059
3468
  const context = federation.createContext(new URL("https://example.com/"));
3060
3469
  await t.step("success", async () => {
@@ -6689,7 +7098,7 @@ test("ContextImpl.sendActivity()", async (t) => {
6689
7098
  const kv = new MemoryKvStore();
6690
7099
  const federation = new FederationImpl({
6691
7100
  kv,
6692
- contextLoaderFactory: () => mockDocumentLoader
7101
+ allowPrivateAddress: true
6693
7102
  });
6694
7103
  federation.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
6695
7104
  if (identifier !== "1") return null;
@@ -6812,11 +7221,13 @@ test("ContextImpl.sendActivity()", async (t) => {
6812
7221
  });
6813
7222
  await t.step("records recipient span attributes correctly", async () => {
6814
7223
  const [tracerProvider, exporter] = createTestTracerProvider();
6815
- const ctx = new FederationImpl({
7224
+ const federation3 = new FederationImpl({
6816
7225
  kv,
6817
7226
  contextLoaderFactory: () => mockDocumentLoader,
6818
7227
  tracerProvider
6819
- }).createContext(new URL("https://example.com/"), void 0);
7228
+ });
7229
+ federation3.allowPrivateAddress = true;
7230
+ const ctx = federation3.createContext(new URL("https://example.com/"), void 0);
6820
7231
  const activity = new vocab.Create({
6821
7232
  id: new URL("https://example.com/activity/telemetry"),
6822
7233
  actor: new URL("https://example.com/person"),
@@ -6850,7 +7261,7 @@ test("ContextImpl.sendActivity()", async (t) => {
6850
7261
  };
6851
7262
  const federation2 = new FederationImpl({
6852
7263
  kv,
6853
- contextLoaderFactory: () => mockDocumentLoader,
7264
+ allowPrivateAddress: true,
6854
7265
  queue
6855
7266
  });
6856
7267
  federation2.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
@@ -7021,7 +7432,7 @@ test("ContextImpl.sendActivity()", async (t) => {
7021
7432
  const asyncEnqueued = [];
7022
7433
  const fed = new FederationImpl({
7023
7434
  kv,
7024
- contextLoaderFactory: () => mockDocumentLoader,
7435
+ allowPrivateAddress: true,
7025
7436
  queue: {
7026
7437
  enqueue(message) {
7027
7438
  return new Promise((resolve) => {
@@ -7248,10 +7659,10 @@ test({
7248
7659
  const signedInvite = await signObject(new vocab.Invite({ actor: new URL("https://example.com/person2") }), ed25519PrivateKey, ed25519Multikey.id);
7249
7660
  assert(await ctx.routeActivity("id", signedInvite));
7250
7661
  assertEquals(activities, [[null, signedOffer], ["id", signedInvite]]);
7251
- assertFalse(await ctx.routeActivity(null, new vocab.Create({
7662
+ assertFalse(await withDnsLookup(() => Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")), () => ctx.routeActivity(null, new vocab.Create({
7252
7663
  id: new URL("https://example.com/not-found"),
7253
7664
  actor: new URL("https://example.com/person")
7254
- })));
7665
+ }))));
7255
7666
  assertEquals(activities, [[null, signedOffer], ["id", signedInvite]]);
7256
7667
  assertFalse(await ctx.routeActivity(null, new vocab.Create({
7257
7668
  id: new URL("https://example.com/person"),
@@ -7758,7 +8169,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
7758
8169
  const kv = new MemoryKvStore();
7759
8170
  const federation = new FederationImpl({
7760
8171
  kv,
7761
- contextLoaderFactory: () => mockDocumentLoader
8172
+ allowPrivateAddress: true
7762
8173
  });
7763
8174
  await t.step("skip", async () => {
7764
8175
  await new InboxContextImpl(null, {
@@ -7881,6 +8292,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
7881
8292
  contextLoaderFactory: () => mockDocumentLoader,
7882
8293
  tracerProvider
7883
8294
  });
8295
+ federationWithTracing.allowPrivateAddress = true;
7884
8296
  await new InboxContextImpl(null, await signJsonLd({
7885
8297
  "@context": "https://www.w3.org/ns/activitystreams",
7886
8298
  "type": "Create",
@@ -8004,12 +8416,14 @@ test("createFederation() applies httpMessageSignaturesSpecTtl to remembered spec
8004
8416
  return new Response(null, { status: key == null ? 401 : 202 });
8005
8417
  });
8006
8418
  const kv = new TtlRecordingKvStore();
8007
- const ctx = createFederation({
8419
+ const federation = new FederationImpl({
8008
8420
  kv,
8009
8421
  documentLoaderFactory: () => mockDocumentLoader,
8010
8422
  contextLoaderFactory: () => mockDocumentLoader,
8011
8423
  httpMessageSignaturesSpecTtl: { days: 14 }
8012
- }).createContext(new URL("https://example.com/"), void 0);
8424
+ });
8425
+ federation.allowPrivateAddress = true;
8426
+ const ctx = federation.createContext(new URL("https://example.com/"), void 0);
8013
8427
  const specKey = [
8014
8428
  "_fedify",
8015
8429
  "httpMessageSignaturesSpec",
@@ -8056,12 +8470,17 @@ test("createFederation() applies publicKeyTtl to cached public keys", async () =
8056
8470
  });
8057
8471
  const keyId = "https://example.com/person2#key3";
8058
8472
  const kv = new TtlRecordingKvStore();
8059
- const federation = createFederation({
8473
+ const federation = new FederationImpl({
8060
8474
  kv,
8061
8475
  documentLoaderFactory: () => mockDocumentLoader,
8062
8476
  contextLoaderFactory: () => mockDocumentLoader,
8477
+ authenticatedDocumentLoaderFactory: (identity, options) => getAuthenticatedDocumentLoader(identity, {
8478
+ ...options,
8479
+ allowPrivateAddress: true
8480
+ }),
8063
8481
  publicKeyTtl: { days: 7 }
8064
8482
  });
8483
+ federation.allowPrivateAddress = true;
8065
8484
  const inbox = [];
8066
8485
  federation.setActorDispatcher("/users/{identifier}", (_, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
8067
8486
  privateKey: rsaPrivateKey2,
@@ -8683,5 +9102,19 @@ test("createFederation() validates documentLoaderTimeout", () => {
8683
9102
  documentLoaderTimeout: Temporal.Duration.from({ seconds: 30 })
8684
9103
  });
8685
9104
  });
9105
+ /**
9106
+ * Runs the given function while `node:dns/promises`' `lookup()`, which the
9107
+ * default public fetch resolves hostnames with, is replaced, so that no test
9108
+ * depends on the real DNS.
9109
+ */
9110
+ async function withDnsLookup(lookup, fn) {
9111
+ const originalLookup = dns.lookup;
9112
+ dns.lookup = lookup;
9113
+ try {
9114
+ return await fn();
9115
+ } finally {
9116
+ dns.lookup = originalLookup;
9117
+ }
9118
+ }
8686
9119
  //#endregion
8687
9120
  export {};