@fedify/vocab 2.4.0-pr.936.41 → 2.4.0
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 +31328 -8723
- package/dist/mod.d.cts +4599 -6
- package/dist/mod.d.ts +4599 -6
- package/dist/mod.js +31221 -8623
- package/dist-tests/actor-D7_iX4I4.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/{actor-MBpzF3-D.mjs → esm-DP2Spt0u.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/signed-representation.test.mjs +189 -0
- package/dist-tests/translation.test.mjs +404 -0
- package/dist-tests/translation.yaml +91 -0
- package/dist-tests/type.test.mjs +2 -2
- package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-COZcxf00.mjs} +30831 -8515
- package/dist-tests/vocab.test.mjs +677 -10
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +291 -15
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/gateway.test.ts +2637 -0
- package/src/lookup.test.ts +6 -5
- package/src/lookup.ts +460 -33
- package/src/mod.ts +9 -0
- package/src/object.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +1 -1
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/vocab.test.ts +914 -5
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
|
@@ -0,0 +1,1906 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
globalThis.addEventListener = () => {};
|
|
3
|
+
import { F as Person, M as OrderedCollection, S as Image, c as CollectionPage, k as Note, s as Collection, u as Create } from "./vocab-COZcxf00.mjs";
|
|
4
|
+
import { t as esm_default } from "./esm-DP2Spt0u.mjs";
|
|
5
|
+
import { n as traverseCollection, t as lookupObject } from "./lookup-IiNFWuXL.mjs";
|
|
6
|
+
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
7
|
+
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
8
|
+
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
9
|
+
import { FetchError, getDocumentLoader, parseIri, withGatewayHints } from "@fedify/vocab-runtime";
|
|
10
|
+
//#region src/gateway.test.ts
|
|
11
|
+
const did = "did:key:z6Mkabc";
|
|
12
|
+
const objectId = `ap+ef61://${did}/objects/1`;
|
|
13
|
+
const gatewayPath = `/.well-known/apgateway/${did}/objects/1`;
|
|
14
|
+
function note(id = `ap://${did}/objects/1`) {
|
|
15
|
+
return {
|
|
16
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
17
|
+
id,
|
|
18
|
+
type: "Note",
|
|
19
|
+
content: "Portable note"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function createLoader(responses) {
|
|
23
|
+
const fetched = [];
|
|
24
|
+
const loader = async (url) => {
|
|
25
|
+
fetched.push(url);
|
|
26
|
+
const response = responses[url];
|
|
27
|
+
if (response == null) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
|
|
28
|
+
if (response instanceof Error) throw response;
|
|
29
|
+
if (typeof response === "number") throw new FetchError(url, `HTTP ${response}`, new globalThis.Response(null, { status: response }));
|
|
30
|
+
return {
|
|
31
|
+
contextUrl: null,
|
|
32
|
+
documentUrl: url,
|
|
33
|
+
document: response
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
return Object.assign(loader, { fetched });
|
|
37
|
+
}
|
|
38
|
+
function createVerifier(verified = true) {
|
|
39
|
+
const documents = [];
|
|
40
|
+
const verifier = async (document) => {
|
|
41
|
+
documents.push(document);
|
|
42
|
+
return { verified };
|
|
43
|
+
};
|
|
44
|
+
return Object.assign(verifier, { documents });
|
|
45
|
+
}
|
|
46
|
+
function createActivity(object = objectId) {
|
|
47
|
+
return Create.fromJsonLd({
|
|
48
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
49
|
+
type: "Create",
|
|
50
|
+
id: `ap://${did}/activities/1`,
|
|
51
|
+
object
|
|
52
|
+
}, { contextLoader: mockDocumentLoader });
|
|
53
|
+
}
|
|
54
|
+
test("getObject() dereferences a portable IRI through explicit gateways", async () => {
|
|
55
|
+
const document = note();
|
|
56
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: document });
|
|
57
|
+
const verifyPortableObject = createVerifier();
|
|
58
|
+
const activity = await createActivity();
|
|
59
|
+
const object = await activity.getObject({
|
|
60
|
+
documentLoader,
|
|
61
|
+
contextLoader: mockDocumentLoader,
|
|
62
|
+
gateways: ["https://gw1.example", new URL("https://gw2.example/")],
|
|
63
|
+
verifyPortableObject
|
|
64
|
+
});
|
|
65
|
+
assertInstanceOf(object, Note);
|
|
66
|
+
deepStrictEqual(object.id, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
|
|
67
|
+
deepStrictEqual(object.content, "Portable note");
|
|
68
|
+
deepStrictEqual(documentLoader.fetched, [`https://gw1.example${gatewayPath}`]);
|
|
69
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
70
|
+
deepStrictEqual((await activity.getObject({
|
|
71
|
+
documentLoader,
|
|
72
|
+
contextLoader: mockDocumentLoader
|
|
73
|
+
}))?.content, "Portable note");
|
|
74
|
+
deepStrictEqual(documentLoader.fetched.length, 1);
|
|
75
|
+
});
|
|
76
|
+
test("getObject() keeps non-hint query parameters for gateways", async () => {
|
|
77
|
+
const documentLoader = createLoader({ [`https://gw.example${gatewayPath}?page=2`]: note(`ap://${did}/objects/1?page=2`) });
|
|
78
|
+
assertInstanceOf(await (await createActivity(`${objectId}?page=2&@gateway=https%3A%2F%2Fgw.example`)).getObject({
|
|
79
|
+
documentLoader,
|
|
80
|
+
contextLoader: mockDocumentLoader,
|
|
81
|
+
verifyPortableObject: createVerifier()
|
|
82
|
+
}), Note);
|
|
83
|
+
deepStrictEqual(documentLoader.fetched, [`https://gw.example${gatewayPath}?page=2`]);
|
|
84
|
+
});
|
|
85
|
+
test("getObject() tries the next gateway after failures", async () => {
|
|
86
|
+
const documentLoader = createLoader({
|
|
87
|
+
[`https://gw2.example${gatewayPath}`]: /* @__PURE__ */ new TypeError("Network error"),
|
|
88
|
+
[`https://gw3.example${gatewayPath}`]: 500,
|
|
89
|
+
[`https://gw4.example${gatewayPath}`]: note(),
|
|
90
|
+
[`https://gw5.example${gatewayPath}`]: note()
|
|
91
|
+
});
|
|
92
|
+
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
93
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
94
|
+
documentLoader,
|
|
95
|
+
contextLoader: mockDocumentLoader,
|
|
96
|
+
tracerProvider,
|
|
97
|
+
gateways: [
|
|
98
|
+
"https://gw1.example",
|
|
99
|
+
"https://gw2.example",
|
|
100
|
+
"https://gw3.example",
|
|
101
|
+
"https://gw4.example",
|
|
102
|
+
"https://gw5.example"
|
|
103
|
+
],
|
|
104
|
+
verifyPortableObject: createVerifier()
|
|
105
|
+
}), Note);
|
|
106
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
107
|
+
`https://gw1.example${gatewayPath}`,
|
|
108
|
+
`https://gw2.example${gatewayPath}`,
|
|
109
|
+
`https://gw3.example${gatewayPath}`,
|
|
110
|
+
`https://gw4.example${gatewayPath}`
|
|
111
|
+
]);
|
|
112
|
+
const spans = exporter.getSpans("activitypub.lookup_object");
|
|
113
|
+
deepStrictEqual(spans.length, 1);
|
|
114
|
+
deepStrictEqual(spans[0].status.code, 0);
|
|
115
|
+
deepStrictEqual(spans[0].attributes["activitypub.gateway"], "https://gw4.example/");
|
|
116
|
+
deepStrictEqual(spans[0].attributes["activitypub.object.type"], "https://www.w3.org/ns/activitystreams#Note");
|
|
117
|
+
});
|
|
118
|
+
test("getObject() reports errors when every gateway fails", async () => {
|
|
119
|
+
const single = createLoader({ [`https://gw1.example${gatewayPath}`]: 500 });
|
|
120
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
121
|
+
documentLoader: single,
|
|
122
|
+
contextLoader: mockDocumentLoader,
|
|
123
|
+
gateways: ["https://gw1.example"],
|
|
124
|
+
verifyPortableObject: createVerifier()
|
|
125
|
+
}), FetchError);
|
|
126
|
+
const options = {
|
|
127
|
+
documentLoader: createLoader({
|
|
128
|
+
[`https://gw1.example${gatewayPath}`]: 500,
|
|
129
|
+
[`https://gw2.example${gatewayPath}`]: /* @__PURE__ */ new TypeError("Network error")
|
|
130
|
+
}),
|
|
131
|
+
contextLoader: mockDocumentLoader,
|
|
132
|
+
gateways: ["https://gw1.example", "https://gw2.example"],
|
|
133
|
+
verifyPortableObject: createVerifier()
|
|
134
|
+
};
|
|
135
|
+
await rejects(async () => await (await createActivity()).getObject(options), (error) => {
|
|
136
|
+
assertInstanceOf(error, AggregateError);
|
|
137
|
+
deepStrictEqual(error.errors.length, 2);
|
|
138
|
+
assertInstanceOf(error.errors[0], FetchError);
|
|
139
|
+
assertInstanceOf(error.errors[1], TypeError);
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
143
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
144
|
+
...options,
|
|
145
|
+
suppressError: true,
|
|
146
|
+
tracerProvider
|
|
147
|
+
}), null);
|
|
148
|
+
const spans = exporter.getSpans("activitypub.lookup_object");
|
|
149
|
+
deepStrictEqual(spans.length, 1);
|
|
150
|
+
deepStrictEqual(spans[0].status.code, 2);
|
|
151
|
+
const unparsable = createLoader({ [`https://gw1.example${gatewayPath}`]: {
|
|
152
|
+
...note(),
|
|
153
|
+
"@context": "https://example.com/unknown-context"
|
|
154
|
+
} });
|
|
155
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
156
|
+
documentLoader: unparsable,
|
|
157
|
+
contextLoader: mockDocumentLoader,
|
|
158
|
+
gateways: ["https://gw1.example"],
|
|
159
|
+
verifyPortableObject: createVerifier()
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
test("getObject() rejects objects with a mismatching portable ID", async (t) => {
|
|
163
|
+
const mismatches = {
|
|
164
|
+
"another DID": `ap://did:key:z6Mkdef/objects/1`,
|
|
165
|
+
"another path": `ap://${did}/objects/2`,
|
|
166
|
+
"a dot segment": `ap://${did}/objects/x/../1`,
|
|
167
|
+
"a fragment": `ap://${did}/objects/1#fragment`,
|
|
168
|
+
"a compatible identifier of another DID": `https://gw1.example/.well-known/apgateway/did:key:z6Mkdef/objects/1`,
|
|
169
|
+
"a compatible identifier of another path": `https://gw1.example/.well-known/apgateway/${did}/objects/2`,
|
|
170
|
+
"an HTTP(S) ID": "https://gw1.example/objects/1"
|
|
171
|
+
};
|
|
172
|
+
for (const [name, id] of globalThis.Object.entries(mismatches)) await t.step(name, async () => {
|
|
173
|
+
const options = {
|
|
174
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: note(id) }),
|
|
175
|
+
contextLoader: mockDocumentLoader,
|
|
176
|
+
gateways: ["https://gw1.example"],
|
|
177
|
+
verifyPortableObject: createVerifier()
|
|
178
|
+
};
|
|
179
|
+
deepStrictEqual(await (await createActivity()).getObject(options), null);
|
|
180
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
181
|
+
...options,
|
|
182
|
+
crossOrigin: "trust"
|
|
183
|
+
}), null);
|
|
184
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
185
|
+
...options,
|
|
186
|
+
crossOrigin: "throw",
|
|
187
|
+
suppressError: true
|
|
188
|
+
}), null);
|
|
189
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
190
|
+
...options,
|
|
191
|
+
crossOrigin: "throw"
|
|
192
|
+
}), /No gateway returned a valid portable object/);
|
|
193
|
+
});
|
|
194
|
+
await t.step("equivalent spellings", async () => {
|
|
195
|
+
for (const id of [
|
|
196
|
+
`ap+ef61://${did}/objects/1`,
|
|
197
|
+
`ap://did%3Akey%3Az6Mkabc/objects/1`,
|
|
198
|
+
`ap://${did}/objects/1?@gateway=https%3A%2F%2Fother.example`
|
|
199
|
+
]) {
|
|
200
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: note(id) });
|
|
201
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
202
|
+
documentLoader,
|
|
203
|
+
contextLoader: mockDocumentLoader,
|
|
204
|
+
gateways: ["https://gw1.example"],
|
|
205
|
+
verifyPortableObject: createVerifier()
|
|
206
|
+
}), Note);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
await t.step("a mismatch does not stop other gateways", async () => {
|
|
210
|
+
const documentLoader = createLoader({
|
|
211
|
+
[`https://gw1.example${gatewayPath}`]: note(`ap://${did}/objects/2`),
|
|
212
|
+
[`https://gw2.example${gatewayPath}`]: 500,
|
|
213
|
+
[`https://gw3.example${gatewayPath}`]: note()
|
|
214
|
+
});
|
|
215
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
216
|
+
documentLoader,
|
|
217
|
+
contextLoader: mockDocumentLoader,
|
|
218
|
+
gateways: [
|
|
219
|
+
"https://gw1.example",
|
|
220
|
+
"https://gw2.example",
|
|
221
|
+
"https://gw3.example"
|
|
222
|
+
],
|
|
223
|
+
verifyPortableObject: createVerifier(),
|
|
224
|
+
crossOrigin: "throw"
|
|
225
|
+
}), Note);
|
|
226
|
+
});
|
|
227
|
+
await t.step("a mismatch among errors is reported as a mismatch", async () => {
|
|
228
|
+
const documentLoader = createLoader({
|
|
229
|
+
[`https://gw1.example${gatewayPath}`]: 500,
|
|
230
|
+
[`https://gw2.example${gatewayPath}`]: note(`ap://${did}/objects/2`)
|
|
231
|
+
});
|
|
232
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
233
|
+
documentLoader,
|
|
234
|
+
contextLoader: mockDocumentLoader,
|
|
235
|
+
gateways: ["https://gw1.example", "https://gw2.example"],
|
|
236
|
+
verifyPortableObject: createVerifier()
|
|
237
|
+
}), null);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
test("getObject() rejects documents with other shapes", async () => {
|
|
241
|
+
for (const document of [[note()], {
|
|
242
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
243
|
+
"@graph": [note(), note(`ap://${did}/objects/2`)]
|
|
244
|
+
}]) {
|
|
245
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: document });
|
|
246
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
247
|
+
documentLoader,
|
|
248
|
+
contextLoader: mockDocumentLoader,
|
|
249
|
+
gateways: ["https://gw1.example"],
|
|
250
|
+
verifyPortableObject: createVerifier()
|
|
251
|
+
}), null);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
test("getObject() leaves documents without @id to the verifier", async () => {
|
|
255
|
+
const document = {
|
|
256
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
257
|
+
type: "Note",
|
|
258
|
+
content: "No ID"
|
|
259
|
+
};
|
|
260
|
+
const options = {
|
|
261
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: document }),
|
|
262
|
+
contextLoader: mockDocumentLoader,
|
|
263
|
+
gateways: ["https://gw1.example"]
|
|
264
|
+
};
|
|
265
|
+
const rejecting = createVerifier(false);
|
|
266
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
267
|
+
...options,
|
|
268
|
+
verifyPortableObject: rejecting
|
|
269
|
+
}), null);
|
|
270
|
+
deepStrictEqual(rejecting.documents, [document]);
|
|
271
|
+
const object = await (await createActivity()).getObject({
|
|
272
|
+
...options,
|
|
273
|
+
verifyPortableObject: createVerifier()
|
|
274
|
+
});
|
|
275
|
+
assertInstanceOf(object, Note);
|
|
276
|
+
deepStrictEqual(object.id, null);
|
|
277
|
+
});
|
|
278
|
+
test("getObject() applies the portable object verifier", async () => {
|
|
279
|
+
const gateways = ["https://gw1.example", "https://gw2.example"];
|
|
280
|
+
const documentLoader = createLoader({
|
|
281
|
+
[`https://gw1.example${gatewayPath}`]: note(),
|
|
282
|
+
[`https://gw2.example${gatewayPath}`]: note()
|
|
283
|
+
});
|
|
284
|
+
const failing = createVerifier(false);
|
|
285
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
286
|
+
documentLoader,
|
|
287
|
+
contextLoader: mockDocumentLoader,
|
|
288
|
+
gateways,
|
|
289
|
+
verifyPortableObject: failing,
|
|
290
|
+
crossOrigin: "trust"
|
|
291
|
+
}), null);
|
|
292
|
+
deepStrictEqual(failing.documents.length, 2);
|
|
293
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
294
|
+
documentLoader,
|
|
295
|
+
contextLoader: mockDocumentLoader,
|
|
296
|
+
gateways,
|
|
297
|
+
verifyPortableObject: failing,
|
|
298
|
+
crossOrigin: "throw"
|
|
299
|
+
}), /No gateway returned a valid portable object/);
|
|
300
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
301
|
+
documentLoader,
|
|
302
|
+
contextLoader: mockDocumentLoader,
|
|
303
|
+
gateways,
|
|
304
|
+
verifyPortableObject: async () => {
|
|
305
|
+
throw new Error("Verifier failure");
|
|
306
|
+
}
|
|
307
|
+
}), null);
|
|
308
|
+
const activity = await createActivity();
|
|
309
|
+
deepStrictEqual(await activity.getObject({
|
|
310
|
+
documentLoader,
|
|
311
|
+
contextLoader: mockDocumentLoader,
|
|
312
|
+
gateways,
|
|
313
|
+
verifyPortableObject: failing
|
|
314
|
+
}), null);
|
|
315
|
+
assertInstanceOf(await activity.getObject({
|
|
316
|
+
documentLoader,
|
|
317
|
+
contextLoader: mockDocumentLoader,
|
|
318
|
+
gateways,
|
|
319
|
+
verifyPortableObject: createVerifier()
|
|
320
|
+
}), Note);
|
|
321
|
+
});
|
|
322
|
+
test("getObject() requires a verifier before fetching portable IRIs", async () => {
|
|
323
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: note() });
|
|
324
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
325
|
+
documentLoader,
|
|
326
|
+
contextLoader: mockDocumentLoader,
|
|
327
|
+
gateways: ["https://gw1.example"]
|
|
328
|
+
}), (error) => {
|
|
329
|
+
assertInstanceOf(error, TypeError);
|
|
330
|
+
ok(error.message.includes("verifyPortableObject"));
|
|
331
|
+
return true;
|
|
332
|
+
});
|
|
333
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
334
|
+
documentLoader,
|
|
335
|
+
contextLoader: mockDocumentLoader,
|
|
336
|
+
gateways: ["https://gw1.example"],
|
|
337
|
+
suppressError: true
|
|
338
|
+
}), null);
|
|
339
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
340
|
+
});
|
|
341
|
+
test("getObject() suppresses errors for malformed portable IRIs", async () => {
|
|
342
|
+
const activity = await createActivity(`ap://${did}/objects/%ZZ`);
|
|
343
|
+
for (const gateways of [[], ["https://gw1.example"]]) {
|
|
344
|
+
const documentLoader = createLoader({});
|
|
345
|
+
deepStrictEqual(await activity.getObject({
|
|
346
|
+
documentLoader,
|
|
347
|
+
contextLoader: mockDocumentLoader,
|
|
348
|
+
gateways,
|
|
349
|
+
verifyPortableObject: createVerifier(),
|
|
350
|
+
suppressError: true
|
|
351
|
+
}), null);
|
|
352
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
353
|
+
await rejects(async () => await activity.getObject({
|
|
354
|
+
documentLoader,
|
|
355
|
+
contextLoader: mockDocumentLoader,
|
|
356
|
+
gateways,
|
|
357
|
+
verifyPortableObject: createVerifier()
|
|
358
|
+
}), TypeError);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
test("getObject() rejects invalid gateways before fetching", async () => {
|
|
362
|
+
const documentLoader = createLoader({});
|
|
363
|
+
for (const gateway of [
|
|
364
|
+
"https://gw.example/path",
|
|
365
|
+
"https://gw.example/?query",
|
|
366
|
+
"https://user@gw.example",
|
|
367
|
+
"ftp://gw.example",
|
|
368
|
+
"not a URL"
|
|
369
|
+
]) await rejects(async () => await (await createActivity()).getObject({
|
|
370
|
+
documentLoader,
|
|
371
|
+
contextLoader: mockDocumentLoader,
|
|
372
|
+
gateways: [gateway],
|
|
373
|
+
verifyPortableObject: createVerifier(),
|
|
374
|
+
suppressError: true
|
|
375
|
+
}), TypeError);
|
|
376
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
377
|
+
});
|
|
378
|
+
test("getObject() uses @gateway location hints", async (t) => {
|
|
379
|
+
await t.step("in order, skipping invalid and duplicate hints", async () => {
|
|
380
|
+
const documentLoader = createLoader({ [`https://gw2.example${gatewayPath}`]: note() });
|
|
381
|
+
assertInstanceOf(await (await createActivity(`${objectId}?@gateway=https%3A%2F%2Fgw1.example&@gateway=https%3A%2F%2Fgw1.example%2F&@gateway=https%3A%2F%2Finvalid.example%2Fpath&%40gateway=https%3A%2F%2Fgw2.example&gateways=https%3A%2F%2Flegacy.example`)).getObject({
|
|
382
|
+
documentLoader,
|
|
383
|
+
contextLoader: mockDocumentLoader,
|
|
384
|
+
verifyPortableObject: createVerifier()
|
|
385
|
+
}), Note);
|
|
386
|
+
deepStrictEqual(documentLoader.fetched, [`https://gw1.example${gatewayPath}`, `https://gw2.example${gatewayPath}`]);
|
|
387
|
+
});
|
|
388
|
+
await t.step("up to five hints", async () => {
|
|
389
|
+
const documentLoader = createLoader({});
|
|
390
|
+
const hints = [
|
|
391
|
+
1,
|
|
392
|
+
2,
|
|
393
|
+
3,
|
|
394
|
+
4,
|
|
395
|
+
5,
|
|
396
|
+
6,
|
|
397
|
+
7
|
|
398
|
+
].map((i) => `@gateway=https%3A%2F%2Fgw${i}.example`);
|
|
399
|
+
const activity = await createActivity(`${objectId}?${hints.join("&")}`);
|
|
400
|
+
await rejects(async () => await activity.getObject({
|
|
401
|
+
documentLoader,
|
|
402
|
+
contextLoader: mockDocumentLoader,
|
|
403
|
+
verifyPortableObject: createVerifier()
|
|
404
|
+
}));
|
|
405
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
406
|
+
1,
|
|
407
|
+
2,
|
|
408
|
+
3,
|
|
409
|
+
4,
|
|
410
|
+
5
|
|
411
|
+
].map((i) => `https://gw${i}.example${gatewayPath}`));
|
|
412
|
+
});
|
|
413
|
+
await t.step("replaced by explicit gateways", async () => {
|
|
414
|
+
const documentLoader = createLoader({ [`https://explicit.example${gatewayPath}`]: note() });
|
|
415
|
+
assertInstanceOf(await (await createActivity(`${objectId}?@gateway=https%3A%2F%2Fhint.example`)).getObject({
|
|
416
|
+
documentLoader,
|
|
417
|
+
contextLoader: mockDocumentLoader,
|
|
418
|
+
gateways: ["https://explicit.example"],
|
|
419
|
+
verifyPortableObject: createVerifier()
|
|
420
|
+
}), Note);
|
|
421
|
+
deepStrictEqual(documentLoader.fetched, [`https://explicit.example${gatewayPath}`]);
|
|
422
|
+
});
|
|
423
|
+
await t.step("disabled by an empty gateway list", async () => {
|
|
424
|
+
const documentLoader = createLoader({});
|
|
425
|
+
deepStrictEqual(await (await createActivity(`${objectId}?@gateway=https%3A%2F%2Fhint.example`)).getObject({
|
|
426
|
+
documentLoader,
|
|
427
|
+
contextLoader: mockDocumentLoader,
|
|
428
|
+
gateways: [],
|
|
429
|
+
verifyPortableObject: createVerifier(),
|
|
430
|
+
suppressError: true
|
|
431
|
+
}), null);
|
|
432
|
+
deepStrictEqual(documentLoader.fetched, [`ap+ef61://${did}/objects/1?@gateway=https%3A%2F%2Fhint.example`]);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
test("getObject() lets the document loader handle portable IRIs without gateways", async () => {
|
|
436
|
+
const documentLoader = createLoader({ [objectId]: note() });
|
|
437
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
438
|
+
documentLoader,
|
|
439
|
+
contextLoader: mockDocumentLoader,
|
|
440
|
+
verifyPortableObject: createVerifier()
|
|
441
|
+
}), Note);
|
|
442
|
+
deepStrictEqual(documentLoader.fetched, [objectId]);
|
|
443
|
+
const spoofing = createLoader({ [objectId]: note(`ap://${did}/objects/2`) });
|
|
444
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
445
|
+
documentLoader: spoofing,
|
|
446
|
+
contextLoader: mockDocumentLoader,
|
|
447
|
+
verifyPortableObject: createVerifier()
|
|
448
|
+
}), null);
|
|
449
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
450
|
+
documentLoader,
|
|
451
|
+
contextLoader: mockDocumentLoader,
|
|
452
|
+
verifyPortableObject: createVerifier(false)
|
|
453
|
+
}), null);
|
|
454
|
+
});
|
|
455
|
+
test("getObject() shares context documents within a dereference", async (t) => {
|
|
456
|
+
const contextUrl = "https://example.com/portable-context";
|
|
457
|
+
const document = {
|
|
458
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
459
|
+
id: `ap://${did}/objects/1`,
|
|
460
|
+
type: "Note",
|
|
461
|
+
content: "Portable note"
|
|
462
|
+
};
|
|
463
|
+
await t.step("custom contexts are loaded once", async () => {
|
|
464
|
+
const loaded = [];
|
|
465
|
+
let version = 0;
|
|
466
|
+
const contextLoader = async (url) => {
|
|
467
|
+
loaded.push(url);
|
|
468
|
+
if (url === contextUrl) {
|
|
469
|
+
version++;
|
|
470
|
+
return {
|
|
471
|
+
contextUrl: null,
|
|
472
|
+
documentUrl: url,
|
|
473
|
+
document: { "@context": { version: `urn:version:${version}` } }
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
return await mockDocumentLoader(url);
|
|
477
|
+
};
|
|
478
|
+
const contexts = [];
|
|
479
|
+
const verifyPortableObject = async (_, { contextLoader }) => {
|
|
480
|
+
contexts.push((await contextLoader(contextUrl)).document);
|
|
481
|
+
contexts.push((await contextLoader(contextUrl)).document);
|
|
482
|
+
return { verified: true };
|
|
483
|
+
};
|
|
484
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
485
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: document }),
|
|
486
|
+
contextLoader,
|
|
487
|
+
gateways: ["https://gw1.example"],
|
|
488
|
+
verifyPortableObject
|
|
489
|
+
}), Note);
|
|
490
|
+
deepStrictEqual(loaded.filter((url) => url === contextUrl).length, 1);
|
|
491
|
+
deepStrictEqual(contexts, [{ "@context": { version: "urn:version:1" } }, { "@context": { version: "urn:version:1" } }]);
|
|
492
|
+
});
|
|
493
|
+
await t.step("baseline contexts use built-in copies", async () => {
|
|
494
|
+
const loaded = [];
|
|
495
|
+
const contextLoader = async (url) => {
|
|
496
|
+
loaded.push(url);
|
|
497
|
+
return await mockDocumentLoader(url);
|
|
498
|
+
};
|
|
499
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
500
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: note() }),
|
|
501
|
+
contextLoader,
|
|
502
|
+
gateways: ["https://gw1.example"],
|
|
503
|
+
verifyPortableObject: createVerifier()
|
|
504
|
+
}), Note);
|
|
505
|
+
deepStrictEqual(loaded, []);
|
|
506
|
+
});
|
|
507
|
+
await t.step("the snapshot ends with the dereference", async () => {
|
|
508
|
+
const loaded = [];
|
|
509
|
+
const contextLoader = async (url) => {
|
|
510
|
+
loaded.push(url);
|
|
511
|
+
if (url === contextUrl) return {
|
|
512
|
+
contextUrl: null,
|
|
513
|
+
documentUrl: url,
|
|
514
|
+
document: { "@context": {} }
|
|
515
|
+
};
|
|
516
|
+
return await mockDocumentLoader(url);
|
|
517
|
+
};
|
|
518
|
+
const object = await (await createActivity()).getObject({
|
|
519
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: {
|
|
520
|
+
...document,
|
|
521
|
+
attributedTo: "https://example.com/person"
|
|
522
|
+
} }),
|
|
523
|
+
contextLoader,
|
|
524
|
+
gateways: ["https://gw1.example"],
|
|
525
|
+
verifyPortableObject: createVerifier()
|
|
526
|
+
});
|
|
527
|
+
assertInstanceOf(object, Note);
|
|
528
|
+
deepStrictEqual(loaded, [contextUrl]);
|
|
529
|
+
deepStrictEqual((await object.getAttribution({ documentLoader: createLoader({ "https://example.com/person": {
|
|
530
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
531
|
+
id: "https://example.com/person",
|
|
532
|
+
type: "Person"
|
|
533
|
+
} }) }))?.id, new URL("https://example.com/person"));
|
|
534
|
+
deepStrictEqual(loaded, [contextUrl, contextUrl]);
|
|
535
|
+
});
|
|
536
|
+
await t.step("failed context loads are retried", async () => {
|
|
537
|
+
let failures = 1;
|
|
538
|
+
const contextLoader = async (url) => {
|
|
539
|
+
if (url === contextUrl && failures-- > 0) throw new TypeError("Temporary failure");
|
|
540
|
+
if (url === contextUrl) return {
|
|
541
|
+
contextUrl: null,
|
|
542
|
+
documentUrl: url,
|
|
543
|
+
document: { "@context": {} }
|
|
544
|
+
};
|
|
545
|
+
return await mockDocumentLoader(url);
|
|
546
|
+
};
|
|
547
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
548
|
+
documentLoader: createLoader({
|
|
549
|
+
[`https://gw1.example${gatewayPath}`]: document,
|
|
550
|
+
[`https://gw2.example${gatewayPath}`]: document
|
|
551
|
+
}),
|
|
552
|
+
contextLoader,
|
|
553
|
+
gateways: ["https://gw1.example", "https://gw2.example"],
|
|
554
|
+
verifyPortableObject: createVerifier()
|
|
555
|
+
}), Note);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
test("getObjects() dereferences each portable IRI separately", async () => {
|
|
559
|
+
const activity = await Create.fromJsonLd({
|
|
560
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
561
|
+
type: "Create",
|
|
562
|
+
id: `ap://${did}/activities/1`,
|
|
563
|
+
object: [objectId, `ap://${did}/objects/2`]
|
|
564
|
+
}, { contextLoader: mockDocumentLoader });
|
|
565
|
+
const documentLoader = createLoader({
|
|
566
|
+
[`https://gw.example${gatewayPath}`]: note(),
|
|
567
|
+
[`https://gw.example/.well-known/apgateway/${did}/objects/2`]: note()
|
|
568
|
+
});
|
|
569
|
+
const objects = await Array.fromAsync(activity.getObjects({
|
|
570
|
+
documentLoader,
|
|
571
|
+
contextLoader: mockDocumentLoader,
|
|
572
|
+
gateways: ["https://gw.example"],
|
|
573
|
+
verifyPortableObject: createVerifier()
|
|
574
|
+
}));
|
|
575
|
+
deepStrictEqual(objects.length, 1);
|
|
576
|
+
deepStrictEqual(objects[0].id?.href, "ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
577
|
+
});
|
|
578
|
+
test("getObject() ignores gateway options for HTTP(S) IRIs", async () => {
|
|
579
|
+
deepStrictEqual((await new Create({ object: new URL("https://example.com/object") }).getObject({
|
|
580
|
+
documentLoader: mockDocumentLoader,
|
|
581
|
+
contextLoader: mockDocumentLoader,
|
|
582
|
+
gateways: ["https://gw.example"]
|
|
583
|
+
}))?.id, new URL("https://example.com/object"));
|
|
584
|
+
});
|
|
585
|
+
test("getObject() sends the ActivityStreams Accept header to gateways", {
|
|
586
|
+
sanitizeResources: false,
|
|
587
|
+
sanitizeOps: false
|
|
588
|
+
}, async () => {
|
|
589
|
+
esm_default.spyGlobal();
|
|
590
|
+
let accept = null;
|
|
591
|
+
esm_default.get(`https://example.com${gatewayPath}`, (callLog) => {
|
|
592
|
+
accept = callLog.request?.headers.get("Accept") ?? new Headers(callLog.options.headers).get("Accept");
|
|
593
|
+
return {
|
|
594
|
+
headers: { "Content-Type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" },
|
|
595
|
+
body: note()
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
try {
|
|
599
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
600
|
+
documentLoader: getDocumentLoader(),
|
|
601
|
+
contextLoader: mockDocumentLoader,
|
|
602
|
+
gateways: ["https://example.com"],
|
|
603
|
+
verifyPortableObject: createVerifier()
|
|
604
|
+
}), Note);
|
|
605
|
+
ok(accept != null && accept.includes("application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""), `Unexpected Accept header: ${accept}`);
|
|
606
|
+
} finally {
|
|
607
|
+
esm_default.hardReset();
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
const AS = "https://www.w3.org/ns/activitystreams#";
|
|
611
|
+
function gatewayUrl(gateway, path) {
|
|
612
|
+
return `${gateway}/.well-known/apgateway/${did}${path}`;
|
|
613
|
+
}
|
|
614
|
+
function createRecordingVerifier(decide = () => ({ verified: true })) {
|
|
615
|
+
const calls = [];
|
|
616
|
+
const verifier = async (document, options) => {
|
|
617
|
+
calls.push({
|
|
618
|
+
document,
|
|
619
|
+
options
|
|
620
|
+
});
|
|
621
|
+
return decide(document, options);
|
|
622
|
+
};
|
|
623
|
+
return Object.assign(verifier, { calls });
|
|
624
|
+
}
|
|
625
|
+
function portableCollection(path, extra = {}) {
|
|
626
|
+
return {
|
|
627
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
628
|
+
id: `ap://${did}${path}`,
|
|
629
|
+
type: "OrderedCollection",
|
|
630
|
+
totalItems: 0,
|
|
631
|
+
...extra
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
test("getOutbox() passes the fetch source and referrer to the verifier", async () => {
|
|
635
|
+
const person = await Person.fromJsonLd({
|
|
636
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
637
|
+
id: `ap://${did}/actor`,
|
|
638
|
+
type: "Person",
|
|
639
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
640
|
+
outbox: `ap://${did}/actor/outbox?@gateway=${encodeURIComponent("https://hint.example")}`
|
|
641
|
+
}, { contextLoader: mockDocumentLoader });
|
|
642
|
+
const documentLoader = createLoader({ [gatewayUrl("https://hint.example", "/actor/outbox")]: portableCollection("/actor/outbox") });
|
|
643
|
+
const verifier = createRecordingVerifier();
|
|
644
|
+
assertInstanceOf(await person.getOutbox({
|
|
645
|
+
documentLoader,
|
|
646
|
+
contextLoader: mockDocumentLoader,
|
|
647
|
+
verifyPortableObject: verifier
|
|
648
|
+
}), Collection);
|
|
649
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
650
|
+
const { options } = verifier.calls[0];
|
|
651
|
+
deepStrictEqual(options.documentUrl, new URL(gatewayUrl("https://hint.example", "/actor/outbox")));
|
|
652
|
+
deepStrictEqual(options.gateways, void 0);
|
|
653
|
+
deepStrictEqual(options.gatewayHints, [new URL("https://hint.example")]);
|
|
654
|
+
ok(options.referrer?.object === person);
|
|
655
|
+
deepStrictEqual(options.referrer?.property, `${AS}outbox`);
|
|
656
|
+
deepStrictEqual(options.referrer?.acceptance, void 0);
|
|
657
|
+
const person2 = await Person.fromJsonLd({
|
|
658
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
659
|
+
id: `ap://${did}/actor`,
|
|
660
|
+
type: "Person",
|
|
661
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
662
|
+
outbox: `ap://${did}/actor/outbox`
|
|
663
|
+
}, { contextLoader: mockDocumentLoader });
|
|
664
|
+
const verifier2 = createRecordingVerifier();
|
|
665
|
+
await person2.getOutbox({
|
|
666
|
+
documentLoader: createLoader({ [gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox") }),
|
|
667
|
+
contextLoader: mockDocumentLoader,
|
|
668
|
+
gateways: ["https://gw.example"],
|
|
669
|
+
verifyPortableObject: verifier2
|
|
670
|
+
});
|
|
671
|
+
deepStrictEqual(verifier2.calls[0].options.gateways, [new URL("https://gw.example")]);
|
|
672
|
+
deepStrictEqual(verifier2.calls[0].options.gatewayHints, void 0);
|
|
673
|
+
});
|
|
674
|
+
test("getFirst() passes the referrer chain and collection context", async () => {
|
|
675
|
+
const context = Object.freeze({});
|
|
676
|
+
const documentLoader = createLoader({
|
|
677
|
+
[gatewayUrl("https://gw.example", "/actor")]: {
|
|
678
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
679
|
+
id: `ap://${did}/actor`,
|
|
680
|
+
type: "Person",
|
|
681
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
682
|
+
outbox: `ap://${did}/actor/outbox`
|
|
683
|
+
},
|
|
684
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox", { first: {
|
|
685
|
+
id: `ap://${did}/actor/outbox?page=1`,
|
|
686
|
+
type: "OrderedCollectionPage",
|
|
687
|
+
next: `ap://${did}/actor/outbox?page=2`
|
|
688
|
+
} }),
|
|
689
|
+
[gatewayUrl("https://gw.example", "/actor/outbox?page=2")]: {
|
|
690
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
691
|
+
id: `ap://${did}/actor/outbox?page=2`,
|
|
692
|
+
type: "OrderedCollectionPage",
|
|
693
|
+
orderedItems: []
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
const verifier = createRecordingVerifier((document) => document.type === "OrderedCollection" ? {
|
|
697
|
+
verified: true,
|
|
698
|
+
collectionContext: context
|
|
699
|
+
} : { verified: true });
|
|
700
|
+
const options = {
|
|
701
|
+
documentLoader,
|
|
702
|
+
contextLoader: mockDocumentLoader,
|
|
703
|
+
gateways: ["https://gw.example"],
|
|
704
|
+
verifyPortableObject: verifier
|
|
705
|
+
};
|
|
706
|
+
const create = await createActivity(`ap://${did}/actor`);
|
|
707
|
+
const actor = await create.getObject(options);
|
|
708
|
+
assertInstanceOf(actor, Person);
|
|
709
|
+
const outbox = await actor.getOutbox(options);
|
|
710
|
+
assertInstanceOf(outbox, Collection);
|
|
711
|
+
const first = await outbox.getFirst(options);
|
|
712
|
+
assertInstanceOf(first, CollectionPage);
|
|
713
|
+
assertInstanceOf(await first.getNext(options), CollectionPage);
|
|
714
|
+
const { referrer } = verifier.calls[verifier.calls.length - 1].options;
|
|
715
|
+
ok(referrer?.object === first);
|
|
716
|
+
deepStrictEqual(referrer?.property, `${AS}next`);
|
|
717
|
+
deepStrictEqual(referrer?.acceptance, void 0);
|
|
718
|
+
ok(referrer?.referrer?.object === outbox);
|
|
719
|
+
deepStrictEqual(referrer?.referrer?.property, `${AS}first`);
|
|
720
|
+
deepStrictEqual(referrer?.referrer?.acceptance, "verified");
|
|
721
|
+
ok(referrer?.referrer?.collectionContext === context);
|
|
722
|
+
ok(referrer?.referrer?.referrer?.object === actor);
|
|
723
|
+
deepStrictEqual(referrer?.referrer?.referrer?.property, `${AS}outbox`);
|
|
724
|
+
deepStrictEqual(referrer?.referrer?.referrer?.acceptance, "verified");
|
|
725
|
+
ok(referrer?.referrer?.referrer?.referrer?.object === create);
|
|
726
|
+
});
|
|
727
|
+
test("accessors do not trust objects embedded in unsecured collections", async () => {
|
|
728
|
+
const itemPath = "/objects/1";
|
|
729
|
+
const options = {
|
|
730
|
+
documentLoader: createLoader({
|
|
731
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox", { orderedItems: [{
|
|
732
|
+
id: `ap://${did}${itemPath}`,
|
|
733
|
+
type: "Note",
|
|
734
|
+
content: "Forged"
|
|
735
|
+
}, {
|
|
736
|
+
type: "Note",
|
|
737
|
+
content: "Anonymous"
|
|
738
|
+
}] }),
|
|
739
|
+
[gatewayUrl("https://gw.example", itemPath)]: note()
|
|
740
|
+
}),
|
|
741
|
+
contextLoader: mockDocumentLoader,
|
|
742
|
+
gateways: ["https://gw.example"],
|
|
743
|
+
verifyPortableObject: createRecordingVerifier((document) => document.type === "OrderedCollection" ? {
|
|
744
|
+
verified: true,
|
|
745
|
+
unsecured: true
|
|
746
|
+
} : { verified: true })
|
|
747
|
+
};
|
|
748
|
+
const createPerson = () => Person.fromJsonLd({
|
|
749
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
750
|
+
id: `ap://${did}/actor`,
|
|
751
|
+
type: "Person",
|
|
752
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
753
|
+
outbox: `ap://${did}/actor/outbox`
|
|
754
|
+
}, { contextLoader: mockDocumentLoader });
|
|
755
|
+
const outbox = await (await createPerson()).getOutbox(options);
|
|
756
|
+
assertInstanceOf(outbox, Collection);
|
|
757
|
+
deepStrictEqual((await Array.fromAsync(outbox.getItems(options))).map((item) => item instanceof Note ? item.content : null), ["Portable note"]);
|
|
758
|
+
const outbox2 = await (await createPerson()).getOutbox(options);
|
|
759
|
+
assertInstanceOf(outbox2, Collection);
|
|
760
|
+
deepStrictEqual((await Array.fromAsync(outbox2.getItems({
|
|
761
|
+
...options,
|
|
762
|
+
crossOrigin: "trust"
|
|
763
|
+
}))).length, 1);
|
|
764
|
+
const outbox3 = await (await createPerson()).getOutbox(options);
|
|
765
|
+
assertInstanceOf(outbox3, Collection);
|
|
766
|
+
deepStrictEqual(await Array.fromAsync(outbox3.clone().getItems({
|
|
767
|
+
...options,
|
|
768
|
+
documentLoader: createLoader({}),
|
|
769
|
+
suppressError: true
|
|
770
|
+
})), []);
|
|
771
|
+
});
|
|
772
|
+
function createRedirectingLoader(responses) {
|
|
773
|
+
const fetched = [];
|
|
774
|
+
const loader = async (url) => {
|
|
775
|
+
fetched.push(url);
|
|
776
|
+
const response = responses[url];
|
|
777
|
+
if (response == null) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
|
|
778
|
+
return {
|
|
779
|
+
contextUrl: null,
|
|
780
|
+
...response
|
|
781
|
+
};
|
|
782
|
+
};
|
|
783
|
+
return Object.assign(loader, { fetched });
|
|
784
|
+
}
|
|
785
|
+
function createHttpActivity(object) {
|
|
786
|
+
return Create.fromJsonLd({
|
|
787
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
788
|
+
type: "Create",
|
|
789
|
+
id: "https://example.com/activities/1",
|
|
790
|
+
object
|
|
791
|
+
}, { contextLoader: mockDocumentLoader });
|
|
792
|
+
}
|
|
793
|
+
const compatibleNoteId = gatewayUrl("https://gw.example", "/objects/1");
|
|
794
|
+
test("accessors dereference compatible identifiers through their gateways", async (t) => {
|
|
795
|
+
await t.step("through the named gateway", async () => {
|
|
796
|
+
const documentLoader = createLoader({ [compatibleNoteId]: note() });
|
|
797
|
+
const verifier = createRecordingVerifier();
|
|
798
|
+
const activity = await createHttpActivity(compatibleNoteId);
|
|
799
|
+
const object = await activity.getObject({
|
|
800
|
+
documentLoader,
|
|
801
|
+
contextLoader: mockDocumentLoader,
|
|
802
|
+
verifyPortableObject: verifier
|
|
803
|
+
});
|
|
804
|
+
assertInstanceOf(object, Note);
|
|
805
|
+
deepStrictEqual(object.id, parseIri(objectId));
|
|
806
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
|
|
807
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
808
|
+
const { options } = verifier.calls[0];
|
|
809
|
+
deepStrictEqual(options.documentUrl, new URL(compatibleNoteId));
|
|
810
|
+
deepStrictEqual(options.gateways, void 0);
|
|
811
|
+
deepStrictEqual(options.gatewayHints, [new URL("https://gw.example")]);
|
|
812
|
+
ok(options.referrer?.object === activity);
|
|
813
|
+
ok(await activity.getObject({ documentLoader: createLoader({}) }) === object);
|
|
814
|
+
});
|
|
815
|
+
await t.step("then through explicit gateways", async () => {
|
|
816
|
+
const documentLoader = createLoader({ [gatewayUrl("https://gw2.example", "/objects/1")]: note() });
|
|
817
|
+
const verifier = createRecordingVerifier();
|
|
818
|
+
assertInstanceOf(await (await createHttpActivity(compatibleNoteId)).getObject({
|
|
819
|
+
documentLoader,
|
|
820
|
+
contextLoader: mockDocumentLoader,
|
|
821
|
+
gateways: ["https://gw2.example", "https://gw.example"],
|
|
822
|
+
verifyPortableObject: verifier
|
|
823
|
+
}), Note);
|
|
824
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId, gatewayUrl("https://gw2.example", "/objects/1")]);
|
|
825
|
+
deepStrictEqual(verifier.calls[0].options.gateways, [new URL("https://gw2.example"), new URL("https://gw.example")]);
|
|
826
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, void 0);
|
|
827
|
+
const documentLoader2 = createLoader({ [compatibleNoteId]: note() });
|
|
828
|
+
assertInstanceOf(await (await createHttpActivity(compatibleNoteId)).getObject({
|
|
829
|
+
documentLoader: documentLoader2,
|
|
830
|
+
contextLoader: mockDocumentLoader,
|
|
831
|
+
gateways: [],
|
|
832
|
+
verifyPortableObject: createVerifier()
|
|
833
|
+
}), Note);
|
|
834
|
+
deepStrictEqual(documentLoader2.fetched, [compatibleNoteId]);
|
|
835
|
+
});
|
|
836
|
+
await t.step("in plural accessors", async () => {
|
|
837
|
+
const documentLoader = createLoader({ [compatibleNoteId]: note() });
|
|
838
|
+
deepStrictEqual((await Array.fromAsync((await createHttpActivity([compatibleNoteId])).getObjects({
|
|
839
|
+
documentLoader,
|
|
840
|
+
contextLoader: mockDocumentLoader,
|
|
841
|
+
verifyPortableObject: createVerifier()
|
|
842
|
+
}))).map((o) => o.id), [parseIri(objectId)]);
|
|
843
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
test("accessors reject forged objects at compatible identifiers", async () => {
|
|
847
|
+
const forgedId = gatewayUrl("https://evil.example", "/objects/1");
|
|
848
|
+
const forged = {
|
|
849
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
850
|
+
id: forgedId,
|
|
851
|
+
type: "Note",
|
|
852
|
+
attributedTo: gatewayUrl("https://evil.example", "/actor"),
|
|
853
|
+
content: "Forged"
|
|
854
|
+
};
|
|
855
|
+
for (const crossOrigin of [
|
|
856
|
+
void 0,
|
|
857
|
+
"ignore",
|
|
858
|
+
"trust"
|
|
859
|
+
]) {
|
|
860
|
+
const verifier = createRecordingVerifier(() => ({ verified: false }));
|
|
861
|
+
deepStrictEqual(await (await createHttpActivity(forgedId)).getObject({
|
|
862
|
+
documentLoader: createLoader({ [forgedId]: forged }),
|
|
863
|
+
contextLoader: mockDocumentLoader,
|
|
864
|
+
verifyPortableObject: verifier,
|
|
865
|
+
crossOrigin
|
|
866
|
+
}), null);
|
|
867
|
+
deepStrictEqual(verifier.calls.map((c) => c.document), [forged]);
|
|
868
|
+
deepStrictEqual(await (await createHttpActivity(forgedId)).getObject({
|
|
869
|
+
documentLoader: createLoader({ [forgedId]: note() }),
|
|
870
|
+
contextLoader: mockDocumentLoader,
|
|
871
|
+
verifyPortableObject: createVerifier(false),
|
|
872
|
+
crossOrigin
|
|
873
|
+
}), null);
|
|
874
|
+
}
|
|
875
|
+
await rejects(async () => await (await createHttpActivity(forgedId)).getObject({
|
|
876
|
+
documentLoader: createLoader({ [forgedId]: note() }),
|
|
877
|
+
contextLoader: mockDocumentLoader,
|
|
878
|
+
verifyPortableObject: createVerifier(false),
|
|
879
|
+
crossOrigin: "throw"
|
|
880
|
+
}), /No gateway returned a valid portable object/);
|
|
881
|
+
});
|
|
882
|
+
test("accessors reject malformed compatible identifiers", async () => {
|
|
883
|
+
const malformed = [`${compatibleNoteId}?@gateway=https%3A%2F%2Fgw2.example`, compatibleNoteId.replace("https://", "https://user:pass@")];
|
|
884
|
+
for (const url of malformed) {
|
|
885
|
+
const documentLoader = createLoader({ [url]: note() });
|
|
886
|
+
const verifier = createRecordingVerifier();
|
|
887
|
+
const activity = await createHttpActivity(url);
|
|
888
|
+
deepStrictEqual(await activity.getObject({
|
|
889
|
+
documentLoader,
|
|
890
|
+
contextLoader: mockDocumentLoader,
|
|
891
|
+
verifyPortableObject: verifier
|
|
892
|
+
}), null);
|
|
893
|
+
deepStrictEqual(await activity.getObject({
|
|
894
|
+
documentLoader,
|
|
895
|
+
contextLoader: mockDocumentLoader,
|
|
896
|
+
verifyPortableObject: verifier,
|
|
897
|
+
crossOrigin: "throw",
|
|
898
|
+
suppressError: true
|
|
899
|
+
}), null);
|
|
900
|
+
await rejects(async () => await activity.getObject({
|
|
901
|
+
documentLoader,
|
|
902
|
+
contextLoader: mockDocumentLoader,
|
|
903
|
+
verifyPortableObject: verifier,
|
|
904
|
+
crossOrigin: "throw"
|
|
905
|
+
}), /malformed FEP-ef61 compatible identifier/);
|
|
906
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
907
|
+
deepStrictEqual(verifier.calls, []);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
test("accessors keep fetching compatible identifiers as HTTP(S) URLs without verifyPortableObject", async (t) => {
|
|
911
|
+
const document = {
|
|
912
|
+
...note(),
|
|
913
|
+
id: compatibleNoteId
|
|
914
|
+
};
|
|
915
|
+
await t.step("singular accessors", async () => {
|
|
916
|
+
const documentLoader = createLoader({ [compatibleNoteId]: document });
|
|
917
|
+
const activity = await createHttpActivity(compatibleNoteId);
|
|
918
|
+
const object = await activity.getObject({
|
|
919
|
+
documentLoader,
|
|
920
|
+
contextLoader: mockDocumentLoader
|
|
921
|
+
});
|
|
922
|
+
assertInstanceOf(object, Note);
|
|
923
|
+
deepStrictEqual(object.id, new URL(compatibleNoteId));
|
|
924
|
+
deepStrictEqual(await activity.getObject({
|
|
925
|
+
documentLoader,
|
|
926
|
+
contextLoader: mockDocumentLoader,
|
|
927
|
+
verifyPortableObject: createVerifier(false)
|
|
928
|
+
}), null);
|
|
929
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId, compatibleNoteId]);
|
|
930
|
+
});
|
|
931
|
+
await t.step("plural accessors", async () => {
|
|
932
|
+
const documentLoader = createLoader({ [compatibleNoteId]: document });
|
|
933
|
+
const activity = await createHttpActivity([compatibleNoteId]);
|
|
934
|
+
deepStrictEqual((await Array.fromAsync(activity.getObjects({
|
|
935
|
+
documentLoader,
|
|
936
|
+
contextLoader: mockDocumentLoader
|
|
937
|
+
}))).map((o) => o.id), [new URL(compatibleNoteId)]);
|
|
938
|
+
deepStrictEqual(await Array.fromAsync(activity.getObjects({
|
|
939
|
+
documentLoader,
|
|
940
|
+
contextLoader: mockDocumentLoader,
|
|
941
|
+
verifyPortableObject: createVerifier(false)
|
|
942
|
+
})), []);
|
|
943
|
+
});
|
|
944
|
+
await t.step("redirects to ordinary objects", async () => {
|
|
945
|
+
const plainUrl = "https://gw.example/notes/1";
|
|
946
|
+
const documentLoader = createRedirectingLoader({ [compatibleNoteId]: {
|
|
947
|
+
documentUrl: plainUrl,
|
|
948
|
+
document: {
|
|
949
|
+
...note(),
|
|
950
|
+
id: plainUrl
|
|
951
|
+
}
|
|
952
|
+
} });
|
|
953
|
+
for (const activity of [await createHttpActivity(compatibleNoteId)]) {
|
|
954
|
+
deepStrictEqual((await activity.getObject({
|
|
955
|
+
documentLoader,
|
|
956
|
+
contextLoader: mockDocumentLoader
|
|
957
|
+
}))?.id, new URL(plainUrl));
|
|
958
|
+
deepStrictEqual(await activity.getObject({
|
|
959
|
+
documentLoader,
|
|
960
|
+
contextLoader: mockDocumentLoader,
|
|
961
|
+
verifyPortableObject: createVerifier()
|
|
962
|
+
}), null);
|
|
963
|
+
}
|
|
964
|
+
const activity = await createHttpActivity([compatibleNoteId]);
|
|
965
|
+
deepStrictEqual((await Array.fromAsync(activity.getObjects({
|
|
966
|
+
documentLoader,
|
|
967
|
+
contextLoader: mockDocumentLoader
|
|
968
|
+
}))).length, 1);
|
|
969
|
+
deepStrictEqual(await Array.fromAsync(activity.getObjects({
|
|
970
|
+
documentLoader,
|
|
971
|
+
contextLoader: mockDocumentLoader,
|
|
972
|
+
verifyPortableObject: createVerifier()
|
|
973
|
+
})), []);
|
|
974
|
+
});
|
|
975
|
+
await t.step("embedded objects", async () => {
|
|
976
|
+
deepStrictEqual((await (await createHttpActivity({
|
|
977
|
+
type: "Note",
|
|
978
|
+
id: gatewayUrl("https://example.com", "/objects/1"),
|
|
979
|
+
content: "Embedded"
|
|
980
|
+
})).getObject({
|
|
981
|
+
documentLoader: createLoader({}),
|
|
982
|
+
contextLoader: mockDocumentLoader
|
|
983
|
+
}))?.id, new URL(gatewayUrl("https://example.com", "/objects/1")));
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
test("accessors dereference compatible identifiers from portable objects", async () => {
|
|
987
|
+
const compatibleOutbox = gatewayUrl("https://gw.example", "/actor/outbox");
|
|
988
|
+
const person = (outbox) => Person.fromJsonLd({
|
|
989
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
990
|
+
id: `ap://${did}/actor`,
|
|
991
|
+
type: "Person",
|
|
992
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
993
|
+
outbox
|
|
994
|
+
}, { contextLoader: mockDocumentLoader });
|
|
995
|
+
const documentLoader = createRedirectingLoader({
|
|
996
|
+
[compatibleOutbox]: {
|
|
997
|
+
documentUrl: compatibleOutbox,
|
|
998
|
+
document: portableCollection("/actor/outbox")
|
|
999
|
+
},
|
|
1000
|
+
"https://example.com/outbox": {
|
|
1001
|
+
documentUrl: compatibleOutbox,
|
|
1002
|
+
document: portableCollection("/actor/outbox")
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
for (const crossOrigin of [void 0, "trust"]) for (const outbox of [
|
|
1006
|
+
compatibleOutbox,
|
|
1007
|
+
"https://example.com/outbox",
|
|
1008
|
+
{
|
|
1009
|
+
...portableCollection("/actor/outbox"),
|
|
1010
|
+
id: compatibleOutbox
|
|
1011
|
+
}
|
|
1012
|
+
]) {
|
|
1013
|
+
const verifier = createRecordingVerifier();
|
|
1014
|
+
const collection = await (await person(outbox)).getOutbox({
|
|
1015
|
+
documentLoader,
|
|
1016
|
+
contextLoader: mockDocumentLoader,
|
|
1017
|
+
verifyPortableObject: verifier,
|
|
1018
|
+
crossOrigin
|
|
1019
|
+
});
|
|
1020
|
+
assertInstanceOf(collection, OrderedCollection);
|
|
1021
|
+
deepStrictEqual(collection.id, parseIri(`ap://${did}/actor/outbox`));
|
|
1022
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1023
|
+
deepStrictEqual(verifier.calls[0].options.documentUrl, new URL(compatibleOutbox));
|
|
1024
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, [new URL("https://gw.example")]);
|
|
1025
|
+
}
|
|
1026
|
+
await rejects(async () => await (await person(compatibleOutbox)).getOutbox({
|
|
1027
|
+
documentLoader,
|
|
1028
|
+
contextLoader: mockDocumentLoader
|
|
1029
|
+
}), TypeError);
|
|
1030
|
+
deepStrictEqual(await (await person(compatibleOutbox)).getOutbox({
|
|
1031
|
+
documentLoader,
|
|
1032
|
+
contextLoader: mockDocumentLoader,
|
|
1033
|
+
suppressError: true
|
|
1034
|
+
}), null);
|
|
1035
|
+
});
|
|
1036
|
+
test("accessors verify fetched documents that stand for portable objects", async (t) => {
|
|
1037
|
+
const plainUrl = "https://example.com/notes/1";
|
|
1038
|
+
await t.step("a redirect to a compatible identifier", async () => {
|
|
1039
|
+
const documentLoader = createRedirectingLoader({ [plainUrl]: {
|
|
1040
|
+
documentUrl: compatibleNoteId,
|
|
1041
|
+
document: note()
|
|
1042
|
+
} });
|
|
1043
|
+
const verifier = createRecordingVerifier();
|
|
1044
|
+
const object = await (await createHttpActivity(plainUrl)).getObject({
|
|
1045
|
+
documentLoader,
|
|
1046
|
+
contextLoader: mockDocumentLoader,
|
|
1047
|
+
verifyPortableObject: verifier
|
|
1048
|
+
});
|
|
1049
|
+
assertInstanceOf(object, Note);
|
|
1050
|
+
deepStrictEqual(object.id, parseIri(objectId));
|
|
1051
|
+
deepStrictEqual(documentLoader.fetched, [plainUrl]);
|
|
1052
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1053
|
+
deepStrictEqual(verifier.calls[0].options.documentUrl, new URL(compatibleNoteId));
|
|
1054
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, [new URL("https://gw.example")]);
|
|
1055
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1056
|
+
documentLoader,
|
|
1057
|
+
contextLoader: mockDocumentLoader,
|
|
1058
|
+
verifyPortableObject: createVerifier(false)
|
|
1059
|
+
}), null);
|
|
1060
|
+
await rejects(async () => await (await createHttpActivity(plainUrl)).getObject({
|
|
1061
|
+
documentLoader,
|
|
1062
|
+
contextLoader: mockDocumentLoader,
|
|
1063
|
+
verifyPortableObject: createVerifier(false),
|
|
1064
|
+
crossOrigin: "throw"
|
|
1065
|
+
}), /No gateway returned a valid portable object/);
|
|
1066
|
+
});
|
|
1067
|
+
await t.step("a document that claims another object", async () => {
|
|
1068
|
+
for (const id of [
|
|
1069
|
+
`ap://${did}/objects/2`,
|
|
1070
|
+
gatewayUrl("https://gw.example", "/objects/2"),
|
|
1071
|
+
"https://gw.example/notes/1"
|
|
1072
|
+
]) {
|
|
1073
|
+
const verifier = createRecordingVerifier();
|
|
1074
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1075
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1076
|
+
documentUrl: compatibleNoteId,
|
|
1077
|
+
document: note(id)
|
|
1078
|
+
} }),
|
|
1079
|
+
contextLoader: mockDocumentLoader,
|
|
1080
|
+
verifyPortableObject: verifier
|
|
1081
|
+
}), null);
|
|
1082
|
+
deepStrictEqual(verifier.calls, []);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
await t.step("a portable @id", async () => {
|
|
1086
|
+
const verifier = createRecordingVerifier();
|
|
1087
|
+
assertInstanceOf(await (await createHttpActivity(plainUrl)).getObject({
|
|
1088
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1089
|
+
contextLoader: mockDocumentLoader,
|
|
1090
|
+
verifyPortableObject: verifier,
|
|
1091
|
+
crossOrigin: "trust"
|
|
1092
|
+
}), Note);
|
|
1093
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1094
|
+
deepStrictEqual(verifier.calls[0].options.documentUrl, new URL(plainUrl));
|
|
1095
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1096
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1097
|
+
contextLoader: mockDocumentLoader,
|
|
1098
|
+
verifyPortableObject: createVerifier(false),
|
|
1099
|
+
crossOrigin: "trust"
|
|
1100
|
+
}), null);
|
|
1101
|
+
});
|
|
1102
|
+
await t.step("a compatible @id", async () => {
|
|
1103
|
+
const verifier = createRecordingVerifier();
|
|
1104
|
+
const url = "https://gw.example/notes/1";
|
|
1105
|
+
const documentLoader = createLoader({ [url]: note(compatibleNoteId) });
|
|
1106
|
+
const object = await (await createHttpActivity(url)).getObject({
|
|
1107
|
+
documentLoader,
|
|
1108
|
+
contextLoader: mockDocumentLoader,
|
|
1109
|
+
verifyPortableObject: verifier
|
|
1110
|
+
});
|
|
1111
|
+
assertInstanceOf(object, Note);
|
|
1112
|
+
deepStrictEqual(object.id, new URL(compatibleNoteId));
|
|
1113
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1114
|
+
deepStrictEqual(await (await createHttpActivity(url)).getObject({
|
|
1115
|
+
documentLoader,
|
|
1116
|
+
contextLoader: mockDocumentLoader,
|
|
1117
|
+
verifyPortableObject: createVerifier(false)
|
|
1118
|
+
}), null);
|
|
1119
|
+
});
|
|
1120
|
+
await t.step("a portable final URL", async () => {
|
|
1121
|
+
for (const [id, expected] of [[objectId, true], [`ap://${did}/objects/2`, false]]) deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1122
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1123
|
+
documentUrl: `ap://${did}/objects/1`,
|
|
1124
|
+
document: note(id)
|
|
1125
|
+
} }),
|
|
1126
|
+
contextLoader: mockDocumentLoader,
|
|
1127
|
+
verifyPortableObject: createVerifier()
|
|
1128
|
+
}) instanceof Note, expected);
|
|
1129
|
+
await rejects(() => createHttpActivity(plainUrl).then((activity) => activity.getObject({
|
|
1130
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1131
|
+
documentUrl: `ap://${did}/objects/x/../1`,
|
|
1132
|
+
document: note()
|
|
1133
|
+
} }),
|
|
1134
|
+
contextLoader: mockDocumentLoader,
|
|
1135
|
+
verifyPortableObject: createVerifier()
|
|
1136
|
+
})), TypeError);
|
|
1137
|
+
});
|
|
1138
|
+
await t.step("a malformed compatible final URL", async () => {
|
|
1139
|
+
const malformed = `${compatibleNoteId}?@gateway=https%3A%2F%2Fgw2.example`;
|
|
1140
|
+
const documentLoader = createRedirectingLoader({ [plainUrl]: {
|
|
1141
|
+
documentUrl: malformed,
|
|
1142
|
+
document: note()
|
|
1143
|
+
} });
|
|
1144
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1145
|
+
documentLoader,
|
|
1146
|
+
contextLoader: mockDocumentLoader,
|
|
1147
|
+
verifyPortableObject: createVerifier()
|
|
1148
|
+
}), null);
|
|
1149
|
+
await rejects(async () => await (await createHttpActivity(plainUrl)).getObject({
|
|
1150
|
+
documentLoader,
|
|
1151
|
+
contextLoader: mockDocumentLoader,
|
|
1152
|
+
verifyPortableObject: createVerifier(),
|
|
1153
|
+
crossOrigin: "throw"
|
|
1154
|
+
}), /malformed FEP-ef61 compatible identifier/);
|
|
1155
|
+
});
|
|
1156
|
+
await t.step("context documents are shared", async () => {
|
|
1157
|
+
const contextUrl = "https://example.com/portable-context";
|
|
1158
|
+
let version = 0;
|
|
1159
|
+
const contextLoader = async (url) => {
|
|
1160
|
+
if (url === contextUrl) {
|
|
1161
|
+
version++;
|
|
1162
|
+
return {
|
|
1163
|
+
contextUrl: null,
|
|
1164
|
+
documentUrl: url,
|
|
1165
|
+
document: { "@context": { version: `urn:version:${version}` } }
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
return await mockDocumentLoader(url);
|
|
1169
|
+
};
|
|
1170
|
+
const contexts = [];
|
|
1171
|
+
assertInstanceOf(await (await createHttpActivity(plainUrl)).getObject({
|
|
1172
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1173
|
+
documentUrl: compatibleNoteId,
|
|
1174
|
+
document: {
|
|
1175
|
+
...note(),
|
|
1176
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl]
|
|
1177
|
+
}
|
|
1178
|
+
} }),
|
|
1179
|
+
contextLoader,
|
|
1180
|
+
verifyPortableObject: async (_, { contextLoader }) => {
|
|
1181
|
+
contexts.push((await contextLoader(contextUrl)).document);
|
|
1182
|
+
return { verified: true };
|
|
1183
|
+
}
|
|
1184
|
+
}), Note);
|
|
1185
|
+
deepStrictEqual(version, 1);
|
|
1186
|
+
deepStrictEqual(contexts, [{ "@context": { version: "urn:version:1" } }]);
|
|
1187
|
+
});
|
|
1188
|
+
await t.step("without verifyPortableObject", async () => {
|
|
1189
|
+
deepStrictEqual(await (await createHttpActivity(plainUrl)).getObject({
|
|
1190
|
+
documentLoader: createLoader({ [plainUrl]: note() }),
|
|
1191
|
+
contextLoader: mockDocumentLoader
|
|
1192
|
+
}), null);
|
|
1193
|
+
assertInstanceOf(await (await createHttpActivity(plainUrl)).getObject({
|
|
1194
|
+
documentLoader: createRedirectingLoader({ [plainUrl]: {
|
|
1195
|
+
documentUrl: gatewayUrl("https://example.com", "/objects/1"),
|
|
1196
|
+
document: note(gatewayUrl("https://example.com", "/objects/1"))
|
|
1197
|
+
} }),
|
|
1198
|
+
contextLoader: mockDocumentLoader
|
|
1199
|
+
}), Note);
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
test("accessors verify embedded objects that stand for portable objects", async () => {
|
|
1203
|
+
const forgedId = gatewayUrl("https://example.com", "/objects/1");
|
|
1204
|
+
const forged = {
|
|
1205
|
+
type: "Note",
|
|
1206
|
+
id: forgedId,
|
|
1207
|
+
content: "Forged"
|
|
1208
|
+
};
|
|
1209
|
+
for (const crossOrigin of [void 0, "trust"]) {
|
|
1210
|
+
const documentLoader = createLoader({ [forgedId]: note() });
|
|
1211
|
+
const verifier = createRecordingVerifier(() => ({ verified: false }));
|
|
1212
|
+
deepStrictEqual(await (await createHttpActivity(forged)).getObject({
|
|
1213
|
+
documentLoader,
|
|
1214
|
+
contextLoader: mockDocumentLoader,
|
|
1215
|
+
verifyPortableObject: verifier,
|
|
1216
|
+
crossOrigin
|
|
1217
|
+
}), null);
|
|
1218
|
+
deepStrictEqual(documentLoader.fetched, [forgedId]);
|
|
1219
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1220
|
+
deepStrictEqual(await Array.fromAsync((await createHttpActivity([forged])).getObjects({
|
|
1221
|
+
documentLoader,
|
|
1222
|
+
contextLoader: mockDocumentLoader,
|
|
1223
|
+
verifyPortableObject: verifier,
|
|
1224
|
+
crossOrigin
|
|
1225
|
+
})), []);
|
|
1226
|
+
const documentLoader2 = createLoader({ [gatewayUrl("https://gw.example", "/objects/1")]: note() });
|
|
1227
|
+
const verifier2 = createRecordingVerifier();
|
|
1228
|
+
const object = await (await createHttpActivity({
|
|
1229
|
+
type: "Note",
|
|
1230
|
+
id: objectId,
|
|
1231
|
+
content: "Forged"
|
|
1232
|
+
})).getObject({
|
|
1233
|
+
documentLoader: documentLoader2,
|
|
1234
|
+
contextLoader: mockDocumentLoader,
|
|
1235
|
+
gateways: ["https://gw.example"],
|
|
1236
|
+
verifyPortableObject: verifier2,
|
|
1237
|
+
crossOrigin
|
|
1238
|
+
});
|
|
1239
|
+
assertInstanceOf(object, Note);
|
|
1240
|
+
deepStrictEqual(object.content, "Portable note");
|
|
1241
|
+
deepStrictEqual(verifier2.calls.length, 1);
|
|
1242
|
+
}
|
|
1243
|
+
const otherDid = "did:key:z6Mkother";
|
|
1244
|
+
const otherId = `ap://${otherDid}/objects/1`;
|
|
1245
|
+
const documentLoader = createLoader({ [`https://gw.example/.well-known/apgateway/${otherDid}/objects/1`]: note(otherId) });
|
|
1246
|
+
const verifier = createRecordingVerifier();
|
|
1247
|
+
const crossDid = await (await createActivity({
|
|
1248
|
+
type: "Note",
|
|
1249
|
+
id: otherId,
|
|
1250
|
+
content: "Forged"
|
|
1251
|
+
})).getObject({
|
|
1252
|
+
documentLoader,
|
|
1253
|
+
contextLoader: mockDocumentLoader,
|
|
1254
|
+
gateways: ["https://gw.example"],
|
|
1255
|
+
verifyPortableObject: verifier,
|
|
1256
|
+
crossOrigin: "trust"
|
|
1257
|
+
});
|
|
1258
|
+
assertInstanceOf(crossDid, Note);
|
|
1259
|
+
deepStrictEqual(crossDid.content, "Portable note");
|
|
1260
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1261
|
+
const object = await (await createActivity({
|
|
1262
|
+
type: "Note",
|
|
1263
|
+
id: objectId,
|
|
1264
|
+
content: "Embedded"
|
|
1265
|
+
})).getObject({
|
|
1266
|
+
documentLoader: createLoader({}),
|
|
1267
|
+
contextLoader: mockDocumentLoader,
|
|
1268
|
+
verifyPortableObject: createVerifier(false)
|
|
1269
|
+
});
|
|
1270
|
+
assertInstanceOf(object, Note);
|
|
1271
|
+
deepStrictEqual(object.content, "Embedded");
|
|
1272
|
+
});
|
|
1273
|
+
test("singular accessors drop anonymous objects embedded in unsecured collections", async () => {
|
|
1274
|
+
const options = {
|
|
1275
|
+
documentLoader: createLoader({ [gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection("/actor/outbox", { first: {
|
|
1276
|
+
type: "OrderedCollectionPage",
|
|
1277
|
+
orderedItems: [{
|
|
1278
|
+
type: "Note",
|
|
1279
|
+
content: "Anonymous"
|
|
1280
|
+
}]
|
|
1281
|
+
} }) }),
|
|
1282
|
+
contextLoader: mockDocumentLoader,
|
|
1283
|
+
gateways: ["https://gw.example"],
|
|
1284
|
+
verifyPortableObject: createRecordingVerifier(() => ({
|
|
1285
|
+
verified: true,
|
|
1286
|
+
unsecured: true
|
|
1287
|
+
}))
|
|
1288
|
+
};
|
|
1289
|
+
const outbox = await (await Person.fromJsonLd({
|
|
1290
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1291
|
+
id: `ap://${did}/actor`,
|
|
1292
|
+
type: "Person",
|
|
1293
|
+
inbox: `ap://${did}/actor/inbox`,
|
|
1294
|
+
outbox: `ap://${did}/actor/outbox`
|
|
1295
|
+
}, { contextLoader: mockDocumentLoader })).getOutbox(options);
|
|
1296
|
+
assertInstanceOf(outbox, OrderedCollection);
|
|
1297
|
+
deepStrictEqual(await outbox.getFirst(options), null);
|
|
1298
|
+
deepStrictEqual(await Array.fromAsync(traverseCollection(outbox, options)), []);
|
|
1299
|
+
});
|
|
1300
|
+
function createCompatibleActivity(object, id = gatewayUrl("https://gw.example", "/activities/1"), options = {}) {
|
|
1301
|
+
return Create.fromJsonLd({
|
|
1302
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1303
|
+
type: "Create",
|
|
1304
|
+
id,
|
|
1305
|
+
object
|
|
1306
|
+
}, {
|
|
1307
|
+
contextLoader: mockDocumentLoader,
|
|
1308
|
+
...options
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
test("accessors of objects with compatible identifiers dereference references as portable objects", async (t) => {
|
|
1312
|
+
await t.step("verifying the object at the reference", async () => {
|
|
1313
|
+
const documentLoader = createLoader({ [compatibleNoteId]: {
|
|
1314
|
+
...note(),
|
|
1315
|
+
id: compatibleNoteId
|
|
1316
|
+
} });
|
|
1317
|
+
const verifier = createRecordingVerifier();
|
|
1318
|
+
const activity = await createCompatibleActivity(compatibleNoteId);
|
|
1319
|
+
const object = await activity.getObject({
|
|
1320
|
+
documentLoader,
|
|
1321
|
+
contextLoader: mockDocumentLoader,
|
|
1322
|
+
verifyPortableObject: verifier
|
|
1323
|
+
});
|
|
1324
|
+
assertInstanceOf(object, Note);
|
|
1325
|
+
deepStrictEqual(object.id, new URL(compatibleNoteId));
|
|
1326
|
+
deepStrictEqual(documentLoader.fetched, [compatibleNoteId]);
|
|
1327
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1328
|
+
deepStrictEqual(verifier.calls[0].options.gatewayHints, [new URL("https://gw.example")]);
|
|
1329
|
+
ok(verifier.calls[0].options.referrer?.object === activity);
|
|
1330
|
+
deepStrictEqual(verifier.calls[0].options.referrer?.acceptance, void 0);
|
|
1331
|
+
});
|
|
1332
|
+
await t.step("rejecting an unsigned object at the reference", async () => {
|
|
1333
|
+
const documentLoader = createLoader({ [compatibleNoteId]: {
|
|
1334
|
+
...note(),
|
|
1335
|
+
id: compatibleNoteId
|
|
1336
|
+
} });
|
|
1337
|
+
deepStrictEqual(await (await createCompatibleActivity(compatibleNoteId)).getObject({
|
|
1338
|
+
documentLoader,
|
|
1339
|
+
contextLoader: mockDocumentLoader,
|
|
1340
|
+
verifyPortableObject: createVerifier(false)
|
|
1341
|
+
}), null);
|
|
1342
|
+
deepStrictEqual(await Array.fromAsync((await createCompatibleActivity([compatibleNoteId])).getObjects({
|
|
1343
|
+
documentLoader,
|
|
1344
|
+
contextLoader: mockDocumentLoader,
|
|
1345
|
+
verifyPortableObject: createVerifier(false),
|
|
1346
|
+
crossOrigin: "trust"
|
|
1347
|
+
})), []);
|
|
1348
|
+
});
|
|
1349
|
+
await t.step("without verifyPortableObject", async () => {
|
|
1350
|
+
const documentLoader = createLoader({ [compatibleNoteId]: {
|
|
1351
|
+
...note(),
|
|
1352
|
+
id: compatibleNoteId
|
|
1353
|
+
} });
|
|
1354
|
+
const activity = await createCompatibleActivity(compatibleNoteId);
|
|
1355
|
+
await rejects(async () => await activity.getObject({
|
|
1356
|
+
documentLoader,
|
|
1357
|
+
contextLoader: mockDocumentLoader
|
|
1358
|
+
}), TypeError);
|
|
1359
|
+
deepStrictEqual(await activity.getObject({
|
|
1360
|
+
documentLoader,
|
|
1361
|
+
contextLoader: mockDocumentLoader,
|
|
1362
|
+
suppressError: true
|
|
1363
|
+
}), null);
|
|
1364
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
1365
|
+
});
|
|
1366
|
+
await t.step("embedded objects with compatible identifiers", async () => {
|
|
1367
|
+
const documentLoader = createLoader({ [compatibleNoteId]: note() });
|
|
1368
|
+
const verifier = createRecordingVerifier();
|
|
1369
|
+
const object = await (await createCompatibleActivity({
|
|
1370
|
+
type: "Note",
|
|
1371
|
+
id: compatibleNoteId,
|
|
1372
|
+
content: "Embedded"
|
|
1373
|
+
})).getObject({
|
|
1374
|
+
documentLoader,
|
|
1375
|
+
contextLoader: mockDocumentLoader,
|
|
1376
|
+
verifyPortableObject: verifier
|
|
1377
|
+
});
|
|
1378
|
+
assertInstanceOf(object, Note);
|
|
1379
|
+
deepStrictEqual(object.content, "Portable note");
|
|
1380
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1381
|
+
});
|
|
1382
|
+
await t.step("objects with malformed compatible identifiers", async () => {
|
|
1383
|
+
const malformedId = `${gatewayUrl("https://gw.example", "/activities/1")}?@gateway=https%3A%2F%2Fgw2.example`;
|
|
1384
|
+
const activity = await createCompatibleActivity(objectId, malformedId);
|
|
1385
|
+
await rejects(async () => await activity.getObject({
|
|
1386
|
+
documentLoader: createLoader({}),
|
|
1387
|
+
contextLoader: mockDocumentLoader
|
|
1388
|
+
}), TypeError);
|
|
1389
|
+
const plainUrl = "https://example.com/notes/1";
|
|
1390
|
+
const plainLoader = createLoader({ [plainUrl]: {
|
|
1391
|
+
...note(),
|
|
1392
|
+
id: plainUrl
|
|
1393
|
+
} });
|
|
1394
|
+
deepStrictEqual((await (await createCompatibleActivity(plainUrl, malformedId)).getObject({
|
|
1395
|
+
documentLoader: plainLoader,
|
|
1396
|
+
contextLoader: mockDocumentLoader
|
|
1397
|
+
}))?.id, new URL(plainUrl));
|
|
1398
|
+
const claimLoader = createLoader({ [plainUrl]: note() });
|
|
1399
|
+
await rejects(async () => await (await createCompatibleActivity(plainUrl, malformedId)).getObject({
|
|
1400
|
+
documentLoader: claimLoader,
|
|
1401
|
+
contextLoader: mockDocumentLoader
|
|
1402
|
+
}), TypeError);
|
|
1403
|
+
});
|
|
1404
|
+
await t.step("objects with ordinary HTTP(S) identifiers", async () => {
|
|
1405
|
+
const documentLoader = createLoader({ [compatibleNoteId]: {
|
|
1406
|
+
...note(),
|
|
1407
|
+
id: compatibleNoteId
|
|
1408
|
+
} });
|
|
1409
|
+
deepStrictEqual((await (await createHttpActivity(compatibleNoteId)).getObject({
|
|
1410
|
+
documentLoader,
|
|
1411
|
+
contextLoader: mockDocumentLoader
|
|
1412
|
+
}))?.id, new URL(compatibleNoteId));
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
test("objects use the verifyPortableObject option they were parsed with by default", async (t) => {
|
|
1416
|
+
const attributedNote = (id) => ({
|
|
1417
|
+
...note(id),
|
|
1418
|
+
attributedTo: `ap://${did}/actor`
|
|
1419
|
+
});
|
|
1420
|
+
const actor = {
|
|
1421
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1422
|
+
id: `ap://${did}/actor`,
|
|
1423
|
+
type: "Person",
|
|
1424
|
+
inbox: `ap://${did}/actor/inbox`
|
|
1425
|
+
};
|
|
1426
|
+
const documentLoader = createLoader({
|
|
1427
|
+
[compatibleNoteId]: attributedNote(compatibleNoteId),
|
|
1428
|
+
[gatewayUrl("https://gw.example", "/actor")]: actor
|
|
1429
|
+
});
|
|
1430
|
+
await t.step("fromJsonLd()", async () => {
|
|
1431
|
+
const verifier = createRecordingVerifier();
|
|
1432
|
+
const object = await (await createCompatibleActivity(compatibleNoteId, void 0, { verifyPortableObject: verifier })).getObject({
|
|
1433
|
+
documentLoader,
|
|
1434
|
+
contextLoader: mockDocumentLoader
|
|
1435
|
+
});
|
|
1436
|
+
assertInstanceOf(object, Note);
|
|
1437
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1438
|
+
assertInstanceOf(await object.getAttribution({
|
|
1439
|
+
documentLoader,
|
|
1440
|
+
contextLoader: mockDocumentLoader,
|
|
1441
|
+
gateways: ["https://gw.example"]
|
|
1442
|
+
}), Person);
|
|
1443
|
+
deepStrictEqual(verifier.calls.length, 2);
|
|
1444
|
+
const embedded = await (await createCompatibleActivity(attributedNote(`ap://${did}/objects/2`), `ap://${did}/activities/1`, { verifyPortableObject: verifier })).getObject({
|
|
1445
|
+
documentLoader,
|
|
1446
|
+
contextLoader: mockDocumentLoader
|
|
1447
|
+
});
|
|
1448
|
+
assertInstanceOf(embedded, Note);
|
|
1449
|
+
assertInstanceOf(await embedded.getAttribution({
|
|
1450
|
+
documentLoader,
|
|
1451
|
+
contextLoader: mockDocumentLoader,
|
|
1452
|
+
gateways: ["https://gw.example"]
|
|
1453
|
+
}), Person);
|
|
1454
|
+
deepStrictEqual(verifier.calls.length, 3);
|
|
1455
|
+
});
|
|
1456
|
+
await t.step("the constructor and clone()", async () => {
|
|
1457
|
+
const verifier = createRecordingVerifier();
|
|
1458
|
+
const activity = new Create({
|
|
1459
|
+
id: new URL(gatewayUrl("https://gw.example", "/activities/1")),
|
|
1460
|
+
object: new URL(compatibleNoteId)
|
|
1461
|
+
}, {
|
|
1462
|
+
contextLoader: mockDocumentLoader,
|
|
1463
|
+
verifyPortableObject: verifier
|
|
1464
|
+
});
|
|
1465
|
+
assertInstanceOf(await activity.getObject({
|
|
1466
|
+
documentLoader,
|
|
1467
|
+
contextLoader: mockDocumentLoader
|
|
1468
|
+
}), Note);
|
|
1469
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1470
|
+
const clone = activity.clone({ object: new URL(compatibleNoteId) });
|
|
1471
|
+
await rejects(async () => await clone.getObject({
|
|
1472
|
+
documentLoader,
|
|
1473
|
+
contextLoader: mockDocumentLoader
|
|
1474
|
+
}), TypeError);
|
|
1475
|
+
assertInstanceOf(await activity.clone({ object: new URL(compatibleNoteId) }, {
|
|
1476
|
+
contextLoader: mockDocumentLoader,
|
|
1477
|
+
verifyPortableObject: verifier
|
|
1478
|
+
}).getObject({
|
|
1479
|
+
documentLoader,
|
|
1480
|
+
contextLoader: mockDocumentLoader
|
|
1481
|
+
}), Note);
|
|
1482
|
+
deepStrictEqual(verifier.calls.length, 2);
|
|
1483
|
+
});
|
|
1484
|
+
await t.step("an explicit option is passed on to fetched objects", async () => {
|
|
1485
|
+
const stored = createRecordingVerifier();
|
|
1486
|
+
const given = createRecordingVerifier();
|
|
1487
|
+
const activity = await createCompatibleActivity(compatibleNoteId, void 0, { verifyPortableObject: stored });
|
|
1488
|
+
const object = await activity.getObject({
|
|
1489
|
+
documentLoader,
|
|
1490
|
+
contextLoader: mockDocumentLoader,
|
|
1491
|
+
verifyPortableObject: given
|
|
1492
|
+
});
|
|
1493
|
+
assertInstanceOf(object, Note);
|
|
1494
|
+
deepStrictEqual([stored.calls.length, given.calls.length], [0, 1]);
|
|
1495
|
+
await object.getAttribution({
|
|
1496
|
+
documentLoader,
|
|
1497
|
+
contextLoader: mockDocumentLoader,
|
|
1498
|
+
gateways: ["https://gw.example"]
|
|
1499
|
+
});
|
|
1500
|
+
deepStrictEqual([stored.calls.length, given.calls.length], [0, 2]);
|
|
1501
|
+
ok(getDefaultVerifier(activity) === stored);
|
|
1502
|
+
});
|
|
1503
|
+
await t.step("lookupObject()", async () => {
|
|
1504
|
+
const verifier = createRecordingVerifier();
|
|
1505
|
+
const object = await lookupObject(compatibleNoteId, {
|
|
1506
|
+
documentLoader,
|
|
1507
|
+
contextLoader: mockDocumentLoader,
|
|
1508
|
+
verifyPortableObject: verifier
|
|
1509
|
+
});
|
|
1510
|
+
assertInstanceOf(object, Note);
|
|
1511
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1512
|
+
assertInstanceOf(await object.getAttribution({
|
|
1513
|
+
documentLoader,
|
|
1514
|
+
contextLoader: mockDocumentLoader,
|
|
1515
|
+
gateways: ["https://gw.example"]
|
|
1516
|
+
}), Person);
|
|
1517
|
+
deepStrictEqual(verifier.calls.length, 2);
|
|
1518
|
+
});
|
|
1519
|
+
await t.step("property preprocessors", async () => {
|
|
1520
|
+
const verifier = createVerifier();
|
|
1521
|
+
const icon = await (await Person.fromJsonLd({
|
|
1522
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1523
|
+
id: `ap://${did}/actor`,
|
|
1524
|
+
type: "Person",
|
|
1525
|
+
icon: {
|
|
1526
|
+
type: "Link",
|
|
1527
|
+
href: "https://example.com/icon.png"
|
|
1528
|
+
}
|
|
1529
|
+
}, {
|
|
1530
|
+
contextLoader: mockDocumentLoader,
|
|
1531
|
+
verifyPortableObject: verifier
|
|
1532
|
+
})).getIcon({ documentLoader });
|
|
1533
|
+
assertInstanceOf(icon, Image);
|
|
1534
|
+
ok(getDefaultVerifier(icon) === verifier);
|
|
1535
|
+
});
|
|
1536
|
+
});
|
|
1537
|
+
function getDefaultVerifier(object) {
|
|
1538
|
+
return object._verifyPortableObject;
|
|
1539
|
+
}
|
|
1540
|
+
test("accessors pass the verifier of a call on to the objects they fetch", async (t) => {
|
|
1541
|
+
const actorId = `ap://${did}/actor`;
|
|
1542
|
+
const attributedNote = (id) => ({
|
|
1543
|
+
...note(id),
|
|
1544
|
+
attributedTo: actorId
|
|
1545
|
+
});
|
|
1546
|
+
const actor = {
|
|
1547
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1548
|
+
id: actorId,
|
|
1549
|
+
type: "Person",
|
|
1550
|
+
inbox: `${actorId}/inbox`
|
|
1551
|
+
};
|
|
1552
|
+
const actorUrl = gatewayUrl("https://gw.example", "/actor");
|
|
1553
|
+
const plainNoteUrl = "https://example.com/notes/1";
|
|
1554
|
+
const plainActivityUrl = "https://example.com/activities/1";
|
|
1555
|
+
const options = {
|
|
1556
|
+
documentLoader: createLoader({
|
|
1557
|
+
[compatibleNoteId]: attributedNote(compatibleNoteId),
|
|
1558
|
+
[actorUrl]: actor,
|
|
1559
|
+
[plainNoteUrl]: {
|
|
1560
|
+
...attributedNote(plainNoteUrl),
|
|
1561
|
+
id: plainNoteUrl
|
|
1562
|
+
},
|
|
1563
|
+
[plainActivityUrl]: {
|
|
1564
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1565
|
+
type: "Create",
|
|
1566
|
+
id: plainActivityUrl,
|
|
1567
|
+
object: compatibleNoteId
|
|
1568
|
+
}
|
|
1569
|
+
}),
|
|
1570
|
+
contextLoader: mockDocumentLoader
|
|
1571
|
+
};
|
|
1572
|
+
const secondHop = {
|
|
1573
|
+
...options,
|
|
1574
|
+
gateways: ["https://gw.example"]
|
|
1575
|
+
};
|
|
1576
|
+
await t.step("from objects without a default", async () => {
|
|
1577
|
+
const roots = [
|
|
1578
|
+
["constructed", async () => new Create({
|
|
1579
|
+
id: new URL(gatewayUrl("https://gw.example", "/activities/1")),
|
|
1580
|
+
object: new URL(compatibleNoteId)
|
|
1581
|
+
}, { contextLoader: mockDocumentLoader })],
|
|
1582
|
+
["parsed", () => createCompatibleActivity(compatibleNoteId)],
|
|
1583
|
+
["looked up", async () => {
|
|
1584
|
+
const found = await lookupObject(plainActivityUrl, options);
|
|
1585
|
+
assertInstanceOf(found, Create);
|
|
1586
|
+
return found;
|
|
1587
|
+
}]
|
|
1588
|
+
];
|
|
1589
|
+
for (const [name, createRoot] of roots) {
|
|
1590
|
+
const verifier = createRecordingVerifier();
|
|
1591
|
+
const root = await createRoot();
|
|
1592
|
+
ok(getDefaultVerifier(root) == null, name);
|
|
1593
|
+
const object = await root.getObject({
|
|
1594
|
+
...options,
|
|
1595
|
+
verifyPortableObject: verifier
|
|
1596
|
+
});
|
|
1597
|
+
assertInstanceOf(object, Note);
|
|
1598
|
+
deepStrictEqual(verifier.calls.length, 1, name);
|
|
1599
|
+
assertInstanceOf(await object.getAttribution(secondHop), Person);
|
|
1600
|
+
deepStrictEqual(verifier.calls.length, 2, name);
|
|
1601
|
+
ok(getDefaultVerifier(root) == null, name);
|
|
1602
|
+
const verifier2 = createRecordingVerifier();
|
|
1603
|
+
const objects = await Array.fromAsync((await createRoot()).getObjects({
|
|
1604
|
+
...options,
|
|
1605
|
+
verifyPortableObject: verifier2
|
|
1606
|
+
}));
|
|
1607
|
+
deepStrictEqual(objects.length, 1, name);
|
|
1608
|
+
assertInstanceOf(objects[0], Note);
|
|
1609
|
+
const authors = await Array.fromAsync(objects[0].getAttributions(secondHop));
|
|
1610
|
+
deepStrictEqual(authors.length, 1, name);
|
|
1611
|
+
assertInstanceOf(authors[0], Person);
|
|
1612
|
+
deepStrictEqual(verifier2.calls.length, 2, name);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
await t.step("precedence", async () => {
|
|
1616
|
+
const stored = createRecordingVerifier();
|
|
1617
|
+
const given = createRecordingVerifier();
|
|
1618
|
+
const override = createRecordingVerifier();
|
|
1619
|
+
const counts = () => [
|
|
1620
|
+
stored.calls.length,
|
|
1621
|
+
given.calls.length,
|
|
1622
|
+
override.calls.length
|
|
1623
|
+
];
|
|
1624
|
+
const byDefault = await (await createCompatibleActivity(compatibleNoteId, void 0, { verifyPortableObject: stored })).getObject({
|
|
1625
|
+
...options,
|
|
1626
|
+
inheritPortableObjectVerifier: false
|
|
1627
|
+
});
|
|
1628
|
+
assertInstanceOf(byDefault, Note);
|
|
1629
|
+
ok(getDefaultVerifier(byDefault) === stored);
|
|
1630
|
+
deepStrictEqual(counts(), [
|
|
1631
|
+
1,
|
|
1632
|
+
0,
|
|
1633
|
+
0
|
|
1634
|
+
]);
|
|
1635
|
+
const optedOut = await (await createCompatibleActivity(compatibleNoteId, void 0, { verifyPortableObject: stored })).getObject({
|
|
1636
|
+
...options,
|
|
1637
|
+
verifyPortableObject: given,
|
|
1638
|
+
inheritPortableObjectVerifier: false
|
|
1639
|
+
});
|
|
1640
|
+
assertInstanceOf(optedOut, Note);
|
|
1641
|
+
deepStrictEqual(counts(), [
|
|
1642
|
+
1,
|
|
1643
|
+
1,
|
|
1644
|
+
0
|
|
1645
|
+
]);
|
|
1646
|
+
ok(getDefaultVerifier(optedOut) === stored);
|
|
1647
|
+
assertInstanceOf(await optedOut.getAttribution(secondHop), Person);
|
|
1648
|
+
deepStrictEqual(counts(), [
|
|
1649
|
+
2,
|
|
1650
|
+
1,
|
|
1651
|
+
0
|
|
1652
|
+
]);
|
|
1653
|
+
const noDefault = await (await createCompatibleActivity(compatibleNoteId)).getObject({
|
|
1654
|
+
...options,
|
|
1655
|
+
verifyPortableObject: given,
|
|
1656
|
+
inheritPortableObjectVerifier: false
|
|
1657
|
+
});
|
|
1658
|
+
assertInstanceOf(noDefault, Note);
|
|
1659
|
+
deepStrictEqual(counts(), [
|
|
1660
|
+
2,
|
|
1661
|
+
2,
|
|
1662
|
+
0
|
|
1663
|
+
]);
|
|
1664
|
+
ok(getDefaultVerifier(noDefault) == null);
|
|
1665
|
+
await rejects(async () => await noDefault.getAttribution(secondHop), TypeError);
|
|
1666
|
+
const inherited = await (await createCompatibleActivity(compatibleNoteId, void 0, { verifyPortableObject: stored })).getObject({
|
|
1667
|
+
...options,
|
|
1668
|
+
verifyPortableObject: given
|
|
1669
|
+
});
|
|
1670
|
+
assertInstanceOf(inherited, Note);
|
|
1671
|
+
deepStrictEqual(counts(), [
|
|
1672
|
+
2,
|
|
1673
|
+
3,
|
|
1674
|
+
0
|
|
1675
|
+
]);
|
|
1676
|
+
ok(getDefaultVerifier(inherited) === given);
|
|
1677
|
+
assertInstanceOf(await inherited.getAttribution({
|
|
1678
|
+
...secondHop,
|
|
1679
|
+
verifyPortableObject: override
|
|
1680
|
+
}), Person);
|
|
1681
|
+
deepStrictEqual(counts(), [
|
|
1682
|
+
2,
|
|
1683
|
+
3,
|
|
1684
|
+
1
|
|
1685
|
+
]);
|
|
1686
|
+
const optedOuts = await Array.fromAsync((await createCompatibleActivity([compatibleNoteId], void 0, { verifyPortableObject: stored })).getObjects({
|
|
1687
|
+
...options,
|
|
1688
|
+
verifyPortableObject: given,
|
|
1689
|
+
inheritPortableObjectVerifier: false
|
|
1690
|
+
}));
|
|
1691
|
+
deepStrictEqual(optedOuts.length, 1);
|
|
1692
|
+
ok(getDefaultVerifier(optedOuts[0]) === stored);
|
|
1693
|
+
});
|
|
1694
|
+
await t.step("ordinary HTTP(S) responses", async () => {
|
|
1695
|
+
const given = createRecordingVerifier();
|
|
1696
|
+
const stored = createRecordingVerifier();
|
|
1697
|
+
const object = await (await createHttpActivity(plainNoteUrl)).getObject({
|
|
1698
|
+
...options,
|
|
1699
|
+
verifyPortableObject: given
|
|
1700
|
+
});
|
|
1701
|
+
assertInstanceOf(object, Note);
|
|
1702
|
+
deepStrictEqual(given.calls.length, 0);
|
|
1703
|
+
ok(getDefaultVerifier(object) === given);
|
|
1704
|
+
assertInstanceOf(await object.getAttribution(secondHop), Person);
|
|
1705
|
+
deepStrictEqual(given.calls.length, 1);
|
|
1706
|
+
const { referrer } = given.calls[0].options;
|
|
1707
|
+
ok(referrer?.object === object);
|
|
1708
|
+
deepStrictEqual(referrer?.acceptance, void 0);
|
|
1709
|
+
const optedOut = await (await Create.fromJsonLd({
|
|
1710
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1711
|
+
type: "Create",
|
|
1712
|
+
id: plainActivityUrl,
|
|
1713
|
+
object: plainNoteUrl
|
|
1714
|
+
}, {
|
|
1715
|
+
contextLoader: mockDocumentLoader,
|
|
1716
|
+
verifyPortableObject: stored
|
|
1717
|
+
})).getObject({
|
|
1718
|
+
...options,
|
|
1719
|
+
verifyPortableObject: given,
|
|
1720
|
+
inheritPortableObjectVerifier: false
|
|
1721
|
+
});
|
|
1722
|
+
assertInstanceOf(optedOut, Note);
|
|
1723
|
+
ok(getDefaultVerifier(optedOut) === stored);
|
|
1724
|
+
});
|
|
1725
|
+
await t.step("HTTP(S) responses that stand for portable objects", async () => {
|
|
1726
|
+
const redirecting = createRedirectingLoader({ [plainNoteUrl]: {
|
|
1727
|
+
documentUrl: compatibleNoteId,
|
|
1728
|
+
document: attributedNote(compatibleNoteId)
|
|
1729
|
+
} });
|
|
1730
|
+
const given = createRecordingVerifier();
|
|
1731
|
+
const object = await (await createHttpActivity(plainNoteUrl)).getObject({
|
|
1732
|
+
documentLoader: redirecting,
|
|
1733
|
+
contextLoader: mockDocumentLoader,
|
|
1734
|
+
verifyPortableObject: given
|
|
1735
|
+
});
|
|
1736
|
+
assertInstanceOf(object, Note);
|
|
1737
|
+
deepStrictEqual(given.calls.length, 1);
|
|
1738
|
+
ok(getDefaultVerifier(object) === given);
|
|
1739
|
+
const optedOut = await (await createHttpActivity(plainNoteUrl)).getObject({
|
|
1740
|
+
documentLoader: redirecting,
|
|
1741
|
+
contextLoader: mockDocumentLoader,
|
|
1742
|
+
verifyPortableObject: given,
|
|
1743
|
+
inheritPortableObjectVerifier: false
|
|
1744
|
+
});
|
|
1745
|
+
assertInstanceOf(optedOut, Note);
|
|
1746
|
+
deepStrictEqual(given.calls.length, 2);
|
|
1747
|
+
ok(getDefaultVerifier(optedOut) == null);
|
|
1748
|
+
});
|
|
1749
|
+
await t.step("objects that were fetched without a verifier", async () => {
|
|
1750
|
+
const activity = await createHttpActivity(compatibleNoteId);
|
|
1751
|
+
const unverified = await activity.getObject(options);
|
|
1752
|
+
assertInstanceOf(unverified, Note);
|
|
1753
|
+
ok(getDefaultVerifier(unverified) == null);
|
|
1754
|
+
const verifier = createRecordingVerifier();
|
|
1755
|
+
const verified = await activity.getObject({
|
|
1756
|
+
...options,
|
|
1757
|
+
verifyPortableObject: verifier
|
|
1758
|
+
});
|
|
1759
|
+
assertInstanceOf(verified, Note);
|
|
1760
|
+
ok(verified !== unverified);
|
|
1761
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1762
|
+
ok(getDefaultVerifier(verified) === verifier);
|
|
1763
|
+
});
|
|
1764
|
+
await t.step("property preprocessors", async () => {
|
|
1765
|
+
const iconUrl = "https://example.com/icon";
|
|
1766
|
+
const loader = createLoader({ [iconUrl]: {
|
|
1767
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1768
|
+
type: "Link",
|
|
1769
|
+
href: "https://example.com/icon.png"
|
|
1770
|
+
} });
|
|
1771
|
+
const createPerson = () => Person.fromJsonLd({
|
|
1772
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1773
|
+
id: "https://example.com/person",
|
|
1774
|
+
type: "Person",
|
|
1775
|
+
icon: iconUrl
|
|
1776
|
+
}, { contextLoader: mockDocumentLoader });
|
|
1777
|
+
const verifier = createVerifier();
|
|
1778
|
+
const icon = await (await createPerson()).getIcon({
|
|
1779
|
+
documentLoader: loader,
|
|
1780
|
+
contextLoader: mockDocumentLoader,
|
|
1781
|
+
verifyPortableObject: verifier
|
|
1782
|
+
});
|
|
1783
|
+
assertInstanceOf(icon, Image);
|
|
1784
|
+
ok(getDefaultVerifier(icon) === verifier);
|
|
1785
|
+
const optedOut = await (await createPerson()).getIcon({
|
|
1786
|
+
documentLoader: loader,
|
|
1787
|
+
contextLoader: mockDocumentLoader,
|
|
1788
|
+
verifyPortableObject: verifier,
|
|
1789
|
+
inheritPortableObjectVerifier: false
|
|
1790
|
+
});
|
|
1791
|
+
assertInstanceOf(optedOut, Image);
|
|
1792
|
+
ok(getDefaultVerifier(optedOut) == null);
|
|
1793
|
+
});
|
|
1794
|
+
await t.step("embedded objects keep the parent's default", async () => {
|
|
1795
|
+
const embedded = attributedNote(`ap://${did}/objects/2`);
|
|
1796
|
+
const withoutContext = { ...embedded };
|
|
1797
|
+
delete withoutContext["@context"];
|
|
1798
|
+
for (const [name, value] of [["with @context", embedded], ["without @context", withoutContext]]) for (const stored of [void 0, createRecordingVerifier()]) {
|
|
1799
|
+
const given = createRecordingVerifier();
|
|
1800
|
+
const object = await (await createCompatibleActivity(value, `ap://${did}/activities/1`, stored == null ? {} : { verifyPortableObject: stored })).getObject({
|
|
1801
|
+
...options,
|
|
1802
|
+
verifyPortableObject: given
|
|
1803
|
+
});
|
|
1804
|
+
assertInstanceOf(object, Note);
|
|
1805
|
+
ok(getDefaultVerifier(object) === stored, name);
|
|
1806
|
+
deepStrictEqual(given.calls.length, 0, name);
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
await t.step("cached objects", async () => {
|
|
1810
|
+
const weak = createRecordingVerifier();
|
|
1811
|
+
const strict = createRecordingVerifier();
|
|
1812
|
+
for (const [first, second] of [[weak, strict], [strict, weak]]) {
|
|
1813
|
+
const activity = await createCompatibleActivity(compatibleNoteId);
|
|
1814
|
+
const object = await activity.getObject({
|
|
1815
|
+
...options,
|
|
1816
|
+
verifyPortableObject: first
|
|
1817
|
+
});
|
|
1818
|
+
assertInstanceOf(object, Note);
|
|
1819
|
+
const before = second.calls.length;
|
|
1820
|
+
const cached = await activity.getObject({
|
|
1821
|
+
...options,
|
|
1822
|
+
verifyPortableObject: second
|
|
1823
|
+
});
|
|
1824
|
+
ok(cached === object);
|
|
1825
|
+
ok(getDefaultVerifier(cached) === first);
|
|
1826
|
+
deepStrictEqual(second.calls.length, before);
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
await t.step("traverseCollection()", async () => {
|
|
1830
|
+
const collectionUrl = "https://example.com/collection";
|
|
1831
|
+
const pageUrl = "https://example.com/collection?page=1";
|
|
1832
|
+
const loader = createLoader({
|
|
1833
|
+
...Object.fromEntries([
|
|
1834
|
+
compatibleNoteId,
|
|
1835
|
+
actorUrl,
|
|
1836
|
+
plainNoteUrl
|
|
1837
|
+
].map((url) => [url, url === plainNoteUrl ? {
|
|
1838
|
+
...attributedNote(plainNoteUrl),
|
|
1839
|
+
id: plainNoteUrl
|
|
1840
|
+
} : url === actorUrl ? actor : attributedNote(compatibleNoteId)])),
|
|
1841
|
+
[pageUrl]: {
|
|
1842
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1843
|
+
id: pageUrl,
|
|
1844
|
+
type: "OrderedCollectionPage",
|
|
1845
|
+
orderedItems: [
|
|
1846
|
+
{
|
|
1847
|
+
id: "https://example.com/notes/2",
|
|
1848
|
+
type: "Note",
|
|
1849
|
+
attributedTo: actorId
|
|
1850
|
+
},
|
|
1851
|
+
plainNoteUrl,
|
|
1852
|
+
compatibleNoteId
|
|
1853
|
+
]
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
const createCollection = () => OrderedCollection.fromJsonLd({
|
|
1857
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1858
|
+
id: collectionUrl,
|
|
1859
|
+
type: "OrderedCollection",
|
|
1860
|
+
first: pageUrl
|
|
1861
|
+
}, { contextLoader: mockDocumentLoader });
|
|
1862
|
+
const verifier = createRecordingVerifier();
|
|
1863
|
+
const items = await Array.fromAsync(traverseCollection(await createCollection(), {
|
|
1864
|
+
documentLoader: loader,
|
|
1865
|
+
contextLoader: mockDocumentLoader,
|
|
1866
|
+
verifyPortableObject: verifier
|
|
1867
|
+
}));
|
|
1868
|
+
deepStrictEqual(items.length, 3);
|
|
1869
|
+
deepStrictEqual(verifier.calls.length, 1);
|
|
1870
|
+
for (const item of items) {
|
|
1871
|
+
assertInstanceOf(item, Note);
|
|
1872
|
+
ok(getDefaultVerifier(item) === verifier);
|
|
1873
|
+
assertInstanceOf(await item.getAttribution({
|
|
1874
|
+
documentLoader: loader,
|
|
1875
|
+
contextLoader: mockDocumentLoader,
|
|
1876
|
+
gateways: ["https://gw.example"]
|
|
1877
|
+
}), Person);
|
|
1878
|
+
}
|
|
1879
|
+
deepStrictEqual(verifier.calls.length, 4);
|
|
1880
|
+
const optedOut = await Array.fromAsync(traverseCollection(await createCollection(), {
|
|
1881
|
+
documentLoader: loader,
|
|
1882
|
+
contextLoader: mockDocumentLoader,
|
|
1883
|
+
verifyPortableObject: verifier,
|
|
1884
|
+
inheritPortableObjectVerifier: false
|
|
1885
|
+
}));
|
|
1886
|
+
deepStrictEqual(optedOut.length, 3);
|
|
1887
|
+
for (const item of optedOut) ok(getDefaultVerifier(item) == null);
|
|
1888
|
+
});
|
|
1889
|
+
});
|
|
1890
|
+
test("getObject() follows location hints made by withGatewayHints()", async () => {
|
|
1891
|
+
const json = await new Create({
|
|
1892
|
+
id: parseIri(`ap://${did}/activities/1`),
|
|
1893
|
+
object: withGatewayHints(objectId, ["https://gw1.example", "https://gw2.example"])
|
|
1894
|
+
}).toJsonLd({ contextLoader: mockDocumentLoader });
|
|
1895
|
+
deepStrictEqual(json.object, `${objectId}?@gateway=https%3A%2F%2Fgw1.example&@gateway=https%3A%2F%2Fgw2.example`);
|
|
1896
|
+
const parsed = await Create.fromJsonLd(json, { contextLoader: mockDocumentLoader });
|
|
1897
|
+
const documentLoader = createLoader({ [`https://gw2.example${gatewayPath}`]: note() });
|
|
1898
|
+
assertInstanceOf(await parsed.getObject({
|
|
1899
|
+
documentLoader,
|
|
1900
|
+
contextLoader: mockDocumentLoader,
|
|
1901
|
+
verifyPortableObject: createVerifier()
|
|
1902
|
+
}), Note);
|
|
1903
|
+
deepStrictEqual(documentLoader.fetched, [`https://gw1.example${gatewayPath}`, `https://gw2.example${gatewayPath}`]);
|
|
1904
|
+
});
|
|
1905
|
+
//#endregion
|
|
1906
|
+
export {};
|