@fedify/fedify 2.1.30 → 2.1.31
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.
- package/dist/{builder-CTpfuuVp.mjs → builder-isMRh_I6.mjs} +3 -3
- package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
- package/dist/{client-CjDMsn4a.mjs → client-BZr7TGyu.mjs} +113 -64
- package/dist/{client-CSddvgWN.d.ts → client-jE7oGvRh.d.ts} +0 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-DLLaDau8.d.ts → context-C0uGVzgk.d.ts} +28 -2
- package/dist/{context-BNwv9Q1v.d.cts → context-CLaLxi49.d.cts} +28 -2
- package/dist/{deno-BATJ1hq6.mjs → deno-CiaJqTcu.mjs} +1 -1
- package/dist/docloader-DOrwedT7.mjs +124 -0
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +6 -6
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +380 -24
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/send.test.mjs +445 -70
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{http-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
- package/dist/{http-Bjd6I92u.mjs → http-A9mP2uM7.mjs} +194 -96
- package/dist/{http-BoR1M3W7.cjs → http-CRiVlZd5.cjs} +172 -103
- package/dist/{http-CK3efjgM.js → http-Crml3gei.js} +171 -102
- package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
- package/dist/{inbox-9MlBGgs9.mjs → inbox-KESLudq4.mjs} +1 -1
- package/dist/{key-ClQh9Qqh.mjs → key-Dg6-DV_O.mjs} +1 -1
- package/dist/{kv-cache-BxSx_Iyp.cjs → kv-cache-Cqmm-yQa.cjs} +59 -17
- package/dist/{kv-cache-ImApO425.js → kv-cache-DQcyZL5T.js} +60 -18
- package/dist/{ld-Crg0Xmwr.mjs → ld-COjDN6Nb.mjs} +2 -2
- package/dist/{middleware-CmD3lpy4.cjs → middleware-BbgR7xOI.cjs} +184 -59
- package/dist/{middleware-jL9p9V9G.cjs → middleware-BtDQUCcz.cjs} +1 -1
- package/dist/{middleware-CE0b69gj.mjs → middleware-COFlKeYn.mjs} +78 -38
- package/dist/{middleware-Bi5D6DBT.mjs → middleware-CuQvczZd.mjs} +1 -1
- package/dist/{middleware-C2AJFk8W.js → middleware-DIW5S_5L.js} +184 -59
- package/dist/{mod-DNEgTI8M.d.ts → mod-BRnwh2oy.d.ts} +2 -7
- package/dist/{mod-BLDHhuW0.d.ts → mod-C7w-QVUv.d.ts} +1 -1
- package/dist/{mod-CWJNbyju.d.ts → mod-CFRocFeQ.d.ts} +1 -1
- package/dist/{mod-BmlfRBmU.d.cts → mod-CGUH8Ujp.d.cts} +1 -1
- package/dist/{mod-Cn9GJ5ex.d.cts → mod-Co7LEK9X.d.cts} +2 -7
- package/dist/{mod-DiuFTYUt.d.cts → mod-LOEOKbYd.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/client.test.mjs +337 -19
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/otel/exporter.test.mjs +292 -30
- package/dist/otel/mod.cjs +36 -30
- package/dist/otel/mod.d.cts +13 -0
- package/dist/otel/mod.d.ts +13 -0
- package/dist/otel/mod.js +36 -30
- package/dist/{owner-BRoKojK1.mjs → owner-CFHq61cq.mjs} +2 -2
- package/dist/{proof-CHXVlsH_.cjs → proof-0JkSC_pT.cjs} +1 -1
- package/dist/{proof-7R4s10TJ.js → proof-DzksJFK7.js} +1 -1
- package/dist/{proof-DMIAezxz.mjs → proof-KSHlbPCG.mjs} +2 -2
- package/dist/{send-BHIt_8ia.mjs → send-DaDe2YCb.mjs} +117 -32
- package/dist/sig/http.test.mjs +506 -2
- package/dist/sig/key.test.mjs +2 -2
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +1 -1
- package/dist/{temporal-DR0JahMu.mjs → temporal-DNGH8o2E.mjs} +1 -1
- package/dist/testing/mod.d.mts +26 -1
- package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
- package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
- package/dist/utils/docloader.test.mjs +316 -32
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +6 -6
- package/dist/docloader-OLWc7P2D.mjs +0 -82
|
@@ -5,15 +5,30 @@ import { t as esm_default } from "../esm-DVILvP5e.mjs";
|
|
|
5
5
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
6
6
|
import "../std__assert-DflMZha5.mjs";
|
|
7
7
|
import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { u as verifyRequest } from "../http-A9mP2uM7.mjs";
|
|
9
9
|
import { i as rsaPrivateKey2 } from "../keys-CtZLJq76.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { n as getAuthenticatedDocumentLoader, t as createAuthenticatedDocumentLoader } from "../docloader-DOrwedT7.mjs";
|
|
11
11
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
12
12
|
import { FetchError, UrlError } from "@fedify/vocab-runtime";
|
|
13
|
+
import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
|
|
13
14
|
import { configure, reset } from "@logtape/logtape";
|
|
14
|
-
import dns from "node:dns/promises";
|
|
15
15
|
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
16
16
|
//#region src/utils/docloader.test.ts
|
|
17
|
+
const identity = {
|
|
18
|
+
keyId: new URL("https://example.com/key2"),
|
|
19
|
+
privateKey: rsaPrivateKey2
|
|
20
|
+
};
|
|
21
|
+
const directDeps = { openHop: openDirectFetch };
|
|
22
|
+
function publicPolicyHop(lookup) {
|
|
23
|
+
return async (url) => {
|
|
24
|
+
await (await openPublicFetch(url, { lookup })).close();
|
|
25
|
+
return openDirectFetch(url);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const publicLookup = () => Promise.resolve([{
|
|
29
|
+
address: "8.8.8.8",
|
|
30
|
+
family: 4
|
|
31
|
+
}]);
|
|
17
32
|
test("getAuthenticatedDocumentLoader()", async (t) => {
|
|
18
33
|
esm_default.spyGlobal();
|
|
19
34
|
esm_default.get("begin:https://example.com/object", async (cl) => {
|
|
@@ -25,10 +40,7 @@ test("getAuthenticatedDocumentLoader()", async (t) => {
|
|
|
25
40
|
return new Response(JSON.stringify(v != null), { headers: { "Content-Type": "application/json" } });
|
|
26
41
|
});
|
|
27
42
|
await t.step("test", async () => {
|
|
28
|
-
assertEquals(await (
|
|
29
|
-
keyId: new URL("https://example.com/key2"),
|
|
30
|
-
privateKey: rsaPrivateKey2
|
|
31
|
-
}))("https://example.com/object"), {
|
|
43
|
+
assertEquals(await createAuthenticatedDocumentLoader(identity, {}, directDeps)("https://example.com/object"), {
|
|
32
44
|
contextUrl: null,
|
|
33
45
|
documentUrl: "https://example.com/object",
|
|
34
46
|
document: true
|
|
@@ -58,21 +70,16 @@ test("getAuthenticatedDocumentLoader() validates redirects", async (t) => {
|
|
|
58
70
|
privateRequestCount++;
|
|
59
71
|
return Response.json({ private: true });
|
|
60
72
|
});
|
|
73
|
+
const policyDeps = { openHop: publicPolicyHop(publicLookup) };
|
|
61
74
|
await t.step("deny public-to-private redirects", async () => {
|
|
62
|
-
const loader =
|
|
63
|
-
keyId: new URL("https://example.com/key2"),
|
|
64
|
-
privateKey: rsaPrivateKey2
|
|
65
|
-
});
|
|
75
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, policyDeps);
|
|
66
76
|
await assertRejects(() => loader("https://example.com/redirect-to-private"), UrlError);
|
|
67
77
|
assertEquals(privateRequestCount, 0);
|
|
68
78
|
});
|
|
69
79
|
esm_default.get("https://example.com/redirect-to-public", () => Response.redirect("https://www.example.com/document", 302));
|
|
70
80
|
esm_default.get("https://www.example.com/document", () => Response.json({ public: true }));
|
|
71
81
|
await t.step("allow public-to-public redirects", async () => {
|
|
72
|
-
assertEquals((await
|
|
73
|
-
keyId: new URL("https://example.com/key2"),
|
|
74
|
-
privateKey: rsaPrivateKey2
|
|
75
|
-
})("https://example.com/redirect-to-public")).document, { public: true });
|
|
82
|
+
assertEquals((await createAuthenticatedDocumentLoader(identity, {}, policyDeps)("https://example.com/redirect-to-public")).document, { public: true });
|
|
76
83
|
});
|
|
77
84
|
await t.step("allow private redirects when explicitly enabled", async () => {
|
|
78
85
|
assertEquals((await getAuthenticatedDocumentLoader({
|
|
@@ -102,10 +109,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
|
|
|
102
109
|
});
|
|
103
110
|
}, 1e3);
|
|
104
111
|
}));
|
|
105
|
-
const loader =
|
|
106
|
-
keyId: new URL("https://example.com/key2"),
|
|
107
|
-
privateKey: rsaPrivateKey2
|
|
108
|
-
});
|
|
112
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
|
|
109
113
|
const controller = new AbortController();
|
|
110
114
|
const promise = loader("https://example.com/slow-object", { signal: controller.signal });
|
|
111
115
|
controller.abort();
|
|
@@ -116,10 +120,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
|
|
|
116
120
|
}
|
|
117
121
|
});
|
|
118
122
|
await t.step("immediate cancellation", async () => {
|
|
119
|
-
const loader =
|
|
120
|
-
keyId: new URL("https://example.com/key2"),
|
|
121
|
-
privateKey: rsaPrivateKey2
|
|
122
|
-
});
|
|
123
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
|
|
123
124
|
const controller = new AbortController();
|
|
124
125
|
controller.abort();
|
|
125
126
|
await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
|
|
@@ -152,21 +153,17 @@ test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () =>
|
|
|
152
153
|
esm_default.hardReset();
|
|
153
154
|
}
|
|
154
155
|
});
|
|
155
|
-
test("getAuthenticatedDocumentLoader() logs DNS failures as such",
|
|
156
|
-
const loader = getAuthenticatedDocumentLoader({
|
|
157
|
-
keyId: new URL("https://example.com/key2"),
|
|
158
|
-
privateKey: rsaPrivateKey2
|
|
159
|
-
});
|
|
156
|
+
test("getAuthenticatedDocumentLoader() logs DNS failures as such", async (t) => {
|
|
160
157
|
for (const result of [
|
|
161
158
|
"throws",
|
|
162
159
|
"empty",
|
|
163
160
|
"private"
|
|
164
161
|
]) await t.step(result, async () => {
|
|
165
|
-
const
|
|
166
|
-
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
162
|
+
const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
167
163
|
address: "127.0.0.1",
|
|
168
164
|
family: 4
|
|
169
|
-
}])
|
|
165
|
+
}]);
|
|
166
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) });
|
|
170
167
|
const records = [];
|
|
171
168
|
await configure({
|
|
172
169
|
sinks: { buffer: (record) => records.push(record) },
|
|
@@ -200,7 +197,6 @@ test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "De
|
|
|
200
197
|
assertEquals(records[0].properties.error, error);
|
|
201
198
|
} finally {
|
|
202
199
|
await reset();
|
|
203
|
-
dns.lookup = originalLookup;
|
|
204
200
|
}
|
|
205
201
|
});
|
|
206
202
|
});
|
|
@@ -445,6 +441,211 @@ test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", as
|
|
|
445
441
|
esm_default.hardReset();
|
|
446
442
|
}
|
|
447
443
|
});
|
|
444
|
+
test("getAuthenticatedDocumentLoader() closes each hop before opening the next", async (t) => {
|
|
445
|
+
await t.step("redirect", async () => {
|
|
446
|
+
const start = "https://example.com/lifecycle/start";
|
|
447
|
+
const next = "https://example.com/lifecycle/document";
|
|
448
|
+
const hops = recordHops((request) => request.url === start ? {
|
|
449
|
+
status: 302,
|
|
450
|
+
headers: { Location: next },
|
|
451
|
+
body: "Found"
|
|
452
|
+
} : {
|
|
453
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
454
|
+
body: "{\"done\":true}"
|
|
455
|
+
});
|
|
456
|
+
deepStrictEqual(await createAuthenticatedDocumentLoader(identity, {}, hops)(start), {
|
|
457
|
+
contextUrl: null,
|
|
458
|
+
document: { done: true },
|
|
459
|
+
documentUrl: next
|
|
460
|
+
});
|
|
461
|
+
deepStrictEqual(hops.events, [
|
|
462
|
+
`open ${start}`,
|
|
463
|
+
`fetch #1 ${start}`,
|
|
464
|
+
"cancel #1",
|
|
465
|
+
`close ${start}`,
|
|
466
|
+
`open ${next}`,
|
|
467
|
+
`fetch #2 ${next}`,
|
|
468
|
+
"consumed #2",
|
|
469
|
+
`close ${next}`
|
|
470
|
+
]);
|
|
471
|
+
hops.assertInvariants();
|
|
472
|
+
});
|
|
473
|
+
await t.step("Link header alternate", async () => {
|
|
474
|
+
const start = "https://example.com/lifecycle/page";
|
|
475
|
+
const next = "https://example.com/lifecycle/alternate";
|
|
476
|
+
const hops = recordHops((request) => request.url === start ? {
|
|
477
|
+
headers: {
|
|
478
|
+
"Content-Type": "text/plain",
|
|
479
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
480
|
+
},
|
|
481
|
+
body: "not JSON"
|
|
482
|
+
} : {
|
|
483
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
484
|
+
body: "{\"done\":true}"
|
|
485
|
+
});
|
|
486
|
+
deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
|
|
487
|
+
deepStrictEqual(hops.events, [
|
|
488
|
+
`open ${start}`,
|
|
489
|
+
`fetch #1 ${start}`,
|
|
490
|
+
"cancel #1",
|
|
491
|
+
`close ${start}`,
|
|
492
|
+
`open ${next}`,
|
|
493
|
+
`fetch #2 ${next}`,
|
|
494
|
+
"consumed #2",
|
|
495
|
+
`close ${next}`
|
|
496
|
+
]);
|
|
497
|
+
hops.assertInvariants();
|
|
498
|
+
});
|
|
499
|
+
await t.step("one lookup per hop", async () => {
|
|
500
|
+
const start = "https://first.example/start";
|
|
501
|
+
const next = "https://second.example/document";
|
|
502
|
+
const lookups = [];
|
|
503
|
+
const lookup = (hostname) => {
|
|
504
|
+
lookups.push(hostname);
|
|
505
|
+
return Promise.resolve([{
|
|
506
|
+
address: "8.8.8.8",
|
|
507
|
+
family: 4
|
|
508
|
+
}]);
|
|
509
|
+
};
|
|
510
|
+
await withFetchStub((request) => new URL(request.url).pathname === "/start" ? Response.redirect(next, 302) : Response.json({ done: true }), async (fetched) => {
|
|
511
|
+
deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) })(start)).document, { done: true });
|
|
512
|
+
deepStrictEqual(lookups, ["first.example", "second.example"]);
|
|
513
|
+
deepStrictEqual(fetched.length, 2);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
async function captureLogs(fn) {
|
|
518
|
+
const records = [];
|
|
519
|
+
await configure({
|
|
520
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
521
|
+
loggers: [{
|
|
522
|
+
category: "fedify",
|
|
523
|
+
sinks: ["buffer"],
|
|
524
|
+
lowestLevel: "debug"
|
|
525
|
+
}, {
|
|
526
|
+
category: ["logtape", "meta"],
|
|
527
|
+
sinks: []
|
|
528
|
+
}],
|
|
529
|
+
reset: true
|
|
530
|
+
});
|
|
531
|
+
try {
|
|
532
|
+
await fn();
|
|
533
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
534
|
+
} finally {
|
|
535
|
+
await reset();
|
|
536
|
+
}
|
|
537
|
+
return records;
|
|
538
|
+
}
|
|
539
|
+
function getCancelFailures(records) {
|
|
540
|
+
return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.error.message);
|
|
541
|
+
}
|
|
542
|
+
const linkAlternate = (next) => ({
|
|
543
|
+
headers: {
|
|
544
|
+
"Content-Type": "text/plain",
|
|
545
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
546
|
+
},
|
|
547
|
+
body: "not JSON"
|
|
548
|
+
});
|
|
549
|
+
const jsonDocument = {
|
|
550
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
551
|
+
body: "{\"done\":true}"
|
|
552
|
+
};
|
|
553
|
+
test("getAuthenticatedDocumentLoader() logs bodies failing to cancel and loads the next hop", async (t) => {
|
|
554
|
+
for (const kind of ["redirect", "Link header alternate"]) await t.step(kind, async () => {
|
|
555
|
+
const start = "https://example.com/lifecycle/cancel-failure";
|
|
556
|
+
const next = "https://example.com/lifecycle/cancel-failure-next";
|
|
557
|
+
const hops = recordHops((request) => request.url === start ? {
|
|
558
|
+
...kind === "redirect" ? {
|
|
559
|
+
status: 302,
|
|
560
|
+
headers: { Location: next },
|
|
561
|
+
body: "Found"
|
|
562
|
+
} : linkAlternate(next),
|
|
563
|
+
failCancel: true
|
|
564
|
+
} : jsonDocument);
|
|
565
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, hops);
|
|
566
|
+
let document;
|
|
567
|
+
const records = await captureLogs(async () => {
|
|
568
|
+
document = (await loader(start)).document;
|
|
569
|
+
});
|
|
570
|
+
deepStrictEqual(document, { done: true });
|
|
571
|
+
deepStrictEqual(hops.events, [
|
|
572
|
+
`open ${start}`,
|
|
573
|
+
`fetch #1 ${start}`,
|
|
574
|
+
"cancel #1",
|
|
575
|
+
`close ${start}`,
|
|
576
|
+
`open ${next}`,
|
|
577
|
+
`fetch #2 ${next}`,
|
|
578
|
+
"consumed #2",
|
|
579
|
+
`close ${next}`
|
|
580
|
+
]);
|
|
581
|
+
deepStrictEqual(getCancelFailures(records), ["cancel #1 failed"]);
|
|
582
|
+
hops.assertInvariants();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
test("getAuthenticatedDocumentLoader() is not blocked by a body cancellation that never settles", async (t) => {
|
|
586
|
+
await t.step("redirect and Link header alternate", async () => {
|
|
587
|
+
const start = "https://example.com/lifecycle/hanging-start";
|
|
588
|
+
const page = "https://example.com/lifecycle/hanging-page";
|
|
589
|
+
const next = "https://example.com/lifecycle/hanging-document";
|
|
590
|
+
const hops = recordHops((request) => ({
|
|
591
|
+
...request.url === start ? {
|
|
592
|
+
status: 302,
|
|
593
|
+
headers: { Location: page },
|
|
594
|
+
body: "Found"
|
|
595
|
+
} : request.url === page ? linkAlternate(next) : jsonDocument,
|
|
596
|
+
hangCancel: true
|
|
597
|
+
}));
|
|
598
|
+
deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
|
|
599
|
+
deepStrictEqual(hops.events, [
|
|
600
|
+
`open ${start}`,
|
|
601
|
+
`fetch #1 ${start}`,
|
|
602
|
+
"cancel #1",
|
|
603
|
+
`close ${start}`,
|
|
604
|
+
`open ${page}`,
|
|
605
|
+
`fetch #2 ${page}`,
|
|
606
|
+
"cancel #2",
|
|
607
|
+
`close ${page}`,
|
|
608
|
+
`open ${next}`,
|
|
609
|
+
`fetch #3 ${next}`,
|
|
610
|
+
"consumed #3",
|
|
611
|
+
`close ${next}`
|
|
612
|
+
]);
|
|
613
|
+
hops.assertInvariants();
|
|
614
|
+
});
|
|
615
|
+
await t.step("not found", async () => {
|
|
616
|
+
const url = "https://example.com/lifecycle/hanging-missing";
|
|
617
|
+
const hops = recordHops(() => ({
|
|
618
|
+
status: 404,
|
|
619
|
+
body: "Not Found",
|
|
620
|
+
hangCancel: true
|
|
621
|
+
}));
|
|
622
|
+
await rejects(createAuthenticatedDocumentLoader(identity, {}, hops)(url), FetchError);
|
|
623
|
+
deepStrictEqual(hops.events.slice(0, 2), [`open ${url}`, `fetch #1 ${url}`]);
|
|
624
|
+
deepStrictEqual(hops.events.at(-1), `close ${url}`);
|
|
625
|
+
hops.assertInvariants();
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
test("getAuthenticatedDocumentLoader() completes with responses cloned by fetch-mock", async () => {
|
|
629
|
+
esm_default.mockGlobal();
|
|
630
|
+
try {
|
|
631
|
+
const base = "https://example.com/tee/";
|
|
632
|
+
esm_default.get(`${base}start`, () => new Response("Found", {
|
|
633
|
+
status: 302,
|
|
634
|
+
headers: { Location: `${base}page` }
|
|
635
|
+
}));
|
|
636
|
+
esm_default.get(`${base}page`, () => new Response("not JSON", { headers: {
|
|
637
|
+
"Content-Type": "text/plain",
|
|
638
|
+
Link: `<${base}document>; rel="alternate"; type="application/activity+json"`
|
|
639
|
+
} }));
|
|
640
|
+
esm_default.get(`${base}document`, () => new Response("{\"done\":true}", { headers: { "Content-Type": "application/activity+json" } }));
|
|
641
|
+
esm_default.get(`${base}missing`, () => new Response("Not Found", { status: 404 }));
|
|
642
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
|
|
643
|
+
deepStrictEqual((await loader(`${base}start`)).document, { done: true });
|
|
644
|
+
await rejects(loader(`${base}missing`), FetchError);
|
|
645
|
+
} finally {
|
|
646
|
+
esm_default.hardReset();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
448
649
|
const unsupportedUrls = [
|
|
449
650
|
"ftp://example.com/collection",
|
|
450
651
|
"file:///collection",
|
|
@@ -579,5 +780,88 @@ for (const url of unsupportedUrls) {
|
|
|
579
780
|
});
|
|
580
781
|
});
|
|
581
782
|
}
|
|
783
|
+
function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
|
|
784
|
+
const bytes = new TextEncoder().encode(init.body ?? "");
|
|
785
|
+
const chunkSize = init.chunkSize ?? 256;
|
|
786
|
+
let offset = 0;
|
|
787
|
+
let settled = false;
|
|
788
|
+
const body = new ReadableStream({
|
|
789
|
+
pull(controller) {
|
|
790
|
+
if (offset >= bytes.length) {
|
|
791
|
+
settled = true;
|
|
792
|
+
events.push(`consumed #${index}`);
|
|
793
|
+
controller.close();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
controller.enqueue(bytes.slice(offset, offset + chunkSize));
|
|
797
|
+
offset += chunkSize;
|
|
798
|
+
},
|
|
799
|
+
cancel() {
|
|
800
|
+
settled = true;
|
|
801
|
+
events.push(`cancel #${index}${cancelSuffix()}`);
|
|
802
|
+
if (init.failCancel) throw new Error(`cancel #${index} failed`);
|
|
803
|
+
if (init.hangCancel) return new Promise(() => {});
|
|
804
|
+
}
|
|
805
|
+
}, { highWaterMark: 0 });
|
|
806
|
+
return {
|
|
807
|
+
response: new Response(body, {
|
|
808
|
+
status: init.status ?? 200,
|
|
809
|
+
headers: init.headers
|
|
810
|
+
}),
|
|
811
|
+
settled: () => settled
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function recordHops(respond) {
|
|
815
|
+
const events = [];
|
|
816
|
+
const requests = [];
|
|
817
|
+
const unsettledAtClose = [];
|
|
818
|
+
let opens = 0;
|
|
819
|
+
let closes = 0;
|
|
820
|
+
let fetches = 0;
|
|
821
|
+
return {
|
|
822
|
+
events,
|
|
823
|
+
requests,
|
|
824
|
+
openHop(url) {
|
|
825
|
+
opens++;
|
|
826
|
+
events.push(`open ${url}`);
|
|
827
|
+
const tracked = [];
|
|
828
|
+
let closed = false;
|
|
829
|
+
let closing = false;
|
|
830
|
+
return Promise.resolve({
|
|
831
|
+
url: new URL(url),
|
|
832
|
+
async fetch(input, init) {
|
|
833
|
+
if (closed) throw new TypeError("The session is closed.");
|
|
834
|
+
const request = new Request(input instanceof Request ? input.clone() : input, init);
|
|
835
|
+
const index = ++fetches;
|
|
836
|
+
requests.push(request);
|
|
837
|
+
events.push(`fetch #${index} ${request.url}`);
|
|
838
|
+
const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
|
|
839
|
+
tracked.push({
|
|
840
|
+
...recorded,
|
|
841
|
+
index
|
|
842
|
+
});
|
|
843
|
+
return recorded.response;
|
|
844
|
+
},
|
|
845
|
+
close() {
|
|
846
|
+
if (closed) return Promise.resolve();
|
|
847
|
+
closed = true;
|
|
848
|
+
closes++;
|
|
849
|
+
closing = true;
|
|
850
|
+
for (const { response, settled, index } of tracked) {
|
|
851
|
+
if (settled()) continue;
|
|
852
|
+
if (response.body?.locked) unsettledAtClose.push(index);
|
|
853
|
+
else response.body?.cancel().catch(() => {});
|
|
854
|
+
}
|
|
855
|
+
events.push(`close ${url}`);
|
|
856
|
+
return Promise.resolve();
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
},
|
|
860
|
+
assertInvariants() {
|
|
861
|
+
deepStrictEqual(opens, closes);
|
|
862
|
+
deepStrictEqual(unsettledAtClose, []);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
582
866
|
//#endregion
|
|
583
867
|
export {};
|
package/dist/utils/mod.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { Temporal } = require("@js-temporal/polyfill");
|
|
2
2
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
3
3
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
-
const require_kv_cache = require("../kv-cache-
|
|
4
|
+
const require_kv_cache = require("../kv-cache-Cqmm-yQa.cjs");
|
|
5
5
|
exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
|
|
6
6
|
exports.kvCache = require_kv_cache.kvCache;
|
package/dist/utils/mod.d.cts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-
|
|
2
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-Co7LEK9X.cjs";
|
|
3
3
|
export { getAuthenticatedDocumentLoader, kvCache };
|
package/dist/utils/mod.d.ts
CHANGED
package/dist/utils/mod.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
|
-
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-
|
|
3
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-DQcyZL5T.js";
|
|
4
4
|
export { getAuthenticatedDocumentLoader, kvCache };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/fedify",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.31",
|
|
4
4
|
"description": "An ActivityPub server framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ActivityPub",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"dependencies": {
|
|
133
133
|
"@js-temporal/polyfill": "^0.5.1",
|
|
134
134
|
"@logtape/logtape": "^2.0.5",
|
|
135
|
-
"@opentelemetry/api": "
|
|
135
|
+
"@opentelemetry/api": "~1.9.0",
|
|
136
136
|
"@opentelemetry/core": "^2.5.0",
|
|
137
137
|
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
|
138
138
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
@@ -144,9 +144,9 @@
|
|
|
144
144
|
"uri-template-router": "^1.0.0",
|
|
145
145
|
"url-template": "^3.1.1",
|
|
146
146
|
"urlpattern-polyfill": "^10.1.0",
|
|
147
|
-
"@fedify/vocab": "2.1.
|
|
148
|
-
"@fedify/webfinger": "2.1.
|
|
149
|
-
"@fedify/vocab
|
|
147
|
+
"@fedify/vocab-runtime": "2.1.31",
|
|
148
|
+
"@fedify/webfinger": "2.1.31",
|
|
149
|
+
"@fedify/vocab": "2.1.31"
|
|
150
150
|
},
|
|
151
151
|
"devDependencies": {
|
|
152
152
|
"@std/assert": "npm:@jsr/std__assert@^0.226.0",
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"typescript": "^6.0.0",
|
|
160
160
|
"wrangler": "^4.17.0",
|
|
161
161
|
"@fedify/fixture": "2.0.0",
|
|
162
|
-
"@fedify/vocab-tools": "^2.1.
|
|
162
|
+
"@fedify/vocab-tools": "^2.1.31"
|
|
163
163
|
},
|
|
164
164
|
"scripts": {
|
|
165
165
|
"build:self": "tsdown",
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import "@js-temporal/polyfill";
|
|
2
|
-
import "urlpattern-polyfill";
|
|
3
|
-
globalThis.addEventListener = () => {};
|
|
4
|
-
import { s as validateCryptoKey } from "./key-ClQh9Qqh.mjs";
|
|
5
|
-
import { n as doubleKnock } from "./http-Bjd6I92u.mjs";
|
|
6
|
-
import { curry } from "es-toolkit";
|
|
7
|
-
import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
8
|
-
import { getLogger } from "@logtape/logtape";
|
|
9
|
-
//#region src/utils/docloader.ts
|
|
10
|
-
const logger = getLogger([
|
|
11
|
-
"fedify",
|
|
12
|
-
"utils",
|
|
13
|
-
"docloader"
|
|
14
|
-
]);
|
|
15
|
-
const DEFAULT_MAX_REDIRECTION = 20;
|
|
16
|
-
/**
|
|
17
|
-
* Gets an authenticated {@link DocumentLoader} for the given identity.
|
|
18
|
-
* Note that an authenticated document loader intentionally does not cache
|
|
19
|
-
* the fetched documents.
|
|
20
|
-
* At most 20 HTTP redirects and alternate document links are followed in total
|
|
21
|
-
* per call. Revisiting a URL within that chain throws a {@link FetchError}.
|
|
22
|
-
* @param identity The identity to get the document loader for.
|
|
23
|
-
* The actor's key pair.
|
|
24
|
-
* @param options The options for the document loader.
|
|
25
|
-
* @returns The authenticated document loader.
|
|
26
|
-
* @throws {TypeError} If the key is invalid or unsupported.
|
|
27
|
-
* @since 0.4.0
|
|
28
|
-
*/
|
|
29
|
-
function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}) {
|
|
30
|
-
validateCryptoKey(identity.privateKey);
|
|
31
|
-
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
32
|
-
options?.signal?.throwIfAborted();
|
|
33
|
-
let currentUrl = new URL(url).href;
|
|
34
|
-
await validateUrl(currentUrl);
|
|
35
|
-
visited.add(currentUrl);
|
|
36
|
-
const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
|
|
37
|
-
function follow(nextUrl) {
|
|
38
|
-
options?.signal?.throwIfAborted();
|
|
39
|
-
if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
40
|
-
if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
|
|
41
|
-
redirected++;
|
|
42
|
-
}
|
|
43
|
-
const response = await doubleKnock(originalRequest, identity, {
|
|
44
|
-
specDeterminer,
|
|
45
|
-
log: curry(logRequest)(logger),
|
|
46
|
-
tracerProvider,
|
|
47
|
-
signal: options?.signal,
|
|
48
|
-
validateRedirect: async (nextUrl) => {
|
|
49
|
-
follow(nextUrl);
|
|
50
|
-
await validateUrl(nextUrl);
|
|
51
|
-
visited.add(nextUrl);
|
|
52
|
-
currentUrl = nextUrl;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
return getRemoteDocument(currentUrl, response, (alternateUrl) => {
|
|
56
|
-
follow(alternateUrl);
|
|
57
|
-
return load(alternateUrl, options, redirected, visited);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
async function validateUrl(url) {
|
|
61
|
-
const parsedUrl = new URL(url);
|
|
62
|
-
const protocol = parsedUrl.protocol;
|
|
63
|
-
if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
|
|
64
|
-
if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
|
|
65
|
-
if (!allowPrivateAddress) try {
|
|
66
|
-
await validatePublicUrl(url);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
69
|
-
url,
|
|
70
|
-
error
|
|
71
|
-
});
|
|
72
|
-
else logger.error("Disallowed private URL: {url}", {
|
|
73
|
-
url,
|
|
74
|
-
error
|
|
75
|
-
});
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return (url, options) => load(url, options);
|
|
80
|
-
}
|
|
81
|
-
//#endregion
|
|
82
|
-
export { getAuthenticatedDocumentLoader as t };
|