@fedify/vocab 2.0.0-dev.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 (167) hide show
  1. package/LICENSE +20 -0
  2. package/deno.json +31 -0
  3. package/dist/accept.yaml +15 -0
  4. package/dist/activity.yaml +98 -0
  5. package/dist/actor.test.d.ts +2 -0
  6. package/dist/actor.test.js +6095 -0
  7. package/dist/add.yaml +16 -0
  8. package/dist/announce.yaml +30 -0
  9. package/dist/application.yaml +324 -0
  10. package/dist/arrive.yaml +15 -0
  11. package/dist/article.yaml +46 -0
  12. package/dist/audio.yaml +11 -0
  13. package/dist/block.yaml +16 -0
  14. package/dist/chatmessage.yaml +50 -0
  15. package/dist/collection.yaml +154 -0
  16. package/dist/collectionpage.yaml +55 -0
  17. package/dist/create.yaml +28 -0
  18. package/dist/dataintegrityproof.yaml +56 -0
  19. package/dist/delete.yaml +27 -0
  20. package/dist/deno-B-ypIMwF.js +1282 -0
  21. package/dist/didservice.yaml +22 -0
  22. package/dist/dislike.yaml +14 -0
  23. package/dist/document.yaml +31 -0
  24. package/dist/emoji.yaml +12 -0
  25. package/dist/emojireact.yaml +17 -0
  26. package/dist/endpoints.yaml +85 -0
  27. package/dist/event.yaml +11 -0
  28. package/dist/export.yaml +9 -0
  29. package/dist/flag.yaml +15 -0
  30. package/dist/follow.yaml +19 -0
  31. package/dist/group.yaml +324 -0
  32. package/dist/hashtag.yaml +14 -0
  33. package/dist/ignore.yaml +14 -0
  34. package/dist/image.yaml +9 -0
  35. package/dist/intransitiveactivity.yaml +15 -0
  36. package/dist/invite.yaml +14 -0
  37. package/dist/join.yaml +14 -0
  38. package/dist/key.yaml +28 -0
  39. package/dist/leave.yaml +14 -0
  40. package/dist/like.yaml +16 -0
  41. package/dist/link.yaml +101 -0
  42. package/dist/listen.yaml +12 -0
  43. package/dist/lookup.test.d.ts +2 -0
  44. package/dist/lookup.test.js +690 -0
  45. package/dist/mention.yaml +9 -0
  46. package/dist/mod.cjs +42036 -0
  47. package/dist/mod.d.cts +15329 -0
  48. package/dist/mod.d.ts +15330 -0
  49. package/dist/mod.js +41936 -0
  50. package/dist/move.yaml +15 -0
  51. package/dist/multikey.yaml +36 -0
  52. package/dist/note.yaml +48 -0
  53. package/dist/object.yaml +404 -0
  54. package/dist/offer.yaml +15 -0
  55. package/dist/orderedcollection.yaml +39 -0
  56. package/dist/orderedcollectionpage.yaml +50 -0
  57. package/dist/organization.yaml +324 -0
  58. package/dist/page.yaml +11 -0
  59. package/dist/person.yaml +324 -0
  60. package/dist/place.yaml +75 -0
  61. package/dist/profile.yaml +26 -0
  62. package/dist/propertyvalue.yaml +32 -0
  63. package/dist/question.yaml +103 -0
  64. package/dist/read.yaml +13 -0
  65. package/dist/reject.yaml +14 -0
  66. package/dist/relationship.yaml +52 -0
  67. package/dist/remove.yaml +14 -0
  68. package/dist/service.yaml +324 -0
  69. package/dist/source.yaml +26 -0
  70. package/dist/tentativeaccept.yaml +14 -0
  71. package/dist/tentativereject.yaml +14 -0
  72. package/dist/tombstone.yaml +24 -0
  73. package/dist/travel.yaml +16 -0
  74. package/dist/type-CNuABalk.js +13 -0
  75. package/dist/type.test.d.ts +2 -0
  76. package/dist/type.test.js +24 -0
  77. package/dist/undo.yaml +26 -0
  78. package/dist/update.yaml +58 -0
  79. package/dist/utils-BSWXlrig.js +13 -0
  80. package/dist/video.yaml +11 -0
  81. package/dist/view.yaml +13 -0
  82. package/dist/vocab-DBispxj5.js +41603 -0
  83. package/dist/vocab.test.d.ts +2 -0
  84. package/dist/vocab.test.js +1304 -0
  85. package/package.json +79 -0
  86. package/scripts/codegen.ts +20 -0
  87. package/src/__snapshots__/vocab.test.ts.snap +7903 -0
  88. package/src/accept.yaml +15 -0
  89. package/src/activity.yaml +98 -0
  90. package/src/actor.test.ts +263 -0
  91. package/src/actor.ts +293 -0
  92. package/src/add.yaml +16 -0
  93. package/src/announce.yaml +30 -0
  94. package/src/application.yaml +324 -0
  95. package/src/arrive.yaml +15 -0
  96. package/src/article.yaml +46 -0
  97. package/src/audio.yaml +11 -0
  98. package/src/block.yaml +16 -0
  99. package/src/chatmessage.yaml +50 -0
  100. package/src/collection.yaml +154 -0
  101. package/src/collectionpage.yaml +55 -0
  102. package/src/constants.ts +11 -0
  103. package/src/create.yaml +28 -0
  104. package/src/dataintegrityproof.yaml +56 -0
  105. package/src/delete.yaml +27 -0
  106. package/src/didservice.yaml +22 -0
  107. package/src/dislike.yaml +14 -0
  108. package/src/document.yaml +31 -0
  109. package/src/emoji.yaml +12 -0
  110. package/src/emojireact.yaml +17 -0
  111. package/src/endpoints.yaml +85 -0
  112. package/src/event.yaml +11 -0
  113. package/src/export.yaml +9 -0
  114. package/src/flag.yaml +15 -0
  115. package/src/follow.yaml +19 -0
  116. package/src/group.yaml +324 -0
  117. package/src/handle.ts +104 -0
  118. package/src/hashtag.yaml +14 -0
  119. package/src/ignore.yaml +14 -0
  120. package/src/image.yaml +9 -0
  121. package/src/intransitiveactivity.yaml +15 -0
  122. package/src/invite.yaml +14 -0
  123. package/src/join.yaml +14 -0
  124. package/src/key.yaml +28 -0
  125. package/src/keys.ts +50 -0
  126. package/src/leave.yaml +14 -0
  127. package/src/like.yaml +16 -0
  128. package/src/link.yaml +101 -0
  129. package/src/listen.yaml +12 -0
  130. package/src/lookup.test.ts +681 -0
  131. package/src/lookup.ts +318 -0
  132. package/src/mention.yaml +9 -0
  133. package/src/mod.ts +57 -0
  134. package/src/move.yaml +15 -0
  135. package/src/multikey.yaml +36 -0
  136. package/src/note.yaml +48 -0
  137. package/src/object.yaml +404 -0
  138. package/src/offer.yaml +15 -0
  139. package/src/orderedcollection.yaml +39 -0
  140. package/src/orderedcollectionpage.yaml +50 -0
  141. package/src/organization.yaml +324 -0
  142. package/src/page.yaml +11 -0
  143. package/src/person.yaml +324 -0
  144. package/src/place.yaml +75 -0
  145. package/src/profile.yaml +26 -0
  146. package/src/propertyvalue.yaml +32 -0
  147. package/src/question.yaml +103 -0
  148. package/src/read.yaml +13 -0
  149. package/src/reject.yaml +14 -0
  150. package/src/relationship.yaml +52 -0
  151. package/src/remove.yaml +14 -0
  152. package/src/service.yaml +324 -0
  153. package/src/source.yaml +26 -0
  154. package/src/tentativeaccept.yaml +14 -0
  155. package/src/tentativereject.yaml +14 -0
  156. package/src/tombstone.yaml +24 -0
  157. package/src/travel.yaml +16 -0
  158. package/src/type.test.ts +20 -0
  159. package/src/type.ts +102 -0
  160. package/src/undo.yaml +26 -0
  161. package/src/update.yaml +58 -0
  162. package/src/utils.ts +9 -0
  163. package/src/video.yaml +11 -0
  164. package/src/view.yaml +13 -0
  165. package/src/vocab.bench.ts +204 -0
  166. package/src/vocab.test.ts +2014 -0
  167. package/tsdown.config.ts +65 -0
