@fedify/fedify 2.3.14 → 2.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/{builder-C6yXExXl.mjs → builder-B6vSXYxn.mjs} +2 -2
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-1He2YuBF.mjs → client-DpUNYKla.mjs} +113 -64
  4. package/dist/{client-CSddvgWN.d.ts → client-jE7oGvRh.d.ts} +0 -1
  5. package/dist/compat/mod.d.cts +1 -1
  6. package/dist/compat/mod.d.ts +1 -1
  7. package/dist/compat/transformers.test.mjs +1 -1
  8. package/dist/{context-MOrCC7yl.d.cts → context-CE_Tj_Oc.d.cts} +28 -2
  9. package/dist/{context-BWbdE_MZ.d.ts → context-H_5tZXg-.d.ts} +28 -2
  10. package/dist/{deno-y3L4Oa94.mjs → deno-BCuL3mid.mjs} +1 -1
  11. package/dist/docloader-D2dr1_6B.mjs +125 -0
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +5 -5
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/metrics.test.mjs +1 -1
  17. package/dist/federation/middleware.test.mjs +455 -29
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/send.test.mjs +498 -88
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{http-DNM9KyIO.js → http-BkjxY3U0.js} +172 -103
  26. package/dist/{http-BCSf1KIY.mjs → http-CSDcThvz.mjs} +196 -98
  27. package/dist/{http-DQmNpfym.d.ts → http-CZPYFK3b.d.cts} +1 -2
  28. package/dist/{http-DiWQB8Mr.d.cts → http-DmP0sr2l.d.ts} +1 -2
  29. package/dist/{http-D77Wjrcg.cjs → http-gOkAiIe1.cjs} +173 -104
  30. package/dist/{inbox-NObX26c9.mjs → inbox-BHxez4d5.mjs} +2 -2
  31. package/dist/{key-APURZ0ky.mjs → key-D8IMQ72M.mjs} +2 -2
  32. package/dist/{kv-cache-Bg4utAuv.cjs → kv-cache-Alm5UmJa.cjs} +59 -17
  33. package/dist/{kv-cache-C69dzgQi.mjs → kv-cache-Blr9nDDp.mjs} +1 -1
  34. package/dist/{kv-cache-LBCwHNLu.js → kv-cache-CkXgU-ak.js} +60 -18
  35. package/dist/{ld-BoLqbDaL.mjs → ld-CVxxE1Pp.mjs} +3 -3
  36. package/dist/{metrics-DBSl8wEr.mjs → metrics-SYIZHFf_.mjs} +1 -1
  37. package/dist/{middleware-BQUQ0pPl.cjs → middleware-4yPyyoF5.cjs} +237 -99
  38. package/dist/{middleware-CZ8tI1qL.mjs → middleware-CfskJGfV.mjs} +81 -41
  39. package/dist/{middleware-DAqo2eOY.mjs → middleware-D3JWfiCu.mjs} +1 -1
  40. package/dist/{middleware-CRd1JmuL.js → middleware-DhN7c65B.js} +238 -100
  41. package/dist/{mod-LmD03uRA.d.ts → mod-CDFFESfL.d.ts} +1 -1
  42. package/dist/{mod-Bid79LFU.d.cts → mod-C_gFmeNq.d.cts} +1 -1
  43. package/dist/{mod-Co1M3Ux0.d.cts → mod-DDVkmZ_h.d.cts} +2 -8
  44. package/dist/{mod-B6PwkUMU.d.cts → mod-DDjtbkZO.d.cts} +1 -1
  45. package/dist/{mod-Br8yJNcE.d.ts → mod-DMjhnFbg.d.ts} +2 -8
  46. package/dist/{mod-TLEvgT1i.d.ts → mod-HQXFvOHs.d.ts} +1 -1
  47. package/dist/mod.cjs +5 -5
  48. package/dist/mod.d.cts +6 -6
  49. package/dist/mod.d.ts +6 -6
  50. package/dist/mod.js +5 -5
  51. package/dist/nodeinfo/client.test.mjs +337 -19
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/nodeinfo/mod.cjs +1 -1
  54. package/dist/nodeinfo/mod.d.cts +1 -1
  55. package/dist/nodeinfo/mod.d.ts +1 -1
  56. package/dist/nodeinfo/mod.js +1 -1
  57. package/dist/otel/exporter.test.mjs +292 -30
  58. package/dist/otel/mod.cjs +36 -30
  59. package/dist/otel/mod.d.cts +13 -0
  60. package/dist/otel/mod.d.ts +13 -0
  61. package/dist/otel/mod.js +36 -30
  62. package/dist/{owner-DjKvngeb.mjs → owner-BluLVPf0.mjs} +2 -2
  63. package/dist/{proof-CWTnYhjS.cjs → proof-CR-HwI0l.cjs} +1 -1
  64. package/dist/{proof-D2oFOdNA.mjs → proof-Co6nQBVR.mjs} +3 -3
  65. package/dist/{proof-s9sy44kx.js → proof-Dg0d9QBo.js} +1 -1
  66. package/dist/{send-xDK5UnL-.mjs → send-D_DnqOFA.mjs} +171 -73
  67. package/dist/sig/http.test.mjs +528 -2
  68. package/dist/sig/key.test.mjs +2 -2
  69. package/dist/sig/ld.test.mjs +2 -2
  70. package/dist/sig/mod.cjs +2 -2
  71. package/dist/sig/mod.d.cts +2 -2
  72. package/dist/sig/mod.d.ts +2 -2
  73. package/dist/sig/mod.js +2 -2
  74. package/dist/sig/owner.test.mjs +1 -1
  75. package/dist/sig/proof.test.mjs +1 -1
  76. package/dist/{temporal-C6icc5n9.mjs → temporal-B8FUNkXB.mjs} +1 -1
  77. package/dist/testing/mod.d.mts +26 -0
  78. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  79. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  80. package/dist/utils/docloader.test.mjs +318 -34
  81. package/dist/utils/kv-cache.test.mjs +1 -1
  82. package/dist/utils/mod.cjs +1 -1
  83. package/dist/utils/mod.d.cts +1 -1
  84. package/dist/utils/mod.d.ts +1 -1
  85. package/dist/utils/mod.js +1 -1
  86. package/package.json +8 -8
  87. package/dist/docloader-yuyzZC9a.mjs +0 -83
