@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271
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 +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- 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/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
5
|
+
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
6
|
+
import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
|
|
7
|
+
import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
|
|
8
|
+
import "../std__assert-DvMDRPes.mjs";
|
|
9
|
+
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
10
|
+
import { a as signObject, c as verifyPortableObjectProof } from "../proof-Dxy6ByqI.mjs";
|
|
11
|
+
import { n as ed25519PrivateKey, r as ed25519PublicKey } from "../keys-DIJNHW-Y.mjs";
|
|
12
|
+
import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-D7_LO-Vs.mjs";
|
|
13
|
+
import { t as verifyPortableObject } from "../portable-collection-C74P1vtC.mjs";
|
|
14
|
+
import { c as verifyServedPortableObjects } from "../compound-proof-B-9mXxFW.mjs";
|
|
15
|
+
import { n as RouterError } from "../router-u5O5KCQr.mjs";
|
|
16
|
+
import { configure, reset } from "@logtape/logtape";
|
|
17
|
+
import { Create, Note, Object as Object$1, OrderedCollection, PUBLIC_COLLECTION, Person, Tombstone, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
18
|
+
import { FetchError, exportDidKey, formatIri, parseIri, toCompatibleEf61Id } from "@fedify/vocab-runtime";
|
|
19
|
+
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
20
|
+
//#region src/federation/portable-collection.test.ts
|
|
21
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
22
|
+
const keyId = new URL(`${did}#${did.slice(8)}`);
|
|
23
|
+
const otherKeyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
|
|
24
|
+
const otherDid = await exportDidKey(otherKeyPair.publicKey);
|
|
25
|
+
const otherKeyId = new URL(`${otherDid}#${otherDid.slice(8)}`);
|
|
26
|
+
const ORIGIN = "https://example.com";
|
|
27
|
+
const ACCEPT = "application/activity+json";
|
|
28
|
+
function gatewayUrl(path, authority = did) {
|
|
29
|
+
return `${ORIGIN}/.well-known/apgateway/${authority}${path}`;
|
|
30
|
+
}
|
|
31
|
+
function portableId(path, authority = did) {
|
|
32
|
+
return `ap+ef61://${authority}${path}`;
|
|
33
|
+
}
|
|
34
|
+
async function fetchJson(federation, url, init = {}) {
|
|
35
|
+
const response = await federation.fetch(new Request(url, {
|
|
36
|
+
...init,
|
|
37
|
+
headers: {
|
|
38
|
+
Accept: ACCEPT,
|
|
39
|
+
...init.headers
|
|
40
|
+
}
|
|
41
|
+
}), { contextData: void 0 });
|
|
42
|
+
const body = await response.text();
|
|
43
|
+
const json = response.headers.get("Content-Type")?.includes("json");
|
|
44
|
+
return [response, body === "" || !json ? null : JSON.parse(body)];
|
|
45
|
+
}
|
|
46
|
+
async function sign(object, privateKey = ed25519PrivateKey, key = keyId) {
|
|
47
|
+
return await signObject(object, privateKey, key, { contextLoader: mockDocumentLoader });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The DIDs of the actors that this server hosts, which the actor dispatcher
|
|
51
|
+
* looks up by identifier, like an application would in its database.
|
|
52
|
+
*/
|
|
53
|
+
const users = {
|
|
54
|
+
alice: did,
|
|
55
|
+
bob: did,
|
|
56
|
+
carol: otherDid,
|
|
57
|
+
compatible: did,
|
|
58
|
+
mismatch: did,
|
|
59
|
+
"no-outbox": did,
|
|
60
|
+
https: did,
|
|
61
|
+
tombstone: did,
|
|
62
|
+
"request-did": did
|
|
63
|
+
};
|
|
64
|
+
const NOTES_PER_PAGE = 2;
|
|
65
|
+
const PAGES = [
|
|
66
|
+
"0",
|
|
67
|
+
"1",
|
|
68
|
+
"2"
|
|
69
|
+
];
|
|
70
|
+
function getNoteValues(identifier, cursor, index) {
|
|
71
|
+
return { id: `${identifier}-${cursor}-${index}` };
|
|
72
|
+
}
|
|
73
|
+
function createTestFederation(options = {}) {
|
|
74
|
+
const calls = options.calls ?? [];
|
|
75
|
+
const federation = createFederation({
|
|
76
|
+
kv: new MemoryKvStore(),
|
|
77
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
78
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
79
|
+
});
|
|
80
|
+
const noteCache = /* @__PURE__ */ new Map();
|
|
81
|
+
const getNote = async (ctx, id, authority) => {
|
|
82
|
+
const key = `${authority} ${id}`;
|
|
83
|
+
let note = noteCache.get(key);
|
|
84
|
+
if (note == null) {
|
|
85
|
+
note = await sign(new Note({
|
|
86
|
+
id: ctx.getPortableObjectUri(Note, { id }, authority),
|
|
87
|
+
to: PUBLIC_COLLECTION,
|
|
88
|
+
content: `Note ${id}`
|
|
89
|
+
}), authority === did ? ed25519PrivateKey : otherKeyPair.privateKey, authority === did ? keyId : otherKeyId);
|
|
90
|
+
noteCache.set(key, note);
|
|
91
|
+
}
|
|
92
|
+
return note;
|
|
93
|
+
};
|
|
94
|
+
federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
|
|
95
|
+
calls.push(`actor ${identifier}`);
|
|
96
|
+
const userDid = users[identifier];
|
|
97
|
+
if (userDid == null) return null;
|
|
98
|
+
switch (identifier) {
|
|
99
|
+
case "https": return new Person({
|
|
100
|
+
id: ctx.getActorUri(identifier),
|
|
101
|
+
outbox: ctx.getOutboxUri(identifier)
|
|
102
|
+
});
|
|
103
|
+
case "tombstone": return new Tombstone({ id: ctx.getPortableActorUri(identifier, userDid) });
|
|
104
|
+
case "request-did": {
|
|
105
|
+
const authority = ctx.portableRequest?.authority;
|
|
106
|
+
return new Person({
|
|
107
|
+
id: ctx.getPortableActorUri(identifier, authority ?? userDid),
|
|
108
|
+
outbox: ctx.getPortableOutboxUri(identifier, authority ?? userDid),
|
|
109
|
+
gateways: [new URL(ORIGIN)]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
case "compatible": {
|
|
113
|
+
const compatible = (id) => toCompatibleEf61Id(id, ORIGIN);
|
|
114
|
+
return new Person({
|
|
115
|
+
id: compatible(ctx.getPortableActorUri(identifier, userDid)),
|
|
116
|
+
outbox: compatible(ctx.getPortableOutboxUri(identifier, userDid)),
|
|
117
|
+
gateways: [new URL(ORIGIN), new URL("https://other.example")]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return await sign(new Person({
|
|
122
|
+
id: ctx.getPortableActorUri(identifier, userDid),
|
|
123
|
+
preferredUsername: identifier,
|
|
124
|
+
inbox: ctx.getPortableInboxUri(identifier, userDid),
|
|
125
|
+
outbox: identifier === "no-outbox" ? null : identifier === "mismatch" ? ctx.getPortableOutboxUri("alice", userDid) : ctx.getPortableOutboxUri(identifier, userDid),
|
|
126
|
+
followers: ctx.getPortableFollowersUri(identifier, userDid),
|
|
127
|
+
featured: ctx.getPortableFeaturedUri(identifier, userDid),
|
|
128
|
+
gateways: [new URL(ORIGIN)]
|
|
129
|
+
}), userDid === did ? ed25519PrivateKey : otherKeyPair.privateKey, userDid === did ? keyId : otherKeyId);
|
|
130
|
+
});
|
|
131
|
+
const createCache = /* @__PURE__ */ new Map();
|
|
132
|
+
const getCreate = async (ctx, id, authority) => {
|
|
133
|
+
const key = `${authority} ${id}`;
|
|
134
|
+
let create = createCache.get(key);
|
|
135
|
+
if (create == null) {
|
|
136
|
+
create = await sign(new Create({
|
|
137
|
+
id: ctx.getPortableObjectUri(Create, { id }, authority),
|
|
138
|
+
actor: ctx.getPortableActorUri("alice", authority),
|
|
139
|
+
to: PUBLIC_COLLECTION,
|
|
140
|
+
object: await getNote(ctx, id, authority)
|
|
141
|
+
}), authority === did ? ed25519PrivateKey : otherKeyPair.privateKey, authority === did ? keyId : otherKeyId);
|
|
142
|
+
createCache.set(key, create);
|
|
143
|
+
}
|
|
144
|
+
return create;
|
|
145
|
+
};
|
|
146
|
+
federation.setObjectDispatcher(Note, "/notes/{id}", (ctx, values) => ctx.portableRequest == null ? null : getNote(ctx, values.id, ctx.portableRequest.authority));
|
|
147
|
+
federation.setObjectDispatcher(Create, "/creates/{id}", (ctx, values) => ctx.portableRequest == null ? null : getCreate(ctx, values.id, ctx.portableRequest.authority));
|
|
148
|
+
const outbox = federation.setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier, cursor) => {
|
|
149
|
+
calls.push(`outbox ${identifier} ${cursor}`);
|
|
150
|
+
if (cursor == null) return null;
|
|
151
|
+
const authority = users[identifier];
|
|
152
|
+
const items = [];
|
|
153
|
+
for (let i = 0; i < NOTES_PER_PAGE; i++) items.push(await getCreate(ctx, getNoteValues(identifier, cursor, i).id, authority));
|
|
154
|
+
const index = PAGES.indexOf(cursor);
|
|
155
|
+
return {
|
|
156
|
+
items,
|
|
157
|
+
prevCursor: index > 0 ? PAGES[index - 1] : null,
|
|
158
|
+
nextCursor: index < PAGES.length - 1 ? PAGES[index + 1] : null
|
|
159
|
+
};
|
|
160
|
+
}).setFirstCursor((_ctx, identifier) => {
|
|
161
|
+
calls.push(`firstCursor ${identifier}`);
|
|
162
|
+
return PAGES[0];
|
|
163
|
+
}).setLastCursor(() => PAGES[PAGES.length - 1]).setCounter((_ctx, identifier) => {
|
|
164
|
+
calls.push(`counter ${identifier}`);
|
|
165
|
+
return PAGES.length * NOTES_PER_PAGE;
|
|
166
|
+
});
|
|
167
|
+
if (options.authorize) outbox.authorize((ctx, identifier) => {
|
|
168
|
+
calls.push(`authorize ${identifier} ${ctx.portableRequest?.authority ?? ""}`);
|
|
169
|
+
return ctx.request.headers.get("X-Allowed") === "yes";
|
|
170
|
+
});
|
|
171
|
+
federation.setFollowersDispatcher("/users/{identifier}/followers", (_ctx, _identifier) => ({ items: [{
|
|
172
|
+
id: new URL("https://a.example/users/1"),
|
|
173
|
+
inboxId: null
|
|
174
|
+
}, {
|
|
175
|
+
id: new URL("https://b.example/users/2"),
|
|
176
|
+
inboxId: null
|
|
177
|
+
}] }));
|
|
178
|
+
federation.setFeaturedDispatcher("/users/{identifier}/featured", () => ({ items: [] }));
|
|
179
|
+
if (options.inboxDispatcher ?? true) federation.setInboxDispatcher("/users/{identifier}/inbox", () => ({ items: [] }));
|
|
180
|
+
federation.setInboxListeners("/users/{identifier}/inbox");
|
|
181
|
+
federation.setOrderedCollectionDispatcher("bookmarks", Note, "/users/{identifier}/bookmarks", async (ctx, values, cursor) => {
|
|
182
|
+
calls.push(`bookmarks ${values.identifier} ${cursor}`);
|
|
183
|
+
const authority = users[values.identifier];
|
|
184
|
+
const tag = ctx.url.searchParams.get("tag") ?? "all";
|
|
185
|
+
return {
|
|
186
|
+
items: [await getNote(ctx, `${tag}-${cursor ?? "all"}`, authority)],
|
|
187
|
+
nextCursor: cursor === "0" ? "1" : null,
|
|
188
|
+
prevCursor: cursor === "1" ? "0" : null
|
|
189
|
+
};
|
|
190
|
+
}).setFirstCursor(() => "0").mapPortableOwner((_ctx, values) => values.identifier === "nobody" ? null : values.identifier);
|
|
191
|
+
federation.setCollectionDispatcher("drafts", Note, "/users/{identifier}/drafts", () => ({ items: [] }));
|
|
192
|
+
federation.setCollectionDispatcher("embedded", Object$1, "/embedded/{kind}", async (ctx, values) => {
|
|
193
|
+
const note = await getNote(ctx, values.kind, did);
|
|
194
|
+
switch (values.kind) {
|
|
195
|
+
case "signed": return { items: [note] };
|
|
196
|
+
case "other-did": return { items: [await getNote(ctx, "other", otherDid)] };
|
|
197
|
+
case "unsigned": return { items: [new Note({
|
|
198
|
+
id: ctx.getPortableObjectUri(Note, { id: "u" }, did),
|
|
199
|
+
content: "Unsigned"
|
|
200
|
+
})] };
|
|
201
|
+
case "tampered": return { items: [note.clone({ content: "Tampered" })] };
|
|
202
|
+
case "ordinary": return { items: [new Note({
|
|
203
|
+
id: new URL("https://example.com/notes/ordinary"),
|
|
204
|
+
content: "Ordinary"
|
|
205
|
+
})] };
|
|
206
|
+
case "nested": return { items: [await sign(new Create({
|
|
207
|
+
id: ctx.getPortableObjectUri(Note, { id: "create" }, did),
|
|
208
|
+
actor: ctx.getPortableActorUri("alice", did),
|
|
209
|
+
object: new Note({
|
|
210
|
+
id: ctx.getPortableObjectUri(Note, { id: "inner" }, did),
|
|
211
|
+
content: "Unsigned inner"
|
|
212
|
+
})
|
|
213
|
+
}))] };
|
|
214
|
+
case "replies": return { items: [await sign(new Note({
|
|
215
|
+
id: ctx.getPortableObjectUri(Note, { id: "replies" }, did),
|
|
216
|
+
content: "Replies",
|
|
217
|
+
replies: new OrderedCollection({
|
|
218
|
+
id: new URL(ctx.getPortableObjectUri(Note, { id: "replies" }, did).href + "/replies"),
|
|
219
|
+
totalItems: 0
|
|
220
|
+
})
|
|
221
|
+
}))] };
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}).mapPortableOwner(() => "alice");
|
|
225
|
+
return federation;
|
|
226
|
+
}
|
|
227
|
+
test("Federation.fetch() serves portable outboxes", async (t) => {
|
|
228
|
+
const federation = createTestFederation();
|
|
229
|
+
await t.step("collection", async () => {
|
|
230
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/outbox"));
|
|
231
|
+
assertEquals(response.status, 200);
|
|
232
|
+
assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
|
|
233
|
+
assertEquals(json?.id, portableId("/users/alice/outbox"));
|
|
234
|
+
assertEquals(json?.type, "OrderedCollection");
|
|
235
|
+
assertEquals(json?.attributedTo, portableId("/users/alice"));
|
|
236
|
+
assertEquals(json?.totalItems, PAGES.length * NOTES_PER_PAGE);
|
|
237
|
+
assertEquals(json?.first, portableId("/users/alice/outbox?cursor=0"));
|
|
238
|
+
assertEquals(json?.last, portableId("/users/alice/outbox?cursor=2"));
|
|
239
|
+
});
|
|
240
|
+
await t.step("pages", async () => {
|
|
241
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/outbox?cursor=1"));
|
|
242
|
+
assertEquals(response.status, 200);
|
|
243
|
+
assertEquals(json?.id, portableId("/users/alice/outbox?cursor=1"));
|
|
244
|
+
assertEquals(json?.type, "OrderedCollectionPage");
|
|
245
|
+
assertEquals(json?.partOf, portableId("/users/alice/outbox"));
|
|
246
|
+
assertEquals(json?.attributedTo, portableId("/users/alice"));
|
|
247
|
+
assertEquals(json?.prev, portableId("/users/alice/outbox?cursor=0"));
|
|
248
|
+
assertEquals(json?.next, portableId("/users/alice/outbox?cursor=2"));
|
|
249
|
+
const items = json?.orderedItems;
|
|
250
|
+
assertEquals(items.map((item) => item.id), [0, 1].map((i) => portableId(`/creates/${getNoteValues("alice", "1", i).id}`)));
|
|
251
|
+
assert(items.every((item) => item.proof != null));
|
|
252
|
+
assert(items.every((item) => item["@context"] != null));
|
|
253
|
+
});
|
|
254
|
+
await t.step("HEAD", async () => {
|
|
255
|
+
const response = await federation.fetch(new Request(gatewayUrl("/users/alice/outbox?cursor=0"), {
|
|
256
|
+
method: "HEAD",
|
|
257
|
+
headers: { Accept: ACCEPT }
|
|
258
|
+
}), { contextData: void 0 });
|
|
259
|
+
assertEquals(response.status, 200);
|
|
260
|
+
assertEquals(await response.text(), "");
|
|
261
|
+
});
|
|
262
|
+
await t.step("location hints and other parameters", async () => {
|
|
263
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/outbox?cursor=1&@gateway=https%3A%2F%2Fexample.com"));
|
|
264
|
+
assertEquals(response.status, 200);
|
|
265
|
+
assertEquals(json?.id, portableId("/users/alice/outbox?cursor=1"));
|
|
266
|
+
assertEquals(json?.partOf, portableId("/users/alice/outbox"));
|
|
267
|
+
});
|
|
268
|
+
await t.step("actors sharing a DID", async () => {
|
|
269
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/bob/outbox"));
|
|
270
|
+
assertEquals(response.status, 200);
|
|
271
|
+
assertEquals(json?.id, portableId("/users/bob/outbox"));
|
|
272
|
+
assertEquals(json?.attributedTo, portableId("/users/bob"));
|
|
273
|
+
});
|
|
274
|
+
await t.step("another DID", async () => {
|
|
275
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/carol/outbox", otherDid));
|
|
276
|
+
assertEquals(response.status, 200);
|
|
277
|
+
assertEquals(json?.id, portableId("/users/carol/outbox", otherDid));
|
|
278
|
+
});
|
|
279
|
+
await t.step("compatible identifiers", async () => {
|
|
280
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/compatible/outbox"));
|
|
281
|
+
assertEquals(response.status, 200);
|
|
282
|
+
assertEquals(json?.id, gatewayUrl("/users/compatible/outbox"));
|
|
283
|
+
assertEquals(json?.attributedTo, gatewayUrl("/users/compatible"));
|
|
284
|
+
assertEquals(json?.first, gatewayUrl("/users/compatible/outbox?cursor=0"));
|
|
285
|
+
});
|
|
286
|
+
await t.step("not acceptable", async () => {
|
|
287
|
+
assertEquals((await federation.fetch(new Request(gatewayUrl("/users/alice/outbox"), { headers: { Accept: "text/html" } }), { contextData: void 0 })).status, 406);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
test("Federation.fetch() does not serve collections of other DIDs", async (t) => {
|
|
291
|
+
const calls = [];
|
|
292
|
+
const federation = createTestFederation({ calls });
|
|
293
|
+
const cases = {
|
|
294
|
+
"a DID that is not the actor's": gatewayUrl("/users/alice/outbox", otherDid),
|
|
295
|
+
"an actor that is not portable": gatewayUrl("/users/https/outbox"),
|
|
296
|
+
"an actor whose outbox is another": gatewayUrl("/users/mismatch/outbox"),
|
|
297
|
+
"an actor without an outbox": gatewayUrl("/users/no-outbox/outbox"),
|
|
298
|
+
"a deleted actor": gatewayUrl("/users/tombstone/outbox"),
|
|
299
|
+
"an unknown actor": gatewayUrl("/users/unknown/outbox"),
|
|
300
|
+
"an actor dispatcher taking the DID from the request": gatewayUrl("/users/request-did/outbox", otherDid),
|
|
301
|
+
"an unregistered collection": gatewayUrl("/users/alice/liked"),
|
|
302
|
+
"a custom collection without an owner mapper": gatewayUrl("/users/alice/drafts"),
|
|
303
|
+
"a custom collection whose owner mapper returns null": gatewayUrl("/users/nobody/bookmarks"),
|
|
304
|
+
"a custom collection of another DID": gatewayUrl("/users/alice/bookmarks", otherDid)
|
|
305
|
+
};
|
|
306
|
+
for (const [name, url] of Object.entries(cases)) await t.step(name, async () => {
|
|
307
|
+
calls.length = 0;
|
|
308
|
+
const [response] = await fetchJson(federation, url);
|
|
309
|
+
assertEquals(response.status, 404);
|
|
310
|
+
assertFalse(calls.some((call) => call.startsWith("outbox ")));
|
|
311
|
+
assertFalse(calls.some((call) => call.startsWith("bookmarks ")));
|
|
312
|
+
});
|
|
313
|
+
await t.step("malformed DIDs", async () => {
|
|
314
|
+
const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox", "did:key:u" + did.slice(9)));
|
|
315
|
+
assertEquals(response.status, 400);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
test("Federation.fetch() serves portable inbox collections only with an inbox dispatcher", async () => {
|
|
319
|
+
const [response, json] = await fetchJson(createTestFederation(), gatewayUrl("/users/alice/inbox"));
|
|
320
|
+
assertEquals(response.status, 200);
|
|
321
|
+
assertEquals(json?.id, portableId("/users/alice/inbox"));
|
|
322
|
+
const [response2] = await fetchJson(createTestFederation({ inboxDispatcher: false }), gatewayUrl("/users/alice/inbox"));
|
|
323
|
+
assertEquals(response2.status, 404);
|
|
324
|
+
});
|
|
325
|
+
test("Federation.fetch() filters portable followers collections", async () => {
|
|
326
|
+
const federation = createTestFederation();
|
|
327
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/followers?base-url=" + encodeURIComponent("https://a.example/path")));
|
|
328
|
+
assertEquals(response.status, 200);
|
|
329
|
+
assertEquals(json?.id, portableId("/users/alice/followers?base-url=" + encodeURIComponent("https://a.example/")));
|
|
330
|
+
assertEquals(json?.orderedItems, ["https://a.example/users/1"]);
|
|
331
|
+
const collectionId = json?.id;
|
|
332
|
+
const [pageResponse, page] = await fetchJson(federation, gatewayUrl("/users/alice/followers?base-url=" + encodeURIComponent("https://a.example/path") + "&cursor=0"));
|
|
333
|
+
assertEquals(pageResponse.status, 200);
|
|
334
|
+
assertEquals(page?.id, `${collectionId}&cursor=0`);
|
|
335
|
+
assertEquals(page?.partOf, collectionId);
|
|
336
|
+
const [invalidResponse, invalidPage] = await fetchJson(federation, gatewayUrl("/users/alice/followers?base-url=invalid&cursor=0"));
|
|
337
|
+
assertEquals(invalidResponse.status, 200);
|
|
338
|
+
assertEquals(invalidPage?.id, portableId("/users/alice/followers?cursor=0"));
|
|
339
|
+
assertEquals(invalidPage?.partOf, portableId("/users/alice/followers"));
|
|
340
|
+
});
|
|
341
|
+
test("Federation.fetch() authorizes portable collection requests", async (t) => {
|
|
342
|
+
const calls = [];
|
|
343
|
+
const federation = createTestFederation({
|
|
344
|
+
authorize: true,
|
|
345
|
+
calls
|
|
346
|
+
});
|
|
347
|
+
await t.step("denied", async () => {
|
|
348
|
+
calls.length = 0;
|
|
349
|
+
const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox"));
|
|
350
|
+
assertEquals(response.status, 401);
|
|
351
|
+
assertEquals(calls, ["actor alice", `authorize alice ${did}`]);
|
|
352
|
+
});
|
|
353
|
+
await t.step("allowed", async () => {
|
|
354
|
+
calls.length = 0;
|
|
355
|
+
const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox?cursor=0"), { headers: { "X-Allowed": "yes" } });
|
|
356
|
+
assertEquals(response.status, 200);
|
|
357
|
+
assertEquals(calls.filter((call) => call.startsWith("authorize ")), [`authorize alice ${did}`]);
|
|
358
|
+
});
|
|
359
|
+
await t.step("a DID that is not the actor's", async () => {
|
|
360
|
+
calls.length = 0;
|
|
361
|
+
const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox", otherDid), { headers: { "X-Allowed": "yes" } });
|
|
362
|
+
assertEquals(response.status, 404);
|
|
363
|
+
assertEquals(calls, ["actor alice"]);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
test("Federation.fetch() serves portable custom collections", async (t) => {
|
|
367
|
+
const federation = createTestFederation();
|
|
368
|
+
await t.step("collection", async () => {
|
|
369
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks?tag=fedify"));
|
|
370
|
+
assertEquals(response.status, 200);
|
|
371
|
+
assertEquals(json?.id, portableId("/users/alice/bookmarks?tag=fedify"));
|
|
372
|
+
assertEquals(json?.attributedTo, portableId("/users/alice"));
|
|
373
|
+
assertEquals(json?.first, portableId("/users/alice/bookmarks?tag=fedify&cursor=0"));
|
|
374
|
+
});
|
|
375
|
+
await t.step("pages", async () => {
|
|
376
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks?tag=fedify&cursor=0"));
|
|
377
|
+
assertEquals(response.status, 200);
|
|
378
|
+
assertEquals(json?.id, portableId("/users/alice/bookmarks?tag=fedify&cursor=0"));
|
|
379
|
+
assertEquals(json?.partOf, portableId("/users/alice/bookmarks?tag=fedify"));
|
|
380
|
+
assertEquals(json?.next, portableId("/users/alice/bookmarks?tag=fedify&cursor=1"));
|
|
381
|
+
assertEquals((json?.orderedItems)[0].id, portableId("/notes/fedify-0"));
|
|
382
|
+
});
|
|
383
|
+
await t.step("compatible identifiers", async () => {
|
|
384
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/compatible/bookmarks"));
|
|
385
|
+
assertEquals(response.status, 200);
|
|
386
|
+
assertEquals(json?.id, gatewayUrl("/users/compatible/bookmarks"));
|
|
387
|
+
assertEquals(json?.attributedTo, gatewayUrl("/users/compatible"));
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
test("Federation.fetch() applies the proof policy to embedded portable objects", async (t) => {
|
|
391
|
+
const federation = createTestFederation();
|
|
392
|
+
for (const [kind, status] of Object.entries({
|
|
393
|
+
signed: 200,
|
|
394
|
+
"other-did": 200,
|
|
395
|
+
ordinary: 200,
|
|
396
|
+
replies: 200,
|
|
397
|
+
unsigned: 500,
|
|
398
|
+
tampered: 500,
|
|
399
|
+
nested: 500
|
|
400
|
+
})) await t.step(kind, async () => {
|
|
401
|
+
const [response] = await fetchJson(federation, gatewayUrl(`/embedded/${kind}`));
|
|
402
|
+
assertEquals(response.status, status);
|
|
403
|
+
});
|
|
404
|
+
await t.step("HEAD", async () => {
|
|
405
|
+
const response = await federation.fetch(new Request(gatewayUrl("/embedded/unsigned"), {
|
|
406
|
+
method: "HEAD",
|
|
407
|
+
headers: { Accept: ACCEPT }
|
|
408
|
+
}), { contextData: void 0 });
|
|
409
|
+
assertEquals(response.status, 500);
|
|
410
|
+
assertEquals(await response.text(), "");
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
test("Context builds portable collection IDs", async (t) => {
|
|
414
|
+
const ctx = createTestFederation().createContext(new URL(ORIGIN), void 0);
|
|
415
|
+
const helpers = {
|
|
416
|
+
getPortableOutboxUri: [(authority) => ctx.getPortableOutboxUri("alice", authority), "/users/alice/outbox"],
|
|
417
|
+
getPortableFollowersUri: [(authority) => ctx.getPortableFollowersUri("alice", authority), "/users/alice/followers"],
|
|
418
|
+
getPortableFeaturedUri: [(authority) => ctx.getPortableFeaturedUri("alice", authority), "/users/alice/featured"],
|
|
419
|
+
getPortableCollectionUri: [(authority) => ctx.getPortableCollectionUri("bookmarks", { identifier: "alice" }, authority), "/users/alice/bookmarks"]
|
|
420
|
+
};
|
|
421
|
+
for (const [name, [helper, path]] of Object.entries(helpers)) await t.step(name, () => {
|
|
422
|
+
assertEquals(formatIri(helper(did)), portableId(path));
|
|
423
|
+
assertThrows(() => helper(void 0), TypeError);
|
|
424
|
+
assertThrows(() => helper("https://example.com"), TypeError);
|
|
425
|
+
});
|
|
426
|
+
await t.step("unregistered collections", () => {
|
|
427
|
+
assertThrows(() => ctx.getPortableLikedUri("alice", did), RouterError);
|
|
428
|
+
assertThrows(() => ctx.getPortableFollowingUri("alice", did), RouterError);
|
|
429
|
+
assertThrows(() => ctx.getPortableFeaturedTagsUri("alice", did), RouterError);
|
|
430
|
+
assertThrows(() => ctx.getPortableCollectionUri("unknown", {}, did), RouterError);
|
|
431
|
+
});
|
|
432
|
+
await t.step("default authority of RequestContext", async () => {
|
|
433
|
+
const federation = createFederation({
|
|
434
|
+
kv: new MemoryKvStore(),
|
|
435
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
436
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
437
|
+
});
|
|
438
|
+
const built = [];
|
|
439
|
+
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new Person({
|
|
440
|
+
id: ctx.getPortableActorUri(identifier, did),
|
|
441
|
+
outbox: ctx.getPortableOutboxUri(identifier, did)
|
|
442
|
+
}));
|
|
443
|
+
federation.setOutboxDispatcher("/users/{identifier}/outbox", (ctx, identifier) => {
|
|
444
|
+
built.push(formatIri(ctx.getPortableOutboxUri(identifier)));
|
|
445
|
+
return { items: [] };
|
|
446
|
+
});
|
|
447
|
+
const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox"));
|
|
448
|
+
assertEquals(response.status, 200);
|
|
449
|
+
assertEquals(built, [portableId("/users/alice/outbox")]);
|
|
450
|
+
const requestContext = federation.createContext(new Request(`${ORIGIN}/`), void 0);
|
|
451
|
+
assertThrows(() => requestContext.getPortableOutboxUri("alice"), TypeError);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
/**
|
|
455
|
+
* Captures Fedify's warnings logged while `run` executes.
|
|
456
|
+
*/
|
|
457
|
+
async function captureWarnings(run) {
|
|
458
|
+
const records = [];
|
|
459
|
+
await reset();
|
|
460
|
+
try {
|
|
461
|
+
await configure({
|
|
462
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
463
|
+
filters: {},
|
|
464
|
+
loggers: [{
|
|
465
|
+
category: ["logtape", "meta"],
|
|
466
|
+
sinks: []
|
|
467
|
+
}, {
|
|
468
|
+
category: [],
|
|
469
|
+
sinks: ["buffer"],
|
|
470
|
+
lowestLevel: "warning"
|
|
471
|
+
}]
|
|
472
|
+
});
|
|
473
|
+
await run();
|
|
474
|
+
} finally {
|
|
475
|
+
await reset();
|
|
476
|
+
}
|
|
477
|
+
return records.filter((record) => record.category[0] === "fedify");
|
|
478
|
+
}
|
|
479
|
+
test("Actor dispatchers warn about mismatching portable collections", async () => {
|
|
480
|
+
const federation = createTestFederation();
|
|
481
|
+
const mismatches = (await captureWarnings(async () => {
|
|
482
|
+
for (const identifier of [
|
|
483
|
+
"alice",
|
|
484
|
+
"mismatch",
|
|
485
|
+
"https"
|
|
486
|
+
]) await fetchJson(federation, `${ORIGIN}/users/${identifier}`);
|
|
487
|
+
})).filter((record) => String(record.rawMessage).includes("does not match the portable ID"));
|
|
488
|
+
assertEquals(mismatches.map((record) => record.properties.property), ["outbox"]);
|
|
489
|
+
assertEquals(mismatches[0].properties.value, ctxOutbox("alice"));
|
|
490
|
+
});
|
|
491
|
+
function ctxOutbox(identifier) {
|
|
492
|
+
return new URL(`ap+ef61://${encodeURIComponent(did)}/users/${identifier}/outbox`).href;
|
|
493
|
+
}
|
|
494
|
+
test("Fedify traverses portable collections that Fedify serves", async () => {
|
|
495
|
+
const federation = createTestFederation();
|
|
496
|
+
const loader = (origin) => async (url) => {
|
|
497
|
+
if (!/^https?:/.test(url)) throw new Error(`Not found: ${url}`);
|
|
498
|
+
const target = new URL(url);
|
|
499
|
+
const request = new URL(target.pathname + target.search, ORIGIN);
|
|
500
|
+
const response = await federation.fetch(new Request(request, { headers: { Accept: ACCEPT } }), { contextData: void 0 });
|
|
501
|
+
if (!response.ok) throw new FetchError(url, `HTTP ${response.status}`, response);
|
|
502
|
+
return {
|
|
503
|
+
contextUrl: null,
|
|
504
|
+
documentUrl: new URL(target.pathname + target.search, origin).href,
|
|
505
|
+
document: await response.json()
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
const options = {
|
|
509
|
+
documentLoader: loader(ORIGIN),
|
|
510
|
+
contextLoader: mockDocumentLoader,
|
|
511
|
+
gateways: [ORIGIN],
|
|
512
|
+
verifyPortableObject
|
|
513
|
+
};
|
|
514
|
+
const actorUri = (gateway) => `${portableId("/users/alice")}?@gateway=${encodeURIComponent(gateway)}`;
|
|
515
|
+
const actor = await lookupObject(actorUri(ORIGIN), options);
|
|
516
|
+
assert(actor instanceof Person);
|
|
517
|
+
const outbox = await actor.getOutbox(options);
|
|
518
|
+
assert(outbox instanceof OrderedCollection);
|
|
519
|
+
assertEquals((await Array.fromAsync(traverseCollection(outbox, options))).map((item) => item instanceof Create ? formatIri(item.id) : null), PAGES.flatMap((cursor) => [0, 1].map((i) => portableId(`/creates/${getNoteValues("alice", cursor, i).id}`))));
|
|
520
|
+
assertEquals(await actor.getFeatured(options), null);
|
|
521
|
+
const evil = "https://evil.example";
|
|
522
|
+
const evilOptions = {
|
|
523
|
+
...options,
|
|
524
|
+
documentLoader: loader(evil),
|
|
525
|
+
gateways: [evil]
|
|
526
|
+
};
|
|
527
|
+
const actor2 = await lookupObject(actorUri(evil), evilOptions);
|
|
528
|
+
assert(actor2 instanceof Person);
|
|
529
|
+
assertEquals(await actor2.getOutbox(evilOptions), null);
|
|
530
|
+
});
|
|
531
|
+
test("Fedify traverses portable collections through their owners' gateways", async () => {
|
|
532
|
+
const federation = createTestFederation();
|
|
533
|
+
const requests = [];
|
|
534
|
+
const documentLoader = async (url) => {
|
|
535
|
+
requests.push(url);
|
|
536
|
+
if (!/^https?:/.test(url)) throw new Error(`Not found: ${url}`);
|
|
537
|
+
const target = new URL(url);
|
|
538
|
+
const response = await federation.fetch(new Request(new URL(target.pathname + target.search, ORIGIN), { headers: { Accept: ACCEPT } }), { contextData: void 0 });
|
|
539
|
+
if (!response.ok) throw new FetchError(url, `HTTP ${response.status}`, response);
|
|
540
|
+
return {
|
|
541
|
+
contextUrl: null,
|
|
542
|
+
documentUrl: url,
|
|
543
|
+
document: await response.json()
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
const options = {
|
|
547
|
+
documentLoader,
|
|
548
|
+
contextLoader: mockDocumentLoader,
|
|
549
|
+
verifyPortableObject
|
|
550
|
+
};
|
|
551
|
+
const actor = await lookupObject(portableId("/users/alice"), {
|
|
552
|
+
...options,
|
|
553
|
+
gateways: [ORIGIN]
|
|
554
|
+
});
|
|
555
|
+
assert(actor instanceof Person);
|
|
556
|
+
const outbox = await actor.getOutbox(options);
|
|
557
|
+
assert(outbox instanceof OrderedCollection);
|
|
558
|
+
assertEquals((await Array.fromAsync(traverseCollection(outbox, options))).length, PAGES.length * NOTES_PER_PAGE);
|
|
559
|
+
assert(requests.every((url) => url.startsWith(`${ORIGIN}/`)));
|
|
560
|
+
});
|
|
561
|
+
test("Fedify accepts empty portable collections that Fedify serves", async () => {
|
|
562
|
+
const federation = createTestFederation();
|
|
563
|
+
const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/inbox"));
|
|
564
|
+
assertEquals(response.status, 200);
|
|
565
|
+
assertEquals(json?.totalItems, 0);
|
|
566
|
+
assertEquals(await verifyPortableObjectProof(json, { contextLoader: mockDocumentLoader }), {
|
|
567
|
+
verified: false,
|
|
568
|
+
reason: { type: "unsecuredCollection" }
|
|
569
|
+
});
|
|
570
|
+
const documentLoader = async (url) => {
|
|
571
|
+
const target = new URL(url);
|
|
572
|
+
const response = await federation.fetch(new Request(new URL(target.pathname + target.search, ORIGIN), { headers: { Accept: ACCEPT } }), { contextData: void 0 });
|
|
573
|
+
if (!response.ok) throw new FetchError(url, `HTTP ${response.status}`, response);
|
|
574
|
+
return {
|
|
575
|
+
contextUrl: null,
|
|
576
|
+
documentUrl: url,
|
|
577
|
+
document: await response.json()
|
|
578
|
+
};
|
|
579
|
+
};
|
|
580
|
+
const options = {
|
|
581
|
+
documentLoader,
|
|
582
|
+
contextLoader: mockDocumentLoader,
|
|
583
|
+
gateways: [ORIGIN],
|
|
584
|
+
verifyPortableObject
|
|
585
|
+
};
|
|
586
|
+
const actor = await lookupObject(portableId("/users/alice"), options);
|
|
587
|
+
assert(actor instanceof Person);
|
|
588
|
+
const inbox = await actor.getInbox(options);
|
|
589
|
+
assert(inbox instanceof OrderedCollection);
|
|
590
|
+
assertEquals(inbox.totalItems, 0);
|
|
591
|
+
assertEquals(await Array.fromAsync(traverseCollection(inbox, options)), []);
|
|
592
|
+
});
|
|
593
|
+
test("verifyServedPortableObjects() classifies unsigned maps under their effective contexts", async () => {
|
|
594
|
+
const AS = "https://www.w3.org/ns/activitystreams";
|
|
595
|
+
const collection = (member) => ({
|
|
596
|
+
"@context": AS,
|
|
597
|
+
id: portableId("/collection"),
|
|
598
|
+
type: "OrderedCollection",
|
|
599
|
+
orderedItems: [member]
|
|
600
|
+
});
|
|
601
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
602
|
+
id: portableId("/replies"),
|
|
603
|
+
totalItems: 0
|
|
604
|
+
})), { verified: true });
|
|
605
|
+
assertEquals(await verifyServedPortableObjects(collection({ id: portableId("/notes/1") })), { verified: true });
|
|
606
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
607
|
+
id: portableId("/notes/1"),
|
|
608
|
+
content: "Hi"
|
|
609
|
+
})), {
|
|
610
|
+
verified: false,
|
|
611
|
+
path: "/orderedItems/0",
|
|
612
|
+
id: portableId("/notes/1"),
|
|
613
|
+
reason: "missingProof"
|
|
614
|
+
});
|
|
615
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
616
|
+
"@context": { totalItems: `${AS}#content` },
|
|
617
|
+
type: "Note",
|
|
618
|
+
tag: {
|
|
619
|
+
id: portableId("/notes/2"),
|
|
620
|
+
totalItems: "Hi"
|
|
621
|
+
}
|
|
622
|
+
})), {
|
|
623
|
+
verified: false,
|
|
624
|
+
path: "/orderedItems/0/tag",
|
|
625
|
+
id: portableId("/notes/2"),
|
|
626
|
+
reason: "missingProof"
|
|
627
|
+
});
|
|
628
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
629
|
+
"@context": { tag: {
|
|
630
|
+
"@id": `${AS}#tag`,
|
|
631
|
+
"@context": {}
|
|
632
|
+
} },
|
|
633
|
+
type: "Note",
|
|
634
|
+
tag: {
|
|
635
|
+
id: portableId("/replies"),
|
|
636
|
+
totalItems: 0
|
|
637
|
+
}
|
|
638
|
+
})), {
|
|
639
|
+
verified: false,
|
|
640
|
+
path: "/orderedItems/0",
|
|
641
|
+
id: "",
|
|
642
|
+
reason: "unsupportedContext"
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
test("verifyServedPortableObjects() verifies embedded maps as they are served", async () => {
|
|
646
|
+
const AS = "https://www.w3.org/ns/activitystreams";
|
|
647
|
+
const signed = await (await sign(new Note({
|
|
648
|
+
id: parseIri(portableId("/notes/signed")),
|
|
649
|
+
content: "Hi"
|
|
650
|
+
}))).toJsonLd({
|
|
651
|
+
format: "compact",
|
|
652
|
+
contextLoader: mockDocumentLoader
|
|
653
|
+
});
|
|
654
|
+
const collection = (member) => ({
|
|
655
|
+
"@context": AS,
|
|
656
|
+
id: portableId("/collection"),
|
|
657
|
+
type: "OrderedCollection",
|
|
658
|
+
orderedItems: [member]
|
|
659
|
+
});
|
|
660
|
+
assertEquals(await verifyServedPortableObjects(collection(signed), { contextLoader: mockDocumentLoader }), { verified: true });
|
|
661
|
+
const context = signed["@context"];
|
|
662
|
+
assertEquals((await verifyServedPortableObjects(collection({
|
|
663
|
+
...signed,
|
|
664
|
+
"@context": [...Array.isArray(context) ? context : [context], { content: `${AS}#summary` }]
|
|
665
|
+
}), { contextLoader: mockDocumentLoader })).verified, false);
|
|
666
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
667
|
+
"@context": { uri: "@id" },
|
|
668
|
+
uri: portableId("/notes/aliased"),
|
|
669
|
+
type: "Note",
|
|
670
|
+
content: "Unsigned"
|
|
671
|
+
})), {
|
|
672
|
+
verified: false,
|
|
673
|
+
path: "",
|
|
674
|
+
id: portableId("/notes/aliased"),
|
|
675
|
+
reason: "hiddenPortableObject"
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
test("verifyServedPortableObjects() finds portable objects hidden by contexts", async () => {
|
|
679
|
+
const AS = "https://www.w3.org/ns/activitystreams";
|
|
680
|
+
const collection = (member) => ({
|
|
681
|
+
"@context": AS,
|
|
682
|
+
id: portableId("/collection"),
|
|
683
|
+
type: "OrderedCollection",
|
|
684
|
+
orderedItems: [member]
|
|
685
|
+
});
|
|
686
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
687
|
+
"@context": { uri: "id" },
|
|
688
|
+
uri: portableId("/notes/aliased"),
|
|
689
|
+
type: "Note",
|
|
690
|
+
content: "Unsigned"
|
|
691
|
+
})), {
|
|
692
|
+
verified: false,
|
|
693
|
+
path: "",
|
|
694
|
+
id: portableId("/notes/aliased"),
|
|
695
|
+
reason: "hiddenPortableObject"
|
|
696
|
+
});
|
|
697
|
+
for (const decoy of [{
|
|
698
|
+
id: portableId("/notes/decoy"),
|
|
699
|
+
totalItems: 0
|
|
700
|
+
}, null]) {
|
|
701
|
+
const id = decoy == null ? portableId("/collection") : decoy.id;
|
|
702
|
+
const document = collection({
|
|
703
|
+
"@context": { uri: "id" },
|
|
704
|
+
uri: id,
|
|
705
|
+
type: "Note",
|
|
706
|
+
content: "Unsigned"
|
|
707
|
+
});
|
|
708
|
+
if (decoy != null) document.orderedItems.push(decoy);
|
|
709
|
+
assertEquals(await verifyServedPortableObjects(document), {
|
|
710
|
+
verified: false,
|
|
711
|
+
path: "",
|
|
712
|
+
id,
|
|
713
|
+
reason: "hiddenPortableObject"
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
717
|
+
"@context": { proof: `${AS}#tag` },
|
|
718
|
+
id: "https://example.com/notes/ordinary",
|
|
719
|
+
type: "Note",
|
|
720
|
+
proof: {
|
|
721
|
+
id: portableId("/notes/hidden"),
|
|
722
|
+
type: "Note",
|
|
723
|
+
content: "Unsigned"
|
|
724
|
+
}
|
|
725
|
+
})), {
|
|
726
|
+
verified: false,
|
|
727
|
+
path: "",
|
|
728
|
+
id: portableId("/notes/hidden"),
|
|
729
|
+
reason: "hiddenPortableObject"
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
test("verifyServedPortableObjects() leaves non-portable remote objects alone", async () => {
|
|
733
|
+
const AS = "https://www.w3.org/ns/activitystreams";
|
|
734
|
+
const collection = (member) => ({
|
|
735
|
+
"@context": AS,
|
|
736
|
+
id: portableId("/collection"),
|
|
737
|
+
type: "OrderedCollection",
|
|
738
|
+
orderedItems: [member]
|
|
739
|
+
});
|
|
740
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
741
|
+
"@context": [AS, {
|
|
742
|
+
toot: "http://joinmastodon.org/ns#",
|
|
743
|
+
focalPoint: {
|
|
744
|
+
"@container": "@list",
|
|
745
|
+
"@id": "toot:focalPoint"
|
|
746
|
+
}
|
|
747
|
+
}],
|
|
748
|
+
id: "https://mastodon.example/users/bob/statuses/1",
|
|
749
|
+
type: "Note",
|
|
750
|
+
content: `<p>AP: see ${gatewayUrl("/users/alice")}, ap://${did}/users/alice, and did:plc:abcdefghijklmnop</p>`,
|
|
751
|
+
url: gatewayUrl("/users/alice")
|
|
752
|
+
})), { verified: true });
|
|
753
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
754
|
+
"@context": [AS, "https://evil.example/context.jsonld"],
|
|
755
|
+
id: "https://evil.example/activities/1",
|
|
756
|
+
type: "Create",
|
|
757
|
+
object: {
|
|
758
|
+
id: "x:notes/1",
|
|
759
|
+
type: "Note",
|
|
760
|
+
content: "Forged"
|
|
761
|
+
}
|
|
762
|
+
})), {
|
|
763
|
+
verified: false,
|
|
764
|
+
path: "/orderedItems/0",
|
|
765
|
+
id: "https://evil.example/activities/1",
|
|
766
|
+
reason: "unsupportedContext"
|
|
767
|
+
});
|
|
768
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
769
|
+
"@context": [AS, { x: {
|
|
770
|
+
"@id": `ap+ef61://${did}/`,
|
|
771
|
+
"@prefix": true
|
|
772
|
+
} }],
|
|
773
|
+
id: "https://example.com/objects/2",
|
|
774
|
+
type: "Note",
|
|
775
|
+
tag: {
|
|
776
|
+
id: "x:notes/1",
|
|
777
|
+
type: "Note"
|
|
778
|
+
}
|
|
779
|
+
})), {
|
|
780
|
+
verified: false,
|
|
781
|
+
path: "",
|
|
782
|
+
id: portableId("/notes/1"),
|
|
783
|
+
reason: "hiddenPortableObject"
|
|
784
|
+
});
|
|
785
|
+
assertEquals(await verifyServedPortableObjects(collection({
|
|
786
|
+
"@context": [AS, {
|
|
787
|
+
toot: "http://joinmastodon.org/ns#",
|
|
788
|
+
id: "toot:notId",
|
|
789
|
+
focalPoint: {
|
|
790
|
+
"@container": "@list",
|
|
791
|
+
"@id": "toot:focalPoint"
|
|
792
|
+
}
|
|
793
|
+
}],
|
|
794
|
+
id: "https://example.com/objects/3",
|
|
795
|
+
type: "Note",
|
|
796
|
+
tag: {
|
|
797
|
+
id: portableId("/notes/3"),
|
|
798
|
+
type: "Note"
|
|
799
|
+
}
|
|
800
|
+
})), {
|
|
801
|
+
verified: false,
|
|
802
|
+
path: "/orderedItems/0",
|
|
803
|
+
id: "https://example.com/objects/3",
|
|
804
|
+
reason: "unsupportedContext"
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
test("Federation.fetch() authorizes portable custom collection requests", async () => {
|
|
808
|
+
const federation = createFederation({
|
|
809
|
+
kv: new MemoryKvStore(),
|
|
810
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
811
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
812
|
+
});
|
|
813
|
+
const calls = [];
|
|
814
|
+
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new Person({ id: ctx.getPortableActorUri(identifier, did) }));
|
|
815
|
+
federation.setCollectionDispatcher("bookmarks", Note, "/users/{identifier}/bookmarks", (_ctx, values) => {
|
|
816
|
+
calls.push(`dispatch ${values.identifier}`);
|
|
817
|
+
return { items: [] };
|
|
818
|
+
}).mapPortableOwner((_ctx, values) => values.identifier).authorize((ctx, values) => {
|
|
819
|
+
calls.push(`authorize ${values.identifier} ${ctx.portableRequest?.authority}`);
|
|
820
|
+
return ctx.request.headers.get("X-Allowed") === "yes";
|
|
821
|
+
});
|
|
822
|
+
const [denied] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks"));
|
|
823
|
+
assertEquals(denied.status, 401);
|
|
824
|
+
assertEquals(calls, [`authorize alice ${did}`]);
|
|
825
|
+
calls.length = 0;
|
|
826
|
+
const [allowed, json] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks"), { headers: { "X-Allowed": "yes" } });
|
|
827
|
+
assertEquals(allowed.status, 200);
|
|
828
|
+
assertEquals(json?.id, portableId("/users/alice/bookmarks"));
|
|
829
|
+
assertEquals(calls, [`authorize alice ${did}`, "dispatch alice"]);
|
|
830
|
+
});
|
|
831
|
+
//#endregion
|
|
832
|
+
export {};
|