@fedify/fedify 2.4.0-dev.2149 → 2.4.0-dev.2157
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-W2fIPRjv.mjs → builder-CLaS4-yA.mjs} +56 -3
- package/dist/{client-DpnpgjyX.mjs → client-CEnNz8O7.mjs} +7 -3
- 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/{compound-proof-B1QH25S6.mjs → compound-proof-D7K94KPz.mjs} +216 -4
- package/dist/{context-DfSVPcJ8.mjs → context-B2rlkkyJ.mjs} +9 -2
- package/dist/{context-CrDDVrbF.d.cts → context-BMggDR-l.d.cts} +394 -5
- package/dist/{context-B2C8nuN3.d.ts → context-exYu7gDU.d.ts} +394 -5
- package/dist/{deno-yXpYZm4K.mjs → deno-D1gcWWqB.mjs} +1 -1
- package/dist/{dist-BQvArojP.mjs → dist-DJQM4u7i.mjs} +2 -1
- package/dist/docloader-BMqr3hji.mjs +79 -0
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +4 -4
- package/dist/federation/handler.test.mjs +6 -6
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +187 -10
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +1 -1
- package/dist/federation/mq.test.mjs +98 -69
- package/dist/federation/outgoing-proof.test.mjs +5 -5
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +768 -0
- package/dist/federation/portable-gateway-keys.test.mjs +4 -4
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable.test.mjs +4 -4
- package/dist/federation/send.test.mjs +84 -3
- package/dist/federation/tasks/enqueue.test.mjs +3 -3
- package/dist/federation/tasks/tasks.test.mjs +4 -4
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{federation-D4j-ZXUN.cjs → federation-2e8XrN7q.cjs} +700 -70
- package/dist/{http-C_jobbLz.cjs → http-BObQoB0j.cjs} +18 -3
- package/dist/{http-BoH-e8sR.mjs → http-CGcLv8rh.mjs} +9 -5
- package/dist/{http-CBRurnlR.js → http-Dgtt45-K.js} +13 -4
- package/dist/{key-BtO7WCfP.mjs → key-CPM4r6tP.mjs} +2 -2
- package/dist/{kv-cache-E0es0w4R.js → kv-cache-BweyIZTb.js} +35 -9
- package/dist/{kv-cache-BxUqVENn.cjs → kv-cache-D2fnB8qu.cjs} +34 -8
- package/dist/{kv-cache-CMuTXrkC.mjs → kv-cache-bdPqTR1k.mjs} +1 -1
- package/dist/{ld-YYBfDbe_.mjs → ld-lU_ZqzsQ.mjs} +4 -4
- package/dist/{metrics-DMqdDud_.mjs → metrics-B41pRPfd.mjs} +1 -1
- package/dist/{middleware-D6uiL1Tw.js → middleware-BYWyYjmX.js} +702 -72
- package/dist/{middleware-C97UJpkA.mjs → middleware-TrCVULgs.mjs} +1 -1
- package/dist/{middleware-CBGR92fz.mjs → middleware-xdukzV3x.mjs} +432 -81
- package/dist/{mod-C06GoL5Z.d.ts → mod-CnGtADvO.d.ts} +1 -1
- package/dist/{mod-CZetqT5h.d.ts → mod-DaiBrWDW.d.ts} +2 -0
- package/dist/{mod-B7SfEqIP.d.cts → mod-Decmur1U.d.cts} +1 -1
- package/dist/{mod-D4g9ffEn.d.cts → mod-Mt4aO3DX.d.cts} +2 -0
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +5 -5
- package/dist/mod.d.ts +5 -5
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/client.test.mjs +49 -1
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/{owner-Cn6CFYBy.mjs → owner-D63yPo_i.mjs} +3 -3
- package/dist/{portable-collection-CWgXwOdR.cjs → portable-collection-B8ojoQ50.cjs} +1 -1
- package/dist/{portable-collection-CyhFWEB-.js → portable-collection-C3xEzYri.js} +1 -1
- package/dist/{portable-collection-B3swTTja.mjs → portable-collection-_lZg-osX.mjs} +2 -2
- package/dist/{portable-key-HM5Siym0.mjs → portable-key-D8uWZF7b.mjs} +1 -1
- package/dist/{proof-DWkXfZIU.mjs → proof-C2YwD8a_.mjs} +10 -5
- package/dist/{send-BQDT_8bc.mjs → send-D9SGhxTB.mjs} +18 -4
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +2 -2
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +42 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +5 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +2 -2
- package/dist/sig/portable-key.test.mjs +6 -6
- package/dist/sig/proof.test.mjs +3 -3
- package/dist/{tasks-dbCShlCb.mjs → tasks-IG3pHfvZ.mjs} +1 -1
- package/dist/{temporal-CFI__wjQ.mjs → temporal-q6FSekcN.mjs} +1 -1
- package/dist/testing/context.test.mjs +1 -1
- package/dist/testing/kv.test.mjs +3 -3
- package/dist/testing/mod.d.mts +393 -4
- package/dist/testing/mod.mjs +4 -4
- package/dist/{testing-otQhu4aD.mjs → testing-C1itIjqW.mjs} +3 -3
- package/dist/{types-C_cNlNVK.js → types-_S2_MeLF.js} +7 -3
- package/dist/{types-Be2zbyeG.cjs → types-poIao40f.cjs} +7 -3
- package/dist/utils/docloader.test.mjs +297 -2
- 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/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +7 -7
- package/dist/docloader-C4h6PL7m.mjs +0 -53
|
@@ -92,8 +92,8 @@ const DEFAULT_MAX_REDIRECTION = 5;
|
|
|
92
92
|
* bounce the request to an internal address.
|
|
93
93
|
* @param url The URL to fetch.
|
|
94
94
|
* @param headers The request headers.
|
|
95
|
-
* @returns The final response, or `null` if the URL is disallowed
|
|
96
|
-
* are too many redirects.
|
|
95
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
96
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
97
97
|
*/
|
|
98
98
|
async function fetchPublicUrl(url, headers) {
|
|
99
99
|
let current = url instanceof URL ? url : new URL(url);
|
|
@@ -103,7 +103,11 @@ async function fetchPublicUrl(url, headers) {
|
|
|
103
103
|
await validatePublicUrl(current.href);
|
|
104
104
|
} catch (e) {
|
|
105
105
|
if (e instanceof UrlError) {
|
|
106
|
-
logger.error("
|
|
106
|
+
if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
|
|
107
|
+
url: current.href,
|
|
108
|
+
error: e
|
|
109
|
+
});
|
|
110
|
+
else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
|
|
107
111
|
url: current.href,
|
|
108
112
|
error: e
|
|
109
113
|
});
|
|
@@ -91,8 +91,8 @@ const DEFAULT_MAX_REDIRECTION = 5;
|
|
|
91
91
|
* bounce the request to an internal address.
|
|
92
92
|
* @param url The URL to fetch.
|
|
93
93
|
* @param headers The request headers.
|
|
94
|
-
* @returns The final response, or `null` if the URL is disallowed
|
|
95
|
-
* are too many redirects.
|
|
94
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
95
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
96
96
|
*/
|
|
97
97
|
async function fetchPublicUrl(url, headers) {
|
|
98
98
|
let current = url instanceof URL ? url : new URL(url);
|
|
@@ -102,7 +102,11 @@ async function fetchPublicUrl(url, headers) {
|
|
|
102
102
|
await (0, _fedify_vocab_runtime.validatePublicUrl)(current.href);
|
|
103
103
|
} catch (e) {
|
|
104
104
|
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
105
|
-
logger.error("
|
|
105
|
+
if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
|
|
106
|
+
url: current.href,
|
|
107
|
+
error: e
|
|
108
|
+
});
|
|
109
|
+
else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
|
|
106
110
|
url: current.href,
|
|
107
111
|
error: e
|
|
108
112
|
});
|
|
@@ -5,12 +5,14 @@ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
|
5
5
|
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
6
6
|
import "../std__assert-DvMDRPes.mjs";
|
|
7
7
|
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
8
|
-
import { d as verifyRequest } from "../http-
|
|
8
|
+
import { d as verifyRequest } from "../http-CGcLv8rh.mjs";
|
|
9
9
|
import { i as rsaPrivateKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
10
|
-
import { t as getAuthenticatedDocumentLoader } from "../docloader-
|
|
10
|
+
import { t as getAuthenticatedDocumentLoader } from "../docloader-BMqr3hji.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 dns from "node:dns/promises";
|
|
15
|
+
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
14
16
|
//#region src/utils/docloader.test.ts
|
|
15
17
|
test("getAuthenticatedDocumentLoader()", async (t) => {
|
|
16
18
|
esm_default.spyGlobal();
|
|
@@ -239,5 +241,298 @@ test("authenticated document loader suppresses private URL logs", async () => {
|
|
|
239
241
|
await reset();
|
|
240
242
|
}
|
|
241
243
|
});
|
|
244
|
+
test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted" }, async (t) => {
|
|
245
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
246
|
+
keyId: new URL("https://example.com/key2"),
|
|
247
|
+
privateKey: rsaPrivateKey2
|
|
248
|
+
});
|
|
249
|
+
for (const result of [
|
|
250
|
+
"throws",
|
|
251
|
+
"empty",
|
|
252
|
+
"private"
|
|
253
|
+
]) await t.step(result, async () => {
|
|
254
|
+
const originalLookup = dns.lookup;
|
|
255
|
+
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
256
|
+
address: "127.0.0.1",
|
|
257
|
+
family: 4
|
|
258
|
+
}]));
|
|
259
|
+
const records = [];
|
|
260
|
+
await configure({
|
|
261
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
262
|
+
loggers: [{
|
|
263
|
+
category: "fedify",
|
|
264
|
+
sinks: ["buffer"],
|
|
265
|
+
lowestLevel: "debug"
|
|
266
|
+
}, {
|
|
267
|
+
category: ["logtape", "meta"],
|
|
268
|
+
sinks: []
|
|
269
|
+
}],
|
|
270
|
+
reset: true
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
const url = "https://dns-failure.invalid/object";
|
|
274
|
+
const error = await assertRejects(() => loader(url), UrlError);
|
|
275
|
+
assertEquals(error.reason, result === "private" ? "disallowed" : "dns");
|
|
276
|
+
assertEquals(records.map((r) => [
|
|
277
|
+
r.level,
|
|
278
|
+
r.rawMessage,
|
|
279
|
+
r.properties.url
|
|
280
|
+
]), [result === "private" ? [
|
|
281
|
+
"error",
|
|
282
|
+
"Disallowed private URL: {url}",
|
|
283
|
+
url
|
|
284
|
+
] : [
|
|
285
|
+
"debug",
|
|
286
|
+
"DNS lookup failed for {url}",
|
|
287
|
+
url
|
|
288
|
+
]]);
|
|
289
|
+
assertEquals(records[0].properties.error, error);
|
|
290
|
+
} finally {
|
|
291
|
+
await reset();
|
|
292
|
+
dns.lookup = originalLookup;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
test("getAuthenticatedDocumentLoader() bounds alternate document chains", async (t) => {
|
|
297
|
+
const base = "https://example.com/alternate-chain/";
|
|
298
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
299
|
+
keyId: new URL("https://example.com/key2"),
|
|
300
|
+
privateKey: rsaPrivateKey2
|
|
301
|
+
}, { allowPrivateAddress: true });
|
|
302
|
+
for (const mode of [
|
|
303
|
+
"header",
|
|
304
|
+
"html",
|
|
305
|
+
"mixed"
|
|
306
|
+
]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
|
|
307
|
+
esm_default.mockGlobal();
|
|
308
|
+
let requests = 0;
|
|
309
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
310
|
+
requests++;
|
|
311
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
312
|
+
if (index === hops || requests > 30) return Response.json({ done: true });
|
|
313
|
+
const next = `${base}${index + 1}`;
|
|
314
|
+
if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
|
|
315
|
+
return alternate(next, mode === "html");
|
|
316
|
+
});
|
|
317
|
+
try {
|
|
318
|
+
if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
|
|
319
|
+
else await rejects(loader(`${base}0`), (error) => {
|
|
320
|
+
ok(error instanceof FetchError);
|
|
321
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
deepStrictEqual(requests, 21);
|
|
325
|
+
} finally {
|
|
326
|
+
esm_default.hardReset();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
for (const mode of [
|
|
330
|
+
"header",
|
|
331
|
+
"html",
|
|
332
|
+
"alternate-redirect",
|
|
333
|
+
"redirect-alternate",
|
|
334
|
+
"redirect-intermediate"
|
|
335
|
+
]) await t.step(`${mode}: cycle`, async () => {
|
|
336
|
+
esm_default.mockGlobal();
|
|
337
|
+
let requests = 0;
|
|
338
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
339
|
+
requests++;
|
|
340
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
341
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
342
|
+
const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
|
|
343
|
+
if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
|
|
344
|
+
return alternate(next, mode === "html");
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
348
|
+
ok(error instanceof FetchError);
|
|
349
|
+
ok(error.message.includes("Redirect loop detected:"));
|
|
350
|
+
return true;
|
|
351
|
+
});
|
|
352
|
+
deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
|
|
353
|
+
} finally {
|
|
354
|
+
esm_default.hardReset();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
await t.step("alternate followed by 20 redirects shares the limit", async () => {
|
|
358
|
+
esm_default.mockGlobal();
|
|
359
|
+
let requests = 0;
|
|
360
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
361
|
+
requests++;
|
|
362
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
363
|
+
if (index === 21) return Response.json({ done: true });
|
|
364
|
+
return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
368
|
+
ok(error instanceof FetchError);
|
|
369
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
372
|
+
deepStrictEqual(requests, 21);
|
|
373
|
+
} finally {
|
|
374
|
+
esm_default.hardReset();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
await t.step("relative alternate after redirect and isolated calls", async () => {
|
|
378
|
+
esm_default.mockGlobal();
|
|
379
|
+
esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
380
|
+
esm_default.get(`${base}html`, alternate("./document", true));
|
|
381
|
+
esm_default.get(`${base}document`, Response.json({ done: true }));
|
|
382
|
+
try {
|
|
383
|
+
for (let i = 0; i < 2; i++) {
|
|
384
|
+
const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
|
|
385
|
+
for (const result of results) {
|
|
386
|
+
deepStrictEqual(result.document, { done: true });
|
|
387
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
esm_default.hardReset();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
function alternate(next, html) {
|
|
395
|
+
return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
|
|
396
|
+
"Content-Type": "text/plain",
|
|
397
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
398
|
+
} });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
test("getAuthenticatedDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
402
|
+
const base = "https://example.com/alternate-abort/";
|
|
403
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
404
|
+
keyId: new URL("https://example.com/key2"),
|
|
405
|
+
privateKey: rsaPrivateKey2
|
|
406
|
+
}, { allowPrivateAddress: true });
|
|
407
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
408
|
+
esm_default.mockGlobal();
|
|
409
|
+
const controller = new AbortController();
|
|
410
|
+
const reason = /* @__PURE__ */ new Error("Alternate document loading cancelled");
|
|
411
|
+
let requests = 0;
|
|
412
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
413
|
+
requests++;
|
|
414
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
415
|
+
if (index === 2) return Response.json({ done: true });
|
|
416
|
+
if (index === 1) controller.abort(reason);
|
|
417
|
+
const next = `${base}${index + 1}`;
|
|
418
|
+
return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
|
|
419
|
+
"Content-Type": "text/plain",
|
|
420
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
421
|
+
} });
|
|
422
|
+
});
|
|
423
|
+
try {
|
|
424
|
+
await rejects(loader(`${base}0`, { signal: controller.signal }), (error) => {
|
|
425
|
+
ok(error === reason);
|
|
426
|
+
return true;
|
|
427
|
+
});
|
|
428
|
+
deepStrictEqual(requests, 2);
|
|
429
|
+
} finally {
|
|
430
|
+
esm_default.hardReset();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
test("getAuthenticatedDocumentLoader() tracks fallback redirects after alternates", async () => {
|
|
435
|
+
esm_default.mockGlobal();
|
|
436
|
+
let requests = 0;
|
|
437
|
+
let knocks = 0;
|
|
438
|
+
const first = "https://example.com/fallback-alternate/a";
|
|
439
|
+
const second = "https://example.com/fallback-alternate/b";
|
|
440
|
+
esm_default.get(first, () => {
|
|
441
|
+
requests++;
|
|
442
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
443
|
+
return new Response("", { headers: {
|
|
444
|
+
"Content-Type": "text/plain",
|
|
445
|
+
Link: `<${second}>; rel="alternate"; type="application/activity+json"`
|
|
446
|
+
} });
|
|
447
|
+
});
|
|
448
|
+
esm_default.get(second, () => {
|
|
449
|
+
requests++;
|
|
450
|
+
return ++knocks % 2 === 1 ? new Response("", { status: 401 }) : Response.redirect(first, 302);
|
|
451
|
+
});
|
|
452
|
+
try {
|
|
453
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
454
|
+
keyId: new URL("https://example.com/key2"),
|
|
455
|
+
privateKey: rsaPrivateKey2
|
|
456
|
+
}, { allowPrivateAddress: true });
|
|
457
|
+
await assertRejects(() => loader(first), FetchError, "Redirect loop detected:");
|
|
458
|
+
assertEquals(requests, 3);
|
|
459
|
+
} finally {
|
|
460
|
+
esm_default.hardReset();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
test("getAuthenticatedDocumentLoader() resolves relative alternates without response URLs", async () => {
|
|
464
|
+
const originalFetch = globalThis.fetch;
|
|
465
|
+
const base = "https://example.com/empty-response-url/";
|
|
466
|
+
const urls = [];
|
|
467
|
+
globalThis.fetch = (input) => {
|
|
468
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
469
|
+
urls.push(url);
|
|
470
|
+
if (url === `${base}start`) return Promise.resolve(Response.redirect(`${base}nested/page`, 302));
|
|
471
|
+
if (url === `${base}nested/page`) return Promise.resolve(new Response("<link rel=\"alternate\" type=\"application/activity+json\" href=\"document\">", { headers: { "Content-Type": "text/html" } }));
|
|
472
|
+
assertEquals(url, `${base}nested/document`);
|
|
473
|
+
return Promise.resolve(Response.json({ done: true }));
|
|
474
|
+
};
|
|
475
|
+
try {
|
|
476
|
+
assertEquals(await getAuthenticatedDocumentLoader({
|
|
477
|
+
keyId: new URL("https://example.com/key2"),
|
|
478
|
+
privateKey: rsaPrivateKey2
|
|
479
|
+
}, { allowPrivateAddress: true })(`${base}start`), {
|
|
480
|
+
contextUrl: null,
|
|
481
|
+
document: { done: true },
|
|
482
|
+
documentUrl: `${base}nested/document`
|
|
483
|
+
});
|
|
484
|
+
assertEquals(urls, [
|
|
485
|
+
`${base}start`,
|
|
486
|
+
`${base}nested/page`,
|
|
487
|
+
`${base}nested/document`
|
|
488
|
+
]);
|
|
489
|
+
} finally {
|
|
490
|
+
globalThis.fetch = originalFetch;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
test("getAuthenticatedDocumentLoader() validates alternate addresses", async () => {
|
|
494
|
+
esm_default.mockGlobal();
|
|
495
|
+
let privateRequests = 0;
|
|
496
|
+
const url = "https://8.8.8.8/alternate";
|
|
497
|
+
esm_default.get(url, new Response("", { headers: {
|
|
498
|
+
"Content-Type": "text/plain",
|
|
499
|
+
Link: "<http://127.0.0.1/private>; rel=\"alternate\"; type=\"application/activity+json\""
|
|
500
|
+
} }));
|
|
501
|
+
esm_default.get("http://127.0.0.1/private", () => {
|
|
502
|
+
privateRequests++;
|
|
503
|
+
return Response.json({ private: true });
|
|
504
|
+
});
|
|
505
|
+
try {
|
|
506
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
507
|
+
keyId: new URL("https://example.com/key2"),
|
|
508
|
+
privateKey: rsaPrivateKey2
|
|
509
|
+
});
|
|
510
|
+
await assertRejects(() => loader(url), UrlError);
|
|
511
|
+
assertEquals(privateRequests, 0);
|
|
512
|
+
} finally {
|
|
513
|
+
esm_default.hardReset();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", async () => {
|
|
517
|
+
esm_default.mockGlobal();
|
|
518
|
+
let requests = 0;
|
|
519
|
+
const url = "https://example.com/pre-aborted-alternate";
|
|
520
|
+
esm_default.get(url, () => {
|
|
521
|
+
requests++;
|
|
522
|
+
return Response.json({ done: true });
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
526
|
+
keyId: new URL("https://example.com/key2"),
|
|
527
|
+
privateKey: rsaPrivateKey2
|
|
528
|
+
}, { allowPrivateAddress: true });
|
|
529
|
+
const controller = new AbortController();
|
|
530
|
+
controller.abort();
|
|
531
|
+
await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
|
|
532
|
+
deepStrictEqual(requests, 0);
|
|
533
|
+
} finally {
|
|
534
|
+
esm_default.hardReset();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
242
537
|
//#endregion
|
|
243
538
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Temporal } from "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-bdPqTR1k.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,5 +1,5 @@
|
|
|
1
1
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const require_kv_cache = require("../kv-cache-
|
|
3
|
+
const require_kv_cache = require("../kv-cache-D2fnB8qu.cjs");
|
|
4
4
|
exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
|
|
5
5
|
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-Mt4aO3DX.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 "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-BweyIZTb.js";
|
|
4
4
|
export { getAuthenticatedDocumentLoader, kvCache };
|
package/dist/vocab/cjs.test.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { strictEqual } from "node:assert";
|
|
6
6
|
import { test } from "@fedify/fixture";
|
|
7
7
|
//#region src/vocab/cjs.test.ts
|
|
8
|
-
test("CommonJS vocab entry exports Object", { ignore: "Deno" in globalThis }, () => {
|
|
8
|
+
test("CommonJS vocab entry exports Object", { ignore: "Deno" in globalThis || "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" }, () => {
|
|
9
9
|
const vocab = createRequire(import.meta.url)("./mod.cjs");
|
|
10
10
|
strictEqual(typeof vocab.Object, "function");
|
|
11
11
|
strictEqual(typeof vocab.Update, "function");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/fedify",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.2157+b76e5888",
|
|
4
4
|
"description": "An ActivityPub server framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ActivityPub",
|
|
@@ -151,10 +151,10 @@
|
|
|
151
151
|
"structured-field-values": "^2.0.4",
|
|
152
152
|
"temporal-polyfill": "^1.0.1",
|
|
153
153
|
"urlpattern-polyfill": "^10.1.0",
|
|
154
|
-
"@fedify/
|
|
155
|
-
"@fedify/
|
|
156
|
-
"@fedify/webfinger": "2.4.0-dev.
|
|
157
|
-
"@fedify/vocab": "2.4.0-dev.
|
|
154
|
+
"@fedify/vocab-runtime": "2.4.0-dev.2157+b76e5888",
|
|
155
|
+
"@fedify/uri-template": "2.4.0-dev.2157+b76e5888",
|
|
156
|
+
"@fedify/webfinger": "2.4.0-dev.2157+b76e5888",
|
|
157
|
+
"@fedify/vocab": "2.4.0-dev.2157+b76e5888"
|
|
158
158
|
},
|
|
159
159
|
"devDependencies": {
|
|
160
160
|
"@std/assert": "npm:@jsr/std__assert@^0.226.0",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"typescript": "^6.0.0",
|
|
168
168
|
"wrangler": "^4.17.0",
|
|
169
169
|
"@fedify/fixture": "2.0.0",
|
|
170
|
-
"@fedify/vocab-tools": "^2.4.0-dev.
|
|
170
|
+
"@fedify/vocab-tools": "^2.4.0-dev.2157+b76e5888"
|
|
171
171
|
},
|
|
172
172
|
"scripts": {
|
|
173
173
|
"build:self": "tsdown",
|
|
@@ -177,6 +177,6 @@
|
|
|
177
177
|
"test": "cd dist/ && node --test",
|
|
178
178
|
"pretest:bun": "pnpm build",
|
|
179
179
|
"test:bun": "cd dist/ && bun test --timeout 60000",
|
|
180
|
-
"test:cfworkers": "deno task codegen && wrangler deploy --dry-run --outdir src/cfworkers && node --import=tsx src/cfworkers/client.ts"
|
|
180
|
+
"test:cfworkers": "deno task codegen && wrangler deploy --dry-run --outdir src/cfworkers && node --import=tsx scripts/check_cfworkers.ts && node --import=tsx src/cfworkers/client.ts"
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import "temporal-polyfill";
|
|
2
|
-
import "urlpattern-polyfill";
|
|
3
|
-
globalThis.addEventListener = () => {};
|
|
4
|
-
import { c as validateCryptoKey } from "./key-BtO7WCfP.mjs";
|
|
5
|
-
import { n as doubleKnock } from "./http-BoH-e8sR.mjs";
|
|
6
|
-
import { getLogger } from "@logtape/logtape";
|
|
7
|
-
import { curry } from "es-toolkit";
|
|
8
|
-
import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
9
|
-
//#region src/utils/docloader.ts
|
|
10
|
-
const logger = getLogger([
|
|
11
|
-
"fedify",
|
|
12
|
-
"utils",
|
|
13
|
-
"docloader"
|
|
14
|
-
]);
|
|
15
|
-
/**
|
|
16
|
-
* Gets an authenticated {@link DocumentLoader} for the given identity.
|
|
17
|
-
* Note that an authenticated document loader intentionally does not cache
|
|
18
|
-
* the fetched documents.
|
|
19
|
-
* @param identity The identity to get the document loader for.
|
|
20
|
-
* The actor's key pair.
|
|
21
|
-
* @param options The options for the document loader.
|
|
22
|
-
* @returns The authenticated document loader.
|
|
23
|
-
* @throws {TypeError} If the key is invalid or unsupported.
|
|
24
|
-
* @since 0.4.0
|
|
25
|
-
*/
|
|
26
|
-
function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
|
|
27
|
-
validateCryptoKey(identity.privateKey);
|
|
28
|
-
async function load(url, options) {
|
|
29
|
-
await validateUrl(url, options);
|
|
30
|
-
return getRemoteDocument(url, await doubleKnock(createActivityPubRequest(url, { userAgent }), identity, {
|
|
31
|
-
maxRedirection,
|
|
32
|
-
specDeterminer,
|
|
33
|
-
log: curry(logRequest)(logger),
|
|
34
|
-
tracerProvider,
|
|
35
|
-
signal: options?.signal,
|
|
36
|
-
validateRedirect: (url) => validateUrl(url, options)
|
|
37
|
-
}), load, options);
|
|
38
|
-
}
|
|
39
|
-
async function validateUrl(url, options) {
|
|
40
|
-
if (!allowPrivateAddress) try {
|
|
41
|
-
await validatePublicUrl(url);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
if (error instanceof UrlError) logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
|
|
44
|
-
url,
|
|
45
|
-
error
|
|
46
|
-
});
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return load;
|
|
51
|
-
}
|
|
52
|
-
//#endregion
|
|
53
|
-
export { getAuthenticatedDocumentLoader as t };
|