@@ -9,15 +9,15 @@ import { t as assertRejects } from "../assert_rejects-D1HYAik3.mjs";
9
9
  import { n as assertInstanceOf, t as assert } from "../assert-CEO06XCw.mjs";
10
10
  import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
11
11
  import { t as assertStrictEquals } from "../assert_strict_equals-XEgZAlrj.mjs";
12
- import { g as recordInboxActivity } from "../metrics-DBSl8wEr.mjs";
13
- import { l as verifyRequest, s as signRequest } from "../http-BCSf1KIY.mjs";
12
+ import { g as recordInboxActivity } from "../metrics-SYIZHFf_.mjs";
13
+ import { c as signRequest, u as verifyRequest } from "../http-CSDcThvz.mjs";
14
14
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
15
- import { t as getAuthenticatedDocumentLoader } from "../docloader-yuyzZC9a.mjs";
15
+ import { n as getAuthenticatedDocumentLoader } from "../docloader-D2dr1_6B.mjs";
16
16
  import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
17
- import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-BoLqbDaL.mjs";
18
- import { t as doesActorOwnKey } from "../owner-DjKvngeb.mjs";
19
- import { i as verifyObject, r as signObject } from "../proof-D2oFOdNA.mjs";
20
- import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-CZ8tI1qL.mjs";
17
+ import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-CVxxE1Pp.mjs";
18
+ import { t as doesActorOwnKey } from "../owner-BluLVPf0.mjs";
19
+ import { i as verifyObject, r as signObject } from "../proof-Co6nQBVR.mjs";
20
+ import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-CfskJGfV.mjs";
21
21
  import { t as CircuitBreaker } from "../circuit-breaker-D1fxthDI.mjs";
22
22
  import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-CAdwjgMi.mjs";
23
23
  import { configure, reset } from "@logtape/logtape";
