@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.
- package/dist/{builder-C6yXExXl.mjs → builder-B6vSXYxn.mjs} +2 -2
- package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
- package/dist/{client-1He2YuBF.mjs → client-DpUNYKla.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-MOrCC7yl.d.cts → context-CE_Tj_Oc.d.cts} +28 -2
- package/dist/{context-BWbdE_MZ.d.ts → context-H_5tZXg-.d.ts} +28 -2
- package/dist/{deno-y3L4Oa94.mjs → deno-BCuL3mid.mjs} +1 -1
- package/dist/docloader-D2dr1_6B.mjs +125 -0
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +455 -29
- 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 +498 -88
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{http-DNM9KyIO.js → http-BkjxY3U0.js} +172 -103
- package/dist/{http-BCSf1KIY.mjs → http-CSDcThvz.mjs} +196 -98
- package/dist/{http-DQmNpfym.d.ts → http-CZPYFK3b.d.cts} +1 -2
- package/dist/{http-DiWQB8Mr.d.cts → http-DmP0sr2l.d.ts} +1 -2
- package/dist/{http-D77Wjrcg.cjs → http-gOkAiIe1.cjs} +173 -104
- package/dist/{inbox-NObX26c9.mjs → inbox-BHxez4d5.mjs} +2 -2
- package/dist/{key-APURZ0ky.mjs → key-D8IMQ72M.mjs} +2 -2
- package/dist/{kv-cache-Bg4utAuv.cjs → kv-cache-Alm5UmJa.cjs} +59 -17
- package/dist/{kv-cache-C69dzgQi.mjs → kv-cache-Blr9nDDp.mjs} +1 -1
- package/dist/{kv-cache-LBCwHNLu.js → kv-cache-CkXgU-ak.js} +60 -18
- package/dist/{ld-BoLqbDaL.mjs → ld-CVxxE1Pp.mjs} +3 -3
- package/dist/{metrics-DBSl8wEr.mjs → metrics-SYIZHFf_.mjs} +1 -1
- package/dist/{middleware-BQUQ0pPl.cjs → middleware-4yPyyoF5.cjs} +237 -99
- package/dist/{middleware-CZ8tI1qL.mjs → middleware-CfskJGfV.mjs} +81 -41
- package/dist/{middleware-DAqo2eOY.mjs → middleware-D3JWfiCu.mjs} +1 -1
- package/dist/{middleware-CRd1JmuL.js → middleware-DhN7c65B.js} +238 -100
- package/dist/{mod-LmD03uRA.d.ts → mod-CDFFESfL.d.ts} +1 -1
- package/dist/{mod-Bid79LFU.d.cts → mod-C_gFmeNq.d.cts} +1 -1
- package/dist/{mod-Co1M3Ux0.d.cts → mod-DDVkmZ_h.d.cts} +2 -8
- package/dist/{mod-B6PwkUMU.d.cts → mod-DDjtbkZO.d.cts} +1 -1
- package/dist/{mod-Br8yJNcE.d.ts → mod-DMjhnFbg.d.ts} +2 -8
- package/dist/{mod-TLEvgT1i.d.ts → mod-HQXFvOHs.d.ts} +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-DjKvngeb.mjs → owner-BluLVPf0.mjs} +2 -2
- package/dist/{proof-CWTnYhjS.cjs → proof-CR-HwI0l.cjs} +1 -1
- package/dist/{proof-D2oFOdNA.mjs → proof-Co6nQBVR.mjs} +3 -3
- package/dist/{proof-s9sy44kx.js → proof-Dg0d9QBo.js} +1 -1
- package/dist/{send-xDK5UnL-.mjs → send-D_DnqOFA.mjs} +171 -73
- package/dist/sig/http.test.mjs +528 -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-C6icc5n9.mjs → temporal-B8FUNkXB.mjs} +1 -1
- package/dist/testing/mod.d.mts +26 -0
- 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 +318 -34
- package/dist/utils/kv-cache.test.mjs +1 -1
- 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 +8 -8
- package/dist/docloader-yuyzZC9a.mjs +0 -83
|
@@ -5,15 +5,30 @@ import { t as esm_default } from "../esm-vrlUxr60.mjs";
|
|
|
5
5
|
import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
|
|
6
6
|
import "../std__assert-BjJn2daJ.mjs";
|
|
7
7
|
import { t as assertRejects } from "../assert_rejects-D1HYAik3.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { u as verifyRequest } from "../http-CSDcThvz.mjs";
|
|
9
9
|
import { i as rsaPrivateKey2 } from "../keys-BAK-tUlf.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { n as getAuthenticatedDocumentLoader, t as createAuthenticatedDocumentLoader } from "../docloader-D2dr1_6B.mjs";
|
|
11
11
|
import { configure, reset } from "@logtape/logtape";
|
|
12
12
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
13
13
|
import { FetchError, UrlError } from "@fedify/vocab-runtime";
|
|
14
|
-
import
|
|
14
|
+
import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
|
|
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
|
|
@@ -57,10 +69,10 @@ test("getAuthenticatedDocumentLoader()", async (t) => {
|
|
|
57
69
|
const index = Number(cl.url.split("/").at(-1));
|
|
58
70
|
return Response.redirect(`https://example.com/custom-too-many-redirects/${index + 1}`, 302);
|
|
59
71
|
});
|
|
60
|
-
const loader =
|
|
72
|
+
const loader = createAuthenticatedDocumentLoader({
|
|
61
73
|
keyId: new URL("https://example.com/key2"),
|
|
62
74
|
privateKey: rsaPrivateKey2
|
|
63
|
-
}, { maxRedirection: 1 });
|
|
75
|
+
}, { maxRedirection: 1 }, directDeps);
|
|
64
76
|
await assertRejects(() => loader("https://example.com/custom-too-many-redirects/0"), Error, "Too many redirections");
|
|
65
77
|
assertEquals(requestCount, 2);
|
|
66
78
|
esm_default.hardReset();
|
|
@@ -74,21 +86,16 @@ test("getAuthenticatedDocumentLoader() validates redirects", async (t) => {
|
|
|
74
86
|
privateRequestCount++;
|
|
75
87
|
return Response.json({ private: true });
|
|
76
88
|
});
|
|
89
|
+
const policyDeps = { openHop: publicPolicyHop(publicLookup) };
|
|
77
90
|
await t.step("deny public-to-private redirects", async () => {
|
|
78
|
-
const loader =
|
|
79
|
-
keyId: new URL("https://example.com/key2"),
|
|
80
|
-
privateKey: rsaPrivateKey2
|
|
81
|
-
});
|
|
91
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, policyDeps);
|
|
82
92
|
await assertRejects(() => loader("https://example.com/redirect-to-private"), UrlError);
|
|
83
93
|
assertEquals(privateRequestCount, 0);
|
|
84
94
|
});
|
|
85
95
|
esm_default.get("https://example.com/redirect-to-public", () => Response.redirect("https://www.example.com/document", 302));
|
|
86
96
|
esm_default.get("https://www.example.com/document", () => Response.json({ public: true }));
|
|
87
97
|
await t.step("allow public-to-public redirects", async () => {
|
|
88
|
-
assertEquals((await
|
|
89
|
-
keyId: new URL("https://example.com/key2"),
|
|
90
|
-
privateKey: rsaPrivateKey2
|
|
91
|
-
})("https://example.com/redirect-to-public")).document, { public: true });
|
|
98
|
+
assertEquals((await createAuthenticatedDocumentLoader(identity, {}, policyDeps)("https://example.com/redirect-to-public")).document, { public: true });
|
|
92
99
|
});
|
|
93
100
|
await t.step("allow private redirects when explicitly enabled", async () => {
|
|
94
101
|
assertEquals((await getAuthenticatedDocumentLoader({
|
|
@@ -118,10 +125,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
|
|
|
118
125
|
});
|
|
119
126
|
}, 1e3);
|
|
120
127
|
}));
|
|
121
|
-
const loader =
|
|
122
|
-
keyId: new URL("https://example.com/key2"),
|
|
123
|
-
privateKey: rsaPrivateKey2
|
|
124
|
-
});
|
|
128
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
|
|
125
129
|
const controller = new AbortController();
|
|
126
130
|
const promise = loader("https://example.com/slow-object", { signal: controller.signal });
|
|
127
131
|
controller.abort();
|
|
@@ -132,10 +136,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
|
|
|
132
136
|
}
|
|
133
137
|
});
|
|
134
138
|
await t.step("immediate cancellation", async () => {
|
|
135
|
-
const loader =
|
|
136
|
-
keyId: new URL("https://example.com/key2"),
|
|
137
|
-
privateKey: rsaPrivateKey2
|
|
138
|
-
});
|
|
139
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
|
|
139
140
|
const controller = new AbortController();
|
|
140
141
|
controller.abort();
|
|
141
142
|
await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
|
|
@@ -168,21 +169,17 @@ test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () =>
|
|
|
168
169
|
esm_default.hardReset();
|
|
169
170
|
}
|
|
170
171
|
});
|
|
171
|
-
test("getAuthenticatedDocumentLoader() logs DNS failures as such",
|
|
172
|
-
const loader = getAuthenticatedDocumentLoader({
|
|
173
|
-
keyId: new URL("https://example.com/key2"),
|
|
174
|
-
privateKey: rsaPrivateKey2
|
|
175
|
-
});
|
|
172
|
+
test("getAuthenticatedDocumentLoader() logs DNS failures as such", async (t) => {
|
|
176
173
|
for (const result of [
|
|
177
174
|
"throws",
|
|
178
175
|
"empty",
|
|
179
176
|
"private"
|
|
180
177
|
]) await t.step(result, async () => {
|
|
181
|
-
const
|
|
182
|
-
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
178
|
+
const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
183
179
|
address: "127.0.0.1",
|
|
184
180
|
family: 4
|
|
185
|
-
}])
|
|
181
|
+
}]);
|
|
182
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) });
|
|
186
183
|
const records = [];
|
|
187
184
|
await configure({
|
|
188
185
|
sinks: { buffer: (record) => records.push(record) },
|
|
@@ -216,7 +213,6 @@ test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "De
|
|
|
216
213
|
assertEquals(records[0].properties.error, error);
|
|
217
214
|
} finally {
|
|
218
215
|
await reset();
|
|
219
|
-
dns.lookup = originalLookup;
|
|
220
216
|
}
|
|
221
217
|
});
|
|
222
218
|
});
|
|
@@ -461,6 +457,211 @@ test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", as
|
|
|
461
457
|
esm_default.hardReset();
|
|
462
458
|
}
|
|
463
459
|
});
|
|
460
|
+
test("getAuthenticatedDocumentLoader() closes each hop before opening the next", async (t) => {
|
|
461
|
+
await t.step("redirect", async () => {
|
|
462
|
+
const start = "https://example.com/lifecycle/start";
|
|
463
|
+
const next = "https://example.com/lifecycle/document";
|
|
464
|
+
const hops = recordHops((request) => request.url === start ? {
|
|
465
|
+
status: 302,
|
|
466
|
+
headers: { Location: next },
|
|
467
|
+
body: "Found"
|
|
468
|
+
} : {
|
|
469
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
470
|
+
body: "{\"done\":true}"
|
|
471
|
+
});
|
|
472
|
+
deepStrictEqual(await createAuthenticatedDocumentLoader(identity, {}, hops)(start), {
|
|
473
|
+
contextUrl: null,
|
|
474
|
+
document: { done: true },
|
|
475
|
+
documentUrl: next
|
|
476
|
+
});
|
|
477
|
+
deepStrictEqual(hops.events, [
|
|
478
|
+
`open ${start}`,
|
|
479
|
+
`fetch #1 ${start}`,
|
|
480
|
+
"cancel #1",
|
|
481
|
+
`close ${start}`,
|
|
482
|
+
`open ${next}`,
|
|
483
|
+
`fetch #2 ${next}`,
|
|
484
|
+
"consumed #2",
|
|
485
|
+
`close ${next}`
|
|
486
|
+
]);
|
|
487
|
+
hops.assertInvariants();
|
|
488
|
+
});
|
|
489
|
+
await t.step("Link header alternate", async () => {
|
|
490
|
+
const start = "https://example.com/lifecycle/page";
|
|
491
|
+
const next = "https://example.com/lifecycle/alternate";
|
|
492
|
+
const hops = recordHops((request) => request.url === start ? {
|
|
493
|
+
headers: {
|
|
494
|
+
"Content-Type": "text/plain",
|
|
495
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
496
|
+
},
|
|
497
|
+
body: "not JSON"
|
|
498
|
+
} : {
|
|
499
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
500
|
+
body: "{\"done\":true}"
|
|
501
|
+
});
|
|
502
|
+
deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
|
|
503
|
+
deepStrictEqual(hops.events, [
|
|
504
|
+
`open ${start}`,
|
|
505
|
+
`fetch #1 ${start}`,
|
|
506
|
+
"cancel #1",
|
|
507
|
+
`close ${start}`,
|
|
508
|
+
`open ${next}`,
|
|
509
|
+
`fetch #2 ${next}`,
|
|
510
|
+
"consumed #2",
|
|
511
|
+
`close ${next}`
|
|
512
|
+
]);
|
|
513
|
+
hops.assertInvariants();
|
|
514
|
+
});
|
|
515
|
+
await t.step("one lookup per hop", async () => {
|
|
516
|
+
const start = "https://first.example/start";
|
|
517
|
+
const next = "https://second.example/document";
|
|
518
|
+
const lookups = [];
|
|
519
|
+
const lookup = (hostname) => {
|
|
520
|
+
lookups.push(hostname);
|
|
521
|
+
return Promise.resolve([{
|
|
522
|
+
address: "8.8.8.8",
|
|
523
|
+
family: 4
|
|
524
|
+
}]);
|
|
525
|
+
};
|
|
526
|
+
await withFetchStub((request) => new URL(request.url).pathname === "/start" ? Response.redirect(next, 302) : Response.json({ done: true }), async (fetched) => {
|
|
527
|
+
deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) })(start)).document, { done: true });
|
|
528
|
+
deepStrictEqual(lookups, ["first.example", "second.example"]);
|
|
529
|
+
deepStrictEqual(fetched.length, 2);
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
async function captureLogs(fn) {
|
|
534
|
+
const records = [];
|
|
535
|
+
await configure({
|
|
536
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
537
|
+
loggers: [{
|
|
538
|
+
category: "fedify",
|
|
539
|
+
sinks: ["buffer"],
|
|
540
|
+
lowestLevel: "debug"
|
|
541
|
+
}, {
|
|
542
|
+
category: ["logtape", "meta"],
|
|
543
|
+
sinks: []
|
|
544
|
+
}],
|
|
545
|
+
reset: true
|
|
546
|
+
});
|
|
547
|
+
try {
|
|
548
|
+
await fn();
|
|
549
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
550
|
+
} finally {
|
|
551
|
+
await reset();
|
|
552
|
+
}
|
|
553
|
+
return records;
|
|
554
|
+
}
|
|
555
|
+
function getCancelFailures(records) {
|
|
556
|
+
return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.error.message);
|
|
557
|
+
}
|
|
558
|
+
const linkAlternate = (next) => ({
|
|
559
|
+
headers: {
|
|
560
|
+
"Content-Type": "text/plain",
|
|
561
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
562
|
+
},
|
|
563
|
+
body: "not JSON"
|
|
564
|
+
});
|
|
565
|
+
const jsonDocument = {
|
|
566
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
567
|
+
body: "{\"done\":true}"
|
|
568
|
+
};
|
|
569
|
+
test("getAuthenticatedDocumentLoader() logs bodies failing to cancel and loads the next hop", async (t) => {
|
|
570
|
+
for (const kind of ["redirect", "Link header alternate"]) await t.step(kind, async () => {
|
|
571
|
+
const start = "https://example.com/lifecycle/cancel-failure";
|
|
572
|
+
const next = "https://example.com/lifecycle/cancel-failure-next";
|
|
573
|
+
const hops = recordHops((request) => request.url === start ? {
|
|
574
|
+
...kind === "redirect" ? {
|
|
575
|
+
status: 302,
|
|
576
|
+
headers: { Location: next },
|
|
577
|
+
body: "Found"
|
|
578
|
+
} : linkAlternate(next),
|
|
579
|
+
failCancel: true
|
|
580
|
+
} : jsonDocument);
|
|
581
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, hops);
|
|
582
|
+
let document;
|
|
583
|
+
const records = await captureLogs(async () => {
|
|
584
|
+
document = (await loader(start)).document;
|
|
585
|
+
});
|
|
586
|
+
deepStrictEqual(document, { done: true });
|
|
587
|
+
deepStrictEqual(hops.events, [
|
|
588
|
+
`open ${start}`,
|
|
589
|
+
`fetch #1 ${start}`,
|
|
590
|
+
"cancel #1",
|
|
591
|
+
`close ${start}`,
|
|
592
|
+
`open ${next}`,
|
|
593
|
+
`fetch #2 ${next}`,
|
|
594
|
+
"consumed #2",
|
|
595
|
+
`close ${next}`
|
|
596
|
+
]);
|
|
597
|
+
deepStrictEqual(getCancelFailures(records), ["cancel #1 failed"]);
|
|
598
|
+
hops.assertInvariants();
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
test("getAuthenticatedDocumentLoader() is not blocked by a body cancellation that never settles", async (t) => {
|
|
602
|
+
await t.step("redirect and Link header alternate", async () => {
|
|
603
|
+
const start = "https://example.com/lifecycle/hanging-start";
|
|
604
|
+
const page = "https://example.com/lifecycle/hanging-page";
|
|
605
|
+
const next = "https://example.com/lifecycle/hanging-document";
|
|
606
|
+
const hops = recordHops((request) => ({
|
|
607
|
+
...request.url === start ? {
|
|
608
|
+
status: 302,
|
|
609
|
+
headers: { Location: page },
|
|
610
|
+
body: "Found"
|
|
611
|
+
} : request.url === page ? linkAlternate(next) : jsonDocument,
|
|
612
|
+
hangCancel: true
|
|
613
|
+
}));
|
|
614
|
+
deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
|
|
615
|
+
deepStrictEqual(hops.events, [
|
|
616
|
+
`open ${start}`,
|
|
617
|
+
`fetch #1 ${start}`,
|
|
618
|
+
"cancel #1",
|
|
619
|
+
`close ${start}`,
|
|
620
|
+
`open ${page}`,
|
|
621
|
+
`fetch #2 ${page}`,
|
|
622
|
+
"cancel #2",
|
|
623
|
+
`close ${page}`,
|
|
624
|
+
`open ${next}`,
|
|
625
|
+
`fetch #3 ${next}`,
|
|
626
|
+
"consumed #3",
|
|
627
|
+
`close ${next}`
|
|
628
|
+
]);
|
|
629
|
+
hops.assertInvariants();
|
|
630
|
+
});
|
|
631
|
+
await t.step("not found", async () => {
|
|
632
|
+
const url = "https://example.com/lifecycle/hanging-missing";
|
|
633
|
+
const hops = recordHops(() => ({
|
|
634
|
+
status: 404,
|
|
635
|
+
body: "Not Found",
|
|
636
|
+
hangCancel: true
|
|
637
|
+
}));
|
|
638
|
+
await rejects(createAuthenticatedDocumentLoader(identity, {}, hops)(url), FetchError);
|
|
639
|
+
deepStrictEqual(hops.events.slice(0, 2), [`open ${url}`, `fetch #1 ${url}`]);
|
|
640
|
+
deepStrictEqual(hops.events.at(-1), `close ${url}`);
|
|
641
|
+
hops.assertInvariants();
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
test("getAuthenticatedDocumentLoader() completes with responses cloned by fetch-mock", async () => {
|
|
645
|
+
esm_default.mockGlobal();
|
|
646
|
+
try {
|
|
647
|
+
const base = "https://example.com/tee/";
|
|
648
|
+
esm_default.get(`${base}start`, () => new Response("Found", {
|
|
649
|
+
status: 302,
|
|
650
|
+
headers: { Location: `${base}page` }
|
|
651
|
+
}));
|
|
652
|
+
esm_default.get(`${base}page`, () => new Response("not JSON", { headers: {
|
|
653
|
+
"Content-Type": "text/plain",
|
|
654
|
+
Link: `<${base}document>; rel="alternate"; type="application/activity+json"`
|
|
655
|
+
} }));
|
|
656
|
+
esm_default.get(`${base}document`, () => new Response("{\"done\":true}", { headers: { "Content-Type": "application/activity+json" } }));
|
|
657
|
+
esm_default.get(`${base}missing`, () => new Response("Not Found", { status: 404 }));
|
|
658
|
+
const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
|
|
659
|
+
deepStrictEqual((await loader(`${base}start`)).document, { done: true });
|
|
660
|
+
await rejects(loader(`${base}missing`), FetchError);
|
|
661
|
+
} finally {
|
|
662
|
+
esm_default.hardReset();
|
|
663
|
+
}
|
|
664
|
+
});
|
|
464
665
|
const unsupportedUrls = [
|
|
465
666
|
"ftp://example.com/collection",
|
|
466
667
|
"file:///collection",
|
|
@@ -595,5 +796,88 @@ for (const url of unsupportedUrls) {
|
|
|
595
796
|
});
|
|
596
797
|
});
|
|
597
798
|
}
|
|
799
|
+
function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
|
|
800
|
+
const bytes = new TextEncoder().encode(init.body ?? "");
|
|
801
|
+
const chunkSize = init.chunkSize ?? 256;
|
|
802
|
+
let offset = 0;
|
|
803
|
+
let settled = false;
|
|
804
|
+
const body = new ReadableStream({
|
|
805
|
+
pull(controller) {
|
|
806
|
+
if (offset >= bytes.length) {
|
|
807
|
+
settled = true;
|
|
808
|
+
events.push(`consumed #${index}`);
|
|
809
|
+
controller.close();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
controller.enqueue(bytes.slice(offset, offset + chunkSize));
|
|
813
|
+
offset += chunkSize;
|
|
814
|
+
},
|
|
815
|
+
cancel() {
|
|
816
|
+
settled = true;
|
|
817
|
+
events.push(`cancel #${index}${cancelSuffix()}`);
|
|
818
|
+
if (init.failCancel) throw new Error(`cancel #${index} failed`);
|
|
819
|
+
if (init.hangCancel) return new Promise(() => {});
|
|
820
|
+
}
|
|
821
|
+
}, { highWaterMark: 0 });
|
|
822
|
+
return {
|
|
823
|
+
response: new Response(body, {
|
|
824
|
+
status: init.status ?? 200,
|
|
825
|
+
headers: init.headers
|
|
826
|
+
}),
|
|
827
|
+
settled: () => settled
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function recordHops(respond) {
|
|
831
|
+
const events = [];
|
|
832
|
+
const requests = [];
|
|
833
|
+
const unsettledAtClose = [];
|
|
834
|
+
let opens = 0;
|
|
835
|
+
let closes = 0;
|
|
836
|
+
let fetches = 0;
|
|
837
|
+
return {
|
|
838
|
+
events,
|
|
839
|
+
requests,
|
|
840
|
+
openHop(url) {
|
|
841
|
+
opens++;
|
|
842
|
+
events.push(`open ${url}`);
|
|
843
|
+
const tracked = [];
|
|
844
|
+
let closed = false;
|
|
845
|
+
let closing = false;
|
|
846
|
+
return Promise.resolve({
|
|
847
|
+
url: new URL(url),
|
|
848
|
+
async fetch(input, init) {
|
|
849
|
+
if (closed) throw new TypeError("The session is closed.");
|
|
850
|
+
const request = new Request(input instanceof Request ? input.clone() : input, init);
|
|
851
|
+
const index = ++fetches;
|
|
852
|
+
requests.push(request);
|
|
853
|
+
events.push(`fetch #${index} ${request.url}`);
|
|
854
|
+
const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
|
|
855
|
+
tracked.push({
|
|
856
|
+
...recorded,
|
|
857
|
+
index
|
|
858
|
+
});
|
|
859
|
+
return recorded.response;
|
|
860
|
+
},
|
|
861
|
+
close() {
|
|
862
|
+
if (closed) return Promise.resolve();
|
|
863
|
+
closed = true;
|
|
864
|
+
closes++;
|
|
865
|
+
closing = true;
|
|
866
|
+
for (const { response, settled, index } of tracked) {
|
|
867
|
+
if (settled()) continue;
|
|
868
|
+
if (response.body?.locked) unsettledAtClose.push(index);
|
|
869
|
+
else response.body?.cancel().catch(() => {});
|
|
870
|
+
}
|
|
871
|
+
events.push(`close ${url}`);
|
|
872
|
+
return Promise.resolve();
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
},
|
|
876
|
+
assertInvariants() {
|
|
877
|
+
deepStrictEqual(opens, closes);
|
|
878
|
+
deepStrictEqual(unsettledAtClose, []);
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
598
882
|
//#endregion
|
|
599
883
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as kvCache, t as MockKvStore } from "../kv-cache-
|
|
4
|
+
import { n as kvCache, t as MockKvStore } from "../kv-cache-Blr9nDDp.mjs";
|
|
5
5
|
import { deepStrictEqual, throws } from "node:assert";
|
|
6
6
|
import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
7
7
|
import { preloadedContexts } from "@fedify/vocab-runtime";
|
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-Alm5UmJa.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-DDVkmZ_h.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-CkXgU-ak.js";
|
|
4
4
|
export { getAuthenticatedDocumentLoader, kvCache };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/fedify",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.15",
|
|
4
4
|
"description": "An ActivityPub server framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ActivityPub",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"dependencies": {
|
|
142
142
|
"@js-temporal/polyfill": "^0.5.1",
|
|
143
143
|
"@logtape/logtape": "^2.2.0",
|
|
144
|
-
"@opentelemetry/api": "
|
|
144
|
+
"@opentelemetry/api": "~1.9.1",
|
|
145
145
|
"@opentelemetry/core": "^2.7.1",
|
|
146
146
|
"@opentelemetry/sdk-metrics": "2.7.1",
|
|
147
147
|
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
@@ -152,10 +152,10 @@
|
|
|
152
152
|
"jsonld": "^9.0.0",
|
|
153
153
|
"structured-field-values": "^2.0.4",
|
|
154
154
|
"urlpattern-polyfill": "^10.1.0",
|
|
155
|
-
"@fedify/vocab
|
|
156
|
-
"@fedify/
|
|
157
|
-
"@fedify/webfinger": "2.3.
|
|
158
|
-
"@fedify/
|
|
155
|
+
"@fedify/vocab": "2.3.15",
|
|
156
|
+
"@fedify/vocab-runtime": "2.3.15",
|
|
157
|
+
"@fedify/webfinger": "2.3.15",
|
|
158
|
+
"@fedify/uri-template": "2.3.15"
|
|
159
159
|
},
|
|
160
160
|
"devDependencies": {
|
|
161
161
|
"@std/assert": "npm:@jsr/std__assert@^0.226.0",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
"tsx": "^4.21.0",
|
|
168
168
|
"typescript": "^6.0.0",
|
|
169
169
|
"wrangler": "^4.17.0",
|
|
170
|
-
"@fedify/
|
|
171
|
-
"@fedify/
|
|
170
|
+
"@fedify/vocab-tools": "^2.3.15",
|
|
171
|
+
"@fedify/fixture": "2.0.0"
|
|
172
172
|
},
|
|
173
173
|
"scripts": {
|
|
174
174
|
"build:self": "tsdown",
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import "@js-temporal/polyfill";
|
|
2
|
-
import "urlpattern-polyfill";
|
|
3
|
-
globalThis.addEventListener = () => {};
|
|
4
|
-
import { s as validateCryptoKey } from "./key-APURZ0ky.mjs";
|
|
5
|
-
import { n as doubleKnock } from "./http-BCSf1KIY.mjs";
|
|
6
|
-
import { getLogger } from "@logtape/logtape";
|
|
7
|
-
import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
8
|
-
import { curry } from "es-toolkit";
|
|
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, maxRedirection, 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
|
-
maxRedirection,
|
|
45
|
-
specDeterminer,
|
|
46
|
-
log: curry(logRequest)(logger),
|
|
47
|
-
tracerProvider,
|
|
48
|
-
signal: options?.signal,
|
|
49
|
-
validateRedirect: async (nextUrl) => {
|
|
50
|
-
follow(nextUrl);
|
|
51
|
-
await validateUrl(nextUrl);
|
|
52
|
-
visited.add(nextUrl);
|
|
53
|
-
currentUrl = nextUrl;
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
return getRemoteDocument(currentUrl, response, (alternateUrl) => {
|
|
57
|
-
follow(alternateUrl);
|
|
58
|
-
return load(alternateUrl, options, redirected, visited);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
async function validateUrl(url) {
|
|
62
|
-
const parsedUrl = new URL(url);
|
|
63
|
-
const protocol = parsedUrl.protocol;
|
|
64
|
-
if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
|
|
65
|
-
if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
|
|
66
|
-
if (!allowPrivateAddress) try {
|
|
67
|
-
await validatePublicUrl(url);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
70
|
-
url,
|
|
71
|
-
error
|
|
72
|
-
});
|
|
73
|
-
else logger.error("Disallowed private URL: {url}", {
|
|
74
|
-
url,
|
|
75
|
-
error
|
|
76
|
-
});
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return (url, options) => load(url, options);
|
|
81
|
-
}
|
|
82
|
-
//#endregion
|
|
83
|
-
export { getAuthenticatedDocumentLoader as t };
|