@fedify/vocab 2.4.0-dev.2056 → 2.4.0-dev.2065
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/deno.json +1 -1
- package/dist/mod.cjs +16674 -9026
- package/dist/mod.d.cts +28 -2
- package/dist/mod.d.ts +28 -2
- package/dist/mod.js +16675 -9027
- package/dist-tests/{actor-BgeBgt_8.mjs → actor-B0IFZjgb.mjs} +2 -2
- package/dist-tests/actor.test.mjs +3 -3
- package/dist-tests/{esm-Df0Yo-Tp.mjs → esm-C9nxP5_e.mjs} +1 -1
- package/dist-tests/gateway.test.mjs +695 -7
- package/dist-tests/{lookup-C13R72YU.mjs → lookup-BnzAXNN7.mjs} +48 -49
- package/dist-tests/lookup.test.mjs +3 -3
- package/dist-tests/portable-lookup.test.mjs +100 -4
- package/dist-tests/signed-representation.test.mjs +1 -1
- package/dist-tests/translation.test.mjs +3 -3
- package/dist-tests/type.test.mjs +1 -1
- package/dist-tests/{vocab-BJGPw--L.mjs → vocab-BHtBRusa.mjs} +16631 -8982
- package/dist-tests/vocab.test.mjs +1 -1
- package/package.json +4 -4
- package/src/gateway.test.ts +980 -7
- package/src/lookup.ts +114 -65
- package/src/portable-lookup.test.ts +133 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
|
-
import {
|
|
4
|
-
import { t as esm_default } from "./esm-
|
|
3
|
+
import { F as Person, M as OrderedCollection, c as CollectionPage, k as Note, s as Collection, u as Create } from "./vocab-BHtBRusa.mjs";
|
|
4
|
+
import { t as esm_default } from "./esm-C9nxP5_e.mjs";
|
|
5
|
+
import { n as traverseCollection } from "./lookup-BnzAXNN7.mjs";
|
|
5
6
|
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
6
7
|
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
7
8
|
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
8
|
-
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
9
|
+
import { FetchError, getDocumentLoader, parseIri } from "@fedify/vocab-runtime";
|
|
9
10
|
//#region src/gateway.test.ts
|
|
10
11
|
const did = "did:key:z6Mkabc";
|
|
11
12
|
const objectId = `ap+ef61://${did}/objects/1`;
|
|
@@ -506,11 +507,16 @@ test("getObject() shares context documents within a dereference", async (t) => {
|
|
|
506
507
|
const loaded = [];
|
|
507
508
|
const contextLoader = async (url) => {
|
|
508
509
|
loaded.push(url);
|
|
510
|
+
if (url === contextUrl) return {
|
|
511
|
+
contextUrl: null,
|
|
512
|
+
documentUrl: url,
|
|
513
|
+
document: { "@context": {} }
|
|
514
|
+
};
|
|
509
515
|
return await mockDocumentLoader(url);
|
|
510
516
|
};
|
|
511
517
|
const object = await (await createActivity()).getObject({
|
|
512
518
|
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: {
|
|
513
|
-
...
|
|
519
|
+
...document,
|
|
514
520
|
attributedTo: "https://example.com/person"
|
|
515
521
|
} }),
|
|
516
522
|
contextLoader,
|
|
@@ -518,13 +524,13 @@ test("getObject() shares context documents within a dereference", async (t) => {
|
|
|
518
524
|
verifyPortableObject: createVerifier()
|
|
519
525
|
});
|
|
520
526
|
assertInstanceOf(object, Note);
|
|
521
|
-
deepStrictEqual(loaded, []);
|
|
527
|
+
deepStrictEqual(loaded, [contextUrl]);
|
|
522
528
|
deepStrictEqual((await object.getAttribution({ documentLoader: createLoader({ "https://example.com/person": {
|
|
523
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
529
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
524
530
|
id: "https://example.com/person",
|
|
525
531
|
type: "Person"
|
|
526
532
|
} }) }))?.id, new URL("https://example.com/person"));
|
|
527
|
-
|
|
533
|
+
deepStrictEqual(loaded, [contextUrl, contextUrl]);
|
|
528
534
|
});
|
|
529
535
|
await t.step("failed context loads are retried", async () => {
|
|
530
536
|
let failures = 1;
|
|
@@ -600,5 +606,687 @@ test("getObject() sends the ActivityStreams Accept header to gateways", {
|
|
|
600
606
|
esm_default.hardReset();
|
|
601
607
|
}
|
|
602
608
|
});
|
|
609
|
+
const AS = "https://www.w3.org/ns/activitystreams#";
|
|
610
|
+
function gatewayUrl(gateway, path) {
|
|
611
|
+
return `${gateway}/.well-known/apgateway/${did}${path}`;
|
|
612
|
+
}
|
|
613
|
+
function createRecordingVerifier(decide = () => ({ verified: true })) {
|
|
614
|
+
const calls = [];
|
|
615
|
+
const verifier = async (document, options) => {
|
|
616
|
+
calls.push({
|
|
617
|
+
document,
|
|
618
|
+
options
|
|
619
|
+
});
|
|
620
|
+
return decide(document, options);
|
|
621
|
+
};
|
|
622
|
+
return Object.assign(verifier, { calls });
|
|
623
|
+
}
|
|
624
|
+
function portableCollection(path, extra = {}) {
|
|
625
|
+
return {
|
|
626
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
627
|
+
id: `ap://${did}${path}`,
|
|
628
|
+
type: "OrderedCollection",
|
|
629
|
+
totalItems: 0,
|
|
630
|
+
...extra
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
test("getOutbox() passes the fetch source and referrer to the verifier", async () => {
|
|
634
|
+
const person = await Person.fromJsonLd({
|
|
635
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
636
|
+
id: `ap://${did}/actor`,
|
|
637
|
+
type: "Person",
|
|
638
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
639
|
+
outbox: `ap://${did}/actor/outbox?@gateway=${encodeURIComponent("https://hint.example")}`
|
|
640
|
+
}, { contextLoader: mockDocumentLoader });
|
|
641
|
+
const documentLoader = createLoader({ [gatewayUrl("https://hint.example", "/actor/outbox")]: portableCollection("/actor/outbox") });
|
|
642
|
+
const verifier = createRecordingVerifier();
|
|
643
|
+
assertInstanceOf(await person.getOutbox({
|
|
644
|
+
documentLoader,
|
|
645
|
+
contextLoader: mockDocumentLoader,
|
|
646
|
+
verifyPortableObject: verifier
|
|
647
|
+
}), Collection);
|
|
648
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
649
|
+
const { options } = verifier.calls[0];
|
|
650
|
+
deepStrictEqual(options.documentUrl, new URL(gatewayUrl("https://hint.example", "/actor/outbox")));
|
|
651
|
+
deepStrictEqual(options.gateways, void 0);
|
|
652
|
+
deepStrictEqual(options.gatewayHints, [new URL("https://hint.example")]);
|
|
653
|
+
ok(options.referrer?.object === person);
|
|
654
|
+
deepStrictEqual(options.referrer?.property, `${AS}outbox`);
|
|
655
|
+
deepStrictEqual(options.referrer?.acceptance, void 0);
|
|
656
|
+
const person2 = await Person.fromJsonLd({
|
|
657
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
658
|
+
id: `ap://${did}/actor`,
|
|
659
|
+
type: "Person",
|
|
660
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
661
|
+
outbox: `ap://${did}/actor/outbox`
|
|
662
|
+
}, { contextLoader: mockDocumentLoader });
|
|
663
|
+
const verifier2 = createRecordingVerifier();
|
|
664
|
+
await person2.getOutbox({
|
|
665
|
+
documentLoader: createLoader({ [gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox") }),
|
|
666
|
+
contextLoader: mockDocumentLoader,
|
|
667
|
+
gateways: ["https://gw.example"],
|
|
668
|
+
verifyPortableObject: verifier2
|
|
669
|
+
});
|
|
670
|
+
deepStrictEqual(verifier2.calls[0].options.gateways, [new URL("https://gw.example")]);
|
|
671
|
+
deepStrictEqual(verifier2.calls[0].options.gatewayHints, void 0);
|
|
672
|
+
});
|
|
673
|
+
test("getFirst() passes the referrer chain and collection context", async () => {
|
|
674
|
+
const context = Object.freeze({});
|
|
675
|
+
const documentLoader = createLoader({
|
|
676
|
+
[gatewayUrl("https://gw.example", "/actor")]: {
|
|
677
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
678
|
+
id: `ap://${did}/actor`,
|
|
679
|
+
type: "Person",
|
|
680
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
681
|
+
outbox: `ap://${did}/actor/outbox`
|
|
682
|
+
},
|
|
683
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox", { first: {
|
|
684
|
+
id: `ap://${did}/actor/outbox?page=1`,
|
|
685
|
+
type: "OrderedCollectionPage",
|
|
686
|
+
next: `ap://${did}/actor/outbox?page=2`
|
|
687
|
+
} }),
|
|
688
|
+
[gatewayUrl("https://gw.example", "/actor/outbox?page=2")]: {
|
|
689
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
690
|
+
id: `ap://${did}/actor/outbox?page=2`,
|
|
691
|
+
type: "OrderedCollectionPage",
|
|
692
|
+
orderedItems: []
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
const verifier = createRecordingVerifier((document) => document.type === "OrderedCollection" ? {
|
|
696
|
+
verified: true,
|
|
697
|
+
collectionContext: context
|
|
698
|
+
} : { verified: true });
|
|
699
|
+
const options = {
|
|
700
|
+
documentLoader,
|
|
701
|
+
contextLoader: mockDocumentLoader,
|
|
702
|
+
gateways: ["https://gw.example"],
|
|
703
|
+
verifyPortableObject: verifier
|
|
704
|
+
};
|
|
705
|
+
const create = await createActivity(`ap://${did}/actor`);
|
|
706
|
+
const actor = await create.getObject(options);
|
|
707
|
+
assertInstanceOf(actor, Person);
|
|
708
|
+
const outbox = await actor.getOutbox(options);
|
|
709
|
+
assertInstanceOf(outbox, Collection);
|
|
710
|
+
const first = await outbox.getFirst(options);
|
|
711
|
+
assertInstanceOf(first, CollectionPage);
|
|
712
|
+
assertInstanceOf(await first.getNext(options), CollectionPage);
|
|
713
|
+
const { referrer } = verifier.calls[verifier.calls.length - 1].options;
|
|
714
|
+
ok(referrer?.object === first);
|
|
715
|
+
deepStrictEqual(referrer?.property, `${AS}next`);
|
|
716
|
+
deepStrictEqual(referrer?.acceptance, void 0);
|
|
717
|
+
ok(referrer?.referrer?.object === outbox);
|
|
718
|
+
deepStrictEqual(referrer?.referrer?.property, `${AS}first`);
|
|
719
|
+
deepStrictEqual(referrer?.referrer?.acceptance, "verified");
|
|
720
|
+
ok(referrer?.referrer?.collectionContext === context);
|
|
721
|
+
ok(referrer?.referrer?.referrer?.object === actor);
|
|
722
|
+
deepStrictEqual(referrer?.referrer?.referrer?.property, `${AS}outbox`);
|
|
723
|
+
deepStrictEqual(referrer?.referrer?.referrer?.acceptance, "verified");
|
|
724
|
+
ok(referrer?.referrer?.referrer?.referrer?.object === create);
|
|
725
|
+
});
|
|
726
|
+
test("accessors do not trust objects embedded in unsecured collections", async () => {
|
|
727
|
+
const itemPath = "/objects/1";
|
|
728
|
+
const options = {
|
|
729
|
+
documentLoader: createLoader({
|
|
730
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox", { orderedItems: [{
|
|
731
|
+
id: `ap://${did}${itemPath}`,
|
|
732
|
+
type: "Note",
|
|
733
|
+
content: "Forged"
|
|
734
|
+
}, {
|
|
735
|
+
type: "Note",
|
|
736
|
+
content: "Anonymous"
|
|
737
|
+
}] }),
|
|
738
|
+
[gatewayUrl("https://gw.example", itemPath)]: note()
|
|
739
|
+
}),
|
|
740
|
+
contextLoader: mockDocumentLoader,
|
|
741
|
+
gateways: ["https://gw.example"],
|
|
742
|
+
verifyPortableObject: createRecordingVerifier((document) => document.type === "OrderedCollection" ? {
|
|
743
|
+
verified: true,
|
|
744
|
+
unsecured: true
|
|
745
|
+
} : { verified: true })
|
|
746
|
+
};
|
|
747
|
+
const createPerson = () => Person.fromJsonLd({
|
|
748
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
749
|
+
id: `ap://${did}/actor`,
|
|
750
|
+
type: "Person",
|
|
751
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
752
|
+
outbox: `ap://${did}/actor/outbox`
|
|
753
|
+
}, { contextLoader: mockDocumentLoader });
|
|
754
|
+
const outbox = await (await createPerson()).getOutbox(options);
|
|
755
|
+
assertInstanceOf(outbox, Collection);
|
|
756
|
+
deepStrictEqual((await Array.fromAsync(outbox.getItems(options))).map((item) => item instanceof Note ? item.content : null), ["Portable note"]);
|
|
757
|
+
const outbox2 = await (await createPerson()).getOutbox(options);
|
|
758
|
+
assertInstanceOf(outbox2, Collection);
|
|
759
|
+
deepStrictEqual((await Array.fromAsync(outbox2.getItems({
|
|
760
|
+
...options,
|
|
761
|
+
crossOrigin: "trust"
|
|
762
|
+
}))).length, 1);
|
|
763
|
+
const outbox3 = await (await createPerson()).getOutbox(options);
|
|
764
|
+
assertInstanceOf(outbox3, Collection);
|
|
765
|
+
deepStrictEqual(await Array.fromAsync(outbox3.clone().getItems({
|
|
766
|
+
...options,
|
|
767
|
+
documentLoader: createLoader({}),
|
|
768
|
+
suppressError: true
|
|
769
|
+
})), []);
|
|
770
|
+
});
|
|
771
|
+
function createRedirectingLoader(responses) {
|
|
772
|
+
const fetched = [];
|
|
773
|
+
const loader = async (url) => {
|
|
774
|
+
fetched.push(url);
|
|
775
|
+
const response = responses[url];
|
|
776
|
+
if (response == null) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
|
|
777
|
+
return {
|
|
778
|
+
contextUrl: null,
|
|
779
|
+
...response
|
|
780
|
+
};
|
|
781
|
+
};
|
|
782
|
+
return Object.assign(loader, { fetched });
|
|
783
|
+
}
|
|
784
|
+
function createHttpActivity(object) {
|
|
785
|
+
return Create.fromJsonLd({
|
|
786
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
787
|
+
type: "Create",
|
|
788
|
+
id: "https://example.com/activities/1",
|
|
789
|
+
object
|
|
790
|
+
}, { contextLoader: mockDocumentLoader });
|
|
791
|
+
}
|
|
792
|
+
const compatibleNoteId = gatewayUrl("https://gw.example", "/objects/1");
|
|
793
|
+
test("accessors dereference compatible identifiers through their gateways", async (t) => {
|
|
794
|
+
await t.step("through the named gateway", async () => {
|
|
795
|
+
const documentLoader = createLoader({ [compatibleNoteId]: note() });
|
|
796
|
+
const verifier = createRecordingVerifier();
|
|
797
|
+
const activity = await createHttpActivity(compatibleNoteId);
|
|
798
|
+
const object = await activity.getObject({
|
|
799
|
+
documentLoader,
|
|
800
|
+
contextLoader: mockDocumentLoader,
|
|
801
|
+
verifyPortableObject: verifier
|
|
802
|
+
});
|
|
803
|
+
assertInstanceOf(object, Note);
|
|
804
|
+
deepStrictEqual(object.id, parseIri(objectId));
|
|
805
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
|
|
806
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
807
|
+
const { options } = verifier.calls[0];
|
|
808
|
+
deepStrictEqual(options.documentUrl, new URL(compatibleNoteId));
|
|
809
|
+
deepStrictEqual(options.gateways, void 0);
|
|
810
|
+
deepStrictEqual(options.gatewayHints, [new URL("https://gw.example")]);
|
|
811
|
+
ok(options.referrer?.object === activity);
|
|
812
|
+
ok(await activity.getObject({ documentLoader: createLoader({}) }) === object);
|
|
813
|
+
});
|
|
814
|
+
await t.step("then through explicit gateways", async () => {
|
|
815
|
+
const documentLoader = createLoader({ [gatewayUrl("https://gw2.example", "/objects/1")]: note() });
|
|
816
|
+
const verifier = createRecordingVerifier();
|
|
817
|
+
assertInstanceOf(await (await createHttpActivity(compatibleNoteId)).getObject({
|
|
818
|
+
documentLoader,
|
|
819
|
+
contextLoader: mockDocumentLoader,
|
|
820
|
+
gateways: ["https://gw2.example", "https://gw.example"],
|
|
821
|
+
verifyPortableObject: verifier
|
|
822
|
+
}), Note);
|
|
823
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId, gatewayUrl("https://gw2.example", "/objects/1")]);
|
|
824
|
+
deepStrictEqual(verifier.calls[0].options.gateways, [new URL("https://gw2.example"), new URL("https://gw.example")]);
|
|
825
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, void 0);
|
|
826
|
+
const documentLoader2 = createLoader({ [compatibleNoteId]: note() });
|
|
827
|
+
assertInstanceOf(await (await createHttpActivity(compatibleNoteId)).getObject({
|
|
828
|
+
documentLoader: documentLoader2,
|
|
829
|
+
contextLoader: mockDocumentLoader,
|
|
830
|
+
gateways: [],
|
|
831
|
+
verifyPortableObject: createVerifier()
|
|
832
|
+
}), Note);
|
|
833
|
+
deepStrictEqual(documentLoader2.fetched, [compatibleNoteId]);
|
|
834
|
+
});
|
|
835
|
+
await t.step("in plural accessors", async () => {
|
|
836
|
+
const documentLoader = createLoader({ [compatibleNoteId]: note() });
|
|
837
|
+
deepStrictEqual((await Array.fromAsync((await createHttpActivity([compatibleNoteId])).getObjects({
|
|
838
|
+
documentLoader,
|
|
839
|
+
contextLoader: mockDocumentLoader,
|
|
840
|
+
verifyPortableObject: createVerifier()
|
|
841
|
+
}))).map((o) => o.id), [parseIri(objectId)]);
|
|
842
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
test("accessors reject forged objects at compatible identifiers", async () => {
|
|
846
|
+
const forgedId = gatewayUrl("https://evil.example", "/objects/1");
|
|
847
|
+
const forged = {
|
|
848
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
849
|
+
id: forgedId,
|
|
850
|
+
type: "Note",
|
|
851
|
+
attributedTo: gatewayUrl("https://evil.example", "/actor"),
|
|
852
|
+
content: "Forged"
|
|
853
|
+
};
|
|
854
|
+
for (const crossOrigin of [
|
|
855
|
+
void 0,
|
|
856
|
+
"ignore",
|
|
857
|
+
"trust"
|
|
858
|
+
]) {
|
|
859
|
+
const verifier = createRecordingVerifier();
|
|
860
|
+
deepStrictEqual(await (await createHttpActivity(forgedId)).getObject({
|
|
861
|
+
documentLoader: createLoader({ [forgedId]: forged }),
|
|
862
|
+
contextLoader: mockDocumentLoader,
|
|
863
|
+
verifyPortableObject: verifier,
|
|
864
|
+
crossOrigin
|
|
865
|
+
}), null);
|
|
866
|
+
deepStrictEqual(verifier.calls, []);
|
|
867
|
+
deepStrictEqual(await (await createHttpActivity(forgedId)).getObject({
|
|
868
|
+
documentLoader: createLoader({ [forgedId]: note() }),
|
|
869
|
+
contextLoader: mockDocumentLoader,
|
|
870
|
+
verifyPortableObject: createVerifier(false),
|
|
871
|
+
crossOrigin
|
|
872
|
+
}), null);
|
|
873
|
+
}
|
|
874
|
+
await rejects(async () => await (await createHttpActivity(forgedId)).getObject({
|
|
875
|
+
documentLoader: createLoader({ [forgedId]: note() }),
|
|
876
|
+
contextLoader: mockDocumentLoader,
|
|
877
|
+
verifyPortableObject: createVerifier(false),
|
|
878
|
+
crossOrigin: "throw"
|
|
879
|
+
}), /No gateway returned a valid portable object/);
|
|
880
|
+
});
|
|
881
|
+
test("accessors reject malformed compatible identifiers", async () => {
|
|
882
|
+
const malformed = [`${compatibleNoteId}?@gateway=https%3A%2F%2Fgw2.example`, compatibleNoteId.replace("https://", "https://user:pass@")];
|
|
883
|
+
for (const url of malformed) {
|
|
884
|
+
const documentLoader = createLoader({ [url]: note() });
|
|
885
|
+
const verifier = createRecordingVerifier();
|
|
886
|
+
const activity = await createHttpActivity(url);
|
|
887
|
+
deepStrictEqual(await activity.getObject({
|
|
888
|
+
documentLoader,
|
|
889
|
+
contextLoader: mockDocumentLoader,
|
|
890
|
+
verifyPortableObject: verifier
|
|
891
|
+
}), null);
|
|
892
|
+
deepStrictEqual(await activity.getObject({
|
|
893
|
+
documentLoader,
|
|
894
|
+
contextLoader: mockDocumentLoader,
|
|
895
|
+
verifyPortableObject: verifier,
|
|
896
|
+
crossOrigin: "throw",
|
|
897
|
+
suppressError: true
|
|
898
|
+
}), null);
|
|
899
|
+
await rejects(async () => await activity.getObject({
|
|
900
|
+
documentLoader,
|
|
901
|
+
contextLoader: mockDocumentLoader,
|
|
902
|
+
verifyPortableObject: verifier,
|
|
903
|
+
crossOrigin: "throw"
|
|
904
|
+
}), /malformed FEP-ef61 compatible identifier/);
|
|
905
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
906
|
+
deepStrictEqual(verifier.calls, []);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
test("accessors keep fetching compatible identifiers as HTTP(S) URLs without verifyPortableObject", async (t) => {
|
|
910
|
+
const document = {
|
|
911
|
+
...note(),
|
|
912
|
+
id: compatibleNoteId
|
|
913
|
+
};
|
|
914
|
+
await t.step("singular accessors", async () => {
|
|
915
|
+
const documentLoader = createLoader({ [compatibleNoteId]: document });
|
|
916
|
+
const activity = await createHttpActivity(compatibleNoteId);
|
|
917
|
+
const object = await activity.getObject({
|
|
918
|
+
documentLoader,
|
|
919
|
+
contextLoader: mockDocumentLoader
|
|
920
|
+
});
|
|
921
|
+
assertInstanceOf(object, Note);
|
|
922
|
+
deepStrictEqual(object.id, new URL(compatibleNoteId));
|
|
923
|
+
deepStrictEqual(await activity.getObject({
|
|
924
|
+
documentLoader,
|
|
925
|
+
contextLoader: mockDocumentLoader,
|
|
926
|
+
verifyPortableObject: createVerifier()
|
|
927
|
+
}), null);
|
|
928
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId, compatibleNoteId]);
|
|
929
|
+
});
|
|
930
|
+
await t.step("plural accessors", async () => {
|
|
931
|
+
const documentLoader = createLoader({ [compatibleNoteId]: document });
|
|
932
|
+
const activity = await createHttpActivity([compatibleNoteId]);
|
|
933
|
+
deepStrictEqual((await Array.fromAsync(activity.getObjects({
|
|
934
|
+
documentLoader,
|
|
935
|
+
contextLoader: mockDocumentLoader
|
|
936
|
+
}))).map((o) => o.id), [new URL(compatibleNoteId)]);
|
|
937
|
+
deepStrictEqual(await Array.fromAsync(activity.getObjects({
|
|
938
|
+
documentLoader,
|
|
939
|
+
contextLoader: mockDocumentLoader,
|
|
940
|
+
verifyPortableObject: createVerifier()
|
|
941
|
+
})), []);
|
|
942
|
+
});
|
|
943
|
+
await t.step("redirects to ordinary objects", async () => {
|
|
944
|
+
const plainUrl = "https://gw.example/notes/1";
|
|
945
|
+
const documentLoader = createRedirectingLoader({ [compatibleNoteId]: {
|
|
946
|
+
documentUrl: plainUrl,
|
|
947
|
+
document: {
|
|
948
|
+
...note(),
|
|
949
|
+
id: plainUrl
|
|
950
|
+
}
|
|
951
|
+
} });
|
|
952
|
+
for (const activity of [await createHttpActivity(compatibleNoteId)]) {
|
|
953
|
+
deepStrictEqual((await activity.getObject({
|
|
954
|
+
documentLoader,
|
|
955
|
+
contextLoader: mockDocumentLoader
|
|
956
|
+
}))?.id, new URL(plainUrl));
|
|
957
|
+
deepStrictEqual(await activity.getObject({
|
|
958
|
+
documentLoader,
|
|
959
|
+
contextLoader: mockDocumentLoader,
|
|
960
|
+
verifyPortableObject: createVerifier()
|
|
961
|
+
}), null);
|
|
962
|
+
}
|
|
963
|
+
const activity = await createHttpActivity([compatibleNoteId]);
|
|
964
|
+
deepStrictEqual((await Array.fromAsync(activity.getObjects({
|
|
965
|
+
documentLoader,
|
|
966
|
+
contextLoader: mockDocumentLoader
|
|
967
|
+
}))).length, 1);
|
|
968
|
+
deepStrictEqual(await Array.fromAsync(activity.getObjects({
|
|
969
|
+
documentLoader,
|
|
970
|
+
contextLoader: mockDocumentLoader,
|
|
971
|
+
verifyPortableObject: createVerifier()
|
|
972
|
+
})), []);
|
|
973
|
+
});
|
|
974
|
+
await t.step("embedded objects", async () => {
|
|
975
|
+
deepStrictEqual((await (await createHttpActivity({
|
|
976
|
+
type: "Note",
|
|
977
|
+
id: gatewayUrl("https://example.com", "/objects/1"),
|
|
978
|
+
content: "Embedded"
|
|
979
|
+
})).getObject({
|
|
980
|
+
documentLoader: createLoader({}),
|
|
981
|
+
contextLoader: mockDocumentLoader
|
|
982
|
+
}))?.id, new URL(gatewayUrl("https://example.com", "/objects/1")));
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
test("accessors dereference compatible identifiers from portable objects", async () => {
|
|
986
|
+
const compatibleOutbox = gatewayUrl("https://gw.example", "/actor/outbox");
|
|
987
|
+
const person = (outbox) => Person.fromJsonLd({
|
|
988
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
989
|
+
id: `ap://${did}/actor`,
|
|
990
|
+
type: "Person",
|
|
991
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
992
|
+
outbox
|
|
993
|
+
}, { contextLoader: mockDocumentLoader });
|
|
994
|
+
const documentLoader = createRedirectingLoader({
|
|
995
|
+
[compatibleOutbox]: {
|
|
996
|
+
documentUrl: compatibleOutbox,
|
|
997
|
+
document: portableCollection("/actor/outbox")
|
|
998
|
+
},
|
|
999
|
+
"https://example.com/outbox": {
|
|
1000
|
+
documentUrl: compatibleOutbox,
|
|
1001
|
+
document: portableCollection("/actor/outbox")
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
for (const crossOrigin of [void 0, "trust"]) for (const outbox of [
|
|
1005
|
+
compatibleOutbox,
|
|
1006
|
+
"https://example.com/outbox",
|
|
1007
|
+
{
|
|
1008
|
+
...portableCollection("/actor/outbox"),
|
|
1009
|
+
id: compatibleOutbox
|
|
1010
|
+
}
|
|
1011
|
+
]) {
|
|
1012
|
+
const verifier = createRecordingVerifier();
|
|
1013
|
+
const collection = await (await person(outbox)).getOutbox({
|
|
1014
|
+
documentLoader,
|
|
1015
|
+
contextLoader: mockDocumentLoader,
|
|
1016
|
+
verifyPortableObject: verifier,
|
|
1017
|
+
crossOrigin
|
|
1018
|
+
});
|
|
1019
|
+
assertInstanceOf(collection, OrderedCollection);
|
|
1020
|
+
deepStrictEqual(collection.id, parseIri(`ap://${did}/actor/outbox`));
|
|
1021
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1022
|
+
deepStrictEqual(verifier.calls[0].options.documentUrl, new URL(compatibleOutbox));
|
|
1023
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, [new URL("https://gw.example")]);
|
|
1024
|
+
}
|
|
1025
|
+
await rejects(async () => await (await person(compatibleOutbox)).getOutbox({
|
|
1026
|
+
documentLoader,
|
|
1027
|
+
contextLoader: mockDocumentLoader
|
|
1028
|
+
}), TypeError);
|
|
1029
|
+
deepStrictEqual(await (await person(compatibleOutbox)).getOutbox({
|
|
1030
|
+
documentLoader,
|
|
1031
|
+
contextLoader: mockDocumentLoader,
|
|
1032
|
+
suppressError: true
|
|
1033
|
+
}), null);
|
|
1034
|
+
});
|
|
1035
|
+
test("accessors verify fetched documents that stand for portable objects", async (t) => {
|
|
1036
|
+
const plainUrl = "https://example.com/notes/1";
|
|
1037
|
+
await t.step("a redirect to a compatible identifier", async () => {
|
|
1038
|
+
const documentLoader = createRedirectingLoader({ [plainUrl]: {
|
|
1039
|
+
documentUrl: compatibleNoteId,
|
|
1040
|
+
document: note()
|
|
1041
|
+
} });
|
|
1042
|
+
const verifier = createRecordingVerifier();
|
|
1043
|
+
const object = await (await createHttpActivity(plainUrl)).getObject({
|
|
1044
|
+
documentLoader,
|
|
1045
|
+
contextLoader: mockDocumentLoader,
|
|
1046
|
+
verifyPortableObject: verifier
|
|
1047
|
+
});
|
|
1048
|
+
assertInstanceOf(object, Note);
|
|
1049
|
+
deepStrictEqual(object.id, parseIri(objectId));
|
|
1050
|
+
deepStrictEqual(documentLoader.fetched, [plainUrl]);
|
|
1051
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1052
|
+
deepStrictEqual(verifier.calls[0].options.documentUrl, new URL(compatibleNoteId));
|
|
1053
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, [new URL("https://gw.example")]);
|
|
1054
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1055
|
+
documentLoader,
|
|
1056
|
+
contextLoader: mockDocumentLoader,
|
|
1057
|
+
verifyPortableObject: createVerifier(false)
|
|
1058
|
+
}), null);
|
|
1059
|
+
await rejects(async () => await (await createHttpActivity(plainUrl)).getObject({
|
|
1060
|
+
documentLoader,
|
|
1061
|
+
contextLoader: mockDocumentLoader,
|
|
1062
|
+
verifyPortableObject: createVerifier(false),
|
|
1063
|
+
crossOrigin: "throw"
|
|
1064
|
+
}), /No gateway returned a valid portable object/);
|
|
1065
|
+
});
|
|
1066
|
+
await t.step("a document that claims another object", async () => {
|
|
1067
|
+
for (const id of [
|
|
1068
|
+
`ap://${did}/objects/2`,
|
|
1069
|
+
compatibleNoteId,
|
|
1070
|
+
"https://gw.example/notes/1"
|
|
1071
|
+
]) {
|
|
1072
|
+
const verifier = createRecordingVerifier();
|
|
1073
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1074
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1075
|
+
documentUrl: compatibleNoteId,
|
|
1076
|
+
document: note(id)
|
|
1077
|
+
} }),
|
|
1078
|
+
contextLoader: mockDocumentLoader,
|
|
1079
|
+
verifyPortableObject: verifier
|
|
1080
|
+
}), null);
|
|
1081
|
+
deepStrictEqual(verifier.calls, []);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
await t.step("a portable @id", async () => {
|
|
1085
|
+
const verifier = createRecordingVerifier();
|
|
1086
|
+
assertInstanceOf(await (await createHttpActivity(plainUrl)).getObject({
|
|
1087
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1088
|
+
contextLoader: mockDocumentLoader,
|
|
1089
|
+
verifyPortableObject: verifier,
|
|
1090
|
+
crossOrigin: "trust"
|
|
1091
|
+
}), Note);
|
|
1092
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1093
|
+
deepStrictEqual(verifier.calls[0].options.documentUrl, new URL(plainUrl));
|
|
1094
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1095
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1096
|
+
contextLoader: mockDocumentLoader,
|
|
1097
|
+
verifyPortableObject: createVerifier(false),
|
|
1098
|
+
crossOrigin: "trust"
|
|
1099
|
+
}), null);
|
|
1100
|
+
});
|
|
1101
|
+
await t.step("a compatible @id", async () => {
|
|
1102
|
+
const verifier = createRecordingVerifier();
|
|
1103
|
+
const url = "https://gw.example/notes/1";
|
|
1104
|
+
deepStrictEqual(await (await createHttpActivity(url)).getObject({
|
|
1105
|
+
documentLoader: createLoader({ [url]: note(compatibleNoteId) }),
|
|
1106
|
+
contextLoader: mockDocumentLoader,
|
|
1107
|
+
verifyPortableObject: verifier
|
|
1108
|
+
}), null);
|
|
1109
|
+
deepStrictEqual(verifier.calls, []);
|
|
1110
|
+
});
|
|
1111
|
+
await t.step("a portable final URL", async () => {
|
|
1112
|
+
for (const [id, expected] of [[objectId, true], [`ap://${did}/objects/2`, false]]) deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1113
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1114
|
+
documentUrl: `ap://${did}/objects/1`,
|
|
1115
|
+
document: note(id)
|
|
1116
|
+
} }),
|
|
1117
|
+
contextLoader: mockDocumentLoader,
|
|
1118
|
+
verifyPortableObject: createVerifier()
|
|
1119
|
+
}) instanceof Note, expected);
|
|
1120
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1121
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1122
|
+
documentUrl: `ap://${did}/objects/x/../1`,
|
|
1123
|
+
document: note()
|
|
1124
|
+
} }),
|
|
1125
|
+
contextLoader: mockDocumentLoader,
|
|
1126
|
+
verifyPortableObject: createVerifier()
|
|
1127
|
+
}), null);
|
|
1128
|
+
});
|
|
1129
|
+
await t.step("a malformed compatible final URL", async () => {
|
|
1130
|
+
const malformed = `${compatibleNoteId}?@gateway=https%3A%2F%2Fgw2.example`;
|
|
1131
|
+
const documentLoader = createRedirectingLoader({ [plainUrl]: {
|
|
1132
|
+
documentUrl: malformed,
|
|
1133
|
+
document: note()
|
|
1134
|
+
} });
|
|
1135
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1136
|
+
documentLoader,
|
|
1137
|
+
contextLoader: mockDocumentLoader,
|
|
1138
|
+
verifyPortableObject: createVerifier()
|
|
1139
|
+
}), null);
|
|
1140
|
+
await rejects(async () => await (await createHttpActivity(plainUrl)).getObject({
|
|
1141
|
+
documentLoader,
|
|
1142
|
+
contextLoader: mockDocumentLoader,
|
|
1143
|
+
verifyPortableObject: createVerifier(),
|
|
1144
|
+
crossOrigin: "throw"
|
|
1145
|
+
}), /malformed FEP-ef61 compatible identifier/);
|
|
1146
|
+
});
|
|
1147
|
+
await t.step("context documents are shared", async () => {
|
|
1148
|
+
const contextUrl = "https://example.com/portable-context";
|
|
1149
|
+
let version = 0;
|
|
1150
|
+
const contextLoader = async (url) => {
|
|
1151
|
+
if (url === contextUrl) {
|
|
1152
|
+
version++;
|
|
1153
|
+
return {
|
|
1154
|
+
contextUrl: null,
|
|
1155
|
+
documentUrl: url,
|
|
1156
|
+
document: { "@context": { version: `urn:version:${version}` } }
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return await mockDocumentLoader(url);
|
|
1160
|
+
};
|
|
1161
|
+
const contexts = [];
|
|
1162
|
+
assertInstanceOf(await (await createHttpActivity(plainUrl)).getObject({
|
|
1163
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1164
|
+
documentUrl: compatibleNoteId,
|
|
1165
|
+
document: {
|
|
1166
|
+
...note(),
|
|
1167
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl]
|
|
1168
|
+
}
|
|
1169
|
+
} }),
|
|
1170
|
+
contextLoader,
|
|
1171
|
+
verifyPortableObject: async (_, { contextLoader }) => {
|
|
1172
|
+
contexts.push((await contextLoader(contextUrl)).document);
|
|
1173
|
+
return { verified: true };
|
|
1174
|
+
}
|
|
1175
|
+
}), Note);
|
|
1176
|
+
deepStrictEqual(version, 1);
|
|
1177
|
+
deepStrictEqual(contexts, [{ "@context": { version: "urn:version:1" } }]);
|
|
1178
|
+
});
|
|
1179
|
+
await t.step("without verifyPortableObject", async () => {
|
|
1180
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1181
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1182
|
+
contextLoader: mockDocumentLoader
|
|
1183
|
+
}), null);
|
|
1184
|
+
assertInstanceOf(await (await createHttpActivity(plainUrl)).getObject({
|
|
1185
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1186
|
+
documentUrl: gatewayUrl("https://example.com", "/objects/1"),
|
|
1187
|
+
document: note(gatewayUrl("https://example.com", "/objects/1"))
|
|
1188
|
+
} }),
|
|
1189
|
+
contextLoader: mockDocumentLoader
|
|
1190
|
+
}), Note);
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
test("accessors verify embedded objects that stand for portable objects", async () => {
|
|
1194
|
+
const forgedId = gatewayUrl("https://example.com", "/objects/1");
|
|
1195
|
+
const forged = {
|
|
1196
|
+
type: "Note",
|
|
1197
|
+
id: forgedId,
|
|
1198
|
+
content: "Forged"
|
|
1199
|
+
};
|
|
1200
|
+
for (const crossOrigin of [void 0, "trust"]) {
|
|
1201
|
+
const documentLoader = createLoader({ [forgedId]: note() });
|
|
1202
|
+
const verifier = createRecordingVerifier(() => ({ verified: false }));
|
|
1203
|
+
deepStrictEqual(await (await createHttpActivity(forged)).getObject({
|
|
1204
|
+
documentLoader,
|
|
1205
|
+
contextLoader: mockDocumentLoader,
|
|
1206
|
+
verifyPortableObject: verifier,
|
|
1207
|
+
crossOrigin
|
|
1208
|
+
}), null);
|
|
1209
|
+
deepStrictEqual(documentLoader.fetched, [forgedId]);
|
|
1210
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1211
|
+
deepStrictEqual(await Array.fromAsync((await createHttpActivity([forged])).getObjects({
|
|
1212
|
+
documentLoader,
|
|
1213
|
+
contextLoader: mockDocumentLoader,
|
|
1214
|
+
verifyPortableObject: verifier,
|
|
1215
|
+
crossOrigin
|
|
1216
|
+
})), []);
|
|
1217
|
+
const documentLoader2 = createLoader({ [gatewayUrl("https://gw.example", "/objects/1")]: note() });
|
|
1218
|
+
const verifier2 = createRecordingVerifier();
|
|
1219
|
+
const object = await (await createHttpActivity({
|
|
1220
|
+
type: "Note",
|
|
1221
|
+
id: objectId,
|
|
1222
|
+
content: "Forged"
|
|
1223
|
+
})).getObject({
|
|
1224
|
+
documentLoader: documentLoader2,
|
|
1225
|
+
contextLoader: mockDocumentLoader,
|
|
1226
|
+
gateways: ["https://gw.example"],
|
|
1227
|
+
verifyPortableObject: verifier2,
|
|
1228
|
+
crossOrigin
|
|
1229
|
+
});
|
|
1230
|
+
assertInstanceOf(object, Note);
|
|
1231
|
+
deepStrictEqual(object.content, "Portable note");
|
|
1232
|
+
deepStrictEqual(verifier2.calls.length, 1);
|
|
1233
|
+
}
|
|
1234
|
+
const otherDid = "did:key:z6Mkother";
|
|
1235
|
+
const otherId = `ap://${otherDid}/objects/1`;
|
|
1236
|
+
const documentLoader = createLoader({ [`https://gw.example/.well-known/apgateway/${otherDid}/objects/1`]: note(otherId) });
|
|
1237
|
+
const verifier = createRecordingVerifier();
|
|
1238
|
+
const crossDid = await (await createActivity({
|
|
1239
|
+
type: "Note",
|
|
1240
|
+
id: otherId,
|
|
1241
|
+
content: "Forged"
|
|
1242
|
+
})).getObject({
|
|
1243
|
+
documentLoader,
|
|
1244
|
+
contextLoader: mockDocumentLoader,
|
|
1245
|
+
gateways: ["https://gw.example"],
|
|
1246
|
+
verifyPortableObject: verifier,
|
|
1247
|
+
crossOrigin: "trust"
|
|
1248
|
+
});
|
|
1249
|
+
assertInstanceOf(crossDid, Note);
|
|
1250
|
+
deepStrictEqual(crossDid.content, "Portable note");
|
|
1251
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1252
|
+
const object = await (await createActivity({
|
|
1253
|
+
type: "Note",
|
|
1254
|
+
id: objectId,
|
|
1255
|
+
content: "Embedded"
|
|
1256
|
+
})).getObject({
|
|
1257
|
+
documentLoader: createLoader({}),
|
|
1258
|
+
contextLoader: mockDocumentLoader,
|
|
1259
|
+
verifyPortableObject: createVerifier(false)
|
|
1260
|
+
});
|
|
1261
|
+
assertInstanceOf(object, Note);
|
|
1262
|
+
deepStrictEqual(object.content, "Embedded");
|
|
1263
|
+
});
|
|
1264
|
+
test("singular accessors drop anonymous objects embedded in unsecured collections", async () => {
|
|
1265
|
+
const options = {
|
|
1266
|
+
documentLoader: createLoader({ [gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox", { first: {
|
|
1267
|
+
type: "OrderedCollectionPage",
|
|
1268
|
+
orderedItems: [{
|
|
1269
|
+
type: "Note",
|
|
1270
|
+
content: "Anonymous"
|
|
1271
|
+
}]
|
|
1272
|
+
} }) }),
|
|
1273
|
+
contextLoader: mockDocumentLoader,
|
|
1274
|
+
gateways: ["https://gw.example"],
|
|
1275
|
+
verifyPortableObject: createRecordingVerifier(() => ({
|
|
1276
|
+
verified: true,
|
|
1277
|
+
unsecured: true
|
|
1278
|
+
}))
|
|
1279
|
+
};
|
|
1280
|
+
const outbox = await (await Person.fromJsonLd({
|
|
1281
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1282
|
+
id: `ap://${did}/actor`,
|
|
1283
|
+
type: "Person",
|
|
1284
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
1285
|
+
outbox: `ap://${did}/actor/outbox`
|
|
1286
|
+
}, { contextLoader: mockDocumentLoader })).getOutbox(options);
|
|
1287
|
+
assertInstanceOf(outbox, OrderedCollection);
|
|
1288
|
+
deepStrictEqual(await outbox.getFirst(options), null);
|
|
1289
|
+
deepStrictEqual(await Array.fromAsync(traverseCollection(outbox, options)), []);
|
|
1290
|
+
});
|
|
603
1291
|
//#endregion
|
|
604
1292
|
export {};
|