@@ -0,0 +1,681 @@
1
+ import {
2
+ createTestTracerProvider,
3
+ mockDocumentLoader,
4
+ test,
5
+ } from "@fedify/fixture";
6
+ import fetchMock from "fetch-mock";
7
+ import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
8
+ import { lookupObject, traverseCollection } from "./lookup.ts";
9
+ import { assertInstanceOf } from "./utils.ts";
10
+ import { Collection, Note, Object, Person } from "./vocab.ts";
11
+
12
+ test("lookupObject()", {
13
+ sanitizeResources: false,
14
+ sanitizeOps: false,
15
+ }, async (t) => {
16
+ fetchMock.spyGlobal();
17
+
18
+ fetchMock.get(
19
+ "begin:https://example.com/.well-known/webfinger",
20
+ {
21
+ subject: "acct:johndoe@example.com",
22
+ links: [
23
+ {
24
+ rel: "alternate",
25
+ href: "https://example.com/object",
26
+ type: "application/activity+json",
27
+ },
28
+ {
29
+ rel: "self",
30
+ href: "https://example.com/html/person",
31
+ type: "text/html",
32
+ },
33
+ {
34
+ rel: "self",
35
+ href: "https://example.com/person",
36
+ type: "application/activity+json",
37
+ },
38
+ ],
39
+ },
40
+ );
41
+
42
+ const options = {
43
+ documentLoader: mockDocumentLoader,
44
+ contextLoader: mockDocumentLoader,
45
+ };
46
+
47
+ await t.step("actor", async () => {
48
+ const person = await lookupObject("@johndoe@example.com", options);
49
+ assertInstanceOf(person, Person);
50
+ deepStrictEqual(person.id, new URL("https://example.com/person"));
51
+ equal(person.name, "John Doe");
52
+ const person2 = await lookupObject("johndoe@example.com", options);
53
+ deepStrictEqual(person2, person);
54
+ const person3 = await lookupObject("acct:johndoe@example.com", options);
55
+ deepStrictEqual(person3, person);
56
+ });
57
+
58
+ await t.step("object", async () => {
59
+ const object = await lookupObject("https://example.com/object", options);
60
+ assertInstanceOf(object, Object);
61
+ deepStrictEqual(
62
+ object,
63
+ new Object({
64
+ id: new URL("https://example.com/object"),
65
+ name: "Fetched object",
66
+ }),
67
+ );
68
+ const person = await lookupObject(
69
+ "https://example.com/hong-gildong",
70
+ options,
71
+ );
72
+ assertInstanceOf(person, Person);
73
+ deepStrictEqual(
74
+ person,
75
+ new Person({
76
+ id: new URL("https://example.com/hong-gildong"),
77
+ name: "Hong Gildong",
78
+ }),
79
+ );
80
+ });
81
+
82
+ fetchMock.removeRoutes();
83
+ fetchMock.get("begin:https://example.com/.well-known/webfinger", {
84
+ subject: "acct:janedoe@example.com",
85
+ links: [
86
+ {
87
+ rel: "self",
88
+ href: "https://example.com/404",
89
+ type: "application/activity+json",
90
+ },
91
+ ],
92
+ });
93
+
94
+ await t.step("not found", async () => {
95
+ deepStrictEqual(await lookupObject("janedoe@example.com", options), null);
96
+ deepStrictEqual(
97
+ await lookupObject("https://example.com/404", options),
98
+ null,
99
+ );
100
+ });
101
+
102
+ fetchMock.removeRoutes();
103
+ fetchMock.get(
104
+ "begin:https://example.com/.well-known/webfinger",
105
+ () =>
106
+ new Promise((resolve) => {
107
+ setTimeout(() => {
108
+ resolve({
109
+ subject: "acct:johndoe@example.com",
110
+ links: [
111
+ {
112
+ rel: "self",
113
+ href: "https://example.com/person",
114
+ type: "application/activity+json",
115
+ },
116
+ ],
117
+ });
118
+ }, 1000);
119
+ }),
120
+ );
121
+
122
+ await t.step("request cancellation", async () => {
123
+ const controller = new AbortController();
124
+ const promise = lookupObject("johndoe@example.com", {
125
+ ...options,
126
+ signal: controller.signal,
127
+ });
128
+
129
+ controller.abort();
130
+ deepStrictEqual(await promise, null);
131
+ });
132
+
133
+ fetchMock.removeRoutes();
134
+ fetchMock.get(
135
+ "begin:https://example.com/.well-known/webfinger",
136
+ {
137
+ subject: "acct:johndoe@example.com",
138
+ links: [
139
+ {
140
+ rel: "self",
141
+ href: "https://example.com/person",
142
+ type: "application/activity+json",
143
+ },
144
+ ],
145
+ },
146
+ );
147
+
148
+ await t.step("successful request with signal", async () => {
149
+ const controller = new AbortController();
150
+ const person = await lookupObject("johndoe@example.com", {
151
+ ...options,
152
+ signal: controller.signal,
153
+ });
154
+ assertInstanceOf(person, Person);
155
+ deepStrictEqual(person.id, new URL("https://example.com/person"));
156
+ });
157
+
158
+ fetchMock.removeRoutes();
159
+ fetchMock.get(
160
+ "begin:https://example.com/.well-known/webfinger",
161
+ () =>
162
+ new Promise((resolve) => {
163
+ setTimeout(() => {
164
+ resolve({
165
+ subject: "acct:johndoe@example.com",
166
+ links: [
167
+ {
168
+ rel: "self",
169
+ href: "https://example.com/person",
170
+ type: "application/activity+json",
171
+ },
172
+ ],
173
+ });
174
+ }, 500);
175
+ }),
176
+ );
177
+
178
+ await t.step("cancellation with immediate abort", async () => {
179
+ const controller = new AbortController();
180
+ controller.abort();
181
+
182
+ const result = await lookupObject("johndoe@example.com", {
183
+ ...options,
184
+ signal: controller.signal,
185
+ });
186
+ deepStrictEqual(result, null);
187
+ });
188
+
189
+ fetchMock.removeRoutes();
190
+ fetchMock.get(
191
+ "https://example.com/slow-object",
192
+ () =>
193
+ new Promise((resolve) => {
194
+ setTimeout(() => {
195
+ resolve({
196
+ status: 200,
197
+ headers: { "Content-Type": "application/activity+json" },
198
+ body: {
199
+ "@context": "https://www.w3.org/ns/activitystreams",
200
+ type: "Note",
201
+ content: "Slow response",
202
+ },
203
+ });
204
+ }, 1000);
205
+ }),
206
+ );
207
+
208
+ await t.step("direct object fetch cancellation", async () => {
209
+ const controller = new AbortController();
210
+ const promise = lookupObject("https://example.com/slow-object", {
211
+ contextLoader: mockDocumentLoader,
212
+ signal: controller.signal,
213
+ });
214
+
215
+ controller.abort();
216
+ deepStrictEqual(await promise, null);
217
+ });
218
+
219
+ fetchMock.hardReset();
220
+ fetchMock.removeRoutes();
221
+ });
222
+
223
+ test("traverseCollection()", {
224
+ sanitizeResources: false,
225
+ sanitizeOps: false,
226
+ }, async () => {
227
+ const options = {
228
+ documentLoader: mockDocumentLoader,
229
+ contextLoader: mockDocumentLoader,
230
+ };
231
+ const collection = await lookupObject(
232
+ "https://example.com/collection",
233
+ options,
234
+ );
235
+ assertInstanceOf(collection, Collection);
236
+ deepStrictEqual(
237
+ await Array.fromAsync(traverseCollection(collection, options)),
238
+ [
239
+ new Note({ content: "This is a simple note" }),
240
+ new Note({ content: "This is another simple note" }),
241
+ new Note({ content: "This is a third simple note" }),
242
+ ],
243
+ );
244
+ const pagedCollection = await lookupObject(
245
+ "https://example.com/paged-collection",
246
+ options,
247
+ );
248
+ assertInstanceOf(pagedCollection, Collection);
249
+ deepStrictEqual(
250
+ await Array.fromAsync(traverseCollection(pagedCollection, options)),
251
+ [
252
+ new Note({ content: "This is a simple note" }),
253
+ new Note({ content: "This is another simple note" }),
254
+ new Note({ content: "This is a third simple note" }),
255
+ ],
256
+ );
257
+ deepStrictEqual(
258
+ await Array.fromAsync(
259
+ traverseCollection(pagedCollection, {
260
+ ...options,
261
+ interval: { milliseconds: 250 },
262
+ }),
263
+ ),
264
+ [
265
+ new Note({ content: "This is a simple note" }),
266
+ new Note({ content: "This is another simple note" }),
267
+ new Note({ content: "This is a third simple note" }),
268
+ ],
269
+ );
270
+ });
271
+
272
+ test("FEP-fe34: lookupObject() cross-origin security", {
273
+ sanitizeResources: false,
274
+ sanitizeOps: false,
275
+ }, async (t) => {
276
+ await t.step(
277
+ "crossOrigin: ignore (default) - returns null for cross-origin objects",
278
+ async () => {
279
+ // Create a mock document loader that returns an object with different origin
280
+ // deno-lint-ignore require-await
281
+ const crossOriginDocumentLoader = async (url: string) => {
282
+ if (url === "https://example.com/note") {
283
+ return {
284
+ documentUrl: url,
285
+ contextUrl: null,
286
+ document: {
287
+ "@context": "https://www.w3.org/ns/activitystreams",
288
+ type: "Note",
289
+ id: "https://malicious.com/fake-note", // Different origin!
290
+ content: "This is a spoofed note from a different origin",
291
+ },
292
+ };
293
+ }
294
+ throw new Error(`Unexpected URL: ${url}`);
295
+ };
296
+
297
+ const result = await lookupObject("https://example.com/note", {
298
+ documentLoader: crossOriginDocumentLoader,
299
+ contextLoader: mockDocumentLoader,
300
+ });
301
+
302
+ // Should return null and log a warning (default behavior)
303
+ deepStrictEqual(result, null);
304
+ },
305
+ );
306
+
307
+ await t.step(
308
+ "crossOrigin: throw - throws error for cross-origin objects",
309
+ async () => {
310
+ // deno-lint-ignore require-await
311
+ const crossOriginDocumentLoader = async (url: string) => {
312
+ if (url === "https://example.com/note") {
313
+ return {
314
+ documentUrl: url,
315
+ contextUrl: null,
316
+ document: {
317
+ "@context": "https://www.w3.org/ns/activitystreams",
318
+ type: "Note",
319
+ id: "https://malicious.com/fake-note", // Different origin!
320
+ content: "This is a spoofed note from a different origin",
321
+ },
322
+ };
323
+ }
324
+ throw new Error(`Unexpected URL: ${url}`);
325
+ };
326
+
327
+ await rejects(
328
+ () =>
329
+ lookupObject("https://example.com/note", {
330
+ documentLoader: crossOriginDocumentLoader,
331
+ contextLoader: mockDocumentLoader,
332
+ crossOrigin: "throw",
333
+ }),
334
+ Error,
335
+ "The object's @id (https://malicious.com/fake-note) has a different origin than the document URL (https://example.com/note)",
336
+ );
337
+ },
338
+ );
339
+
340
+ await t.step("crossOrigin: trust - allows cross-origin objects", async () => {
341
+ // deno-lint-ignore require-await
342
+ const crossOriginDocumentLoader = async (url: string) => {
343
+ if (url === "https://example.com/note") {
344
+ return {
345
+ documentUrl: url,
346
+ contextUrl: null,
347
+ document: {
348
+ "@context": "https://www.w3.org/ns/activitystreams",
349
+ type: "Note",
350
+ id: "https://malicious.com/fake-note", // Different origin!
351
+ content: "This is a spoofed note from a different origin",
352
+ },
353
+ };
354
+ }
355
+ throw new Error(`Unexpected URL: ${url}`);
356
+ };
357
+
358
+ const result = await lookupObject("https://example.com/note", {
359
+ documentLoader: crossOriginDocumentLoader,
360
+ contextLoader: mockDocumentLoader,
361
+ crossOrigin: "trust",
362
+ });
363
+
364
+ assertInstanceOf(result, Note);
365
+ deepStrictEqual(result.id, new URL("https://malicious.com/fake-note"));
366
+ deepStrictEqual(
367
+ result.content,
368
+ "This is a spoofed note from a different origin",
369
+ );
370
+ });
371
+
372
+ await t.step("same-origin objects are always trusted", async () => {
373
+ // deno-lint-ignore require-await
374
+ const sameOriginDocumentLoader = async (url: string) => {
375
+ if (url === "https://example.com/note") {
376
+ return {
377
+ documentUrl: url,
378
+ contextUrl: null,
379
+ document: {
380
+ "@context": "https://www.w3.org/ns/activitystreams",
381
+ type: "Note",
382
+ id: "https://example.com/note", // Same origin
383
+ content: "This is a legitimate note from the same origin",
384
+ },
385
+ };
386
+ }
387
+ throw new Error(`Unexpected URL: ${url}`);
388
+ };
389
+
390
+ const result = await lookupObject("https://example.com/note", {
391
+ documentLoader: sameOriginDocumentLoader,
392
+ contextLoader: mockDocumentLoader,
393
+ });
394
+
395
+ assertInstanceOf(result, Note);
396
+ deepStrictEqual(result.id, new URL("https://example.com/note"));
397
+ deepStrictEqual(
398
+ result.content,
399
+ "This is a legitimate note from the same origin",
400
+ );
401
+ });
402
+
403
+ await t.step("objects without @id are trusted", async () => {
404
+ // deno-lint-ignore require-await
405
+ const noIdDocumentLoader = async (url: string) => {
406
+ if (url === "https://example.com/note") {
407
+ return {
408
+ documentUrl: url,
409
+ contextUrl: null,
410
+ document: {
411
+ "@context": "https://www.w3.org/ns/activitystreams",
412
+ type: "Note",
413
+ // No @id field
414
+ content: "This is a note without an ID",
415
+ },
416
+ };
417
+ }
418
+ throw new Error(`Unexpected URL: ${url}`);
419
+ };
420
+
421
+ const result = await lookupObject("https://example.com/note", {
422
+ documentLoader: noIdDocumentLoader,
423
+ contextLoader: mockDocumentLoader,
424
+ });
425
+
426
+ assertInstanceOf(result, Note);
427
+ deepStrictEqual(result.id, null);
428
+ deepStrictEqual(result.content, "This is a note without an ID");
429
+ });
430
+
431
+ await t.step("WebFinger lookup with cross-origin actor URL", async () => {
432
+ fetchMock.spyGlobal();
433
+
434
+ // Mock WebFinger response
435
+ fetchMock.get("begin:https://example.com/.well-known/webfinger", {
436
+ subject: "acct:user@example.com",
437
+ links: [
438
+ {
439
+ rel: "self",
440
+ href: "https://different-origin.com/actor", // Cross-origin actor URL
441
+ type: "application/activity+json",
442
+ },
443
+ ],
444
+ });
445
+
446
+ // Mock document loader for the cross-origin actor
447
+ // deno-lint-ignore require-await
448
+ const webfingerDocumentLoader = async (url: string) => {
449
+ if (url === "https://different-origin.com/actor") {
450
+ return {
451
+ documentUrl: url,
452
+ contextUrl: null,
453
+ document: {
454
+ "@context": "https://www.w3.org/ns/activitystreams",
455
+ type: "Person",
456
+ id: "https://malicious.com/fake-actor", // Different origin than document URL!
457
+ name: "Fake Actor",
458
+ },
459
+ };
460
+ }
461
+ throw new Error(`Unexpected URL: ${url}`);
462
+ };
463
+
464
+ // Default behavior should return null
465
+ const result1 = await lookupObject("@user@example.com", {
466
+ documentLoader: webfingerDocumentLoader,
467
+ contextLoader: mockDocumentLoader,
468
+ });
469
+ deepStrictEqual(result1, null);
470
+
471
+ // With crossOrigin: throw, should throw error
472
+ await rejects(
473
+ () =>
474
+ lookupObject("@user@example.com", {
475
+ documentLoader: webfingerDocumentLoader,
476
+ contextLoader: mockDocumentLoader,
477
+ crossOrigin: "throw",
478
+ }),
479
+ Error,
480
+ "The object's @id (https://malicious.com/fake-actor) has a different origin than the document URL (https://different-origin.com/actor)",
481
+ );
482
+
483
+ // With crossOrigin: trust, should return the object
484
+ const result2 = await lookupObject("@user@example.com", {
485
+ documentLoader: webfingerDocumentLoader,
486
+ contextLoader: mockDocumentLoader,
487
+ crossOrigin: "trust",
488
+ });
489
+ assertInstanceOf(result2, Person);
490
+ deepStrictEqual(result2.id, new URL("https://malicious.com/fake-actor"));
491
+
492
+ fetchMock.removeRoutes();
493
+ fetchMock.hardReset();
494
+ });
495
+
496
+ await t.step("subdomain same-origin check", async () => {
497
+ // Test that different subdomains are considered different origins
498
+ // deno-lint-ignore require-await
499
+ const subdomainDocumentLoader = async (url: string) => {
500
+ if (url === "https://api.example.com/note") {
501
+ return {
502
+ documentUrl: url,
503
+ contextUrl: null,
504
+ document: {
505
+ "@context": "https://www.w3.org/ns/activitystreams",
506
+ type: "Note",
507
+ id: "https://www.example.com/note", // Different subdomain = different origin
508
+ content: "Cross-subdomain note",
509
+ },
510
+ };
511
+ }
512
+ throw new Error(`Unexpected URL: ${url}`);
513
+ };
514
+
515
+ const result = await lookupObject("https://api.example.com/note", {
516
+ documentLoader: subdomainDocumentLoader,
517
+ contextLoader: mockDocumentLoader,
518
+ });
519
+
520
+ deepStrictEqual(result, null); // Should be blocked
521
+ });
522
+
523
+ await t.step("different port same-origin check", async () => {
524
+ // Test that different ports are considered different origins
525
+ // deno-lint-ignore require-await
526
+ const differentPortDocumentLoader = async (url: string) => {
527
+ if (url === "https://example.com:8080/note") {
528
+ return {
529
+ documentUrl: url,
530
+ contextUrl: null,
531
+ document: {
532
+ "@context": "https://www.w3.org/ns/activitystreams",
533
+ type: "Note",
534
+ id: "https://example.com:9090/note", // Different port = different origin
535
+ content: "Cross-port note",
536
+ },
537
+ };
538
+ }
539
+ throw new Error(`Unexpected URL: ${url}`);
540
+ };
541
+
542
+ const result = await lookupObject("https://example.com:8080/note", {
543
+ documentLoader: differentPortDocumentLoader,
544
+ contextLoader: mockDocumentLoader,
545
+ });
546
+
547
+ deepStrictEqual(result, null); // Should be blocked
548
+ });
549
+
550
+ await t.step("protocol difference same-origin check", async () => {
551
+ // Test that different protocols are considered different origins
552
+ // deno-lint-ignore require-await
553
+ const differentProtocolDocumentLoader = async (url: string) => {
554
+ if (url === "https://example.com/note") {
555
+ return {
556
+ documentUrl: url,
557
+ contextUrl: null,
558
+ document: {
559
+ "@context": "https://www.w3.org/ns/activitystreams",
560
+ type: "Note",
561
+ id: "http://example.com/note", // Different protocol = different origin
562
+ content: "Cross-protocol note",
563
+ },
564
+ };
565
+ }
566
+ throw new Error(`Unexpected URL: ${url}`);
567
+ };
568
+
569
+ const result = await lookupObject("https://example.com/note", {
570
+ documentLoader: differentProtocolDocumentLoader,
571
+ contextLoader: mockDocumentLoader,
572
+ });
573
+
574
+ deepStrictEqual(result, null); // Should be blocked
575
+ });
576
+
577
+ await t.step("error handling with crossOrigin throw option", async () => {
578
+ // Test that other errors (not cross-origin) are still thrown normally
579
+ // deno-lint-ignore require-await
580
+ const errorDocumentLoader = async (_url: string) => {
581
+ throw new Error("Network error");
582
+ };
583
+
584
+ // Network errors should not be confused with cross-origin errors
585
+ const result = await lookupObject("https://example.com/note", {
586
+ documentLoader: errorDocumentLoader,
587
+ contextLoader: mockDocumentLoader,
588
+ crossOrigin: "throw",
589
+ });
590
+
591
+ // Should return null because the document loader failed,
592
+ // not because of cross-origin policy
593
+ deepStrictEqual(result, null);
594
+ });
595
+
596
+ await t.step("malformed JSON handling with cross-origin policy", async () => {
597
+ // deno-lint-ignore require-await
598
+ const malformedJsonDocumentLoader = async (url: string) => {
599
+ if (url === "https://example.com/note") {
600
+ return {
601
+ documentUrl: url,
602
+ contextUrl: null,
603
+ document: "invalid json", // Malformed document
604
+ };
605
+ }
606
+ throw new Error(`Unexpected URL: ${url}`);
607
+ };
608
+
609
+ // Should return null for malformed JSON regardless of crossOrigin setting
610
+ deepStrictEqual(
611
+ await lookupObject("https://example.com/note", {
612
+ documentLoader: malformedJsonDocumentLoader,
613
+ contextLoader: mockDocumentLoader,
614
+ crossOrigin: "ignore",
615
+ }),
616
+ null,
617
+ );
618
+
619
+ deepStrictEqual(
620
+ await lookupObject("https://example.com/note", {
621
+ documentLoader: malformedJsonDocumentLoader,
622
+ contextLoader: mockDocumentLoader,
623
+ crossOrigin: "throw",
624
+ }),
625
+ null,
626
+ );
627
+
628
+ deepStrictEqual(
629
+ await lookupObject("https://example.com/note", {
630
+ documentLoader: malformedJsonDocumentLoader,
631
+ contextLoader: mockDocumentLoader,
632
+ crossOrigin: "trust",
633
+ }),
634
+ null,
635
+ );
636
+ });
637
+ });
638
+
639
+ test("lookupObject() records OpenTelemetry span events", async () => {
640
+ const [tracerProvider, exporter] = createTestTracerProvider();
641
+
642
+ const object = await lookupObject("https://example.com/object", {
643
+ documentLoader: mockDocumentLoader,
644
+ contextLoader: mockDocumentLoader,
645
+ tracerProvider,
646
+ });
647
+
648
+ assertInstanceOf(object, Object);
649
+
650
+ // Check that the span was recorded
651
+ const spans = exporter.getSpans("activitypub.lookup_object");
652
+ deepStrictEqual(spans.length, 1);
653
+ const span = spans[0];
654
+
655
+ // Check span attributes
656
+ deepStrictEqual(
657
+ span.attributes["activitypub.object.id"],
658
+ "https://example.com/object",
659
+ );
660
+
661
+ // Check that the object.fetched event was recorded
662
+ const events = exporter.getEvents(
663
+ "activitypub.lookup_object",
664
+ "activitypub.object.fetched",
665
+ );
666
+ deepStrictEqual(events.length, 1);
667
+ const event = events[0];
668
+
669
+ // Verify event attributes
670
+ ok(event.attributes != null);
671
+ ok(typeof event.attributes["activitypub.object.type"] === "string");
672
+ ok(typeof event.attributes["activitypub.object.json"] === "string");
673
+
674
+ // Verify the JSON contains the object
675
+ const recordedObject = JSON.parse(
676
+ event.attributes["activitypub.object.json"] as string,
677
+ );
678
+ deepStrictEqual(recordedObject.id, "https://example.com/object");
679
+ });
680
+
681
+ // cSpell: ignore gildong