@fedify/vocab 2.4.0-dev.1504 → 2.4.0-dev.1528

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,
@@ -479,6 +481,966 @@ test("Activity.fromJsonLd()", async () => {
479
481
  );
480
482
  });
481
483
 
484
+ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
485
+ const did = "did:key:z6Mkabc";
486
+ const portableActor =
487
+ `ap://${did}/actor?gateways=https%3A%2F%2Fserver.example`;
488
+ const portableObject =
489
+ `ap://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
490
+ const uppercasePortableObject =
491
+ `AP://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
492
+ const portablePage = `ap://${did}/actor/outbox?page=2`;
493
+
494
+ const note = await Note.fromJsonLd({
495
+ "@context": "https://www.w3.org/ns/activitystreams",
496
+ type: "Note",
497
+ id: uppercasePortableObject,
498
+ attributedTo: `ap+ef61://${
499
+ encodeURIComponent(did)
500
+ }/actor?gateways=https%3A%2F%2Fserver.example`,
501
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
502
+ deepStrictEqual(
503
+ note.id,
504
+ new URL(
505
+ "ap+ef61://did%3Akey%3Az6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example",
506
+ ),
507
+ );
508
+ deepStrictEqual(
509
+ note.attributionId,
510
+ new URL(
511
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
512
+ ),
513
+ );
514
+ const noteJson = await note.toJsonLd({
515
+ contextLoader: mockDocumentLoader,
516
+ }) as Record<string, unknown>;
517
+ deepStrictEqual(noteJson.type, "Note");
518
+ deepStrictEqual(
519
+ noteJson.id,
520
+ "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example",
521
+ );
522
+ deepStrictEqual(
523
+ noteJson.attributedTo,
524
+ "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
525
+ );
526
+
527
+ const activity = await Activity.fromJsonLd({
528
+ "@context": "https://www.w3.org/ns/activitystreams",
529
+ type: "Create",
530
+ actor: portableActor,
531
+ object: portableObject,
532
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
533
+ deepStrictEqual(
534
+ activity.actorId,
535
+ new URL(
536
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
537
+ ),
538
+ );
539
+ const activityJson = await activity.toJsonLd({
540
+ contextLoader: mockDocumentLoader,
541
+ }) as Record<string, unknown>;
542
+ deepStrictEqual(activityJson.type, "Create");
543
+ deepStrictEqual(
544
+ activityJson.actor,
545
+ "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example",
546
+ );
547
+ deepStrictEqual(
548
+ activityJson.object,
549
+ "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example",
550
+ );
551
+
552
+ const person = await Person.fromJsonLd({
553
+ "@context": "https://www.w3.org/ns/activitystreams",
554
+ type: "Person",
555
+ inbox: `ap+ef61://${did}/actor/inbox`,
556
+ outbox: `ap+ef61://${did}/actor/outbox`,
557
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
558
+ deepStrictEqual(
559
+ person.inboxId,
560
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/inbox"),
561
+ );
562
+ deepStrictEqual(
563
+ person.outboxId,
564
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox"),
565
+ );
566
+ const personJson = await person.toJsonLd({
567
+ contextLoader: mockDocumentLoader,
568
+ }) as Record<string, unknown>;
569
+ deepStrictEqual(personJson.type, "Person");
570
+ deepStrictEqual(personJson.inbox, "ap+ef61://did:key:z6Mkabc/actor/inbox");
571
+ deepStrictEqual(
572
+ personJson.outbox,
573
+ "ap+ef61://did:key:z6Mkabc/actor/outbox",
574
+ );
575
+
576
+ const page = await OrderedCollectionPage.fromJsonLd({
577
+ "@context": "https://www.w3.org/ns/activitystreams",
578
+ type: "OrderedCollectionPage",
579
+ next: portablePage,
580
+ prev: `ap+ef61://${encodeURIComponent(did)}/actor/outbox?page=1`,
581
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
582
+ deepStrictEqual(
583
+ page.nextId,
584
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=2"),
585
+ );
586
+ deepStrictEqual(
587
+ page.prevId,
588
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=1"),
589
+ );
590
+ const pageJson = await page.toJsonLd({
591
+ contextLoader: mockDocumentLoader,
592
+ }) as Record<string, unknown>;
593
+ deepStrictEqual(pageJson.type, "OrderedCollectionPage");
594
+ deepStrictEqual(
595
+ pageJson.next,
596
+ "ap+ef61://did:key:z6Mkabc/actor/outbox?page=2",
597
+ );
598
+ deepStrictEqual(
599
+ pageJson.prev,
600
+ "ap+ef61://did:key:z6Mkabc/actor/outbox?page=1",
601
+ );
602
+ });
603
+
604
+ test("fromJsonLd() caches text that mentions portable ActivityPub IRIs", async () => {
605
+ const noteJson = {
606
+ "@context": [
607
+ "https://www.w3.org/ns/activitystreams",
608
+ { extra: "https://example.com/ns#extra" },
609
+ ],
610
+ type: "Note",
611
+ id: "https://example.com/notes/1",
612
+ content: "This is text about ap://did:key:z6Mkabc/actor.",
613
+ extra: "This extension property should stay cached.",
614
+ };
615
+
616
+ const note = await Note.fromJsonLd(noteJson, {
617
+ documentLoader: mockDocumentLoader,
618
+ contextLoader: mockDocumentLoader,
619
+ });
620
+
621
+ deepStrictEqual(await note.toJsonLd(), noteJson);
622
+ });
623
+
624
+ test("fromJsonLd() preserves extensions with portable ActivityPub IRIs", async () => {
625
+ const note = await Note.fromJsonLd({
626
+ "@context": [
627
+ "https://www.w3.org/ns/activitystreams",
628
+ { extra: "https://example.com/ns#extra" },
629
+ ],
630
+ type: "Note",
631
+ id: "ap://did:key:z6Mkabc/objects/1",
632
+ extra: "This extension property should stay cached.",
633
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
634
+
635
+ const jsonLd = await note.toJsonLd() as Record<string, unknown>;
636
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
637
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
638
+ });
639
+
640
+ test("fromJsonLd() preserves unmapped terms with portable IRIs", async () => {
641
+ const note = await Note.fromJsonLd({
642
+ "@context": "https://www.w3.org/ns/activitystreams",
643
+ type: "Note",
644
+ id: "ap://did:key:z6Mkabc/objects/1",
645
+ extra: "This unmapped property should stay cached.",
646
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
647
+
648
+ const jsonLd = await note.toJsonLd() as Record<string, unknown>;
649
+ deepStrictEqual(
650
+ jsonLd.extra,
651
+ "This unmapped property should stay cached.",
652
+ );
653
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
654
+ });
655
+
656
+ test("fromJsonLd() preserves expanded arrays with portable IRIs", async () => {
657
+ const expanded = [
658
+ {
659
+ "@id": "https://example.com/activities/1",
660
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
661
+ "https://www.w3.org/ns/activitystreams#actor": [
662
+ { "@id": "ap://did:key:z6Mkabc/actor" },
663
+ ],
664
+ "https://www.w3.org/ns/activitystreams#object": [
665
+ { "@id": "https://example.com/objects/1" },
666
+ ],
667
+ },
668
+ {
669
+ "@id": "https://example.com/objects/1",
670
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
671
+ "https://www.w3.org/ns/activitystreams#content": [
672
+ { "@value": "Sibling node should stay cached." },
673
+ ],
674
+ },
675
+ ];
676
+
677
+ const activity = await Activity.fromJsonLd(expanded, {
678
+ documentLoader: mockDocumentLoader,
679
+ contextLoader: mockDocumentLoader,
680
+ });
681
+
682
+ deepStrictEqual(
683
+ activity.actorId,
684
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
685
+ );
686
+ deepStrictEqual(await activity.toJsonLd(), [
687
+ {
688
+ "@id": "https://example.com/activities/1",
689
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
690
+ "https://www.w3.org/ns/activitystreams#actor": [
691
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
692
+ ],
693
+ "https://www.w3.org/ns/activitystreams#object": [
694
+ { "@id": "https://example.com/objects/1" },
695
+ ],
696
+ },
697
+ {
698
+ "@id": "https://example.com/objects/1",
699
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
700
+ "https://www.w3.org/ns/activitystreams#content": [
701
+ { "@value": "Sibling node should stay cached." },
702
+ ],
703
+ },
704
+ ]);
705
+ deepStrictEqual(expanded[0]["https://www.w3.org/ns/activitystreams#actor"], [
706
+ { "@id": "ap://did:key:z6Mkabc/actor" },
707
+ ]);
708
+ });
709
+
710
+ test("fromJsonLd() preserves single-node expanded arrays with portable IRIs", async () => {
711
+ const expanded = [
712
+ {
713
+ "@id": "ap://did:key:z6Mkabc/objects/1",
714
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
715
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
716
+ { "@id": "ap://did:key:z6Mkabc/actor" },
717
+ ],
718
+ "https://www.w3.org/ns/activitystreams#content": [
719
+ { "@value": "Single expanded node should stay cached as an array." },
720
+ ],
721
+ },
722
+ ];
723
+
724
+ const note = await Note.fromJsonLd(expanded, {
725
+ documentLoader: mockDocumentLoader,
726
+ contextLoader: mockDocumentLoader,
727
+ });
728
+
729
+ deepStrictEqual(await note.toJsonLd(), [
730
+ {
731
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
732
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
733
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
734
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
735
+ ],
736
+ "https://www.w3.org/ns/activitystreams#content": [
737
+ { "@value": "Single expanded node should stay cached as an array." },
738
+ ],
739
+ },
740
+ ]);
741
+ deepStrictEqual(expanded[0]["@id"], "ap://did:key:z6Mkabc/objects/1");
742
+ });
743
+
744
+ test("fromJsonLd() preserves no-context object shape with portable IRIs", async () => {
745
+ const expanded = {
746
+ "@id": "ap://did:key:z6Mkabc/objects/1",
747
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
748
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
749
+ { "@id": "ap://did:key:z6Mkabc/actor" },
750
+ ],
751
+ "https://www.w3.org/ns/activitystreams#content": [
752
+ { "@value": "No-context object shape should stay cached." },
753
+ ],
754
+ };
755
+
756
+ const note = await Note.fromJsonLd(expanded, {
757
+ documentLoader: mockDocumentLoader,
758
+ contextLoader: mockDocumentLoader,
759
+ });
760
+
761
+ deepStrictEqual(await note.toJsonLd(), {
762
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
763
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
764
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
765
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
766
+ ],
767
+ "https://www.w3.org/ns/activitystreams#content": [
768
+ { "@value": "No-context object shape should stay cached." },
769
+ ],
770
+ });
771
+ deepStrictEqual(expanded["@id"], "ap://did:key:z6Mkabc/objects/1");
772
+ });
773
+
774
+ test("fromJsonLd() preserves expanded subtype cache types", async () => {
775
+ const expanded = [
776
+ {
777
+ "@id": "https://example.com/activities/1",
778
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
779
+ "https://www.w3.org/ns/activitystreams#actor": [
780
+ { "@id": "https://example.com/actors/alice" },
781
+ ],
782
+ "https://www.w3.org/ns/activitystreams#object": [
783
+ { "@id": "https://example.com/objects/1" },
784
+ ],
785
+ },
786
+ {
787
+ "@id": "https://example.com/objects/1",
788
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
789
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
790
+ { "@id": "ap://did:key:z6Mkabc/actor" },
791
+ ],
792
+ },
793
+ ];
794
+
795
+ const activity = await Activity.fromJsonLd(expanded, {
796
+ documentLoader: mockDocumentLoader,
797
+ contextLoader: mockDocumentLoader,
798
+ });
799
+
800
+ deepStrictEqual(await activity.toJsonLd(), [
801
+ {
802
+ "@id": "https://example.com/activities/1",
803
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
804
+ "https://www.w3.org/ns/activitystreams#actor": [
805
+ { "@id": "https://example.com/actors/alice" },
806
+ ],
807
+ "https://www.w3.org/ns/activitystreams#object": [
808
+ { "@id": "https://example.com/objects/1" },
809
+ ],
810
+ },
811
+ {
812
+ "@id": "https://example.com/objects/1",
813
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
814
+ "https://www.w3.org/ns/activitystreams#attributedTo": [
815
+ { "@id": "ap+ef61://did:key:z6Mkabc/actor" },
816
+ ],
817
+ },
818
+ ]);
819
+ deepStrictEqual(expanded[0]["@type"], [
820
+ "https://www.w3.org/ns/activitystreams#Create",
821
+ ]);
822
+ });
823
+
824
+ test("fromJsonLd() preserves compact array contexts with portable IRIs", async () => {
825
+ const note = await Note.fromJsonLd([{
826
+ "@context": "https://www.w3.org/ns/activitystreams",
827
+ type: "Note",
828
+ id: "ap://did:key:z6Mkabc/objects/1",
829
+ }], {
830
+ documentLoader: mockDocumentLoader,
831
+ contextLoader: mockDocumentLoader,
832
+ });
833
+
834
+ deepStrictEqual(await note.toJsonLd(), [{
835
+ "@context": "https://www.w3.org/ns/activitystreams",
836
+ type: "Note",
837
+ id: "ap+ef61://did:key:z6Mkabc/objects/1",
838
+ }]);
839
+ });
840
+
841
+ test("fromJsonLd() preserves compact single-item arrays with portable IRIs", async () => {
842
+ const createJson = {
843
+ "@context": "https://www.w3.org/ns/activitystreams",
844
+ type: "Create",
845
+ actor: ["ap://did:key:z6Mkabc/actor"],
846
+ object: "https://example.com/objects/1",
847
+ };
848
+
849
+ const create = await Create.fromJsonLd(createJson, {
850
+ documentLoader: mockDocumentLoader,
851
+ contextLoader: mockDocumentLoader,
852
+ });
853
+
854
+ deepStrictEqual(await create.toJsonLd(), {
855
+ "@context": "https://www.w3.org/ns/activitystreams",
856
+ type: "Create",
857
+ actor: ["ap+ef61://did:key:z6Mkabc/actor"],
858
+ object: "https://example.com/objects/1",
859
+ });
860
+ deepStrictEqual(createJson.actor, ["ap://did:key:z6Mkabc/actor"]);
861
+ });
862
+
863
+ test("fromJsonLd() preserves compact multi-node arrays with portable IRIs", async () => {
864
+ const activityJson = [
865
+ {
866
+ "@context": "https://www.w3.org/ns/activitystreams",
867
+ type: "Create",
868
+ id: "https://example.com/activities/1",
869
+ actor: "https://example.com/actors/alice",
870
+ object: "https://example.com/objects/1",
871
+ },
872
+ {
873
+ "@context": "https://www.w3.org/ns/activitystreams",
874
+ type: "Note",
875
+ id: "https://example.com/objects/1",
876
+ attributedTo: "ap://did:key:z6Mkabc/actor",
877
+ },
878
+ ];
879
+
880
+ const activity = await Activity.fromJsonLd(activityJson, {
881
+ documentLoader: mockDocumentLoader,
882
+ contextLoader: mockDocumentLoader,
883
+ });
884
+
885
+ deepStrictEqual(await activity.toJsonLd(), [
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+ef61://did:key:z6Mkabc/actor",
898
+ },
899
+ ]);
900
+ });
901
+
902
+ test("fromJsonLd() preserves nested unmapped terms with portable IRIs", async () => {
903
+ const noteJson = {
904
+ "@context": "https://www.w3.org/ns/activitystreams",
905
+ type: "Note",
906
+ id: "ap://did:key:z6Mkabc/objects/1",
907
+ attachment: {
908
+ type: "Object",
909
+ name: "Attachment with an unmapped extension.",
910
+ extra: "This nested unmapped property should stay cached.",
911
+ },
912
+ };
913
+
914
+ const note = await Note.fromJsonLd(noteJson, {
915
+ documentLoader: mockDocumentLoader,
916
+ contextLoader: mockDocumentLoader,
917
+ });
918
+
919
+ deepStrictEqual(await note.toJsonLd(), {
920
+ "@context": "https://www.w3.org/ns/activitystreams",
921
+ type: "Note",
922
+ id: "ap+ef61://did:key:z6Mkabc/objects/1",
923
+ attachment: {
924
+ type: "Object",
925
+ name: "Attachment with an unmapped extension.",
926
+ extra: "This nested unmapped property should stay cached.",
927
+ },
928
+ });
929
+ });
930
+
931
+ test("fromJsonLd() preserves compact array item extension contexts with portable IRIs", async () => {
932
+ const activityJson = [
933
+ {
934
+ "@context": "https://www.w3.org/ns/activitystreams",
935
+ type: "Create",
936
+ id: "https://example.com/activities/1",
937
+ actor: "https://example.com/actors/alice",
938
+ object: "https://example.com/objects/1",
939
+ },
940
+ {
941
+ "@context": [
942
+ "https://www.w3.org/ns/activitystreams",
943
+ {
944
+ extraRef: {
945
+ "@id": "https://example.com/ns#extraRef",
946
+ "@type": "@id",
947
+ },
948
+ },
949
+ ],
950
+ type: "Note",
951
+ id: "https://example.com/objects/1",
952
+ extraRef: "ap://did:key:z6Mkabc/extra",
953
+ },
954
+ ];
955
+
956
+ const activity = await Activity.fromJsonLd(activityJson, {
957
+ documentLoader: mockDocumentLoader,
958
+ contextLoader: mockDocumentLoader,
959
+ });
960
+
961
+ deepStrictEqual(await activity.toJsonLd(), [
962
+ {
963
+ "@context": "https://www.w3.org/ns/activitystreams",
964
+ type: "Create",
965
+ id: "https://example.com/activities/1",
966
+ actor: "https://example.com/actors/alice",
967
+ object: "https://example.com/objects/1",
968
+ },
969
+ {
970
+ "@context": [
971
+ "https://www.w3.org/ns/activitystreams",
972
+ {
973
+ extraRef: {
974
+ "@id": "https://example.com/ns#extraRef",
975
+ "@type": "@id",
976
+ },
977
+ },
978
+ ],
979
+ type: "Note",
980
+ id: "https://example.com/objects/1",
981
+ extraRef: "ap+ef61://did:key:z6Mkabc/extra",
982
+ },
983
+ ]);
984
+ });
985
+
986
+ test("fromJsonLd() formats portable IRIs in JSON-LD containers", async () => {
987
+ const note = await Note.fromJsonLd({
988
+ "@context": "https://www.w3.org/ns/activitystreams",
989
+ type: "Note",
990
+ id: "https://example.com/notes/1",
991
+ attributedTo: {
992
+ "@list": ["ap://did:key:z6Mkabc/actor"],
993
+ },
994
+ to: {
995
+ "@set": ["ap://did:key:z6Mkabc/followers"],
996
+ },
997
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
998
+
999
+ const jsonLd = await note.toJsonLd() as Record<string, unknown>;
1000
+ deepStrictEqual(jsonLd.attributedTo, {
1001
+ "@list": ["ap+ef61://did:key:z6Mkabc/actor"],
1002
+ });
1003
+ deepStrictEqual(jsonLd.to, "ap+ef61://did:key:z6Mkabc/followers");
1004
+ });
1005
+
1006
+ test("fromJsonLd() formats portable IRIs hidden behind JSON-LD aliases", async () => {
1007
+ const activity = await Activity.fromJsonLd({
1008
+ "@context": [
1009
+ "https://www.w3.org/ns/activitystreams",
1010
+ {
1011
+ as: {
1012
+ "@id": "https://www.w3.org/ns/activitystreams#",
1013
+ "@prefix": true,
1014
+ },
1015
+ extra: "https://example.com/ns#extra",
1016
+ extraRef: {
1017
+ "@id": "https://example.com/ns#extraRef",
1018
+ "@type": "@id",
1019
+ },
1020
+ actorRef: {
1021
+ "@id": "as:actor",
1022
+ "@type": "@id",
1023
+ },
1024
+ targetRef: {
1025
+ "@id": "as:target",
1026
+ "@type": "@id",
1027
+ },
1028
+ },
1029
+ ],
1030
+ type: "Create",
1031
+ actorRef: "ap://did:key:z6Mkabc/actor",
1032
+ object: "https://example.com/objects/1",
1033
+ targetRef: "ap://did:key:z6Mkabc/target",
1034
+ extra: "This extension property should stay cached.",
1035
+ extraRef: "ap://did:key:z6Mkabc/extra",
1036
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1037
+
1038
+ deepStrictEqual(
1039
+ activity.actorId,
1040
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
1041
+ );
1042
+ deepStrictEqual(
1043
+ activity.targetId,
1044
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/target"),
1045
+ );
1046
+ const jsonLd = await activity.toJsonLd({
1047
+ contextLoader: mockDocumentLoader,
1048
+ }) as Record<string, unknown>;
1049
+ deepStrictEqual(jsonLd.actor, "ap+ef61://did:key:z6Mkabc/actor");
1050
+ deepStrictEqual("actorRef" in jsonLd, false);
1051
+ deepStrictEqual(jsonLd.target, "ap+ef61://did:key:z6Mkabc/target");
1052
+ deepStrictEqual("targetRef" in jsonLd, false);
1053
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
1054
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1055
+ });
1056
+
1057
+ test("fromJsonLd() preserves portable IRIs in @id extension terms", async () => {
1058
+ const note = await Note.fromJsonLd({
1059
+ "@context": [
1060
+ "https://www.w3.org/ns/activitystreams",
1061
+ {
1062
+ extraRef: {
1063
+ "@id": "https://example.com/ns#extraRef",
1064
+ "@type": "@id",
1065
+ },
1066
+ },
1067
+ ],
1068
+ type: "Note",
1069
+ id: "https://example.com/notes/1",
1070
+ extraRef: "ap://did:key:z6Mkabc/extra",
1071
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1072
+
1073
+ const jsonLd = await note.toJsonLd({
1074
+ contextLoader: mockDocumentLoader,
1075
+ }) as Record<string, unknown>;
1076
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1077
+ });
1078
+
1079
+ test("fromJsonLd() ignores malformed portable IRIs in extension cache terms", async () => {
1080
+ const noteJson = {
1081
+ "@context": [
1082
+ "https://www.w3.org/ns/activitystreams",
1083
+ {
1084
+ extraRef: {
1085
+ "@id": "https://example.com/ns#extraRef",
1086
+ "@type": "@id",
1087
+ },
1088
+ },
1089
+ ],
1090
+ type: "Note",
1091
+ id: "https://example.com/notes/1",
1092
+ extraRef: "ap://example.com/not-portable",
1093
+ };
1094
+
1095
+ const note = await Note.fromJsonLd(noteJson, {
1096
+ documentLoader: mockDocumentLoader,
1097
+ contextLoader: mockDocumentLoader,
1098
+ });
1099
+
1100
+ deepStrictEqual(await note.toJsonLd(), noteJson);
1101
+ });
1102
+
1103
+ test("fromJsonLd() preserves portable IRIs in @id typed terms", async () => {
1104
+ const note = await Note.fromJsonLd({
1105
+ "@context": [
1106
+ "https://www.w3.org/ns/activitystreams",
1107
+ {
1108
+ "@vocab": "https://example.com/ns#",
1109
+ extraRef: { "@type": "@id" },
1110
+ },
1111
+ ],
1112
+ type: "Note",
1113
+ id: "https://example.com/notes/1",
1114
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1115
+ extraRef: "ap://did:key:z6Mkabc/extra",
1116
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1117
+
1118
+ const jsonLd = await note.toJsonLd({
1119
+ contextLoader: mockDocumentLoader,
1120
+ }) as Record<string, unknown>;
1121
+ deepStrictEqual(
1122
+ jsonLd.content,
1123
+ "This text mentions ap://did:key:z6Mkabc/text.",
1124
+ );
1125
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1126
+ });
1127
+
1128
+ test("fromJsonLd() preserves portable IRIs hidden behind remote contexts", async () => {
1129
+ const contextUrl = "https://example.com/contexts/portable-iris";
1130
+ const contextLoader: DocumentLoader = async (
1131
+ resource: string,
1132
+ options,
1133
+ ): Promise<RemoteDocument> => {
1134
+ if (resource === contextUrl) {
1135
+ return {
1136
+ contextUrl: null,
1137
+ documentUrl: resource,
1138
+ document: {
1139
+ "@context": [
1140
+ "https://www.w3.org/ns/activitystreams",
1141
+ {
1142
+ "@vocab": "https://example.com/ns#",
1143
+ extra: "https://example.com/ns#extra",
1144
+ extraRef: { "@type": "@id" },
1145
+ },
1146
+ ],
1147
+ },
1148
+ };
1149
+ }
1150
+ return await mockDocumentLoader(resource, options);
1151
+ };
1152
+ const note = await Note.fromJsonLd({
1153
+ "@context": contextUrl,
1154
+ type: "Note",
1155
+ id: "https://example.com/notes/1",
1156
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1157
+ extra: "This extension property should stay cached.",
1158
+ extraRef: "ap://did:key:z6Mkabc/extra",
1159
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1160
+
1161
+ const jsonLd = await note.toJsonLd({ contextLoader }) as Record<
1162
+ string,
1163
+ unknown
1164
+ >;
1165
+ deepStrictEqual(
1166
+ jsonLd.content,
1167
+ "This text mentions ap://did:key:z6Mkabc/text.",
1168
+ );
1169
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
1170
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
1171
+ });
1172
+
1173
+ test("fromJsonLd() formats portable IRIs hidden behind nested remote contexts", async () => {
1174
+ const rootContextUrl = "https://example.com/contexts/nested-portable-iris";
1175
+ const nestedContextUrl = "https://example.com/contexts/nested-portable-ref";
1176
+ const contextLoader: DocumentLoader = async (
1177
+ resource: string,
1178
+ options,
1179
+ ): Promise<RemoteDocument> => {
1180
+ if (resource === rootContextUrl) {
1181
+ return {
1182
+ contextUrl: null,
1183
+ documentUrl: resource,
1184
+ document: {
1185
+ "@context": [
1186
+ "https://www.w3.org/ns/activitystreams",
1187
+ {
1188
+ "@vocab": "https://example.com/ns#",
1189
+ extraContainer: "https://example.com/ns#extraContainer",
1190
+ },
1191
+ ],
1192
+ },
1193
+ };
1194
+ }
1195
+ if (resource === nestedContextUrl) {
1196
+ return {
1197
+ contextUrl: null,
1198
+ documentUrl: resource,
1199
+ document: {
1200
+ "@context": {
1201
+ "@vocab": "https://example.com/ns#",
1202
+ extra: "https://example.com/ns#extra",
1203
+ extraRef: { "@type": "@id" },
1204
+ },
1205
+ },
1206
+ };
1207
+ }
1208
+ return await mockDocumentLoader(resource, options);
1209
+ };
1210
+ const note = await Note.fromJsonLd({
1211
+ "@context": rootContextUrl,
1212
+ type: "Note",
1213
+ id: "https://example.com/notes/1",
1214
+ extraContainer: {
1215
+ "@context": nestedContextUrl,
1216
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1217
+ extra: "This nested extension object should stay cached.",
1218
+ extraRef: "ap://did:key:z6Mkabc/extra",
1219
+ },
1220
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1221
+
1222
+ const jsonLd = await note.toJsonLd({ contextLoader }) as Record<
1223
+ string,
1224
+ unknown
1225
+ >;
1226
+ deepStrictEqual(jsonLd.extraContainer, {
1227
+ "@context": nestedContextUrl,
1228
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
1229
+ extra: "This nested extension object should stay cached.",
1230
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/extra" },
1231
+ });
1232
+ });
1233
+
1234
+ test("fromJsonLd() formats portable IRIs in sibling remote-context objects", async () => {
1235
+ const rootContextUrl = "https://example.com/contexts/sibling-containers";
1236
+ const nestedContextUrl = "https://example.com/contexts/sibling-extra-ref";
1237
+ const contextLoader: DocumentLoader = async (
1238
+ resource: string,
1239
+ options,
1240
+ ): Promise<RemoteDocument> => {
1241
+ if (resource === rootContextUrl) {
1242
+ return {
1243
+ contextUrl: null,
1244
+ documentUrl: resource,
1245
+ document: {
1246
+ "@context": [
1247
+ "https://www.w3.org/ns/activitystreams",
1248
+ {
1249
+ "@vocab": "https://example.com/ns#",
1250
+ firstExtraContainer: "https://example.com/ns#firstExtraContainer",
1251
+ secondExtraContainer:
1252
+ "https://example.com/ns#secondExtraContainer",
1253
+ },
1254
+ ],
1255
+ },
1256
+ };
1257
+ }
1258
+ if (resource === nestedContextUrl) {
1259
+ return {
1260
+ contextUrl: null,
1261
+ documentUrl: resource,
1262
+ document: {
1263
+ "@context": {
1264
+ "@vocab": "https://example.com/ns#",
1265
+ extraRef: { "@type": "@id" },
1266
+ },
1267
+ },
1268
+ };
1269
+ }
1270
+ return await mockDocumentLoader(resource, options);
1271
+ };
1272
+ const note = await Note.fromJsonLd({
1273
+ "@context": rootContextUrl,
1274
+ type: "Note",
1275
+ id: "https://example.com/notes/1",
1276
+ firstExtraContainer: {
1277
+ "@context": nestedContextUrl,
1278
+ content: "No portable IRI here.",
1279
+ },
1280
+ secondExtraContainer: {
1281
+ "@context": nestedContextUrl,
1282
+ extraRef: "ap://did:key:z6Mkabc/second",
1283
+ },
1284
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1285
+
1286
+ const jsonLd = await note.toJsonLd({ contextLoader }) as Record<
1287
+ string,
1288
+ unknown
1289
+ >;
1290
+ deepStrictEqual(jsonLd.firstExtraContainer, {
1291
+ "@context": nestedContextUrl,
1292
+ content: "No portable IRI here.",
1293
+ });
1294
+ deepStrictEqual(jsonLd.secondExtraContainer, {
1295
+ "@context": nestedContextUrl,
1296
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/second" },
1297
+ });
1298
+ });
1299
+
1300
+ test("fromJsonLd() batches unmapped portable IRI term checks", async () => {
1301
+ const contextUrl = "https://example.com/contexts/batched-portable-aliases";
1302
+ let contextLoads = 0;
1303
+ const contextLoader: DocumentLoader = async (
1304
+ resource: string,
1305
+ options,
1306
+ ): Promise<RemoteDocument> => {
1307
+ if (resource === contextUrl) {
1308
+ contextLoads++;
1309
+ return {
1310
+ contextUrl: null,
1311
+ documentUrl: resource,
1312
+ document: {
1313
+ "@context": [
1314
+ "https://www.w3.org/ns/activitystreams",
1315
+ {
1316
+ as: {
1317
+ "@id": "https://www.w3.org/ns/activitystreams#",
1318
+ "@prefix": true,
1319
+ },
1320
+ actorRef0: { "@id": "as:actor", "@type": "@id" },
1321
+ actorRef1: { "@id": "as:actor", "@type": "@id" },
1322
+ targetRef0: { "@id": "as:target", "@type": "@id" },
1323
+ targetRef1: { "@id": "as:target", "@type": "@id" },
1324
+ objectRef0: { "@id": "as:object", "@type": "@id" },
1325
+ objectRef1: { "@id": "as:object", "@type": "@id" },
1326
+ },
1327
+ ],
1328
+ },
1329
+ };
1330
+ }
1331
+ return await mockDocumentLoader(resource, options);
1332
+ };
1333
+
1334
+ const activity = await Activity.fromJsonLd({
1335
+ "@context": contextUrl,
1336
+ type: "Create",
1337
+ actorRef0: "ap://did:key:z6Mkabc/actor0",
1338
+ actorRef1: "ap://did:key:z6Mkabc/actor1",
1339
+ targetRef0: "ap://did:key:z6Mkabc/target0",
1340
+ targetRef1: "ap://did:key:z6Mkabc/target1",
1341
+ objectRef0: "https://example.com/objects/0",
1342
+ objectRef1: "https://example.com/objects/1",
1343
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1344
+
1345
+ await activity.toJsonLd({ contextLoader });
1346
+
1347
+ ok(contextLoads <= 5);
1348
+ });
1349
+
1350
+ test("fromJsonLd() falls back when portable IRI cache merge fails", async () => {
1351
+ const contextUrl = "https://example.com/contexts/failing-cache-merge";
1352
+ let contextLoads = 0;
1353
+ const contextLoader: DocumentLoader = async (
1354
+ resource: string,
1355
+ options,
1356
+ ): Promise<RemoteDocument> => {
1357
+ if (resource === contextUrl) {
1358
+ contextLoads++;
1359
+ if (contextLoads > 1) throw new Error("merge context unavailable");
1360
+ return {
1361
+ contextUrl: null,
1362
+ documentUrl: resource,
1363
+ document: {
1364
+ "@context": [
1365
+ "https://www.w3.org/ns/activitystreams",
1366
+ {
1367
+ "@vocab": "https://example.com/ns#",
1368
+ extraRef: { "@type": "@id" },
1369
+ },
1370
+ ],
1371
+ },
1372
+ };
1373
+ }
1374
+ return await mockDocumentLoader(resource, options);
1375
+ };
1376
+
1377
+ const note = await Note.fromJsonLd({
1378
+ "@context": contextUrl,
1379
+ type: "Note",
1380
+ id: "ap://did:key:z6Mkabc/objects/1",
1381
+ extraRef: "ap://did:key:z6Mkabc/extra",
1382
+ }, { documentLoader: mockDocumentLoader, contextLoader });
1383
+
1384
+ const jsonLd = await note.toJsonLd({
1385
+ contextLoader: mockDocumentLoader,
1386
+ }) as Record<string, unknown>;
1387
+
1388
+ deepStrictEqual(jsonLd.type, "Note");
1389
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
1390
+ });
1391
+
1392
+ test("fromJsonLd() formats portable IRIs in scalar URL values", async () => {
1393
+ const note = await Note.fromJsonLd({
1394
+ "@context": [
1395
+ "https://www.w3.org/ns/activitystreams",
1396
+ "https://gotosocial.org/ns",
1397
+ { quoteUrl: "as:quoteUrl" },
1398
+ ],
1399
+ type: "Note",
1400
+ id: "https://example.com/notes/1",
1401
+ quoteUrl: "ap://did:key:z6Mkabc/objects/1",
1402
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1403
+
1404
+ deepStrictEqual(
1405
+ note.quoteUrl,
1406
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"),
1407
+ );
1408
+ const jsonLd = await note.toJsonLd({
1409
+ contextLoader: mockDocumentLoader,
1410
+ }) as Record<string, unknown>;
1411
+ deepStrictEqual(jsonLd.quoteUrl, "ap+ef61://did:key:z6Mkabc/objects/1");
1412
+ });
1413
+
1414
+ test("fromJsonLd() formats portable IRIs in URL value lists", async () => {
1415
+ const note = await Note.fromJsonLd({
1416
+ "@context": [
1417
+ "https://www.w3.org/ns/activitystreams",
1418
+ "https://gotosocial.org/ns",
1419
+ { quoteUrl: "as:quoteUrl" },
1420
+ ],
1421
+ type: "Note",
1422
+ id: "https://example.com/notes/1",
1423
+ quoteUrl: {
1424
+ "@list": [
1425
+ { "@value": "ap://did:key:z6Mkabc/objects/1" },
1426
+ ],
1427
+ },
1428
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
1429
+
1430
+ deepStrictEqual(
1431
+ note.quoteUrl,
1432
+ new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"),
1433
+ );
1434
+ const jsonLd = await note.toJsonLd({
1435
+ contextLoader: mockDocumentLoader,
1436
+ }) as Record<string, unknown>;
1437
+ deepStrictEqual(jsonLd.quoteUrl, {
1438
+ "@list": [
1439
+ "ap+ef61://did:key:z6Mkabc/objects/1",
1440
+ ],
1441
+ });
1442
+ });
1443
+
482
1444
  test({
483
1445
  name: "Activity.getObject()",
484
1446
  permissions: { env: true, read: true },
@@ -544,6 +1506,37 @@ test({
544
1506
  },
545
1507
  });
546
1508
 
1509
+ test("Activity.getObject() fetches canonical portable IRIs", async () => {
1510
+ const fetchedUrls: string[] = [];
1511
+ // deno-lint-ignore require-await
1512
+ const documentLoader: DocumentLoader = async (url) => {
1513
+ fetchedUrls.push(url);
1514
+ return {
1515
+ contextUrl: null,
1516
+ documentUrl: url,
1517
+ document: {
1518
+ "@context": "https://www.w3.org/ns/activitystreams",
1519
+ id: url,
1520
+ type: "Note",
1521
+ content: "Fetched portable object",
1522
+ },
1523
+ };
1524
+ };
1525
+ const activity = await Activity.fromJsonLd({
1526
+ "@context": "https://www.w3.org/ns/activitystreams",
1527
+ type: "Create",
1528
+ object: "ap://did:key:z6Mkabc/objects/1",
1529
+ }, { documentLoader, contextLoader: mockDocumentLoader });
1530
+
1531
+ const object = await activity.getObject({
1532
+ documentLoader,
1533
+ contextLoader: mockDocumentLoader,
1534
+ });
1535
+
1536
+ assertInstanceOf(object, Note);
1537
+ deepStrictEqual(fetchedUrls, ["ap+ef61://did:key:z6Mkabc/objects/1"]);
1538
+ });
1539
+
547
1540
  test({
548
1541
  name: "Activity.getObjects()",
549
1542
  permissions: { env: true, read: true },
@@ -2462,6 +3455,31 @@ test("FEP-fe34: crossOrigin trust behavior", async () => {
2462
3455
  deepStrictEqual(result?.content, "This is a spoofed note");
2463
3456
  });
2464
3457
 
3458
+ test("FEP-fe34: id-less owners honor crossOrigin trust", async () => {
3459
+ const create = await Create.fromJsonLd({
3460
+ "@context": "https://www.w3.org/ns/activitystreams",
3461
+ "@type": "Create",
3462
+ "actor": "https://example.com/actor",
3463
+ "object": {
3464
+ "@type": "Note",
3465
+ "@id": "https://different-origin.com/note",
3466
+ "content": "Embedded note",
3467
+ },
3468
+ });
3469
+
3470
+ const result = await create.getObject({
3471
+ crossOrigin: "trust",
3472
+ // deno-lint-ignore require-await
3473
+ documentLoader: async (url) => {
3474
+ throw new Error(`Unexpected fetch: ${url}`);
3475
+ },
3476
+ });
3477
+
3478
+ assertInstanceOf(result, Note);
3479
+ deepStrictEqual(result.id, new URL("https://different-origin.com/note"));
3480
+ deepStrictEqual(result.content, "Embedded note");
3481
+ });
3482
+
2465
3483
  test("FEP-fe34: Same origin objects are trusted", async () => {
2466
3484
  // deno-lint-ignore require-await
2467
3485
  const sameOriginDocumentLoader = async (url: string) => {
@@ -2609,6 +3627,44 @@ test("FEP-fe34: Constructor vs JSON-LD parsing trust difference", async () => {
2609
3627
  deepStrictEqual(jsonLdResult?.content, "Fetched from origin");
2610
3628
  });
2611
3629
 
3630
+ test(
3631
+ "FEP-fe34: Portable DID authorities are cross-origin boundaries",
3632
+ async () => {
3633
+ const create = await Create.fromJsonLd({
3634
+ "@context": "https://www.w3.org/ns/activitystreams",
3635
+ "@type": "Create",
3636
+ "@id": "ap://did:key:z6MkOwner/create",
3637
+ "actor": "ap://did:key:z6MkOwner/actor",
3638
+ "object": {
3639
+ "@type": "Note",
3640
+ "@id": "ap://did:key:z6MkOther/note",
3641
+ "content": "Embedded portable note",
3642
+ },
3643
+ });
3644
+
3645
+ // deno-lint-ignore require-await
3646
+ const documentLoader = async (url: string) => {
3647
+ if (url === "ap+ef61://did:key:z6MkOther/note") {
3648
+ return {
3649
+ documentUrl: url,
3650
+ contextUrl: null,
3651
+ document: {
3652
+ "@context": "https://www.w3.org/ns/activitystreams",
3653
+ "@type": "Note",
3654
+ "@id": "ap://did:key:z6MkOther/note",
3655
+ "content": "Fetched portable note",
3656
+ },
3657
+ };
3658
+ }
3659
+ throw new Error("Document not found");
3660
+ };
3661
+
3662
+ const result = await create.getObject({ documentLoader });
3663
+ assertInstanceOf(result, Note);
3664
+ deepStrictEqual(result.content, "Fetched portable note");
3665
+ },
3666
+ );
3667
+
2612
3668
  test("FEP-fe34: Array properties respect cross-origin policy", async () => {
2613
3669
  // deno-lint-ignore require-await
2614
3670
  const crossOriginDocumentLoader = async (url: string) => {
@@ -2716,6 +3772,82 @@ test("FEP-fe34: Array properties with crossOrigin trust option", async () => {
2716
3772
  deepStrictEqual((items[1] as Note).content, "Legitimate note 2");
2717
3773
  });
2718
3774
 
3775
+ test("FEP-fe34: id-less arrays honor crossOrigin trust", async () => {
3776
+ const collection = await Collection.fromJsonLd({
3777
+ "@context": "https://www.w3.org/ns/activitystreams",
3778
+ "@type": "Collection",
3779
+ "items": [
3780
+ {
3781
+ "@type": "Note",
3782
+ "@id": "https://different-origin.com/note1",
3783
+ "content": "Embedded note 1",
3784
+ },
3785
+ {
3786
+ "@type": "Note",
3787
+ "@id": "https://different-origin.com/note2",
3788
+ "content": "Embedded note 2",
3789
+ },
3790
+ ],
3791
+ });
3792
+
3793
+ const items = [];
3794
+ for await (
3795
+ const item of collection.getItems({
3796
+ crossOrigin: "trust",
3797
+ // deno-lint-ignore require-await
3798
+ documentLoader: async (url) => {
3799
+ throw new Error(`Unexpected fetch: ${url}`);
3800
+ },
3801
+ })
3802
+ ) {
3803
+ items.push(item);
3804
+ }
3805
+
3806
+ deepStrictEqual(items.length, 2);
3807
+ assertInstanceOf(items[0], Note);
3808
+ assertInstanceOf(items[1], Note);
3809
+ deepStrictEqual((items[0] as Note).content, "Embedded note 1");
3810
+ deepStrictEqual((items[1] as Note).content, "Embedded note 2");
3811
+ });
3812
+
3813
+ test("FEP-fe34: Array properties track trust per item", async () => {
3814
+ const collection = new Collection({
3815
+ id: new URL("https://example.com/collection"),
3816
+ items: [
3817
+ new Note({
3818
+ id: new URL("https://malicious.com/fake-note1"),
3819
+ content: "Trusted constructor note 1",
3820
+ }),
3821
+ new URL("https://different-origin.com/note2"),
3822
+ ],
3823
+ });
3824
+ // deno-lint-ignore require-await
3825
+ const documentLoader = async (url: string) => {
3826
+ if (url === "https://different-origin.com/note2") {
3827
+ return {
3828
+ documentUrl: url,
3829
+ contextUrl: null,
3830
+ document: {
3831
+ "@context": "https://www.w3.org/ns/activitystreams",
3832
+ "@type": "Note",
3833
+ "@id": "https://malicious.com/fake-note2",
3834
+ "content": "Untrusted fetched note 2",
3835
+ },
3836
+ };
3837
+ }
3838
+ throw new Error("Document not found");
3839
+ };
3840
+
3841
+ const items = [];
3842
+ for await (const item of collection.getItems({ documentLoader })) {
3843
+ items.push(item);
3844
+ }
3845
+
3846
+ deepStrictEqual(items.length, 1);
3847
+ assertInstanceOf(items[0], Note);
3848
+ deepStrictEqual((items[0] as Note).content, "Trusted constructor note 1");
3849
+ });
3850
+
2719
3851
  test(
2720
3852
  "FEP-fe34: Embedded objects in arrays from JSON-LD respect cross-origin policy",
2721
3853
  async () => {