@fedify/vocab 2.4.0-dev.2056 → 2.4.0-dev.2065
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/mod.cjs +16674 -9026
- package/dist/mod.d.cts +28 -2
- package/dist/mod.d.ts +28 -2
- package/dist/mod.js +16675 -9027
- package/dist-tests/{actor-BgeBgt_8.mjs → actor-B0IFZjgb.mjs} +2 -2
- package/dist-tests/actor.test.mjs +3 -3
- package/dist-tests/{esm-Df0Yo-Tp.mjs → esm-C9nxP5_e.mjs} +1 -1
- package/dist-tests/gateway.test.mjs +695 -7
- package/dist-tests/{lookup-C13R72YU.mjs → lookup-BnzAXNN7.mjs} +48 -49
- package/dist-tests/lookup.test.mjs +3 -3
- package/dist-tests/portable-lookup.test.mjs +100 -4
- package/dist-tests/signed-representation.test.mjs +1 -1
- package/dist-tests/translation.test.mjs +3 -3
- package/dist-tests/type.test.mjs +1 -1
- package/dist-tests/{vocab-BJGPw--L.mjs → vocab-BHtBRusa.mjs} +16631 -8982
- package/dist-tests/vocab.test.mjs +1 -1
- package/package.json +4 -4
- package/src/gateway.test.ts +980 -7
- package/src/lookup.ts +114 -65
- package/src/portable-lookup.test.ts +133 -0
package/src/gateway.test.ts
CHANGED
|
@@ -7,13 +7,24 @@ import {
|
|
|
7
7
|
type DocumentLoader,
|
|
8
8
|
FetchError,
|
|
9
9
|
getDocumentLoader,
|
|
10
|
+
parseIri,
|
|
11
|
+
type PortableObjectVerification,
|
|
10
12
|
type PortableObjectVerifier,
|
|
13
|
+
type PortableObjectVerifierOptions,
|
|
11
14
|
type RemoteDocument,
|
|
12
15
|
} from "@fedify/vocab-runtime";
|
|
13
16
|
import fetchMock from "fetch-mock";
|
|
14
17
|
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
18
|
+
import { traverseCollection } from "./lookup.ts";
|
|
15
19
|
import { assertInstanceOf } from "./utils.ts";
|
|
16
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
Collection,
|
|
22
|
+
CollectionPage,
|
|
23
|
+
Create,
|
|
24
|
+
Note,
|
|
25
|
+
OrderedCollection,
|
|
26
|
+
Person,
|
|
27
|
+
} from "./vocab.ts";
|
|
17
28
|
|
|
18
29
|
const did = "did:key:z6Mkabc";
|
|
19
30
|
const objectId = `ap+ef61://${did}/objects/1`;
|
|
@@ -739,12 +750,19 @@ test("getObject() shares context documents within a dereference", async (t) => {
|
|
|
739
750
|
const loaded: string[] = [];
|
|
740
751
|
const contextLoader: DocumentLoader = async (url) => {
|
|
741
752
|
loaded.push(url);
|
|
753
|
+
if (url === contextUrl) {
|
|
754
|
+
return {
|
|
755
|
+
contextUrl: null,
|
|
756
|
+
documentUrl: url,
|
|
757
|
+
document: { "@context": {} },
|
|
758
|
+
};
|
|
759
|
+
}
|
|
742
760
|
return await mockDocumentLoader(url);
|
|
743
761
|
};
|
|
744
762
|
const object = await (await createActivity()).getObject({
|
|
745
763
|
documentLoader: createLoader({
|
|
746
764
|
[`https://gw1.example${gatewayPath}`]: {
|
|
747
|
-
...
|
|
765
|
+
...document,
|
|
748
766
|
attributedTo: "https://example.com/person",
|
|
749
767
|
},
|
|
750
768
|
}),
|
|
@@ -753,20 +771,20 @@ test("getObject() shares context documents within a dereference", async (t) => {
|
|
|
753
771
|
verifyPortableObject: createVerifier(),
|
|
754
772
|
});
|
|
755
773
|
assertInstanceOf(object, Note);
|
|
756
|
-
deepStrictEqual(loaded, []);
|
|
757
|
-
// Later dereferences on the returned object
|
|
758
|
-
//
|
|
774
|
+
deepStrictEqual(loaded, [contextUrl]);
|
|
775
|
+
// Later dereferences on the returned object do not reuse the snapshot,
|
|
776
|
+
// so they load the custom context through the caller's loader again:
|
|
759
777
|
const person = await object.getAttribution({
|
|
760
778
|
documentLoader: createLoader({
|
|
761
779
|
"https://example.com/person": {
|
|
762
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
780
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
763
781
|
id: "https://example.com/person",
|
|
764
782
|
type: "Person",
|
|
765
783
|
},
|
|
766
784
|
}),
|
|
767
785
|
});
|
|
768
786
|
deepStrictEqual(person?.id, new URL("https://example.com/person"));
|
|
769
|
-
|
|
787
|
+
deepStrictEqual(loaded, [contextUrl, contextUrl]);
|
|
770
788
|
});
|
|
771
789
|
|
|
772
790
|
await t.step("failed context loads are retried", async () => {
|
|
@@ -870,3 +888,958 @@ test("getObject() sends the ActivityStreams Accept header to gateways", {
|
|
|
870
888
|
fetchMock.hardReset();
|
|
871
889
|
}
|
|
872
890
|
});
|
|
891
|
+
|
|
892
|
+
const AS = "https://www.w3.org/ns/activitystreams#";
|
|
893
|
+
|
|
894
|
+
function gatewayUrl(gateway: string, path: string): string {
|
|
895
|
+
return `${gateway}/.well-known/apgateway/${did}${path}`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function createRecordingVerifier(
|
|
899
|
+
decide: (
|
|
900
|
+
document: unknown,
|
|
901
|
+
options: PortableObjectVerifierOptions,
|
|
902
|
+
) => PortableObjectVerification = () => ({ verified: true }),
|
|
903
|
+
): PortableObjectVerifier & {
|
|
904
|
+
readonly calls: {
|
|
905
|
+
document: unknown;
|
|
906
|
+
options: PortableObjectVerifierOptions;
|
|
907
|
+
}[];
|
|
908
|
+
} {
|
|
909
|
+
const calls: {
|
|
910
|
+
document: unknown;
|
|
911
|
+
options: PortableObjectVerifierOptions;
|
|
912
|
+
}[] = [];
|
|
913
|
+
// deno-lint-ignore require-await
|
|
914
|
+
const verifier = async (
|
|
915
|
+
document: unknown,
|
|
916
|
+
options: PortableObjectVerifierOptions,
|
|
917
|
+
) => {
|
|
918
|
+
calls.push({ document, options });
|
|
919
|
+
return decide(document, options);
|
|
920
|
+
};
|
|
921
|
+
return Object.assign(verifier, { calls });
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function portableCollection(
|
|
925
|
+
path: string,
|
|
926
|
+
extra: Record<string, unknown> = {},
|
|
927
|
+
): Record<string, unknown> {
|
|
928
|
+
return {
|
|
929
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
930
|
+
id: `ap://${did}${path}`,
|
|
931
|
+
type: "OrderedCollection",
|
|
932
|
+
totalItems: 0,
|
|
933
|
+
...extra,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
test("getOutbox() passes the fetch source and referrer to the verifier", async () => {
|
|
938
|
+
const person = await Person.fromJsonLd({
|
|
939
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
940
|
+
id: `ap://${did}/actor`,
|
|
941
|
+
type: "Person",
|
|
942
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
943
|
+
outbox: `ap://${did}/actor/outbox?@gateway=${
|
|
944
|
+
encodeURIComponent("https://hint.example")
|
|
945
|
+
}`,
|
|
946
|
+
}, { contextLoader: mockDocumentLoader });
|
|
947
|
+
const documentLoader = createLoader({
|
|
948
|
+
[gatewayUrl("https://hint.example", "/actor/outbox")]: portableCollection(
|
|
949
|
+
"/actor/outbox",
|
|
950
|
+
),
|
|
951
|
+
});
|
|
952
|
+
const verifier = createRecordingVerifier();
|
|
953
|
+
const outbox = await person.getOutbox({
|
|
954
|
+
documentLoader,
|
|
955
|
+
contextLoader: mockDocumentLoader,
|
|
956
|
+
verifyPortableObject: verifier,
|
|
957
|
+
});
|
|
958
|
+
assertInstanceOf(outbox, Collection);
|
|
959
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
960
|
+
const { options } = verifier.calls[0];
|
|
961
|
+
deepStrictEqual(
|
|
962
|
+
options.documentUrl,
|
|
963
|
+
new URL(gatewayUrl("https://hint.example", "/actor/outbox")),
|
|
964
|
+
);
|
|
965
|
+
deepStrictEqual(options.gateways, undefined);
|
|
966
|
+
deepStrictEqual(options.gatewayHints, [new URL("https://hint.example")]);
|
|
967
|
+
ok(options.referrer?.object === person);
|
|
968
|
+
deepStrictEqual(options.referrer?.property, `${AS}outbox`);
|
|
969
|
+
deepStrictEqual(options.referrer?.acceptance, undefined);
|
|
970
|
+
|
|
971
|
+
// Explicit gateways are passed as such:
|
|
972
|
+
const person2 = await Person.fromJsonLd({
|
|
973
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
974
|
+
id: `ap://${did}/actor`,
|
|
975
|
+
type: "Person",
|
|
976
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
977
|
+
outbox: `ap://${did}/actor/outbox`,
|
|
978
|
+
}, { contextLoader: mockDocumentLoader });
|
|
979
|
+
const verifier2 = createRecordingVerifier();
|
|
980
|
+
await person2.getOutbox({
|
|
981
|
+
documentLoader: createLoader({
|
|
982
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection(
|
|
983
|
+
"/actor/outbox",
|
|
984
|
+
),
|
|
985
|
+
}),
|
|
986
|
+
contextLoader: mockDocumentLoader,
|
|
987
|
+
gateways: ["https://gw.example"],
|
|
988
|
+
verifyPortableObject: verifier2,
|
|
989
|
+
});
|
|
990
|
+
deepStrictEqual(verifier2.calls[0].options.gateways, [
|
|
991
|
+
new URL("https://gw.example"),
|
|
992
|
+
]);
|
|
993
|
+
deepStrictEqual(verifier2.calls[0].options.gatewayHints, undefined);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
test("getFirst() passes the referrer chain and collection context", async () => {
|
|
997
|
+
const context = Object.freeze({});
|
|
998
|
+
const documentLoader = createLoader({
|
|
999
|
+
[gatewayUrl("https://gw.example", "/actor")]: {
|
|
1000
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1001
|
+
id: `ap://${did}/actor`,
|
|
1002
|
+
type: "Person",
|
|
1003
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
1004
|
+
outbox: `ap://${did}/actor/outbox`,
|
|
1005
|
+
},
|
|
1006
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection(
|
|
1007
|
+
"/actor/outbox",
|
|
1008
|
+
{
|
|
1009
|
+
first: {
|
|
1010
|
+
id: `ap://${did}/actor/outbox?page=1`,
|
|
1011
|
+
type: "OrderedCollectionPage",
|
|
1012
|
+
next: `ap://${did}/actor/outbox?page=2`,
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
),
|
|
1016
|
+
[gatewayUrl("https://gw.example", "/actor/outbox?page=2")]: {
|
|
1017
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1018
|
+
id: `ap://${did}/actor/outbox?page=2`,
|
|
1019
|
+
type: "OrderedCollectionPage",
|
|
1020
|
+
orderedItems: [],
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
// The collection is signed in this scenario (verified without the
|
|
1024
|
+
// unsecured flag), so its embedded first page is trusted by origin:
|
|
1025
|
+
const verifier = createRecordingVerifier((document) =>
|
|
1026
|
+
(document as Record<string, unknown>).type === "OrderedCollection"
|
|
1027
|
+
? { verified: true, collectionContext: context }
|
|
1028
|
+
: { verified: true }
|
|
1029
|
+
);
|
|
1030
|
+
const options = {
|
|
1031
|
+
documentLoader,
|
|
1032
|
+
contextLoader: mockDocumentLoader,
|
|
1033
|
+
gateways: ["https://gw.example"],
|
|
1034
|
+
verifyPortableObject: verifier,
|
|
1035
|
+
};
|
|
1036
|
+
const create = await createActivity(`ap://${did}/actor`);
|
|
1037
|
+
const actor = await create.getObject(options);
|
|
1038
|
+
assertInstanceOf(actor, Person);
|
|
1039
|
+
const outbox = await actor.getOutbox(options);
|
|
1040
|
+
assertInstanceOf(outbox, Collection);
|
|
1041
|
+
const first = await outbox.getFirst(options);
|
|
1042
|
+
assertInstanceOf(first, CollectionPage);
|
|
1043
|
+
const next = await first.getNext(options);
|
|
1044
|
+
assertInstanceOf(next, CollectionPage);
|
|
1045
|
+
const { referrer } = verifier.calls[verifier.calls.length - 1].options;
|
|
1046
|
+
// next ← (embedded) first ← outbox ← actor ← create:
|
|
1047
|
+
ok(referrer?.object === first);
|
|
1048
|
+
deepStrictEqual(referrer?.property, `${AS}next`);
|
|
1049
|
+
deepStrictEqual(referrer?.acceptance, undefined);
|
|
1050
|
+
ok(referrer?.referrer?.object === outbox);
|
|
1051
|
+
deepStrictEqual(referrer?.referrer?.property, `${AS}first`);
|
|
1052
|
+
deepStrictEqual(referrer?.referrer?.acceptance, "verified");
|
|
1053
|
+
ok(referrer?.referrer?.collectionContext === context);
|
|
1054
|
+
ok(referrer?.referrer?.referrer?.object === actor);
|
|
1055
|
+
deepStrictEqual(referrer?.referrer?.referrer?.property, `${AS}outbox`);
|
|
1056
|
+
deepStrictEqual(referrer?.referrer?.referrer?.acceptance, "verified");
|
|
1057
|
+
ok(referrer?.referrer?.referrer?.referrer?.object === create);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test("accessors do not trust objects embedded in unsecured collections", async () => {
|
|
1061
|
+
const itemPath = "/objects/1";
|
|
1062
|
+
const documentLoader = createLoader({
|
|
1063
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection(
|
|
1064
|
+
"/actor/outbox",
|
|
1065
|
+
{
|
|
1066
|
+
orderedItems: [
|
|
1067
|
+
// A forged copy of a portable object:
|
|
1068
|
+
{ id: `ap://${did}${itemPath}`, type: "Note", content: "Forged" },
|
|
1069
|
+
// An object that cannot be verified on its own:
|
|
1070
|
+
{ type: "Note", content: "Anonymous" },
|
|
1071
|
+
],
|
|
1072
|
+
},
|
|
1073
|
+
),
|
|
1074
|
+
[gatewayUrl("https://gw.example", itemPath)]: note(),
|
|
1075
|
+
});
|
|
1076
|
+
const verifier = createRecordingVerifier((document) =>
|
|
1077
|
+
(document as Record<string, unknown>).type === "OrderedCollection"
|
|
1078
|
+
? { verified: true, unsecured: true }
|
|
1079
|
+
: { verified: true }
|
|
1080
|
+
);
|
|
1081
|
+
const options = {
|
|
1082
|
+
documentLoader,
|
|
1083
|
+
contextLoader: mockDocumentLoader,
|
|
1084
|
+
gateways: ["https://gw.example"],
|
|
1085
|
+
verifyPortableObject: verifier,
|
|
1086
|
+
};
|
|
1087
|
+
const createPerson = () =>
|
|
1088
|
+
Person.fromJsonLd({
|
|
1089
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1090
|
+
id: `ap://${did}/actor`,
|
|
1091
|
+
type: "Person",
|
|
1092
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
1093
|
+
outbox: `ap://${did}/actor/outbox`,
|
|
1094
|
+
}, { contextLoader: mockDocumentLoader });
|
|
1095
|
+
const outbox = await (await createPerson()).getOutbox(options);
|
|
1096
|
+
assertInstanceOf(outbox, Collection);
|
|
1097
|
+
const items = await Array.fromAsync(outbox.getItems(options));
|
|
1098
|
+
deepStrictEqual(
|
|
1099
|
+
items.map((item) => item instanceof Note ? item.content : null),
|
|
1100
|
+
["Portable note"],
|
|
1101
|
+
);
|
|
1102
|
+
// Even crossOrigin: "trust" does not make them trusted:
|
|
1103
|
+
const outbox2 = await (await createPerson()).getOutbox(options);
|
|
1104
|
+
assertInstanceOf(outbox2, Collection);
|
|
1105
|
+
const items2 = await Array.fromAsync(
|
|
1106
|
+
outbox2.getItems({ ...options, crossOrigin: "trust" }),
|
|
1107
|
+
);
|
|
1108
|
+
deepStrictEqual(items2.length, 1);
|
|
1109
|
+
|
|
1110
|
+
// A clone keeps the restriction, so its embedded objects are fetched
|
|
1111
|
+
// again (and fail here, since nothing can be fetched):
|
|
1112
|
+
const outbox3 = await (await createPerson()).getOutbox(options);
|
|
1113
|
+
assertInstanceOf(outbox3, Collection);
|
|
1114
|
+
const items3 = await Array.fromAsync(
|
|
1115
|
+
outbox3.clone().getItems({
|
|
1116
|
+
...options,
|
|
1117
|
+
documentLoader: createLoader({}),
|
|
1118
|
+
suppressError: true,
|
|
1119
|
+
}),
|
|
1120
|
+
);
|
|
1121
|
+
deepStrictEqual(items3, []);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
function createRedirectingLoader(
|
|
1125
|
+
responses: Record<string, { documentUrl: string; document: unknown }>,
|
|
1126
|
+
): DocumentLoader & { readonly fetched: string[] } {
|
|
1127
|
+
const fetched: string[] = [];
|
|
1128
|
+
// deno-lint-ignore require-await
|
|
1129
|
+
const loader = async (url: string): Promise<RemoteDocument> => {
|
|
1130
|
+
fetched.push(url);
|
|
1131
|
+
const response = responses[url];
|
|
1132
|
+
if (response == null) {
|
|
1133
|
+
throw new FetchError(
|
|
1134
|
+
url,
|
|
1135
|
+
"HTTP 404",
|
|
1136
|
+
new globalThis.Response(null, { status: 404 }),
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return { contextUrl: null, ...response };
|
|
1140
|
+
};
|
|
1141
|
+
return Object.assign(loader, { fetched });
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function createHttpActivity(object: unknown): Promise<Create> {
|
|
1145
|
+
return Create.fromJsonLd({
|
|
1146
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1147
|
+
type: "Create",
|
|
1148
|
+
id: "https://example.com/activities/1",
|
|
1149
|
+
object,
|
|
1150
|
+
}, { contextLoader: mockDocumentLoader });
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const compatibleNoteId = gatewayUrl("https://gw.example", "/objects/1");
|
|
1154
|
+
|
|
1155
|
+
test("accessors dereference compatible identifiers through their gateways", async (t) => {
|
|
1156
|
+
await t.step("through the named gateway", async () => {
|
|
1157
|
+
const documentLoader = createLoader({ [compatibleNoteId]: note() });
|
|
1158
|
+
const verifier = createRecordingVerifier();
|
|
1159
|
+
const activity = await createHttpActivity(compatibleNoteId);
|
|
1160
|
+
const object = await activity.getObject({
|
|
1161
|
+
documentLoader,
|
|
1162
|
+
contextLoader: mockDocumentLoader,
|
|
1163
|
+
verifyPortableObject: verifier,
|
|
1164
|
+
});
|
|
1165
|
+
assertInstanceOf(object, Note);
|
|
1166
|
+
deepStrictEqual(object.id, parseIri(objectId));
|
|
1167
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
|
|
1168
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1169
|
+
const { options } = verifier.calls[0];
|
|
1170
|
+
deepStrictEqual(options.documentUrl, new URL(compatibleNoteId));
|
|
1171
|
+
deepStrictEqual(options.gateways, undefined);
|
|
1172
|
+
deepStrictEqual(options.gatewayHints, [new URL("https://gw.example")]);
|
|
1173
|
+
ok(options.referrer?.object === activity);
|
|
1174
|
+
// The verified object is cached:
|
|
1175
|
+
ok(
|
|
1176
|
+
await activity.getObject({ documentLoader: createLoader({}) }) === object,
|
|
1177
|
+
);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
await t.step("then through explicit gateways", async () => {
|
|
1181
|
+
const documentLoader = createLoader({
|
|
1182
|
+
[gatewayUrl("https://gw2.example", "/objects/1")]: note(),
|
|
1183
|
+
});
|
|
1184
|
+
const verifier = createRecordingVerifier();
|
|
1185
|
+
const object = await (await createHttpActivity(compatibleNoteId))
|
|
1186
|
+
.getObject({
|
|
1187
|
+
documentLoader,
|
|
1188
|
+
contextLoader: mockDocumentLoader,
|
|
1189
|
+
gateways: ["https://gw2.example", "https://gw.example"],
|
|
1190
|
+
verifyPortableObject: verifier,
|
|
1191
|
+
});
|
|
1192
|
+
assertInstanceOf(object, Note);
|
|
1193
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
1194
|
+
compatibleNoteId,
|
|
1195
|
+
gatewayUrl("https://gw2.example", "/objects/1"),
|
|
1196
|
+
]);
|
|
1197
|
+
deepStrictEqual(verifier.calls[0].options.gateways, [
|
|
1198
|
+
new URL("https://gw2.example"),
|
|
1199
|
+
new URL("https://gw.example"),
|
|
1200
|
+
]);
|
|
1201
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, undefined);
|
|
1202
|
+
|
|
1203
|
+
// An empty list still asks the gateway that the identifier names:
|
|
1204
|
+
const documentLoader2 = createLoader({ [compatibleNoteId]: note() });
|
|
1205
|
+
const object2 = await (await createHttpActivity(compatibleNoteId))
|
|
1206
|
+
.getObject({
|
|
1207
|
+
documentLoader: documentLoader2,
|
|
1208
|
+
contextLoader: mockDocumentLoader,
|
|
1209
|
+
gateways: [],
|
|
1210
|
+
verifyPortableObject: createVerifier(),
|
|
1211
|
+
});
|
|
1212
|
+
assertInstanceOf(object2, Note);
|
|
1213
|
+
deepStrictEqual(documentLoader2.fetched, [compatibleNoteId]);
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
await t.step("in plural accessors", async () => {
|
|
1217
|
+
const documentLoader = createLoader({ [compatibleNoteId]: note() });
|
|
1218
|
+
const objects = await Array.fromAsync(
|
|
1219
|
+
(await createHttpActivity([compatibleNoteId])).getObjects({
|
|
1220
|
+
documentLoader,
|
|
1221
|
+
contextLoader: mockDocumentLoader,
|
|
1222
|
+
verifyPortableObject: createVerifier(),
|
|
1223
|
+
}),
|
|
1224
|
+
);
|
|
1225
|
+
deepStrictEqual(objects.map((o) => o.id), [parseIri(objectId)]);
|
|
1226
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
test("accessors reject forged objects at compatible identifiers", async () => {
|
|
1231
|
+
const forgedId = gatewayUrl("https://evil.example", "/objects/1");
|
|
1232
|
+
// The unsigned object from the issue, identified by the compatible
|
|
1233
|
+
// identifier itself:
|
|
1234
|
+
const forged = {
|
|
1235
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1236
|
+
id: forgedId,
|
|
1237
|
+
type: "Note",
|
|
1238
|
+
attributedTo: gatewayUrl("https://evil.example", "/actor"),
|
|
1239
|
+
content: "Forged",
|
|
1240
|
+
};
|
|
1241
|
+
for (const crossOrigin of [undefined, "ignore", "trust"] as const) {
|
|
1242
|
+
// Its @id is not a portable ID, so it fails the identity check:
|
|
1243
|
+
const verifier = createRecordingVerifier();
|
|
1244
|
+
deepStrictEqual(
|
|
1245
|
+
await (await createHttpActivity(forgedId)).getObject({
|
|
1246
|
+
documentLoader: createLoader({ [forgedId]: forged }),
|
|
1247
|
+
contextLoader: mockDocumentLoader,
|
|
1248
|
+
verifyPortableObject: verifier,
|
|
1249
|
+
crossOrigin,
|
|
1250
|
+
}),
|
|
1251
|
+
null,
|
|
1252
|
+
);
|
|
1253
|
+
deepStrictEqual(verifier.calls, []);
|
|
1254
|
+
// With a portable ID, it fails the proof policy:
|
|
1255
|
+
deepStrictEqual(
|
|
1256
|
+
await (await createHttpActivity(forgedId)).getObject({
|
|
1257
|
+
documentLoader: createLoader({ [forgedId]: note() }),
|
|
1258
|
+
contextLoader: mockDocumentLoader,
|
|
1259
|
+
verifyPortableObject: createVerifier(false),
|
|
1260
|
+
crossOrigin,
|
|
1261
|
+
}),
|
|
1262
|
+
null,
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
await rejects(
|
|
1266
|
+
async () =>
|
|
1267
|
+
await (await createHttpActivity(forgedId)).getObject({
|
|
1268
|
+
documentLoader: createLoader({ [forgedId]: note() }),
|
|
1269
|
+
contextLoader: mockDocumentLoader,
|
|
1270
|
+
verifyPortableObject: createVerifier(false),
|
|
1271
|
+
crossOrigin: "throw",
|
|
1272
|
+
}),
|
|
1273
|
+
/No gateway returned a valid portable object/,
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test("accessors reject malformed compatible identifiers", async () => {
|
|
1278
|
+
const malformed = [
|
|
1279
|
+
// Location hints are not allowed in compatible identifiers:
|
|
1280
|
+
`${compatibleNoteId}?@gateway=https%3A%2F%2Fgw2.example`,
|
|
1281
|
+
// Nor are credentials:
|
|
1282
|
+
compatibleNoteId.replace("https://", "https://user:pass@"),
|
|
1283
|
+
];
|
|
1284
|
+
for (const url of malformed) {
|
|
1285
|
+
const documentLoader = createLoader({ [url]: note() });
|
|
1286
|
+
const verifier = createRecordingVerifier();
|
|
1287
|
+
const activity = await createHttpActivity(url);
|
|
1288
|
+
deepStrictEqual(
|
|
1289
|
+
await activity.getObject({
|
|
1290
|
+
documentLoader,
|
|
1291
|
+
contextLoader: mockDocumentLoader,
|
|
1292
|
+
verifyPortableObject: verifier,
|
|
1293
|
+
}),
|
|
1294
|
+
null,
|
|
1295
|
+
);
|
|
1296
|
+
deepStrictEqual(
|
|
1297
|
+
await activity.getObject({
|
|
1298
|
+
documentLoader,
|
|
1299
|
+
contextLoader: mockDocumentLoader,
|
|
1300
|
+
verifyPortableObject: verifier,
|
|
1301
|
+
crossOrigin: "throw",
|
|
1302
|
+
suppressError: true,
|
|
1303
|
+
}),
|
|
1304
|
+
null,
|
|
1305
|
+
);
|
|
1306
|
+
await rejects(
|
|
1307
|
+
async () =>
|
|
1308
|
+
await activity.getObject({
|
|
1309
|
+
documentLoader,
|
|
1310
|
+
contextLoader: mockDocumentLoader,
|
|
1311
|
+
verifyPortableObject: verifier,
|
|
1312
|
+
crossOrigin: "throw",
|
|
1313
|
+
}),
|
|
1314
|
+
/malformed FEP-ef61 compatible identifier/,
|
|
1315
|
+
);
|
|
1316
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
1317
|
+
deepStrictEqual(verifier.calls, []);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
test("accessors keep fetching compatible identifiers as HTTP(S) URLs without verifyPortableObject", async (t) => {
|
|
1322
|
+
const document = { ...note(), id: compatibleNoteId };
|
|
1323
|
+
|
|
1324
|
+
await t.step("singular accessors", async () => {
|
|
1325
|
+
const documentLoader = createLoader({ [compatibleNoteId]: document });
|
|
1326
|
+
const activity = await createHttpActivity(compatibleNoteId);
|
|
1327
|
+
const object = await activity.getObject({
|
|
1328
|
+
documentLoader,
|
|
1329
|
+
contextLoader: mockDocumentLoader,
|
|
1330
|
+
});
|
|
1331
|
+
assertInstanceOf(object, Note);
|
|
1332
|
+
deepStrictEqual(object.id, new URL(compatibleNoteId));
|
|
1333
|
+
// It is not cached, so the portable object policy applies later:
|
|
1334
|
+
deepStrictEqual(
|
|
1335
|
+
await activity.getObject({
|
|
1336
|
+
documentLoader,
|
|
1337
|
+
contextLoader: mockDocumentLoader,
|
|
1338
|
+
verifyPortableObject: createVerifier(),
|
|
1339
|
+
}),
|
|
1340
|
+
null,
|
|
1341
|
+
);
|
|
1342
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
1343
|
+
compatibleNoteId,
|
|
1344
|
+
compatibleNoteId,
|
|
1345
|
+
]);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
await t.step("plural accessors", async () => {
|
|
1349
|
+
const documentLoader = createLoader({ [compatibleNoteId]: document });
|
|
1350
|
+
const activity = await createHttpActivity([compatibleNoteId]);
|
|
1351
|
+
const objects = await Array.fromAsync(activity.getObjects({
|
|
1352
|
+
documentLoader,
|
|
1353
|
+
contextLoader: mockDocumentLoader,
|
|
1354
|
+
}));
|
|
1355
|
+
deepStrictEqual(objects.map((o) => o.id), [new URL(compatibleNoteId)]);
|
|
1356
|
+
deepStrictEqual(
|
|
1357
|
+
await Array.fromAsync(activity.getObjects({
|
|
1358
|
+
documentLoader,
|
|
1359
|
+
contextLoader: mockDocumentLoader,
|
|
1360
|
+
verifyPortableObject: createVerifier(),
|
|
1361
|
+
})),
|
|
1362
|
+
[],
|
|
1363
|
+
);
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
await t.step("redirects to ordinary objects", async () => {
|
|
1367
|
+
const plainUrl = "https://gw.example/notes/1";
|
|
1368
|
+
const documentLoader = createRedirectingLoader({
|
|
1369
|
+
[compatibleNoteId]: {
|
|
1370
|
+
documentUrl: plainUrl,
|
|
1371
|
+
document: { ...note(), id: plainUrl },
|
|
1372
|
+
},
|
|
1373
|
+
});
|
|
1374
|
+
for (const activity of [await createHttpActivity(compatibleNoteId)]) {
|
|
1375
|
+
const object = await activity.getObject({
|
|
1376
|
+
documentLoader,
|
|
1377
|
+
contextLoader: mockDocumentLoader,
|
|
1378
|
+
});
|
|
1379
|
+
deepStrictEqual(object?.id, new URL(plainUrl));
|
|
1380
|
+
deepStrictEqual(
|
|
1381
|
+
await activity.getObject({
|
|
1382
|
+
documentLoader,
|
|
1383
|
+
contextLoader: mockDocumentLoader,
|
|
1384
|
+
verifyPortableObject: createVerifier(),
|
|
1385
|
+
}),
|
|
1386
|
+
null,
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const activity = await createHttpActivity([compatibleNoteId]);
|
|
1390
|
+
deepStrictEqual(
|
|
1391
|
+
(await Array.fromAsync(activity.getObjects({
|
|
1392
|
+
documentLoader,
|
|
1393
|
+
contextLoader: mockDocumentLoader,
|
|
1394
|
+
}))).length,
|
|
1395
|
+
1,
|
|
1396
|
+
);
|
|
1397
|
+
deepStrictEqual(
|
|
1398
|
+
await Array.fromAsync(activity.getObjects({
|
|
1399
|
+
documentLoader,
|
|
1400
|
+
contextLoader: mockDocumentLoader,
|
|
1401
|
+
verifyPortableObject: createVerifier(),
|
|
1402
|
+
})),
|
|
1403
|
+
[],
|
|
1404
|
+
);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
await t.step("embedded objects", async () => {
|
|
1408
|
+
const activity = await createHttpActivity({
|
|
1409
|
+
type: "Note",
|
|
1410
|
+
id: gatewayUrl("https://example.com", "/objects/1"),
|
|
1411
|
+
content: "Embedded",
|
|
1412
|
+
});
|
|
1413
|
+
const object = await activity.getObject({
|
|
1414
|
+
documentLoader: createLoader({}),
|
|
1415
|
+
contextLoader: mockDocumentLoader,
|
|
1416
|
+
});
|
|
1417
|
+
deepStrictEqual(
|
|
1418
|
+
object?.id,
|
|
1419
|
+
new URL(gatewayUrl("https://example.com", "/objects/1")),
|
|
1420
|
+
);
|
|
1421
|
+
});
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test("accessors dereference compatible identifiers from portable objects", async () => {
|
|
1425
|
+
const compatibleOutbox = gatewayUrl("https://gw.example", "/actor/outbox");
|
|
1426
|
+
const person = (outbox: unknown) =>
|
|
1427
|
+
Person.fromJsonLd({
|
|
1428
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1429
|
+
id: `ap://${did}/actor`,
|
|
1430
|
+
type: "Person",
|
|
1431
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
1432
|
+
outbox,
|
|
1433
|
+
}, { contextLoader: mockDocumentLoader });
|
|
1434
|
+
const documentLoader = createRedirectingLoader({
|
|
1435
|
+
[compatibleOutbox]: {
|
|
1436
|
+
documentUrl: compatibleOutbox,
|
|
1437
|
+
document: portableCollection("/actor/outbox"),
|
|
1438
|
+
},
|
|
1439
|
+
"https://example.com/outbox": {
|
|
1440
|
+
documentUrl: compatibleOutbox,
|
|
1441
|
+
document: portableCollection("/actor/outbox"),
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
for (const crossOrigin of [undefined, "trust"] as const) {
|
|
1445
|
+
for (
|
|
1446
|
+
const outbox of [
|
|
1447
|
+
// A compatible identifier as the reference:
|
|
1448
|
+
compatibleOutbox,
|
|
1449
|
+
// A redirect to a compatible identifier:
|
|
1450
|
+
"https://example.com/outbox",
|
|
1451
|
+
// An embedded object with a compatible identifier:
|
|
1452
|
+
{ ...portableCollection("/actor/outbox"), id: compatibleOutbox },
|
|
1453
|
+
]
|
|
1454
|
+
) {
|
|
1455
|
+
const verifier = createRecordingVerifier();
|
|
1456
|
+
const collection = await (await person(outbox)).getOutbox({
|
|
1457
|
+
documentLoader,
|
|
1458
|
+
contextLoader: mockDocumentLoader,
|
|
1459
|
+
verifyPortableObject: verifier,
|
|
1460
|
+
crossOrigin,
|
|
1461
|
+
});
|
|
1462
|
+
assertInstanceOf(collection, OrderedCollection);
|
|
1463
|
+
deepStrictEqual(collection.id, parseIri(`ap://${did}/actor/outbox`));
|
|
1464
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1465
|
+
deepStrictEqual(
|
|
1466
|
+
verifier.calls[0].options.documentUrl,
|
|
1467
|
+
new URL(compatibleOutbox),
|
|
1468
|
+
);
|
|
1469
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, [
|
|
1470
|
+
new URL("https://gw.example"),
|
|
1471
|
+
]);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
// Without verifyPortableObject, they are handled like portable IRIs:
|
|
1475
|
+
await rejects(
|
|
1476
|
+
async () =>
|
|
1477
|
+
await (await person(compatibleOutbox)).getOutbox({
|
|
1478
|
+
documentLoader,
|
|
1479
|
+
contextLoader: mockDocumentLoader,
|
|
1480
|
+
}),
|
|
1481
|
+
TypeError,
|
|
1482
|
+
);
|
|
1483
|
+
deepStrictEqual(
|
|
1484
|
+
await (await person(compatibleOutbox)).getOutbox({
|
|
1485
|
+
documentLoader,
|
|
1486
|
+
contextLoader: mockDocumentLoader,
|
|
1487
|
+
suppressError: true,
|
|
1488
|
+
}),
|
|
1489
|
+
null,
|
|
1490
|
+
);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
test("accessors verify fetched documents that stand for portable objects", async (t) => {
|
|
1494
|
+
const plainUrl = "https://example.com/notes/1";
|
|
1495
|
+
|
|
1496
|
+
await t.step("a redirect to a compatible identifier", async () => {
|
|
1497
|
+
const documentLoader = createRedirectingLoader({
|
|
1498
|
+
[plainUrl]: { documentUrl: compatibleNoteId, document: note() },
|
|
1499
|
+
});
|
|
1500
|
+
const verifier = createRecordingVerifier();
|
|
1501
|
+
const object = await (await createHttpActivity(plainUrl)).getObject({
|
|
1502
|
+
documentLoader,
|
|
1503
|
+
contextLoader: mockDocumentLoader,
|
|
1504
|
+
verifyPortableObject: verifier,
|
|
1505
|
+
});
|
|
1506
|
+
assertInstanceOf(object, Note);
|
|
1507
|
+
deepStrictEqual(object.id, parseIri(objectId));
|
|
1508
|
+
// Verified in place, without another request:
|
|
1509
|
+
deepStrictEqual(documentLoader.fetched, [plainUrl]);
|
|
1510
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1511
|
+
deepStrictEqual(
|
|
1512
|
+
verifier.calls[0].options.documentUrl,
|
|
1513
|
+
new URL(compatibleNoteId),
|
|
1514
|
+
);
|
|
1515
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, [
|
|
1516
|
+
new URL("https://gw.example"),
|
|
1517
|
+
]);
|
|
1518
|
+
// It is rejected if the verifier rejects it:
|
|
1519
|
+
deepStrictEqual(
|
|
1520
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1521
|
+
documentLoader,
|
|
1522
|
+
contextLoader: mockDocumentLoader,
|
|
1523
|
+
verifyPortableObject: createVerifier(false),
|
|
1524
|
+
}),
|
|
1525
|
+
null,
|
|
1526
|
+
);
|
|
1527
|
+
await rejects(
|
|
1528
|
+
async () =>
|
|
1529
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1530
|
+
documentLoader,
|
|
1531
|
+
contextLoader: mockDocumentLoader,
|
|
1532
|
+
verifyPortableObject: createVerifier(false),
|
|
1533
|
+
crossOrigin: "throw",
|
|
1534
|
+
}),
|
|
1535
|
+
/No gateway returned a valid portable object/,
|
|
1536
|
+
);
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
await t.step("a document that claims another object", async () => {
|
|
1540
|
+
for (
|
|
1541
|
+
const id of [
|
|
1542
|
+
`ap://${did}/objects/2`,
|
|
1543
|
+
compatibleNoteId,
|
|
1544
|
+
"https://gw.example/notes/1",
|
|
1545
|
+
]
|
|
1546
|
+
) {
|
|
1547
|
+
const verifier = createRecordingVerifier();
|
|
1548
|
+
deepStrictEqual(
|
|
1549
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1550
|
+
documentLoader: createRedirectingLoader({
|
|
1551
|
+
[plainUrl]: { documentUrl: compatibleNoteId, document: note(id) },
|
|
1552
|
+
}),
|
|
1553
|
+
contextLoader: mockDocumentLoader,
|
|
1554
|
+
verifyPortableObject: verifier,
|
|
1555
|
+
}),
|
|
1556
|
+
null,
|
|
1557
|
+
);
|
|
1558
|
+
deepStrictEqual(verifier.calls, []);
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
await t.step("a portable @id", async () => {
|
|
1563
|
+
const verifier = createRecordingVerifier();
|
|
1564
|
+
const object = await (await createHttpActivity(plainUrl)).getObject({
|
|
1565
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1566
|
+
contextLoader: mockDocumentLoader,
|
|
1567
|
+
verifyPortableObject: verifier,
|
|
1568
|
+
// Does not skip the portable object policy:
|
|
1569
|
+
crossOrigin: "trust",
|
|
1570
|
+
});
|
|
1571
|
+
assertInstanceOf(object, Note);
|
|
1572
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1573
|
+
deepStrictEqual(verifier.calls[0].options.documentUrl, new URL(plainUrl));
|
|
1574
|
+
deepStrictEqual(
|
|
1575
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1576
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1577
|
+
contextLoader: mockDocumentLoader,
|
|
1578
|
+
verifyPortableObject: createVerifier(false),
|
|
1579
|
+
crossOrigin: "trust",
|
|
1580
|
+
}),
|
|
1581
|
+
null,
|
|
1582
|
+
);
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
await t.step("a compatible @id", async () => {
|
|
1586
|
+
const verifier = createRecordingVerifier();
|
|
1587
|
+
const url = "https://gw.example/notes/1";
|
|
1588
|
+
deepStrictEqual(
|
|
1589
|
+
await (await createHttpActivity(url)).getObject({
|
|
1590
|
+
documentLoader: createLoader({ [url]: note(compatibleNoteId) }),
|
|
1591
|
+
contextLoader: mockDocumentLoader,
|
|
1592
|
+
verifyPortableObject: verifier,
|
|
1593
|
+
}),
|
|
1594
|
+
null,
|
|
1595
|
+
);
|
|
1596
|
+
deepStrictEqual(verifier.calls, []);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
await t.step("a portable final URL", async () => {
|
|
1600
|
+
for (
|
|
1601
|
+
const [id, expected] of [
|
|
1602
|
+
[objectId, true],
|
|
1603
|
+
[`ap://${did}/objects/2`, false],
|
|
1604
|
+
] as const
|
|
1605
|
+
) {
|
|
1606
|
+
const object = await (await createHttpActivity(plainUrl)).getObject({
|
|
1607
|
+
documentLoader: createRedirectingLoader({
|
|
1608
|
+
[plainUrl]: {
|
|
1609
|
+
documentUrl: `ap://${did}/objects/1`,
|
|
1610
|
+
document: note(id),
|
|
1611
|
+
},
|
|
1612
|
+
}),
|
|
1613
|
+
contextLoader: mockDocumentLoader,
|
|
1614
|
+
verifyPortableObject: createVerifier(),
|
|
1615
|
+
});
|
|
1616
|
+
deepStrictEqual(object instanceof Note, expected);
|
|
1617
|
+
}
|
|
1618
|
+
// A final URL that URL parsing would turn into another ID is refused:
|
|
1619
|
+
deepStrictEqual(
|
|
1620
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1621
|
+
documentLoader: createRedirectingLoader({
|
|
1622
|
+
[plainUrl]: {
|
|
1623
|
+
documentUrl: `ap://${did}/objects/x/../1`,
|
|
1624
|
+
document: note(),
|
|
1625
|
+
},
|
|
1626
|
+
}),
|
|
1627
|
+
contextLoader: mockDocumentLoader,
|
|
1628
|
+
verifyPortableObject: createVerifier(),
|
|
1629
|
+
}),
|
|
1630
|
+
null,
|
|
1631
|
+
);
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
await t.step("a malformed compatible final URL", async () => {
|
|
1635
|
+
const malformed = `${compatibleNoteId}?@gateway=https%3A%2F%2Fgw2.example`;
|
|
1636
|
+
const documentLoader = createRedirectingLoader({
|
|
1637
|
+
[plainUrl]: { documentUrl: malformed, document: note() },
|
|
1638
|
+
});
|
|
1639
|
+
deepStrictEqual(
|
|
1640
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1641
|
+
documentLoader,
|
|
1642
|
+
contextLoader: mockDocumentLoader,
|
|
1643
|
+
verifyPortableObject: createVerifier(),
|
|
1644
|
+
}),
|
|
1645
|
+
null,
|
|
1646
|
+
);
|
|
1647
|
+
await rejects(
|
|
1648
|
+
async () =>
|
|
1649
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1650
|
+
documentLoader,
|
|
1651
|
+
contextLoader: mockDocumentLoader,
|
|
1652
|
+
verifyPortableObject: createVerifier(),
|
|
1653
|
+
crossOrigin: "throw",
|
|
1654
|
+
}),
|
|
1655
|
+
/malformed FEP-ef61 compatible identifier/,
|
|
1656
|
+
);
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
await t.step("context documents are shared", async () => {
|
|
1660
|
+
const contextUrl = "https://example.com/portable-context";
|
|
1661
|
+
let version = 0;
|
|
1662
|
+
const contextLoader: DocumentLoader = async (url) => {
|
|
1663
|
+
if (url === contextUrl) {
|
|
1664
|
+
version++;
|
|
1665
|
+
return {
|
|
1666
|
+
contextUrl: null,
|
|
1667
|
+
documentUrl: url,
|
|
1668
|
+
document: { "@context": { version: `urn:version:${version}` } },
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
return await mockDocumentLoader(url);
|
|
1672
|
+
};
|
|
1673
|
+
const contexts: unknown[] = [];
|
|
1674
|
+
const object = await (await createHttpActivity(plainUrl)).getObject({
|
|
1675
|
+
documentLoader: createRedirectingLoader({
|
|
1676
|
+
[plainUrl]: {
|
|
1677
|
+
documentUrl: compatibleNoteId,
|
|
1678
|
+
document: {
|
|
1679
|
+
...note(),
|
|
1680
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
1681
|
+
},
|
|
1682
|
+
},
|
|
1683
|
+
}),
|
|
1684
|
+
contextLoader,
|
|
1685
|
+
verifyPortableObject: async (_, { contextLoader }) => {
|
|
1686
|
+
contexts.push((await contextLoader!(contextUrl)).document);
|
|
1687
|
+
return { verified: true };
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
assertInstanceOf(object, Note);
|
|
1691
|
+
deepStrictEqual(version, 1);
|
|
1692
|
+
deepStrictEqual(contexts, [{ "@context": { version: "urn:version:1" } }]);
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
await t.step("without verifyPortableObject", async () => {
|
|
1696
|
+
// Fetched as before, i.e., refused as a cross-origin object:
|
|
1697
|
+
deepStrictEqual(
|
|
1698
|
+
await (await createHttpActivity(plainUrl)).getObject({
|
|
1699
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1700
|
+
contextLoader: mockDocumentLoader,
|
|
1701
|
+
}),
|
|
1702
|
+
null,
|
|
1703
|
+
);
|
|
1704
|
+
// A redirect to a compatible identifier of the same origin:
|
|
1705
|
+
const object = await (await createHttpActivity(plainUrl)).getObject({
|
|
1706
|
+
documentLoader: createRedirectingLoader({
|
|
1707
|
+
[plainUrl]: {
|
|
1708
|
+
documentUrl: gatewayUrl("https://example.com", "/objects/1"),
|
|
1709
|
+
document: note(gatewayUrl("https://example.com", "/objects/1")),
|
|
1710
|
+
},
|
|
1711
|
+
}),
|
|
1712
|
+
contextLoader: mockDocumentLoader,
|
|
1713
|
+
});
|
|
1714
|
+
assertInstanceOf(object, Note);
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
test("accessors verify embedded objects that stand for portable objects", async () => {
|
|
1719
|
+
const forgedId = gatewayUrl("https://example.com", "/objects/1");
|
|
1720
|
+
const forged = { type: "Note", id: forgedId, content: "Forged" };
|
|
1721
|
+
for (const crossOrigin of [undefined, "trust"] as const) {
|
|
1722
|
+
// A compatible identifier of the parent's origin:
|
|
1723
|
+
const documentLoader = createLoader({ [forgedId]: note() });
|
|
1724
|
+
const verifier = createRecordingVerifier(() => ({ verified: false }));
|
|
1725
|
+
deepStrictEqual(
|
|
1726
|
+
await (await createHttpActivity(forged)).getObject({
|
|
1727
|
+
documentLoader,
|
|
1728
|
+
contextLoader: mockDocumentLoader,
|
|
1729
|
+
verifyPortableObject: verifier,
|
|
1730
|
+
crossOrigin,
|
|
1731
|
+
}),
|
|
1732
|
+
null,
|
|
1733
|
+
);
|
|
1734
|
+
deepStrictEqual(documentLoader.fetched, [forgedId]);
|
|
1735
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1736
|
+
deepStrictEqual(
|
|
1737
|
+
await Array.fromAsync(
|
|
1738
|
+
(await createHttpActivity([forged])).getObjects({
|
|
1739
|
+
documentLoader,
|
|
1740
|
+
contextLoader: mockDocumentLoader,
|
|
1741
|
+
verifyPortableObject: verifier,
|
|
1742
|
+
crossOrigin,
|
|
1743
|
+
}),
|
|
1744
|
+
),
|
|
1745
|
+
[],
|
|
1746
|
+
);
|
|
1747
|
+
|
|
1748
|
+
// A portable IRI embedded in a non-portable object:
|
|
1749
|
+
const documentLoader2 = createLoader({
|
|
1750
|
+
[gatewayUrl("https://gw.example", "/objects/1")]: note(),
|
|
1751
|
+
});
|
|
1752
|
+
const verifier2 = createRecordingVerifier();
|
|
1753
|
+
const object = await (await createHttpActivity({
|
|
1754
|
+
type: "Note",
|
|
1755
|
+
id: objectId,
|
|
1756
|
+
content: "Forged",
|
|
1757
|
+
})).getObject({
|
|
1758
|
+
documentLoader: documentLoader2,
|
|
1759
|
+
contextLoader: mockDocumentLoader,
|
|
1760
|
+
gateways: ["https://gw.example"],
|
|
1761
|
+
verifyPortableObject: verifier2,
|
|
1762
|
+
crossOrigin,
|
|
1763
|
+
});
|
|
1764
|
+
assertInstanceOf(object, Note);
|
|
1765
|
+
deepStrictEqual(object.content, "Portable note");
|
|
1766
|
+
deepStrictEqual(verifier2.calls.length, 1);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// A portable object with another DID embedded in a portable object:
|
|
1770
|
+
const otherDid = "did:key:z6Mkother";
|
|
1771
|
+
const otherId = `ap://${otherDid}/objects/1`;
|
|
1772
|
+
const documentLoader = createLoader({
|
|
1773
|
+
[`https://gw.example/.well-known/apgateway/${otherDid}/objects/1`]: note(
|
|
1774
|
+
otherId,
|
|
1775
|
+
),
|
|
1776
|
+
});
|
|
1777
|
+
const verifier = createRecordingVerifier();
|
|
1778
|
+
const crossDid = await (await createActivity({
|
|
1779
|
+
type: "Note",
|
|
1780
|
+
id: otherId,
|
|
1781
|
+
content: "Forged",
|
|
1782
|
+
} as unknown as string)).getObject({
|
|
1783
|
+
documentLoader,
|
|
1784
|
+
contextLoader: mockDocumentLoader,
|
|
1785
|
+
gateways: ["https://gw.example"],
|
|
1786
|
+
verifyPortableObject: verifier,
|
|
1787
|
+
crossOrigin: "trust",
|
|
1788
|
+
});
|
|
1789
|
+
assertInstanceOf(crossDid, Note);
|
|
1790
|
+
deepStrictEqual(crossDid.content, "Portable note");
|
|
1791
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1792
|
+
|
|
1793
|
+
// A portable object embedded in a portable object with the same DID is
|
|
1794
|
+
// still trusted as embedded:
|
|
1795
|
+
const activity = await createActivity({
|
|
1796
|
+
type: "Note",
|
|
1797
|
+
id: objectId,
|
|
1798
|
+
content: "Embedded",
|
|
1799
|
+
} as unknown as string);
|
|
1800
|
+
const object = await activity.getObject({
|
|
1801
|
+
documentLoader: createLoader({}),
|
|
1802
|
+
contextLoader: mockDocumentLoader,
|
|
1803
|
+
verifyPortableObject: createVerifier(false),
|
|
1804
|
+
});
|
|
1805
|
+
assertInstanceOf(object, Note);
|
|
1806
|
+
deepStrictEqual(object.content, "Embedded");
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
test("singular accessors drop anonymous objects embedded in unsecured collections", async () => {
|
|
1810
|
+
const documentLoader = createLoader({
|
|
1811
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection(
|
|
1812
|
+
"/actor/outbox",
|
|
1813
|
+
{
|
|
1814
|
+
// An anonymous page cannot be fetched and verified on its own:
|
|
1815
|
+
first: {
|
|
1816
|
+
type: "OrderedCollectionPage",
|
|
1817
|
+
orderedItems: [{ type: "Note", content: "Anonymous" }],
|
|
1818
|
+
},
|
|
1819
|
+
},
|
|
1820
|
+
),
|
|
1821
|
+
});
|
|
1822
|
+
const options = {
|
|
1823
|
+
documentLoader,
|
|
1824
|
+
contextLoader: mockDocumentLoader,
|
|
1825
|
+
gateways: ["https://gw.example"],
|
|
1826
|
+
verifyPortableObject: createRecordingVerifier(() => ({
|
|
1827
|
+
verified: true,
|
|
1828
|
+
unsecured: true,
|
|
1829
|
+
})),
|
|
1830
|
+
};
|
|
1831
|
+
const person = await Person.fromJsonLd({
|
|
1832
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1833
|
+
id: `ap://${did}/actor`,
|
|
1834
|
+
type: "Person",
|
|
1835
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
1836
|
+
outbox: `ap://${did}/actor/outbox`,
|
|
1837
|
+
}, { contextLoader: mockDocumentLoader });
|
|
1838
|
+
const outbox = await person.getOutbox(options);
|
|
1839
|
+
assertInstanceOf(outbox, OrderedCollection);
|
|
1840
|
+
deepStrictEqual(await outbox.getFirst(options), null);
|
|
1841
|
+
deepStrictEqual(
|
|
1842
|
+
await Array.fromAsync(traverseCollection(outbox, options)),
|
|
1843
|
+
[],
|
|
1844
|
+
);
|
|
1845
|
+
});
|