@fedify/relay 2.4.0-dev.1962 → 2.4.0-dev.1988
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/litepub.test.js +61 -43
- package/dist/mastodon.test.js +27 -23
- package/dist/{types-BAtaDvQt.js → types-DuILuqql.js} +201 -151
- package/package.json +4 -4
package/dist/litepub.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as exportSpki, r as getDocumentLoader, t as isRelayFollowerData } from "./types-
|
|
4
|
+
import { n as exportSpki, r as getDocumentLoader, t as isRelayFollowerData } from "./types-DuILuqql.js";
|
|
5
5
|
import { MemoryKvStore, signRequest } from "@fedify/fedify";
|
|
6
6
|
import { createRelay } from "@fedify/relay";
|
|
7
7
|
import { deepStrictEqual, ok, strictEqual } from "node:assert";
|
|
@@ -25,6 +25,22 @@ const mockDocumentLoader = async (url) => {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
else if (url === "https://remote.example.com/users/dave" || url === "https://remote.example.com/users/dave#main-key") return {
|
|
29
|
+
contextUrl: null,
|
|
30
|
+
documentUrl: url.replace(/#main-key$/, ""),
|
|
31
|
+
document: {
|
|
32
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
|
|
33
|
+
id: url,
|
|
34
|
+
type: "Person",
|
|
35
|
+
preferredUsername: "dave",
|
|
36
|
+
inbox: daveInbox,
|
|
37
|
+
publicKey: {
|
|
38
|
+
id: "https://remote.example.com/users/dave#main-key",
|
|
39
|
+
owner: url.replace(/#main-key$/, ""),
|
|
40
|
+
publicKeyPem: await exportSpki(rsaKeyPair.publicKey)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
28
44
|
else if (url === "https://remote.example.com/notes/1") return {
|
|
29
45
|
contextUrl: null,
|
|
30
46
|
documentUrl: url,
|
|
@@ -52,6 +68,31 @@ const rsaPublicKey = {
|
|
|
52
68
|
id: new URL("https://remote.example.com/users/alice#main-key"),
|
|
53
69
|
...rsaKeyPair.publicKey
|
|
54
70
|
};
|
|
71
|
+
const davePublicKey = {
|
|
72
|
+
id: new URL("https://remote.example.com/users/dave#main-key"),
|
|
73
|
+
...rsaKeyPair.publicKey
|
|
74
|
+
};
|
|
75
|
+
const INBOX_PREFIX = "https://[2001:db8:1::";
|
|
76
|
+
const daveInbox = `${INBOX_PREFIX}1]/users/dave/inbox`;
|
|
77
|
+
const pendingInbox = `${INBOX_PREFIX}2]/users/bob/inbox`;
|
|
78
|
+
const acceptedInbox = `${INBOX_PREFIX}3]/users/carol/inbox`;
|
|
79
|
+
const recorders = /* @__PURE__ */ new Map();
|
|
80
|
+
function recordInbox(inbox) {
|
|
81
|
+
const recorded = [];
|
|
82
|
+
recorders.set(inbox, recorded);
|
|
83
|
+
return recorded;
|
|
84
|
+
}
|
|
85
|
+
const nextFetch = globalThis.fetch;
|
|
86
|
+
globalThis.fetch = (async (input, init) => {
|
|
87
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
88
|
+
if (!request.url.startsWith(INBOX_PREFIX)) return await nextFetch(input, init);
|
|
89
|
+
const recorded = recorders.get(request.url);
|
|
90
|
+
if (recorded != null) recorded.push({
|
|
91
|
+
method: request.method,
|
|
92
|
+
body: await request.json()
|
|
93
|
+
});
|
|
94
|
+
return new Response(null, { status: 202 });
|
|
95
|
+
});
|
|
55
96
|
describe("LitePubRelay", () => {
|
|
56
97
|
test("constructor with required options", () => {
|
|
57
98
|
ok(createRelay("litepub", {
|
|
@@ -183,9 +224,9 @@ describe("LitePubRelay", () => {
|
|
|
183
224
|
}
|
|
184
225
|
});
|
|
185
226
|
const follower = new Person({
|
|
186
|
-
id: new URL("https://remote.example.com/users/
|
|
187
|
-
preferredUsername: "
|
|
188
|
-
inbox: new URL(
|
|
227
|
+
id: new URL("https://remote.example.com/users/dave"),
|
|
228
|
+
preferredUsername: "dave",
|
|
229
|
+
inbox: new URL(daveInbox)
|
|
189
230
|
});
|
|
190
231
|
const followActivity = new Follow({
|
|
191
232
|
id: new URL("https://remote.example.com/activities/follow/1"),
|
|
@@ -197,32 +238,20 @@ describe("LitePubRelay", () => {
|
|
|
197
238
|
headers: { "Content-Type": "application/activity+json" },
|
|
198
239
|
body: JSON.stringify(await followActivity.toJsonLd({ contextLoader: mockDocumentLoader }))
|
|
199
240
|
});
|
|
200
|
-
request = await signRequest(request, rsaKeyPair.privateKey,
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
const outboundRequest = input instanceof Request ? input : new Request(input, init);
|
|
205
|
-
if (outboundRequest.url === "https://remote.example.com/users/alice/inbox") {
|
|
206
|
-
deliveredActivities.push(await outboundRequest.json());
|
|
207
|
-
return new Response(null, { status: 202 });
|
|
208
|
-
}
|
|
209
|
-
return originalFetch(input, init);
|
|
210
|
-
});
|
|
211
|
-
try {
|
|
212
|
-
await relay.fetch(request);
|
|
213
|
-
} finally {
|
|
214
|
-
globalThis.fetch = originalFetch;
|
|
215
|
-
}
|
|
241
|
+
request = await signRequest(request, rsaKeyPair.privateKey, davePublicKey.id);
|
|
242
|
+
const deliveredActivities = recordInbox(daveInbox);
|
|
243
|
+
const response = await relay.fetch(request);
|
|
244
|
+
ok(response.status === 200 || response.status === 202, `Unexpected inbox response status: ${response.status}`);
|
|
216
245
|
strictEqual(handlerCalled, true);
|
|
217
246
|
ok(handlerActor);
|
|
218
|
-
const followerData = await kv.get(["follower", "https://remote.example.com/users/
|
|
247
|
+
const followerData = await kv.get(["follower", "https://remote.example.com/users/dave"]);
|
|
219
248
|
ok(isRelayFollowerData(followerData));
|
|
220
249
|
strictEqual(followerData.state, "pending");
|
|
221
|
-
const reciprocalFollow = deliveredActivities.find((activity) => activity.type === "Follow");
|
|
250
|
+
const reciprocalFollow = deliveredActivities.map(({ body }) => body).find((activity) => activity.type === "Follow");
|
|
222
251
|
ok(reciprocalFollow, "Expected a reciprocal Follow activity");
|
|
223
252
|
strictEqual(reciprocalFollow.actor, "https://relay.example.com/users/relay");
|
|
224
|
-
strictEqual(reciprocalFollow.object, "https://remote.example.com/users/
|
|
225
|
-
strictEqual(reciprocalFollow.to, "https://remote.example.com/users/
|
|
253
|
+
strictEqual(reciprocalFollow.object, "https://remote.example.com/users/dave");
|
|
254
|
+
strictEqual(reciprocalFollow.to, "https://remote.example.com/users/dave");
|
|
226
255
|
});
|
|
227
256
|
test("handles Follow activity with subscription rejection", async () => {
|
|
228
257
|
const kv = new MemoryKvStore();
|
|
@@ -562,12 +591,12 @@ describe("LitePubRelay", () => {
|
|
|
562
591
|
const pendingFollower = new Person({
|
|
563
592
|
id: new URL("https://pending.example.com/users/bob"),
|
|
564
593
|
preferredUsername: "bob",
|
|
565
|
-
inbox: new URL(
|
|
594
|
+
inbox: new URL(pendingInbox)
|
|
566
595
|
});
|
|
567
596
|
const acceptedFollower = new Person({
|
|
568
597
|
id: new URL("https://accepted.example.com/users/carol"),
|
|
569
598
|
preferredUsername: "carol",
|
|
570
|
-
inbox: new URL(
|
|
599
|
+
inbox: new URL(acceptedInbox)
|
|
571
600
|
});
|
|
572
601
|
await kv.set(["follower", pendingFollower.id.href], {
|
|
573
602
|
actor: await pendingFollower.toJsonLd(),
|
|
@@ -598,23 +627,12 @@ describe("LitePubRelay", () => {
|
|
|
598
627
|
body: JSON.stringify(await createActivity.toJsonLd({ contextLoader: mockDocumentLoader }))
|
|
599
628
|
});
|
|
600
629
|
request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
return new Response(null, { status: 202 });
|
|
608
|
-
}
|
|
609
|
-
return await originalFetch(input, init);
|
|
610
|
-
});
|
|
611
|
-
try {
|
|
612
|
-
const response = await relay.fetch(request);
|
|
613
|
-
ok(response.status === 200 || response.status === 202);
|
|
614
|
-
} finally {
|
|
615
|
-
globalThis.fetch = originalFetch;
|
|
616
|
-
}
|
|
617
|
-
deepStrictEqual(deliveredInboxUrls, ["https://accepted.example.com/users/carol/inbox"]);
|
|
630
|
+
const deliveredToPending = recordInbox(pendingInbox);
|
|
631
|
+
const deliveredToAccepted = recordInbox(acceptedInbox);
|
|
632
|
+
const response = await relay.fetch(request);
|
|
633
|
+
ok(response.status === 200 || response.status === 202, `Unexpected inbox response status: ${response.status}`);
|
|
634
|
+
strictEqual(deliveredToAccepted.length, 1);
|
|
635
|
+
deepStrictEqual(deliveredToPending, []);
|
|
618
636
|
});
|
|
619
637
|
test("handles Update activity with Announce forwarding", async () => {
|
|
620
638
|
const relay = createRelay("litepub", {
|
package/dist/mastodon.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as exportSpki, r as getDocumentLoader, t as isRelayFollowerData } from "./types-
|
|
4
|
+
import { n as exportSpki, r as getDocumentLoader, t as isRelayFollowerData } from "./types-DuILuqql.js";
|
|
5
5
|
import { MemoryKvStore, signJsonLd, signRequest } from "@fedify/fedify";
|
|
6
6
|
import { createRelay } from "@fedify/relay";
|
|
7
7
|
import { deepStrictEqual, ok, strictEqual } from "node:assert";
|
|
@@ -52,6 +52,25 @@ const rsaPublicKey = {
|
|
|
52
52
|
id: new URL("https://remote.example.com/users/alice#main-key"),
|
|
53
53
|
...rsaKeyPair.publicKey
|
|
54
54
|
};
|
|
55
|
+
const INBOX_PREFIX = "https://[2001:db8:2::";
|
|
56
|
+
const followerInbox = `${INBOX_PREFIX}1]/users/bob/inbox`;
|
|
57
|
+
const recorders = /* @__PURE__ */ new Map();
|
|
58
|
+
function recordInbox(inbox) {
|
|
59
|
+
const recorded = [];
|
|
60
|
+
recorders.set(inbox, recorded);
|
|
61
|
+
return recorded;
|
|
62
|
+
}
|
|
63
|
+
const nextFetch = globalThis.fetch;
|
|
64
|
+
globalThis.fetch = (async (input, init) => {
|
|
65
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
66
|
+
if (!request.url.startsWith(INBOX_PREFIX)) return await nextFetch(input, init);
|
|
67
|
+
const recorded = recorders.get(request.url);
|
|
68
|
+
if (recorded != null) recorded.push({
|
|
69
|
+
method: request.method,
|
|
70
|
+
body: await request.json()
|
|
71
|
+
});
|
|
72
|
+
return new Response(null, { status: 202 });
|
|
73
|
+
});
|
|
55
74
|
describe("MastodonRelay", () => {
|
|
56
75
|
test("constructor with required options", () => {
|
|
57
76
|
ok(createRelay("mastodon", {
|
|
@@ -439,7 +458,7 @@ describe("MastodonRelay", () => {
|
|
|
439
458
|
const follower = new Person({
|
|
440
459
|
id: new URL("https://follower.example.com/users/bob"),
|
|
441
460
|
preferredUsername: "bob",
|
|
442
|
-
inbox: new URL(
|
|
461
|
+
inbox: new URL(followerInbox)
|
|
443
462
|
});
|
|
444
463
|
await kv.set(["follower", follower.id.href], {
|
|
445
464
|
actor: await follower.toJsonLd(),
|
|
@@ -463,27 +482,12 @@ describe("MastodonRelay", () => {
|
|
|
463
482
|
body: JSON.stringify(signedAnnounce)
|
|
464
483
|
});
|
|
465
484
|
request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
deliveryMethod = outboundRequest.method;
|
|
473
|
-
deliveredActivity = await outboundRequest.json();
|
|
474
|
-
return new Response(null, { status: 202 });
|
|
475
|
-
}
|
|
476
|
-
return originalFetch(input, init);
|
|
477
|
-
});
|
|
478
|
-
try {
|
|
479
|
-
const response = await relay.fetch(request);
|
|
480
|
-
ok(response.status === 200 || response.status === 202, `Unexpected inbox response status: ${response.status}`);
|
|
481
|
-
} finally {
|
|
482
|
-
globalThis.fetch = originalFetch;
|
|
483
|
-
}
|
|
484
|
-
ok(deliveredActivity, "Expected Announce delivery to the follower inbox");
|
|
485
|
-
strictEqual(deliveryMethod, "POST");
|
|
486
|
-
deepStrictEqual(deliveredActivity, signedAnnounce);
|
|
485
|
+
const delivered = recordInbox(followerInbox);
|
|
486
|
+
const response = await relay.fetch(request);
|
|
487
|
+
ok(response.status === 200 || response.status === 202, `Unexpected inbox response status: ${response.status}`);
|
|
488
|
+
strictEqual(delivered.length, 1, "Expected exactly one delivery");
|
|
489
|
+
strictEqual(delivered[0].method, "POST");
|
|
490
|
+
deepStrictEqual(delivered[0].body, signedAnnounce);
|
|
487
491
|
});
|
|
488
492
|
test("ignores Follow activity without required fields", async () => {
|
|
489
493
|
const kv = new MemoryKvStore();
|
|
@@ -25832,6 +25832,34 @@ const preloadedContexts = {
|
|
|
25832
25832
|
"@type": "@id"
|
|
25833
25833
|
}
|
|
25834
25834
|
} },
|
|
25835
|
+
"https://w3id.org/fep/22cd": { "@context": {
|
|
25836
|
+
"fep-22cd": "https://w3id.org/fep/22cd#",
|
|
25837
|
+
"schema": "https://schema.org/",
|
|
25838
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
25839
|
+
"translations": {
|
|
25840
|
+
"@id": "fep-22cd:translations",
|
|
25841
|
+
"@container": "@set"
|
|
25842
|
+
},
|
|
25843
|
+
"Translation": "fep-22cd:Translation",
|
|
25844
|
+
"sourceUpdated": {
|
|
25845
|
+
"@id": "fep-22cd:sourceUpdated",
|
|
25846
|
+
"@type": "xsd:dateTime"
|
|
25847
|
+
},
|
|
25848
|
+
"translator": {
|
|
25849
|
+
"@id": "schema:translator",
|
|
25850
|
+
"@type": "@id",
|
|
25851
|
+
"@container": "@set"
|
|
25852
|
+
},
|
|
25853
|
+
"inLanguage": "schema:inLanguage",
|
|
25854
|
+
"translationOfWork": {
|
|
25855
|
+
"@id": "schema:translationOfWork",
|
|
25856
|
+
"@type": "@id"
|
|
25857
|
+
},
|
|
25858
|
+
"isBasedOn": {
|
|
25859
|
+
"@id": "schema:isBasedOn",
|
|
25860
|
+
"@type": "@id"
|
|
25861
|
+
}
|
|
25862
|
+
} },
|
|
25835
25863
|
"https://w3id.org/fep/5711": { "@context": {
|
|
25836
25864
|
"likesOf": {
|
|
25837
25865
|
"@id": "https://w3id.org/fep/5711#likesOf",
|
|
@@ -26074,7 +26102,147 @@ const preloadedContexts = {
|
|
|
26074
26102
|
} }
|
|
26075
26103
|
};
|
|
26076
26104
|
var name = "@fedify/vocab-runtime";
|
|
26077
|
-
var version = "2.4.0-dev.
|
|
26105
|
+
var version = "2.4.0-dev.1988+c5c9d617";
|
|
26106
|
+
/**
|
|
26107
|
+
* Error thrown when fetching a JSON-LD document failed.
|
|
26108
|
+
*/
|
|
26109
|
+
var FetchError = class extends Error {
|
|
26110
|
+
/**
|
|
26111
|
+
* The URL that failed to fetch.
|
|
26112
|
+
*/
|
|
26113
|
+
url;
|
|
26114
|
+
/**
|
|
26115
|
+
* The HTTP response that failed, if available.
|
|
26116
|
+
*/
|
|
26117
|
+
response;
|
|
26118
|
+
/**
|
|
26119
|
+
* Constructs a new `FetchError`.
|
|
26120
|
+
*
|
|
26121
|
+
* @param url The URL that failed to fetch.
|
|
26122
|
+
* @param message Error message.
|
|
26123
|
+
* @param response The failed HTTP response, if available.
|
|
26124
|
+
*/
|
|
26125
|
+
constructor(url, message, response) {
|
|
26126
|
+
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
26127
|
+
this.name = "FetchError";
|
|
26128
|
+
this.url = typeof url === "string" ? new URL(url) : url;
|
|
26129
|
+
this.response = response;
|
|
26130
|
+
}
|
|
26131
|
+
};
|
|
26132
|
+
/**
|
|
26133
|
+
* Creates a request for the given URL.
|
|
26134
|
+
* @param url The URL to create the request for.
|
|
26135
|
+
* @param options The options for the request.
|
|
26136
|
+
* @returns The created request.
|
|
26137
|
+
* @internal
|
|
26138
|
+
*/
|
|
26139
|
+
function createActivityPubRequest(url, options = {}) {
|
|
26140
|
+
return new Request(url, {
|
|
26141
|
+
headers: {
|
|
26142
|
+
Accept: "application/activity+json, application/ld+json",
|
|
26143
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
26144
|
+
},
|
|
26145
|
+
redirect: "manual"
|
|
26146
|
+
});
|
|
26147
|
+
}
|
|
26148
|
+
/**
|
|
26149
|
+
* Gets the user agent string for the given application and URL.
|
|
26150
|
+
* @param options The options for making the user agent string.
|
|
26151
|
+
* @returns The user agent string.
|
|
26152
|
+
* @since 1.3.0
|
|
26153
|
+
*/
|
|
26154
|
+
function getUserAgent({ software, url } = {}) {
|
|
26155
|
+
const fedify = `Fedify/${version}`;
|
|
26156
|
+
const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${process$1.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${process$1.versions.node}` : null;
|
|
26157
|
+
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
26158
|
+
if (runtime != null) userAgent.push(runtime);
|
|
26159
|
+
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
26160
|
+
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
26161
|
+
}
|
|
26162
|
+
/**
|
|
26163
|
+
* Logs the request.
|
|
26164
|
+
* @param request The request to log.
|
|
26165
|
+
* @internal
|
|
26166
|
+
*/
|
|
26167
|
+
function logRequest(logger, request) {
|
|
26168
|
+
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
26169
|
+
method: request.method,
|
|
26170
|
+
url: request.url,
|
|
26171
|
+
headers: Object.fromEntries(request.headers.entries())
|
|
26172
|
+
});
|
|
26173
|
+
}
|
|
26174
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
26175
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
26176
|
+
/** A body exceeded the byte limit. @internal */
|
|
26177
|
+
var BodyTooLargeError = class extends FetchError {
|
|
26178
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
26179
|
+
constructor(url, maxBytes) {
|
|
26180
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
26181
|
+
this.name = "BodyTooLargeError";
|
|
26182
|
+
}
|
|
26183
|
+
};
|
|
26184
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
26185
|
+
function validateBodySizeLimit(maxBytes) {
|
|
26186
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
26187
|
+
}
|
|
26188
|
+
/**
|
|
26189
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
26190
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
26191
|
+
* @param message The response or request to read.
|
|
26192
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
26193
|
+
* @param url The URL to include in errors and logs.
|
|
26194
|
+
* @returns The decoded body text.
|
|
26195
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
26196
|
+
* @internal
|
|
26197
|
+
*/
|
|
26198
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
26199
|
+
validateBodySizeLimit(maxBytes);
|
|
26200
|
+
const reader = message.body?.getReader();
|
|
26201
|
+
const tooLarge = () => {
|
|
26202
|
+
getLogger([
|
|
26203
|
+
"fedify",
|
|
26204
|
+
"runtime",
|
|
26205
|
+
"body"
|
|
26206
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
26207
|
+
url: url.toString(),
|
|
26208
|
+
maxBytes
|
|
26209
|
+
});
|
|
26210
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
26211
|
+
};
|
|
26212
|
+
try {
|
|
26213
|
+
const length = message.headers.get("Content-Length");
|
|
26214
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
26215
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
26216
|
+
if (reader == null) return "";
|
|
26217
|
+
let size = 0;
|
|
26218
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
26219
|
+
const { done, value } = await reader.read();
|
|
26220
|
+
if (done) {
|
|
26221
|
+
controller.close();
|
|
26222
|
+
return;
|
|
26223
|
+
}
|
|
26224
|
+
size += value.byteLength;
|
|
26225
|
+
if (size > maxBytes) tooLarge();
|
|
26226
|
+
controller.enqueue(value);
|
|
26227
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
26228
|
+
try {
|
|
26229
|
+
const chunks = [];
|
|
26230
|
+
while (true) {
|
|
26231
|
+
const { done, value } = await textReader.read();
|
|
26232
|
+
if (done) break;
|
|
26233
|
+
chunks.push(value);
|
|
26234
|
+
}
|
|
26235
|
+
return chunks.join("");
|
|
26236
|
+
} finally {
|
|
26237
|
+
textReader.releaseLock();
|
|
26238
|
+
}
|
|
26239
|
+
} catch (error) {
|
|
26240
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
26241
|
+
throw error;
|
|
26242
|
+
} finally {
|
|
26243
|
+
reader?.releaseLock();
|
|
26244
|
+
}
|
|
26245
|
+
}
|
|
26078
26246
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
26079
26247
|
const regexpLinkWhitespace = /[\n\r\s\t]/;
|
|
26080
26248
|
function validateURI(uri) {
|
|
@@ -26254,74 +26422,6 @@ var HttpHeaderLink = class HttpHeaderLink {
|
|
|
26254
26422
|
return new this(...inputs).toString();
|
|
26255
26423
|
}
|
|
26256
26424
|
};
|
|
26257
|
-
/**
|
|
26258
|
-
* Error thrown when fetching a JSON-LD document failed.
|
|
26259
|
-
*/
|
|
26260
|
-
var FetchError = class extends Error {
|
|
26261
|
-
/**
|
|
26262
|
-
* The URL that failed to fetch.
|
|
26263
|
-
*/
|
|
26264
|
-
url;
|
|
26265
|
-
/**
|
|
26266
|
-
* The HTTP response that failed, if available.
|
|
26267
|
-
*/
|
|
26268
|
-
response;
|
|
26269
|
-
/**
|
|
26270
|
-
* Constructs a new `FetchError`.
|
|
26271
|
-
*
|
|
26272
|
-
* @param url The URL that failed to fetch.
|
|
26273
|
-
* @param message Error message.
|
|
26274
|
-
* @param response The failed HTTP response, if available.
|
|
26275
|
-
*/
|
|
26276
|
-
constructor(url, message, response) {
|
|
26277
|
-
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
26278
|
-
this.name = "FetchError";
|
|
26279
|
-
this.url = typeof url === "string" ? new URL(url) : url;
|
|
26280
|
-
this.response = response;
|
|
26281
|
-
}
|
|
26282
|
-
};
|
|
26283
|
-
/**
|
|
26284
|
-
* Creates a request for the given URL.
|
|
26285
|
-
* @param url The URL to create the request for.
|
|
26286
|
-
* @param options The options for the request.
|
|
26287
|
-
* @returns The created request.
|
|
26288
|
-
* @internal
|
|
26289
|
-
*/
|
|
26290
|
-
function createActivityPubRequest(url, options = {}) {
|
|
26291
|
-
return new Request(url, {
|
|
26292
|
-
headers: {
|
|
26293
|
-
Accept: "application/activity+json, application/ld+json",
|
|
26294
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
26295
|
-
},
|
|
26296
|
-
redirect: "manual"
|
|
26297
|
-
});
|
|
26298
|
-
}
|
|
26299
|
-
/**
|
|
26300
|
-
* Gets the user agent string for the given application and URL.
|
|
26301
|
-
* @param options The options for making the user agent string.
|
|
26302
|
-
* @returns The user agent string.
|
|
26303
|
-
* @since 1.3.0
|
|
26304
|
-
*/
|
|
26305
|
-
function getUserAgent({ software, url } = {}) {
|
|
26306
|
-
const fedify = `Fedify/${version}`;
|
|
26307
|
-
const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${process$1.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${process$1.versions.node}` : null;
|
|
26308
|
-
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
26309
|
-
if (runtime != null) userAgent.push(runtime);
|
|
26310
|
-
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
26311
|
-
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
26312
|
-
}
|
|
26313
|
-
/**
|
|
26314
|
-
* Logs the request.
|
|
26315
|
-
* @param request The request to log.
|
|
26316
|
-
* @internal
|
|
26317
|
-
*/
|
|
26318
|
-
function logRequest(logger, request) {
|
|
26319
|
-
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
26320
|
-
method: request.method,
|
|
26321
|
-
url: request.url,
|
|
26322
|
-
headers: Object.fromEntries(request.headers.entries())
|
|
26323
|
-
});
|
|
26324
|
-
}
|
|
26325
26425
|
const logger = getLogger([
|
|
26326
26426
|
"fedify",
|
|
26327
26427
|
"runtime",
|
|
@@ -26336,58 +26436,6 @@ function createResponseMetadata(response) {
|
|
|
26336
26436
|
statusText: response.statusText
|
|
26337
26437
|
});
|
|
26338
26438
|
}
|
|
26339
|
-
async function cancelResponseBody(response) {
|
|
26340
|
-
if (response.body != null) await response.body.cancel();
|
|
26341
|
-
}
|
|
26342
|
-
async function readBoundedText(response, maxBytes) {
|
|
26343
|
-
const contentLength = response.headers.get("Content-Length");
|
|
26344
|
-
if (contentLength != null) {
|
|
26345
|
-
const size = Number(contentLength);
|
|
26346
|
-
if (size > maxBytes) {
|
|
26347
|
-
await cancelResponseBody(response);
|
|
26348
|
-
return {
|
|
26349
|
-
text: "",
|
|
26350
|
-
size,
|
|
26351
|
-
tooLarge: true
|
|
26352
|
-
};
|
|
26353
|
-
}
|
|
26354
|
-
}
|
|
26355
|
-
if (response.body == null) return {
|
|
26356
|
-
text: "",
|
|
26357
|
-
size: 0,
|
|
26358
|
-
tooLarge: false
|
|
26359
|
-
};
|
|
26360
|
-
const reader = response.body.getReader();
|
|
26361
|
-
const decoder = new TextDecoder();
|
|
26362
|
-
let text = "";
|
|
26363
|
-
let size = 0;
|
|
26364
|
-
try {
|
|
26365
|
-
while (true) {
|
|
26366
|
-
const result = await reader.read();
|
|
26367
|
-
if (result.done) break;
|
|
26368
|
-
const chunkSize = result.value.byteLength;
|
|
26369
|
-
if (size + chunkSize > maxBytes) {
|
|
26370
|
-
size += chunkSize;
|
|
26371
|
-
await reader.cancel();
|
|
26372
|
-
return {
|
|
26373
|
-
text: "",
|
|
26374
|
-
size,
|
|
26375
|
-
tooLarge: true
|
|
26376
|
-
};
|
|
26377
|
-
}
|
|
26378
|
-
size += chunkSize;
|
|
26379
|
-
text += decoder.decode(result.value, { stream: true });
|
|
26380
|
-
}
|
|
26381
|
-
text += decoder.decode();
|
|
26382
|
-
return {
|
|
26383
|
-
text,
|
|
26384
|
-
size,
|
|
26385
|
-
tooLarge: false
|
|
26386
|
-
};
|
|
26387
|
-
} finally {
|
|
26388
|
-
reader.releaseLock();
|
|
26389
|
-
}
|
|
26390
|
-
}
|
|
26391
26439
|
/**
|
|
26392
26440
|
* Gets a {@link RemoteDocument} from the given response.
|
|
26393
26441
|
* @param url The URL of the document to load.
|
|
@@ -26443,42 +26491,44 @@ async function getRemoteDocument(url, response, fetch) {
|
|
|
26443
26491
|
let document;
|
|
26444
26492
|
if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
|
|
26445
26493
|
const errorResponse = createResponseMetadata(response);
|
|
26446
|
-
|
|
26447
|
-
|
|
26494
|
+
let html;
|
|
26495
|
+
try {
|
|
26496
|
+
html = await readBoundedText(response, MAX_HTML_SIZE, documentUrl);
|
|
26497
|
+
} catch (error) {
|
|
26498
|
+
if (!(error instanceof BodyTooLargeError)) throw error;
|
|
26448
26499
|
logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
|
|
26449
26500
|
url: documentUrl,
|
|
26450
|
-
|
|
26501
|
+
maxBytes: MAX_HTML_SIZE
|
|
26451
26502
|
});
|
|
26452
26503
|
throw new FetchError(documentUrl, `HTML document is too large to scan for an ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
|
|
26453
|
-
}
|
|
26454
|
-
|
|
26455
|
-
|
|
26456
|
-
|
|
26457
|
-
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
|
|
26465
|
-
|
|
26466
|
-
|
|
26467
|
-
|
|
26468
|
-
|
|
26469
|
-
|
|
26470
|
-
|
|
26471
|
-
|
|
26472
|
-
}
|
|
26473
|
-
}
|
|
26474
|
-
try {
|
|
26475
|
-
document = JSON.parse(html.text);
|
|
26476
|
-
} catch (error) {
|
|
26477
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
26478
|
-
throw new FetchError(documentUrl, `HTML document has no ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
|
|
26504
|
+
}
|
|
26505
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
26506
|
+
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
26507
|
+
let tagMatch;
|
|
26508
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
26509
|
+
const tagContent = tagMatch[2];
|
|
26510
|
+
let attrMatch;
|
|
26511
|
+
const attribs = {};
|
|
26512
|
+
attrPattern.lastIndex = 0;
|
|
26513
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
26514
|
+
const key = attrMatch[1].toLowerCase();
|
|
26515
|
+
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
26516
|
+
}
|
|
26517
|
+
if (attribs.rel === "alternate" && "type" in attribs && (attribs.type === "application/activity+json" || attribs.type === "application/ld+json" || attribs.type.startsWith("application/ld+json;")) && "href" in attribs && new URL(attribs.href, docUrl).href !== docUrl.href) {
|
|
26518
|
+
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
26519
|
+
alternateUrl: attribs.href,
|
|
26520
|
+
url: documentUrl
|
|
26521
|
+
});
|
|
26522
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
26479
26523
|
}
|
|
26480
26524
|
}
|
|
26481
|
-
|
|
26525
|
+
try {
|
|
26526
|
+
document = JSON.parse(html);
|
|
26527
|
+
} catch (error) {
|
|
26528
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
26529
|
+
throw new FetchError(documentUrl, `HTML document has no ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
|
|
26530
|
+
}
|
|
26531
|
+
} else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
|
|
26482
26532
|
logger.debug("Fetched document: {status} {url} {headers}", {
|
|
26483
26533
|
status: response.status,
|
|
26484
26534
|
url: documentUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/relay",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.1988+c5c9d617",
|
|
4
4
|
"description": "ActivityPub relay support for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Fedify",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@logtape/logtape": "^2.2.0",
|
|
49
49
|
"temporal-polyfill": "^1.0.1",
|
|
50
|
-
"@fedify/
|
|
51
|
-
"@fedify/
|
|
50
|
+
"@fedify/fedify": "^2.4.0-dev.1988+c5c9d617",
|
|
51
|
+
"@fedify/vocab": "2.4.0-dev.1988+c5c9d617"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsdown": "^0.22.0",
|
|
55
55
|
"typescript": "^6.0.0",
|
|
56
56
|
"urlpattern-polyfill": "^10.1.0",
|
|
57
|
-
"@fedify/vocab-runtime": "^2.4.0-dev.
|
|
57
|
+
"@fedify/vocab-runtime": "^2.4.0-dev.1988+c5c9d617"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build:self": "tsdown",
|