@fedify/vocab 2.4.0-dev.2035 → 2.4.0-dev.2047
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 +5472 -645
- package/dist/mod.d.cts +416 -1
- package/dist/mod.d.ts +416 -1
- package/dist/mod.js +5472 -645
- package/dist-tests/{actor-BX07d5tK.mjs → actor-chbmxipX.mjs} +71 -2
- package/dist-tests/actor.test.mjs +3 -3
- package/dist-tests/{esm-C_FjLvhG.mjs → esm-Df0Yo-Tp.mjs} +1 -1
- package/dist-tests/gateway.test.mjs +604 -0
- package/dist-tests/{lookup-B9lgdxnM.mjs → lookup-CvrMdVSJ.mjs} +211 -9
- package/dist-tests/lookup.test.mjs +3 -3
- package/dist-tests/portable-lookup.test.mjs +335 -0
- package/dist-tests/signed-representation.test.mjs +1 -1
- package/dist-tests/translation.test.mjs +3 -3
- package/dist-tests/type.test.mjs +1 -1
- package/dist-tests/{vocab-Dw_Vm0NE.mjs → vocab-BJGPw--L.mjs} +5109 -551
- package/dist-tests/vocab.test.mjs +9 -4
- package/package.json +4 -4
- package/src/actor.ts +110 -1
- package/src/gateway.test.ts +872 -0
- package/src/lookup.ts +302 -2
- package/src/portable-lookup.test.ts +457 -0
- package/src/vocab.test.ts +8 -1
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
globalThis.addEventListener = () => {};
|
|
3
|
+
import { O as Note, l as Create } from "./vocab-BJGPw--L.mjs";
|
|
4
|
+
import { t as esm_default } from "./esm-Df0Yo-Tp.mjs";
|
|
5
|
+
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
6
|
+
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
7
|
+
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
8
|
+
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
9
|
+
//#region src/gateway.test.ts
|
|
10
|
+
const did = "did:key:z6Mkabc";
|
|
11
|
+
const objectId = `ap+ef61://${did}/objects/1`;
|
|
12
|
+
const gatewayPath = `/.well-known/apgateway/${did}/objects/1`;
|
|
13
|
+
function note(id = `ap://${did}/objects/1`) {
|
|
14
|
+
return {
|
|
15
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
16
|
+
id,
|
|
17
|
+
type: "Note",
|
|
18
|
+
content: "Portable note"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function createLoader(responses) {
|
|
22
|
+
const fetched = [];
|
|
23
|
+
const loader = async (url) => {
|
|
24
|
+
fetched.push(url);
|
|
25
|
+
const response = responses[url];
|
|
26
|
+
if (response == null) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
|
|
27
|
+
if (response instanceof Error) throw response;
|
|
28
|
+
if (typeof response === "number") throw new FetchError(url, `HTTP ${response}`, new globalThis.Response(null, { status: response }));
|
|
29
|
+
return {
|
|
30
|
+
contextUrl: null,
|
|
31
|
+
documentUrl: url,
|
|
32
|
+
document: response
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
return Object.assign(loader, { fetched });
|
|
36
|
+
}
|
|
37
|
+
function createVerifier(verified = true) {
|
|
38
|
+
const documents = [];
|
|
39
|
+
const verifier = async (document) => {
|
|
40
|
+
documents.push(document);
|
|
41
|
+
return { verified };
|
|
42
|
+
};
|
|
43
|
+
return Object.assign(verifier, { documents });
|
|
44
|
+
}
|
|
45
|
+
function createActivity(object = objectId) {
|
|
46
|
+
return Create.fromJsonLd({
|
|
47
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
48
|
+
type: "Create",
|
|
49
|
+
id: `ap://${did}/activities/1`,
|
|
50
|
+
object
|
|
51
|
+
}, { contextLoader: mockDocumentLoader });
|
|
52
|
+
}
|
|
53
|
+
test("getObject() dereferences a portable IRI through explicit gateways", async () => {
|
|
54
|
+
const document = note();
|
|
55
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: document });
|
|
56
|
+
const verifyPortableObject = createVerifier();
|
|
57
|
+
const activity = await createActivity();
|
|
58
|
+
const object = await activity.getObject({
|
|
59
|
+
documentLoader,
|
|
60
|
+
contextLoader: mockDocumentLoader,
|
|
61
|
+
gateways: ["https://gw1.example", new URL("https://gw2.example/")],
|
|
62
|
+
verifyPortableObject
|
|
63
|
+
});
|
|
64
|
+
assertInstanceOf(object, Note);
|
|
65
|
+
deepStrictEqual(object.id, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
|
|
66
|
+
deepStrictEqual(object.content, "Portable note");
|
|
67
|
+
deepStrictEqual(documentLoader.fetched, [`https://gw1.example${gatewayPath}`]);
|
|
68
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
69
|
+
deepStrictEqual((await activity.getObject({
|
|
70
|
+
documentLoader,
|
|
71
|
+
contextLoader: mockDocumentLoader
|
|
72
|
+
}))?.content, "Portable note");
|
|
73
|
+
deepStrictEqual(documentLoader.fetched.length, 1);
|
|
74
|
+
});
|
|
75
|
+
test("getObject() keeps non-hint query parameters for gateways", async () => {
|
|
76
|
+
const documentLoader = createLoader({ [`https://gw.example${gatewayPath}?page=2`]: note(`ap://${did}/objects/1?page=2`) });
|
|
77
|
+
assertInstanceOf(await (await createActivity(`${objectId}?page=2&@gateway=https%3A%2F%2Fgw.example`)).getObject({
|
|
78
|
+
documentLoader,
|
|
79
|
+
contextLoader: mockDocumentLoader,
|
|
80
|
+
verifyPortableObject: createVerifier()
|
|
81
|
+
}), Note);
|
|
82
|
+
deepStrictEqual(documentLoader.fetched, [`https://gw.example${gatewayPath}?page=2`]);
|
|
83
|
+
});
|
|
84
|
+
test("getObject() tries the next gateway after failures", async () => {
|
|
85
|
+
const documentLoader = createLoader({
|
|
86
|
+
[`https://gw2.example${gatewayPath}`]: /* @__PURE__ */ new TypeError("Network error"),
|
|
87
|
+
[`https://gw3.example${gatewayPath}`]: 500,
|
|
88
|
+
[`https://gw4.example${gatewayPath}`]: note(),
|
|
89
|
+
[`https://gw5.example${gatewayPath}`]: note()
|
|
90
|
+
});
|
|
91
|
+
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
92
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
93
|
+
documentLoader,
|
|
94
|
+
contextLoader: mockDocumentLoader,
|
|
95
|
+
tracerProvider,
|
|
96
|
+
gateways: [
|
|
97
|
+
"https://gw1.example",
|
|
98
|
+
"https://gw2.example",
|
|
99
|
+
"https://gw3.example",
|
|
100
|
+
"https://gw4.example",
|
|
101
|
+
"https://gw5.example"
|
|
102
|
+
],
|
|
103
|
+
verifyPortableObject: createVerifier()
|
|
104
|
+
}), Note);
|
|
105
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
106
|
+
`https://gw1.example${gatewayPath}`,
|
|
107
|
+
`https://gw2.example${gatewayPath}`,
|
|
108
|
+
`https://gw3.example${gatewayPath}`,
|
|
109
|
+
`https://gw4.example${gatewayPath}`
|
|
110
|
+
]);
|
|
111
|
+
const spans = exporter.getSpans("activitypub.lookup_object");
|
|
112
|
+
deepStrictEqual(spans.length, 1);
|
|
113
|
+
deepStrictEqual(spans[0].status.code, 0);
|
|
114
|
+
deepStrictEqual(spans[0].attributes["activitypub.gateway"], "https://gw4.example/");
|
|
115
|
+
deepStrictEqual(spans[0].attributes["activitypub.object.type"], "https://www.w3.org/ns/activitystreams#Note");
|
|
116
|
+
});
|
|
117
|
+
test("getObject() reports errors when every gateway fails", async () => {
|
|
118
|
+
const single = createLoader({ [`https://gw1.example${gatewayPath}`]: 500 });
|
|
119
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
120
|
+
documentLoader: single,
|
|
121
|
+
contextLoader: mockDocumentLoader,
|
|
122
|
+
gateways: ["https://gw1.example"],
|
|
123
|
+
verifyPortableObject: createVerifier()
|
|
124
|
+
}), FetchError);
|
|
125
|
+
const options = {
|
|
126
|
+
documentLoader: createLoader({
|
|
127
|
+
[`https://gw1.example${gatewayPath}`]: 500,
|
|
128
|
+
[`https://gw2.example${gatewayPath}`]: /* @__PURE__ */ new TypeError("Network error")
|
|
129
|
+
}),
|
|
130
|
+
contextLoader: mockDocumentLoader,
|
|
131
|
+
gateways: ["https://gw1.example", "https://gw2.example"],
|
|
132
|
+
verifyPortableObject: createVerifier()
|
|
133
|
+
};
|
|
134
|
+
await rejects(async () => await (await createActivity()).getObject(options), (error) => {
|
|
135
|
+
assertInstanceOf(error, AggregateError);
|
|
136
|
+
deepStrictEqual(error.errors.length, 2);
|
|
137
|
+
assertInstanceOf(error.errors[0], FetchError);
|
|
138
|
+
assertInstanceOf(error.errors[1], TypeError);
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
142
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
143
|
+
...options,
|
|
144
|
+
suppressError: true,
|
|
145
|
+
tracerProvider
|
|
146
|
+
}), null);
|
|
147
|
+
const spans = exporter.getSpans("activitypub.lookup_object");
|
|
148
|
+
deepStrictEqual(spans.length, 1);
|
|
149
|
+
deepStrictEqual(spans[0].status.code, 2);
|
|
150
|
+
const unparsable = createLoader({ [`https://gw1.example${gatewayPath}`]: {
|
|
151
|
+
...note(),
|
|
152
|
+
"@context": "https://example.com/unknown-context"
|
|
153
|
+
} });
|
|
154
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
155
|
+
documentLoader: unparsable,
|
|
156
|
+
contextLoader: mockDocumentLoader,
|
|
157
|
+
gateways: ["https://gw1.example"],
|
|
158
|
+
verifyPortableObject: createVerifier()
|
|
159
|
+
}));
|
|
160
|
+
});
|
|
161
|
+
test("getObject() rejects objects with a mismatching portable ID", async (t) => {
|
|
162
|
+
const mismatches = {
|
|
163
|
+
"another DID": `ap://did:key:z6Mkdef/objects/1`,
|
|
164
|
+
"another path": `ap://${did}/objects/2`,
|
|
165
|
+
"a dot segment": `ap://${did}/objects/x/../1`,
|
|
166
|
+
"a fragment": `ap://${did}/objects/1#fragment`,
|
|
167
|
+
"a compatible identifier": `https://gw1.example${gatewayPath}`,
|
|
168
|
+
"an HTTP(S) ID": "https://gw1.example/objects/1"
|
|
169
|
+
};
|
|
170
|
+
for (const [name, id] of globalThis.Object.entries(mismatches)) await t.step(name, async () => {
|
|
171
|
+
const options = {
|
|
172
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: note(id) }),
|
|
173
|
+
contextLoader: mockDocumentLoader,
|
|
174
|
+
gateways: ["https://gw1.example"],
|
|
175
|
+
verifyPortableObject: createVerifier()
|
|
176
|
+
};
|
|
177
|
+
deepStrictEqual(await (await createActivity()).getObject(options), null);
|
|
178
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
179
|
+
...options,
|
|
180
|
+
crossOrigin: "trust"
|
|
181
|
+
}), null);
|
|
182
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
183
|
+
...options,
|
|
184
|
+
crossOrigin: "throw",
|
|
185
|
+
suppressError: true
|
|
186
|
+
}), null);
|
|
187
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
188
|
+
...options,
|
|
189
|
+
crossOrigin: "throw"
|
|
190
|
+
}), /No gateway returned a valid portable object/);
|
|
191
|
+
});
|
|
192
|
+
await t.step("equivalent spellings", async () => {
|
|
193
|
+
for (const id of [
|
|
194
|
+
`ap+ef61://${did}/objects/1`,
|
|
195
|
+
`ap://did%3Akey%3Az6Mkabc/objects/1`,
|
|
196
|
+
`ap://${did}/objects/1?@gateway=https%3A%2F%2Fother.example`
|
|
197
|
+
]) {
|
|
198
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: note(id) });
|
|
199
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
200
|
+
documentLoader,
|
|
201
|
+
contextLoader: mockDocumentLoader,
|
|
202
|
+
gateways: ["https://gw1.example"],
|
|
203
|
+
verifyPortableObject: createVerifier()
|
|
204
|
+
}), Note);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
await t.step("a mismatch does not stop other gateways", async () => {
|
|
208
|
+
const documentLoader = createLoader({
|
|
209
|
+
[`https://gw1.example${gatewayPath}`]: note(`ap://${did}/objects/2`),
|
|
210
|
+
[`https://gw2.example${gatewayPath}`]: 500,
|
|
211
|
+
[`https://gw3.example${gatewayPath}`]: note()
|
|
212
|
+
});
|
|
213
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
214
|
+
documentLoader,
|
|
215
|
+
contextLoader: mockDocumentLoader,
|
|
216
|
+
gateways: [
|
|
217
|
+
"https://gw1.example",
|
|
218
|
+
"https://gw2.example",
|
|
219
|
+
"https://gw3.example"
|
|
220
|
+
],
|
|
221
|
+
verifyPortableObject: createVerifier(),
|
|
222
|
+
crossOrigin: "throw"
|
|
223
|
+
}), Note);
|
|
224
|
+
});
|
|
225
|
+
await t.step("a mismatch among errors is reported as a mismatch", async () => {
|
|
226
|
+
const documentLoader = createLoader({
|
|
227
|
+
[`https://gw1.example${gatewayPath}`]: 500,
|
|
228
|
+
[`https://gw2.example${gatewayPath}`]: note(`ap://${did}/objects/2`)
|
|
229
|
+
});
|
|
230
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
231
|
+
documentLoader,
|
|
232
|
+
contextLoader: mockDocumentLoader,
|
|
233
|
+
gateways: ["https://gw1.example", "https://gw2.example"],
|
|
234
|
+
verifyPortableObject: createVerifier()
|
|
235
|
+
}), null);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
test("getObject() rejects documents with other shapes", async () => {
|
|
239
|
+
for (const document of [[note()], {
|
|
240
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
241
|
+
"@graph": [note(), note(`ap://${did}/objects/2`)]
|
|
242
|
+
}]) {
|
|
243
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: document });
|
|
244
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
245
|
+
documentLoader,
|
|
246
|
+
contextLoader: mockDocumentLoader,
|
|
247
|
+
gateways: ["https://gw1.example"],
|
|
248
|
+
verifyPortableObject: createVerifier()
|
|
249
|
+
}), null);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
test("getObject() leaves documents without @id to the verifier", async () => {
|
|
253
|
+
const document = {
|
|
254
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
255
|
+
type: "Note",
|
|
256
|
+
content: "No ID"
|
|
257
|
+
};
|
|
258
|
+
const options = {
|
|
259
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: document }),
|
|
260
|
+
contextLoader: mockDocumentLoader,
|
|
261
|
+
gateways: ["https://gw1.example"]
|
|
262
|
+
};
|
|
263
|
+
const rejecting = createVerifier(false);
|
|
264
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
265
|
+
...options,
|
|
266
|
+
verifyPortableObject: rejecting
|
|
267
|
+
}), null);
|
|
268
|
+
deepStrictEqual(rejecting.documents, [document]);
|
|
269
|
+
const object = await (await createActivity()).getObject({
|
|
270
|
+
...options,
|
|
271
|
+
verifyPortableObject: createVerifier()
|
|
272
|
+
});
|
|
273
|
+
assertInstanceOf(object, Note);
|
|
274
|
+
deepStrictEqual(object.id, null);
|
|
275
|
+
});
|
|
276
|
+
test("getObject() applies the portable object verifier", async () => {
|
|
277
|
+
const gateways = ["https://gw1.example", "https://gw2.example"];
|
|
278
|
+
const documentLoader = createLoader({
|
|
279
|
+
[`https://gw1.example${gatewayPath}`]: note(),
|
|
280
|
+
[`https://gw2.example${gatewayPath}`]: note()
|
|
281
|
+
});
|
|
282
|
+
const failing = createVerifier(false);
|
|
283
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
284
|
+
documentLoader,
|
|
285
|
+
contextLoader: mockDocumentLoader,
|
|
286
|
+
gateways,
|
|
287
|
+
verifyPortableObject: failing,
|
|
288
|
+
crossOrigin: "trust"
|
|
289
|
+
}), null);
|
|
290
|
+
deepStrictEqual(failing.documents.length, 2);
|
|
291
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
292
|
+
documentLoader,
|
|
293
|
+
contextLoader: mockDocumentLoader,
|
|
294
|
+
gateways,
|
|
295
|
+
verifyPortableObject: failing,
|
|
296
|
+
crossOrigin: "throw"
|
|
297
|
+
}), /No gateway returned a valid portable object/);
|
|
298
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
299
|
+
documentLoader,
|
|
300
|
+
contextLoader: mockDocumentLoader,
|
|
301
|
+
gateways,
|
|
302
|
+
verifyPortableObject: async () => {
|
|
303
|
+
throw new Error("Verifier failure");
|
|
304
|
+
}
|
|
305
|
+
}), null);
|
|
306
|
+
const activity = await createActivity();
|
|
307
|
+
deepStrictEqual(await activity.getObject({
|
|
308
|
+
documentLoader,
|
|
309
|
+
contextLoader: mockDocumentLoader,
|
|
310
|
+
gateways,
|
|
311
|
+
verifyPortableObject: failing
|
|
312
|
+
}), null);
|
|
313
|
+
assertInstanceOf(await activity.getObject({
|
|
314
|
+
documentLoader,
|
|
315
|
+
contextLoader: mockDocumentLoader,
|
|
316
|
+
gateways,
|
|
317
|
+
verifyPortableObject: createVerifier()
|
|
318
|
+
}), Note);
|
|
319
|
+
});
|
|
320
|
+
test("getObject() requires a verifier before fetching portable IRIs", async () => {
|
|
321
|
+
const documentLoader = createLoader({ [`https://gw1.example${gatewayPath}`]: note() });
|
|
322
|
+
await rejects(async () => await (await createActivity()).getObject({
|
|
323
|
+
documentLoader,
|
|
324
|
+
contextLoader: mockDocumentLoader,
|
|
325
|
+
gateways: ["https://gw1.example"]
|
|
326
|
+
}), (error) => {
|
|
327
|
+
assertInstanceOf(error, TypeError);
|
|
328
|
+
ok(error.message.includes("verifyPortableObject"));
|
|
329
|
+
return true;
|
|
330
|
+
});
|
|
331
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
332
|
+
documentLoader,
|
|
333
|
+
contextLoader: mockDocumentLoader,
|
|
334
|
+
gateways: ["https://gw1.example"],
|
|
335
|
+
suppressError: true
|
|
336
|
+
}), null);
|
|
337
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
338
|
+
});
|
|
339
|
+
test("getObject() suppresses errors for malformed portable IRIs", async () => {
|
|
340
|
+
const activity = await createActivity(`ap://${did}/objects/%ZZ`);
|
|
341
|
+
for (const gateways of [[], ["https://gw1.example"]]) {
|
|
342
|
+
const documentLoader = createLoader({});
|
|
343
|
+
deepStrictEqual(await activity.getObject({
|
|
344
|
+
documentLoader,
|
|
345
|
+
contextLoader: mockDocumentLoader,
|
|
346
|
+
gateways,
|
|
347
|
+
verifyPortableObject: createVerifier(),
|
|
348
|
+
suppressError: true
|
|
349
|
+
}), null);
|
|
350
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
351
|
+
await rejects(async () => await activity.getObject({
|
|
352
|
+
documentLoader,
|
|
353
|
+
contextLoader: mockDocumentLoader,
|
|
354
|
+
gateways,
|
|
355
|
+
verifyPortableObject: createVerifier()
|
|
356
|
+
}), TypeError);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
test("getObject() rejects invalid gateways before fetching", async () => {
|
|
360
|
+
const documentLoader = createLoader({});
|
|
361
|
+
for (const gateway of [
|
|
362
|
+
"https://gw.example/path",
|
|
363
|
+
"https://gw.example/?query",
|
|
364
|
+
"https://user@gw.example",
|
|
365
|
+
"ftp://gw.example",
|
|
366
|
+
"not a URL"
|
|
367
|
+
]) await rejects(async () => await (await createActivity()).getObject({
|
|
368
|
+
documentLoader,
|
|
369
|
+
contextLoader: mockDocumentLoader,
|
|
370
|
+
gateways: [gateway],
|
|
371
|
+
verifyPortableObject: createVerifier(),
|
|
372
|
+
suppressError: true
|
|
373
|
+
}), TypeError);
|
|
374
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
375
|
+
});
|
|
376
|
+
test("getObject() uses @gateway location hints", async (t) => {
|
|
377
|
+
await t.step("in order, skipping invalid and duplicate hints", async () => {
|
|
378
|
+
const documentLoader = createLoader({ [`https://gw2.example${gatewayPath}`]: note() });
|
|
379
|
+
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({
|
|
380
|
+
documentLoader,
|
|
381
|
+
contextLoader: mockDocumentLoader,
|
|
382
|
+
verifyPortableObject: createVerifier()
|
|
383
|
+
}), Note);
|
|
384
|
+
deepStrictEqual(documentLoader.fetched, [`https://gw1.example${gatewayPath}`, `https://gw2.example${gatewayPath}`]);
|
|
385
|
+
});
|
|
386
|
+
await t.step("up to five hints", async () => {
|
|
387
|
+
const documentLoader = createLoader({});
|
|
388
|
+
const hints = [
|
|
389
|
+
1,
|
|
390
|
+
2,
|
|
391
|
+
3,
|
|
392
|
+
4,
|
|
393
|
+
5,
|
|
394
|
+
6,
|
|
395
|
+
7
|
|
396
|
+
].map((i) => `@gateway=https%3A%2F%2Fgw${i}.example`);
|
|
397
|
+
const activity = await createActivity(`${objectId}?${hints.join("&")}`);
|
|
398
|
+
await rejects(async () => await activity.getObject({
|
|
399
|
+
documentLoader,
|
|
400
|
+
contextLoader: mockDocumentLoader,
|
|
401
|
+
verifyPortableObject: createVerifier()
|
|
402
|
+
}));
|
|
403
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
404
|
+
1,
|
|
405
|
+
2,
|
|
406
|
+
3,
|
|
407
|
+
4,
|
|
408
|
+
5
|
|
409
|
+
].map((i) => `https://gw${i}.example${gatewayPath}`));
|
|
410
|
+
});
|
|
411
|
+
await t.step("replaced by explicit gateways", async () => {
|
|
412
|
+
const documentLoader = createLoader({ [`https://explicit.example${gatewayPath}`]: note() });
|
|
413
|
+
assertInstanceOf(await (await createActivity(`${objectId}?@gateway=https%3A%2F%2Fhint.example`)).getObject({
|
|
414
|
+
documentLoader,
|
|
415
|
+
contextLoader: mockDocumentLoader,
|
|
416
|
+
gateways: ["https://explicit.example"],
|
|
417
|
+
verifyPortableObject: createVerifier()
|
|
418
|
+
}), Note);
|
|
419
|
+
deepStrictEqual(documentLoader.fetched, [`https://explicit.example${gatewayPath}`]);
|
|
420
|
+
});
|
|
421
|
+
await t.step("disabled by an empty gateway list", async () => {
|
|
422
|
+
const documentLoader = createLoader({});
|
|
423
|
+
deepStrictEqual(await (await createActivity(`${objectId}?@gateway=https%3A%2F%2Fhint.example`)).getObject({
|
|
424
|
+
documentLoader,
|
|
425
|
+
contextLoader: mockDocumentLoader,
|
|
426
|
+
gateways: [],
|
|
427
|
+
verifyPortableObject: createVerifier(),
|
|
428
|
+
suppressError: true
|
|
429
|
+
}), null);
|
|
430
|
+
deepStrictEqual(documentLoader.fetched, [`ap+ef61://${did}/objects/1?@gateway=https%3A%2F%2Fhint.example`]);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
test("getObject() lets the document loader handle portable IRIs without gateways", async () => {
|
|
434
|
+
const documentLoader = createLoader({ [objectId]: note() });
|
|
435
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
436
|
+
documentLoader,
|
|
437
|
+
contextLoader: mockDocumentLoader,
|
|
438
|
+
verifyPortableObject: createVerifier()
|
|
439
|
+
}), Note);
|
|
440
|
+
deepStrictEqual(documentLoader.fetched, [objectId]);
|
|
441
|
+
const spoofing = createLoader({ [objectId]: note(`ap://${did}/objects/2`) });
|
|
442
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
443
|
+
documentLoader: spoofing,
|
|
444
|
+
contextLoader: mockDocumentLoader,
|
|
445
|
+
verifyPortableObject: createVerifier()
|
|
446
|
+
}), null);
|
|
447
|
+
deepStrictEqual(await (await createActivity()).getObject({
|
|
448
|
+
documentLoader,
|
|
449
|
+
contextLoader: mockDocumentLoader,
|
|
450
|
+
verifyPortableObject: createVerifier(false)
|
|
451
|
+
}), null);
|
|
452
|
+
});
|
|
453
|
+
test("getObject() shares context documents within a dereference", async (t) => {
|
|
454
|
+
const contextUrl = "https://example.com/portable-context";
|
|
455
|
+
const document = {
|
|
456
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
457
|
+
id: `ap://${did}/objects/1`,
|
|
458
|
+
type: "Note",
|
|
459
|
+
content: "Portable note"
|
|
460
|
+
};
|
|
461
|
+
await t.step("custom contexts are loaded once", async () => {
|
|
462
|
+
const loaded = [];
|
|
463
|
+
let version = 0;
|
|
464
|
+
const contextLoader = async (url) => {
|
|
465
|
+
loaded.push(url);
|
|
466
|
+
if (url === contextUrl) {
|
|
467
|
+
version++;
|
|
468
|
+
return {
|
|
469
|
+
contextUrl: null,
|
|
470
|
+
documentUrl: url,
|
|
471
|
+
document: { "@context": { version: `urn:version:${version}` } }
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return await mockDocumentLoader(url);
|
|
475
|
+
};
|
|
476
|
+
const contexts = [];
|
|
477
|
+
const verifyPortableObject = async (_, { contextLoader }) => {
|
|
478
|
+
contexts.push((await contextLoader(contextUrl)).document);
|
|
479
|
+
contexts.push((await contextLoader(contextUrl)).document);
|
|
480
|
+
return { verified: true };
|
|
481
|
+
};
|
|
482
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
483
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: document }),
|
|
484
|
+
contextLoader,
|
|
485
|
+
gateways: ["https://gw1.example"],
|
|
486
|
+
verifyPortableObject
|
|
487
|
+
}), Note);
|
|
488
|
+
deepStrictEqual(loaded.filter((url) => url === contextUrl).length, 1);
|
|
489
|
+
deepStrictEqual(contexts, [{ "@context": { version: "urn:version:1" } }, { "@context": { version: "urn:version:1" } }]);
|
|
490
|
+
});
|
|
491
|
+
await t.step("baseline contexts use built-in copies", async () => {
|
|
492
|
+
const loaded = [];
|
|
493
|
+
const contextLoader = async (url) => {
|
|
494
|
+
loaded.push(url);
|
|
495
|
+
return await mockDocumentLoader(url);
|
|
496
|
+
};
|
|
497
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
498
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: note() }),
|
|
499
|
+
contextLoader,
|
|
500
|
+
gateways: ["https://gw1.example"],
|
|
501
|
+
verifyPortableObject: createVerifier()
|
|
502
|
+
}), Note);
|
|
503
|
+
deepStrictEqual(loaded, []);
|
|
504
|
+
});
|
|
505
|
+
await t.step("the snapshot ends with the dereference", async () => {
|
|
506
|
+
const loaded = [];
|
|
507
|
+
const contextLoader = async (url) => {
|
|
508
|
+
loaded.push(url);
|
|
509
|
+
return await mockDocumentLoader(url);
|
|
510
|
+
};
|
|
511
|
+
const object = await (await createActivity()).getObject({
|
|
512
|
+
documentLoader: createLoader({ [`https://gw1.example${gatewayPath}`]: {
|
|
513
|
+
...note(),
|
|
514
|
+
attributedTo: "https://example.com/person"
|
|
515
|
+
} }),
|
|
516
|
+
contextLoader,
|
|
517
|
+
gateways: ["https://gw1.example"],
|
|
518
|
+
verifyPortableObject: createVerifier()
|
|
519
|
+
});
|
|
520
|
+
assertInstanceOf(object, Note);
|
|
521
|
+
deepStrictEqual(loaded, []);
|
|
522
|
+
deepStrictEqual((await object.getAttribution({ documentLoader: createLoader({ "https://example.com/person": {
|
|
523
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
524
|
+
id: "https://example.com/person",
|
|
525
|
+
type: "Person"
|
|
526
|
+
} }) }))?.id, new URL("https://example.com/person"));
|
|
527
|
+
ok(loaded.includes("https://www.w3.org/ns/activitystreams"));
|
|
528
|
+
});
|
|
529
|
+
await t.step("failed context loads are retried", async () => {
|
|
530
|
+
let failures = 1;
|
|
531
|
+
const contextLoader = async (url) => {
|
|
532
|
+
if (url === contextUrl && failures-- > 0) throw new TypeError("Temporary failure");
|
|
533
|
+
if (url === contextUrl) return {
|
|
534
|
+
contextUrl: null,
|
|
535
|
+
documentUrl: url,
|
|
536
|
+
document: { "@context": {} }
|
|
537
|
+
};
|
|
538
|
+
return await mockDocumentLoader(url);
|
|
539
|
+
};
|
|
540
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
541
|
+
documentLoader: createLoader({
|
|
542
|
+
[`https://gw1.example${gatewayPath}`]: document,
|
|
543
|
+
[`https://gw2.example${gatewayPath}`]: document
|
|
544
|
+
}),
|
|
545
|
+
contextLoader,
|
|
546
|
+
gateways: ["https://gw1.example", "https://gw2.example"],
|
|
547
|
+
verifyPortableObject: createVerifier()
|
|
548
|
+
}), Note);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
test("getObjects() dereferences each portable IRI separately", async () => {
|
|
552
|
+
const activity = await Create.fromJsonLd({
|
|
553
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
554
|
+
type: "Create",
|
|
555
|
+
id: `ap://${did}/activities/1`,
|
|
556
|
+
object: [objectId, `ap://${did}/objects/2`]
|
|
557
|
+
}, { contextLoader: mockDocumentLoader });
|
|
558
|
+
const documentLoader = createLoader({
|
|
559
|
+
[`https://gw.example${gatewayPath}`]: note(),
|
|
560
|
+
[`https://gw.example/.well-known/apgateway/${did}/objects/2`]: note()
|
|
561
|
+
});
|
|
562
|
+
const objects = await Array.fromAsync(activity.getObjects({
|
|
563
|
+
documentLoader,
|
|
564
|
+
contextLoader: mockDocumentLoader,
|
|
565
|
+
gateways: ["https://gw.example"],
|
|
566
|
+
verifyPortableObject: createVerifier()
|
|
567
|
+
}));
|
|
568
|
+
deepStrictEqual(objects.length, 1);
|
|
569
|
+
deepStrictEqual(objects[0].id?.href, "ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
570
|
+
});
|
|
571
|
+
test("getObject() ignores gateway options for HTTP(S) IRIs", async () => {
|
|
572
|
+
deepStrictEqual((await new Create({ object: new URL("https://example.com/object") }).getObject({
|
|
573
|
+
documentLoader: mockDocumentLoader,
|
|
574
|
+
contextLoader: mockDocumentLoader,
|
|
575
|
+
gateways: ["https://gw.example"]
|
|
576
|
+
}))?.id, new URL("https://example.com/object"));
|
|
577
|
+
});
|
|
578
|
+
test("getObject() sends the ActivityStreams Accept header to gateways", {
|
|
579
|
+
sanitizeResources: false,
|
|
580
|
+
sanitizeOps: false
|
|
581
|
+
}, async () => {
|
|
582
|
+
esm_default.spyGlobal();
|
|
583
|
+
let accept = null;
|
|
584
|
+
esm_default.get(`https://example.com${gatewayPath}`, (callLog) => {
|
|
585
|
+
accept = callLog.request?.headers.get("Accept") ?? new Headers(callLog.options.headers).get("Accept");
|
|
586
|
+
return {
|
|
587
|
+
headers: { "Content-Type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" },
|
|
588
|
+
body: note()
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
try {
|
|
592
|
+
assertInstanceOf(await (await createActivity()).getObject({
|
|
593
|
+
documentLoader: getDocumentLoader(),
|
|
594
|
+
contextLoader: mockDocumentLoader,
|
|
595
|
+
gateways: ["https://example.com"],
|
|
596
|
+
verifyPortableObject: createVerifier()
|
|
597
|
+
}), Note);
|
|
598
|
+
ok(accept != null && accept.includes("application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""), `Unexpected Accept header: ${accept}`);
|
|
599
|
+
} finally {
|
|
600
|
+
esm_default.hardReset();
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
//#endregion
|
|
604
|
+
export {};
|