@fedify/fedify 2.4.0-dev.2084 → 2.4.0-dev.2099
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-BGlpUocK.mjs → builder-Bno54o_-.mjs} +34 -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-B6Aj2kGY.mjs → compound-proof-X3SxXVbc.mjs} +191 -13
- package/dist/{context-CZjZYXFC.d.cts → context-C43spTGI.d.cts} +5 -2
- package/dist/{context-BSywYspi.d.ts → context-MORIQJIh.d.ts} +5 -2
- package/dist/{deno-Dn-py9Zy.mjs → deno-dxdvl2XZ.mjs} +1 -1
- package/dist/{docloader-BUq5bYJr.mjs → docloader-eBxQDsk9.mjs} +2 -2
- 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 +3 -3
- package/dist/federation/handler.test.mjs +5 -5
- 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 +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +254 -5
- package/dist/federation/portable-gateway-keys.test.mjs +72 -11
- package/dist/federation/portable-inbox.test.mjs +487 -10
- package/dist/federation/portable.test.mjs +182 -4
- package/dist/federation/send.test.mjs +3 -3
- 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 +89 -1
- package/dist/{federation-BLGx8JUH.cjs → federation-C4Ef3JKa.cjs} +1487 -1070
- package/dist/{http-B5q1CHtL.cjs → http-C8cZTE3Y.cjs} +1224 -985
- package/dist/{http-4PCCvhsU.js → http-D-rbr_Sy.js} +1663 -1460
- package/dist/{http-B0VRNipE.mjs → http-DsgawTd8.mjs} +82 -7
- package/dist/{key-BJrp524f.mjs → key-DYfmjnmp.mjs} +39 -99
- package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DIxwMeNi.js} +1 -1
- package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
- package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
- package/dist/{ld-BmFz5NLA.mjs → ld-B00wWHzc.mjs} +3 -3
- package/dist/{metrics-Beod6tzO.mjs → metrics-BP3vdBpb.mjs} +1 -1
- package/dist/{middleware-BKeQYPm0.mjs → middleware-Br1_d-Ga.mjs} +606 -389
- package/dist/{middleware-D_9pNYe9.js → middleware-CcXMe400.js} +1602 -1185
- package/dist/{middleware-B7CNsyqt.mjs → middleware-CeBVBa61.mjs} +1 -1
- package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
- package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
- package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
- package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +3 -3
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-CsKVgMzk.mjs → owner-BJPDRDRq.mjs} +5 -4
- package/dist/{owner-BQFZtFV4.cjs → owner-DSzH7wro.cjs} +2 -2
- package/dist/{owner-_pbbgLJK.js → owner-U12fDpQW.js} +2 -2
- package/dist/{portable-key-B4R-CiIV.mjs → portable-key-D0mWbV9G.mjs} +60 -41
- package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
- package/dist/{proof-ozG4HfcQ.mjs → proof-ChjBwnep.mjs} +59 -18
- package/dist/{send-ClPt8nYu.mjs → send-yFX0E-ye.mjs} +3 -3
- 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 +160 -3
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +72 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +3 -3
- package/dist/sig/mod.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +69 -5
- package/dist/sig/portable-key.test.mjs +176 -10
- package/dist/sig/proof.test.mjs +107 -4
- package/dist/{sig-k3C8sF7g.cjs → sig-B689Nj7y.cjs} +19 -3
- package/dist/{sig-DhdomAN0.js → sig-C85KqAcx.js} +19 -3
- package/dist/{temporal-Ba0EAZAI.mjs → temporal-Dl9x-Qw4.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +5 -2
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
|
@@ -8,14 +8,15 @@ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
|
|
|
8
8
|
import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
|
|
9
9
|
import "../std__assert-DvMDRPes.mjs";
|
|
10
10
|
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
11
|
-
import { p as signJsonLd } from "../ld-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { p as signJsonLd } from "../ld-B00wWHzc.mjs";
|
|
12
|
+
import { i as signObject } from "../proof-ChjBwnep.mjs";
|
|
13
|
+
import { f as verifyRequestDetailed, l as signRequest } from "../http-DsgawTd8.mjs";
|
|
14
14
|
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
15
|
-
import {
|
|
16
|
-
import { Create, Follow, Note, Person, Tombstone } from "@fedify/vocab";
|
|
15
|
+
import { _ as coversDelivery, o as createFederation, v as forwardPortableInboxActivity, y as resolvePortableInboxForwardingOptions } from "../middleware-Br1_d-Ga.mjs";
|
|
16
|
+
import { Create, CryptographicKey, Follow, Note, Person, Tombstone } from "@fedify/vocab";
|
|
17
17
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
18
18
|
import { exportDidKey, formatIri, parseIri } from "@fedify/vocab-runtime";
|
|
19
|
+
import { encodeBase64 } from "byte-encodings/base64";
|
|
19
20
|
//#region src/federation/portable-inbox.test.ts
|
|
20
21
|
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
21
22
|
const keyId = new URL(`${did}#${did.substring(8)}`);
|
|
@@ -522,6 +523,118 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
|
|
|
522
523
|
esm_default.hardReset();
|
|
523
524
|
}
|
|
524
525
|
});
|
|
526
|
+
function compatibleAlice(path = "/users/alice", { authority = did, origin = LOCAL, gateways = [
|
|
527
|
+
LOCAL,
|
|
528
|
+
GATEWAY2,
|
|
529
|
+
GATEWAY3
|
|
530
|
+
] } = {}) {
|
|
531
|
+
return {
|
|
532
|
+
id: new URL(`${origin}/.well-known/apgateway/${authority}${path}`),
|
|
533
|
+
inbox: new URL(inboxUrl(authority, `${path}/inbox`, origin)),
|
|
534
|
+
gateways: gateways.map((g) => new URL(g))
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
test("Federation.fetch() delivers to portable inboxes of compatible-ID actors", async (t) => {
|
|
538
|
+
const apAlice = parseIri(`ap+ef61://${did}/users/alice`);
|
|
539
|
+
const apInbox = parseIri(`ap+ef61://${did}/users/alice/inbox`);
|
|
540
|
+
const cases = [
|
|
541
|
+
[
|
|
542
|
+
"compatible actor and inbox",
|
|
543
|
+
compatibleAlice(),
|
|
544
|
+
""
|
|
545
|
+
],
|
|
546
|
+
[
|
|
547
|
+
"on another gateway",
|
|
548
|
+
compatibleAlice(void 0, { origin: GATEWAY2 }),
|
|
549
|
+
""
|
|
550
|
+
],
|
|
551
|
+
[
|
|
552
|
+
"compatible actor and ap: inbox",
|
|
553
|
+
{
|
|
554
|
+
...compatibleAlice(),
|
|
555
|
+
inbox: apInbox
|
|
556
|
+
},
|
|
557
|
+
""
|
|
558
|
+
],
|
|
559
|
+
[
|
|
560
|
+
"ap: actor and compatible inbox",
|
|
561
|
+
{
|
|
562
|
+
...compatibleAlice(),
|
|
563
|
+
id: apAlice
|
|
564
|
+
},
|
|
565
|
+
""
|
|
566
|
+
],
|
|
567
|
+
[
|
|
568
|
+
"compatible inbox with a query",
|
|
569
|
+
{
|
|
570
|
+
...compatibleAlice(),
|
|
571
|
+
inbox: new URL(inboxUrl() + "?page=1")
|
|
572
|
+
},
|
|
573
|
+
"?page=1"
|
|
574
|
+
]
|
|
575
|
+
];
|
|
576
|
+
for (const [name, alice, query] of cases) await t.step(name, async () => {
|
|
577
|
+
const queue = new RecordingQueue();
|
|
578
|
+
const { federation, received } = setup({
|
|
579
|
+
queue,
|
|
580
|
+
actor: (_ctx, identifier) => identifier === "alice" ? new Person(alice) : null
|
|
581
|
+
});
|
|
582
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
583
|
+
assertEquals(received.length, 1);
|
|
584
|
+
assertEquals(received[0].recipient, "alice");
|
|
585
|
+
assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3) + query, inboxUrl(did, "/users/alice/inbox", GATEWAY2) + query]);
|
|
586
|
+
});
|
|
587
|
+
const json = await signedFollow();
|
|
588
|
+
const rejections = [
|
|
589
|
+
["DID mismatch", new Person(compatibleAlice(void 0, { authority: otherDid }))],
|
|
590
|
+
["inbox mismatch", new Person({
|
|
591
|
+
...compatibleAlice(),
|
|
592
|
+
inbox: new URL(inboxUrl(did, "/users/alice/other-inbox"))
|
|
593
|
+
})],
|
|
594
|
+
["this server is not a gateway of the actor", new Person(compatibleAlice(void 0, { gateways: [GATEWAY2] }))],
|
|
595
|
+
["malformed actor ID", new Person({
|
|
596
|
+
...compatibleAlice(),
|
|
597
|
+
id: new URL(compatibleAlice().id.href + "?@gateway=https%3A%2F%2Fexample.com")
|
|
598
|
+
})],
|
|
599
|
+
["malformed inbox ID", new Person({
|
|
600
|
+
...compatibleAlice(),
|
|
601
|
+
inbox: new URL(inboxUrl() + "?@gateway=https%3A%2F%2Fexample.com")
|
|
602
|
+
})]
|
|
603
|
+
];
|
|
604
|
+
for (const [name, actor] of rejections) await t.step(name, async () => {
|
|
605
|
+
const { federation, received } = setup({ actor: (_ctx, identifier) => identifier === "alice" ? actor : null });
|
|
606
|
+
assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 404);
|
|
607
|
+
assertEquals(received.length, 0);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
test("forwardPortableInboxActivity() deduplicates compatible activity IDs", async () => {
|
|
611
|
+
const kv = new MemoryKvStore();
|
|
612
|
+
const queue = new RecordingQueue();
|
|
613
|
+
const forward = (activityId) => forwardPortableInboxActivity({
|
|
614
|
+
recipient: {
|
|
615
|
+
actorId: compatibleAlice().id,
|
|
616
|
+
inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
|
|
617
|
+
canonicalInboxId: `ap+ef61://${did}/users/alice/inbox`,
|
|
618
|
+
gateways: [new URL(LOCAL), new URL(GATEWAY2)]
|
|
619
|
+
},
|
|
620
|
+
activity: { id: activityId.href },
|
|
621
|
+
activityId,
|
|
622
|
+
activityType: "https://www.w3.org/ns/activitystreams#Follow",
|
|
623
|
+
excludedOrigins: [LOCAL],
|
|
624
|
+
baseUrl: LOCAL,
|
|
625
|
+
kv,
|
|
626
|
+
kvPrefix: ["_fedify", "portableInboxForwarding"],
|
|
627
|
+
outboxQueue: queue
|
|
628
|
+
});
|
|
629
|
+
const path = `${did}/follows/compatible`;
|
|
630
|
+
assertEquals((await forward(new URL(`${LOCAL}/.well-known/apgateway/${path}`))).length, 1);
|
|
631
|
+
for (const id of [
|
|
632
|
+
new URL(`${GATEWAY2}/.well-known/apgateway/${path}`),
|
|
633
|
+
new URL(`${LOCAL}/.well-known/apgateway/${path}?x=1`),
|
|
634
|
+
parseIri(`ap://${path}`)
|
|
635
|
+
]) assertEquals(await forward(id), [], id.href);
|
|
636
|
+
assertEquals(queue.outbox.length, 1);
|
|
637
|
+
});
|
|
525
638
|
const aliceId = parseIri(`ap+ef61://${did}/users/alice`);
|
|
526
639
|
const rsaGatewayKeyPair = await crypto.subtle.generateKey({
|
|
527
640
|
name: "RSASSA-PKCS1-v1_5",
|
|
@@ -547,12 +660,12 @@ function signatureKeyId(headers) {
|
|
|
547
660
|
* at {@link LOCAL} does: signed by her DID, and embedding the gateway keys in
|
|
548
661
|
* its `assertionMethod`.
|
|
549
662
|
*/
|
|
550
|
-
async function createGatewayLoader(keyPairs) {
|
|
663
|
+
async function createGatewayLoader(keyPairs, origin = LOCAL) {
|
|
551
664
|
const { federation } = setup({
|
|
552
665
|
aliceKeyPairs: keyPairs,
|
|
553
666
|
mapPortableActorId: mapAlice
|
|
554
667
|
});
|
|
555
|
-
const keys = await federation.createContext(new URL(
|
|
668
|
+
const keys = await federation.createContext(new URL(origin)).getActorKeyPairs("alice");
|
|
556
669
|
const document = await (await signObject(new Person({
|
|
557
670
|
id: aliceId,
|
|
558
671
|
inbox: parseIri(`${aliceId.href}/inbox`),
|
|
@@ -564,7 +677,7 @@ async function createGatewayLoader(keyPairs) {
|
|
|
564
677
|
assertionMethods: keys.map((k) => k.multikey)
|
|
565
678
|
}), ed25519PrivateKey, keyId, { contextLoader: mockDocumentLoader })).toJsonLd({ contextLoader: mockDocumentLoader });
|
|
566
679
|
return (url) => {
|
|
567
|
-
if (url.replace(/#.*$/, "") !== compatibleAliceId()) return mockDocumentLoader(url);
|
|
680
|
+
if (url.replace(/#.*$/, "") !== compatibleAliceId(origin)) return mockDocumentLoader(url);
|
|
568
681
|
return Promise.resolve({
|
|
569
682
|
contextUrl: null,
|
|
570
683
|
documentUrl: url,
|
|
@@ -807,14 +920,240 @@ test("A Fedify gateway accepts signed forwarded portable inbox deliveries", asyn
|
|
|
807
920
|
}
|
|
808
921
|
await assertSignedBy(forwarded, `${compatibleAliceId()}#main-key`, await createGatewayLoader([rsaGatewayKeyPair]));
|
|
809
922
|
const queue = new RecordingQueue();
|
|
923
|
+
const loader = await createGatewayLoader([rsaGatewayKeyPair]);
|
|
810
924
|
const { federation, received } = setup({
|
|
811
925
|
queue,
|
|
812
|
-
options: {
|
|
926
|
+
options: {
|
|
927
|
+
origin: GATEWAY2,
|
|
928
|
+
documentLoaderFactory: () => loader
|
|
929
|
+
}
|
|
813
930
|
});
|
|
814
931
|
assertEquals((await federation.fetch(toRequest(forwarded), { contextData: void 0 })).status, 202);
|
|
815
932
|
assertEquals(received.length, 1);
|
|
816
933
|
assertEquals(received[0].recipient, "alice");
|
|
817
|
-
assertEquals(queue.outbox.map((m) => m.inbox)
|
|
934
|
+
assertEquals(queue.outbox.map((m) => m.inbox), [inboxUrl(did, "/users/alice/inbox", GATEWAY3)]);
|
|
935
|
+
});
|
|
936
|
+
/** Records the URLs that a document loader is asked for. */
|
|
937
|
+
function recordLoader(loader) {
|
|
938
|
+
const urls = [];
|
|
939
|
+
return {
|
|
940
|
+
urls,
|
|
941
|
+
loader: (url, options) => {
|
|
942
|
+
urls.push(url);
|
|
943
|
+
return loader(url, options);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
const gateway2KeyId = new URL(`${compatibleAliceId(GATEWAY2)}#main-key`);
|
|
948
|
+
/** Signs a delivery to Alice's inbox on {@link LOCAL}. */
|
|
949
|
+
async function signDelivery(body, { keyId = gateway2KeyId, privateKey = rsaGatewayKeyPair.privateKey, spec = "draft-cavage-http-signatures-12", components, label, headers } = {}) {
|
|
950
|
+
const signed = await signRequest(post(inboxUrl(), body, headers), privateKey, keyId, {
|
|
951
|
+
spec,
|
|
952
|
+
...components == null ? {} : { body: null },
|
|
953
|
+
rfc9421: {
|
|
954
|
+
label,
|
|
955
|
+
components: components?.map((value) => ({
|
|
956
|
+
value,
|
|
957
|
+
params: {}
|
|
958
|
+
}))
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
return new Request(signed.url, {
|
|
962
|
+
method: "POST",
|
|
963
|
+
headers: signed.headers,
|
|
964
|
+
body: JSON.stringify(body)
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
async function deliverToGateway(request, { loader, kv, maxTargets }) {
|
|
968
|
+
const queue = new RecordingQueue();
|
|
969
|
+
const { federation, received, kv: store } = setup({
|
|
970
|
+
queue,
|
|
971
|
+
kv,
|
|
972
|
+
options: {
|
|
973
|
+
documentLoaderFactory: () => loader,
|
|
974
|
+
...maxTargets == null ? {} : { portableInboxForwarding: { maxTargets } }
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
const response = await federation.fetch(request, { contextData: void 0 });
|
|
978
|
+
assert(received.length <= 1);
|
|
979
|
+
return {
|
|
980
|
+
response,
|
|
981
|
+
queue,
|
|
982
|
+
kv: store
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function forwardedGateways(queue) {
|
|
986
|
+
return queue.outbox.map((m) => new URL(m.inbox).origin).sort();
|
|
987
|
+
}
|
|
988
|
+
test("Federation.fetch() does not forward portable inbox deliveries back to the gateway that forwarded them", async (t) => {
|
|
989
|
+
const gatewayLoader = await createGatewayLoader([rsaGatewayKeyPair], GATEWAY2);
|
|
990
|
+
for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) await t.step(spec, async () => {
|
|
991
|
+
const { loader, urls } = recordLoader(gatewayLoader);
|
|
992
|
+
const json = await signedFollow();
|
|
993
|
+
const { response, queue, kv } = await deliverToGateway(await signDelivery(json, { spec }), { loader });
|
|
994
|
+
assertEquals(response.status, 202);
|
|
995
|
+
assertEquals(forwardedGateways(queue), [GATEWAY3]);
|
|
996
|
+
assertEquals(urls.filter((u) => u.startsWith(GATEWAY2)), [gateway2KeyId.href]);
|
|
997
|
+
const { queue: queue2 } = await deliverToGateway(post(inboxUrl(), json), {
|
|
998
|
+
loader,
|
|
999
|
+
kv
|
|
1000
|
+
});
|
|
1001
|
+
assertEquals(queue2.outbox, []);
|
|
1002
|
+
});
|
|
1003
|
+
await t.step("identified before the targets are capped", async () => {
|
|
1004
|
+
const { queue } = await deliverToGateway(await signDelivery(await signedFollow()), {
|
|
1005
|
+
loader: gatewayLoader,
|
|
1006
|
+
maxTargets: 1
|
|
1007
|
+
});
|
|
1008
|
+
assertEquals(forwardedGateways(queue), [GATEWAY3]);
|
|
1009
|
+
});
|
|
1010
|
+
await t.step("with an unrelated signature first", async () => {
|
|
1011
|
+
const { loader, urls } = recordLoader(gatewayLoader);
|
|
1012
|
+
const json = await signedFollow();
|
|
1013
|
+
const unrelated = await signDelivery(json, {
|
|
1014
|
+
spec: "rfc9421",
|
|
1015
|
+
keyId: new URL("https://unrelated.example/key"),
|
|
1016
|
+
privateKey: rsaPrivateKey3,
|
|
1017
|
+
label: "sig1"
|
|
1018
|
+
});
|
|
1019
|
+
const gateway = await signDelivery(json, {
|
|
1020
|
+
spec: "rfc9421",
|
|
1021
|
+
label: "sig2"
|
|
1022
|
+
});
|
|
1023
|
+
const headers = new Headers(gateway.headers);
|
|
1024
|
+
for (const name of ["Signature-Input", "Signature"]) headers.set(name, `${unrelated.headers.get(name)}, ${gateway.headers.get(name)}`);
|
|
1025
|
+
const { queue } = await deliverToGateway(new Request(gateway.url, {
|
|
1026
|
+
method: "POST",
|
|
1027
|
+
headers,
|
|
1028
|
+
body: JSON.stringify(json)
|
|
1029
|
+
}), { loader });
|
|
1030
|
+
assertEquals(forwardedGateways(queue), [GATEWAY3]);
|
|
1031
|
+
assertFalse(urls.some((u) => u.startsWith("https://unrelated.example/")));
|
|
1032
|
+
});
|
|
1033
|
+
await t.step("activity authenticated by a Linked Data Signature", async () => {
|
|
1034
|
+
const { queue } = await deliverToGateway(await signDelivery(await signJsonLd({
|
|
1035
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1036
|
+
id: `https://example.com/follows/lds-${++activityCounter}`,
|
|
1037
|
+
type: "Follow",
|
|
1038
|
+
actor: "https://example.com/person2",
|
|
1039
|
+
object: formatIri(aliceId)
|
|
1040
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader })), { loader: gatewayLoader });
|
|
1041
|
+
assertEquals(forwardedGateways(queue), [GATEWAY3]);
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
test("Federation.fetch() forwards portable inbox deliveries whose sending gateway is not identified", async (t) => {
|
|
1045
|
+
const gatewayLoader = await createGatewayLoader([rsaGatewayKeyPair], GATEWAY2);
|
|
1046
|
+
const bobKeyId = new URL(`${GATEWAY2}/.well-known/apgateway/${did}/users/bob#main-key`);
|
|
1047
|
+
const compatibleId = new URL(compatibleAliceId(GATEWAY2));
|
|
1048
|
+
const unsignedActor = await new Person({
|
|
1049
|
+
id: compatibleId,
|
|
1050
|
+
inbox: new URL(`${compatibleId.href}/inbox`),
|
|
1051
|
+
publicKey: rsaPublicKey2.clone({
|
|
1052
|
+
id: gateway2KeyId,
|
|
1053
|
+
owner: compatibleId
|
|
1054
|
+
})
|
|
1055
|
+
}).toJsonLd({ contextLoader: mockDocumentLoader });
|
|
1056
|
+
const unsignedLoader = (url) => {
|
|
1057
|
+
if (url.replace(/#.*$/, "") !== compatibleId.href) return mockDocumentLoader(url);
|
|
1058
|
+
return Promise.resolve({
|
|
1059
|
+
contextUrl: null,
|
|
1060
|
+
documentUrl: url,
|
|
1061
|
+
document: structuredClone(unsignedActor)
|
|
1062
|
+
});
|
|
1063
|
+
};
|
|
1064
|
+
const json = await signedFollow();
|
|
1065
|
+
const contentDigest = `sha-256=:${encodeBase64(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(json))))}:`;
|
|
1066
|
+
const cases = [
|
|
1067
|
+
{
|
|
1068
|
+
name: "unsigned",
|
|
1069
|
+
unsigned: true,
|
|
1070
|
+
fetches: false
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
name: "invalid signature",
|
|
1074
|
+
sign: { privateKey: rsaPrivateKey2 },
|
|
1075
|
+
fetches: true
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
name: "unknown key",
|
|
1079
|
+
loader: mockDocumentLoader,
|
|
1080
|
+
fetches: true
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
name: "gateway not in gateways",
|
|
1084
|
+
sign: { keyId: new URL(`https://unlisted.example/.well-known/apgateway/${did}/users/alice#main-key`) },
|
|
1085
|
+
fetches: false
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
name: "key of another actor",
|
|
1089
|
+
sign: { keyId: bobKeyId },
|
|
1090
|
+
fetches: false
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: "content-digest not covered",
|
|
1094
|
+
sign: {
|
|
1095
|
+
spec: "rfc9421",
|
|
1096
|
+
components: ["@method", "@target-uri"]
|
|
1097
|
+
},
|
|
1098
|
+
fetches: false
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
name: "target URI not covered",
|
|
1102
|
+
sign: {
|
|
1103
|
+
spec: "rfc9421",
|
|
1104
|
+
components: [
|
|
1105
|
+
"@method",
|
|
1106
|
+
"@authority",
|
|
1107
|
+
"@path",
|
|
1108
|
+
"content-digest"
|
|
1109
|
+
],
|
|
1110
|
+
headers: { "Content-Digest": contentDigest }
|
|
1111
|
+
},
|
|
1112
|
+
fetches: false
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
name: "non-canonical spelling of content-digest",
|
|
1116
|
+
sign: {
|
|
1117
|
+
spec: "rfc9421",
|
|
1118
|
+
components: [
|
|
1119
|
+
"@method",
|
|
1120
|
+
"@target-uri",
|
|
1121
|
+
"Content-Digest"
|
|
1122
|
+
],
|
|
1123
|
+
headers: { "Content-Digest": "sha-256=:AAAA:" }
|
|
1124
|
+
},
|
|
1125
|
+
fetches: false
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
name: "key not vouched for by the DID",
|
|
1129
|
+
sign: { privateKey: rsaPrivateKey2 },
|
|
1130
|
+
loader: unsignedLoader,
|
|
1131
|
+
fetches: true
|
|
1132
|
+
}
|
|
1133
|
+
];
|
|
1134
|
+
for (const { name, sign, unsigned, loader, fetches } of cases) await t.step(name, async () => {
|
|
1135
|
+
const recorded = recordLoader(loader ?? gatewayLoader);
|
|
1136
|
+
const { response, queue } = await deliverToGateway(unsigned ? post(inboxUrl(), json) : await signDelivery(json, sign), { loader: recorded.loader });
|
|
1137
|
+
assertEquals(response.status, 202);
|
|
1138
|
+
assertEquals(forwardedGateways(queue), [GATEWAY3, GATEWAY2].sort());
|
|
1139
|
+
assertEquals(recorded.urls.some((u) => u.includes("/.well-known/apgateway/")), fetches);
|
|
1140
|
+
});
|
|
1141
|
+
await t.step("already forwarded to the gateway", async () => {
|
|
1142
|
+
const recorded = recordLoader(gatewayLoader);
|
|
1143
|
+
const kv = new MemoryKvStore();
|
|
1144
|
+
const json = await signedFollow();
|
|
1145
|
+
const { queue } = await deliverToGateway(post(inboxUrl(), json), {
|
|
1146
|
+
loader: recorded.loader,
|
|
1147
|
+
kv
|
|
1148
|
+
});
|
|
1149
|
+
assertEquals(forwardedGateways(queue), [GATEWAY3, GATEWAY2].sort());
|
|
1150
|
+
const { queue: queue2 } = await deliverToGateway(await signDelivery(json), {
|
|
1151
|
+
loader: recorded.loader,
|
|
1152
|
+
kv
|
|
1153
|
+
});
|
|
1154
|
+
assertEquals(queue2.outbox, []);
|
|
1155
|
+
assertEquals(recorded.urls.filter((u) => u.startsWith(GATEWAY2)), []);
|
|
1156
|
+
});
|
|
818
1157
|
});
|
|
819
1158
|
function forwardingParameters(overrides = {}) {
|
|
820
1159
|
const activityId = parseIri(`ap+ef61://${did}/follows/${++activityCounter}`);
|
|
@@ -915,6 +1254,144 @@ test("forwardPortableInboxActivity() does not wait for slow keys", async () => {
|
|
|
915
1254
|
esm_default.hardReset();
|
|
916
1255
|
}
|
|
917
1256
|
});
|
|
1257
|
+
function gatewaySignature(keyId, verify) {
|
|
1258
|
+
return {
|
|
1259
|
+
keyId,
|
|
1260
|
+
coversDelivery: true,
|
|
1261
|
+
verify
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function gatewayKeyOf(keyId, owner = aliceId) {
|
|
1265
|
+
return new CryptographicKey({
|
|
1266
|
+
id: keyId,
|
|
1267
|
+
owner,
|
|
1268
|
+
publicKey: rsaGatewayKeyPair.publicKey
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
test("forwardPortableInboxActivity() skips the gateway that forwarded the activity", async (t) => {
|
|
1272
|
+
const recipient = {
|
|
1273
|
+
actorId: parseIri(`ap://${did}/users/alice`),
|
|
1274
|
+
inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
|
|
1275
|
+
canonicalInboxId: `ap://${did}/users/alice/inbox`,
|
|
1276
|
+
gateways: [
|
|
1277
|
+
new URL(LOCAL),
|
|
1278
|
+
new URL(GATEWAY3),
|
|
1279
|
+
new URL(GATEWAY2)
|
|
1280
|
+
]
|
|
1281
|
+
};
|
|
1282
|
+
const owners = [["identified", aliceId], ["identified by a key of a compatible identifier", new URL(compatibleAliceId())]];
|
|
1283
|
+
for (const [name, owner] of owners) await t.step(name, async () => {
|
|
1284
|
+
const queue = new RecordingQueue();
|
|
1285
|
+
assertEquals((await forwardPortableInboxActivity(forwardingParameters({
|
|
1286
|
+
recipient,
|
|
1287
|
+
outboxQueue: queue,
|
|
1288
|
+
signatures: [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId, owner)))]
|
|
1289
|
+
}))).map((u) => u.origin), [GATEWAY3]);
|
|
1290
|
+
});
|
|
1291
|
+
const notIdentified = [
|
|
1292
|
+
["verify() rejects", [gatewaySignature(gateway2KeyId, () => Promise.reject(/* @__PURE__ */ new Error("Failed.")))]],
|
|
1293
|
+
["verify() throws", [gatewaySignature(gateway2KeyId, () => {
|
|
1294
|
+
throw new Error("Failed.");
|
|
1295
|
+
})]],
|
|
1296
|
+
["not verified", [gatewaySignature(gateway2KeyId, () => Promise.resolve(null))]],
|
|
1297
|
+
["the key is not owned by a portable actor", [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId, new URL(`${GATEWAY2}/users/alice`))))]],
|
|
1298
|
+
["the key is owned by another actor", [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId, parseIri(`ap://${did}/users/bob`))))]],
|
|
1299
|
+
["another key is verified", [gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(new URL(`${compatibleAliceId(GATEWAY3)}#main-key`))))]],
|
|
1300
|
+
["the signature does not cover the delivery", [{
|
|
1301
|
+
...gatewaySignature(gateway2KeyId, () => Promise.resolve(gatewayKeyOf(gateway2KeyId))),
|
|
1302
|
+
coversDelivery: false
|
|
1303
|
+
}]]
|
|
1304
|
+
];
|
|
1305
|
+
for (const [name, signatures] of notIdentified) await t.step(name, async () => {
|
|
1306
|
+
const queue = new RecordingQueue();
|
|
1307
|
+
assertEquals((await forwardPortableInboxActivity(forwardingParameters({
|
|
1308
|
+
recipient,
|
|
1309
|
+
outboxQueue: queue,
|
|
1310
|
+
signatures
|
|
1311
|
+
}))).map((u) => u.origin), [GATEWAY3, GATEWAY2]);
|
|
1312
|
+
});
|
|
1313
|
+
await t.step("verified only if it may save a request", async (t) => {
|
|
1314
|
+
const cases = [
|
|
1315
|
+
["forwarding is turned off", { options: resolvePortableInboxForwardingOptions({ maxTargets: 0 }) }],
|
|
1316
|
+
["no compare-and-swap", { kv: {
|
|
1317
|
+
...new MemoryKvStore(),
|
|
1318
|
+
cas: void 0
|
|
1319
|
+
} }],
|
|
1320
|
+
["this server", { excludedOrigins: [LOCAL, GATEWAY2] }]
|
|
1321
|
+
];
|
|
1322
|
+
for (const [name, overrides] of cases) await t.step(name, async () => {
|
|
1323
|
+
let calls = 0;
|
|
1324
|
+
await forwardPortableInboxActivity(forwardingParameters({
|
|
1325
|
+
recipient,
|
|
1326
|
+
outboxQueue: new RecordingQueue(),
|
|
1327
|
+
signatures: [gatewaySignature(gateway2KeyId, () => {
|
|
1328
|
+
calls++;
|
|
1329
|
+
return Promise.resolve(gatewayKeyOf(gateway2KeyId));
|
|
1330
|
+
})],
|
|
1331
|
+
...overrides
|
|
1332
|
+
}));
|
|
1333
|
+
assertEquals(calls, 0);
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
await t.step("verification does not finish in time", async () => {
|
|
1337
|
+
let release = () => {};
|
|
1338
|
+
const pending = new Promise((resolve) => release = resolve);
|
|
1339
|
+
const kv = new MemoryKvStore();
|
|
1340
|
+
const parameters = forwardingParameters({
|
|
1341
|
+
recipient,
|
|
1342
|
+
kv,
|
|
1343
|
+
outboxQueue: new RecordingQueue(),
|
|
1344
|
+
options: resolvePortableInboxForwardingOptions({
|
|
1345
|
+
maxTargets: 1,
|
|
1346
|
+
deadline: { milliseconds: 10 }
|
|
1347
|
+
}),
|
|
1348
|
+
signatures: [gatewaySignature(gateway2KeyId, () => pending)]
|
|
1349
|
+
});
|
|
1350
|
+
const started = performance.now();
|
|
1351
|
+
const forwarded = await forwardPortableInboxActivity(parameters);
|
|
1352
|
+
assert(performance.now() - started < 5e3);
|
|
1353
|
+
assertEquals(forwarded.map((u) => u.origin), [GATEWAY3]);
|
|
1354
|
+
release(gatewayKeyOf(gateway2KeyId));
|
|
1355
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1356
|
+
const claimed = [];
|
|
1357
|
+
for await (const { key } of kv.list(parameters.kvPrefix)) claimed.push(String(key.at(-1)));
|
|
1358
|
+
assertEquals(claimed, [GATEWAY3]);
|
|
1359
|
+
});
|
|
1360
|
+
});
|
|
1361
|
+
test("coversDelivery()", () => {
|
|
1362
|
+
const signature = (label, ...names) => ({
|
|
1363
|
+
keyId: gateway2KeyId,
|
|
1364
|
+
label,
|
|
1365
|
+
components: names.map((value) => ({
|
|
1366
|
+
value,
|
|
1367
|
+
params: {}
|
|
1368
|
+
}))
|
|
1369
|
+
});
|
|
1370
|
+
assert(coversDelivery(signature("sig1", "@method", "@target-uri", "@authority", "content-digest")));
|
|
1371
|
+
assert(coversDelivery(signature(null, "(request-target)", "host", "date", "digest")));
|
|
1372
|
+
assertFalse(coversDelivery(signature("sig1", "@method", "@target-uri")));
|
|
1373
|
+
assertFalse(coversDelivery(signature("sig1", "@method", "@target-uri", "Content-Digest")));
|
|
1374
|
+
assertFalse(coversDelivery(signature("sig1", "@method", "@authority", "@path", "content-digest")));
|
|
1375
|
+
assertFalse(coversDelivery(signature(null, "(request-target)", "digest")));
|
|
1376
|
+
assertFalse(coversDelivery({
|
|
1377
|
+
keyId: gateway2KeyId,
|
|
1378
|
+
label: "sig1",
|
|
1379
|
+
components: [
|
|
1380
|
+
{
|
|
1381
|
+
value: "@method",
|
|
1382
|
+
params: {}
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
value: "@target-uri",
|
|
1386
|
+
params: {}
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
value: "content-digest",
|
|
1390
|
+
params: { req: true }
|
|
1391
|
+
}
|
|
1392
|
+
]
|
|
1393
|
+
}));
|
|
1394
|
+
});
|
|
918
1395
|
test("Context.getPortableInboxUri()", async (t) => {
|
|
919
1396
|
const { federation } = setup();
|
|
920
1397
|
await t.step("Context", () => {
|