@fedify/fedify 2.4.5-dev.2462 → 2.4.5
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/README.md +3 -0
- package/dist/{builder-BBUGd9wr.mjs → builder-CUAVOHg6.mjs} +2 -2
- package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.cts} +0 -1
- package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.ts} +0 -1
- package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
- 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-CariICma.mjs → compound-proof-BNjXclzO.mjs} +2 -2
- package/dist/{context-7NyiqIcg.d.ts → context-CVOOd9pD.d.ts} +28 -2
- package/dist/{context-tsYPJ2Ew.d.cts → context-D0gFIgev.d.cts} +28 -2
- package/dist/{deno-BO7-a3hz.mjs → deno-DcJISdwu.mjs} +1 -1
- package/dist/docloader-CP1UxMLL.mjs +136 -0
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +6 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +4 -4
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox-report.test.mjs +5 -5
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +465 -32
- 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/outgoing-proof.test.mjs +9 -6
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.mjs +4 -4
- package/dist/federation/portable-delivery.test.mjs +2 -2
- package/dist/federation/portable-gateway-keys.test.mjs +11 -5
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable-parse-uri.test.mjs +1 -1
- package/dist/federation/portable-route-activity.test.mjs +2 -2
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +498 -88
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{federation-Dd9IwOEi.cjs → federation-Bkfq2YpS.cjs} +240 -102
- package/dist/{http-DLf8QYD6.cjs → http-DHVH0DqC.cjs} +179 -110
- package/dist/{http-D7L2pqLT.mjs → http-DuGmr8J8.mjs} +204 -106
- package/dist/{http-BVrOR6MG.js → http-R0ty0Udi.js} +178 -109
- package/dist/{inbox-DtEKOr3W.mjs → inbox-Bl1Y0s_A.mjs} +2 -2
- package/dist/{key-DYxuyd-s.d.cts → key-CyCcut3h.d.ts} +1 -2
- package/dist/{key-C1nv7_jD.d.ts → key-VEL1QqHF.d.cts} +1 -2
- package/dist/{kv-cache-Dkzuqs8B.cjs → kv-cache-BtBSBZi9.cjs} +72 -27
- package/dist/{kv-cache-m3RIr811.mjs → kv-cache-DAV2_U44.mjs} +1 -1
- package/dist/{kv-cache-DoZoRS9p.js → kv-cache-FnLtmATH.js} +73 -28
- package/dist/{ld-PJLGA2MJ.mjs → ld-Cy5GqbvS.mjs} +2 -2
- package/dist/{metrics-CGR9cI-Y.mjs → metrics-CW8LoN7n.mjs} +1 -1
- package/dist/{middleware-DPI-Jp9w.mjs → middleware-CdP-lz8Q.mjs} +1 -1
- package/dist/{middleware-ByanhTvb.mjs → middleware-D2fk45uN.mjs} +82 -42
- package/dist/{middleware-BxhBd7pw.js → middleware-D4rQARvq.js} +241 -103
- package/dist/{mod-DEZXwV6J.d.ts → mod-B1Qt5cQQ.d.ts} +2 -9
- package/dist/{mod-Xj5P4s4d.d.ts → mod-B5COx5Wd.d.ts} +1 -1
- package/dist/{mod-CavhAgLw.d.cts → mod-BTxJG7ZH.d.cts} +1 -1
- package/dist/{mod-CtpF2BU5.d.ts → mod-CpyjFA9s.d.ts} +1 -1
- package/dist/{mod-BojP2Bw3.d.cts → mod-makPMVMg.d.cts} +1 -1
- package/dist/{mod-Dw-nPxQh.d.cts → mod-ss0wBUH1.d.cts} +2 -9
- 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-B5c276hH.mjs → owner-CbQZqFim.mjs} +3 -3
- package/dist/{portable-collection-BMZY9qT-.js → portable-collection-B5Aryi8I.js} +1 -1
- package/dist/{portable-collection-CR5uwjws.cjs → portable-collection-C_Kmvl3q.cjs} +1 -1
- package/dist/{portable-collection-C88tBiy0.mjs → portable-collection-D2JYDadi.mjs} +2 -2
- package/dist/{portable-key-DxTXUpCK.mjs → portable-key-Cc-ctNU2.mjs} +1 -1
- package/dist/{proof-DNu-qP0i.mjs → proof-UN4CW55z.mjs} +3 -3
- package/dist/{send-CdCifQs4.mjs → send-BzbX2e6v.mjs} +174 -76
- 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 +553 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +1 -1
- package/dist/sig/mitra-receive-interop.test.mjs +4 -4
- package/dist/sig/mitra-send-interop.test.mjs +3 -3
- 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/portable-collection.test.mjs +1 -1
- package/dist/sig/portable-interop.test.mjs +3 -3
- package/dist/sig/portable-key.test.mjs +5 -5
- package/dist/sig/proof.test.mjs +4 -4
- package/dist/sig/tootik-request-interop.test.mjs +4 -4
- package/dist/sig/tootik-send-interop.test.mjs +3 -3
- package/dist/sig/verification.test.mjs +3 -3
- package/dist/{temporal-BuMu2bgP.mjs → temporal-B7Wvxrjs.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +26 -0
- package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
- package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
- package/dist/utils/docloader.test.mjs +344 -36
- 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 +7 -7
- package/dist/docloader-DQNRDYDb.mjs +0 -91
|
@@ -10,16 +10,20 @@ import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
|
|
|
10
10
|
import "../std__assert-DvMDRPes.mjs";
|
|
11
11
|
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
12
12
|
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { p as verifyRequest } from "../http-DuGmr8J8.mjs";
|
|
14
14
|
import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
|
|
15
|
-
import { t as doesActorOwnKey } from "../owner-
|
|
16
|
-
import { n as extractInboxes, r as
|
|
15
|
+
import { t as doesActorOwnKey } from "../owner-CbQZqFim.mjs";
|
|
16
|
+
import { i as sendActivity, n as extractInboxes, r as fetchWithValidatedRedirects, t as SendActivityError } from "../send-BzbX2e6v.mjs";
|
|
17
|
+
import { configure, reset } from "@logtape/logtape";
|
|
17
18
|
import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
|
|
18
19
|
import { FetchError, UrlError } from "@fedify/vocab-runtime";
|
|
19
20
|
import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
20
|
-
import
|
|
21
|
+
import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
|
|
21
22
|
import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
22
23
|
//#region src/federation/send.test.ts
|
|
24
|
+
function sendDirect(options) {
|
|
25
|
+
return sendActivity(options, { openHop: openDirectFetch });
|
|
26
|
+
}
|
|
23
27
|
test("extractInboxes()", () => {
|
|
24
28
|
const recipients = [
|
|
25
29
|
new Person({
|
|
@@ -155,7 +159,7 @@ test("sendActivity()", async (t) => {
|
|
|
155
159
|
"id": "https://example.com/activity",
|
|
156
160
|
"actor": "https://example.com/person"
|
|
157
161
|
};
|
|
158
|
-
await
|
|
162
|
+
await sendDirect({
|
|
159
163
|
activity,
|
|
160
164
|
keys: [{
|
|
161
165
|
privateKey: rsaPrivateKey2,
|
|
@@ -171,7 +175,7 @@ test("sendActivity()", async (t) => {
|
|
|
171
175
|
assertEquals(request?.headers.get("Content-Type"), "application/activity+json");
|
|
172
176
|
assertEquals(request?.headers.get("X-Test"), "test");
|
|
173
177
|
httpSigVerified = null;
|
|
174
|
-
await assertRejects(() =>
|
|
178
|
+
await assertRejects(() => sendDirect({
|
|
175
179
|
activity: {
|
|
176
180
|
...activity,
|
|
177
181
|
actor: "https://example.com/person2"
|
|
@@ -200,7 +204,7 @@ test("sendActivity()", async (t) => {
|
|
|
200
204
|
"id": "https://example.com/activity",
|
|
201
205
|
"actor": "https://example.com/person"
|
|
202
206
|
};
|
|
203
|
-
await assertRejects(() =>
|
|
207
|
+
await assertRejects(() => sendDirect({
|
|
204
208
|
activity,
|
|
205
209
|
activityId: "https://example.com/activity",
|
|
206
210
|
keys: [{
|
|
@@ -218,7 +222,7 @@ test("sendActivity()", async (t) => {
|
|
|
218
222
|
"actor": "https://example.com/person"
|
|
219
223
|
};
|
|
220
224
|
try {
|
|
221
|
-
await
|
|
225
|
+
await sendDirect({
|
|
222
226
|
activity,
|
|
223
227
|
activityId: "https://example.com/activity",
|
|
224
228
|
keys: [{
|
|
@@ -253,7 +257,7 @@ test("sendActivity()", async (t) => {
|
|
|
253
257
|
});
|
|
254
258
|
throw failure;
|
|
255
259
|
});
|
|
256
|
-
const error = await assertRejects(() =>
|
|
260
|
+
const error = await assertRejects(() => sendDirect({
|
|
257
261
|
activity,
|
|
258
262
|
activityId: "https://example.com/activity",
|
|
259
263
|
keys: [{
|
|
@@ -278,7 +282,7 @@ test("sendActivity()", async (t) => {
|
|
|
278
282
|
"actor": "https://example.com/person"
|
|
279
283
|
};
|
|
280
284
|
try {
|
|
281
|
-
await
|
|
285
|
+
await sendDirect({
|
|
282
286
|
activity,
|
|
283
287
|
activityId: "https://example.com/activity",
|
|
284
288
|
keys: [{
|
|
@@ -307,7 +311,7 @@ test("sendActivity()", async (t) => {
|
|
|
307
311
|
"actor": "https://example.com/person"
|
|
308
312
|
};
|
|
309
313
|
try {
|
|
310
|
-
await
|
|
314
|
+
await sendDirect({
|
|
311
315
|
activity,
|
|
312
316
|
activityId: "https://example.com/activity",
|
|
313
317
|
keys: [{
|
|
@@ -337,7 +341,7 @@ test("sendActivity()", async (t) => {
|
|
|
337
341
|
"actor": "https://example.com/person"
|
|
338
342
|
};
|
|
339
343
|
try {
|
|
340
|
-
await
|
|
344
|
+
await sendDirect({
|
|
341
345
|
activity,
|
|
342
346
|
activityId: "https://example.com/activity",
|
|
343
347
|
keys: [{
|
|
@@ -364,7 +368,7 @@ test("sendActivity()", async (t) => {
|
|
|
364
368
|
"actor": "https://example.com/person"
|
|
365
369
|
};
|
|
366
370
|
try {
|
|
367
|
-
await
|
|
371
|
+
await sendDirect({
|
|
368
372
|
activity,
|
|
369
373
|
activityId: "https://example.com/activity",
|
|
370
374
|
keys: [{
|
|
@@ -393,7 +397,7 @@ test("sendActivity()", async (t) => {
|
|
|
393
397
|
"actor": "https://example.com/person"
|
|
394
398
|
};
|
|
395
399
|
try {
|
|
396
|
-
await
|
|
400
|
+
await sendDirect({
|
|
397
401
|
activity,
|
|
398
402
|
activityId: "https://example.com/activity",
|
|
399
403
|
keys: [{
|
|
@@ -417,7 +421,7 @@ test("sendActivity() records OpenTelemetry span events", async (t) => {
|
|
|
417
421
|
await t.step("successful send", async () => {
|
|
418
422
|
esm_default.get("https://example.com/", { status: 404 });
|
|
419
423
|
esm_default.post("https://example.com/inbox", { status: 202 });
|
|
420
|
-
await
|
|
424
|
+
await sendDirect({
|
|
421
425
|
activity: {
|
|
422
426
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
423
427
|
type: "Create",
|
|
@@ -451,6 +455,41 @@ test("sendActivity() records OpenTelemetry span events", async (t) => {
|
|
|
451
455
|
esm_default.hardReset();
|
|
452
456
|
});
|
|
453
457
|
});
|
|
458
|
+
test("sendActivity() validates the initial policy before serializing", async () => {
|
|
459
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
460
|
+
let serialized = false;
|
|
461
|
+
await assertRejects(() => sendActivity({
|
|
462
|
+
activity: { toJSON() {
|
|
463
|
+
serialized = true;
|
|
464
|
+
throw new Error("must not serialize");
|
|
465
|
+
} },
|
|
466
|
+
keys: [],
|
|
467
|
+
inbox: new URL("http://127.0.0.1/inbox"),
|
|
468
|
+
meterProvider
|
|
469
|
+
}), UrlError);
|
|
470
|
+
assertFalse(serialized);
|
|
471
|
+
assertEquals(recorder.getMeasurements("activitypub.delivery.sent"), []);
|
|
472
|
+
});
|
|
473
|
+
test("sendActivity() closes the initial session when serialization fails", async () => {
|
|
474
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
475
|
+
const url = "https://example.com/lifecycle/serialization-failure";
|
|
476
|
+
const hops = recordHops(() => ({
|
|
477
|
+
status: 202,
|
|
478
|
+
body: "Accepted"
|
|
479
|
+
}));
|
|
480
|
+
const failure = /* @__PURE__ */ new Error("serialization failed");
|
|
481
|
+
assertEquals(await assertRejects(() => sendActivity({
|
|
482
|
+
activity: { toJSON() {
|
|
483
|
+
throw failure;
|
|
484
|
+
} },
|
|
485
|
+
keys: [],
|
|
486
|
+
inbox: new URL(url),
|
|
487
|
+
meterProvider
|
|
488
|
+
}, { openHop: hops.openHop }), Error, "serialization failed"), failure);
|
|
489
|
+
assertEquals(hops.events, [`open ${url}`, `close ${url}`]);
|
|
490
|
+
assertEquals(recorder.getMeasurements("activitypub.delivery.sent"), []);
|
|
491
|
+
hops.assertInvariants();
|
|
492
|
+
});
|
|
454
493
|
test("sendActivity() records OpenTelemetry delivery metrics", async (t) => {
|
|
455
494
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
456
495
|
esm_default.spyGlobal();
|
|
@@ -691,85 +730,456 @@ for (const signed of [false, true]) {
|
|
|
691
730
|
});
|
|
692
731
|
});
|
|
693
732
|
}
|
|
694
|
-
for (const signed of [false, true]) test({
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
733
|
+
for (const signed of [false, true]) test(`sendActivity() classifies DNS failures (signed: ${signed})`, async (t) => {
|
|
734
|
+
const activity = {
|
|
735
|
+
type: "Create",
|
|
736
|
+
id: "https://example.com/activity"
|
|
737
|
+
};
|
|
738
|
+
const destination = "https://delivery.invalid/inbox";
|
|
739
|
+
const publicInbox = "https://8.8.8.8/inbox";
|
|
740
|
+
const keys = signed ? [{
|
|
741
|
+
privateKey: rsaPrivateKey2,
|
|
742
|
+
keyId: rsaPublicKey2.id
|
|
743
|
+
}] : [];
|
|
744
|
+
for (const result of [
|
|
745
|
+
"throws",
|
|
746
|
+
"empty",
|
|
747
|
+
"cname",
|
|
748
|
+
"private"
|
|
749
|
+
]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
|
|
750
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
751
|
+
const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
|
|
752
|
+
const lookups = [];
|
|
753
|
+
const lookup = (hostname) => {
|
|
754
|
+
lookups.push(hostname);
|
|
755
|
+
assertEquals(hostname, "delivery.invalid");
|
|
756
|
+
if (result === "throws") return Promise.reject(resolverError);
|
|
757
|
+
return Promise.resolve(result === "empty" ? [] : [{
|
|
758
|
+
address: result === "private" ? "127.0.0.1" : "alias.invalid.",
|
|
759
|
+
family: 4
|
|
760
|
+
}]);
|
|
701
761
|
};
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
}] : [];
|
|
708
|
-
for (const result of [
|
|
709
|
-
"throws",
|
|
710
|
-
"empty",
|
|
711
|
-
"cname",
|
|
712
|
-
"private"
|
|
713
|
-
]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
|
|
714
|
-
const [meterProvider, recorder] = createTestMeterProvider();
|
|
715
|
-
const originalLookup = dns.lookup;
|
|
716
|
-
const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
|
|
717
|
-
const lookups = [];
|
|
718
|
-
dns.lookup = ((hostname, options) => {
|
|
719
|
-
lookups.push(hostname);
|
|
720
|
-
assertEquals(hostname, "delivery.invalid");
|
|
721
|
-
assertEquals(options, { all: true });
|
|
722
|
-
if (result === "throws") return Promise.reject(resolverError);
|
|
723
|
-
return Promise.resolve(result === "empty" ? [] : [{
|
|
724
|
-
address: result === "private" ? "127.0.0.1" : "alias.invalid.",
|
|
725
|
-
family: 4
|
|
726
|
-
}]);
|
|
762
|
+
try {
|
|
763
|
+
esm_default.mockGlobal().catch(202);
|
|
764
|
+
if (redirected) esm_default.route(publicInbox, {
|
|
765
|
+
status: 307,
|
|
766
|
+
headers: { Location: destination }
|
|
727
767
|
});
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
768
|
+
const send = () => sendActivity({
|
|
769
|
+
activity,
|
|
770
|
+
activityType: "https://www.w3.org/ns/activitystreams#Create",
|
|
771
|
+
meterProvider,
|
|
772
|
+
keys,
|
|
773
|
+
inbox: new URL(redirected ? publicInbox : destination)
|
|
774
|
+
}, { openHop: (url) => openPublicFetch(url, { lookup }) });
|
|
775
|
+
if (result === "private") assertEquals((await assertRejects(send, UrlError)).reason, "disallowed");
|
|
776
|
+
else {
|
|
777
|
+
const error = await assertRejects(send, FetchError);
|
|
778
|
+
assertEquals(error.url.href, destination);
|
|
779
|
+
assertInstanceOf(error.cause, UrlError);
|
|
780
|
+
assertEquals(error.cause.reason, "dns");
|
|
781
|
+
assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
|
|
782
|
+
}
|
|
783
|
+
const expectedCount = result === "private" && !redirected ? 0 : 1;
|
|
784
|
+
const sent = recorder.getMeasurements("activitypub.delivery.sent");
|
|
785
|
+
const durations = recorder.getMeasurements("activitypub.delivery.duration");
|
|
786
|
+
assertEquals(sent.length, expectedCount);
|
|
787
|
+
assertEquals(durations.length, expectedCount);
|
|
788
|
+
if (expectedCount === 1) {
|
|
789
|
+
assertEquals(sent[0].value, 1);
|
|
790
|
+
assertEquals(sent[0].attributes, {
|
|
791
|
+
"activitypub.remote.host": redirected ? "8.8.8.8" : "delivery.invalid",
|
|
792
|
+
"activitypub.activity.type": "https://www.w3.org/ns/activitystreams#Create",
|
|
793
|
+
"activitypub.delivery.success": false
|
|
740
794
|
});
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const error = await assertRejects(send, FetchError);
|
|
744
|
-
assertEquals(error.url.href, destination);
|
|
745
|
-
assertInstanceOf(error.cause, UrlError);
|
|
746
|
-
assertEquals(error.cause.reason, "dns");
|
|
747
|
-
assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
|
|
748
|
-
}
|
|
749
|
-
const expectedCount = result === "private" && !redirected ? 0 : 1;
|
|
750
|
-
const sent = recorder.getMeasurements("activitypub.delivery.sent");
|
|
751
|
-
const durations = recorder.getMeasurements("activitypub.delivery.duration");
|
|
752
|
-
assertEquals(sent.length, expectedCount);
|
|
753
|
-
assertEquals(durations.length, expectedCount);
|
|
754
|
-
if (expectedCount === 1) {
|
|
755
|
-
assertEquals(sent[0].value, 1);
|
|
756
|
-
assertEquals(sent[0].attributes, {
|
|
757
|
-
"activitypub.remote.host": redirected ? "8.8.8.8" : "delivery.invalid",
|
|
758
|
-
"activitypub.activity.type": "https://www.w3.org/ns/activitystreams#Create",
|
|
759
|
-
"activitypub.delivery.success": false
|
|
760
|
-
});
|
|
761
|
-
assertEquals(durations[0].attributes, sent[0].attributes);
|
|
762
|
-
assertGreaterOrEqual(durations[0].value, 0);
|
|
763
|
-
}
|
|
764
|
-
assertEquals(lookups, ["delivery.invalid"]);
|
|
765
|
-
assertEquals(esm_default.callHistory.calls(destination).length, 0);
|
|
766
|
-
assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
|
|
767
|
-
} finally {
|
|
768
|
-
dns.lookup = originalLookup;
|
|
769
|
-
esm_default.hardReset();
|
|
795
|
+
assertEquals(durations[0].attributes, sent[0].attributes);
|
|
796
|
+
assertGreaterOrEqual(durations[0].value, 0);
|
|
770
797
|
}
|
|
798
|
+
assertEquals(lookups, ["delivery.invalid"]);
|
|
799
|
+
assertEquals(esm_default.callHistory.calls(destination).length, 0);
|
|
800
|
+
assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
|
|
801
|
+
} finally {
|
|
802
|
+
esm_default.hardReset();
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
const lifecycleActivity = {
|
|
807
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
808
|
+
type: "Create",
|
|
809
|
+
id: "https://example.com/activity",
|
|
810
|
+
actor: "https://example.com/person"
|
|
811
|
+
};
|
|
812
|
+
const signedKeys = [{
|
|
813
|
+
privateKey: rsaPrivateKey2,
|
|
814
|
+
keyId: rsaPublicKey2.id
|
|
815
|
+
}];
|
|
816
|
+
const unsignedKeys = [{
|
|
817
|
+
privateKey: ed25519PrivateKey,
|
|
818
|
+
keyId: ed25519Multikey.id
|
|
819
|
+
}];
|
|
820
|
+
test("sendActivity() cancels a successful response body", async (t) => {
|
|
821
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
822
|
+
const inbox = "https://example.com/lifecycle/inbox";
|
|
823
|
+
const hops = recordHops(() => ({
|
|
824
|
+
status: 202,
|
|
825
|
+
body: "Accepted"
|
|
826
|
+
}));
|
|
827
|
+
await sendActivity({
|
|
828
|
+
activity: lifecycleActivity,
|
|
829
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
830
|
+
inbox: new URL(inbox)
|
|
831
|
+
}, { openHop: hops.openHop });
|
|
832
|
+
assertEquals(hops.events, [
|
|
833
|
+
`open ${inbox}`,
|
|
834
|
+
`fetch #1 ${inbox}`,
|
|
835
|
+
"cancel #1",
|
|
836
|
+
`close ${inbox}`
|
|
837
|
+
]);
|
|
838
|
+
hops.assertInvariants();
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
test("sendActivity() reads at most 1 KiB of an error response body", async (t) => {
|
|
842
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
843
|
+
const inbox = "https://example.com/lifecycle/error";
|
|
844
|
+
const hops = recordHops(() => ({
|
|
845
|
+
status: 500,
|
|
846
|
+
body: "x".repeat(4096)
|
|
847
|
+
}));
|
|
848
|
+
const error = await assertRejects(() => sendActivity({
|
|
849
|
+
activity: lifecycleActivity,
|
|
850
|
+
activityId: lifecycleActivity.id,
|
|
851
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
852
|
+
inbox: new URL(inbox)
|
|
853
|
+
}, { openHop: hops.openHop }), SendActivityError);
|
|
854
|
+
assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
|
|
855
|
+
assert(error.message.includes("… (truncated)"));
|
|
856
|
+
assertEquals(hops.events, [
|
|
857
|
+
`open ${inbox}`,
|
|
858
|
+
`fetch #1 ${inbox}`,
|
|
859
|
+
"cancel #1",
|
|
860
|
+
...signed ? [`fetch #2 ${inbox}`, "cancel #2"] : [],
|
|
861
|
+
`close ${inbox}`
|
|
862
|
+
]);
|
|
863
|
+
hops.assertInvariants();
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
test("sendActivity() closes each redirect hop before opening the next", async (t) => {
|
|
867
|
+
for (const [signed, status] of [[true, 307], [false, 308]]) await t.step(`signed: ${signed}, ${status}`, async () => {
|
|
868
|
+
const inbox = "https://example.com/lifecycle/redirect";
|
|
869
|
+
const next = "https://example.com/lifecycle/redirected";
|
|
870
|
+
const hops = recordHops((request) => request.url === inbox ? {
|
|
871
|
+
status,
|
|
872
|
+
headers: { Location: next },
|
|
873
|
+
body: "Moved"
|
|
874
|
+
} : {
|
|
875
|
+
status: 202,
|
|
876
|
+
body: "Accepted"
|
|
771
877
|
});
|
|
878
|
+
await sendActivity({
|
|
879
|
+
activity: lifecycleActivity,
|
|
880
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
881
|
+
inbox: new URL(inbox)
|
|
882
|
+
}, { openHop: hops.openHop });
|
|
883
|
+
assertEquals(hops.events, [
|
|
884
|
+
`open ${inbox}`,
|
|
885
|
+
`fetch #1 ${inbox}`,
|
|
886
|
+
"cancel #1",
|
|
887
|
+
`close ${inbox}`,
|
|
888
|
+
`open ${next}`,
|
|
889
|
+
`fetch #2 ${next}`,
|
|
890
|
+
"cancel #2",
|
|
891
|
+
`close ${next}`
|
|
892
|
+
]);
|
|
893
|
+
assertEquals(hops.requests[1].method, "POST");
|
|
894
|
+
assertEquals(await hops.requests[1].text(), JSON.stringify(lifecycleActivity));
|
|
895
|
+
hops.assertInvariants();
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
async function captureLogs(fn) {
|
|
899
|
+
const records = [];
|
|
900
|
+
await configure({
|
|
901
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
902
|
+
loggers: [{
|
|
903
|
+
category: "fedify",
|
|
904
|
+
sinks: ["buffer"],
|
|
905
|
+
lowestLevel: "debug"
|
|
906
|
+
}, {
|
|
907
|
+
category: ["logtape", "meta"],
|
|
908
|
+
sinks: []
|
|
909
|
+
}],
|
|
910
|
+
reset: true
|
|
911
|
+
});
|
|
912
|
+
try {
|
|
913
|
+
await fn();
|
|
914
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
915
|
+
} finally {
|
|
916
|
+
await reset();
|
|
772
917
|
}
|
|
918
|
+
return records;
|
|
919
|
+
}
|
|
920
|
+
function getCancelFailureUrls(records) {
|
|
921
|
+
return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.url);
|
|
922
|
+
}
|
|
923
|
+
test("fetchWithValidatedRedirects() logs a redirect body failing to cancel and follows the redirect", async (t) => {
|
|
924
|
+
const url = "https://example.com/lifecycle/cancel-failure";
|
|
925
|
+
const next = "https://example.com/lifecycle/cancel-failure-final";
|
|
926
|
+
const respond = (_, index) => index === 1 ? {
|
|
927
|
+
status: 302,
|
|
928
|
+
headers: { Location: next },
|
|
929
|
+
body: "Found",
|
|
930
|
+
failCancel: true
|
|
931
|
+
} : {
|
|
932
|
+
status: 202,
|
|
933
|
+
body: "Accepted"
|
|
934
|
+
};
|
|
935
|
+
await t.step("fetchWithValidatedRedirects()", async () => {
|
|
936
|
+
const hops = recordHops(respond);
|
|
937
|
+
let result;
|
|
938
|
+
const records = await captureLogs(async () => {
|
|
939
|
+
result = await fetchWithValidatedRedirects(new Request(url, {
|
|
940
|
+
method: "POST",
|
|
941
|
+
body: "activity"
|
|
942
|
+
}), hops.openHop, (response) => response.text());
|
|
943
|
+
});
|
|
944
|
+
assertEquals(result, "Accepted");
|
|
945
|
+
assertEquals(hops.events, [
|
|
946
|
+
`open ${url}`,
|
|
947
|
+
`fetch #1 ${url}`,
|
|
948
|
+
"cancel #1",
|
|
949
|
+
`close ${url}`,
|
|
950
|
+
`open ${next}`,
|
|
951
|
+
`fetch #2 ${next}`,
|
|
952
|
+
"consumed #2",
|
|
953
|
+
`close ${next}`
|
|
954
|
+
]);
|
|
955
|
+
assertEquals(getCancelFailureUrls(records), [url]);
|
|
956
|
+
hops.assertInvariants();
|
|
957
|
+
});
|
|
958
|
+
for (const signed of [true, false]) await t.step(`signed sendActivity(): ${signed}`, async () => {
|
|
959
|
+
const hops = recordHops(respond);
|
|
960
|
+
const records = await captureLogs(() => sendActivity({
|
|
961
|
+
activity: lifecycleActivity,
|
|
962
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
963
|
+
inbox: new URL(url)
|
|
964
|
+
}, { openHop: hops.openHop }));
|
|
965
|
+
assertEquals(hops.events, [
|
|
966
|
+
`open ${url}`,
|
|
967
|
+
`fetch #1 ${url}`,
|
|
968
|
+
"cancel #1",
|
|
969
|
+
`close ${url}`,
|
|
970
|
+
`open ${next}`,
|
|
971
|
+
`fetch #2 ${next}`,
|
|
972
|
+
"cancel #2",
|
|
973
|
+
`close ${next}`
|
|
974
|
+
]);
|
|
975
|
+
assertEquals(getCancelFailureUrls(records), [url]);
|
|
976
|
+
hops.assertInvariants();
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
test("fetchWithValidatedRedirects() closes the session when consuming fails", async () => {
|
|
980
|
+
const url = "https://example.com/lifecycle/consume-failure";
|
|
981
|
+
const hops = recordHops(() => ({
|
|
982
|
+
status: 200,
|
|
983
|
+
body: "x".repeat(1024)
|
|
984
|
+
}));
|
|
985
|
+
await assertRejects(() => fetchWithValidatedRedirects(new Request(url, {
|
|
986
|
+
method: "POST",
|
|
987
|
+
body: "activity"
|
|
988
|
+
}), hops.openHop, () => Promise.reject(/* @__PURE__ */ new Error("consume failed"))), Error, "consume failed");
|
|
989
|
+
assertEquals(hops.events, [
|
|
990
|
+
`open ${url}`,
|
|
991
|
+
`fetch #1 ${url}`,
|
|
992
|
+
"cancel #1 by close",
|
|
993
|
+
`close ${url}`
|
|
994
|
+
]);
|
|
995
|
+
hops.assertInvariants();
|
|
996
|
+
});
|
|
997
|
+
test("sendActivity() logs a successful response body failing to cancel", async (t) => {
|
|
998
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
999
|
+
const inbox = "https://example.com/lifecycle/cancel-failure-inbox";
|
|
1000
|
+
const hops = recordHops(() => ({
|
|
1001
|
+
status: 202,
|
|
1002
|
+
body: "Accepted",
|
|
1003
|
+
failCancel: true
|
|
1004
|
+
}));
|
|
1005
|
+
const records = await captureLogs(() => sendActivity({
|
|
1006
|
+
activity: lifecycleActivity,
|
|
1007
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1008
|
+
inbox: new URL(inbox)
|
|
1009
|
+
}, { openHop: hops.openHop }));
|
|
1010
|
+
assertEquals(hops.events, [
|
|
1011
|
+
`open ${inbox}`,
|
|
1012
|
+
`fetch #1 ${inbox}`,
|
|
1013
|
+
"cancel #1",
|
|
1014
|
+
`close ${inbox}`
|
|
1015
|
+
]);
|
|
1016
|
+
assertEquals(getCancelFailureUrls(records), [inbox]);
|
|
1017
|
+
hops.assertInvariants();
|
|
1018
|
+
});
|
|
773
1019
|
});
|
|
1020
|
+
test("sendActivity() is not blocked by a body cancellation that never settles", async (t) => {
|
|
1021
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
1022
|
+
const inbox = "https://example.com/lifecycle/hanging-cancel";
|
|
1023
|
+
const next = "https://example.com/lifecycle/hanging-cancel-final";
|
|
1024
|
+
const hops = recordHops((request) => request.url === inbox ? {
|
|
1025
|
+
status: 302,
|
|
1026
|
+
headers: { Location: next },
|
|
1027
|
+
body: "Found",
|
|
1028
|
+
hangCancel: true
|
|
1029
|
+
} : {
|
|
1030
|
+
status: 202,
|
|
1031
|
+
body: "Accepted",
|
|
1032
|
+
hangCancel: true
|
|
1033
|
+
});
|
|
1034
|
+
await sendActivity({
|
|
1035
|
+
activity: lifecycleActivity,
|
|
1036
|
+
keys: signed ? signedKeys : unsignedKeys,
|
|
1037
|
+
inbox: new URL(inbox)
|
|
1038
|
+
}, { openHop: hops.openHop });
|
|
1039
|
+
assertEquals(hops.events, [
|
|
1040
|
+
`open ${inbox}`,
|
|
1041
|
+
`fetch #1 ${inbox}`,
|
|
1042
|
+
"cancel #1",
|
|
1043
|
+
`close ${inbox}`,
|
|
1044
|
+
`open ${next}`,
|
|
1045
|
+
`fetch #2 ${next}`,
|
|
1046
|
+
"cancel #2",
|
|
1047
|
+
`close ${next}`
|
|
1048
|
+
]);
|
|
1049
|
+
hops.assertInvariants();
|
|
1050
|
+
});
|
|
1051
|
+
await t.step("truncated error body", async () => {
|
|
1052
|
+
const inbox = "https://example.com/lifecycle/hanging-error";
|
|
1053
|
+
const hops = recordHops(() => ({
|
|
1054
|
+
status: 500,
|
|
1055
|
+
body: "x".repeat(4096),
|
|
1056
|
+
hangCancel: true
|
|
1057
|
+
}));
|
|
1058
|
+
assertEquals((await assertRejects(() => sendActivity({
|
|
1059
|
+
activity: lifecycleActivity,
|
|
1060
|
+
keys: unsignedKeys,
|
|
1061
|
+
inbox: new URL(inbox)
|
|
1062
|
+
}, { openHop: hops.openHop }), SendActivityError)).responseBody, "x".repeat(1024) + "… (truncated)");
|
|
1063
|
+
assertEquals(hops.events, [
|
|
1064
|
+
`open ${inbox}`,
|
|
1065
|
+
`fetch #1 ${inbox}`,
|
|
1066
|
+
"cancel #1",
|
|
1067
|
+
`close ${inbox}`
|
|
1068
|
+
]);
|
|
1069
|
+
hops.assertInvariants();
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
test("sendActivity() completes with responses cloned by fetch-mock", async (t) => {
|
|
1073
|
+
for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
|
|
1074
|
+
esm_default.spyGlobal();
|
|
1075
|
+
try {
|
|
1076
|
+
esm_default.post("https://example.com/tee/inbox", () => new Response("Moved", {
|
|
1077
|
+
status: 307,
|
|
1078
|
+
headers: { Location: "https://example.com/tee/final" }
|
|
1079
|
+
}));
|
|
1080
|
+
esm_default.post("https://example.com/tee/final", () => new Response("Accepted", { status: 202 }));
|
|
1081
|
+
esm_default.post("https://example.com/tee/error", () => new Response("x".repeat(4096), { status: 404 }));
|
|
1082
|
+
const keys = signed ? signedKeys : unsignedKeys;
|
|
1083
|
+
await sendDirect({
|
|
1084
|
+
activity: lifecycleActivity,
|
|
1085
|
+
keys,
|
|
1086
|
+
inbox: new URL("https://example.com/tee/inbox")
|
|
1087
|
+
});
|
|
1088
|
+
assertEquals(esm_default.callHistory.calls("https://example.com/tee/final").length, 1);
|
|
1089
|
+
const error = await assertRejects(() => sendDirect({
|
|
1090
|
+
activity: lifecycleActivity,
|
|
1091
|
+
keys,
|
|
1092
|
+
inbox: new URL("https://example.com/tee/error")
|
|
1093
|
+
}), SendActivityError);
|
|
1094
|
+
assertEquals(error.statusCode, 404);
|
|
1095
|
+
assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
|
|
1096
|
+
} finally {
|
|
1097
|
+
esm_default.hardReset();
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
|
|
1102
|
+
const bytes = new TextEncoder().encode(init.body ?? "");
|
|
1103
|
+
const chunkSize = init.chunkSize ?? 256;
|
|
1104
|
+
let offset = 0;
|
|
1105
|
+
let settled = false;
|
|
1106
|
+
const body = new ReadableStream({
|
|
1107
|
+
pull(controller) {
|
|
1108
|
+
if (offset >= bytes.length) {
|
|
1109
|
+
settled = true;
|
|
1110
|
+
events.push(`consumed #${index}`);
|
|
1111
|
+
controller.close();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
controller.enqueue(bytes.slice(offset, offset + chunkSize));
|
|
1115
|
+
offset += chunkSize;
|
|
1116
|
+
},
|
|
1117
|
+
cancel() {
|
|
1118
|
+
settled = true;
|
|
1119
|
+
events.push(`cancel #${index}${cancelSuffix()}`);
|
|
1120
|
+
if (init.failCancel) throw new Error(`cancel #${index} failed`);
|
|
1121
|
+
if (init.hangCancel) return new Promise(() => {});
|
|
1122
|
+
}
|
|
1123
|
+
}, { highWaterMark: 0 });
|
|
1124
|
+
return {
|
|
1125
|
+
response: new Response(body, {
|
|
1126
|
+
status: init.status ?? 200,
|
|
1127
|
+
headers: init.headers
|
|
1128
|
+
}),
|
|
1129
|
+
settled: () => settled
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function recordHops(respond) {
|
|
1133
|
+
const events = [];
|
|
1134
|
+
const requests = [];
|
|
1135
|
+
const unsettledAtClose = [];
|
|
1136
|
+
let opens = 0;
|
|
1137
|
+
let closes = 0;
|
|
1138
|
+
let fetches = 0;
|
|
1139
|
+
return {
|
|
1140
|
+
events,
|
|
1141
|
+
requests,
|
|
1142
|
+
openHop(url) {
|
|
1143
|
+
opens++;
|
|
1144
|
+
events.push(`open ${url}`);
|
|
1145
|
+
const tracked = [];
|
|
1146
|
+
let closed = false;
|
|
1147
|
+
let closing = false;
|
|
1148
|
+
return Promise.resolve({
|
|
1149
|
+
url: new URL(url),
|
|
1150
|
+
async fetch(input, init) {
|
|
1151
|
+
if (closed) throw new TypeError("The session is closed.");
|
|
1152
|
+
const request = new Request(input instanceof Request ? input.clone() : input, init);
|
|
1153
|
+
const index = ++fetches;
|
|
1154
|
+
requests.push(request);
|
|
1155
|
+
events.push(`fetch #${index} ${request.url}`);
|
|
1156
|
+
const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
|
|
1157
|
+
tracked.push({
|
|
1158
|
+
...recorded,
|
|
1159
|
+
index
|
|
1160
|
+
});
|
|
1161
|
+
return recorded.response;
|
|
1162
|
+
},
|
|
1163
|
+
close() {
|
|
1164
|
+
if (closed) return Promise.resolve();
|
|
1165
|
+
closed = true;
|
|
1166
|
+
closes++;
|
|
1167
|
+
closing = true;
|
|
1168
|
+
for (const { response, settled, index } of tracked) {
|
|
1169
|
+
if (settled()) continue;
|
|
1170
|
+
if (response.body?.locked) unsettledAtClose.push(index);
|
|
1171
|
+
else response.body?.cancel().catch(() => {});
|
|
1172
|
+
}
|
|
1173
|
+
events.push(`close ${url}`);
|
|
1174
|
+
return Promise.resolve();
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
},
|
|
1178
|
+
assertInvariants() {
|
|
1179
|
+
assertEquals(opens, closes);
|
|
1180
|
+
assertEquals(unsettledAtClose, []);
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
774
1184
|
//#endregion
|
|
775
1185
|
export {};
|