@fedify/vocab 2.4.0-pr.934.40 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +31713 -8698
  3. package/dist/mod.d.cts +4708 -7
  4. package/dist/mod.d.ts +4708 -7
  5. package/dist/mod.js +31606 -8598
  6. package/dist-tests/actor-D7_iX4I4.mjs +276 -0
  7. package/dist-tests/actor.test.mjs +55 -49
  8. package/dist-tests/application.yaml +15 -0
  9. package/dist-tests/audio.yaml +2 -1
  10. package/dist-tests/document.yaml +14 -1
  11. package/dist-tests/{actor-CnnyTI7i.mjs → esm-DP2Spt0u.mjs} +15 -211
  12. package/dist-tests/gateway.test.mjs +1906 -0
  13. package/dist-tests/group.yaml +15 -0
  14. package/dist-tests/image.yaml +2 -1
  15. package/dist-tests/link.yaml +14 -1
  16. package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
  17. package/dist-tests/lookup.test.mjs +10 -268
  18. package/dist-tests/object.yaml +15 -0
  19. package/dist-tests/organization.yaml +15 -0
  20. package/dist-tests/page.yaml +2 -1
  21. package/dist-tests/person.yaml +15 -0
  22. package/dist-tests/portable-lookup.test.mjs +653 -0
  23. package/dist-tests/service.yaml +15 -0
  24. package/dist-tests/signed-representation.test.mjs +189 -0
  25. package/dist-tests/translation.test.mjs +404 -0
  26. package/dist-tests/translation.yaml +91 -0
  27. package/dist-tests/type.test.mjs +2 -2
  28. package/dist-tests/video.yaml +2 -1
  29. package/dist-tests/{vocab-OU5GYbNg.mjs → vocab-COZcxf00.mjs} +31227 -8501
  30. package/dist-tests/vocab.test.mjs +824 -6
  31. package/package.json +5 -5
  32. package/src/__snapshots__/vocab.test.ts.snap +345 -30
  33. package/src/actor.test.ts +41 -40
  34. package/src/actor.ts +120 -1
  35. package/src/application.yaml +15 -0
  36. package/src/audio.yaml +2 -1
  37. package/src/document.yaml +14 -1
  38. package/src/gateway.test.ts +2637 -0
  39. package/src/group.yaml +15 -0
  40. package/src/image.yaml +2 -1
  41. package/src/link.yaml +14 -1
  42. package/src/lookup.test.ts +6 -5
  43. package/src/lookup.ts +460 -33
  44. package/src/mod.ts +9 -0
  45. package/src/object.yaml +15 -0
  46. package/src/organization.yaml +15 -0
  47. package/src/page.yaml +2 -1
  48. package/src/person.yaml +15 -0
  49. package/src/portable-lookup.test.ts +889 -0
  50. package/src/preprocessors.ts +4 -2
  51. package/src/service.yaml +15 -0
  52. package/src/signed-representation.test.ts +277 -0
  53. package/src/translation.test.ts +518 -0
  54. package/src/translation.yaml +91 -0
  55. package/src/video.yaml +2 -1
  56. package/src/vocab.test.ts +1155 -2
  57. /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
  58. /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
