@fedify/vocab 2.4.0-pr.936.41 → 2.4.1

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.
Files changed (36) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +32014 -9323
  3. package/dist/mod.d.cts +4770 -91
  4. package/dist/mod.d.ts +4770 -91
  5. package/dist/mod.js +31907 -9223
  6. package/dist-tests/actor-D_QaCMro.mjs +276 -0
  7. package/dist-tests/actor.test.mjs +55 -49
  8. package/dist-tests/{actor-MBpzF3-D.mjs → esm-YWo4aQQe.mjs} +15 -211
  9. package/dist-tests/gateway.test.mjs +1906 -0
  10. package/dist-tests/lookup-BevjwZhB.mjs +480 -0
  11. package/dist-tests/lookup.test.mjs +10 -268
  12. package/dist-tests/object.yaml +15 -0
  13. package/dist-tests/portable-lookup.test.mjs +653 -0
  14. package/dist-tests/signed-representation.test.mjs +189 -0
  15. package/dist-tests/translation.test.mjs +404 -0
  16. package/dist-tests/translation.yaml +91 -0
  17. package/dist-tests/type.test.mjs +2 -2
  18. package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-DaniQwU7.mjs} +31669 -9267
  19. package/dist-tests/vocab.test.mjs +901 -11
  20. package/package.json +5 -5
  21. package/src/__snapshots__/vocab.test.ts.snap +291 -15
  22. package/src/actor.test.ts +41 -40
  23. package/src/actor.ts +120 -1
  24. package/src/gateway.test.ts +2637 -0
  25. package/src/lookup.test.ts +6 -5
  26. package/src/lookup.ts +460 -33
  27. package/src/mod.ts +9 -0
  28. package/src/object.yaml +15 -0
  29. package/src/portable-lookup.test.ts +889 -0
  30. package/src/preprocessors.ts +1 -1
  31. package/src/signed-representation.test.ts +277 -0
  32. package/src/translation.test.ts +518 -0
  33. package/src/translation.yaml +91 -0
  34. package/src/vocab.test.ts +1219 -5
  35. /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
  36. /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-DaniQwU7.mjs";
4
+ import { t as esm_default } from "./esm-YWo4aQQe.mjs";
5
+ import { n as traverseCollection, t as lookupObject } from "./lookup-BevjwZhB.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 {};