@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,2014 @@
1
+ import { mockDocumentLoader, test } from "@fedify/fixture";
2
+ import { decodeMultibase, LanguageString } from "@fedify/vocab-runtime";
3
+ import {
4
+ areAllScalarTypes,
5
+ loadSchemaFiles,
6
+ type PropertySchema,
7
+ type TypeSchema,
8
+ } from "@fedify/vocab-tools";
9
+ import { pascalCase } from "es-toolkit";
10
+ import {
11
+ deepStrictEqual,
12
+ notDeepStrictEqual,
13
+ ok,
14
+ rejects,
15
+ throws,
16
+ } from "node:assert/strict";
17
+ import { assertInstanceOf } from "./utils.ts";
18
+ import * as vocab from "./vocab.ts";
19
+ import {
20
+ Activity,
21
+ Announce,
22
+ Collection,
23
+ Create,
24
+ CryptographicKey,
25
+ type DataIntegrityProof,
26
+ Follow,
27
+ Hashtag,
28
+ Link,
29
+ Note,
30
+ Object,
31
+ OrderedCollectionPage,
32
+ Person,
33
+ Place,
34
+ Question,
35
+ Source,
36
+ } from "./vocab.ts";
37
+
38
+ test("new Object()", () => {
39
+ const obj = new Object({
40
+ name: "Test",
41
+ contents: [
42
+ new LanguageString("Hello", "en"),
43
+ new LanguageString("你好", "zh"),
44
+ ],
45
+ });
46
+ deepStrictEqual(obj.name, "Test");
47
+ deepStrictEqual(obj.contents[0], new LanguageString("Hello", "en"));
48
+ deepStrictEqual(obj.contents[1], new LanguageString("你好", "zh"));
49
+
50
+ throws(
51
+ () => new Object({ id: 123 as unknown as URL }),
52
+ TypeError,
53
+ );
54
+ throws(
55
+ () => new Object({ name: "singular", names: ["plural"] }),
56
+ TypeError,
57
+ );
58
+ throws(
59
+ () => new Object({ name: 123 as unknown as string }),
60
+ TypeError,
61
+ );
62
+ throws(
63
+ () => new Object({ names: "foo" as unknown as string[] }),
64
+ TypeError,
65
+ );
66
+ throws(
67
+ () => new Object({ names: ["foo", 123 as unknown as string] }),
68
+ TypeError,
69
+ );
70
+ });
71
+
72
+ test("Object.clone()", () => {
73
+ const obj = new Object({
74
+ id: new URL("https://example.com/"),
75
+ name: "Test",
76
+ contents: [
77
+ new LanguageString("Hello", "en"),
78
+ new LanguageString("你好", "zh"),
79
+ ],
80
+ });
81
+
82
+ const clone = obj.clone({ content: "Modified" });
83
+ assertInstanceOf(clone, Object);
84
+ deepStrictEqual(clone.id, new URL("https://example.com/"));
85
+ deepStrictEqual(clone.name, "Test");
86
+ deepStrictEqual(clone.content, "Modified");
87
+
88
+ const cloned2 = obj.clone({ id: new URL("https://example.com/modified") });
89
+ assertInstanceOf(cloned2, Object);
90
+ deepStrictEqual(cloned2.id, new URL("https://example.com/modified"));
91
+ deepStrictEqual(cloned2.name, "Test");
92
+ deepStrictEqual(cloned2.contents, [
93
+ new LanguageString("Hello", "en"),
94
+ new LanguageString("你好", "zh"),
95
+ ]);
96
+
97
+ throws(
98
+ () => obj.clone({ id: 123 as unknown as URL }),
99
+ TypeError,
100
+ );
101
+ throws(
102
+ () => obj.clone({ name: "singular", names: ["plural"] }),
103
+ TypeError,
104
+ );
105
+ throws(
106
+ () => obj.clone({ name: 123 as unknown as string }),
107
+ TypeError,
108
+ );
109
+ throws(
110
+ () => obj.clone({ names: "foo" as unknown as string[] }),
111
+ TypeError,
112
+ );
113
+ throws(
114
+ () => obj.clone({ names: ["foo", 123 as unknown as string] }),
115
+ TypeError,
116
+ );
117
+ });
118
+
119
+ test("Object.fromJsonLd()", async () => {
120
+ const obj = await Object.fromJsonLd({
121
+ "@context": "https://www.w3.org/ns/activitystreams",
122
+ "type": "Object",
123
+ "name": "Test",
124
+ "contentMap": {
125
+ "en": "Hello",
126
+ "zh": "你好",
127
+ },
128
+ "source": {
129
+ "content": "Hello",
130
+ "mediaType": "text/plain",
131
+ },
132
+ "published": "2025-01-01 12:34:56",
133
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
134
+ assertInstanceOf(obj, Object);
135
+ deepStrictEqual(obj.name, "Test");
136
+ deepStrictEqual(obj.contents, [
137
+ new LanguageString("Hello", "en"),
138
+ new LanguageString("你好", "zh"),
139
+ ]);
140
+ assertInstanceOf(obj.source, Source);
141
+ deepStrictEqual(obj.source.content, "Hello");
142
+ deepStrictEqual(obj.source.mediaType, "text/plain");
143
+ deepStrictEqual(obj.published, Temporal.Instant.from("2025-01-01T12:34:56Z"));
144
+
145
+ const createJsonLd = {
146
+ "@context": "https://www.w3.org/ns/activitystreams",
147
+ "type": "Create",
148
+ "name": "Test",
149
+ "contentMap": {
150
+ "en": "Hello",
151
+ "zh": "你好",
152
+ },
153
+ "object": {
154
+ "type": "Note",
155
+ "content": "Content",
156
+ },
157
+ };
158
+ const create = await Object.fromJsonLd(
159
+ createJsonLd,
160
+ { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader },
161
+ );
162
+ assertInstanceOf(create, Create);
163
+ deepStrictEqual(create.name, "Test");
164
+ deepStrictEqual(create.contents, [
165
+ new LanguageString("Hello", "en"),
166
+ new LanguageString("你好", "zh"),
167
+ ]);
168
+ deepStrictEqual(await create.toJsonLd(), createJsonLd);
169
+ const note = await create.getObject();
170
+ assertInstanceOf(note, Note);
171
+ deepStrictEqual(note.content, "Content");
172
+
173
+ const empty = await Object.fromJsonLd({});
174
+ assertInstanceOf(empty, Object);
175
+
176
+ await rejects(
177
+ () => Object.fromJsonLd(null),
178
+ TypeError,
179
+ );
180
+ await rejects(
181
+ () => Object.fromJsonLd(undefined),
182
+ TypeError,
183
+ );
184
+ });
185
+
186
+ test("Object.toJsonLd()", async () => {
187
+ const obj = new Object({
188
+ name: "Test",
189
+ contents: [
190
+ new LanguageString("Hello", "en"),
191
+ new LanguageString("你好", "zh"),
192
+ ],
193
+ });
194
+ deepStrictEqual(
195
+ await obj.toJsonLd({ format: "expand", contextLoader: mockDocumentLoader }),
196
+ [
197
+ {
198
+ "@type": [
199
+ "https://www.w3.org/ns/activitystreams#Object",
200
+ ],
201
+ "https://www.w3.org/ns/activitystreams#name": [
202
+ { "@value": "Test" },
203
+ ],
204
+ "https://www.w3.org/ns/activitystreams#content": [
205
+ { "@value": "Hello", "@language": "en" },
206
+ { "@value": "你好", "@language": "zh" },
207
+ ],
208
+ },
209
+ ],
210
+ );
211
+ deepStrictEqual(await obj.toJsonLd({ contextLoader: mockDocumentLoader }), {
212
+ "@context": [
213
+ "https://www.w3.org/ns/activitystreams",
214
+ "https://w3id.org/security/data-integrity/v1",
215
+ {
216
+ fedibird: "http://fedibird.com/ns#",
217
+ sensitive: "as:sensitive",
218
+ emojiReactions: {
219
+ "@id": "fedibird:emojiReactions",
220
+ "@type": "@id",
221
+ },
222
+ },
223
+ ],
224
+ type: "Object",
225
+ name: "Test",
226
+ contentMap: {
227
+ en: "Hello",
228
+ zh: "你好",
229
+ },
230
+ });
231
+ });
232
+
233
+ test("Note.toJsonLd()", async () => {
234
+ const note = new Note({
235
+ tags: [
236
+ new Hashtag({
237
+ name: "#Fedify",
238
+ href: new URL("https://fedify.dev/"),
239
+ }),
240
+ ],
241
+ });
242
+ deepStrictEqual(await note.toJsonLd({ contextLoader: mockDocumentLoader }), {
243
+ "@context": [
244
+ "https://www.w3.org/ns/activitystreams",
245
+ "https://w3id.org/security/data-integrity/v1",
246
+ {
247
+ Emoji: "toot:Emoji",
248
+ Hashtag: "as:Hashtag",
249
+ _misskey_quote: "misskey:_misskey_quote",
250
+ fedibird: "http://fedibird.com/ns#",
251
+ misskey: "https://misskey-hub.net/ns#",
252
+ quoteUri: "fedibird:quoteUri",
253
+ quoteUrl: "as:quoteUrl",
254
+ sensitive: "as:sensitive",
255
+ toot: "http://joinmastodon.org/ns#",
256
+ emojiReactions: {
257
+ "@id": "fedibird:emojiReactions",
258
+ "@type": "@id",
259
+ },
260
+ },
261
+ ],
262
+ tag: {
263
+ "@context": [
264
+ "https://www.w3.org/ns/activitystreams",
265
+ {
266
+ Hashtag: "as:Hashtag",
267
+ },
268
+ ],
269
+ href: "https://fedify.dev/",
270
+ name: "#Fedify",
271
+ type: "Hashtag",
272
+ },
273
+ type: "Note",
274
+ });
275
+
276
+ const noteWithName = note.clone({
277
+ name: "Test",
278
+ });
279
+ deepStrictEqual(
280
+ await noteWithName.toJsonLd({ contextLoader: mockDocumentLoader }),
281
+ await noteWithName.toJsonLd({
282
+ contextLoader: mockDocumentLoader,
283
+ format: "compact",
284
+ }),
285
+ );
286
+ });
287
+
288
+ test("Activity.fromJsonLd()", async () => {
289
+ const follow = await Activity.fromJsonLd(
290
+ {
291
+ "@context": "https://www.w3.org/ns/activitystreams",
292
+ id: "https://activitypub.academy/80c50305-7405-4e38-809f-697647a1f679",
293
+ type: "Follow",
294
+ actor: "https://activitypub.academy/users/egulia_anbeiss",
295
+ object: "https://example.com/users/hongminhee",
296
+ },
297
+ { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader },
298
+ );
299
+ assertInstanceOf(follow, Follow);
300
+ deepStrictEqual(
301
+ follow.id,
302
+ new URL("https://activitypub.academy/80c50305-7405-4e38-809f-697647a1f679"),
303
+ );
304
+ deepStrictEqual(
305
+ follow.actorId,
306
+ new URL("https://activitypub.academy/users/egulia_anbeiss"),
307
+ );
308
+ deepStrictEqual(
309
+ follow.objectId,
310
+ new URL("https://example.com/users/hongminhee"),
311
+ );
312
+
313
+ const create = await Activity.fromJsonLd(
314
+ {
315
+ "@context": [
316
+ "https://www.w3.org/ns/activitystreams",
317
+ "https://w3id.org/security/data-integrity/v1",
318
+ ],
319
+ type: "Create",
320
+ actor: "https://server.example/users/alice",
321
+ object: {
322
+ type: "Note",
323
+ content: "Hello world",
324
+ },
325
+ proof: {
326
+ type: "DataIntegrityProof",
327
+ cryptosuite: "eddsa-jcs-2022",
328
+ verificationMethod: "https://server.example/users/alice#ed25519-key",
329
+ proofPurpose: "assertionMethod",
330
+ proofValue:
331
+ // cSpell: disable
332
+ "z3sXaxjKs4M3BRicwWA9peyNPJvJqxtGsDmpt1jjoHCjgeUf71TRFz56osPSfDErszyLp5Ks1EhYSgpDaNM977Rg2",
333
+ // cSpell: enable
334
+ created: "2023-02-24T23:36:38Z",
335
+ },
336
+ },
337
+ { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader },
338
+ );
339
+ const proofs: DataIntegrityProof[] = [];
340
+ for await (const proof of create.getProofs()) proofs.push(proof);
341
+ deepStrictEqual(proofs.length, 1);
342
+ deepStrictEqual(proofs[0].cryptosuite, "eddsa-jcs-2022");
343
+ deepStrictEqual(
344
+ proofs[0].verificationMethodId,
345
+ new URL("https://server.example/users/alice#ed25519-key"),
346
+ );
347
+ deepStrictEqual(proofs[0].proofPurpose, "assertionMethod");
348
+ deepStrictEqual(
349
+ proofs[0].proofValue,
350
+ decodeMultibase(
351
+ // cSpell: disable
352
+ "z3sXaxjKs4M3BRicwWA9peyNPJvJqxtGsDmpt1jjoHCjgeUf71TRFz56osPSfDErszyLp5Ks1EhYSgpDaNM977Rg2",
353
+ // cSpell: enable
354
+ ),
355
+ );
356
+ deepStrictEqual(
357
+ proofs[0].created,
358
+ Temporal.Instant.from("2023-02-24T23:36:38Z"),
359
+ );
360
+ });
361
+
362
+ test({
363
+ name: "Activity.getObject()",
364
+ permissions: { env: true, read: true },
365
+ async fn() {
366
+ const activity = new Activity({
367
+ object: new URL("https://example.com/announce"),
368
+ });
369
+ const announce = await activity.getObject({
370
+ documentLoader: mockDocumentLoader,
371
+ contextLoader: mockDocumentLoader,
372
+ });
373
+ assertInstanceOf(announce, Announce);
374
+ deepStrictEqual(announce.id, new URL("https://example.com/announce"));
375
+
376
+ const object = await announce.getObject();
377
+ assertInstanceOf(object, Object);
378
+ deepStrictEqual(object.id, new URL("https://example.com/object"));
379
+ deepStrictEqual(object.name, "Fetched object");
380
+
381
+ // Is hydration applied to toJsonLd()?
382
+ const jsonLd = await activity.toJsonLd();
383
+ deepStrictEqual(jsonLd, {
384
+ "@context": [
385
+ "https://w3id.org/identity/v1",
386
+ "https://www.w3.org/ns/activitystreams",
387
+ "https://w3id.org/security/v1",
388
+ "https://w3id.org/security/data-integrity/v1",
389
+ ],
390
+ type: "Activity",
391
+ object: {
392
+ id: "https://example.com/announce",
393
+ type: "Announce",
394
+ object: {
395
+ type: "Object",
396
+ id: "https://example.com/object",
397
+ name: "Fetched object",
398
+ },
399
+ },
400
+ });
401
+
402
+ const activity2 = new Activity({
403
+ object: new URL("https://example.com/not-found"),
404
+ });
405
+ deepStrictEqual(await activity2.getObject({ suppressError: true }), null);
406
+
407
+ const activity3 = await Activity.fromJsonLd({
408
+ "@context": "https://www.w3.org/ns/activitystreams",
409
+ type: "Create",
410
+ object: {
411
+ "@context": "https://www.w3.org/ns/activitystreams",
412
+ type: "Note",
413
+ content: "Hello world",
414
+ },
415
+ });
416
+ const object3 = await activity3.getObject();
417
+ assertInstanceOf(object3, Note);
418
+ deepStrictEqual(await object3.toJsonLd(), {
419
+ "@context": "https://www.w3.org/ns/activitystreams",
420
+ type: "Note",
421
+ content: "Hello world",
422
+ });
423
+ },
424
+ });
425
+
426
+ test({
427
+ name: "Activity.getObjects()",
428
+ permissions: { env: true, read: true },
429
+ async fn() {
430
+ const activity = new Activity({
431
+ objects: [
432
+ new URL("https://example.com/object"),
433
+ new Object({
434
+ name: "Second object",
435
+ }),
436
+ ],
437
+ });
438
+ const objects = await Array.fromAsync(
439
+ activity.getObjects({
440
+ documentLoader: mockDocumentLoader,
441
+ contextLoader: mockDocumentLoader,
442
+ }),
443
+ );
444
+ deepStrictEqual(objects.length, 2);
445
+ assertInstanceOf(objects[0], Object);
446
+ deepStrictEqual(objects[0].id, new URL("https://example.com/object"));
447
+ deepStrictEqual(objects[0].name, "Fetched object");
448
+ assertInstanceOf(objects[1], Object);
449
+ deepStrictEqual(objects[1].name, "Second object");
450
+
451
+ const activity2 = new Activity({
452
+ objects: [
453
+ new URL("https://example.com/not-found"),
454
+ new Object({
455
+ name: "Second object",
456
+ }),
457
+ ],
458
+ });
459
+ const objects2 = await Array.fromAsync(
460
+ activity2.getObjects({ suppressError: true }),
461
+ );
462
+ deepStrictEqual(objects2.length, 1);
463
+ assertInstanceOf(objects2[0], Object);
464
+ deepStrictEqual(objects2[0].name, "Second object");
465
+ },
466
+ });
467
+
468
+ test("Activity.clone()", async () => {
469
+ const activity = new Activity({
470
+ actor: new Person({
471
+ name: "John Doe",
472
+ }),
473
+ object: new Object({
474
+ name: "Test",
475
+ }),
476
+ name: "Test",
477
+ summary: "Test",
478
+ });
479
+ const clone = activity.clone({
480
+ object: new Object({
481
+ name: "Modified",
482
+ }),
483
+ summary: "Modified",
484
+ });
485
+ deepStrictEqual((await activity.getActor())?.name, "John Doe");
486
+ deepStrictEqual((await clone.getActor())?.name, "John Doe");
487
+ deepStrictEqual((await activity.getObject())?.name, "Test");
488
+ deepStrictEqual((await clone.getObject())?.name, "Modified");
489
+ deepStrictEqual(activity.name, "Test");
490
+ deepStrictEqual(clone.name, "Test");
491
+ deepStrictEqual(activity.summary, "Test");
492
+ deepStrictEqual(clone.summary, "Modified");
493
+
494
+ throws(
495
+ () => activity.clone({ summary: "singular", summaries: ["plural"] }),
496
+ TypeError,
497
+ );
498
+ });
499
+
500
+ test("Question.voters", async () => {
501
+ const question = new Question({
502
+ voters: 123,
503
+ });
504
+ const json = await question.toJsonLd({ format: "compact" });
505
+ ok(typeof json === "object" && json != null);
506
+ ok("votersCount" in json);
507
+ deepStrictEqual((json as Record<string, unknown>)["votersCount"], 123);
508
+ });
509
+
510
+ test({
511
+ name: "Deno.inspect(Object)",
512
+ ignore: !("Deno" in globalThis),
513
+ fn() {
514
+ const obj = new Object({
515
+ id: new URL("https://example.com/"),
516
+ attribution: new URL("https://example.com/foo"),
517
+ name: "Test",
518
+ contents: [
519
+ new LanguageString("Hello", "en"),
520
+ new LanguageString("你好", "zh"),
521
+ ],
522
+ });
523
+ deepStrictEqual(
524
+ Deno.inspect(obj, { colors: false, sorted: true, compact: false }),
525
+ "Deno" in globalThis
526
+ ? "Object {\n" +
527
+ ' attribution: URL "https://example.com/foo",\n' +
528
+ " contents: [\n" +
529
+ ' <en> "Hello",\n' +
530
+ ' <zh> "你好"\n' +
531
+ " ],\n" +
532
+ ' id: URL "https://example.com/",\n' +
533
+ ' name: "Test"\n' +
534
+ "}"
535
+ : "Object {\n" +
536
+ " attribution: URL 'https://example.com/foo',\n" +
537
+ " contents: [\n" +
538
+ " <en> 'Hello',\n" +
539
+ " <zh> '你好'\n" +
540
+ " ],\n" +
541
+ " id: URL 'https://example.com/',\n" +
542
+ " name: 'Test'\n" +
543
+ "}",
544
+ );
545
+ },
546
+ });
547
+
548
+ test("Person.fromJsonLd()", async () => {
549
+ const person = await Person.fromJsonLd({
550
+ "@context": [
551
+ "https://www.w3.org/ns/activitystreams",
552
+ "https://w3id.org/security/v1",
553
+ ],
554
+ "id": "https://todon.eu/users/hongminhee",
555
+ "publicKey": {
556
+ "id": "https://todon.eu/users/hongminhee#main-key",
557
+ "owner": "https://todon.eu/users/hongminhee",
558
+ // cSpell: disable
559
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n" +
560
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsRuvCkgJtflBTl4OVsm\n" +
561
+ "nt/J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR\n" +
562
+ "5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI+ezn24GHsZ/v1JIo77lerX5\n" +
563
+ "k4HNwTNVt+yaZVQWaOMR3+6FwziQR6kd0VuG9/a9dgAnz2cEoORRC1i4W7IZaB1s\n" +
564
+ "Znh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQh\n" +
565
+ "Ie/YUBOGj/ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2\n" +
566
+ "uwIDAQAB\n" +
567
+ "-----END PUBLIC KEY-----\n",
568
+ // cSpell: enable
569
+ },
570
+ }, {
571
+ documentLoader: mockDocumentLoader,
572
+ contextLoader: mockDocumentLoader,
573
+ baseUrl: new URL("https://todon.eu/"),
574
+ });
575
+ deepStrictEqual(
576
+ person.publicKeyId,
577
+ new URL("https://todon.eu/users/hongminhee#main-key"),
578
+ );
579
+ const publicKey = await person.getPublicKey({
580
+ documentLoader: mockDocumentLoader,
581
+ });
582
+ assertInstanceOf(publicKey, CryptographicKey);
583
+ deepStrictEqual(
584
+ publicKey?.ownerId,
585
+ new URL("https://todon.eu/users/hongminhee"),
586
+ );
587
+
588
+ const person2 = await Person.fromJsonLd({
589
+ "@context": [
590
+ "https://www.w3.org/ns/activitystreams",
591
+ {
592
+ alsoKnownAs: {
593
+ "@id": "as:alsoKnownAs",
594
+ "@type": "@id",
595
+ },
596
+ },
597
+ ],
598
+ "type": "Person",
599
+ // cSpell: disable
600
+ "alsoKnownAs": "at://did:plc:x7xdowahlhm5xulzqw4ehv6q",
601
+ // cSpell: enable
602
+ });
603
+ deepStrictEqual(
604
+ person2.aliasId,
605
+ // cSpell: disable
606
+ new URL("at://did%3Aplc%3Ax7xdowahlhm5xulzqw4ehv6q"),
607
+ // cSpell: enable
608
+ );
609
+ });
610
+
611
+ test("Person.toJsonLd()", async () => {
612
+ const person = new Person({
613
+ aliases: [new URL("https://example.com/alias")],
614
+ });
615
+ deepStrictEqual(await person.toJsonLd(), {
616
+ "@context": [
617
+ "https://www.w3.org/ns/activitystreams",
618
+ "https://w3id.org/security/v1",
619
+ "https://w3id.org/security/data-integrity/v1",
620
+ "https://www.w3.org/ns/did/v1",
621
+ "https://w3id.org/security/multikey/v1",
622
+ {
623
+ PropertyValue: "schema:PropertyValue",
624
+ alsoKnownAs: {
625
+ "@id": "as:alsoKnownAs",
626
+ "@type": "@id",
627
+ },
628
+ movedTo: {
629
+ "@id": "as:movedTo",
630
+ "@type": "@id",
631
+ },
632
+ discoverable: "toot:discoverable",
633
+ featured: {
634
+ "@id": "toot:featured",
635
+ "@type": "@id",
636
+ },
637
+ featuredTags: {
638
+ "@id": "toot:featuredTags",
639
+ "@type": "@id",
640
+ },
641
+ indexable: "toot:indexable",
642
+ _misskey_followedMessage: "misskey:_misskey_followedMessage",
643
+ isCat: "misskey:isCat",
644
+ manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
645
+ memorial: "toot:memorial",
646
+ misskey: "https://misskey-hub.net/ns#",
647
+ schema: "http://schema.org#",
648
+ suspended: "toot:suspended",
649
+ toot: "http://joinmastodon.org/ns#",
650
+ value: "schema:value",
651
+ Emoji: "toot:Emoji",
652
+ },
653
+ ],
654
+ alsoKnownAs: "https://example.com/alias",
655
+ type: "Person",
656
+ });
657
+ });
658
+
659
+ test("Collection.fromJsonLd()", async () => {
660
+ const collection = await Collection.fromJsonLd({
661
+ "@context": [
662
+ "https://www.w3.org/ns/activitystreams",
663
+ "https://w3id.org/fep/5711",
664
+ ],
665
+ "type": "Collection",
666
+ "id": "https://example.com/collection/jzc50wc28l",
667
+ "inboxOf": "https://example.com/person/bup9a8eqm",
668
+ });
669
+ deepStrictEqual(
670
+ collection.id,
671
+ new URL("https://example.com/collection/jzc50wc28l"),
672
+ );
673
+ deepStrictEqual(
674
+ collection.inboxOfId,
675
+ new URL("https://example.com/person/bup9a8eqm"),
676
+ );
677
+ });
678
+
679
+ test("Note.quoteUrl", async () => {
680
+ const note = new Note({
681
+ quoteUrl: new URL("https://example.com/object"),
682
+ });
683
+ const expected = {
684
+ "@context": [
685
+ "https://www.w3.org/ns/activitystreams",
686
+ "https://w3id.org/security/data-integrity/v1",
687
+ {
688
+ Emoji: "toot:Emoji",
689
+ Hashtag: "as:Hashtag",
690
+ _misskey_quote: "misskey:_misskey_quote",
691
+ fedibird: "http://fedibird.com/ns#",
692
+ misskey: "https://misskey-hub.net/ns#",
693
+ quoteUri: "fedibird:quoteUri",
694
+ quoteUrl: "as:quoteUrl",
695
+ sensitive: "as:sensitive",
696
+ toot: "http://joinmastodon.org/ns#",
697
+ emojiReactions: {
698
+ "@id": "fedibird:emojiReactions",
699
+ "@type": "@id",
700
+ },
701
+ },
702
+ ],
703
+ _misskey_quote: "https://example.com/object",
704
+ quoteUri: "https://example.com/object",
705
+ quoteUrl: "https://example.com/object",
706
+ type: "Note",
707
+ };
708
+ deepStrictEqual(await note.toJsonLd(), expected);
709
+ deepStrictEqual(await note.toJsonLd({ format: "compact" }), expected);
710
+
711
+ const jsonLd: Record<string, unknown> = {
712
+ "@context": [
713
+ "https://www.w3.org/ns/activitystreams",
714
+ {
715
+ _misskey_quote: "misskey:_misskey_quote",
716
+ fedibird: "http://fedibird.com/ns#",
717
+ misskey: "https://misskey-hub.net/ns#",
718
+ quoteUri: "fedibird:quoteUri",
719
+ quoteUrl: "as:quoteUrl",
720
+ },
721
+ ],
722
+ type: "Note",
723
+ quoteUrl: "https://example.com/object",
724
+ _misskey_quote: "https://example.com/object2",
725
+ quoteUri: "https://example.com/object3",
726
+ };
727
+ const loaded = await Note.fromJsonLd(jsonLd);
728
+ deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/object"));
729
+
730
+ delete jsonLd.quoteUrl;
731
+ const loaded2 = await Note.fromJsonLd(jsonLd);
732
+ deepStrictEqual(loaded2.quoteUrl, new URL("https://example.com/object2"));
733
+
734
+ delete jsonLd._misskey_quote;
735
+ const loaded3 = await Note.fromJsonLd(jsonLd);
736
+ deepStrictEqual(loaded3.quoteUrl, new URL("https://example.com/object3"));
737
+ });
738
+
739
+ test("Key.publicKey", async () => {
740
+ const jwk = {
741
+ kty: "RSA",
742
+ alg: "RS256",
743
+ // cSpell: disable
744
+ n: "xsRuvCkgJtflBTl4OVsmnt_J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWN" +
745
+ "LqC4eogkJaeJ4RR5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI-ezn24GHsZ_v1J" +
746
+ "Io77lerX5k4HNwTNVt-yaZVQWaOMR3-6FwziQR6kd0VuG9_a9dgAnz2cEoORRC1i4W7IZa" +
747
+ "B1sZnh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQhIe_" +
748
+ "YUBOGj_ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2uw",
749
+ e: "AQAB",
750
+ // cSpell: enable
751
+ key_ops: ["verify"],
752
+ ext: true,
753
+ };
754
+ const key = new CryptographicKey({
755
+ publicKey: await crypto.subtle.importKey(
756
+ "jwk",
757
+ jwk,
758
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
759
+ true,
760
+ ["verify"],
761
+ ),
762
+ });
763
+ const jsonLd = await key.toJsonLd({ contextLoader: mockDocumentLoader });
764
+ deepStrictEqual(jsonLd, {
765
+ "@context": "https://w3id.org/security/v1",
766
+ publicKeyPem: "-----BEGIN PUBLIC KEY-----\n" +
767
+ // cSpell: disable
768
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsRuvCkgJtflBTl4OVsm\n" +
769
+ "nt/J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR\n" +
770
+ "5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI+ezn24GHsZ/v1JIo77lerX5\n" +
771
+ "k4HNwTNVt+yaZVQWaOMR3+6FwziQR6kd0VuG9/a9dgAnz2cEoORRC1i4W7IZaB1s\n" +
772
+ "Znh1WbHbevlGd72HSXll5rocPIHn8gq6xpBgpHwRphlRsgn4KHaJ6brXDIJjrnQh\n" +
773
+ "Ie/YUBOGj/ImSEXhRwlFerKsoAVnZ0Hwbfa46qk44TAt8CyoPMWmpK6pt0ng4pQ2\n" +
774
+ "uwIDAQAB\n" +
775
+ // cSpell: enable
776
+ "-----END PUBLIC KEY-----\n",
777
+ type: "CryptographicKey",
778
+ });
779
+ const loadedKey = await CryptographicKey.fromJsonLd(jsonLd, {
780
+ documentLoader: mockDocumentLoader,
781
+ contextLoader: mockDocumentLoader,
782
+ });
783
+ notDeepStrictEqual(loadedKey.publicKey, null);
784
+ deepStrictEqual(
785
+ await crypto.subtle.exportKey("jwk", loadedKey.publicKey!),
786
+ jwk,
787
+ );
788
+ });
789
+
790
+ test("Place.fromJsonLd()", async () => {
791
+ const place = await Place.fromJsonLd({
792
+ "@context": "https://www.w3.org/ns/activitystreams",
793
+ type: "Place",
794
+ name: "Fresno Area",
795
+ latitude: 36.75,
796
+ longitude: 119.7667,
797
+ radius: 15,
798
+ units: "miles",
799
+ }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader });
800
+ assertInstanceOf(place, Place);
801
+ deepStrictEqual(place.name, "Fresno Area");
802
+ deepStrictEqual(place.latitude, 36.75);
803
+ deepStrictEqual(place.longitude, 119.7667);
804
+ deepStrictEqual(place.radius, 15);
805
+ deepStrictEqual(place.units, "miles");
806
+
807
+ let jsonLd = await place.toJsonLd({ contextLoader: mockDocumentLoader });
808
+ deepStrictEqual(jsonLd, {
809
+ "@context": "https://www.w3.org/ns/activitystreams",
810
+ type: "Place",
811
+ name: "Fresno Area",
812
+ latitude: 36.75,
813
+ longitude: 119.7667,
814
+ radius: 15,
815
+ units: "miles",
816
+ });
817
+
818
+ jsonLd = await place.toJsonLd({
819
+ format: "compact",
820
+ contextLoader: mockDocumentLoader,
821
+ });
822
+ deepStrictEqual(jsonLd, {
823
+ "@context": [
824
+ "https://www.w3.org/ns/activitystreams",
825
+ "https://w3id.org/security/data-integrity/v1",
826
+ ],
827
+ type: "Place",
828
+ name: "Fresno Area",
829
+ latitude: 36.75,
830
+ longitude: 119.7667,
831
+ radius: 15,
832
+ units: "miles",
833
+ });
834
+ });
835
+
836
+ test("Actor.getOutbox()", async () => {
837
+ const person = new Person({
838
+ outbox: new URL("https://example.com/orderedcollectionpage"),
839
+ });
840
+ const outbox = await person.getOutbox({ documentLoader: mockDocumentLoader });
841
+ assertInstanceOf(outbox, OrderedCollectionPage);
842
+ deepStrictEqual(outbox.totalItems, 1);
843
+ });
844
+
845
+ test("Link.fromJsonLd()", async () => {
846
+ const link = await Link.fromJsonLd({
847
+ "@context": "https://www.w3.org/ns/activitystreams",
848
+ "type": "Link",
849
+ "rel": "canonical",
850
+ "href":
851
+ "at://did:plc:ia76kvnndjutgedggx2ibrem/app.bsky.feed.post/3lyxjjs27jkqg",
852
+ });
853
+ deepStrictEqual(link.rel, "canonical");
854
+ deepStrictEqual(
855
+ link.href,
856
+ new URL(
857
+ "at://did%3Aplc%3Aia76kvnndjutgedggx2ibrem/app.bsky.feed.post/3lyxjjs27jkqg",
858
+ ),
859
+ );
860
+
861
+ const link2 = await Link.fromJsonLd({
862
+ "@context": "https://www.w3.org/ns/activitystreams",
863
+ "type": "Link",
864
+ "href": "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26",
865
+ });
866
+ deepStrictEqual(
867
+ link2.href,
868
+ new URL("at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"),
869
+ );
870
+
871
+ const link3 = await Link.fromJsonLd({
872
+ "@context": "https://www.w3.org/ns/activitystreams",
873
+ "type": "Link",
874
+ "href": "at://did:plc:ia76kvnndjutgedggx2ibrem",
875
+ });
876
+ deepStrictEqual(
877
+ link3.href,
878
+ new URL("at://did%3Aplc%3Aia76kvnndjutgedggx2ibrem"),
879
+ );
880
+ });
881
+
882
+ test("Person.fromJsonLd() with relative URLs", async () => {
883
+ const json = {
884
+ "@context": [
885
+ "https://www.w3.org/ns/activitystreams",
886
+ "https://w3id.org/security/v1",
887
+ ],
888
+ id: "https://example.com/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c",
889
+ type: "Person",
890
+ name: "Test User",
891
+ icon: { type: "Image", url: "/avatars/test-avatar.jpg" },
892
+ };
893
+
894
+ const person = await Person.fromJsonLd(json, {
895
+ documentLoader: mockDocumentLoader,
896
+ contextLoader: mockDocumentLoader,
897
+ });
898
+
899
+ const icon = await person.getIcon();
900
+ deepStrictEqual(
901
+ icon?.url,
902
+ new URL("https://example.com/avatars/test-avatar.jpg"),
903
+ );
904
+
905
+ const json2 = {
906
+ "@context": [
907
+ "https://www.w3.org/ns/activitystreams",
908
+ "https://w3id.org/security/v1",
909
+ ],
910
+ id: "https://example.com/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c",
911
+ type: "Person",
912
+ name: "Test User",
913
+ icon: {
914
+ id: "https://media.example.com/avatars/test-avatar.jpg",
915
+ type: "Image",
916
+ url: "/avatars/test-avatar.jpg",
917
+ },
918
+ };
919
+
920
+ const person2 = await Person.fromJsonLd(json2, {
921
+ documentLoader: mockDocumentLoader,
922
+ contextLoader: mockDocumentLoader,
923
+ });
924
+
925
+ const icon2 = await person2.getIcon();
926
+ deepStrictEqual(
927
+ icon2?.url,
928
+ new URL("https://media.example.com/avatars/test-avatar.jpg"),
929
+ );
930
+ });
931
+
932
+ test("Person.fromJsonLd() with relative URLs and baseUrl", async () => {
933
+ const json = {
934
+ "@context": [
935
+ "https://www.w3.org/ns/activitystreams",
936
+ "https://w3id.org/security/v1",
937
+ ],
938
+ "id": "https://example.com/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c",
939
+ "type": "Person",
940
+ "name": "Test User",
941
+ "icon": {
942
+ "type": "Image",
943
+ "url": "/avatars/test-avatar.jpg",
944
+ },
945
+ };
946
+
947
+ const personWithBase = await Person.fromJsonLd(json, {
948
+ documentLoader: mockDocumentLoader,
949
+ contextLoader: mockDocumentLoader,
950
+ baseUrl: new URL("https://example.com"),
951
+ });
952
+
953
+ const icon = await personWithBase.getIcon();
954
+ deepStrictEqual(
955
+ icon?.url,
956
+ new URL("https://example.com/avatars/test-avatar.jpg"),
957
+ );
958
+ });
959
+
960
+ test("FEP-fe34: Trust tracking in object construction", async () => {
961
+ // Test that objects created with embedded objects have trust set
962
+ const note = new Note({
963
+ id: new URL("https://example.com/note"),
964
+ content: "Hello World",
965
+ });
966
+
967
+ const create = new Create({
968
+ id: new URL("https://example.com/create"),
969
+ actor: new URL("https://example.com/actor"),
970
+ object: note, // Embedded object should be trusted
971
+ });
972
+
973
+ // Trust should be automatically set for embedded objects during construction
974
+ // We can verify this by checking that the object is returned immediately
975
+ // without requiring remote fetching
976
+ deepStrictEqual(create.objectId, new URL("https://example.com/note"));
977
+
978
+ // Should return the embedded object directly (no remote fetch needed)
979
+ const result = await create.getObject();
980
+ deepStrictEqual(result, note);
981
+ deepStrictEqual(result?.content, "Hello World");
982
+ });
983
+
984
+ test("FEP-fe34: Trust tracking in object cloning", () => {
985
+ const originalNote = new Note({
986
+ id: new URL("https://example.com/note"),
987
+ content: "Original content",
988
+ });
989
+
990
+ const create = new Create({
991
+ id: new URL("https://example.com/create"),
992
+ actor: new URL("https://example.com/actor"),
993
+ object: originalNote,
994
+ });
995
+
996
+ const newNote = new Note({
997
+ id: new URL("https://example.com/new-note"),
998
+ content: "New content",
999
+ });
1000
+
1001
+ // Clone with a new embedded object - should establish new trust
1002
+ const clonedCreate = create.clone({
1003
+ object: newNote,
1004
+ });
1005
+
1006
+ deepStrictEqual(
1007
+ clonedCreate.objectId,
1008
+ new URL("https://example.com/new-note"),
1009
+ );
1010
+ });
1011
+
1012
+ test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
1013
+ // Create a mock document loader that returns objects with different origins
1014
+ // deno-lint-ignore require-await
1015
+ const crossOriginDocumentLoader = async (url: string) => {
1016
+ if (url === "https://different-origin.com/note") {
1017
+ return {
1018
+ documentUrl: url,
1019
+ contextUrl: null,
1020
+ document: {
1021
+ "@context": "https://www.w3.org/ns/activitystreams",
1022
+ "@type": "Note",
1023
+ "@id": "https://malicious.com/fake-note", // Different origin!
1024
+ "content": "This is a spoofed note",
1025
+ },
1026
+ };
1027
+ }
1028
+ throw new Error("Document not found");
1029
+ };
1030
+
1031
+ const create = new Create({
1032
+ id: new URL("https://example.com/create"),
1033
+ actor: new URL("https://example.com/actor"),
1034
+ object: new URL("https://different-origin.com/note"),
1035
+ });
1036
+
1037
+ // Default behavior should ignore cross-origin objects and return null
1038
+ const result = await create.getObject({
1039
+ documentLoader: crossOriginDocumentLoader,
1040
+ });
1041
+ deepStrictEqual(result, null);
1042
+ });
1043
+
1044
+ test("FEP-fe34: crossOrigin throw behavior", async () => {
1045
+ // deno-lint-ignore require-await
1046
+ const crossOriginDocumentLoader = async (url: string) => {
1047
+ if (url === "https://different-origin.com/note") {
1048
+ return {
1049
+ documentUrl: url,
1050
+ contextUrl: null,
1051
+ document: {
1052
+ "@context": "https://www.w3.org/ns/activitystreams",
1053
+ "@type": "Note",
1054
+ "@id": "https://malicious.com/fake-note", // Different origin!
1055
+ "content": "This is a spoofed note",
1056
+ },
1057
+ };
1058
+ }
1059
+ throw new Error("Document not found");
1060
+ };
1061
+
1062
+ const create = new Create({
1063
+ id: new URL("https://example.com/create"),
1064
+ actor: new URL("https://example.com/actor"),
1065
+ object: new URL("https://different-origin.com/note"),
1066
+ });
1067
+
1068
+ // Should throw an error when encountering cross-origin objects
1069
+ await rejects(
1070
+ () =>
1071
+ create.getObject({
1072
+ documentLoader: crossOriginDocumentLoader,
1073
+ crossOrigin: "throw",
1074
+ }),
1075
+ Error,
1076
+ );
1077
+ });
1078
+
1079
+ test("FEP-fe34: crossOrigin trust behavior", async () => {
1080
+ // deno-lint-ignore require-await
1081
+ const crossOriginDocumentLoader = async (url: string) => {
1082
+ if (url === "https://different-origin.com/note") {
1083
+ return {
1084
+ documentUrl: url,
1085
+ contextUrl: null,
1086
+ document: {
1087
+ "@context": "https://www.w3.org/ns/activitystreams",
1088
+ "@type": "Note",
1089
+ "@id": "https://malicious.com/fake-note", // Different origin!
1090
+ "content": "This is a spoofed note",
1091
+ },
1092
+ };
1093
+ }
1094
+ throw new Error("Document not found");
1095
+ };
1096
+
1097
+ const create = new Create({
1098
+ id: new URL("https://example.com/create"),
1099
+ actor: new URL("https://example.com/actor"),
1100
+ object: new URL("https://different-origin.com/note"),
1101
+ });
1102
+
1103
+ // Should bypass origin checks and return the object
1104
+ const result = await create.getObject({
1105
+ documentLoader: crossOriginDocumentLoader,
1106
+ crossOrigin: "trust",
1107
+ });
1108
+
1109
+ assertInstanceOf(result, Note);
1110
+ deepStrictEqual(result?.id, new URL("https://malicious.com/fake-note"));
1111
+ deepStrictEqual(result?.content, "This is a spoofed note");
1112
+ });
1113
+
1114
+ test("FEP-fe34: Same origin objects are trusted", async () => {
1115
+ // deno-lint-ignore require-await
1116
+ const sameOriginDocumentLoader = async (url: string) => {
1117
+ if (url === "https://example.com/note") {
1118
+ return {
1119
+ documentUrl: url,
1120
+ contextUrl: null,
1121
+ document: {
1122
+ "@context": "https://www.w3.org/ns/activitystreams",
1123
+ "@type": "Note",
1124
+ "@id": "https://example.com/note", // Same origin
1125
+ "content": "This is a legitimate note",
1126
+ },
1127
+ };
1128
+ }
1129
+ throw new Error("Document not found");
1130
+ };
1131
+
1132
+ const create = new Create({
1133
+ id: new URL("https://example.com/create"),
1134
+ actor: new URL("https://example.com/actor"),
1135
+ object: new URL("https://example.com/note"),
1136
+ });
1137
+
1138
+ // Same origin objects should be returned normally
1139
+ const result = await create.getObject({
1140
+ documentLoader: sameOriginDocumentLoader,
1141
+ });
1142
+
1143
+ assertInstanceOf(result, Note);
1144
+ deepStrictEqual(result?.id, new URL("https://example.com/note"));
1145
+ deepStrictEqual(result?.content, "This is a legitimate note");
1146
+ });
1147
+
1148
+ test(
1149
+ "FEP-fe34: Embedded cross-origin objects from JSON-LD are ignored by default",
1150
+ async () => {
1151
+ // Mock document loader for creating the Create object from JSON-LD
1152
+ // deno-lint-ignore require-await
1153
+ const createDocumentLoader = async (url: string) => {
1154
+ if (url === "https://example.com/create") {
1155
+ return {
1156
+ documentUrl: url,
1157
+ contextUrl: null,
1158
+ document: {
1159
+ "@context": "https://www.w3.org/ns/activitystreams",
1160
+ "@type": "Create",
1161
+ "@id": "https://example.com/create",
1162
+ "actor": "https://example.com/actor",
1163
+ "object": {
1164
+ "@type": "Note",
1165
+ // Different origin from parent!
1166
+ "@id": "https://different-origin.com/note",
1167
+ "content": "Embedded note from JSON-LD",
1168
+ },
1169
+ },
1170
+ };
1171
+ }
1172
+ throw new Error("Document not found");
1173
+ };
1174
+
1175
+ // Create object from JSON-LD (embedded objects won't be trusted)
1176
+ const create = await Create.fromJsonLd(
1177
+ await createDocumentLoader("https://example.com/create").then((r) =>
1178
+ r.document
1179
+ ),
1180
+ { documentLoader: createDocumentLoader },
1181
+ );
1182
+
1183
+ // Mock document loader that would return the "legitimate" version
1184
+ // deno-lint-ignore require-await
1185
+ const objectDocumentLoader = async (url: string) => {
1186
+ if (url === "https://different-origin.com/note") {
1187
+ return {
1188
+ documentUrl: url,
1189
+ contextUrl: null,
1190
+ document: {
1191
+ "@context": "https://www.w3.org/ns/activitystreams",
1192
+ "@type": "Note",
1193
+ "@id": "https://different-origin.com/note",
1194
+ "content": "Legitimate note from origin",
1195
+ },
1196
+ };
1197
+ }
1198
+ throw new Error("Document not found");
1199
+ };
1200
+
1201
+ // Should fetch from origin instead of trusting embedded object
1202
+ const result = await create.getObject({
1203
+ documentLoader: objectDocumentLoader,
1204
+ });
1205
+ assertInstanceOf(result, Note);
1206
+ deepStrictEqual(result?.content, "Legitimate note from origin");
1207
+ },
1208
+ );
1209
+
1210
+ test("FEP-fe34: Constructor vs JSON-LD parsing trust difference", async () => {
1211
+ // 1. Constructor-created objects: embedded objects are trusted
1212
+ const constructorCreate = new Create({
1213
+ id: new URL("https://example.com/create"),
1214
+ actor: new URL("https://example.com/actor"),
1215
+ object: new Note({
1216
+ id: new URL("https://different-origin.com/note"), // Different origin!
1217
+ content: "Constructor embedded note",
1218
+ }),
1219
+ });
1220
+
1221
+ // Should return the embedded object directly (trusted)
1222
+ const constructorResult = await constructorCreate.getObject();
1223
+ deepStrictEqual(constructorResult?.content, "Constructor embedded note");
1224
+
1225
+ // 2. JSON-LD parsed objects: embedded objects are NOT trusted
1226
+ const jsonLdCreate = await Create.fromJsonLd({
1227
+ "@context": "https://www.w3.org/ns/activitystreams",
1228
+ "@type": "Create",
1229
+ "@id": "https://example.com/create",
1230
+ "actor": "https://example.com/actor",
1231
+ "object": {
1232
+ "@type": "Note",
1233
+ "@id": "https://different-origin.com/note", // Same different origin!
1234
+ "content": "JSON-LD embedded note",
1235
+ },
1236
+ });
1237
+
1238
+ // Mock document loader for the cross-origin fetch
1239
+ // deno-lint-ignore require-await
1240
+ const documentLoader = async (url: string) => {
1241
+ if (url === "https://different-origin.com/note") {
1242
+ return {
1243
+ documentUrl: url,
1244
+ contextUrl: null,
1245
+ document: {
1246
+ "@context": "https://www.w3.org/ns/activitystreams",
1247
+ "@type": "Note",
1248
+ "@id": "https://different-origin.com/note",
1249
+ "content": "Fetched from origin",
1250
+ },
1251
+ };
1252
+ }
1253
+ throw new Error("Document not found");
1254
+ };
1255
+
1256
+ // Should fetch from origin instead of using embedded object (not trusted)
1257
+ const jsonLdResult = await jsonLdCreate.getObject({ documentLoader });
1258
+ deepStrictEqual(jsonLdResult?.content, "Fetched from origin");
1259
+ });
1260
+
1261
+ test("FEP-fe34: Array properties respect cross-origin policy", async () => {
1262
+ // deno-lint-ignore require-await
1263
+ const crossOriginDocumentLoader = async (url: string) => {
1264
+ if (url === "https://different-origin.com/note1") {
1265
+ return {
1266
+ documentUrl: url,
1267
+ contextUrl: null,
1268
+ document: {
1269
+ "@context": "https://www.w3.org/ns/activitystreams",
1270
+ "@type": "Note",
1271
+ "@id": "https://malicious.com/fake-note1", // Different origin!
1272
+ "content": "Fake note 1",
1273
+ },
1274
+ };
1275
+ } else if (url === "https://example.com/note2") {
1276
+ return {
1277
+ documentUrl: url,
1278
+ contextUrl: null,
1279
+ document: {
1280
+ "@context": "https://www.w3.org/ns/activitystreams",
1281
+ "@type": "Note",
1282
+ "@id": "https://example.com/note2", // Same origin
1283
+ "content": "Legitimate note 2",
1284
+ },
1285
+ };
1286
+ }
1287
+ throw new Error("Document not found");
1288
+ };
1289
+
1290
+ const collection = new Collection({
1291
+ id: new URL("https://example.com/collection"),
1292
+ items: [
1293
+ new URL("https://different-origin.com/note1"), // Cross-origin
1294
+ new URL("https://example.com/note2"), // Same origin
1295
+ ],
1296
+ });
1297
+
1298
+ const items = [];
1299
+ for await (
1300
+ const item of collection.getItems({
1301
+ documentLoader: crossOriginDocumentLoader,
1302
+ })
1303
+ ) {
1304
+ items.push(item);
1305
+ }
1306
+
1307
+ // Should only get the same-origin item, cross-origin item should be filtered out
1308
+ deepStrictEqual(items.length, 1);
1309
+ assertInstanceOf(items[0], Note);
1310
+ deepStrictEqual((items[0] as Note).content, "Legitimate note 2");
1311
+ });
1312
+
1313
+ test("FEP-fe34: Array properties with crossOrigin trust option", async () => {
1314
+ // deno-lint-ignore require-await
1315
+ const crossOriginDocumentLoader = async (url: string) => {
1316
+ if (url === "https://different-origin.com/note1") {
1317
+ return {
1318
+ documentUrl: url,
1319
+ contextUrl: null,
1320
+ document: {
1321
+ "@context": "https://www.w3.org/ns/activitystreams",
1322
+ "@type": "Note",
1323
+ "@id": "https://malicious.com/fake-note1", // Different origin!
1324
+ "content": "Fake note 1",
1325
+ },
1326
+ };
1327
+ } else if (url === "https://example.com/note2") {
1328
+ return {
1329
+ documentUrl: url,
1330
+ contextUrl: null,
1331
+ document: {
1332
+ "@context": "https://www.w3.org/ns/activitystreams",
1333
+ "@type": "Note",
1334
+ "@id": "https://example.com/note2", // Same origin
1335
+ "content": "Legitimate note 2",
1336
+ },
1337
+ };
1338
+ }
1339
+ throw new Error("Document not found");
1340
+ };
1341
+
1342
+ const collection = new Collection({
1343
+ id: new URL("https://example.com/collection"),
1344
+ items: [
1345
+ new URL("https://different-origin.com/note1"), // Cross-origin
1346
+ new URL("https://example.com/note2"), // Same origin
1347
+ ],
1348
+ });
1349
+
1350
+ const items = [];
1351
+ for await (
1352
+ const item of collection.getItems({
1353
+ documentLoader: crossOriginDocumentLoader,
1354
+ crossOrigin: "trust",
1355
+ })
1356
+ ) {
1357
+ items.push(item);
1358
+ }
1359
+
1360
+ // Should get both items when trust mode is enabled
1361
+ deepStrictEqual(items.length, 2);
1362
+ assertInstanceOf(items[0], Note);
1363
+ assertInstanceOf(items[1], Note);
1364
+ deepStrictEqual((items[0] as Note).content, "Fake note 1");
1365
+ deepStrictEqual((items[1] as Note).content, "Legitimate note 2");
1366
+ });
1367
+
1368
+ test(
1369
+ "FEP-fe34: Embedded objects in arrays from JSON-LD respect cross-origin policy",
1370
+ async () => {
1371
+ // Mock document loader for creating the Collection object from JSON-LD
1372
+ // deno-lint-ignore require-await
1373
+ const collectionDocumentLoader = async (url: string) => {
1374
+ if (url === "https://example.com/collection") {
1375
+ return {
1376
+ documentUrl: url,
1377
+ contextUrl: null,
1378
+ document: {
1379
+ "@context": "https://www.w3.org/ns/activitystreams",
1380
+ "@type": "Collection",
1381
+ "@id": "https://example.com/collection",
1382
+ "items": [
1383
+ {
1384
+ "@type": "Note",
1385
+ "@id": "https://example.com/trusted-note", // Same origin
1386
+ "content": "Trusted embedded note from JSON-LD",
1387
+ },
1388
+ {
1389
+ "@type": "Note",
1390
+ "@id": "https://different-origin.com/untrusted-note", // Different origin!
1391
+ "content": "Untrusted embedded note from JSON-LD",
1392
+ },
1393
+ ],
1394
+ },
1395
+ };
1396
+ }
1397
+ throw new Error("Document not found");
1398
+ };
1399
+
1400
+ // Create collection from JSON-LD (embedded objects won't be trusted)
1401
+ const collection = await Collection.fromJsonLd(
1402
+ await collectionDocumentLoader("https://example.com/collection").then((
1403
+ r,
1404
+ ) => r.document),
1405
+ { documentLoader: collectionDocumentLoader },
1406
+ );
1407
+
1408
+ // Mock document loader for fetching objects
1409
+ // deno-lint-ignore require-await
1410
+ const itemDocumentLoader = async (url: string) => {
1411
+ if (url === "https://example.com/trusted-note") {
1412
+ return {
1413
+ documentUrl: url,
1414
+ contextUrl: null,
1415
+ document: {
1416
+ "@context": "https://www.w3.org/ns/activitystreams",
1417
+ "@type": "Note",
1418
+ "@id": "https://example.com/trusted-note",
1419
+ "content": "Trusted note from origin",
1420
+ },
1421
+ };
1422
+ } else if (url === "https://different-origin.com/untrusted-note") {
1423
+ return {
1424
+ documentUrl: url,
1425
+ contextUrl: null,
1426
+ document: {
1427
+ "@context": "https://www.w3.org/ns/activitystreams",
1428
+ "@type": "Note",
1429
+ "@id": "https://different-origin.com/untrusted-note",
1430
+ "content": "Legitimate note from actual origin",
1431
+ },
1432
+ };
1433
+ }
1434
+ throw new Error("Document not found");
1435
+ };
1436
+
1437
+ const items = [];
1438
+ for await (
1439
+ const item of collection.getItems({ documentLoader: itemDocumentLoader })
1440
+ ) {
1441
+ items.push(item);
1442
+ }
1443
+
1444
+ // Should get both items
1445
+ deepStrictEqual(items.length, 2);
1446
+
1447
+ // First item (same origin) - should use embedded object since it's same-origin as parent
1448
+ assertInstanceOf(items[0], Note);
1449
+ deepStrictEqual(
1450
+ (items[0] as Note).content,
1451
+ "Trusted embedded note from JSON-LD",
1452
+ );
1453
+
1454
+ // Second item (cross-origin) - should be fetched from origin, not embedded version
1455
+ assertInstanceOf(items[1], Note);
1456
+ deepStrictEqual(
1457
+ (items[1] as Note).content,
1458
+ "Legitimate note from actual origin",
1459
+ );
1460
+ },
1461
+ );
1462
+
1463
+ function getAllProperties(
1464
+ type: TypeSchema,
1465
+ types: Record<string, TypeSchema>,
1466
+ ): PropertySchema[] {
1467
+ const props: PropertySchema[] = type.properties;
1468
+ if (type.extends != null) {
1469
+ props.push(...getAllProperties(types[type.extends], types));
1470
+ }
1471
+ return props;
1472
+ }
1473
+
1474
+ const ed25519PublicKey = new CryptographicKey({
1475
+ id: new URL("https://example.com/person2#key4"),
1476
+ owner: new URL("https://example.com/person2"),
1477
+ publicKey: await crypto.subtle.importKey(
1478
+ "jwk",
1479
+ {
1480
+ crv: "Ed25519",
1481
+ ext: true,
1482
+ key_ops: ["verify"],
1483
+ kty: "OKP",
1484
+ // cSpell: disable
1485
+ x: "LR8epAGDe-cVq5p2Tx49CCfphpk1rNhkNoY9i-XEUfg",
1486
+ // cSpell: enable
1487
+ },
1488
+ "Ed25519",
1489
+ true,
1490
+ ["verify"],
1491
+ ),
1492
+ }) as CryptographicKey & { publicKey: CryptoKey };
1493
+
1494
+ const rsaPublicKey = new CryptographicKey({
1495
+ id: new URL("https://example.com/key"),
1496
+ owner: new URL("https://example.com/person"),
1497
+ publicKey: await crypto.subtle.importKey(
1498
+ "jwk",
1499
+ {
1500
+ kty: "RSA",
1501
+ alg: "RS256",
1502
+ // cSpell: disable
1503
+ n: "yIB9rotX8G6r6_6toT-x24BUiQ_HaPH1Em9dOt4c94s-OPFoEdH7DY7Iym9A8Ll" +
1504
+ "H4JaGF8KD38bLHWe1S4x0jV3gHJKhK7veJfGZCKUENcQecBZ-YWUs5HWvUIX1vVB" +
1505
+ "__0luHrg6BQKGOrSOE-WIAxyr0qsWCFfZzQrvSnUD2yvg1arJX2xhms14uxoRd5K" +
1506
+ "g9efKSCmmQaNEapicARUmFWrIEpGFa_nUUnqimssAGw1eZFqf3wA4TjhsuARBhGa" +
1507
+ "Jtv_3KEa016eMZxy3kDlOjZnXZTaTgWkXdodwUvy8563fes3Al6BlcS2iJ9qbtha" +
1508
+ "8rSm0FHqoUKH73JsLPKQIwQ",
1509
+ e: "AQAB",
1510
+ // cSpell: enable
1511
+ key_ops: ["verify"],
1512
+ ext: true,
1513
+ },
1514
+ {
1515
+ name: "RSASSA-PKCS1-v1_5",
1516
+ hash: "SHA-256",
1517
+ },
1518
+ true,
1519
+ ["verify"],
1520
+ ),
1521
+ }) as CryptographicKey & { publicKey: CryptoKey };
1522
+
1523
+ // deno-lint-ignore no-explicit-any
1524
+ const sampleValues: Record<string, any> = {
1525
+ "http://www.w3.org/2001/XMLSchema#boolean": true,
1526
+ "http://www.w3.org/2001/XMLSchema#integer": -123,
1527
+ "http://www.w3.org/2001/XMLSchema#nonNegativeInteger": 123,
1528
+ "http://www.w3.org/2001/XMLSchema#float": 12.34,
1529
+ "http://www.w3.org/2001/XMLSchema#string": "hello",
1530
+ "http://www.w3.org/2001/XMLSchema#anyURI": new URL("https://example.com/"),
1531
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString": new LanguageString(
1532
+ "hello",
1533
+ "en",
1534
+ ),
1535
+ "http://www.w3.org/2001/XMLSchema#dateTime": Temporal.Instant.from(
1536
+ "2024-03-03T08:30:06.796196096Z",
1537
+ ),
1538
+ "http://www.w3.org/2001/XMLSchema#duration": Temporal.Duration.from({
1539
+ hours: 1,
1540
+ }),
1541
+ "https://w3id.org/security#cryptosuiteString": "eddsa-jcs-2022",
1542
+ // deno-fmt-ignore
1543
+ "https://w3id.org/security#multibase": new Uint8Array([
1544
+ 0x8f, 0x9b, 0x5a, 0xc9, 0x14, 0x17, 0xd0, 0xd1, 0x88, 0xbe, 0xfa, 0x85,
1545
+ 0x8f, 0x74, 0x44, 0x98, 0x1d, 0xc8, 0x79, 0xda, 0xba, 0x50, 0x98, 0x3c,
1546
+ 0x43, 0xeb, 0xcf, 0x72, 0x5f, 0x38, 0x58, 0x11, 0x9f, 0x23, 0xc5, 0xbf,
1547
+ 0x84, 0x23, 0x76, 0xa2, 0x1d, 0x53, 0xc0, 0xbe, 0x1a, 0xaa, 0x96, 0x6e,
1548
+ 0x30, 0x65, 0x59, 0x76, 0xf0, 0xb0, 0xdb, 0x78, 0x0d, 0xf5, 0xc1, 0xad,
1549
+ 0x3f, 0xbd, 0xf3, 0x07,
1550
+ ]),
1551
+ "fedify:langTag": new Intl.Locale("en-Latn-US"),
1552
+ "fedify:url": new URL("https://fedify.dev/"),
1553
+ "fedify:publicKey": rsaPublicKey.publicKey,
1554
+ "fedify:multibaseKey": ed25519PublicKey.publicKey,
1555
+ "fedify:proofPurpose": "assertionMethod",
1556
+ "fedify:units": "m",
1557
+ };
1558
+
1559
+ const types: Record<string, TypeSchema> =
1560
+ navigator?.userAgent === "Cloudflare-Workers"
1561
+ ? {} // FIXME: Cloudflare Workers does not support async I/O within global scope
1562
+ : await loadSchemaFiles(import.meta.dirname!);
1563
+ for (const typeUri in types) {
1564
+ const type = types[typeUri];
1565
+ // @ts-ignore: classes are all different
1566
+ const cls = vocab[type.name];
1567
+ sampleValues[typeUri] = new cls({
1568
+ "@id": "https://example.com/",
1569
+ "@type": typeUri,
1570
+ });
1571
+ }
1572
+
1573
+ const { assertSnapshot } = await import("@std/testing/snapshot").catch(
1574
+ () => ({ assertSnapshot: () => Promise.resolve() }),
1575
+ );
1576
+
1577
+ for (const typeUri in types) {
1578
+ const type = types[typeUri];
1579
+ // @ts-ignore: classes are all different
1580
+ const cls = vocab[type.name];
1581
+ const allProperties = getAllProperties(type, types);
1582
+ const initValues = globalThis.Object.fromEntries(
1583
+ allProperties.map((property) =>
1584
+ !property.functional
1585
+ ? [property.pluralName, property.range.map((t) => sampleValues[t])]
1586
+ : [property.singularName, sampleValues[property.range[0]]]
1587
+ ),
1588
+ );
1589
+
1590
+ test(`new ${type.name}() [auto]`, async () => {
1591
+ const instance = new cls(initValues);
1592
+ for (const property of allProperties) {
1593
+ if (areAllScalarTypes(property.range, types)) {
1594
+ if (property.functional || property.singularAccessor) {
1595
+ deepStrictEqual(
1596
+ instance[property.singularName],
1597
+ sampleValues[property.range[0]],
1598
+ );
1599
+ }
1600
+ if (!property.functional) {
1601
+ deepStrictEqual(
1602
+ instance[property.pluralName],
1603
+ property.range.map((t) => sampleValues[t]),
1604
+ );
1605
+ }
1606
+ } else {
1607
+ if (property.functional || property.singularAccessor) {
1608
+ deepStrictEqual(
1609
+ await instance[`get${pascalCase(property.singularName)}`].call(
1610
+ instance,
1611
+ { documentLoader: mockDocumentLoader },
1612
+ ),
1613
+ sampleValues[property.range[0]],
1614
+ );
1615
+ deepStrictEqual(
1616
+ instance[`${property.singularName}Id`],
1617
+ sampleValues[property.range[0]].id,
1618
+ );
1619
+ }
1620
+ if (!property.functional) {
1621
+ deepStrictEqual(
1622
+ await Array.fromAsync(
1623
+ instance[`get${pascalCase(property.pluralName)}`].call(
1624
+ instance,
1625
+ { documentLoader: mockDocumentLoader },
1626
+ ),
1627
+ ),
1628
+ property.range.map((t) => sampleValues[t]),
1629
+ );
1630
+ deepStrictEqual(
1631
+ instance[`${property.singularName}Ids`],
1632
+ property.range.map((t) => sampleValues[t].id).filter((i) =>
1633
+ i != null
1634
+ ),
1635
+ );
1636
+ }
1637
+ }
1638
+
1639
+ const empty = new cls({});
1640
+ for (const property of allProperties) {
1641
+ if (areAllScalarTypes(property.range, types)) {
1642
+ if (property.functional || property.singularAccessor) {
1643
+ deepStrictEqual(empty[property.singularName], null);
1644
+ }
1645
+ if (!property.functional) {
1646
+ deepStrictEqual(empty[property.pluralName], []);
1647
+ }
1648
+ } else {
1649
+ if (property.functional || property.singularAccessor) {
1650
+ deepStrictEqual(
1651
+ await empty[`get${pascalCase(property.singularName)}`].call(
1652
+ empty,
1653
+ { documentLoader: mockDocumentLoader },
1654
+ ),
1655
+ null,
1656
+ );
1657
+ deepStrictEqual(empty[`${property.singularName}Id`], null);
1658
+ }
1659
+ if (!property.functional) {
1660
+ deepStrictEqual(
1661
+ await Array.fromAsync(
1662
+ empty[`get${pascalCase(property.pluralName)}`].call(
1663
+ empty,
1664
+ { documentLoader: mockDocumentLoader },
1665
+ ),
1666
+ ),
1667
+ [],
1668
+ );
1669
+ deepStrictEqual(empty[`${property.singularName}Ids`], []);
1670
+ }
1671
+ }
1672
+ }
1673
+ }
1674
+
1675
+ for (const property of allProperties) {
1676
+ if (!property.functional && property.singularAccessor) {
1677
+ throws(
1678
+ () =>
1679
+ new cls({
1680
+ [property.singularName]: sampleValues[property.range[0]],
1681
+ [property.pluralName]: property.range.map((t) => sampleValues[t]),
1682
+ }),
1683
+ TypeError,
1684
+ );
1685
+ }
1686
+ }
1687
+
1688
+ const instance2 = new cls({
1689
+ id: new URL("https://example.com/"),
1690
+ ...globalThis.Object.fromEntries(
1691
+ allProperties.filter((p) => !areAllScalarTypes(p.range, types)).map(
1692
+ (p) =>
1693
+ p.functional
1694
+ ? [p.singularName, new URL("https://example.com/test")]
1695
+ : [p.pluralName, [new URL("https://example.com/test")]],
1696
+ ),
1697
+ ),
1698
+ });
1699
+ for (const property of allProperties) {
1700
+ if (areAllScalarTypes(property.range, types)) continue;
1701
+ if (property.functional || property.singularAccessor) {
1702
+ deepStrictEqual(
1703
+ instance2[`${property.singularName}Id`],
1704
+ new URL("https://example.com/test"),
1705
+ );
1706
+ }
1707
+ if (!property.functional) {
1708
+ deepStrictEqual(
1709
+ instance2[`${property.singularName}Ids`],
1710
+ [new URL("https://example.com/test")],
1711
+ );
1712
+ }
1713
+ }
1714
+
1715
+ throws(
1716
+ () => new cls({ id: 123 as unknown as URL }),
1717
+ TypeError,
1718
+ "The id must be a URL.",
1719
+ );
1720
+
1721
+ for (const property of allProperties) {
1722
+ const wrongValues = globalThis.Object.fromEntries(
1723
+ globalThis.Object.entries(initValues),
1724
+ );
1725
+ if (property.functional) {
1726
+ wrongValues[property.singularName] = {};
1727
+ } else {
1728
+ wrongValues[property.pluralName] = [{}];
1729
+ }
1730
+ throws(() => new cls(wrongValues), TypeError);
1731
+ }
1732
+ });
1733
+
1734
+ test(`${type.name}.clone() [auto]`, () => {
1735
+ const instance = new cls({});
1736
+ for (const property of allProperties) {
1737
+ if (!property.functional && property.singularAccessor) {
1738
+ throws(
1739
+ () =>
1740
+ instance.clone({
1741
+ [property.singularName]: sampleValues[property.range[0]],
1742
+ [property.pluralName]: property.range.map((t) => sampleValues[t]),
1743
+ }),
1744
+ TypeError,
1745
+ );
1746
+ }
1747
+ }
1748
+
1749
+ throws(
1750
+ () => instance.clone({ id: 123 as unknown as URL }),
1751
+ TypeError,
1752
+ "The id must be a URL.",
1753
+ );
1754
+ for (const property of allProperties) {
1755
+ const wrongValues = globalThis.Object.fromEntries(
1756
+ globalThis.Object.entries(initValues),
1757
+ );
1758
+ if (property.functional) {
1759
+ wrongValues[property.singularName] = {};
1760
+ } else {
1761
+ wrongValues[property.pluralName] = [{}];
1762
+ }
1763
+ throws(() => instance.clone(wrongValues), TypeError);
1764
+ }
1765
+ });
1766
+
1767
+ for (const property of allProperties) {
1768
+ if (areAllScalarTypes(property.range, types)) continue;
1769
+
1770
+ const docLoader = async (url: string) => {
1771
+ if (url !== `https://example.com/test`) throw new Error("Not Found");
1772
+ return {
1773
+ documentUrl: url,
1774
+ contextUrl: null,
1775
+ document: await sampleValues[property.range[0]].toJsonLd({
1776
+ contextLoader: mockDocumentLoader,
1777
+ }),
1778
+ };
1779
+ };
1780
+
1781
+ if (property.functional || property.singularAccessor) {
1782
+ test(
1783
+ `${type.name}.get${pascalCase(property.singularName)}() [auto]`,
1784
+ async () => {
1785
+ const instance = new cls({
1786
+ [property.singularName]: new URL("https://example.com/test"),
1787
+ });
1788
+ const value =
1789
+ await instance[`get${pascalCase(property.singularName)}`]
1790
+ .call(instance, { documentLoader: docLoader });
1791
+ deepStrictEqual(value, sampleValues[property.range[0]]);
1792
+
1793
+ if (property.untyped) return;
1794
+ const wrongRef = new cls({
1795
+ [property.singularName]: new URL("https://example.com/wrong-type"),
1796
+ });
1797
+ await rejects(
1798
+ () =>
1799
+ wrongRef[`get${pascalCase(property.singularName)}`].call(
1800
+ wrongRef,
1801
+ {
1802
+ documentLoader: mockDocumentLoader,
1803
+ },
1804
+ ),
1805
+ TypeError,
1806
+ );
1807
+ },
1808
+ );
1809
+ }
1810
+ if (!property.functional) {
1811
+ test(
1812
+ `${type.name}.get${pascalCase(property.pluralName)}() [auto]`,
1813
+ async () => {
1814
+ const instance = new cls({
1815
+ [property.pluralName]: [new URL("https://example.com/test")],
1816
+ });
1817
+ const value = instance[`get${pascalCase(property.pluralName)}`].call(
1818
+ instance,
1819
+ { documentLoader: docLoader },
1820
+ );
1821
+ deepStrictEqual(await Array.fromAsync(value), [
1822
+ sampleValues[property.range[0]],
1823
+ ]);
1824
+
1825
+ if (property.untyped) return;
1826
+ const wrongRef = new cls({
1827
+ [property.pluralName]: [new URL("https://example.com/wrong-type")],
1828
+ });
1829
+ await rejects(
1830
+ () =>
1831
+ Array.fromAsync(
1832
+ wrongRef[`get${pascalCase(property.pluralName)}`].call(
1833
+ wrongRef,
1834
+ {
1835
+ documentLoader: mockDocumentLoader,
1836
+ },
1837
+ ),
1838
+ ),
1839
+ TypeError,
1840
+ );
1841
+ },
1842
+ );
1843
+ }
1844
+ }
1845
+
1846
+ test(`${type.name}.fromJsonLd() [auto]`, async () => {
1847
+ const instance = await cls.fromJsonLd(
1848
+ {
1849
+ "@id": "https://example.com/",
1850
+ "@type": typeUri,
1851
+ },
1852
+ { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader },
1853
+ ) as vocab.Object;
1854
+ assertInstanceOf(instance, cls);
1855
+ deepStrictEqual(instance.id, new URL("https://example.com/"));
1856
+ deepStrictEqual(
1857
+ await instance.toJsonLd(),
1858
+ {
1859
+ "@id": "https://example.com/",
1860
+ "@type": typeUri,
1861
+ },
1862
+ );
1863
+ deepStrictEqual(
1864
+ await instance.toJsonLd({
1865
+ format: "compact",
1866
+ contextLoader: mockDocumentLoader,
1867
+ }),
1868
+ {
1869
+ "@context": type.defaultContext,
1870
+ "id": "https://example.com/",
1871
+ "type": type.compactName ??
1872
+ (type.name === "DataIntegrityProof" ? type.name : type.uri),
1873
+ },
1874
+ );
1875
+
1876
+ if (type.extends != null) {
1877
+ await rejects(() =>
1878
+ cls.fromJsonLd({
1879
+ "@id": "https://example.com/",
1880
+ "@type": "https://example.com/",
1881
+ }), TypeError);
1882
+ }
1883
+
1884
+ await rejects(() => cls.fromJsonLd(null), TypeError);
1885
+ await rejects(() => cls.fromJsonLd(undefined), TypeError);
1886
+ });
1887
+
1888
+ test(`${type.name}.toJsonLd() [auto]`, async () => {
1889
+ const instance = new cls({
1890
+ id: new URL("https://example.com/"),
1891
+ ...initValues,
1892
+ });
1893
+ const jsonLd = await instance.toJsonLd({
1894
+ contextLoader: mockDocumentLoader,
1895
+ });
1896
+ deepStrictEqual(jsonLd["@context"], type.defaultContext);
1897
+ deepStrictEqual(jsonLd.id, "https://example.com/");
1898
+ const restored = await cls.fromJsonLd(jsonLd, {
1899
+ documentLoader: mockDocumentLoader,
1900
+ contextLoader: mockDocumentLoader,
1901
+ });
1902
+ deepStrictEqual(restored, instance);
1903
+ deepStrictEqual(
1904
+ await restored.toJsonLd({ contextLoader: mockDocumentLoader }),
1905
+ jsonLd,
1906
+ );
1907
+
1908
+ const jsonLd2 = await instance.toJsonLd({
1909
+ contextLoader: mockDocumentLoader,
1910
+ format: "compact",
1911
+ context: "https://www.w3.org/ns/activitystreams",
1912
+ });
1913
+ deepStrictEqual(
1914
+ jsonLd2["@context"],
1915
+ "https://www.w3.org/ns/activitystreams",
1916
+ );
1917
+ deepStrictEqual(jsonLd2.id, "https://example.com/");
1918
+ const restored2 = await cls.fromJsonLd(jsonLd2, {
1919
+ documentLoader: mockDocumentLoader,
1920
+ contextLoader: mockDocumentLoader,
1921
+ });
1922
+ deepStrictEqual(restored2, instance);
1923
+
1924
+ const expanded = await instance.toJsonLd({
1925
+ contextLoader: mockDocumentLoader,
1926
+ format: "expand",
1927
+ });
1928
+ const restored3 = await cls.fromJsonLd(expanded, {
1929
+ documentLoader: mockDocumentLoader,
1930
+ contextLoader: mockDocumentLoader,
1931
+ });
1932
+ deepStrictEqual(restored3, instance);
1933
+
1934
+ const instance2 = new cls({
1935
+ id: new URL("https://example.com/"),
1936
+ ...initValues,
1937
+ ...globalThis.Object.fromEntries(
1938
+ allProperties.filter((p) => !areAllScalarTypes(p.range, types)).map(
1939
+ (p) =>
1940
+ p.functional
1941
+ ? [p.singularName, new URL("https://example.com/test")]
1942
+ : [p.pluralName, [new URL("https://example.com/test")]],
1943
+ ),
1944
+ ),
1945
+ });
1946
+ const jsonLd3 = await instance2.toJsonLd({
1947
+ contextLoader: mockDocumentLoader,
1948
+ });
1949
+ const restored4 = await cls.fromJsonLd(jsonLd3, {
1950
+ documentLoader: mockDocumentLoader,
1951
+ contextLoader: mockDocumentLoader,
1952
+ });
1953
+ deepStrictEqual(restored4, instance2);
1954
+
1955
+ rejects(
1956
+ () =>
1957
+ instance.toJsonLd({ context: "https://www.w3.org/ns/activitystreams" }),
1958
+ TypeError,
1959
+ );
1960
+ rejects(
1961
+ () =>
1962
+ instance.toJsonLd({
1963
+ format: "expand",
1964
+ context: "https://www.w3.org/ns/activitystreams",
1965
+ }),
1966
+ TypeError,
1967
+ );
1968
+ });
1969
+
1970
+ if ("Deno" in globalThis) {
1971
+ Deno.test(`Deno.inspect(${type.name}) [auto]`, async (t) => {
1972
+ const empty = new cls({});
1973
+ deepStrictEqual(Deno.inspect(empty), `${type.name} {}`);
1974
+
1975
+ const instance = new cls({
1976
+ id: new URL("https://example.com/"),
1977
+ ...initValues,
1978
+ });
1979
+ await assertSnapshot(t, Deno.inspect(instance));
1980
+
1981
+ const instance2 = instance.clone(
1982
+ globalThis.Object.fromEntries(
1983
+ type.properties.filter((p) => !areAllScalarTypes(p.range, types)).map(
1984
+ (p) =>
1985
+ p.functional
1986
+ ? [p.singularName, new URL("https://example.com/")]
1987
+ : [p.pluralName, [new URL("https://example.com/")]],
1988
+ ),
1989
+ ),
1990
+ );
1991
+ await assertSnapshot(t, Deno.inspect(instance2));
1992
+
1993
+ const instance3 = instance.clone(
1994
+ globalThis.Object.fromEntries(
1995
+ type.properties.filter((p) => !p.functional).map(
1996
+ (p) => {
1997
+ ok(!p.functional);
1998
+ return [
1999
+ p.pluralName,
2000
+ [sampleValues[p.range[0]], sampleValues[p.range[0]]],
2001
+ ];
2002
+ },
2003
+ ),
2004
+ ),
2005
+ );
2006
+ // @ts-ignore: t is TestContext in node:test but Deno.TestContext in Deno
2007
+ await assertSnapshot(t, Deno.inspect(instance3));
2008
+ });
2009
+ }
2010
+
2011
+ test(`${type.name}.typeId`, () => {
2012
+ deepStrictEqual(cls.typeId, new URL(type.uri));
2013
+ });
2014
+ }