@@ -0,0 +1,653 @@
1
+ import "temporal-polyfill";
2
+ globalThis.addEventListener = () => {};
3
+ import { F as Person } from "./vocab-COZcxf00.mjs";
4
+ import { t as esm_default } from "./esm-DP2Spt0u.mjs";
5
+ import { n as getActorHandle } from "./actor-D7_iX4I4.mjs";
6
+ import { t as lookupObject } from "./lookup-IiNFWuXL.mjs";
7
+ import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
8
+ import { mockDocumentLoader, test } from "@fedify/fixture";
9
+ import { deepStrictEqual, equal, rejects } from "node:assert/strict";
10
+ import { FetchError, parseIri } from "@fedify/vocab-runtime";
11
+ //#region src/portable-lookup.test.ts
12
+ const did = "did:key:z6Mkabc";
13
+ const actorId = `ap+ef61://${did}/actor`;
14
+ const gatewayPath = `/.well-known/apgateway/${did}/actor`;
15
+ const compatibleId = `https://example.com${gatewayPath}`;
16
+ const webFingerPrefix = "begin:https://example.com/.well-known/webfinger";
17
+ const AS_TYPE = "application/activity+json";
18
+ function person(id = `ap://${did}/actor`, extra = {}) {
19
+ return {
20
+ "@context": "https://www.w3.org/ns/activitystreams",
21
+ id,
22
+ type: "Person",
23
+ name: "Alice",
24
+ ...extra
25
+ };
26
+ }
27
+ function createLoader(responses) {
28
+ const fetched = [];
29
+ const loader = async (url, options) => {
30
+ fetched.push(url);
31
+ options?.signal?.throwIfAborted();
32
+ const document = responses[url];
33
+ if (document == null) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
34
+ return {
35
+ contextUrl: null,
36
+ documentUrl: url,
37
+ document
38
+ };
39
+ };
40
+ return Object.assign(loader, { fetched });
41
+ }
42
+ function createVerifier(verified = true) {
43
+ const documents = [];
44
+ const verifier = async (document) => {
45
+ documents.push(document);
46
+ return { verified };
47
+ };
48
+ return Object.assign(verifier, { documents });
49
+ }
50
+ function selfLinks(...hrefs) {
51
+ return {
52
+ subject: "acct:alice@example.com",
53
+ links: hrefs.map((href) => ({
54
+ rel: "self",
55
+ type: AS_TYPE,
56
+ href
57
+ }))
58
+ };
59
+ }
60
+ test("lookupObject() with FEP-ef61 portable actors", {
61
+ sanitizeResources: false,
62
+ sanitizeOps: false
63
+ }, async (t) => {
64
+ esm_default.spyGlobal();
65
+ try {
66
+ await t.step("compatible self link, alias-free JRD", async () => {
67
+ esm_default.removeRoutes();
68
+ esm_default.get(webFingerPrefix, selfLinks(compatibleId));
69
+ const document = person();
70
+ const documentLoader = createLoader({ [compatibleId]: document });
71
+ const verifyPortableObject = createVerifier();
72
+ const actor = await lookupObject("@alice@example.com", {
73
+ documentLoader,
74
+ contextLoader: mockDocumentLoader,
75
+ verifyPortableObject
76
+ });
77
+ assertInstanceOf(actor, Person);
78
+ deepStrictEqual(actor.id, parseIri(actorId));
79
+ deepStrictEqual(documentLoader.fetched, [compatibleId]);
80
+ deepStrictEqual(verifyPortableObject.documents, [document]);
81
+ });
82
+ await t.step("canonical self link asks the WebFinger host first", async () => {
83
+ esm_default.removeRoutes();
84
+ esm_default.get(webFingerPrefix, {
85
+ subject: "acct:alice@example.com",
86
+ links: [{
87
+ rel: "self",
88
+ type: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
89
+ href: `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`
90
+ }]
91
+ });
92
+ const hinted = `https://hint.example${gatewayPath}`;
93
+ const documentLoader = createLoader({ [hinted]: person() });
94
+ assertInstanceOf(await lookupObject("@alice@example.com", {
95
+ documentLoader,
96
+ contextLoader: mockDocumentLoader,
97
+ verifyPortableObject: createVerifier()
98
+ }), Person);
99
+ deepStrictEqual(documentLoader.fetched, [compatibleId, hinted]);
100
+ });
101
+ await t.step("explicit gateways replace WebFinger hints", async () => {
102
+ esm_default.removeRoutes();
103
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`));
104
+ const second = `https://second.example${gatewayPath}`;
105
+ const documentLoader = createLoader({ [second]: person() });
106
+ assertInstanceOf(await lookupObject("@alice@example.com", {
107
+ documentLoader,
108
+ contextLoader: mockDocumentLoader,
109
+ verifyPortableObject: createVerifier(),
110
+ gateways: ["https://second.example"]
111
+ }), Person);
112
+ deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
113
+ });
114
+ await t.step("WebFinger host counts toward the five attempts", async () => {
115
+ esm_default.removeRoutes();
116
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`));
117
+ const documentLoader = createLoader({});
118
+ equal(await lookupObject("@alice@example.com", {
119
+ documentLoader,
120
+ contextLoader: mockDocumentLoader,
121
+ verifyPortableObject: createVerifier(),
122
+ gateways: Array.from({ length: 6 }, (_, i) => `https://g${i}.example`)
123
+ }), null);
124
+ deepStrictEqual(documentLoader.fetched, [compatibleId, ...[
125
+ 0,
126
+ 1,
127
+ 2,
128
+ 3
129
+ ].map((i) => `https://g${i}.example${gatewayPath}`)]);
130
+ });
131
+ await t.step("tries the next self link after a failure", async () => {
132
+ esm_default.removeRoutes();
133
+ const other = `https://other.example${gatewayPath}`;
134
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`, compatibleId, other));
135
+ const documentLoader = createLoader({ [other]: person() });
136
+ assertInstanceOf(await lookupObject("@alice@example.com", {
137
+ documentLoader,
138
+ contextLoader: mockDocumentLoader,
139
+ verifyPortableObject: createVerifier()
140
+ }), Person);
141
+ deepStrictEqual(documentLoader.fetched, [compatibleId, other]);
142
+ });
143
+ await t.step("accepts a compatible identifier as the ID", async () => {
144
+ esm_default.removeRoutes();
145
+ esm_default.get(webFingerPrefix, selfLinks(compatibleId));
146
+ const document = person(compatibleId);
147
+ const documentLoader = createLoader({ [compatibleId]: document });
148
+ const verifyPortableObject = createVerifier();
149
+ const actor = await lookupObject("@alice@example.com", {
150
+ documentLoader,
151
+ contextLoader: mockDocumentLoader,
152
+ verifyPortableObject
153
+ });
154
+ assertInstanceOf(actor, Person);
155
+ deepStrictEqual(actor.id, new URL(compatibleId));
156
+ deepStrictEqual(verifyPortableObject.documents, [document]);
157
+ });
158
+ await t.step("rejects objects that fail the checks", async () => {
159
+ esm_default.removeRoutes();
160
+ esm_default.get(webFingerPrefix, selfLinks(compatibleId));
161
+ for (const [document, verified] of [
162
+ [person(), false],
163
+ [person(`ap://${did}/other`), true],
164
+ [person(`https://example.com/.well-known/apgateway/${did}/other`), true],
165
+ [person(compatibleId), false]
166
+ ]) {
167
+ const documentLoader = createLoader({ [compatibleId]: document });
168
+ const verifyPortableObject = createVerifier(verified);
169
+ equal(await lookupObject("@alice@example.com", {
170
+ documentLoader,
171
+ contextLoader: mockDocumentLoader,
172
+ verifyPortableObject
173
+ }), null);
174
+ deepStrictEqual(documentLoader.fetched, [compatibleId]);
175
+ await rejects(() => lookupObject("@alice@example.com", {
176
+ documentLoader,
177
+ contextLoader: mockDocumentLoader,
178
+ verifyPortableObject,
179
+ crossOrigin: "throw"
180
+ }), { name: "PortableObjectRejectedError" });
181
+ equal(await lookupObject("@alice@example.com", {
182
+ documentLoader,
183
+ contextLoader: mockDocumentLoader,
184
+ verifyPortableObject,
185
+ crossOrigin: "trust"
186
+ }), null);
187
+ }
188
+ });
189
+ await t.step("without verifyPortableObject", async () => {
190
+ esm_default.removeRoutes();
191
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`, compatibleId));
192
+ const documentLoader = createLoader({ [compatibleId]: person() });
193
+ for (const crossOrigin of ["ignore", "trust"]) equal(await lookupObject("@alice@example.com", {
194
+ documentLoader,
195
+ contextLoader: mockDocumentLoader,
196
+ crossOrigin
197
+ }), null);
198
+ deepStrictEqual(documentLoader.fetched, [compatibleId, compatibleId]);
199
+ await rejects(() => lookupObject("@alice@example.com", {
200
+ documentLoader,
201
+ contextLoader: mockDocumentLoader,
202
+ crossOrigin: "throw"
203
+ }), /has a different origin than the document URL/);
204
+ const actor = await lookupObject("@alice@example.com", {
205
+ documentLoader: createLoader({ [compatibleId]: person(compatibleId) }),
206
+ contextLoader: mockDocumentLoader
207
+ });
208
+ assertInstanceOf(actor, Person);
209
+ deepStrictEqual(actor.id, new URL(compatibleId));
210
+ const malformed = `${compatibleId}?gateways=x`;
211
+ const ordinaryLoader = createLoader({ [malformed]: person(malformed) });
212
+ assertInstanceOf(await lookupObject(malformed, {
213
+ documentLoader: ordinaryLoader,
214
+ contextLoader: mockDocumentLoader
215
+ }), Person);
216
+ deepStrictEqual(ordinaryLoader.fetched, [malformed]);
217
+ });
218
+ await t.step("refuses portable IRIs changed by URL parsing", async () => {
219
+ esm_default.removeRoutes();
220
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/x/../actor`, `ap://${did}/x/%2e%2e/actor`));
221
+ const documentLoader = createLoader({ [compatibleId]: person() });
222
+ const options = {
223
+ documentLoader,
224
+ contextLoader: mockDocumentLoader,
225
+ verifyPortableObject: createVerifier()
226
+ };
227
+ equal(await lookupObject("@alice@example.com", options), null);
228
+ equal(await lookupObject(`ap://${did}/x/../actor?@gateway=https%3A%2F%2Fexample.com`, options), null);
229
+ deepStrictEqual(documentLoader.fetched, []);
230
+ });
231
+ await t.step("refuses compatible IDs changed by URL parsing", async () => {
232
+ esm_default.removeRoutes();
233
+ const dotted = compatibleId.replace("/actor", "/x/../actor");
234
+ const documentLoader = createLoader({ [compatibleId]: person() });
235
+ const options = {
236
+ documentLoader,
237
+ contextLoader: mockDocumentLoader,
238
+ verifyPortableObject: createVerifier()
239
+ };
240
+ equal(await lookupObject(dotted, options), null);
241
+ esm_default.get(webFingerPrefix, selfLinks(dotted));
242
+ equal(await lookupObject("@alice@example.com", options), null);
243
+ deepStrictEqual(documentLoader.fetched, []);
244
+ });
245
+ await t.step("bounds gateway requests", async () => {
246
+ esm_default.removeRoutes();
247
+ const hints = Array.from({ length: 5 }, (_, i) => `@gateway=https%3A%2F%2Fg${i}.example`).join("&");
248
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor?${hints}`, `ap://${did}/actor2?${hints}`, `https://g9.example/.well-known/apgateway/${did}/actor`));
249
+ const documentLoader = createLoader({});
250
+ equal(await lookupObject("@alice@example.com", {
251
+ documentLoader,
252
+ contextLoader: mockDocumentLoader,
253
+ verifyPortableObject: createVerifier()
254
+ }), null);
255
+ deepStrictEqual(documentLoader.fetched, [compatibleId, ...[
256
+ 0,
257
+ 1,
258
+ 2,
259
+ 3
260
+ ].map((i) => `https://g${i}.example${gatewayPath}`)]);
261
+ });
262
+ await t.step("cancellation", async () => {
263
+ esm_default.removeRoutes();
264
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`, `https://other.example${gatewayPath}`));
265
+ const controller = new AbortController();
266
+ const verifyPortableObject = () => {
267
+ controller.abort();
268
+ return Promise.resolve({ verified: true });
269
+ };
270
+ const documentLoader = createLoader({ [compatibleId]: person() });
271
+ equal(await lookupObject("@alice@example.com", {
272
+ documentLoader,
273
+ contextLoader: mockDocumentLoader,
274
+ verifyPortableObject,
275
+ signal: controller.signal
276
+ }), null);
277
+ deepStrictEqual(documentLoader.fetched, [compatibleId]);
278
+ });
279
+ } finally {
280
+ esm_default.removeRoutes();
281
+ esm_default.hardReset();
282
+ }
283
+ });
284
+ test("lookupObject() looks up portable identifiers directly", async () => {
285
+ const verifyPortableObject = createVerifier();
286
+ const hinted = `https://gw.example${gatewayPath}`;
287
+ const documentLoader = createLoader({
288
+ [hinted]: person(),
289
+ [`https://example.com${gatewayPath}`]: person(),
290
+ [actorId]: person()
291
+ });
292
+ const options = {
293
+ documentLoader,
294
+ contextLoader: mockDocumentLoader,
295
+ verifyPortableObject
296
+ };
297
+ for (const identifier of [
298
+ `ap://${did}/actor?@gateway=https%3A%2F%2Fgw.example`,
299
+ compatibleId,
300
+ new URL(compatibleId),
301
+ `ap://${did}/actor`,
302
+ parseIri(`ap://${did}/actor`)
303
+ ]) {
304
+ const actor = await lookupObject(identifier, options);
305
+ assertInstanceOf(actor, Person);
306
+ deepStrictEqual(actor.id, parseIri(actorId));
307
+ }
308
+ deepStrictEqual(documentLoader.fetched, [
309
+ hinted,
310
+ compatibleId,
311
+ compatibleId,
312
+ actorId,
313
+ actorId
314
+ ]);
315
+ equal(await lookupObject(`ap://${did}/actor`, {
316
+ documentLoader,
317
+ contextLoader: mockDocumentLoader
318
+ }), null);
319
+ deepStrictEqual(documentLoader.fetched.length, 5);
320
+ });
321
+ test("lookupObject() uses explicit gateways for portable IDs", async (t) => {
322
+ const first = `https://first.example${gatewayPath}`;
323
+ const second = `https://second.example${gatewayPath}`;
324
+ const hint = `https://hint.example${gatewayPath}`;
325
+ const options = {
326
+ contextLoader: mockDocumentLoader,
327
+ verifyPortableObject: createVerifier()
328
+ };
329
+ await t.step("bare ap: ID and one gateway", async () => {
330
+ const documentLoader = createLoader({ [first]: person() });
331
+ assertInstanceOf(await lookupObject(`ap://${did}/actor`, {
332
+ ...options,
333
+ documentLoader,
334
+ gateways: ["https://first.example"]
335
+ }), Person);
336
+ deepStrictEqual(documentLoader.fetched, [first]);
337
+ });
338
+ await t.step("tries several gateways in order", async () => {
339
+ const documentLoader = createLoader({ [second]: person() });
340
+ assertInstanceOf(await lookupObject(actorId, {
341
+ ...options,
342
+ documentLoader,
343
+ gateways: ["https://first.example", new URL("https://second.example")]
344
+ }), Person);
345
+ deepStrictEqual(documentLoader.fetched, [first, second]);
346
+ });
347
+ await t.step("explicit gateways replace location hints", async () => {
348
+ const documentLoader = createLoader({
349
+ [second]: person(),
350
+ [hint]: person()
351
+ });
352
+ assertInstanceOf(await lookupObject(`ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`, {
353
+ ...options,
354
+ documentLoader,
355
+ gateways: ["https://first.example", "https://second.example"]
356
+ }), Person);
357
+ deepStrictEqual(documentLoader.fetched, [first, second]);
358
+ });
359
+ await t.step("empty list uses a custom document loader", async () => {
360
+ const hintedId = `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`;
361
+ const canonicalHintedId = `${actorId}?@gateway=https%3A%2F%2Fhint.example`;
362
+ const documentLoader = createLoader({
363
+ [canonicalHintedId]: person(),
364
+ [hint]: person()
365
+ });
366
+ const actor = await lookupObject(hintedId, {
367
+ ...options,
368
+ documentLoader,
369
+ gateways: []
370
+ });
371
+ deepStrictEqual(documentLoader.fetched, [canonicalHintedId]);
372
+ assertInstanceOf(actor, Person);
373
+ });
374
+ await t.step("invalid gateway throws before fetching", async () => {
375
+ const documentLoader = createLoader({});
376
+ for (const verifyPortableObject of [void 0, createVerifier()]) await rejects(() => lookupObject(actorId, {
377
+ documentLoader,
378
+ contextLoader: mockDocumentLoader,
379
+ verifyPortableObject,
380
+ gateways: ["https://bad.example/path"]
381
+ }), TypeError);
382
+ deepStrictEqual(documentLoader.fetched, []);
383
+ });
384
+ await t.step("a malformed portable ID is refused first", async () => {
385
+ const documentLoader = createLoader({});
386
+ equal(await lookupObject(`ap://${did}/x/../actor`, {
387
+ ...options,
388
+ documentLoader,
389
+ gateways: ["https://bad.example/path"]
390
+ }), null);
391
+ deepStrictEqual(documentLoader.fetched, []);
392
+ });
393
+ });
394
+ test("getActorHandle() with FEP-ef61 portable actors", {
395
+ sanitizeResources: false,
396
+ sanitizeOps: false
397
+ }, async (t) => {
398
+ const actor = new Person({
399
+ id: parseIri(actorId),
400
+ preferredUsername: "alice",
401
+ gateways: [new URL("https://example.com"), new URL("https://example.org")]
402
+ });
403
+ esm_default.spyGlobal();
404
+ try {
405
+ await t.step("verified through the first gateway", async () => {
406
+ esm_default.removeRoutes();
407
+ esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Aalice%40example.com", selfLinks(compatibleId));
408
+ deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
409
+ });
410
+ await t.step("canonical self link", async () => {
411
+ esm_default.removeRoutes();
412
+ esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`));
413
+ deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
414
+ });
415
+ await t.step("follows a verified subject", async () => {
416
+ esm_default.removeRoutes();
417
+ esm_default.get(webFingerPrefix, {
418
+ ...selfLinks(compatibleId),
419
+ subject: "acct:alice@example.org"
420
+ });
421
+ esm_default.get("begin:https://example.org/.well-known/webfinger", {
422
+ ...selfLinks(compatibleId),
423
+ subject: "acct:alice@example.org"
424
+ });
425
+ deepStrictEqual(await getActorHandle(actor), "@alice@example.org");
426
+ esm_default.removeRoutes();
427
+ esm_default.get(webFingerPrefix, {
428
+ ...selfLinks(compatibleId),
429
+ subject: "acct:alice@example.org"
430
+ });
431
+ esm_default.get("begin:https://example.org/.well-known/webfinger", selfLinks(`ap://${did}/other`));
432
+ deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
433
+ });
434
+ await t.step("rejects a WebFinger response for another actor", async () => {
435
+ for (const jrd of [
436
+ selfLinks(`https://example.com/.well-known/apgateway/${did}/other`),
437
+ selfLinks("https://example.com/users/alice"),
438
+ {
439
+ subject: "acct:alice@example.com",
440
+ links: []
441
+ }
442
+ ]) {
443
+ esm_default.removeRoutes();
444
+ esm_default.get(webFingerPrefix, jrd);
445
+ await rejects(() => getActorHandle(actor), TypeError);
446
+ }
447
+ });
448
+ await t.step("does not fall back when WebFinger fails", async () => {
449
+ esm_default.removeRoutes();
450
+ esm_default.get(webFingerPrefix, 404);
451
+ await rejects(() => getActorHandle(actor), TypeError);
452
+ });
453
+ await t.step("not enough information", async () => {
454
+ esm_default.removeRoutes();
455
+ esm_default.clearHistory();
456
+ for (const target of [
457
+ parseIri(actorId),
458
+ new Person({
459
+ id: parseIri(actorId),
460
+ preferredUsername: "alice"
461
+ }),
462
+ new Person({
463
+ id: parseIri(actorId),
464
+ gateways: [new URL("https://example.com")]
465
+ })
466
+ ]) await rejects(() => getActorHandle(target), TypeError);
467
+ deepStrictEqual(esm_default.callHistory.calls().length, 0);
468
+ });
469
+ } finally {
470
+ esm_default.removeRoutes();
471
+ esm_default.hardReset();
472
+ }
473
+ });
474
+ test("lookupObject() reports inferred gateways as hints", async () => {
475
+ const calls = [];
476
+ const verifyPortableObject = async (_, options) => {
477
+ calls.push(options);
478
+ return { verified: true };
479
+ };
480
+ assertInstanceOf(await lookupObject(compatibleId, {
481
+ documentLoader: createLoader({ [compatibleId]: person() }),
482
+ contextLoader: mockDocumentLoader,
483
+ verifyPortableObject
484
+ }), Person);
485
+ deepStrictEqual(calls.length, 1);
486
+ deepStrictEqual(calls[0].gateways, void 0);
487
+ deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
488
+ });
489
+ test("lookupObject() reports explicit gateways to the verifier", async (t) => {
490
+ const calls = [];
491
+ const verifyPortableObject = async (_, options) => {
492
+ calls.push(options);
493
+ return { verified: true };
494
+ };
495
+ await t.step("bare ID with a limited explicit list", async () => {
496
+ const documentLoader = createLoader({ [`https://g4.example${gatewayPath}`]: person() });
497
+ assertInstanceOf(await lookupObject(actorId, {
498
+ documentLoader,
499
+ contextLoader: mockDocumentLoader,
500
+ verifyPortableObject,
501
+ gateways: Array.from({ length: 6 }, (_, i) => `https://g${i}.example`)
502
+ }), Person);
503
+ deepStrictEqual(documentLoader.fetched, [
504
+ 0,
505
+ 1,
506
+ 2,
507
+ 3,
508
+ 4
509
+ ].map((i) => `https://g${i}.example${gatewayPath}`));
510
+ deepStrictEqual(calls.at(-1)?.gateways, [
511
+ 0,
512
+ 1,
513
+ 2,
514
+ 3,
515
+ 4
516
+ ].map((i) => new URL(`https://g${i}.example`)));
517
+ equal(calls.at(-1)?.gatewayHints, void 0);
518
+ });
519
+ await t.step("compatible origin stays first and is deduplicated", async () => {
520
+ const second = `https://second.example${gatewayPath}`;
521
+ const documentLoader = createLoader({ [second]: person() });
522
+ assertInstanceOf(await lookupObject(compatibleId, {
523
+ documentLoader,
524
+ contextLoader: mockDocumentLoader,
525
+ verifyPortableObject,
526
+ gateways: ["https://example.com", "https://second.example"]
527
+ }), Person);
528
+ deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
529
+ deepStrictEqual(calls.at(-1)?.gateways, [new URL("https://example.com"), new URL("https://second.example")]);
530
+ equal(calls.at(-1)?.gatewayHints, void 0);
531
+ });
532
+ await t.step("empty list keeps the compatible origin", async () => {
533
+ const documentLoader = createLoader({ [compatibleId]: person() });
534
+ assertInstanceOf(await lookupObject(compatibleId, {
535
+ documentLoader,
536
+ contextLoader: mockDocumentLoader,
537
+ verifyPortableObject,
538
+ gateways: []
539
+ }), Person);
540
+ deepStrictEqual(documentLoader.fetched, [compatibleId]);
541
+ deepStrictEqual(calls.at(-1)?.gateways, []);
542
+ equal(calls.at(-1)?.gatewayHints, void 0);
543
+ });
544
+ });
545
+ test("lookupObject() verifies fetched documents that stand for portable objects", async (t) => {
546
+ const plainUrl = "https://example.com/users/alice";
547
+ const redirectingLoader = (documentUrl, document) => {
548
+ const fetched = [];
549
+ const loader = async (url) => {
550
+ fetched.push(url);
551
+ if (url !== plainUrl) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
552
+ return {
553
+ contextUrl: null,
554
+ documentUrl,
555
+ document
556
+ };
557
+ };
558
+ return Object.assign(loader, { fetched });
559
+ };
560
+ await t.step("a redirect to a compatible identifier", async () => {
561
+ const calls = [];
562
+ const documentLoader = redirectingLoader(compatibleId, person());
563
+ const actor = await lookupObject(plainUrl, {
564
+ documentLoader,
565
+ contextLoader: mockDocumentLoader,
566
+ verifyPortableObject: async (_, options) => {
567
+ calls.push(options);
568
+ return { verified: true };
569
+ }
570
+ });
571
+ assertInstanceOf(actor, Person);
572
+ deepStrictEqual(actor.id, parseIri(actorId));
573
+ deepStrictEqual(documentLoader.fetched, [plainUrl]);
574
+ deepStrictEqual(calls.length, 1);
575
+ deepStrictEqual(calls[0].documentUrl, new URL(compatibleId));
576
+ deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
577
+ const explicitCalls = [];
578
+ assertInstanceOf(await lookupObject(plainUrl, {
579
+ documentLoader,
580
+ contextLoader: mockDocumentLoader,
581
+ gateways: ["https://other.example"],
582
+ verifyPortableObject: async (_, options) => {
583
+ explicitCalls.push(options);
584
+ return { verified: true };
585
+ }
586
+ }), Person);
587
+ deepStrictEqual(explicitCalls[0].gateways, [new URL("https://other.example")]);
588
+ equal(explicitCalls[0].gatewayHints, void 0);
589
+ await rejects(() => lookupObject(plainUrl, {
590
+ documentLoader,
591
+ contextLoader: mockDocumentLoader,
592
+ verifyPortableObject: createVerifier(),
593
+ gateways: ["https://bad.example/path"]
594
+ }), TypeError);
595
+ equal(await lookupObject(plainUrl, {
596
+ documentLoader,
597
+ contextLoader: mockDocumentLoader,
598
+ verifyPortableObject: createVerifier(false)
599
+ }), null);
600
+ equal(await lookupObject(plainUrl, {
601
+ documentLoader: redirectingLoader(compatibleId, person(`ap://${did}/other`)),
602
+ contextLoader: mockDocumentLoader,
603
+ verifyPortableObject: createVerifier()
604
+ }), null);
605
+ });
606
+ await t.step("a compatible @id", async () => {
607
+ const document = person(compatibleId);
608
+ const documentLoader = redirectingLoader(plainUrl, document);
609
+ const verifyPortableObject = createVerifier();
610
+ const verified = await lookupObject(plainUrl, {
611
+ documentLoader,
612
+ contextLoader: mockDocumentLoader,
613
+ verifyPortableObject
614
+ });
615
+ assertInstanceOf(verified, Person);
616
+ deepStrictEqual(verified.id, new URL(compatibleId));
617
+ deepStrictEqual(verifyPortableObject.documents, [document]);
618
+ equal(await lookupObject(plainUrl, {
619
+ documentLoader,
620
+ contextLoader: mockDocumentLoader,
621
+ verifyPortableObject: createVerifier(false)
622
+ }), null);
623
+ const actor = await lookupObject(plainUrl, {
624
+ documentLoader,
625
+ contextLoader: mockDocumentLoader
626
+ });
627
+ assertInstanceOf(actor, Person);
628
+ deepStrictEqual(actor.id, new URL(compatibleId));
629
+ });
630
+ await t.step("a portable @id", async () => {
631
+ const documentLoader = redirectingLoader(plainUrl, person());
632
+ const actor = await lookupObject(plainUrl, {
633
+ documentLoader,
634
+ contextLoader: mockDocumentLoader,
635
+ verifyPortableObject: createVerifier()
636
+ });
637
+ assertInstanceOf(actor, Person);
638
+ deepStrictEqual(actor.id, parseIri(actorId));
639
+ equal(await lookupObject(plainUrl, {
640
+ documentLoader,
641
+ contextLoader: mockDocumentLoader,
642
+ verifyPortableObject: createVerifier(false)
643
+ }), null);
644
+ await rejects(() => lookupObject(plainUrl, {
645
+ documentLoader,
646
+ contextLoader: mockDocumentLoader,
647
+ verifyPortableObject: createVerifier(false),
648
+ crossOrigin: "throw"
649
+ }), /No gateway returned a valid portable object/);
650
+ });
651
+ });
652
+ //#endregion
653
+ export {};
@@ -7,6 +7,7 @@ entity: true
7
7
  description: Represents a service of any kind.
8
8
  defaultContext:
9
9
  - "https://www.w3.org/ns/activitystreams"
10
+ - "https://w3id.org/fep/ef61"
10
11
  - "https://w3id.org/security/v1"
11
12
  - "https://w3id.org/security/data-integrity/v1"
12
13
  - "https://www.w3.org/ns/did/v1"
@@ -78,6 +79,20 @@ properties:
78
79
  range:
79
80
  - "https://w3id.org/security#Multikey"
80
81
 
82
+ - pluralName: gateways
83
+ singularName: gateway
84
+ singularAccessor: true
85
+ compactName: gateways
86
+ uri: "https://w3id.org/fep/ef61/gateways"
87
+ container: list
88
+ description: |
89
+ Gateways where the latest version of this portable actor object can be
90
+ retrieved.
91
+
92
+ See [FEP-ef61](https://w3id.org/fep/ef61) for details.
93
+ range:
94
+ - "fedify:gatewayUrl"
95
+
81
96
  - singularName: manuallyApprovesFollowers
82
97
  functional: true
83
98
  compactName: manuallyApprovesFollowers