@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.
@@ -1,11 +1,12 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { O as Note, l as Create } from "./vocab-BJGPw--L.mjs";
4
- import { t as esm_default } from "./esm-Df0Yo-Tp.mjs";
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
- ...note(),
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
- ok(loaded.includes("https://www.w3.org/ns/activitystreams"));
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 {};