@fedify/vocab 2.4.0-pr.936.41 → 2.5.0-dev.2271
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 +31412 -8807
- package/dist/mod.d.cts +4599 -6
- package/dist/mod.d.ts +4599 -6
- package/dist/mod.js +31305 -8707
- package/dist-tests/actor-NlmVfaZI.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/{actor-MBpzF3-D.mjs → esm-DxaxLS8O.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/lookup-BUL82PQy.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/signed-representation.test.mjs +189 -0
- package/dist-tests/translation.test.mjs +404 -0
- package/dist-tests/translation.yaml +91 -0
- package/dist-tests/type.test.mjs +2 -2
- package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-hNQYgnUG.mjs} +31068 -8752
- package/dist-tests/vocab.test.mjs +677 -10
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +291 -15
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/gateway.test.ts +2637 -0
- package/src/lookup.test.ts +6 -5
- package/src/lookup.ts +460 -33
- package/src/mod.ts +9 -0
- package/src/object.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +1 -1
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/vocab.test.ts +914 -5
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
2
|
+
import {
|
|
3
|
+
type DocumentLoader,
|
|
4
|
+
FetchError,
|
|
5
|
+
parseIri,
|
|
6
|
+
type PortableObjectVerifier,
|
|
7
|
+
type PortableObjectVerifierOptions,
|
|
8
|
+
type RemoteDocument,
|
|
9
|
+
} from "@fedify/vocab-runtime";
|
|
10
|
+
import fetchMock from "fetch-mock";
|
|
11
|
+
import { deepStrictEqual, equal, rejects } from "node:assert/strict";
|
|
12
|
+
import { getActorHandle } from "./actor.ts";
|
|
13
|
+
import { lookupObject } from "./lookup.ts";
|
|
14
|
+
import { assertInstanceOf } from "./utils.ts";
|
|
15
|
+
import { Person } from "./vocab.ts";
|
|
16
|
+
|
|
17
|
+
const did = "did:key:z6Mkabc";
|
|
18
|
+
const actorId = `ap+ef61://${did}/actor`;
|
|
19
|
+
const gatewayPath = `/.well-known/apgateway/${did}/actor`;
|
|
20
|
+
const compatibleId = `https://example.com${gatewayPath}`;
|
|
21
|
+
const webFingerPrefix = "begin:https://example.com/.well-known/webfinger";
|
|
22
|
+
const AS_TYPE = "application/activity+json";
|
|
23
|
+
|
|
24
|
+
function person(
|
|
25
|
+
id: string = `ap://${did}/actor`,
|
|
26
|
+
extra: Record<string, unknown> = {},
|
|
27
|
+
): Record<string, unknown> {
|
|
28
|
+
return {
|
|
29
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
30
|
+
id,
|
|
31
|
+
type: "Person",
|
|
32
|
+
name: "Alice",
|
|
33
|
+
...extra,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createLoader(
|
|
38
|
+
responses: Record<string, Record<string, unknown>>,
|
|
39
|
+
): DocumentLoader & { readonly fetched: string[] } {
|
|
40
|
+
const fetched: string[] = [];
|
|
41
|
+
// deno-lint-ignore require-await
|
|
42
|
+
const loader = async (
|
|
43
|
+
url: string,
|
|
44
|
+
options?: { signal?: AbortSignal },
|
|
45
|
+
): Promise<RemoteDocument> => {
|
|
46
|
+
fetched.push(url);
|
|
47
|
+
options?.signal?.throwIfAborted();
|
|
48
|
+
const document = responses[url];
|
|
49
|
+
if (document == null) {
|
|
50
|
+
throw new FetchError(
|
|
51
|
+
url,
|
|
52
|
+
"HTTP 404",
|
|
53
|
+
new globalThis.Response(null, { status: 404 }),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return { contextUrl: null, documentUrl: url, document };
|
|
57
|
+
};
|
|
58
|
+
return Object.assign(loader, { fetched });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createVerifier(
|
|
62
|
+
verified = true,
|
|
63
|
+
): PortableObjectVerifier & { readonly documents: unknown[] } {
|
|
64
|
+
const documents: unknown[] = [];
|
|
65
|
+
// deno-lint-ignore require-await
|
|
66
|
+
const verifier = async (document: unknown) => {
|
|
67
|
+
documents.push(document);
|
|
68
|
+
return { verified };
|
|
69
|
+
};
|
|
70
|
+
return Object.assign(verifier, { documents });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function selfLinks(...hrefs: string[]) {
|
|
74
|
+
return {
|
|
75
|
+
subject: "acct:alice@example.com",
|
|
76
|
+
links: hrefs.map((href) => ({ rel: "self", type: AS_TYPE, href })),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test("lookupObject() with FEP-ef61 portable actors", {
|
|
81
|
+
sanitizeResources: false,
|
|
82
|
+
sanitizeOps: false,
|
|
83
|
+
}, async (t) => {
|
|
84
|
+
fetchMock.spyGlobal();
|
|
85
|
+
try {
|
|
86
|
+
await t.step("compatible self link, alias-free JRD", async () => {
|
|
87
|
+
fetchMock.removeRoutes();
|
|
88
|
+
fetchMock.get(webFingerPrefix, selfLinks(compatibleId));
|
|
89
|
+
const document = person();
|
|
90
|
+
const documentLoader = createLoader({ [compatibleId]: document });
|
|
91
|
+
const verifyPortableObject = createVerifier();
|
|
92
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
93
|
+
documentLoader,
|
|
94
|
+
contextLoader: mockDocumentLoader,
|
|
95
|
+
verifyPortableObject,
|
|
96
|
+
});
|
|
97
|
+
assertInstanceOf(actor, Person);
|
|
98
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
99
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
100
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await t.step(
|
|
104
|
+
"canonical self link asks the WebFinger host first",
|
|
105
|
+
async () => {
|
|
106
|
+
fetchMock.removeRoutes();
|
|
107
|
+
fetchMock.get(webFingerPrefix, {
|
|
108
|
+
subject: "acct:alice@example.com",
|
|
109
|
+
links: [{
|
|
110
|
+
rel: "self",
|
|
111
|
+
type:
|
|
112
|
+
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
113
|
+
href: `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`,
|
|
114
|
+
}],
|
|
115
|
+
});
|
|
116
|
+
const hinted = `https://hint.example${gatewayPath}`;
|
|
117
|
+
const documentLoader = createLoader({ [hinted]: person() });
|
|
118
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
119
|
+
documentLoader,
|
|
120
|
+
contextLoader: mockDocumentLoader,
|
|
121
|
+
verifyPortableObject: createVerifier(),
|
|
122
|
+
});
|
|
123
|
+
assertInstanceOf(actor, Person);
|
|
124
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, hinted]);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await t.step("explicit gateways replace WebFinger hints", async () => {
|
|
129
|
+
fetchMock.removeRoutes();
|
|
130
|
+
fetchMock.get(
|
|
131
|
+
webFingerPrefix,
|
|
132
|
+
selfLinks(
|
|
133
|
+
`ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
const second = `https://second.example${gatewayPath}`;
|
|
137
|
+
const documentLoader = createLoader({ [second]: person() });
|
|
138
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
139
|
+
documentLoader,
|
|
140
|
+
contextLoader: mockDocumentLoader,
|
|
141
|
+
verifyPortableObject: createVerifier(),
|
|
142
|
+
gateways: ["https://second.example"],
|
|
143
|
+
});
|
|
144
|
+
assertInstanceOf(actor, Person);
|
|
145
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await t.step("WebFinger host counts toward the five attempts", async () => {
|
|
149
|
+
fetchMock.removeRoutes();
|
|
150
|
+
fetchMock.get(webFingerPrefix, selfLinks(`ap://${did}/actor`));
|
|
151
|
+
const documentLoader = createLoader({});
|
|
152
|
+
equal(
|
|
153
|
+
await lookupObject("@alice@example.com", {
|
|
154
|
+
documentLoader,
|
|
155
|
+
contextLoader: mockDocumentLoader,
|
|
156
|
+
verifyPortableObject: createVerifier(),
|
|
157
|
+
gateways: Array.from(
|
|
158
|
+
{ length: 6 },
|
|
159
|
+
(_, i) => `https://g${i}.example`,
|
|
160
|
+
),
|
|
161
|
+
}),
|
|
162
|
+
null,
|
|
163
|
+
);
|
|
164
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
165
|
+
compatibleId,
|
|
166
|
+
...[0, 1, 2, 3].map((i) => `https://g${i}.example${gatewayPath}`),
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await t.step("tries the next self link after a failure", async () => {
|
|
171
|
+
fetchMock.removeRoutes();
|
|
172
|
+
const other = `https://other.example${gatewayPath}`;
|
|
173
|
+
fetchMock.get(
|
|
174
|
+
webFingerPrefix,
|
|
175
|
+
selfLinks(`ap://${did}/actor`, compatibleId, other),
|
|
176
|
+
);
|
|
177
|
+
const documentLoader = createLoader({ [other]: person() });
|
|
178
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
179
|
+
documentLoader,
|
|
180
|
+
contextLoader: mockDocumentLoader,
|
|
181
|
+
verifyPortableObject: createVerifier(),
|
|
182
|
+
});
|
|
183
|
+
assertInstanceOf(actor, Person);
|
|
184
|
+
// The same gateway is not asked twice for the same portable ID:
|
|
185
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, other]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await t.step("accepts a compatible identifier as the ID", async () => {
|
|
189
|
+
// Some publishers, e.g., tootik, identify their portable actors by
|
|
190
|
+
// compatible identifiers; the document is verified as the portable
|
|
191
|
+
// object, and keeps its own ID:
|
|
192
|
+
fetchMock.removeRoutes();
|
|
193
|
+
fetchMock.get(webFingerPrefix, selfLinks(compatibleId));
|
|
194
|
+
const document = person(compatibleId);
|
|
195
|
+
const documentLoader = createLoader({ [compatibleId]: document });
|
|
196
|
+
const verifyPortableObject = createVerifier();
|
|
197
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
198
|
+
documentLoader,
|
|
199
|
+
contextLoader: mockDocumentLoader,
|
|
200
|
+
verifyPortableObject,
|
|
201
|
+
});
|
|
202
|
+
assertInstanceOf(actor, Person);
|
|
203
|
+
deepStrictEqual(actor.id, new URL(compatibleId));
|
|
204
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await t.step("rejects objects that fail the checks", async () => {
|
|
208
|
+
fetchMock.removeRoutes();
|
|
209
|
+
fetchMock.get(webFingerPrefix, selfLinks(compatibleId));
|
|
210
|
+
for (
|
|
211
|
+
const [document, verified] of [
|
|
212
|
+
[person(), false],
|
|
213
|
+
// Same DID, but another object:
|
|
214
|
+
[person(`ap://${did}/other`), true],
|
|
215
|
+
// A compatible identifier of another object:
|
|
216
|
+
[
|
|
217
|
+
person(`https://example.com/.well-known/apgateway/${did}/other`),
|
|
218
|
+
true,
|
|
219
|
+
],
|
|
220
|
+
// A compatible identifier of the object, but unverified:
|
|
221
|
+
[person(compatibleId), false],
|
|
222
|
+
] as const
|
|
223
|
+
) {
|
|
224
|
+
const documentLoader = createLoader({ [compatibleId]: document });
|
|
225
|
+
const verifyPortableObject = createVerifier(verified);
|
|
226
|
+
equal(
|
|
227
|
+
await lookupObject("@alice@example.com", {
|
|
228
|
+
documentLoader,
|
|
229
|
+
contextLoader: mockDocumentLoader,
|
|
230
|
+
verifyPortableObject,
|
|
231
|
+
}),
|
|
232
|
+
null,
|
|
233
|
+
);
|
|
234
|
+
// No fallback to fetching the compatible identifier as an ordinary
|
|
235
|
+
// HTTPS URL:
|
|
236
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
237
|
+
await rejects(
|
|
238
|
+
() =>
|
|
239
|
+
lookupObject("@alice@example.com", {
|
|
240
|
+
documentLoader,
|
|
241
|
+
contextLoader: mockDocumentLoader,
|
|
242
|
+
verifyPortableObject,
|
|
243
|
+
crossOrigin: "throw",
|
|
244
|
+
}),
|
|
245
|
+
{ name: "PortableObjectRejectedError" },
|
|
246
|
+
);
|
|
247
|
+
equal(
|
|
248
|
+
await lookupObject("@alice@example.com", {
|
|
249
|
+
documentLoader,
|
|
250
|
+
contextLoader: mockDocumentLoader,
|
|
251
|
+
verifyPortableObject,
|
|
252
|
+
crossOrigin: "trust",
|
|
253
|
+
}),
|
|
254
|
+
null,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await t.step("without verifyPortableObject", async () => {
|
|
260
|
+
fetchMock.removeRoutes();
|
|
261
|
+
fetchMock.get(
|
|
262
|
+
webFingerPrefix,
|
|
263
|
+
selfLinks(`ap://${did}/actor`, compatibleId),
|
|
264
|
+
);
|
|
265
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
266
|
+
for (const crossOrigin of ["ignore", "trust"] as const) {
|
|
267
|
+
// The canonical link is skipped, and the compatible identifier is
|
|
268
|
+
// fetched as an ordinary URL, but its portable object is refused:
|
|
269
|
+
equal(
|
|
270
|
+
await lookupObject("@alice@example.com", {
|
|
271
|
+
documentLoader,
|
|
272
|
+
contextLoader: mockDocumentLoader,
|
|
273
|
+
crossOrigin,
|
|
274
|
+
}),
|
|
275
|
+
null,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, compatibleId]);
|
|
279
|
+
await rejects(
|
|
280
|
+
() =>
|
|
281
|
+
lookupObject("@alice@example.com", {
|
|
282
|
+
documentLoader,
|
|
283
|
+
contextLoader: mockDocumentLoader,
|
|
284
|
+
crossOrigin: "throw",
|
|
285
|
+
}),
|
|
286
|
+
/has a different origin than the document URL/,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// An object with an ordinary ID served at a compatible identifier
|
|
290
|
+
// keeps working as before:
|
|
291
|
+
const httpLoader = createLoader({ [compatibleId]: person(compatibleId) });
|
|
292
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
293
|
+
documentLoader: httpLoader,
|
|
294
|
+
contextLoader: mockDocumentLoader,
|
|
295
|
+
});
|
|
296
|
+
assertInstanceOf(actor, Person);
|
|
297
|
+
deepStrictEqual(actor.id, new URL(compatibleId));
|
|
298
|
+
|
|
299
|
+
const malformed = `${compatibleId}?gateways=x`;
|
|
300
|
+
const ordinaryLoader = createLoader({
|
|
301
|
+
[malformed]: person(malformed),
|
|
302
|
+
});
|
|
303
|
+
const ordinary = await lookupObject(malformed, {
|
|
304
|
+
documentLoader: ordinaryLoader,
|
|
305
|
+
contextLoader: mockDocumentLoader,
|
|
306
|
+
});
|
|
307
|
+
assertInstanceOf(ordinary, Person);
|
|
308
|
+
deepStrictEqual(ordinaryLoader.fetched, [malformed]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await t.step("refuses portable IRIs changed by URL parsing", async () => {
|
|
312
|
+
fetchMock.removeRoutes();
|
|
313
|
+
fetchMock.get(
|
|
314
|
+
webFingerPrefix,
|
|
315
|
+
selfLinks(
|
|
316
|
+
`ap://${did}/x/../actor`,
|
|
317
|
+
`ap://${did}/x/%2e%2e/actor`,
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
321
|
+
const options = {
|
|
322
|
+
documentLoader,
|
|
323
|
+
contextLoader: mockDocumentLoader,
|
|
324
|
+
verifyPortableObject: createVerifier(),
|
|
325
|
+
};
|
|
326
|
+
equal(await lookupObject("@alice@example.com", options), null);
|
|
327
|
+
equal(
|
|
328
|
+
await lookupObject(
|
|
329
|
+
`ap://${did}/x/../actor?@gateway=https%3A%2F%2Fexample.com`,
|
|
330
|
+
options,
|
|
331
|
+
),
|
|
332
|
+
null,
|
|
333
|
+
);
|
|
334
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await t.step("refuses compatible IDs changed by URL parsing", async () => {
|
|
338
|
+
fetchMock.removeRoutes();
|
|
339
|
+
const dotted = compatibleId.replace("/actor", "/x/../actor");
|
|
340
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
341
|
+
const options = {
|
|
342
|
+
documentLoader,
|
|
343
|
+
contextLoader: mockDocumentLoader,
|
|
344
|
+
verifyPortableObject: createVerifier(),
|
|
345
|
+
};
|
|
346
|
+
equal(await lookupObject(dotted, options), null);
|
|
347
|
+
fetchMock.get(webFingerPrefix, selfLinks(dotted));
|
|
348
|
+
equal(await lookupObject("@alice@example.com", options), null);
|
|
349
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await t.step("bounds gateway requests", async () => {
|
|
353
|
+
fetchMock.removeRoutes();
|
|
354
|
+
const hints = Array.from(
|
|
355
|
+
{ length: 5 },
|
|
356
|
+
(_, i) => `@gateway=https%3A%2F%2Fg${i}.example`,
|
|
357
|
+
).join("&");
|
|
358
|
+
fetchMock.get(
|
|
359
|
+
webFingerPrefix,
|
|
360
|
+
selfLinks(
|
|
361
|
+
`ap://${did}/actor?${hints}`,
|
|
362
|
+
`ap://${did}/actor2?${hints}`,
|
|
363
|
+
`https://g9.example/.well-known/apgateway/${did}/actor`,
|
|
364
|
+
),
|
|
365
|
+
);
|
|
366
|
+
const documentLoader = createLoader({});
|
|
367
|
+
equal(
|
|
368
|
+
await lookupObject("@alice@example.com", {
|
|
369
|
+
documentLoader,
|
|
370
|
+
contextLoader: mockDocumentLoader,
|
|
371
|
+
verifyPortableObject: createVerifier(),
|
|
372
|
+
}),
|
|
373
|
+
null,
|
|
374
|
+
);
|
|
375
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
376
|
+
compatibleId,
|
|
377
|
+
...[0, 1, 2, 3].map((i) => `https://g${i}.example${gatewayPath}`),
|
|
378
|
+
]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await t.step("cancellation", async () => {
|
|
382
|
+
fetchMock.removeRoutes();
|
|
383
|
+
fetchMock.get(
|
|
384
|
+
webFingerPrefix,
|
|
385
|
+
selfLinks(
|
|
386
|
+
`ap://${did}/actor`,
|
|
387
|
+
`https://other.example${gatewayPath}`,
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
const controller = new AbortController();
|
|
391
|
+
const verifyPortableObject: PortableObjectVerifier = () => {
|
|
392
|
+
controller.abort();
|
|
393
|
+
return Promise.resolve({ verified: true });
|
|
394
|
+
};
|
|
395
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
396
|
+
equal(
|
|
397
|
+
await lookupObject("@alice@example.com", {
|
|
398
|
+
documentLoader,
|
|
399
|
+
contextLoader: mockDocumentLoader,
|
|
400
|
+
verifyPortableObject,
|
|
401
|
+
signal: controller.signal,
|
|
402
|
+
}),
|
|
403
|
+
null,
|
|
404
|
+
);
|
|
405
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
406
|
+
});
|
|
407
|
+
} finally {
|
|
408
|
+
fetchMock.removeRoutes();
|
|
409
|
+
fetchMock.hardReset();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("lookupObject() looks up portable identifiers directly", async () => {
|
|
414
|
+
const verifyPortableObject = createVerifier();
|
|
415
|
+
const hinted = `https://gw.example${gatewayPath}`;
|
|
416
|
+
const documentLoader = createLoader({
|
|
417
|
+
[hinted]: person(),
|
|
418
|
+
[`https://example.com${gatewayPath}`]: person(),
|
|
419
|
+
[actorId]: person(),
|
|
420
|
+
});
|
|
421
|
+
const options = {
|
|
422
|
+
documentLoader,
|
|
423
|
+
contextLoader: mockDocumentLoader,
|
|
424
|
+
verifyPortableObject,
|
|
425
|
+
};
|
|
426
|
+
for (
|
|
427
|
+
const identifier of [
|
|
428
|
+
`ap://${did}/actor?@gateway=https%3A%2F%2Fgw.example`,
|
|
429
|
+
compatibleId,
|
|
430
|
+
new URL(compatibleId),
|
|
431
|
+
// Without hints, the document loader gets the portable ID itself:
|
|
432
|
+
`ap://${did}/actor`,
|
|
433
|
+
parseIri(`ap://${did}/actor`),
|
|
434
|
+
]
|
|
435
|
+
) {
|
|
436
|
+
const actor = await lookupObject(identifier, options);
|
|
437
|
+
assertInstanceOf(actor, Person);
|
|
438
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
439
|
+
}
|
|
440
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
441
|
+
hinted,
|
|
442
|
+
compatibleId,
|
|
443
|
+
compatibleId,
|
|
444
|
+
actorId,
|
|
445
|
+
actorId,
|
|
446
|
+
]);
|
|
447
|
+
// Portable identifiers are not looked up without a verifier:
|
|
448
|
+
equal(
|
|
449
|
+
await lookupObject(`ap://${did}/actor`, {
|
|
450
|
+
documentLoader,
|
|
451
|
+
contextLoader: mockDocumentLoader,
|
|
452
|
+
}),
|
|
453
|
+
null,
|
|
454
|
+
);
|
|
455
|
+
deepStrictEqual(documentLoader.fetched.length, 5);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("lookupObject() uses explicit gateways for portable IDs", async (t) => {
|
|
459
|
+
const first = `https://first.example${gatewayPath}`;
|
|
460
|
+
const second = `https://second.example${gatewayPath}`;
|
|
461
|
+
const hint = `https://hint.example${gatewayPath}`;
|
|
462
|
+
const options = {
|
|
463
|
+
contextLoader: mockDocumentLoader,
|
|
464
|
+
verifyPortableObject: createVerifier(),
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
await t.step("bare ap: ID and one gateway", async () => {
|
|
468
|
+
const documentLoader = createLoader({ [first]: person() });
|
|
469
|
+
assertInstanceOf(
|
|
470
|
+
await lookupObject(`ap://${did}/actor`, {
|
|
471
|
+
...options,
|
|
472
|
+
documentLoader,
|
|
473
|
+
gateways: ["https://first.example"],
|
|
474
|
+
}),
|
|
475
|
+
Person,
|
|
476
|
+
);
|
|
477
|
+
deepStrictEqual(documentLoader.fetched, [first]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await t.step("tries several gateways in order", async () => {
|
|
481
|
+
const documentLoader = createLoader({ [second]: person() });
|
|
482
|
+
assertInstanceOf(
|
|
483
|
+
await lookupObject(actorId, {
|
|
484
|
+
...options,
|
|
485
|
+
documentLoader,
|
|
486
|
+
gateways: ["https://first.example", new URL("https://second.example")],
|
|
487
|
+
}),
|
|
488
|
+
Person,
|
|
489
|
+
);
|
|
490
|
+
deepStrictEqual(documentLoader.fetched, [first, second]);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
await t.step("explicit gateways replace location hints", async () => {
|
|
494
|
+
const documentLoader = createLoader({
|
|
495
|
+
[second]: person(),
|
|
496
|
+
[hint]: person(),
|
|
497
|
+
});
|
|
498
|
+
assertInstanceOf(
|
|
499
|
+
await lookupObject(
|
|
500
|
+
`ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`,
|
|
501
|
+
{
|
|
502
|
+
...options,
|
|
503
|
+
documentLoader,
|
|
504
|
+
gateways: ["https://first.example", "https://second.example"],
|
|
505
|
+
},
|
|
506
|
+
),
|
|
507
|
+
Person,
|
|
508
|
+
);
|
|
509
|
+
deepStrictEqual(documentLoader.fetched, [first, second]);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
await t.step("empty list uses a custom document loader", async () => {
|
|
513
|
+
const hintedId = `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`;
|
|
514
|
+
const canonicalHintedId = `${actorId}?@gateway=https%3A%2F%2Fhint.example`;
|
|
515
|
+
const documentLoader = createLoader({
|
|
516
|
+
[canonicalHintedId]: person(),
|
|
517
|
+
[hint]: person(),
|
|
518
|
+
});
|
|
519
|
+
const actor = await lookupObject(hintedId, {
|
|
520
|
+
...options,
|
|
521
|
+
documentLoader,
|
|
522
|
+
gateways: [],
|
|
523
|
+
});
|
|
524
|
+
deepStrictEqual(documentLoader.fetched, [canonicalHintedId]);
|
|
525
|
+
assertInstanceOf(actor, Person);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
await t.step("invalid gateway throws before fetching", async () => {
|
|
529
|
+
const documentLoader = createLoader({});
|
|
530
|
+
for (const verifyPortableObject of [undefined, createVerifier()]) {
|
|
531
|
+
await rejects(
|
|
532
|
+
() =>
|
|
533
|
+
lookupObject(actorId, {
|
|
534
|
+
documentLoader,
|
|
535
|
+
contextLoader: mockDocumentLoader,
|
|
536
|
+
verifyPortableObject,
|
|
537
|
+
gateways: ["https://bad.example/path"],
|
|
538
|
+
}),
|
|
539
|
+
TypeError,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await t.step("a malformed portable ID is refused first", async () => {
|
|
546
|
+
const documentLoader = createLoader({});
|
|
547
|
+
equal(
|
|
548
|
+
await lookupObject(`ap://${did}/x/../actor`, {
|
|
549
|
+
...options,
|
|
550
|
+
documentLoader,
|
|
551
|
+
gateways: ["https://bad.example/path"],
|
|
552
|
+
}),
|
|
553
|
+
null,
|
|
554
|
+
);
|
|
555
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("getActorHandle() with FEP-ef61 portable actors", {
|
|
560
|
+
sanitizeResources: false,
|
|
561
|
+
sanitizeOps: false,
|
|
562
|
+
}, async (t) => {
|
|
563
|
+
const actor = new Person({
|
|
564
|
+
id: parseIri(actorId),
|
|
565
|
+
preferredUsername: "alice",
|
|
566
|
+
gateways: [new URL("https://example.com"), new URL("https://example.org")],
|
|
567
|
+
});
|
|
568
|
+
fetchMock.spyGlobal();
|
|
569
|
+
try {
|
|
570
|
+
await t.step("verified through the first gateway", async () => {
|
|
571
|
+
fetchMock.removeRoutes();
|
|
572
|
+
fetchMock.get(
|
|
573
|
+
"https://example.com/.well-known/webfinger?resource=acct%3Aalice%40example.com",
|
|
574
|
+
selfLinks(compatibleId),
|
|
575
|
+
);
|
|
576
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
await t.step("canonical self link", async () => {
|
|
580
|
+
fetchMock.removeRoutes();
|
|
581
|
+
fetchMock.get(webFingerPrefix, selfLinks(`ap://${did}/actor`));
|
|
582
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
await t.step("follows a verified subject", async () => {
|
|
586
|
+
fetchMock.removeRoutes();
|
|
587
|
+
fetchMock.get(webFingerPrefix, {
|
|
588
|
+
...selfLinks(compatibleId),
|
|
589
|
+
subject: "acct:alice@example.org",
|
|
590
|
+
});
|
|
591
|
+
fetchMock.get(
|
|
592
|
+
"begin:https://example.org/.well-known/webfinger",
|
|
593
|
+
{ ...selfLinks(compatibleId), subject: "acct:alice@example.org" },
|
|
594
|
+
);
|
|
595
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.org");
|
|
596
|
+
|
|
597
|
+
// An unverified subject is ignored:
|
|
598
|
+
fetchMock.removeRoutes();
|
|
599
|
+
fetchMock.get(webFingerPrefix, {
|
|
600
|
+
...selfLinks(compatibleId),
|
|
601
|
+
subject: "acct:alice@example.org",
|
|
602
|
+
});
|
|
603
|
+
fetchMock.get(
|
|
604
|
+
"begin:https://example.org/.well-known/webfinger",
|
|
605
|
+
selfLinks(`ap://${did}/other`),
|
|
606
|
+
);
|
|
607
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
await t.step("rejects a WebFinger response for another actor", async () => {
|
|
611
|
+
for (
|
|
612
|
+
const jrd of [
|
|
613
|
+
selfLinks(`https://example.com/.well-known/apgateway/${did}/other`),
|
|
614
|
+
selfLinks("https://example.com/users/alice"),
|
|
615
|
+
{ subject: "acct:alice@example.com", links: [] },
|
|
616
|
+
]
|
|
617
|
+
) {
|
|
618
|
+
fetchMock.removeRoutes();
|
|
619
|
+
fetchMock.get(webFingerPrefix, jrd);
|
|
620
|
+
await rejects(() => getActorHandle(actor), TypeError);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
await t.step("does not fall back when WebFinger fails", async () => {
|
|
625
|
+
fetchMock.removeRoutes();
|
|
626
|
+
fetchMock.get(webFingerPrefix, 404);
|
|
627
|
+
await rejects(() => getActorHandle(actor), TypeError);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
await t.step("not enough information", async () => {
|
|
631
|
+
fetchMock.removeRoutes();
|
|
632
|
+
fetchMock.clearHistory();
|
|
633
|
+
for (
|
|
634
|
+
const target of [
|
|
635
|
+
parseIri(actorId),
|
|
636
|
+
new Person({ id: parseIri(actorId), preferredUsername: "alice" }),
|
|
637
|
+
new Person({
|
|
638
|
+
id: parseIri(actorId),
|
|
639
|
+
gateways: [new URL("https://example.com")],
|
|
640
|
+
}),
|
|
641
|
+
]
|
|
642
|
+
) {
|
|
643
|
+
await rejects(() => getActorHandle(target), TypeError);
|
|
644
|
+
}
|
|
645
|
+
deepStrictEqual(fetchMock.callHistory.calls().length, 0);
|
|
646
|
+
});
|
|
647
|
+
} finally {
|
|
648
|
+
fetchMock.removeRoutes();
|
|
649
|
+
fetchMock.hardReset();
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("lookupObject() reports inferred gateways as hints", async () => {
|
|
654
|
+
const calls: PortableObjectVerifierOptions[] = [];
|
|
655
|
+
// deno-lint-ignore require-await
|
|
656
|
+
const verifyPortableObject: PortableObjectVerifier = async (_, options) => {
|
|
657
|
+
calls.push(options);
|
|
658
|
+
return { verified: true };
|
|
659
|
+
};
|
|
660
|
+
const actor = await lookupObject(compatibleId, {
|
|
661
|
+
documentLoader: createLoader({ [compatibleId]: person() }),
|
|
662
|
+
contextLoader: mockDocumentLoader,
|
|
663
|
+
verifyPortableObject,
|
|
664
|
+
});
|
|
665
|
+
assertInstanceOf(actor, Person);
|
|
666
|
+
deepStrictEqual(calls.length, 1);
|
|
667
|
+
deepStrictEqual(calls[0].gateways, undefined);
|
|
668
|
+
deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("lookupObject() reports explicit gateways to the verifier", async (t) => {
|
|
672
|
+
const calls: PortableObjectVerifierOptions[] = [];
|
|
673
|
+
// deno-lint-ignore require-await
|
|
674
|
+
const verifyPortableObject: PortableObjectVerifier = async (_, options) => {
|
|
675
|
+
calls.push(options);
|
|
676
|
+
return { verified: true };
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
await t.step("bare ID with a limited explicit list", async () => {
|
|
680
|
+
const fifth = `https://g4.example${gatewayPath}`;
|
|
681
|
+
const documentLoader = createLoader({ [fifth]: person() });
|
|
682
|
+
assertInstanceOf(
|
|
683
|
+
await lookupObject(actorId, {
|
|
684
|
+
documentLoader,
|
|
685
|
+
contextLoader: mockDocumentLoader,
|
|
686
|
+
verifyPortableObject,
|
|
687
|
+
gateways: Array.from({ length: 6 }, (_, i) => `https://g${i}.example`),
|
|
688
|
+
}),
|
|
689
|
+
Person,
|
|
690
|
+
);
|
|
691
|
+
deepStrictEqual(
|
|
692
|
+
documentLoader.fetched,
|
|
693
|
+
[0, 1, 2, 3, 4].map((i) => `https://g${i}.example${gatewayPath}`),
|
|
694
|
+
);
|
|
695
|
+
deepStrictEqual(
|
|
696
|
+
calls.at(-1)?.gateways,
|
|
697
|
+
[0, 1, 2, 3, 4].map((i) => new URL(`https://g${i}.example`)),
|
|
698
|
+
);
|
|
699
|
+
equal(calls.at(-1)?.gatewayHints, undefined);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
await t.step(
|
|
703
|
+
"compatible origin stays first and is deduplicated",
|
|
704
|
+
async () => {
|
|
705
|
+
const second = `https://second.example${gatewayPath}`;
|
|
706
|
+
const documentLoader = createLoader({ [second]: person() });
|
|
707
|
+
assertInstanceOf(
|
|
708
|
+
await lookupObject(compatibleId, {
|
|
709
|
+
documentLoader,
|
|
710
|
+
contextLoader: mockDocumentLoader,
|
|
711
|
+
verifyPortableObject,
|
|
712
|
+
gateways: ["https://example.com", "https://second.example"],
|
|
713
|
+
}),
|
|
714
|
+
Person,
|
|
715
|
+
);
|
|
716
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
|
|
717
|
+
deepStrictEqual(calls.at(-1)?.gateways, [
|
|
718
|
+
new URL("https://example.com"),
|
|
719
|
+
new URL("https://second.example"),
|
|
720
|
+
]);
|
|
721
|
+
equal(calls.at(-1)?.gatewayHints, undefined);
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
await t.step("empty list keeps the compatible origin", async () => {
|
|
726
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
727
|
+
assertInstanceOf(
|
|
728
|
+
await lookupObject(compatibleId, {
|
|
729
|
+
documentLoader,
|
|
730
|
+
contextLoader: mockDocumentLoader,
|
|
731
|
+
verifyPortableObject,
|
|
732
|
+
gateways: [],
|
|
733
|
+
}),
|
|
734
|
+
Person,
|
|
735
|
+
);
|
|
736
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
737
|
+
deepStrictEqual(calls.at(-1)?.gateways, []);
|
|
738
|
+
equal(calls.at(-1)?.gatewayHints, undefined);
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("lookupObject() verifies fetched documents that stand for portable objects", async (t) => {
|
|
743
|
+
const plainUrl = "https://example.com/users/alice";
|
|
744
|
+
const redirectingLoader = (
|
|
745
|
+
documentUrl: string,
|
|
746
|
+
document: Record<string, unknown>,
|
|
747
|
+
): DocumentLoader & { readonly fetched: string[] } => {
|
|
748
|
+
const fetched: string[] = [];
|
|
749
|
+
// deno-lint-ignore require-await
|
|
750
|
+
const loader = async (url: string): Promise<RemoteDocument> => {
|
|
751
|
+
fetched.push(url);
|
|
752
|
+
if (url !== plainUrl) {
|
|
753
|
+
throw new FetchError(
|
|
754
|
+
url,
|
|
755
|
+
"HTTP 404",
|
|
756
|
+
new globalThis.Response(null, { status: 404 }),
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return { contextUrl: null, documentUrl, document };
|
|
760
|
+
};
|
|
761
|
+
return Object.assign(loader, { fetched });
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
await t.step("a redirect to a compatible identifier", async () => {
|
|
765
|
+
const calls: PortableObjectVerifierOptions[] = [];
|
|
766
|
+
const documentLoader = redirectingLoader(compatibleId, person());
|
|
767
|
+
const actor = await lookupObject(plainUrl, {
|
|
768
|
+
documentLoader,
|
|
769
|
+
contextLoader: mockDocumentLoader,
|
|
770
|
+
// deno-lint-ignore require-await
|
|
771
|
+
verifyPortableObject: async (_, options) => {
|
|
772
|
+
calls.push(options);
|
|
773
|
+
return { verified: true };
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
assertInstanceOf(actor, Person);
|
|
777
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
778
|
+
// Verified in place, without another request:
|
|
779
|
+
deepStrictEqual(documentLoader.fetched, [plainUrl]);
|
|
780
|
+
deepStrictEqual(calls.length, 1);
|
|
781
|
+
deepStrictEqual(calls[0].documentUrl, new URL(compatibleId));
|
|
782
|
+
deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
|
|
783
|
+
const explicitCalls: PortableObjectVerifierOptions[] = [];
|
|
784
|
+
const explicitActor = await lookupObject(plainUrl, {
|
|
785
|
+
documentLoader,
|
|
786
|
+
contextLoader: mockDocumentLoader,
|
|
787
|
+
gateways: ["https://other.example"],
|
|
788
|
+
// deno-lint-ignore require-await
|
|
789
|
+
verifyPortableObject: async (_, options) => {
|
|
790
|
+
explicitCalls.push(options);
|
|
791
|
+
return { verified: true };
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
assertInstanceOf(explicitActor, Person);
|
|
795
|
+
deepStrictEqual(explicitCalls[0].gateways, [
|
|
796
|
+
new URL("https://other.example"),
|
|
797
|
+
]);
|
|
798
|
+
equal(explicitCalls[0].gatewayHints, undefined);
|
|
799
|
+
await rejects(
|
|
800
|
+
() =>
|
|
801
|
+
lookupObject(plainUrl, {
|
|
802
|
+
documentLoader,
|
|
803
|
+
contextLoader: mockDocumentLoader,
|
|
804
|
+
verifyPortableObject: createVerifier(),
|
|
805
|
+
gateways: ["https://bad.example/path"],
|
|
806
|
+
}),
|
|
807
|
+
TypeError,
|
|
808
|
+
);
|
|
809
|
+
equal(
|
|
810
|
+
await lookupObject(plainUrl, {
|
|
811
|
+
documentLoader,
|
|
812
|
+
contextLoader: mockDocumentLoader,
|
|
813
|
+
verifyPortableObject: createVerifier(false),
|
|
814
|
+
}),
|
|
815
|
+
null,
|
|
816
|
+
);
|
|
817
|
+
// A document that claims another object:
|
|
818
|
+
equal(
|
|
819
|
+
await lookupObject(plainUrl, {
|
|
820
|
+
documentLoader: redirectingLoader(
|
|
821
|
+
compatibleId,
|
|
822
|
+
person(`ap://${did}/other`),
|
|
823
|
+
),
|
|
824
|
+
contextLoader: mockDocumentLoader,
|
|
825
|
+
verifyPortableObject: createVerifier(),
|
|
826
|
+
}),
|
|
827
|
+
null,
|
|
828
|
+
);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
await t.step("a compatible @id", async () => {
|
|
832
|
+
const document = person(compatibleId);
|
|
833
|
+
const documentLoader = redirectingLoader(plainUrl, document);
|
|
834
|
+
// The document stands for the portable object, and is verified as it:
|
|
835
|
+
const verifyPortableObject = createVerifier();
|
|
836
|
+
const verified = await lookupObject(plainUrl, {
|
|
837
|
+
documentLoader,
|
|
838
|
+
contextLoader: mockDocumentLoader,
|
|
839
|
+
verifyPortableObject,
|
|
840
|
+
});
|
|
841
|
+
assertInstanceOf(verified, Person);
|
|
842
|
+
deepStrictEqual(verified.id, new URL(compatibleId));
|
|
843
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
844
|
+
equal(
|
|
845
|
+
await lookupObject(plainUrl, {
|
|
846
|
+
documentLoader,
|
|
847
|
+
contextLoader: mockDocumentLoader,
|
|
848
|
+
verifyPortableObject: createVerifier(false),
|
|
849
|
+
}),
|
|
850
|
+
null,
|
|
851
|
+
);
|
|
852
|
+
// Without verifyPortableObject, it is fetched as before:
|
|
853
|
+
const actor = await lookupObject(plainUrl, {
|
|
854
|
+
documentLoader,
|
|
855
|
+
contextLoader: mockDocumentLoader,
|
|
856
|
+
});
|
|
857
|
+
assertInstanceOf(actor, Person);
|
|
858
|
+
deepStrictEqual(actor.id, new URL(compatibleId));
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
await t.step("a portable @id", async () => {
|
|
862
|
+
const documentLoader = redirectingLoader(plainUrl, person());
|
|
863
|
+
const actor = await lookupObject(plainUrl, {
|
|
864
|
+
documentLoader,
|
|
865
|
+
contextLoader: mockDocumentLoader,
|
|
866
|
+
verifyPortableObject: createVerifier(),
|
|
867
|
+
});
|
|
868
|
+
assertInstanceOf(actor, Person);
|
|
869
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
870
|
+
equal(
|
|
871
|
+
await lookupObject(plainUrl, {
|
|
872
|
+
documentLoader,
|
|
873
|
+
contextLoader: mockDocumentLoader,
|
|
874
|
+
verifyPortableObject: createVerifier(false),
|
|
875
|
+
}),
|
|
876
|
+
null,
|
|
877
|
+
);
|
|
878
|
+
await rejects(
|
|
879
|
+
() =>
|
|
880
|
+
lookupObject(plainUrl, {
|
|
881
|
+
documentLoader,
|
|
882
|
+
contextLoader: mockDocumentLoader,
|
|
883
|
+
verifyPortableObject: createVerifier(false),
|
|
884
|
+
crossOrigin: "throw",
|
|
885
|
+
}),
|
|
886
|
+
/No gateway returned a valid portable object/,
|
|
887
|
+
);
|
|
888
|
+
});
|
|
889
|
+
});
|