@fedify/vocab 2.4.0-dev.1570 → 2.4.0-dev.1573

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/src/vocab.test.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { mockDocumentLoader, test } from "@fedify/fixture";
2
2
  import {
3
3
  decodeMultibase,
4
+ type DocumentLoader,
4
5
  LanguageString,
5
6
  parseDecimal,
7
+ type RemoteDocument,
6
8
  } from "@fedify/vocab-runtime";
7
9
  import {
8
10
  areAllScalarTypes,
@@ -34,6 +36,10 @@ import {
34
36
  Delete,
35
37
  Document,
36
38
  Endpoints,
39
+ FeatureAuthorization,
40
+ FeaturedCollection,
41
+ FeaturedItem,
42
+ FeatureRequest,
37
43
  Follow,
38
44
  Hashtag,
39
45
  Intent,
@@ -102,6 +108,23 @@ const QUOTE_REQUEST_CONTEXT = [
102
108
  },
103
109
  ] as const;
104
110
 
111
+ const FEATURE_CONTEXT = [
112
+ "https://www.w3.org/ns/activitystreams",
113
+ "https://w3id.org/security/data-integrity/v1",
114
+ "https://gotosocial.org/ns",
115
+ "https://w3id.org/fep/7aa9",
116
+ ] as const;
117
+
118
+ const FEATURED_COLLECTION_CONTEXT = [
119
+ ...FEATURE_CONTEXT,
120
+ {
121
+ Hashtag: "as:Hashtag",
122
+ discoverable: "toot:discoverable",
123
+ sensitive: "as:sensitive",
124
+ toot: "http://joinmastodon.org/ns#",
125
+ },
126
+ ] as const;
127
+
105
128
  const DELETE_QUOTE_REQUEST_CONTEXT = [
106
129
  "https://w3id.org/identity/v1",
107
130
  "https://www.w3.org/ns/activitystreams",
@@ -474,9 +497,969 @@ test("Activity.fromJsonLd()", async () => {
474
497
  ),
475
498
  );
476
499
  deepStrictEqual(
477
- proofs[0].created,
478
- Temporal.Instant.from("2023-02-24T23:36:38Z"),
500
+ proofs[0].created,
501
+ Temporal.Instant.from("2023-02-24T23:36:38Z"),
502
+ );
503
+ });
504
+
505
+ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
506
+ const did = "did:key:z6Mkabc";
507
+ const portableActor =
508
+ `ap://${did}/actor?gateways=https%3A%2F%2Fserver.example`;
509
+ const portableObject =
510
+ `ap://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
511
+ const uppercasePortableObject =
512
+ `AP://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
513
+ const portablePage = `ap://${did}/actor/outbox?page=2`;
514
+
515
+ const note = await Note.fromJsonLd({
516
+ "@context": "https://www.w3.org/ns/activitystreams",
517
+ type: "Note",
518
+ id: uppercasePortableObject,
519
+ attributedTo: `ap+ef61://${
520
+ encodeURIComponent(did)
521
+ }/actor?gateways=https%3A%2F%2Fserver.example`,
522
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
523
+ deepStrictEqual(
524
+ note.id,
525
+ new URL(
526
+ "ap+ef61://did%3Akey%3Az6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example",
527
+ ),
528
+ );
529
+ deepStrictEqual(
530
+ note.attributionId,
531
+ new URL(
532
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
533
+ ),
534
+ );
535
+ const noteJson = await note.toJsonLd({
536
+ contextLoader: mockDocumentLoader,
537
+ }) as Record<string, unknown>;
538
+ deepStrictEqual(noteJson.type, "Note");
539
+ deepStrictEqual(
540
+ noteJson.id,
541
+ "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example",
542
+ );
543
+ deepStrictEqual(
544
+ noteJson.attributedTo,
545
+ "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
546
+ );
547
+
548
+ const activity = await Activity.fromJsonLd({
549
+ "@context": "https://www.w3.org/ns/activitystreams",
550
+ type: "Create",
551
+ actor: portableActor,
552
+ object: portableObject,
553
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
554
+ deepStrictEqual(
555
+ activity.actorId,
556
+ new URL(
557
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
558
+ ),
559
+ );
560
+ const activityJson = await activity.toJsonLd({
561
+ contextLoader: mockDocumentLoader,
562
+ }) as Record<string, unknown>;
563
+ deepStrictEqual(activityJson.type, "Create");
564
+ deepStrictEqual(
565
+ activityJson.actor,
566
+ "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
567
+ );
568
+ deepStrictEqual(
569
+ activityJson.object,
570
+ "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example",
571
+ );
572
+
573
+ const person = await Person.fromJsonLd({
574
+ "@context": "https://www.w3.org/ns/activitystreams",
575
+ type: "Person",
576
+ inbox: `ap+ef61://${did}/actor/inbox`,
577
+ outbox: `ap+ef61://${did}/actor/outbox`,
578
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
579
+ deepStrictEqual(
580
+ person.inboxId,
581
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/inbox"),
582
+ );
583
+ deepStrictEqual(
584
+ person.outboxId,
585
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox"),
586
+ );
587
+ const personJson = await person.toJsonLd({
588
+ contextLoader: mockDocumentLoader,
589
+ }) as Record<string, unknown>;
590
+ deepStrictEqual(personJson.type, "Person");
591
+ deepStrictEqual(personJson.inbox, "ap+ef61://did:key:z6Mkabc/actor/inbox");
592
+ deepStrictEqual(
593
+ personJson.outbox,
594
+ "ap+ef61://did:key:z6Mkabc/actor/outbox",
595
+ );
596
+
597
+ const page = await OrderedCollectionPage.fromJsonLd({
598
+ "@context": "https://www.w3.org/ns/activitystreams",
599
+ type: "OrderedCollectionPage",
600
+ next: portablePage,
601
+ prev: `ap+ef61://${encodeURIComponent(did)}/actor/outbox?page=1`,
602
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
603
+ deepStrictEqual(
604
+ page.nextId,
605
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=2"),
606
+ );
607
+ deepStrictEqual(
608
+ page.prevId,
609
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=1"),
610
+ );
611
+ const pageJson = await page.toJsonLd({
612
+ contextLoader: mockDocumentLoader,
613
+ }) as Record<string, unknown>;
614
+ deepStrictEqual(pageJson.type, "OrderedCollectionPage");
615
+ deepStrictEqual(
616
+ pageJson.next,
617
+ "ap+ef61://did:key:z6Mkabc/actor/outbox?page=2",
618
+ );
619
+ deepStrictEqual(
620
+ pageJson.prev,
621
+ "ap+ef61://did:key:z6Mkabc/actor/outbox?page=1",
622
+ );
623
+ });
624
+
625
+ test("fromJsonLd() caches text that mentions portable ActivityPub IRIs", async () => {
626
+ const noteJson = {
627
+ "@context": [
628
+ "https://www.w3.org/ns/activitystreams",
629
+ { extra: "https://example.com/ns#extra" },
630
+ ],
631
+ type: "Note",
632
+ id: "https://example.com/notes/1",
633
+ content: "This is text about ap://did:key:z6Mkabc/actor.",
634
+ extra: "This extension property should stay cached.",
635
+ };
636
+
637
+ const note = await Note.fromJsonLd(noteJson, {
638
+ documentLoader: mockDocumentLoader,
639
+ contextLoader: mockDocumentLoader,
640
+ });
641
+
642
+ deepStrictEqual(await note.toJsonLd(), noteJson);
643
+ });
644
+
645
+ test("fromJsonLd() preserves extensions with portable ActivityPub IRIs", async () => {
646
+ const note = await Note.fromJsonLd({
647
+ "@context": [
648
+ "https://www.w3.org/ns/activitystreams",
649
+ { extra: "https://example.com/ns#extra" },
650
+ ],
651
+ type: "Note",
652
+ id: "ap://did:key:z6Mkabc/objects/1",
653
+ extra: "This extension property should stay cached.",
654
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
655
+
656
+ const jsonLd = await note.toJsonLd() as Record<string, unknown>;
657
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
658
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
659
+ });
660
+
661
+ test("fromJsonLd() preserves unmapped terms with portable IRIs", async () => {
662
+ const note = await Note.fromJsonLd({
663
+ "@context": "https://www.w3.org/ns/activitystreams",
664
+ type: "Note",
665
+ id: "ap://did:key:z6Mkabc/objects/1",
666
+ extra: "This unmapped property should stay cached.",
667
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
668
+
669
+ const jsonLd = await note.toJsonLd() as Record<string, unknown>;
670
+ deepStrictEqual(
671
+ jsonLd.extra,
672
+ "This unmapped property should stay cached.",
673
+ );
674
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
675
+ });
676
+
677
+ test("fromJsonLd() preserves expanded arrays with portable IRIs", async () => {
678
+ const expanded = [
679
+ {
680
+ "@id": "https://example.com/activities/1",
681
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
682
+ "https://www.w3.org/ns/activitystreams#actor": [
683
+ { "@id": "ap://did:key:z6Mkabc/actor" },
684
+ ],
685
+ "https://www.w3.org/ns/activitystreams#object": [
686
+ { "@id": "https://example.com/objects/1" },
687
+ ],
688
+ },
689
+ {
690
+ "@id": "https://example.com/objects/1",
691
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
692
+ "https://www.w3.org/ns/activitystreams#content": [
693
+ { "@value": "Sibling node should stay cached." },
694
+ ],
695
+ },
696
+ ];
697
+
698
+ const activity = await Activity.fromJsonLd(expanded, {
699
+ documentLoader: mockDocumentLoader,
700
+ contextLoader: mockDocumentLoader,
701
+ });
702
+
703
+ deepStrictEqual(
704
+ activity.actorId,
705
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
706
+ );
707
+ deepStrictEqual(await activity.toJsonLd(), [
708
+ {
709
+ "@id": "https://example.com/activities/1",
710
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
711
+ "https://www.w3.org/ns/activitystreams#actor": [
712
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
713
+ ],
714
+ "https://www.w3.org/ns/activitystreams#object": [
715
+ { "@id": "https://example.com/objects/1" },
716
+ ],
717
+ },
718
+ {
719
+ "@id": "https://example.com/objects/1",
720
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
721
+ "https://www.w3.org/ns/activitystreams#content": [
722
+ { "@value": "Sibling node should stay cached." },
723
+ ],
724
+ },
725
+ ]);
726
+ deepStrictEqual(expanded[0]["https://www.w3.org/ns/activitystreams#actor"], [
727
+ { "@id": "ap://did:key:z6Mkabc/actor" },
728
+ ]);
729
+ });
730
+
731
+ test("fromJsonLd() preserves single-node expanded arrays with portable IRIs", async () => {
732
+ const expanded = [
733
+ {
734
+ "@id": "ap://did:key:z6Mkabc/objects/1",
735
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
736
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
737
+ { "@id": "ap://did:key:z6Mkabc/actor" },
738
+ ],
739
+ "https://www.w3.org/ns/activitystreams#content": [
740
+ { "@value": "Single expanded node should stay cached as an array." },
741
+ ],
742
+ },
743
+ ];
744
+
745
+ const note = await Note.fromJsonLd(expanded, {
746
+ documentLoader: mockDocumentLoader,
747
+ contextLoader: mockDocumentLoader,
748
+ });
749
+
750
+ deepStrictEqual(await note.toJsonLd(), [
751
+ {
752
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
753
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
754
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
755
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
756
+ ],
757
+ "https://www.w3.org/ns/activitystreams#content": [
758
+ { "@value": "Single expanded node should stay cached as an array." },
759
+ ],
760
+ },
761
+ ]);
762
+ deepStrictEqual(expanded[0]["@id"], "ap://did:key:z6Mkabc/objects/1");
763
+ });
764
+
765
+ test("fromJsonLd() preserves no-context object shape with portable IRIs", async () => {
766
+ const expanded = {
767
+ "@id": "ap://did:key:z6Mkabc/objects/1",
768
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
769
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
770
+ { "@id": "ap://did:key:z6Mkabc/actor" },
771
+ ],
772
+ "https://www.w3.org/ns/activitystreams#content": [
773
+ { "@value": "No-context object shape should stay cached." },
774
+ ],
775
+ };
776
+
777
+ const note = await Note.fromJsonLd(expanded, {
778
+ documentLoader: mockDocumentLoader,
779
+ contextLoader: mockDocumentLoader,
780
+ });
781
+
782
+ deepStrictEqual(await note.toJsonLd(), {
783
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
784
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
785
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
786
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
787
+ ],
788
+ "https://www.w3.org/ns/activitystreams#content": [
789
+ { "@value": "No-context object shape should stay cached." },
790
+ ],
791
+ });
792
+ deepStrictEqual(expanded["@id"], "ap://did:key:z6Mkabc/objects/1");
793
+ });
794
+
795
+ test("fromJsonLd() preserves expanded subtype cache types", async () => {
796
+ const expanded = [
797
+ {
798
+ "@id": "https://example.com/activities/1",
799
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
800
+ "https://www.w3.org/ns/activitystreams#actor": [
801
+ { "@id": "https://example.com/actors/alice" },
802
+ ],
803
+ "https://www.w3.org/ns/activitystreams#object": [
804
+ { "@id": "https://example.com/objects/1" },
805
+ ],
806
+ },
807
+ {
808
+ "@id": "https://example.com/objects/1",
809
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
810
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
811
+ { "@id": "ap://did:key:z6Mkabc/actor" },
812
+ ],
813
+ },
814
+ ];
815
+
816
+ const activity = await Activity.fromJsonLd(expanded, {
817
+ documentLoader: mockDocumentLoader,
818
+ contextLoader: mockDocumentLoader,
819
+ });
820
+
821
+ deepStrictEqual(await activity.toJsonLd(), [
822
+ {
823
+ "@id": "https://example.com/activities/1",
824
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
825
+ "https://www.w3.org/ns/activitystreams#actor": [
826
+ { "@id": "https://example.com/actors/alice" },
827
+ ],
828
+ "https://www.w3.org/ns/activitystreams#object": [
829
+ { "@id": "https://example.com/objects/1" },
830
+ ],
831
+ },
832
+ {
833
+ "@id": "https://example.com/objects/1",
834
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
835
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
836
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
837
+ ],
838
+ },
839
+ ]);
840
+ deepStrictEqual(expanded[0]["@type"], [
841
+ "https://www.w3.org/ns/activitystreams#Create",
842
+ ]);
843
+ });
844
+
845
+ test("fromJsonLd() preserves compact array contexts with portable IRIs", async () => {
846
+ const note = await Note.fromJsonLd([{
847
+ "@context": "https://www.w3.org/ns/activitystreams",
848
+ type: "Note",
849
+ id: "ap://did:key:z6Mkabc/objects/1",
850
+ }], {
851
+ documentLoader: mockDocumentLoader,
852
+ contextLoader: mockDocumentLoader,
853
+ });
854
+
855
+ deepStrictEqual(await note.toJsonLd(), [{
856
+ "@context": "https://www.w3.org/ns/activitystreams",
857
+ type: "Note",
858
+ id: "ap+ef61://did:key:z6Mkabc/objects/1",
859
+ }]);
860
+ });
861
+
862
+ test("fromJsonLd() preserves compact single-item arrays with portable IRIs", async () => {
863
+ const createJson = {
864
+ "@context": "https://www.w3.org/ns/activitystreams",
865
+ type: "Create",
866
+ actor: ["ap://did:key:z6Mkabc/actor"],
867
+ object: "https://example.com/objects/1",
868
+ };
869
+
870
+ const create = await Create.fromJsonLd(createJson, {
871
+ documentLoader: mockDocumentLoader,
872
+ contextLoader: mockDocumentLoader,
873
+ });
874
+
875
+ deepStrictEqual(await create.toJsonLd(), {
876
+ "@context": "https://www.w3.org/ns/activitystreams",
877
+ type: "Create",
878
+ actor: ["ap+ef61://did:key:z6Mkabc/actor"],
879
+ object: "https://example.com/objects/1",
880
+ });
881
+ deepStrictEqual(createJson.actor, ["ap://did:key:z6Mkabc/actor"]);
882
+ });
883
+
884
+ test("fromJsonLd() preserves compact multi-node arrays with portable IRIs", async () => {
885
+ const activityJson = [
886
+ {
887
+ "@context": "https://www.w3.org/ns/activitystreams",
888
+ type: "Create",
889
+ id: "https://example.com/activities/1",
890
+ actor: "https://example.com/actors/alice",
891
+ object: "https://example.com/objects/1",
892
+ },
893
+ {
894
+ "@context": "https://www.w3.org/ns/activitystreams",
895
+ type: "Note",
896
+ id: "https://example.com/objects/1",
897
+ attributedTo: "ap://did:key:z6Mkabc/actor",
898
+ },
899
+ ];
900
+
901
+ const activity = await Activity.fromJsonLd(activityJson, {
902
+ documentLoader: mockDocumentLoader,
903
+ contextLoader: mockDocumentLoader,
904
+ });
905
+
906
+ deepStrictEqual(await activity.toJsonLd(), [
907
+ {
908
+ "@context": "https://www.w3.org/ns/activitystreams",
909
+ type: "Create",
910
+ id: "https://example.com/activities/1",
911
+ actor: "https://example.com/actors/alice",
912
+ object: "https://example.com/objects/1",
913
+ },
914
+ {
915
+ "@context": "https://www.w3.org/ns/activitystreams",
916
+ type: "Note",
917
+ id: "https://example.com/objects/1",
918
+ attributedTo: "ap+ef61://did:key:z6Mkabc/actor",
919
+ },
920
+ ]);
921
+ });
922
+
923
+ test("fromJsonLd() preserves nested unmapped terms with portable IRIs", async () => {
924
+ const noteJson = {
925
+ "@context": "https://www.w3.org/ns/activitystreams",
926
+ type: "Note",
927
+ id: "ap://did:key:z6Mkabc/objects/1",
928
+ attachment: {
929
+ type: "Object",
930
+ name: "Attachment with an unmapped extension.",
931
+ extra: "This nested unmapped property should stay cached.",
932
+ },
933
+ };
934
+
935
+ const note = await Note.fromJsonLd(noteJson, {
936
+ documentLoader: mockDocumentLoader,
937
+ contextLoader: mockDocumentLoader,
938
+ });
939
+
940
+ deepStrictEqual(await note.toJsonLd(), {
941
+ "@context": "https://www.w3.org/ns/activitystreams",
942
+ type: "Note",
943
+ id: "ap+ef61://did:key:z6Mkabc/objects/1",
944
+ attachment: {
945
+ type: "Object",
946
+ name: "Attachment with an unmapped extension.",
947
+ extra: "This nested unmapped property should stay cached.",
948
+ },
949
+ });
950
+ });
951
+
952
+ test("fromJsonLd() preserves compact array item extension contexts with portable IRIs", async () => {
953
+ const activityJson = [
954
+ {
955
+ "@context": "https://www.w3.org/ns/activitystreams",
956
+ type: "Create",
957
+ id: "https://example.com/activities/1",
958
+ actor: "https://example.com/actors/alice",
959
+ object: "https://example.com/objects/1",
960
+ },
961
+ {
962
+ "@context": [
963
+ "https://www.w3.org/ns/activitystreams",
964
+ {
965
+ extraRef: {
966
+ "@id": "https://example.com/ns#extraRef",
967
+ "@type": "@id",
968
+ },
969
+ },
970
+ ],
971
+ type: "Note",
972
+ id: "https://example.com/objects/1",
973
+ extraRef: "ap://did:key:z6Mkabc/extra",
974
+ },
975
+ ];
976
+
977
+ const activity = await Activity.fromJsonLd(activityJson, {
978
+ documentLoader: mockDocumentLoader,
979
+ contextLoader: mockDocumentLoader,
980
+ });
981
+
982
+ deepStrictEqual(await activity.toJsonLd(), [
983
+ {
984
+ "@context": "https://www.w3.org/ns/activitystreams",
985
+ type: "Create",
986
+ id: "https://example.com/activities/1",
987
+ actor: "https://example.com/actors/alice",
988
+ object: "https://example.com/objects/1",
989
+ },
990
+ {
991
+ "@context": [
992
+ "https://www.w3.org/ns/activitystreams",
993
+ {
994
+ extraRef: {
995
+ "@id": "https://example.com/ns#extraRef",
996
+ "@type": "@id",
997
+ },
998
+ },
999
+ ],
1000
+ type: "Note",
1001
+ id: "https://example.com/objects/1",
1002
+ extraRef: "ap+ef61://did:key:z6Mkabc/extra",
1003
+ },
1004
+ ]);
1005
+ });
1006
+
1007
+ test("fromJsonLd() formats portable IRIs in JSON-LD containers", async () => {
1008
+ const note = await Note.fromJsonLd({
1009
+ "@context": "https://www.w3.org/ns/activitystreams",
1010
+ type: "Note",
1011
+ id: "https://example.com/notes/1",
1012
+ attributedTo: {
1013
+ "@list": ["ap://did:key:z6Mkabc/actor"],
1014
+ },
1015
+ to: {
1016
+ "@set": ["ap://did:key:z6Mkabc/followers"],
1017
+ },
1018
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1019
+
1020
+ const jsonLd = await note.toJsonLd() as Record<string, unknown>;
1021
+ deepStrictEqual(jsonLd.attributedTo, {
1022
+ "@list": ["ap+ef61://did:key:z6Mkabc/actor"],
1023
+ });
1024
+ deepStrictEqual(jsonLd.to, "ap+ef61://did:key:z6Mkabc/followers");
1025
+ });
1026
+
1027
+ test("fromJsonLd() formats portable IRIs hidden behind JSON-LD aliases", async () => {
1028
+ const activity = await Activity.fromJsonLd({
1029
+ "@context": [
1030
+ "https://www.w3.org/ns/activitystreams",
1031
+ {
1032
+ as: {
1033
+ "@id": "https://www.w3.org/ns/activitystreams#",
1034
+ "@prefix": true,
1035
+ },
1036
+ extra: "https://example.com/ns#extra",
1037
+ extraRef: {
1038
+ "@id": "https://example.com/ns#extraRef",
1039
+ "@type": "@id",
1040
+ },
1041
+ actorRef: {
1042
+ "@id": "as:actor",
1043
+ "@type": "@id",
1044
+ },
1045
+ targetRef: {
1046
+ "@id": "as:target",
1047
+ "@type": "@id",
1048
+ },
1049
+ },
1050
+ ],
1051
+ type: "Create",
1052
+ actorRef: "ap://did:key:z6Mkabc/actor",
1053
+ object: "https://example.com/objects/1",
1054
+ targetRef: "ap://did:key:z6Mkabc/target",
1055
+ extra: "This extension property should stay cached.",
1056
+ extraRef: "ap://did:key:z6Mkabc/extra",
1057
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1058
+
1059
+ deepStrictEqual(
1060
+ activity.actorId,
1061
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
1062
+ );
1063
+ deepStrictEqual(
1064
+ activity.targetId,
1065
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/target"),
1066
+ );
1067
+ const jsonLd = await activity.toJsonLd({
1068
+ contextLoader: mockDocumentLoader,
1069
+ }) as Record<string, unknown>;
1070
+ deepStrictEqual(jsonLd.actor, "ap+ef61://did:key:z6Mkabc/actor");
1071
+ deepStrictEqual("actorRef" in jsonLd, false);
1072
+ deepStrictEqual(jsonLd.target, "ap+ef61://did:key:z6Mkabc/target");
1073
+ deepStrictEqual("targetRef" in jsonLd, false);
1074
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
1075
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1076
+ });
1077
+
1078
+ test("fromJsonLd() preserves portable IRIs in @id extension terms", async () => {
1079
+ const note = await Note.fromJsonLd({
1080
+ "@context": [
1081
+ "https://www.w3.org/ns/activitystreams",
1082
+ {
1083
+ extraRef: {
1084
+ "@id": "https://example.com/ns#extraRef",
1085
+ "@type": "@id",
1086
+ },
1087
+ },
1088
+ ],
1089
+ type: "Note",
1090
+ id: "https://example.com/notes/1",
1091
+ extraRef: "ap://did:key:z6Mkabc/extra",
1092
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1093
+
1094
+ const jsonLd = await note.toJsonLd({
1095
+ contextLoader: mockDocumentLoader,
1096
+ }) as Record<string, unknown>;
1097
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1098
+ });
1099
+
1100
+ test("fromJsonLd() ignores malformed portable IRIs in extension cache terms", async () => {
1101
+ const noteJson = {
1102
+ "@context": [
1103
+ "https://www.w3.org/ns/activitystreams",
1104
+ {
1105
+ extraRef: {
1106
+ "@id": "https://example.com/ns#extraRef",
1107
+ "@type": "@id",
1108
+ },
1109
+ },
1110
+ ],
1111
+ type: "Note",
1112
+ id: "https://example.com/notes/1",
1113
+ extraRef: "ap://example.com/not-portable",
1114
+ };
1115
+
1116
+ const note = await Note.fromJsonLd(noteJson, {
1117
+ documentLoader: mockDocumentLoader,
1118
+ contextLoader: mockDocumentLoader,
1119
+ });
1120
+
1121
+ deepStrictEqual(await note.toJsonLd(), noteJson);
1122
+ });
1123
+
1124
+ test("fromJsonLd() preserves portable IRIs in @id typed terms", async () => {
1125
+ const note = await Note.fromJsonLd({
1126
+ "@context": [
1127
+ "https://www.w3.org/ns/activitystreams",
1128
+ {
1129
+ "@vocab": "https://example.com/ns#",
1130
+ extraRef: { "@type": "@id" },
1131
+ },
1132
+ ],
1133
+ type: "Note",
1134
+ id: "https://example.com/notes/1",
1135
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1136
+ extraRef: "ap://did:key:z6Mkabc/extra",
1137
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1138
+
1139
+ const jsonLd = await note.toJsonLd({
1140
+ contextLoader: mockDocumentLoader,
1141
+ }) as Record<string, unknown>;
1142
+ deepStrictEqual(
1143
+ jsonLd.content,
1144
+ "This text mentions ap://did:key:z6Mkabc/text.",
1145
+ );
1146
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1147
+ });
1148
+
1149
+ test("fromJsonLd() preserves portable IRIs hidden behind remote contexts", async () => {
1150
+ const contextUrl = "https://example.com/contexts/portable-iris";
1151
+ const contextLoader: DocumentLoader = async (
1152
+ resource: string,
1153
+ options,
1154
+ ): Promise<RemoteDocument> => {
1155
+ if (resource === contextUrl) {
1156
+ return {
1157
+ contextUrl: null,
1158
+ documentUrl: resource,
1159
+ document: {
1160
+ "@context": [
1161
+ "https://www.w3.org/ns/activitystreams",
1162
+ {
1163
+ "@vocab": "https://example.com/ns#",
1164
+ extra: "https://example.com/ns#extra",
1165
+ extraRef: { "@type": "@id" },
1166
+ },
1167
+ ],
1168
+ },
1169
+ };
1170
+ }
1171
+ return await mockDocumentLoader(resource, options);
1172
+ };
1173
+ const note = await Note.fromJsonLd({
1174
+ "@context": contextUrl,
1175
+ type: "Note",
1176
+ id: "https://example.com/notes/1",
1177
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1178
+ extra: "This extension property should stay cached.",
1179
+ extraRef: "ap://did:key:z6Mkabc/extra",
1180
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1181
+
1182
+ const jsonLd = await note.toJsonLd({ contextLoader }) as Record<
1183
+ string,
1184
+ unknown
1185
+ >;
1186
+ deepStrictEqual(
1187
+ jsonLd.content,
1188
+ "This text mentions ap://did:key:z6Mkabc/text.",
1189
+ );
1190
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
1191
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1192
+ });
1193
+
1194
+ test("fromJsonLd() formats portable IRIs hidden behind nested remote contexts", async () => {
1195
+ const rootContextUrl = "https://example.com/contexts/nested-portable-iris";
1196
+ const nestedContextUrl = "https://example.com/contexts/nested-portable-ref";
1197
+ const contextLoader: DocumentLoader = async (
1198
+ resource: string,
1199
+ options,
1200
+ ): Promise<RemoteDocument> => {
1201
+ if (resource === rootContextUrl) {
1202
+ return {
1203
+ contextUrl: null,
1204
+ documentUrl: resource,
1205
+ document: {
1206
+ "@context": [
1207
+ "https://www.w3.org/ns/activitystreams",
1208
+ {
1209
+ "@vocab": "https://example.com/ns#",
1210
+ extraContainer: "https://example.com/ns#extraContainer",
1211
+ },
1212
+ ],
1213
+ },
1214
+ };
1215
+ }
1216
+ if (resource === nestedContextUrl) {
1217
+ return {
1218
+ contextUrl: null,
1219
+ documentUrl: resource,
1220
+ document: {
1221
+ "@context": {
1222
+ "@vocab": "https://example.com/ns#",
1223
+ extra: "https://example.com/ns#extra",
1224
+ extraRef: { "@type": "@id" },
1225
+ },
1226
+ },
1227
+ };
1228
+ }
1229
+ return await mockDocumentLoader(resource, options);
1230
+ };
1231
+ const note = await Note.fromJsonLd({
1232
+ "@context": rootContextUrl,
1233
+ type: "Note",
1234
+ id: "https://example.com/notes/1",
1235
+ extraContainer: {
1236
+ "@context": nestedContextUrl,
1237
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1238
+ extra: "This nested extension object should stay cached.",
1239
+ extraRef: "ap://did:key:z6Mkabc/extra",
1240
+ },
1241
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1242
+
1243
+ const jsonLd = await note.toJsonLd({ contextLoader }) as Record<
1244
+ string,
1245
+ unknown
1246
+ >;
1247
+ deepStrictEqual(jsonLd.extraContainer, {
1248
+ "@context": nestedContextUrl,
1249
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1250
+ extra: "This nested extension object should stay cached.",
1251
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/extra" },
1252
+ });
1253
+ });
1254
+
1255
+ test("fromJsonLd() formats portable IRIs in sibling remote-context objects", async () => {
1256
+ const rootContextUrl = "https://example.com/contexts/sibling-containers";
1257
+ const nestedContextUrl = "https://example.com/contexts/sibling-extra-ref";
1258
+ const contextLoader: DocumentLoader = async (
1259
+ resource: string,
1260
+ options,
1261
+ ): Promise<RemoteDocument> => {
1262
+ if (resource === rootContextUrl) {
1263
+ return {
1264
+ contextUrl: null,
1265
+ documentUrl: resource,
1266
+ document: {
1267
+ "@context": [
1268
+ "https://www.w3.org/ns/activitystreams",
1269
+ {
1270
+ "@vocab": "https://example.com/ns#",
1271
+ firstExtraContainer: "https://example.com/ns#firstExtraContainer",
1272
+ secondExtraContainer:
1273
+ "https://example.com/ns#secondExtraContainer",
1274
+ },
1275
+ ],
1276
+ },
1277
+ };
1278
+ }
1279
+ if (resource === nestedContextUrl) {
1280
+ return {
1281
+ contextUrl: null,
1282
+ documentUrl: resource,
1283
+ document: {
1284
+ "@context": {
1285
+ "@vocab": "https://example.com/ns#",
1286
+ extraRef: { "@type": "@id" },
1287
+ },
1288
+ },
1289
+ };
1290
+ }
1291
+ return await mockDocumentLoader(resource, options);
1292
+ };
1293
+ const note = await Note.fromJsonLd({
1294
+ "@context": rootContextUrl,
1295
+ type: "Note",
1296
+ id: "https://example.com/notes/1",
1297
+ firstExtraContainer: {
1298
+ "@context": nestedContextUrl,
1299
+ content: "No portable IRI here.",
1300
+ },
1301
+ secondExtraContainer: {
1302
+ "@context": nestedContextUrl,
1303
+ extraRef: "ap://did:key:z6Mkabc/second",
1304
+ },
1305
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1306
+
1307
+ const jsonLd = await note.toJsonLd({ contextLoader }) as Record<
1308
+ string,
1309
+ unknown
1310
+ >;
1311
+ deepStrictEqual(jsonLd.firstExtraContainer, {
1312
+ "@context": nestedContextUrl,
1313
+ content: "No portable IRI here.",
1314
+ });
1315
+ deepStrictEqual(jsonLd.secondExtraContainer, {
1316
+ "@context": nestedContextUrl,
1317
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/second" },
1318
+ });
1319
+ });
1320
+
1321
+ test("fromJsonLd() batches unmapped portable IRI term checks", async () => {
1322
+ const contextUrl = "https://example.com/contexts/batched-portable-aliases";
1323
+ let contextLoads = 0;
1324
+ const contextLoader: DocumentLoader = async (
1325
+ resource: string,
1326
+ options,
1327
+ ): Promise<RemoteDocument> => {
1328
+ if (resource === contextUrl) {
1329
+ contextLoads++;
1330
+ return {
1331
+ contextUrl: null,
1332
+ documentUrl: resource,
1333
+ document: {
1334
+ "@context": [
1335
+ "https://www.w3.org/ns/activitystreams",
1336
+ {
1337
+ as: {
1338
+ "@id": "https://www.w3.org/ns/activitystreams#",
1339
+ "@prefix": true,
1340
+ },
1341
+ actorRef0: { "@id": "as:actor", "@type": "@id" },
1342
+ actorRef1: { "@id": "as:actor", "@type": "@id" },
1343
+ targetRef0: { "@id": "as:target", "@type": "@id" },
1344
+ targetRef1: { "@id": "as:target", "@type": "@id" },
1345
+ objectRef0: { "@id": "as:object", "@type": "@id" },
1346
+ objectRef1: { "@id": "as:object", "@type": "@id" },
1347
+ },
1348
+ ],
1349
+ },
1350
+ };
1351
+ }
1352
+ return await mockDocumentLoader(resource, options);
1353
+ };
1354
+
1355
+ const activity = await Activity.fromJsonLd({
1356
+ "@context": contextUrl,
1357
+ type: "Create",
1358
+ actorRef0: "ap://did:key:z6Mkabc/actor0",
1359
+ actorRef1: "ap://did:key:z6Mkabc/actor1",
1360
+ targetRef0: "ap://did:key:z6Mkabc/target0",
1361
+ targetRef1: "ap://did:key:z6Mkabc/target1",
1362
+ objectRef0: "https://example.com/objects/0",
1363
+ objectRef1: "https://example.com/objects/1",
1364
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1365
+
1366
+ await activity.toJsonLd({ contextLoader });
1367
+
1368
+ ok(contextLoads <= 5);
1369
+ });
1370
+
1371
+ test("fromJsonLd() falls back when portable IRI cache merge fails", async () => {
1372
+ const contextUrl = "https://example.com/contexts/failing-cache-merge";
1373
+ let contextLoads = 0;
1374
+ const contextLoader: DocumentLoader = async (
1375
+ resource: string,
1376
+ options,
1377
+ ): Promise<RemoteDocument> => {
1378
+ if (resource === contextUrl) {
1379
+ contextLoads++;
1380
+ if (contextLoads > 1) throw new Error("merge context unavailable");
1381
+ return {
1382
+ contextUrl: null,
1383
+ documentUrl: resource,
1384
+ document: {
1385
+ "@context": [
1386
+ "https://www.w3.org/ns/activitystreams",
1387
+ {
1388
+ "@vocab": "https://example.com/ns#",
1389
+ extraRef: { "@type": "@id" },
1390
+ },
1391
+ ],
1392
+ },
1393
+ };
1394
+ }
1395
+ return await mockDocumentLoader(resource, options);
1396
+ };
1397
+
1398
+ const note = await Note.fromJsonLd({
1399
+ "@context": contextUrl,
1400
+ type: "Note",
1401
+ id: "ap://did:key:z6Mkabc/objects/1",
1402
+ extraRef: "ap://did:key:z6Mkabc/extra",
1403
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1404
+
1405
+ const jsonLd = await note.toJsonLd({
1406
+ contextLoader: mockDocumentLoader,
1407
+ }) as Record<string, unknown>;
1408
+
1409
+ deepStrictEqual(jsonLd.type, "Note");
1410
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
1411
+ });
1412
+
1413
+ test("fromJsonLd() formats portable IRIs in scalar URL values", async () => {
1414
+ const note = await Note.fromJsonLd({
1415
+ "@context": [
1416
+ "https://www.w3.org/ns/activitystreams",
1417
+ "https://gotosocial.org/ns",
1418
+ { quoteUrl: "as:quoteUrl" },
1419
+ ],
1420
+ type: "Note",
1421
+ id: "https://example.com/notes/1",
1422
+ quoteUrl: "ap://did:key:z6Mkabc/objects/1",
1423
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1424
+
1425
+ deepStrictEqual(
1426
+ note.quoteUrl,
1427
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"),
1428
+ );
1429
+ const jsonLd = await note.toJsonLd({
1430
+ contextLoader: mockDocumentLoader,
1431
+ }) as Record<string, unknown>;
1432
+ deepStrictEqual(jsonLd.quoteUrl, "ap+ef61://did:key:z6Mkabc/objects/1");
1433
+ });
1434
+
1435
+ test("fromJsonLd() formats portable IRIs in URL value lists", async () => {
1436
+ const note = await Note.fromJsonLd({
1437
+ "@context": [
1438
+ "https://www.w3.org/ns/activitystreams",
1439
+ "https://gotosocial.org/ns",
1440
+ { quoteUrl: "as:quoteUrl" },
1441
+ ],
1442
+ type: "Note",
1443
+ id: "https://example.com/notes/1",
1444
+ quoteUrl: {
1445
+ "@list": [
1446
+ { "@value": "ap://did:key:z6Mkabc/objects/1" },
1447
+ ],
1448
+ },
1449
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1450
+
1451
+ deepStrictEqual(
1452
+ note.quoteUrl,
1453
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"),
479
1454
  );
1455
+ const jsonLd = await note.toJsonLd({
1456
+ contextLoader: mockDocumentLoader,
1457
+ }) as Record<string, unknown>;
1458
+ deepStrictEqual(jsonLd.quoteUrl, {
1459
+ "@list": [
1460
+ "ap+ef61://did:key:z6Mkabc/objects/1",
1461
+ ],
1462
+ });
480
1463
  });
481
1464
 
482
1465
  test({
@@ -544,6 +1527,37 @@ test({
544
1527
  },
545
1528
  });
546
1529
 
1530
+ test("Activity.getObject() fetches canonical portable IRIs", async () => {
1531
+ const fetchedUrls: string[] = [];
1532
+ // deno-lint-ignore require-await
1533
+ const documentLoader: DocumentLoader = async (url) => {
1534
+ fetchedUrls.push(url);
1535
+ return {
1536
+ contextUrl: null,
1537
+ documentUrl: url,
1538
+ document: {
1539
+ "@context": "https://www.w3.org/ns/activitystreams",
1540
+ id: url,
1541
+ type: "Note",
1542
+ content: "Fetched portable object",
1543
+ },
1544
+ };
1545
+ };
1546
+ const activity = await Activity.fromJsonLd({
1547
+ "@context": "https://www.w3.org/ns/activitystreams",
1548
+ type: "Create",
1549
+ object: "ap://did:key:z6Mkabc/objects/1",
1550
+ }, { documentLoader, contextLoader: mockDocumentLoader });
1551
+
1552
+ const object = await activity.getObject({
1553
+ documentLoader,
1554
+ contextLoader: mockDocumentLoader,
1555
+ });
1556
+
1557
+ assertInstanceOf(object, Note);
1558
+ deepStrictEqual(fetchedUrls, ["ap+ef61://did:key:z6Mkabc/objects/1"]);
1559
+ });
1560
+
547
1561
  test({
548
1562
  name: "Activity.getObjects()",
549
1563
  permissions: { env: true, read: true },
@@ -741,6 +1755,7 @@ test("Person.toJsonLd()", async () => {
741
1755
  "https://www.w3.org/ns/did/v1",
742
1756
  "https://w3id.org/security/multikey/v1",
743
1757
  "https://gotosocial.org/ns",
1758
+ "https://w3id.org/fep/7aa9",
744
1759
  {
745
1760
  PropertyValue: "schema:PropertyValue",
746
1761
  alsoKnownAs: {
@@ -756,6 +1771,10 @@ test("Person.toJsonLd()", async () => {
756
1771
  "@id": "toot:featured",
757
1772
  "@type": "@id",
758
1773
  },
1774
+ featuredCollections: {
1775
+ "@id": "https://w3id.org/fep/7aa9#featuredCollections",
1776
+ "@type": "@id",
1777
+ },
759
1778
  featuredTags: {
760
1779
  "@id": "toot:featuredTags",
761
1780
  "@type": "@id",
@@ -1298,164 +2317,424 @@ test("Collection.fromJsonLd()", async () => {
1298
2317
  "inboxOf": "https://example.com/person/bup9a8eqm",
1299
2318
  });
1300
2319
  deepStrictEqual(
1301
- collection.id,
1302
- new URL("https://example.com/collection/jzc50wc28l"),
2320
+ collection.id,
2321
+ new URL("https://example.com/collection/jzc50wc28l"),
2322
+ );
2323
+ deepStrictEqual(
2324
+ collection.inboxOfId,
2325
+ new URL("https://example.com/person/bup9a8eqm"),
2326
+ );
2327
+ });
2328
+
2329
+ test("Note.quoteUrl", async () => {
2330
+ const note = new Note({
2331
+ quoteUrl: new URL("https://example.com/object"),
2332
+ });
2333
+ const expected = {
2334
+ "@context": NOTE_QUOTE_CONTEXT,
2335
+ _misskey_quote: "https://example.com/object",
2336
+ quoteUri: "https://example.com/object",
2337
+ quoteUrl: "https://example.com/object",
2338
+ type: "Note",
2339
+ };
2340
+ deepStrictEqual(await note.toJsonLd(), expected);
2341
+ deepStrictEqual(await note.toJsonLd({ format: "compact" }), expected);
2342
+
2343
+ const jsonLd: Record<string, unknown> = {
2344
+ "@context": [
2345
+ "https://www.w3.org/ns/activitystreams",
2346
+ {
2347
+ _misskey_quote: "misskey:_misskey_quote",
2348
+ fedibird: "http://fedibird.com/ns#",
2349
+ misskey: "https://misskey-hub.net/ns#",
2350
+ quoteUri: "fedibird:quoteUri",
2351
+ quoteUrl: "as:quoteUrl",
2352
+ },
2353
+ ],
2354
+ type: "Note",
2355
+ quoteUrl: "https://example.com/object",
2356
+ _misskey_quote: "https://example.com/object2",
2357
+ quoteUri: "https://example.com/object3",
2358
+ };
2359
+ const loaded = await Note.fromJsonLd(jsonLd);
2360
+ deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/object"));
2361
+
2362
+ delete jsonLd.quoteUrl;
2363
+ const loaded2 = await Note.fromJsonLd(jsonLd);
2364
+ deepStrictEqual(loaded2.quoteUrl, new URL("https://example.com/object2"));
2365
+
2366
+ delete jsonLd._misskey_quote;
2367
+ const loaded3 = await Note.fromJsonLd(jsonLd);
2368
+ deepStrictEqual(loaded3.quoteUrl, new URL("https://example.com/object3"));
2369
+ });
2370
+
2371
+ test("Note.quote", async () => {
2372
+ const note = new Note({
2373
+ quote: new URL("https://example.com/object"),
2374
+ });
2375
+ const expected = {
2376
+ "@context": NOTE_QUOTE_CONTEXT,
2377
+ quote: "https://example.com/object",
2378
+ type: "Note",
2379
+ };
2380
+ deepStrictEqual(await note.toJsonLd(), expected);
2381
+ deepStrictEqual(await note.toJsonLd({ format: "compact" }), expected);
2382
+
2383
+ const loaded = await Note.fromJsonLd({
2384
+ "@context": [
2385
+ "https://www.w3.org/ns/activitystreams",
2386
+ {
2387
+ quote: {
2388
+ "@id": "https://w3id.org/fep/044f#quote",
2389
+ "@type": "@id",
2390
+ },
2391
+ },
2392
+ ],
2393
+ type: "Note",
2394
+ quote: "https://example.com/object",
2395
+ });
2396
+ deepStrictEqual(loaded.quoteId, new URL("https://example.com/object"));
2397
+ });
2398
+
2399
+ test("Note.quoteAuthorization", async () => {
2400
+ const note = new Note({
2401
+ quoteAuthorization: new URL("https://example.com/authorizations/1"),
2402
+ });
2403
+ const expected = {
2404
+ "@context": NOTE_QUOTE_CONTEXT,
2405
+ quoteAuthorization: "https://example.com/authorizations/1",
2406
+ type: "Note",
2407
+ };
2408
+ deepStrictEqual(await note.toJsonLd(), expected);
2409
+ deepStrictEqual(await note.toJsonLd({ format: "compact" }), expected);
2410
+
2411
+ const loaded = await Note.fromJsonLd({
2412
+ "@context": [
2413
+ "https://www.w3.org/ns/activitystreams",
2414
+ {
2415
+ QuoteAuthorization: "https://w3id.org/fep/044f#QuoteAuthorization",
2416
+ quoteAuthorization: {
2417
+ "@id": "https://w3id.org/fep/044f#quoteAuthorization",
2418
+ "@type": "@id",
2419
+ },
2420
+ },
2421
+ ],
2422
+ type: "Note",
2423
+ quoteAuthorization: "https://example.com/authorizations/1",
2424
+ });
2425
+ deepStrictEqual(
2426
+ loaded.quoteAuthorizationId,
2427
+ new URL("https://example.com/authorizations/1"),
2428
+ );
2429
+
2430
+ const loadedFromGoToSocialContext = await Note.fromJsonLd({
2431
+ "@context": [
2432
+ "https://www.w3.org/ns/activitystreams",
2433
+ "https://gotosocial.org/ns",
2434
+ ],
2435
+ type: "Note",
2436
+ quoteAuthorization: "https://example.com/authorizations/2",
2437
+ }, {
2438
+ documentLoader: mockDocumentLoader,
2439
+ contextLoader: mockDocumentLoader,
2440
+ });
2441
+ deepStrictEqual(
2442
+ loadedFromGoToSocialContext.quoteAuthorizationId,
2443
+ new URL("https://example.com/authorizations/2"),
2444
+ );
2445
+ });
2446
+
2447
+ test("InteractionPolicy.canQuote", async () => {
2448
+ const note = new Note({
2449
+ interactionPolicy: new InteractionPolicy({
2450
+ canQuote: new InteractionRule({
2451
+ automaticApproval: new URL(
2452
+ "https://www.w3.org/ns/activitystreams#Public",
2453
+ ),
2454
+ }),
2455
+ }),
2456
+ });
2457
+ const expected = {
2458
+ "@context": NOTE_QUOTE_CONTEXT,
2459
+ interactionPolicy: {
2460
+ canQuote: {
2461
+ automaticApproval: "as:Public",
2462
+ },
2463
+ },
2464
+ type: "Note",
2465
+ };
2466
+ deepStrictEqual(
2467
+ await note.toJsonLd({ contextLoader: mockDocumentLoader }),
2468
+ expected,
2469
+ );
2470
+
2471
+ const loaded = await Note.fromJsonLd(expected, {
2472
+ documentLoader: mockDocumentLoader,
2473
+ contextLoader: mockDocumentLoader,
2474
+ });
2475
+ deepStrictEqual(
2476
+ await loaded.toJsonLd({ contextLoader: mockDocumentLoader }),
2477
+ expected,
2478
+ );
2479
+ });
2480
+
2481
+ test("InteractionPolicy.canFeature", async () => {
2482
+ const person = new Person({
2483
+ id: new URL("https://example.com/users/alice"),
2484
+ featuredCollections: new URL(
2485
+ "https://example.com/users/alice/featured_collections",
2486
+ ),
2487
+ interactionPolicy: new InteractionPolicy({
2488
+ canFeature: new InteractionRule({
2489
+ automaticApproval: new URL(
2490
+ "https://www.w3.org/ns/activitystreams#Public",
2491
+ ),
2492
+ }),
2493
+ }),
2494
+ });
2495
+ const expected = {
2496
+ "@context": [
2497
+ "https://www.w3.org/ns/activitystreams",
2498
+ "https://w3id.org/security/v1",
2499
+ "https://w3id.org/security/data-integrity/v1",
2500
+ "https://www.w3.org/ns/did/v1",
2501
+ "https://w3id.org/security/multikey/v1",
2502
+ "https://gotosocial.org/ns",
2503
+ "https://w3id.org/fep/7aa9",
2504
+ {
2505
+ Emoji: "toot:Emoji",
2506
+ PropertyValue: "schema:PropertyValue",
2507
+ _misskey_followedMessage: "misskey:_misskey_followedMessage",
2508
+ alsoKnownAs: {
2509
+ "@id": "as:alsoKnownAs",
2510
+ "@type": "@id",
2511
+ },
2512
+ discoverable: "toot:discoverable",
2513
+ featured: {
2514
+ "@id": "toot:featured",
2515
+ "@type": "@id",
2516
+ },
2517
+ featuredCollections: {
2518
+ "@id": "https://w3id.org/fep/7aa9#featuredCollections",
2519
+ "@type": "@id",
2520
+ },
2521
+ featuredTags: {
2522
+ "@id": "toot:featuredTags",
2523
+ "@type": "@id",
2524
+ },
2525
+ indexable: "toot:indexable",
2526
+ isCat: "misskey:isCat",
2527
+ manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
2528
+ memorial: "toot:memorial",
2529
+ misskey: "https://misskey-hub.net/ns#",
2530
+ movedTo: {
2531
+ "@id": "as:movedTo",
2532
+ "@type": "@id",
2533
+ },
2534
+ schema: "http://schema.org#",
2535
+ suspended: "toot:suspended",
2536
+ toot: "http://joinmastodon.org/ns#",
2537
+ value: "schema:value",
2538
+ },
2539
+ ],
2540
+ type: "Person",
2541
+ id: "https://example.com/users/alice",
2542
+ featuredCollections: "https://example.com/users/alice/featured_collections",
2543
+ interactionPolicy: {
2544
+ canFeature: {
2545
+ automaticApproval: "as:Public",
2546
+ },
2547
+ },
2548
+ };
2549
+ deepStrictEqual(
2550
+ await person.toJsonLd({ contextLoader: mockDocumentLoader }),
2551
+ expected,
2552
+ );
2553
+
2554
+ const loaded = await Person.fromJsonLd(expected, {
2555
+ documentLoader: mockDocumentLoader,
2556
+ contextLoader: mockDocumentLoader,
2557
+ });
2558
+ deepStrictEqual(
2559
+ loaded.featuredCollectionsId,
2560
+ new URL("https://example.com/users/alice/featured_collections"),
2561
+ );
2562
+ assertInstanceOf(loaded.interactionPolicy, InteractionPolicy);
2563
+ assertInstanceOf(loaded.interactionPolicy.canFeature, InteractionRule);
2564
+ deepStrictEqual(
2565
+ loaded.interactionPolicy.canFeature.automaticApproval,
2566
+ new URL("https://www.w3.org/ns/activitystreams#Public"),
1303
2567
  );
1304
2568
  deepStrictEqual(
1305
- collection.inboxOfId,
1306
- new URL("https://example.com/person/bup9a8eqm"),
2569
+ await loaded.toJsonLd({ contextLoader: mockDocumentLoader }),
2570
+ expected,
1307
2571
  );
1308
- });
1309
-
1310
- test("Note.quoteUrl", async () => {
1311
- const note = new Note({
1312
- quoteUrl: new URL("https://example.com/object"),
1313
- });
1314
- const expected = {
1315
- "@context": NOTE_QUOTE_CONTEXT,
1316
- _misskey_quote: "https://example.com/object",
1317
- quoteUri: "https://example.com/object",
1318
- quoteUrl: "https://example.com/object",
1319
- type: "Note",
1320
- };
1321
- deepStrictEqual(await note.toJsonLd(), expected);
1322
- deepStrictEqual(await note.toJsonLd({ format: "compact" }), expected);
1323
2572
 
1324
- const jsonLd: Record<string, unknown> = {
2573
+ const loadedFromFepContext = await Person.fromJsonLd({
1325
2574
  "@context": [
1326
2575
  "https://www.w3.org/ns/activitystreams",
2576
+ "https://gotosocial.org/ns",
2577
+ "https://w3id.org/fep/7aa9",
1327
2578
  {
1328
- _misskey_quote: "misskey:_misskey_quote",
1329
- fedibird: "http://fedibird.com/ns#",
1330
- misskey: "https://misskey-hub.net/ns#",
1331
- quoteUri: "fedibird:quoteUri",
1332
- quoteUrl: "as:quoteUrl",
2579
+ featuredCollections: {
2580
+ "@id": "https://w3id.org/fep/7aa9#featuredCollections",
2581
+ "@type": "@id",
2582
+ },
1333
2583
  },
1334
2584
  ],
1335
- type: "Note",
1336
- quoteUrl: "https://example.com/object",
1337
- _misskey_quote: "https://example.com/object2",
1338
- quoteUri: "https://example.com/object3",
1339
- };
1340
- const loaded = await Note.fromJsonLd(jsonLd);
1341
- deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/object"));
1342
-
1343
- delete jsonLd.quoteUrl;
1344
- const loaded2 = await Note.fromJsonLd(jsonLd);
1345
- deepStrictEqual(loaded2.quoteUrl, new URL("https://example.com/object2"));
1346
-
1347
- delete jsonLd._misskey_quote;
1348
- const loaded3 = await Note.fromJsonLd(jsonLd);
1349
- deepStrictEqual(loaded3.quoteUrl, new URL("https://example.com/object3"));
2585
+ type: "Person",
2586
+ id: "https://example.com/users/alice",
2587
+ featuredCollections: "https://example.com/users/alice/featured_collections",
2588
+ }, {
2589
+ documentLoader: mockDocumentLoader,
2590
+ contextLoader: mockDocumentLoader,
2591
+ });
2592
+ deepStrictEqual(
2593
+ loadedFromFepContext.featuredCollectionsId,
2594
+ new URL("https://example.com/users/alice/featured_collections"),
2595
+ );
1350
2596
  });
1351
2597
 
1352
- test("Note.quote", async () => {
1353
- const note = new Note({
1354
- quote: new URL("https://example.com/object"),
2598
+ test("FeaturedCollection.toJsonLd()", async () => {
2599
+ const collection = new FeaturedCollection({
2600
+ id: new URL("https://example.com/users/alice/featured/1"),
2601
+ name: "Cute cats",
2602
+ attribution: new URL("https://example.com/users/alice"),
2603
+ topic: new Hashtag({ name: "#cats" }),
2604
+ discoverable: false,
2605
+ totalItems: 1,
1355
2606
  });
1356
2607
  const expected = {
1357
- "@context": NOTE_QUOTE_CONTEXT,
1358
- quote: "https://example.com/object",
1359
- type: "Note",
2608
+ "@context": FEATURED_COLLECTION_CONTEXT,
2609
+ type: "FeaturedCollection",
2610
+ id: "https://example.com/users/alice/featured/1",
2611
+ attributedTo: "https://example.com/users/alice",
2612
+ name: "Cute cats",
2613
+ totalItems: 1,
2614
+ topic: {
2615
+ type: "Hashtag",
2616
+ name: "#cats",
2617
+ },
2618
+ discoverable: false,
1360
2619
  };
1361
- deepStrictEqual(await note.toJsonLd(), expected);
1362
- deepStrictEqual(await note.toJsonLd({ format: "compact" }), expected);
2620
+ deepStrictEqual(
2621
+ await collection.toJsonLd({ contextLoader: mockDocumentLoader }),
2622
+ expected,
2623
+ );
1363
2624
 
1364
- const loaded = await Note.fromJsonLd({
1365
- "@context": [
1366
- "https://www.w3.org/ns/activitystreams",
1367
- {
1368
- quote: {
1369
- "@id": "https://w3id.org/fep/044f#quote",
1370
- "@type": "@id",
1371
- },
1372
- },
1373
- ],
1374
- type: "Note",
1375
- quote: "https://example.com/object",
2625
+ const loaded = await FeaturedCollection.fromJsonLd(expected, {
2626
+ documentLoader: mockDocumentLoader,
2627
+ contextLoader: mockDocumentLoader,
1376
2628
  });
1377
- deepStrictEqual(loaded.quoteId, new URL("https://example.com/object"));
2629
+ assertInstanceOf(loaded, FeaturedCollection);
2630
+ deepStrictEqual(loaded.name, "Cute cats");
2631
+ deepStrictEqual(
2632
+ loaded.attributionId,
2633
+ new URL("https://example.com/users/alice"),
2634
+ );
2635
+ assertInstanceOf(loaded.topic, Hashtag);
2636
+ deepStrictEqual(loaded.topic.name, "#cats");
2637
+ deepStrictEqual(loaded.discoverable, false);
2638
+ deepStrictEqual(loaded.totalItems, 1);
2639
+ deepStrictEqual(
2640
+ await loaded.toJsonLd({ contextLoader: mockDocumentLoader }),
2641
+ expected,
2642
+ );
1378
2643
  });
1379
2644
 
1380
- test("Note.quoteAuthorization", async () => {
1381
- const note = new Note({
1382
- quoteAuthorization: new URL("https://example.com/authorizations/1"),
2645
+ test("FeaturedItem.toJsonLd()", async () => {
2646
+ const item = new FeaturedItem({
2647
+ id: new URL("https://example.com/users/alice/featured/1/items/1"),
2648
+ featuredObject: new URL("https://example.com/users/bob"),
2649
+ featureAuthorization: new URL("https://example.com/users/bob/stamps/1"),
1383
2650
  });
1384
2651
  const expected = {
1385
- "@context": NOTE_QUOTE_CONTEXT,
1386
- quoteAuthorization: "https://example.com/authorizations/1",
1387
- type: "Note",
2652
+ "@context": FEATURE_CONTEXT,
2653
+ type: "FeaturedItem",
2654
+ id: "https://example.com/users/alice/featured/1/items/1",
2655
+ featuredObject: "https://example.com/users/bob",
2656
+ featureAuthorization: "https://example.com/users/bob/stamps/1",
1388
2657
  };
1389
- deepStrictEqual(await note.toJsonLd(), expected);
1390
- deepStrictEqual(await note.toJsonLd({ format: "compact" }), expected);
2658
+ deepStrictEqual(
2659
+ await item.toJsonLd({ contextLoader: mockDocumentLoader }),
2660
+ expected,
2661
+ );
1391
2662
 
1392
- const loaded = await Note.fromJsonLd({
1393
- "@context": [
1394
- "https://www.w3.org/ns/activitystreams",
1395
- {
1396
- QuoteAuthorization: "https://w3id.org/fep/044f#QuoteAuthorization",
1397
- quoteAuthorization: {
1398
- "@id": "https://w3id.org/fep/044f#quoteAuthorization",
1399
- "@type": "@id",
1400
- },
1401
- },
1402
- ],
1403
- type: "Note",
1404
- quoteAuthorization: "https://example.com/authorizations/1",
2663
+ const loaded = await FeaturedItem.fromJsonLd(expected, {
2664
+ documentLoader: mockDocumentLoader,
2665
+ contextLoader: mockDocumentLoader,
1405
2666
  });
2667
+ assertInstanceOf(loaded, FeaturedItem);
1406
2668
  deepStrictEqual(
1407
- loaded.quoteAuthorizationId,
1408
- new URL("https://example.com/authorizations/1"),
2669
+ loaded.featuredObjectId,
2670
+ new URL("https://example.com/users/bob"),
2671
+ );
2672
+ deepStrictEqual(
2673
+ loaded.featureAuthorizationId,
2674
+ new URL("https://example.com/users/bob/stamps/1"),
1409
2675
  );
2676
+ });
1410
2677
 
1411
- const loadedFromGoToSocialContext = await Note.fromJsonLd({
1412
- "@context": [
1413
- "https://www.w3.org/ns/activitystreams",
1414
- "https://gotosocial.org/ns",
1415
- ],
1416
- type: "Note",
1417
- quoteAuthorization: "https://example.com/authorizations/2",
1418
- }, {
2678
+ test("FeatureAuthorization.fromJsonLd()", async () => {
2679
+ const jsonLd = {
2680
+ "@context": FEATURE_CONTEXT,
2681
+ type: "FeatureAuthorization",
2682
+ id: "https://example.com/users/bob/stamps/1",
2683
+ interactingObject: "https://example.com/users/alice/featured/1",
2684
+ interactionTarget: "https://example.com/users/bob",
2685
+ };
2686
+ const authorization = await FeatureAuthorization.fromJsonLd(jsonLd, {
1419
2687
  documentLoader: mockDocumentLoader,
1420
2688
  contextLoader: mockDocumentLoader,
1421
2689
  });
2690
+ assertInstanceOf(authorization, FeatureAuthorization);
1422
2691
  deepStrictEqual(
1423
- loadedFromGoToSocialContext.quoteAuthorizationId,
1424
- new URL("https://example.com/authorizations/2"),
2692
+ authorization.interactingObjectId,
2693
+ new URL("https://example.com/users/alice/featured/1"),
2694
+ );
2695
+ deepStrictEqual(
2696
+ authorization.interactionTargetId,
2697
+ new URL("https://example.com/users/bob"),
2698
+ );
2699
+ deepStrictEqual(
2700
+ await authorization.toJsonLd({ contextLoader: mockDocumentLoader }),
2701
+ jsonLd,
1425
2702
  );
1426
2703
  });
1427
2704
 
1428
- test("InteractionPolicy.canQuote", async () => {
1429
- const note = new Note({
1430
- interactionPolicy: new InteractionPolicy({
1431
- canQuote: new InteractionRule({
1432
- automaticApproval: new URL(
1433
- "https://www.w3.org/ns/activitystreams#Public",
1434
- ),
1435
- }),
1436
- }),
2705
+ test("FeatureRequest.toJsonLd()", async () => {
2706
+ const request = new FeatureRequest({
2707
+ id: new URL("https://example.com/users/alice/featured/1/requests/1"),
2708
+ object: new URL("https://example.com/users/bob"),
2709
+ instrument: new URL("https://example.com/users/alice/featured/1"),
1437
2710
  });
1438
2711
  const expected = {
1439
- "@context": NOTE_QUOTE_CONTEXT,
1440
- interactionPolicy: {
1441
- canQuote: {
1442
- automaticApproval: "as:Public",
1443
- },
1444
- },
1445
- type: "Note",
2712
+ "@context": [
2713
+ "https://w3id.org/identity/v1",
2714
+ ...FEATURE_CONTEXT,
2715
+ ],
2716
+ type: "FeatureRequest",
2717
+ id: "https://example.com/users/alice/featured/1/requests/1",
2718
+ object: "https://example.com/users/bob",
2719
+ instrument: "https://example.com/users/alice/featured/1",
1446
2720
  };
1447
2721
  deepStrictEqual(
1448
- await note.toJsonLd({ contextLoader: mockDocumentLoader }),
2722
+ await request.toJsonLd({ contextLoader: mockDocumentLoader }),
1449
2723
  expected,
1450
2724
  );
1451
2725
 
1452
- const loaded = await Note.fromJsonLd(expected, {
2726
+ const loaded = await FeatureRequest.fromJsonLd(expected, {
1453
2727
  documentLoader: mockDocumentLoader,
1454
2728
  contextLoader: mockDocumentLoader,
1455
2729
  });
2730
+ assertInstanceOf(loaded, FeatureRequest);
1456
2731
  deepStrictEqual(
1457
- await loaded.toJsonLd({ contextLoader: mockDocumentLoader }),
1458
- expected,
2732
+ loaded.objectId,
2733
+ new URL("https://example.com/users/bob"),
2734
+ );
2735
+ deepStrictEqual(
2736
+ loaded.instrumentId,
2737
+ new URL("https://example.com/users/alice/featured/1"),
1459
2738
  );
1460
2739
  });
1461
2740
 
@@ -2462,6 +3741,31 @@ test("FEP-fe34: crossOrigin trust behavior", async () => {
2462
3741
  deepStrictEqual(result?.content, "This is a spoofed note");
2463
3742
  });
2464
3743
 
3744
+ test("FEP-fe34: id-less owners honor crossOrigin trust", async () => {
3745
+ const create = await Create.fromJsonLd({
3746
+ "@context": "https://www.w3.org/ns/activitystreams",
3747
+ "@type": "Create",
3748
+ "actor": "https://example.com/actor",
3749
+ "object": {
3750
+ "@type": "Note",
3751
+ "@id": "https://different-origin.com/note",
3752
+ "content": "Embedded note",
3753
+ },
3754
+ });
3755
+
3756
+ const result = await create.getObject({
3757
+ crossOrigin: "trust",
3758
+ // deno-lint-ignore require-await
3759
+ documentLoader: async (url) => {
3760
+ throw new Error(`Unexpected fetch: ${url}`);
3761
+ },
3762
+ });
3763
+
3764
+ assertInstanceOf(result, Note);
3765
+ deepStrictEqual(result.id, new URL("https://different-origin.com/note"));
3766
+ deepStrictEqual(result.content, "Embedded note");
3767
+ });
3768
+
2465
3769
  test("FEP-fe34: Same origin objects are trusted", async () => {
2466
3770
  // deno-lint-ignore require-await
2467
3771
  const sameOriginDocumentLoader = async (url: string) => {
@@ -2609,6 +3913,44 @@ test("FEP-fe34: Constructor vs JSON-LD parsing trust difference", async () => {
2609
3913
  deepStrictEqual(jsonLdResult?.content, "Fetched from origin");
2610
3914
  });
2611
3915
 
3916
+ test(
3917
+ "FEP-fe34: Portable DID authorities are cross-origin boundaries",
3918
+ async () => {
3919
+ const create = await Create.fromJsonLd({
3920
+ "@context": "https://www.w3.org/ns/activitystreams",
3921
+ "@type": "Create",
3922
+ "@id": "ap://did:key:z6MkOwner/create",
3923
+ "actor": "ap://did:key:z6MkOwner/actor",
3924
+ "object": {
3925
+ "@type": "Note",
3926
+ "@id": "ap://did:key:z6MkOther/note",
3927
+ "content": "Embedded portable note",
3928
+ },
3929
+ });
3930
+
3931
+ // deno-lint-ignore require-await
3932
+ const documentLoader = async (url: string) => {
3933
+ if (url === "ap+ef61://did:key:z6MkOther/note") {
3934
+ return {
3935
+ documentUrl: url,
3936
+ contextUrl: null,
3937
+ document: {
3938
+ "@context": "https://www.w3.org/ns/activitystreams",
3939
+ "@type": "Note",
3940
+ "@id": "ap://did:key:z6MkOther/note",
3941
+ "content": "Fetched portable note",
3942
+ },
3943
+ };
3944
+ }
3945
+ throw new Error("Document not found");
3946
+ };
3947
+
3948
+ const result = await create.getObject({ documentLoader });
3949
+ assertInstanceOf(result, Note);
3950
+ deepStrictEqual(result.content, "Fetched portable note");
3951
+ },
3952
+ );
3953
+
2612
3954
  test("FEP-fe34: Array properties respect cross-origin policy", async () => {
2613
3955
  // deno-lint-ignore require-await
2614
3956
  const crossOriginDocumentLoader = async (url: string) => {
@@ -2716,6 +4058,82 @@ test("FEP-fe34: Array properties with crossOrigin trust option", async () => {
2716
4058
  deepStrictEqual((items[1] as Note).content, "Legitimate note 2");
2717
4059
  });
2718
4060
 
4061
+ test("FEP-fe34: id-less arrays honor crossOrigin trust", async () => {
4062
+ const collection = await Collection.fromJsonLd({
4063
+ "@context": "https://www.w3.org/ns/activitystreams",
4064
+ "@type": "Collection",
4065
+ "items": [
4066
+ {
4067
+ "@type": "Note",
4068
+ "@id": "https://different-origin.com/note1",
4069
+ "content": "Embedded note 1",
4070
+ },
4071
+ {
4072
+ "@type": "Note",
4073
+ "@id": "https://different-origin.com/note2",
4074
+ "content": "Embedded note 2",
4075
+ },
4076
+ ],
4077
+ });
4078
+
4079
+ const items = [];
4080
+ for await (
4081
+ const item of collection.getItems({
4082
+ crossOrigin: "trust",
4083
+ // deno-lint-ignore require-await
4084
+ documentLoader: async (url) => {
4085
+ throw new Error(`Unexpected fetch: ${url}`);
4086
+ },
4087
+ })
4088
+ ) {
4089
+ items.push(item);
4090
+ }
4091
+
4092
+ deepStrictEqual(items.length, 2);
4093
+ assertInstanceOf(items[0], Note);
4094
+ assertInstanceOf(items[1], Note);
4095
+ deepStrictEqual((items[0] as Note).content, "Embedded note 1");
4096
+ deepStrictEqual((items[1] as Note).content, "Embedded note 2");
4097
+ });
4098
+
4099
+ test("FEP-fe34: Array properties track trust per item", async () => {
4100
+ const collection = new Collection({
4101
+ id: new URL("https://example.com/collection"),
4102
+ items: [
4103
+ new Note({
4104
+ id: new URL("https://malicious.com/fake-note1"),
4105
+ content: "Trusted constructor note 1",
4106
+ }),
4107
+ new URL("https://different-origin.com/note2"),
4108
+ ],
4109
+ });
4110
+ // deno-lint-ignore require-await
4111
+ const documentLoader = async (url: string) => {
4112
+ if (url === "https://different-origin.com/note2") {
4113
+ return {
4114
+ documentUrl: url,
4115
+ contextUrl: null,
4116
+ document: {
4117
+ "@context": "https://www.w3.org/ns/activitystreams",
4118
+ "@type": "Note",
4119
+ "@id": "https://malicious.com/fake-note2",
4120
+ "content": "Untrusted fetched note 2",
4121
+ },
4122
+ };
4123
+ }
4124
+ throw new Error("Document not found");
4125
+ };
4126
+
4127
+ const items = [];
4128
+ for await (const item of collection.getItems({ documentLoader })) {
4129
+ items.push(item);
4130
+ }
4131
+
4132
+ deepStrictEqual(items.length, 1);
4133
+ assertInstanceOf(items[0], Note);
4134
+ deepStrictEqual((items[0] as Note).content, "Trusted constructor note 1");
4135
+ });
4136
+
2719
4137
  test(
2720
4138
  "FEP-fe34: Embedded objects in arrays from JSON-LD respect cross-origin policy",
2721
4139
  async () => {