@@ -27,9 +27,9 @@ import { Create, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
27
27
  import { SpanStatusCode, metrics } from "@opentelemetry/api";
28
28
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
29
29
  import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
30
- import dns from "node:dns/promises";
31
30
  import serialize from "json-canon";
32
31
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
32
+ import dns from "node:dns/promises";
33
33
  //#region ../fixture/src/fixtures/example.com/create.json
34
34
  var id$2 = "https://example.com/create";
35
35
  var create_default = {
@@ -94,7 +94,7 @@ var person2_default = {
94
94
  };
95
95
  //#endregion
96
96
  //#region src/federation/middleware.test.ts
97
- const documentLoader = getDocumentLoader();
97
+ const documentLoader = getDocumentLoader({ allowPrivateAddress: true });
98
98
  let logtapeLock = Promise.resolve();
99
99
  async function withLogtapeLock(fn) {
100
100
  const run = logtapeLock.then(fn, fn);
@@ -649,7 +649,8 @@ test({
649
649
  const federation = createFederation({
650
650
  kv,
651
651
  documentLoaderFactory: () => rejectingLoader,
652
- contextLoaderFactory: () => mockDocumentLoader
652
+ contextLoaderFactory: () => mockDocumentLoader,
653
+ authenticatedDocumentLoaderFactory: (identity) => getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true })
653
654
  });
654
655
  let ctx = federation.createContext(new URL("https://example.com:1234/"), 123);
655
656
  assertEquals(ctx.data, 123);
@@ -760,7 +761,7 @@ test({
760
761
  documentUrl: "https://example.com/auth-check",
761
762
  document: true
762
763
  });
763
- assertEquals(await ctx.lookupObject("https://example.com/object"), null);
764
+ assertEquals(await withDnsLookup(() => Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")), () => ctx.lookupObject("https://example.com/object")), null);
764
765
  await assertRejects(() => ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({})), TypeError, "The activity to send must have at least one actor property.");
765
766
  await ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({ actor: new URL("https://example.com/users/handle") }));
766
767
  esm_default.get("https://example.com/object", () => new Response(JSON.stringify({
@@ -1009,7 +1010,7 @@ test({
1009
1010
  assertStrictEquals(clone.contextLoader, ctx.contextLoader);
1010
1011
  assertStrictEquals(clone.federation, ctx.federation);
1011
1012
  });
1012
- esm_default.get("https://example.com/.well-known/nodeinfo", (cl) => {
1013
+ esm_default.get("path:/.well-known/nodeinfo", (cl) => {
1013
1014
  const headers = cl.options.headers ?? {};
1014
1015
  assertEquals(new Headers(headers).get("User-Agent"), "CustomUserAgent/1.2.3");
1015
1016
  return new Response(JSON.stringify({ links: [{
@@ -1017,7 +1018,7 @@ test({
1017
1018
  href: "https://example.com/nodeinfo/2.1"
1018
1019
  }] }));
1019
1020
  });
1020
- esm_default.get("https://example.com/nodeinfo/2.1", (cl) => {
1021
+ esm_default.get("path:/nodeinfo/2.1", (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({
@@ -1038,7 +1039,11 @@ test({
1038
1039
  kv,
1039
1040
  userAgent: "CustomUserAgent/1.2.3"
1040
1041
  }).createContext(new URL("https://example.com/"), 123);
1041
- assertEquals(await ctx.lookupNodeInfo("https://example.com/"), {
1042
+ const lookupExample = () => Promise.resolve([{
1043
+ address: "93.184.215.14",
1044
+ family: 4
1045
+ }]);
1046
+ assertEquals(await withDnsLookup(lookupExample, () => ctx.lookupNodeInfo("https://example.com/")), {
1042
1047
  software: {
1043
1048
  name: "foo",
1044
1049
  version: "1.2.3"
@@ -1050,7 +1055,7 @@ test({
1050
1055
  localComments: 456
1051
1056
  }
1052
1057
  });
1053
- assertEquals(await ctx.lookupNodeInfo("https://example.com/", { parse: "none" }), {
1058
+ assertEquals(await withDnsLookup(lookupExample, () => ctx.lookupNodeInfo("https://example.com/", { parse: "none" })), {
1054
1059
  software: {
1055
1060
  name: "foo",
1056
1061
  version: "1.2.3"
@@ -1169,6 +1174,408 @@ test({
1169
1174
  esm_default.hardReset();
1170
1175
  }
1171
1176
  });
1177
+ test("Federation.fetch() accepts unencoded path colons", async () => {
1178
+ const identifier = "https://pod.example/alice";
1179
+ const federation = createFederation({
1180
+ kv: new MemoryKvStore(),
1181
+ contextLoaderFactory: () => mockDocumentLoader
1182
+ });
1183
+ const dispatched = [];
1184
+ federation.setActorDispatcher("/users/{identifier}", (ctx, id) => {
1185
+ dispatched.push(id);
1186
+ assertEquals(ctx.url.pathname, "/users/https:%2F%2Fpod.example%2Falice");
1187
+ assertEquals(ctx.url.search, "?page=1");
1188
+ assertEquals(ctx.request.url, ctx.url.href);
1189
+ return new vocab.Person({ id: ctx.getActorUri(id) });
1190
+ });
1191
+ const request = new Request("https://local.example/users/https:%2F%2Fpod.example%2Falice?page=1", { headers: { Accept: "application/activity+json" } });
1192
+ const response = await federation.fetch(request, { contextData: void 0 });
1193
+ assertEquals(response.status, 200);
1194
+ assertEquals(dispatched, [identifier]);
1195
+ const canonical = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice";
1196
+ const body = await response.json();
1197
+ assert(body != null && typeof body === "object" && "id" in body);
1198
+ assertEquals(body.id, canonical);
1199
+ const ctx = federation.createContext(request, void 0);
1200
+ assertEquals(ctx.getActorUri(identifier).href, canonical);
1201
+ assertEquals(ctx.parseUri(new URL(canonical)), {
1202
+ type: "actor",
1203
+ identifier
1204
+ });
1205
+ assertEquals(ctx.parseUri(new URL(request.url)), null);
1206
+ assertEquals(ctx.parseUri(new URL(canonical.replace("local.example", "other.example"))), null);
1207
+ });
1208
+ test("Federation.fetch() verifies signatures over unencoded path colons", async (t) => {
1209
+ const identifier = "https://pod.example/alice";
1210
+ const canonical = "https://local.example/users/https%3A%2F%2Fpod.example%2Falice/inbox";
1211
+ const encoded = canonical + "?delivery=1";
1212
+ const normalized = encoded.replace("%3A", ":");
1213
+ for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) for (const target of [
1214
+ "encoded",
1215
+ "normalized",
1216
+ "changed after signing"
1217
+ ]) await t.step(`${spec}: ${target}`, async () => {
1218
+ const recipients = [];
1219
+ const federation = createFederation({
1220
+ kv: new MemoryKvStore(),
1221
+ documentLoaderFactory: () => mockDocumentLoader,
1222
+ contextLoaderFactory: () => mockDocumentLoader
1223
+ });
1224
+ federation.setActorDispatcher("/users/{identifier}", (ctx, id) => new vocab.Person({ id: ctx.getActorUri(id) })).setKeyPairsDispatcher(() => []);
1225
+ federation.setInboxListeners("/users/{identifier}/inbox").on(vocab.Create, (ctx) => {
1226
+ recipients.push(ctx.recipient);
1227
+ });
1228
+ const signed = await signRequest(new Request(target === "normalized" ? normalized : encoded, {
1229
+ method: "POST",
1230
+ headers: { "Content-Type": "application/activity+json" },
1231
+ body: JSON.stringify(create_default)
1232
+ }), rsaPrivateKey2, rsaPublicKey2.id, { spec });
1233
+ const request = target === "changed after signing" ? new Request(normalized, signed) : signed;
1234
+ const response = await federation.fetch(request, { contextData: void 0 });
1235
+ assertEquals(request.url, target === "encoded" ? encoded : normalized);
1236
+ const valid = target !== "changed after signing";
1237
+ assertEquals(response.status, valid ? 202 : 401);
1238
+ assertEquals(recipients, valid ? [identifier] : []);
1239
+ assertEquals(federation.createContext(new URL(canonical), void 0).getInboxUri(identifier).href, canonical);
1240
+ });
1241
+ });
1242
+ test("RequestContext signature checks", async (t) => {
1243
+ const [tracerProvider, exporter] = createTestTracerProvider();
1244
+ const keyId = rsaPublicKey3.id.href;
1245
+ const ownerId = rsaPublicKey3.ownerId.href;
1246
+ const request = await signRequest(new Request("https://example.com/"), rsaPrivateKey3, rsaPublicKey3.id);
1247
+ function setup(options = {}) {
1248
+ const loads = {
1249
+ key: 0,
1250
+ owner: 0
1251
+ };
1252
+ const loader = async (url, loaderOptions) => {
1253
+ if (url === keyId) loads.key++;
1254
+ else if (url === ownerId) loads.owner++;
1255
+ await options.gate;
1256
+ return await (options.load ?? mockDocumentLoader)(url, loaderOptions);
1257
+ };
1258
+ const federation = createFederation({
1259
+ kv: new MemoryKvStore(),
1260
+ documentLoaderFactory: () => loader,
1261
+ contextLoaderFactory: () => mockDocumentLoader,
1262
+ tracerProvider: options.tracerProvider ?? tracerProvider
1263
+ });
1264
+ return {
1265
+ ctx: federation.createContext(options.request ?? new Request(request), void 0),
1266
+ loads,
1267
+ loader,
1268
+ federation
1269
+ };
1270
+ }
1271
+ try {
1272
+ for (const mode of [
1273
+ "keys",
1274
+ "owners",
1275
+ "warm owners",
1276
+ "mixed"
1277
+ ]) await t.step(`coalesces ${mode}`, async () => {
1278
+ exporter.clear();
1279
+ let release;
1280
+ const { ctx, loads, loader } = setup({ gate: new Promise((resolve) => release = resolve) });
1281
+ if (mode === "warm owners") {
1282
+ release();
1283
+ assert(await ctx.getSignedKey());
1284
+ }
1285
+ const results = Promise.all(Array.from({ length: 8 }, (_, i) => {
1286
+ const options = i % 2 === 0 ? {} : {
1287
+ documentLoader: loader,
1288
+ contextLoader: ctx.contextLoader,
1289
+ tracerProvider
1290
+ };
1291
+ return mode === "keys" || mode === "mixed" && i % 2 === 0 ? ctx.getSignedKey(options) : ctx.getSignedKeyOwner(options);
1292
+ }));
1293
+ release();
1294
+ for (const result of await results) assert(result != null);
1295
+ assertEquals(loads, {
1296
+ key: 1,
1297
+ owner: mode === "keys" ? 0 : 1
1298
+ });
1299
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1300
+ assert(await ctx.getSignedKey());
1301
+ if (mode !== "keys") assert(await ctx.getSignedKeyOwner());
1302
+ assertEquals(loads, {
1303
+ key: 1,
1304
+ owner: mode === "keys" ? 0 : 1
1305
+ });
1306
+ });
1307
+ await t.step("memoizes null keys", async () => {
1308
+ exporter.clear();
1309
+ const { ctx, loads } = setup({ load: () => Promise.reject(/* @__PURE__ */ new Error("unavailable")) });
1310
+ for (let i = 0; i < 3; i++) {
1311
+ assertEquals(await ctx.getSignedKey(), null);
1312
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1313
+ }
1314
+ assertEquals(loads, {
1315
+ key: 1,
1316
+ owner: 0
1317
+ });
1318
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1319
+ assert(await ctx.getSignedKey({ documentLoader: mockDocumentLoader }));
1320
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1321
+ });
1322
+ await t.step("coalesces key-owner FetchError results", async () => {
1323
+ const { ctx, loads } = setup({ load: async (url, options) => {
1324
+ if (url === ownerId) throw new FetchError(new URL(url), "HTTP 401: Unauthorized");
1325
+ return await mockDocumentLoader(url, options);
1326
+ } });
1327
+ assert(await ctx.getSignedKey());
1328
+ assertEquals(await Promise.all(Array.from({ length: 8 }, () => ctx.getSignedKeyOwner())), Array(8).fill(null));
1329
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1330
+ assertEquals(loads, {
1331
+ key: 1,
1332
+ owner: 1
1333
+ });
1334
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1335
+ });
1336
+ await t.step("memoizes unsigned requests", async () => {
1337
+ exporter.clear();
1338
+ const { ctx, loads } = setup({ request: new Request("https://example.com/") });
1339
+ for (let i = 0; i < 3; i++) {
1340
+ assertEquals(await ctx.getSignedKey(), null);
1341
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1342
+ }
1343
+ assertEquals(loads, {
1344
+ key: 0,
1345
+ owner: 0
1346
+ });
1347
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1348
+ });
1349
+ await t.step("memoizes null owners without trusting a key's owner claim", async () => {
1350
+ const { ctx, loads } = setup({ load: async (url, options) => url === ownerId ? {
1351
+ documentUrl: url,
1352
+ contextUrl: null,
1353
+ document: {
1354
+ ...person2_default,
1355
+ publicKey: []
1356
+ }
1357
+ } : await mockDocumentLoader(url, options) });
1358
+ assert(await ctx.getSignedKey());
1359
+ assertEquals(await Promise.all(Array.from({ length: 8 }, () => ctx.getSignedKeyOwner())), Array(8).fill(null));
1360
+ for (let i = 0; i < 3; i++) assertEquals(await ctx.getSignedKeyOwner(), null);
1361
+ assertEquals(loads, {
1362
+ key: 1,
1363
+ owner: 1
1364
+ });
1365
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1366
+ assert(await ctx.getSignedKeyOwner());
1367
+ });
1368
+ await t.step("isolates concurrent document loaders before success", async () => {
1369
+ const { ctx, loads } = setup();
1370
+ let otherLoads = 0;
1371
+ const otherLoader = async (url, options) => {
1372
+ otherLoads++;
1373
+ if (url === ownerId) return {
1374
+ documentUrl: url,
1375
+ contextUrl: null,
1376
+ document: {
1377
+ ...person2_default,
1378
+ name: "Other view"
1379
+ }
1380
+ };
1381
+ return await mockDocumentLoader(url, options);
1382
+ };
1383
+ const [first, second] = await Promise.all([ctx.getSignedKeyOwner(), ctx.getSignedKeyOwner({ documentLoader: otherLoader })]);
1384
+ assertEquals(first?.name, "Jane Doe");
1385
+ assertEquals(second?.name, "Other view");
1386
+ const cached = await ctx.getSignedKeyOwner();
1387
+ assert(cached === first || cached === second);
1388
+ assertStrictEquals(await ctx.getSignedKeyOwner({ documentLoader: otherLoader }), cached);
1389
+ assertEquals(loads, {
1390
+ key: 1,
1391
+ owner: 1
1392
+ });
1393
+ assertEquals(otherLoads, 2);
1394
+ });
1395
+ await t.step("preserves in-flight null while another loader succeeds", async () => {
1396
+ exporter.clear();
1397
+ let release;
1398
+ const gate = new Promise((resolve) => release = resolve);
1399
+ const { ctx, loads } = setup({ load: async () => {
1400
+ await gate;
1401
+ throw new TypeError("fetch failed");
1402
+ } });
1403
+ const failed = Promise.all([ctx.getSignedKey(), ctx.getSignedKeyOwner()]);
1404
+ const owner = await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader });
1405
+ assert(owner);
1406
+ release();
1407
+ assertEquals(await failed, [null, null]);
1408
+ assertStrictEquals(await ctx.getSignedKeyOwner(), owner);
1409
+ assert(await ctx.getSignedKey());
1410
+ assertEquals(loads, {
1411
+ key: 1,
1412
+ owner: 0
1413
+ });
1414
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1415
+ });
1416
+ await t.step("isolates context loaders", async () => {
1417
+ exporter.clear();
1418
+ const { ctx, loads } = setup();
1419
+ const otherContextLoader = (url, options) => mockDocumentLoader(url, options);
1420
+ await Promise.all([ctx.getSignedKeyOwner(), ctx.getSignedKeyOwner({ contextLoader: otherContextLoader })]);
1421
+ assertEquals(loads, {
1422
+ key: 2,
1423
+ owner: 2
1424
+ });
1425
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1426
+ });
1427
+ await t.step("shares work across tracer providers", async () => {
1428
+ exporter.clear();
1429
+ const [otherProvider, otherExporter] = createTestTracerProvider();
1430
+ try {
1431
+ const { ctx, loads } = setup();
1432
+ await Promise.all([
1433
+ ctx.getSignedKeyOwner(),
1434
+ ctx.getSignedKeyOwner({ tracerProvider: otherProvider }),
1435
+ ctx.getSignedKey(),
1436
+ ctx.getSignedKey({ tracerProvider: otherProvider })
1437
+ ]);
1438
+ assert(await ctx.getSignedKeyOwner({ tracerProvider: otherProvider }));
1439
+ assertEquals(loads, {
1440
+ key: 1,
1441
+ owner: 1
1442
+ });
1443
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1444
+ assertEquals(otherExporter.getSpans("http_signatures.verify").length, 0);
1445
+ } finally {
1446
+ await otherProvider.shutdown();
1447
+ }
1448
+ });
1449
+ await t.step("reuses success with a fresh authorization loader", async () => {
1450
+ exporter.clear();
1451
+ const { ctx, loads } = setup();
1452
+ let freshLoads = 0;
1453
+ const freshLoader = (url, options) => {
1454
+ freshLoads++;
1455
+ return mockDocumentLoader(url, options);
1456
+ };
1457
+ const key = await ctx.getSignedKey({ documentLoader: freshLoader });
1458
+ assert(key);
1459
+ assertStrictEquals(await ctx.getSignedKey(), key);
1460
+ const owner = await ctx.getSignedKeyOwner();
1461
+ assert(owner);
1462
+ assertStrictEquals(await ctx.getSignedKeyOwner({ documentLoader: freshLoader }), owner);
1463
+ assertEquals(loads, {
1464
+ key: 0,
1465
+ owner: 1
1466
+ });
1467
+ assertEquals(freshLoads, 1);
1468
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1469
+ });
1470
+ await t.step("retains a transient owner lookup failure as null", async () => {
1471
+ let fail = true;
1472
+ const { ctx, loads } = setup({ load: async (url, options) => {
1473
+ if (url === ownerId && fail) throw new TypeError("fetch failed");
1474
+ return await mockDocumentLoader(url, options);
1475
+ } });
1476
+ assert(await ctx.getSignedKey());
1477
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1478
+ fail = false;
1479
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1480
+ assertEquals(loads, {
1481
+ key: 1,
1482
+ owner: 1
1483
+ });
1484
+ assert(await ctx.getSignedKeyOwner({ documentLoader: mockDocumentLoader }));
1485
+ });
1486
+ await t.step("rechecks an owner after another loader verifies the key", async () => {
1487
+ let fail = true;
1488
+ const { ctx, loads } = setup({ load: async (url, options) => {
1489
+ if (url === keyId && fail) throw new TypeError("fetch failed");
1490
+ return await mockDocumentLoader(url, options);
1491
+ } });
1492
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1493
+ fail = false;
1494
+ assertEquals(await ctx.getSignedKeyOwner(), null);
1495
+ assertEquals(loads, {
1496
+ key: 1,
1497
+ owner: 0
1498
+ });
1499
+ assert(await ctx.getSignedKey({ documentLoader: mockDocumentLoader }));
1500
+ assertEquals((await ctx.getSignedKeyOwner())?.id?.href, ownerId);
1501
+ assertEquals(loads, {
1502
+ key: 1,
1503
+ owner: 1
1504
+ });
1505
+ });
1506
+ await t.step("retries unexpected key rejections from a rejection handler", async () => {
1507
+ exporter.clear();
1508
+ const failure = /* @__PURE__ */ new Error("transient context loader failure");
1509
+ let rejectContext = true;
1510
+ const contextLoader = async (url, options) => {
1511
+ if (rejectContext) throw failure;
1512
+ return await mockDocumentLoader(url, options);
1513
+ };
1514
+ const { ctx, loads } = setup();
1515
+ const options = { contextLoader };
1516
+ const failedKeys = Promise.allSettled([
1517
+ ctx.getSignedKey(options),
1518
+ ctx.getSignedKeyOwner(options),
1519
+ ctx.getSignedKey(options)
1520
+ ]);
1521
+ const recovery = ctx.getSignedKey(options).catch(() => {
1522
+ rejectContext = false;
1523
+ return ctx.getSignedKey(options);
1524
+ });
1525
+ for (const result of await failedKeys) assertEquals(result.status, "rejected");
1526
+ assert(await recovery);
1527
+ assertEquals(loads, {
1528
+ key: 2,
1529
+ owner: 0
1530
+ });
1531
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 2);
1532
+ });
1533
+ await t.step("retries owner rejections while retaining the verified key", async () => {
1534
+ exporter.clear();
1535
+ let rejectContext = false;
1536
+ const contextLoader = async (url, options) => {
1537
+ if (rejectContext) throw new Error("transient owner context failure");
1538
+ return await mockDocumentLoader(url, options);
1539
+ };
1540
+ const { ctx, loads } = setup();
1541
+ const options = { contextLoader };
1542
+ assert(await ctx.getSignedKey(options));
1543
+ assertEquals(loads, {
1544
+ key: 1,
1545
+ owner: 0
1546
+ });
1547
+ rejectContext = true;
1548
+ const failures = await Promise.allSettled([ctx.getSignedKeyOwner(options), ctx.getSignedKeyOwner(options)]);
1549
+ for (const result of failures) assertEquals(result.status, "rejected");
1550
+ assertEquals(loads, {
1551
+ key: 1,
1552
+ owner: 1
1553
+ });
1554
+ rejectContext = false;
1555
+ assert(await ctx.getSignedKeyOwner(options));
1556
+ assertEquals(loads, {
1557
+ key: 1,
1558
+ owner: 2
1559
+ });
1560
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 1);
1561
+ });
1562
+ await t.step("keeps clones and separate requests independent", async () => {
1563
+ exporter.clear();
1564
+ const { ctx, loads, federation } = setup();
1565
+ assert(await ctx.getSignedKeyOwner());
1566
+ const clone = ctx.clone(void 0);
1567
+ const separate = federation.createContext(new Request(request), void 0);
1568
+ await Promise.all([clone.getSignedKeyOwner(), separate.getSignedKeyOwner()]);
1569
+ assertEquals(loads, {
1570
+ key: 3,
1571
+ owner: 3
1572
+ });
1573
+ assertEquals(exporter.getSpans("http_signatures.verify").length, 3);
1574
+ });
1575
+ } finally {
1576
+ await tracerProvider.shutdown();
1577
+ }
1578
+ });
1172
1579
  test("Federation.fetch() [rfc9421] multiple POST signatures", async (t) => {
1173
1580
  for (const validSecond of [true, false]) await t.step(validSecond ? "valid second signature" : "both signatures invalid", async () => {
1174
1581
  const inbox = [];
@@ -1212,7 +1619,7 @@ test("Federation.fetch()", async (t) => {
1212
1619
  kv,
1213
1620
  documentLoaderFactory: () => mockDocumentLoader,
1214
1621
  authenticatedDocumentLoaderFactory(identity) {
1215
- const docLoader = getAuthenticatedDocumentLoader(identity);
1622
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
1216
1623
  return (url) => {
1217
1624
  if (new URL(url).host === "example.com") return docLoader(url);
1218
1625
  return mockDocumentLoader(url);
@@ -1663,7 +2070,7 @@ test("Federation.setInboxListeners()", async (t) => {
1663
2070
  kv,
1664
2071
  documentLoaderFactory: () => mockDocumentLoader,
1665
2072
  authenticatedDocumentLoaderFactory(identity) {
1666
- const docLoader = getAuthenticatedDocumentLoader(identity);
2073
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
1667
2074
  return (url) => {
1668
2075
  const urlObj = new URL(url);
1669
2076
  authenticatedRequests.push([url, identity.keyId.href]);
@@ -1986,7 +2393,7 @@ test("Federation.setInboxListeners()", async (t) => {
1986
2393
  kv,
1987
2394
  documentLoaderFactory: () => mockDocumentLoader,
1988
2395
  authenticatedDocumentLoaderFactory(identity) {
1989
- const docLoader = getAuthenticatedDocumentLoader(identity);
2396
+ const docLoader = getAuthenticatedDocumentLoader(identity, { allowPrivateAddress: true });
1990
2397
  return (url) => {
1991
2398
  if (new URL(url).host === "example.com") return docLoader(url);
1992
2399
  return mockDocumentLoader(url);
@@ -2199,10 +2606,11 @@ test("Federation.setOutboxListeners()", async (t) => {
2199
2606
  sinks: ["buffer"]
2200
2607
  }]
2201
2608
  });
2202
- const federation = createFederation({
2609
+ const federation = new FederationImpl({
2203
2610
  kv,
2204
2611
  documentLoaderFactory: () => mockDocumentLoader
2205
2612
  });
2613
+ federation.allowPrivateAddress = true;
2206
2614
  federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
2207
2615
  privateKey: rsaPrivateKey2,
2208
2616
  publicKey: rsaPublicKey2.publicKey
@@ -2305,10 +2713,11 @@ test("Federation.setOutboxListeners()", async (t) => {
2305
2713
  sinks: ["buffer"]
2306
2714
  }]
2307
2715
  });
2308
- const federation = createFederation({
2716
+ const federation = new FederationImpl({
2309
2717
  kv,
2310
2718
  documentLoaderFactory: () => mockDocumentLoader
2311
2719
  });
2720
+ federation.allowPrivateAddress = true;
2312
2721
  federation.setActorDispatcher("/{identifier}", (_ctx, identifier) => identifier === "person2" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
2313
2722
  privateKey: rsaPrivateKey2,
2314
2723
  publicKey: rsaPublicKey2.publicKey
@@ -2540,7 +2949,7 @@ test("FederationImpl.sendActivity()", async (t) => {
2540
2949
  });
2541
2950
  const federation = new FederationImpl({
2542
2951
  kv: new MemoryKvStore(),
2543
- contextLoaderFactory: () => mockDocumentLoader
2952
+ allowPrivateAddress: true
2544
2953
  });
2545
2954
  const context = federation.createContext(new URL("https://example.com/"));
2546
2955
  await t.step("success", async () => {
@@ -6128,7 +6537,7 @@ test("ContextImpl.sendActivity()", async (t) => {
6128
6537
  const kv = new MemoryKvStore();
6129
6538
  const federation = new FederationImpl({
6130
6539
  kv,
6131
- contextLoaderFactory: () => mockDocumentLoader
6540
+ allowPrivateAddress: true
6132
6541
  });
6133
6542
  federation.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
6134
6543
  if (identifier !== "1") return null;
@@ -6251,11 +6660,13 @@ test("ContextImpl.sendActivity()", async (t) => {
6251
6660
  });
6252
6661
  await t.step("records recipient span attributes correctly", async () => {
6253
6662
  const [tracerProvider, exporter] = createTestTracerProvider();
6254
- const ctx = new FederationImpl({
6663
+ const federation3 = new FederationImpl({
6255
6664
  kv,
6256
6665
  contextLoaderFactory: () => mockDocumentLoader,
6257
6666
  tracerProvider
6258
- }).createContext(new URL("https://example.com/"), void 0);
6667
+ });
6668
+ federation3.allowPrivateAddress = true;
6669
+ const ctx = federation3.createContext(new URL("https://example.com/"), void 0);
6259
6670
  const activity = new vocab.Create({
6260
6671
  id: new URL("https://example.com/activity/telemetry"),
6261
6672
  actor: new URL("https://example.com/person"),
@@ -6289,7 +6700,7 @@ test("ContextImpl.sendActivity()", async (t) => {
6289
6700
  };
6290
6701
  const federation2 = new FederationImpl({
6291
6702
  kv,
6292
- contextLoaderFactory: () => mockDocumentLoader,
6703
+ allowPrivateAddress: true,
6293
6704
  queue
6294
6705
  });
6295
6706
  federation2.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
@@ -6460,7 +6871,7 @@ test("ContextImpl.sendActivity()", async (t) => {
6460
6871
  const asyncEnqueued = [];
6461
6872
  const fed = new FederationImpl({
6462
6873
  kv,
6463
- contextLoaderFactory: () => mockDocumentLoader,
6874
+ allowPrivateAddress: true,
6464
6875
  queue: {
6465
6876
  enqueue(message) {
6466
6877
  return new Promise((resolve) => {
@@ -6687,10 +7098,10 @@ test({
6687
7098
  const signedInvite = await signObject(new vocab.Invite({ actor: new URL("https://example.com/person2") }), ed25519PrivateKey, ed25519Multikey.id);
6688
7099
  assert(await ctx.routeActivity("id", signedInvite));
6689
7100
  assertEquals(activities, [[null, signedOffer], ["id", signedInvite]]);
6690
- assertFalse(await ctx.routeActivity(null, new vocab.Create({
7101
+ assertFalse(await withDnsLookup(() => Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")), () => ctx.routeActivity(null, new vocab.Create({
6691
7102
  id: new URL("https://example.com/not-found"),
6692
7103
  actor: new URL("https://example.com/person")
6693
- })));
7104
+ }))));
6694
7105
  assertEquals(activities, [[null, signedOffer], ["id", signedInvite]]);
6695
7106
  assertFalse(await ctx.routeActivity(null, new vocab.Create({
6696
7107
  id: new URL("https://example.com/person"),
@@ -7196,7 +7607,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
7196
7607
  const kv = new MemoryKvStore();
7197
7608
  const federation = new FederationImpl({
7198
7609
  kv,
7199
- contextLoaderFactory: () => mockDocumentLoader
7610
+ allowPrivateAddress: true
7200
7611
  });
7201
7612
  await t.step("skip", async () => {
7202
7613
  await new InboxContextImpl(null, {
@@ -7319,6 +7730,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => {
7319
7730
  contextLoaderFactory: () => mockDocumentLoader,
7320
7731
  tracerProvider
7321
7732
  });
7733
+ federationWithTracing.allowPrivateAddress = true;
7322
7734
  await new InboxContextImpl(null, await signJsonLd({
7323
7735
  "@context": "https://www.w3.org/ns/activitystreams",
7324
7736
  "type": "Create",
@@ -7511,5 +7923,19 @@ test("Federation.fetch() bounds inbox bodies before dispatch", async () => {
7511
7923
  }), { contextData: void 0 })).status, 202);
7512
7924
  assert(dispatched);
7513
7925
  });
7926
+ /**
7927
+ * Runs the given function while `node:dns/promises`' `lookup()`, which the
7928
+ * default public fetch resolves hostnames with, is replaced, so that no test
7929
+ * depends on the real DNS.
7930
+ */
7931
+ async function withDnsLookup(lookup, fn) {
7932
+ const originalLookup = dns.lookup;
7933
+ dns.lookup = lookup;
7934
+ try {
7935
+ return await fn();
7936
+ } finally {
7937
+ dns.lookup = originalLookup;
7938
+ }
7939
+ }
7514
7940
  //#endregion
7515
7941
  export {};
@@ -2,7 +2,7 @@ const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  require("../chunk-DDcVe30Y.cjs");
5
- const require_middleware = require("../middleware-BQUQ0pPl.cjs");
5
+ const require_middleware = require("../middleware-4yPyyoF5.cjs");
6
6
  let _logtape_logtape = require("@logtape/logtape");
7
7
  let _fedify_uri_template = require("@fedify/uri-template");
8
8
  let es_toolkit = require("es-toolkit");