@fedify/vocab 2.1.23 → 2.1.25

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.
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { S as vocab_exports, _ as Place, a as Create, c as Follow, d as Link, f as Note, g as Person, i as Collection, m as OrderedCollectionPage, n as Announce, o as CryptographicKey, p as Object$1, s as Endpoints, t as Activity, u as Hashtag, v as PropertyValue, x as Source, y as Question } from "./vocab-VQZhfXow.mjs";
3
+ import { S as vocab_exports, _ as Place, a as Create, c as Follow, d as Link, f as Note, g as Person, i as Collection, m as OrderedCollectionPage, n as Announce, o as CryptographicKey, p as Object$1, s as Endpoints, t as Activity, u as Hashtag, v as PropertyValue, x as Source, y as Question } from "./vocab-CyEe0PZp.mjs";
4
4
  import { t as assertInstanceOf } from "./utils-CE8Dk5hm.mjs";
5
5
  import { mockDocumentLoader, test } from "@fedify/fixture";
6
6
  import { deepStrictEqual, notDeepStrictEqual, ok, rejects, throws } from "node:assert/strict";
@@ -354,6 +354,72 @@ test({
354
354
  deepStrictEqual(objects2[0].name, "Second object");
355
355
  }
356
356
  });
357
+ function embeddedPropertyDocument(property) {
358
+ const context = "https://www.w3.org/ns/activitystreams";
359
+ return {
360
+ "@context": [context, { CustomImage: `${context}#Image` }],
361
+ type: property === "attachment" ? "Note" : "Create",
362
+ [property]: [
363
+ {
364
+ "@context": [context, { "@vocab": "https://example.com/" }],
365
+ type: "CustomImage",
366
+ name: "Malformed when reparsed"
367
+ },
368
+ {
369
+ "@context": context,
370
+ type: "Image",
371
+ name: "Cached image"
372
+ },
373
+ {
374
+ type: "Image",
375
+ name: "Decoded image"
376
+ }
377
+ ]
378
+ };
379
+ }
380
+ for (const suppressError of [
381
+ void 0,
382
+ false,
383
+ true
384
+ ]) {
385
+ test(`Note.getAttachments() cached parse failure (suppressError: ${suppressError})`, async () => {
386
+ const jsonLd = embeddedPropertyDocument("attachment");
387
+ const note = await Note.fromJsonLd(jsonLd);
388
+ if (suppressError) {
389
+ for (let i = 0; i < 2; i++) deepStrictEqual((await Array.fromAsync(note.getAttachments({ suppressError }))).map((attachment) => attachment.name), ["Cached image", "Decoded image"]);
390
+ await rejects(() => Array.fromAsync(note.getAttachments()), TypeError);
391
+ } else await rejects(() => Array.fromAsync(note.getAttachments({ suppressError })), TypeError);
392
+ deepStrictEqual(await note.toJsonLd(), jsonLd);
393
+ });
394
+ test(`Activity.getObject() cached parse failure (suppressError: ${suppressError})`, async () => {
395
+ const jsonLd = embeddedPropertyDocument("object");
396
+ const activity = await Activity.fromJsonLd(jsonLd);
397
+ if (suppressError) {
398
+ deepStrictEqual(await activity.getObject({ suppressError }), null);
399
+ deepStrictEqual(await activity.getObject({ suppressError }), null);
400
+ await rejects(() => activity.getObject(), TypeError);
401
+ } else await rejects(() => activity.getObject({ suppressError }), TypeError);
402
+ deepStrictEqual(await activity.toJsonLd(), jsonLd);
403
+ });
404
+ }
405
+ test("Note.getAttachments() suppresses cached context loader failures", async () => {
406
+ const note = await Note.fromJsonLd(embeddedPropertyDocument("attachment"));
407
+ const contextLoader = () => Promise.reject(/* @__PURE__ */ new Error("Context loader failed"));
408
+ await rejects(() => Array.fromAsync(note.getAttachments({ contextLoader })), (error) => error instanceof Error && !(error instanceof TypeError));
409
+ deepStrictEqual((await Array.fromAsync(note.getAttachments({
410
+ contextLoader,
411
+ suppressError: true
412
+ }))).map((attachment) => attachment.name), ["Decoded image"]);
413
+ });
414
+ test("Activity.getObject() suppresses cached context loader failures", async () => {
415
+ const activity = await Activity.fromJsonLd(embeddedPropertyDocument("object"));
416
+ const contextLoader = () => Promise.reject(/* @__PURE__ */ new Error("Context loader failed"));
417
+ await rejects(() => activity.getObject({ contextLoader }), (error) => error instanceof Error && !(error instanceof TypeError));
418
+ deepStrictEqual(await activity.getObject({
419
+ contextLoader,
420
+ suppressError: true
421
+ }), null);
422
+ });
357
423
  test("Activity.clone()", async () => {
358
424
  const activity = new Activity({
359
425
  actor: new Person({ name: "John Doe" }),
@@ -767,6 +833,83 @@ test("Note.quoteUrl", async () => {
767
833
  delete jsonLd._misskey_quote;
768
834
  deepStrictEqual((await Note.fromJsonLd(jsonLd)).quoteUrl, new URL("https://example.com/object3"));
769
835
  });
836
+ test("Note.quoteUrl (IRI-typed alias terms)", async () => {
837
+ const jsonLd = {
838
+ "@context": ["https://www.w3.org/ns/activitystreams", {
839
+ fedibird: "http://fedibird.com/ns#",
840
+ misskey: "https://misskey-hub.net/ns#",
841
+ _misskey_quote: {
842
+ "@id": "misskey:_misskey_quote",
843
+ "@type": "@id"
844
+ },
845
+ quoteUri: {
846
+ "@id": "fedibird:quoteUri",
847
+ "@type": "@id"
848
+ }
849
+ }],
850
+ id: "https://example.com/notes/1",
851
+ type: "Note",
852
+ _misskey_quote: "https://example.com/notes/quoted",
853
+ quoteUri: "https://example.com/notes/quoted2"
854
+ };
855
+ deepStrictEqual((await Note.fromJsonLd(jsonLd)).quoteUrl, new URL("https://example.com/notes/quoted"));
856
+ delete jsonLd._misskey_quote;
857
+ deepStrictEqual((await Note.fromJsonLd(jsonLd)).quoteUrl, new URL("https://example.com/notes/quoted2"));
858
+ });
859
+ test("Note.quoteUrl (IRI-typed primary term)", async () => {
860
+ const context = ["https://www.w3.org/ns/activitystreams", {
861
+ misskey: "https://misskey-hub.net/ns#",
862
+ quoteUrl: {
863
+ "@id": "as:quoteUrl",
864
+ "@type": "@id"
865
+ },
866
+ _misskey_quote: "misskey:_misskey_quote"
867
+ }];
868
+ deepStrictEqual((await Note.fromJsonLd({
869
+ "@context": context,
870
+ type: "Note",
871
+ quoteUrl: "https://example.com/object"
872
+ })).quoteUrl, new URL("https://example.com/object"));
873
+ deepStrictEqual((await Note.fromJsonLd({
874
+ "@context": context,
875
+ type: "Note",
876
+ quoteUrl: "https://example.com/object",
877
+ _misskey_quote: "https://example.com/object2"
878
+ })).quoteUrl, new URL("https://example.com/object"));
879
+ });
880
+ test("Note.quoteUrl (unparsable IRI-valued terms)", async () => {
881
+ const context = ["https://www.w3.org/ns/activitystreams", {
882
+ misskey: "https://misskey-hub.net/ns#",
883
+ _misskey_quote: {
884
+ "@id": "misskey:_misskey_quote",
885
+ "@type": "@id"
886
+ }
887
+ }];
888
+ for (const quote of [
889
+ {
890
+ type: "Note",
891
+ content: "An inlined quote without an id"
892
+ },
893
+ "_:b0",
894
+ "",
895
+ "notes/relative"
896
+ ]) {
897
+ const loaded = await Note.fromJsonLd({
898
+ "@context": context,
899
+ id: "https://example.com/notes/1",
900
+ type: "Note",
901
+ content: "Hello",
902
+ _misskey_quote: quote
903
+ });
904
+ deepStrictEqual(loaded.quoteUrl, null, JSON.stringify(quote));
905
+ deepStrictEqual(loaded.content, "Hello");
906
+ }
907
+ deepStrictEqual((await Note.fromJsonLd({
908
+ "@context": context,
909
+ type: "Note",
910
+ _misskey_quote: "at://did:plc:abc/app.bsky.feed.post/xyz"
911
+ })).quoteUrl, new URL("at://" + encodeURIComponent("did:plc:abc/app.bsky.feed.post/xyz")));
912
+ });
770
913
  test("Key.publicKey", async () => {
771
914
  const jwk = {
772
915
  kty: "RSA",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab",
3
- "version": "2.1.23",
3
+ "version": "2.1.25",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,9 +46,9 @@
46
46
  "es-toolkit": "1.43.0",
47
47
  "jsonld": "^9.0.0",
48
48
  "pkijs": "^3.3.3",
49
- "@fedify/webfinger": "2.1.23",
50
- "@fedify/vocab-runtime": "2.1.23",
51
- "@fedify/vocab-tools": "2.1.23"
49
+ "@fedify/webfinger": "2.1.25",
50
+ "@fedify/vocab-tools": "2.1.25",
51
+ "@fedify/vocab-runtime": "2.1.25"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^22.17.0",
package/src/actor.test.ts CHANGED
@@ -99,45 +99,46 @@ test({
99
99
  async fn(t) {
100
100
  fetchMock.spyGlobal();
101
101
 
102
+ // Node validates addresses before fetch; public IP literals avoid DNS.
102
103
  fetchMock.get(
103
- "begin:https://foo.example.com/.well-known/webfinger?",
104
+ "begin:https://1.1.1.1/.well-known/webfinger?",
104
105
  {
105
- body: { subject: "acct:johndoe@foo.example.com" },
106
+ body: { subject: "acct:johndoe@1.1.1.1" },
106
107
  headers: { "Content-Type": "application/jrd+json" },
107
108
  },
108
109
  );
109
110
 
110
- const actorId = new URL("https://foo.example.com/@john");
111
+ const actorId = new URL("https://1.1.1.1/@john");
111
112
  const actor = new Person({
112
113
  id: actorId,
113
114
  preferredUsername: "john",
114
115
  });
115
116
 
116
117
  await t.step("WebFinger subject", async () => {
117
- deepStrictEqual(await getActorHandle(actor), "@johndoe@foo.example.com");
118
+ deepStrictEqual(await getActorHandle(actor), "@johndoe@1.1.1.1");
118
119
  deepStrictEqual(
119
120
  await getActorHandle(actor, { trimLeadingAt: true }),
120
- "johndoe@foo.example.com",
121
+ "johndoe@1.1.1.1",
121
122
  );
122
123
  deepStrictEqual(
123
124
  await getActorHandle(actorId),
124
- "@johndoe@foo.example.com",
125
+ "@johndoe@1.1.1.1",
125
126
  );
126
127
  deepStrictEqual(
127
128
  await getActorHandle(actorId, { trimLeadingAt: true }),
128
- "johndoe@foo.example.com",
129
+ "johndoe@1.1.1.1",
129
130
  );
130
131
  });
131
132
 
132
133
  fetchMock.removeRoutes();
133
134
  fetchMock.get(
134
- "begin:https://foo.example.com/.well-known/webfinger?",
135
+ "begin:https://1.1.1.1/.well-known/webfinger?",
135
136
  {
136
137
  body: {
137
- subject: "https://foo.example.com/@john",
138
+ subject: "https://1.1.1.1/@john",
138
139
  aliases: [
139
- "acct:john@bar.example.com",
140
- "acct:johndoe@foo.example.com",
140
+ "acct:john@8.8.8.8",
141
+ "acct:johndoe@1.1.1.1",
141
142
  ],
142
143
  },
143
144
  headers: { "Content-Type": "application/jrd+json" },
@@ -145,28 +146,28 @@ test({
145
146
  );
146
147
 
147
148
  await t.step("WebFinger aliases", async () => {
148
- deepStrictEqual(await getActorHandle(actor), "@johndoe@foo.example.com");
149
+ deepStrictEqual(await getActorHandle(actor), "@johndoe@1.1.1.1");
149
150
  deepStrictEqual(
150
151
  await getActorHandle(actor, { trimLeadingAt: true }),
151
- "johndoe@foo.example.com",
152
+ "johndoe@1.1.1.1",
152
153
  );
153
154
  deepStrictEqual(
154
155
  await getActorHandle(actorId),
155
- "@johndoe@foo.example.com",
156
+ "@johndoe@1.1.1.1",
156
157
  );
157
158
  deepStrictEqual(
158
159
  await getActorHandle(actorId, { trimLeadingAt: true }),
159
- "johndoe@foo.example.com",
160
+ "johndoe@1.1.1.1",
160
161
  );
161
162
  });
162
163
 
163
164
  fetchMock.get(
164
- "begin:https://bar.example.com/.well-known/webfinger?",
165
+ "begin:https://8.8.8.8/.well-known/webfinger?",
165
166
  {
166
167
  body: {
167
- subject: "acct:john@bar.example.com",
168
+ subject: "acct:john@8.8.8.8",
168
169
  aliases: [
169
- "https://foo.example.com/@john",
170
+ "https://1.1.1.1/@john",
170
171
  ],
171
172
  },
172
173
  headers: { "Content-Type": "application/jrd+json" },
@@ -174,18 +175,18 @@ test({
174
175
  );
175
176
 
176
177
  await t.step("cross-origin WebFinger resources", async () => {
177
- deepStrictEqual(await getActorHandle(actor), "@john@bar.example.com");
178
+ deepStrictEqual(await getActorHandle(actor), "@john@8.8.8.8");
178
179
  });
179
180
 
180
181
  fetchMock.removeRoutes();
181
182
  fetchMock.get(
182
- "begin:https://foo.example.com/.well-known/webfinger?",
183
+ "begin:https://1.1.1.1/.well-known/webfinger?",
183
184
  { status: 404 },
184
185
  );
185
186
 
186
187
  await t.step("no WebFinger", async () => {
187
- deepStrictEqual(await getActorHandle(actor), "@john@foo.example.com");
188
- rejects(() => getActorHandle(actorId), TypeError);
188
+ deepStrictEqual(await getActorHandle(actor), "@john@1.1.1.1");
189
+ await rejects(() => getActorHandle(actorId), TypeError);
189
190
  });
190
191
 
191
192
  fetchMock.hardReset();
@@ -79,7 +79,8 @@ test("lookupObject()", {
79
79
  );
80
80
  });
81
81
 
82
- fetchMock.removeRoutes();
82
+ // Workers load fixtures through fetch(), so keep the spy fallback between steps.
83
+ fetchMock.removeRoutes({ includeFallback: false });
83
84
  fetchMock.get("begin:https://example.com/.well-known/webfinger", {
84
85
  subject: "acct:janedoe@example.com",
85
86
  links: [
@@ -99,7 +100,7 @@ test("lookupObject()", {
99
100
  );
100
101
  });
101
102
 
102
- fetchMock.removeRoutes();
103
+ fetchMock.removeRoutes({ includeFallback: false });
103
104
  fetchMock.get(
104
105
  "begin:https://example.com/.well-known/webfinger",
105
106
  () =>
@@ -130,7 +131,7 @@ test("lookupObject()", {
130
131
  deepStrictEqual(await promise, null);
131
132
  });
132
133
 
133
- fetchMock.removeRoutes();
134
+ fetchMock.removeRoutes({ includeFallback: false });
134
135
  fetchMock.get(
135
136
  "begin:https://example.com/.well-known/webfinger",
136
137
  {
@@ -155,7 +156,7 @@ test("lookupObject()", {
155
156
  deepStrictEqual(person.id, new URL("https://example.com/person"));
156
157
  });
157
158
 
158
- fetchMock.removeRoutes();
159
+ fetchMock.removeRoutes({ includeFallback: false });
159
160
  fetchMock.get(
160
161
  "begin:https://example.com/.well-known/webfinger",
161
162
  () =>
@@ -186,7 +187,7 @@ test("lookupObject()", {
186
187
  deepStrictEqual(result, null);
187
188
  });
188
189
 
189
- fetchMock.removeRoutes();
190
+ fetchMock.removeRoutes({ includeFallback: false });
190
191
  fetchMock.get(
191
192
  "https://example.com/slow-object",
192
193
  () =>
package/src/vocab.test.ts CHANGED
@@ -542,6 +542,104 @@ test({
542
542
  },
543
543
  });
544
544
 
545
+ // The alias is inherited during initial expansion, but is unavailable when
546
+ // an embedded value is reparsed standalone from the cached JSON-LD.
547
+ function embeddedPropertyDocument(property: "attachment" | "object") {
548
+ const context = "https://www.w3.org/ns/activitystreams";
549
+ return {
550
+ "@context": [context, { CustomImage: `${context}#Image` }],
551
+ type: property === "attachment" ? "Note" : "Create",
552
+ [property]: [
553
+ {
554
+ "@context": [context, { "@vocab": "https://example.com/" }],
555
+ type: "CustomImage",
556
+ name: "Malformed when reparsed",
557
+ },
558
+ { "@context": context, type: "Image", name: "Cached image" },
559
+ { type: "Image", name: "Decoded image" },
560
+ ],
561
+ };
562
+ }
563
+
564
+ for (const suppressError of [undefined, false, true]) {
565
+ test(
566
+ `Note.getAttachments() cached parse failure (suppressError: ${suppressError})`,
567
+ async () => {
568
+ const jsonLd = embeddedPropertyDocument("attachment");
569
+ const note = await Note.fromJsonLd(jsonLd);
570
+ if (suppressError) {
571
+ for (let i = 0; i < 2; i++) {
572
+ const attachments: (Object | Link | PropertyValue)[] = await Array
573
+ .fromAsync(
574
+ note.getAttachments({ suppressError }),
575
+ );
576
+ deepStrictEqual(
577
+ attachments.map((attachment) => attachment.name),
578
+ ["Cached image", "Decoded image"],
579
+ );
580
+ }
581
+ // Suppression must not remove or replace the cached malformed value.
582
+ await rejects(() => Array.fromAsync(note.getAttachments()), TypeError);
583
+ } else {
584
+ await rejects(
585
+ () => Array.fromAsync(note.getAttachments({ suppressError })),
586
+ TypeError,
587
+ );
588
+ }
589
+ deepStrictEqual(await note.toJsonLd(), jsonLd);
590
+ },
591
+ );
592
+
593
+ test(
594
+ `Activity.getObject() cached parse failure (suppressError: ${suppressError})`,
595
+ async () => {
596
+ const jsonLd = embeddedPropertyDocument("object");
597
+ const activity = await Activity.fromJsonLd(jsonLd);
598
+ if (suppressError) {
599
+ deepStrictEqual(await activity.getObject({ suppressError }), null);
600
+ deepStrictEqual(await activity.getObject({ suppressError }), null);
601
+ await rejects(() => activity.getObject(), TypeError);
602
+ } else {
603
+ await rejects(() => activity.getObject({ suppressError }), TypeError);
604
+ }
605
+ deepStrictEqual(await activity.toJsonLd(), jsonLd);
606
+ },
607
+ );
608
+ }
609
+
610
+ test("Note.getAttachments() suppresses cached context loader failures", async () => {
611
+ const note = await Note.fromJsonLd(embeddedPropertyDocument("attachment"));
612
+ const contextLoader = () =>
613
+ Promise.reject(new Error("Context loader failed"));
614
+ await rejects(
615
+ () => Array.fromAsync(note.getAttachments({ contextLoader })),
616
+ (error: unknown) => error instanceof Error && !(error instanceof TypeError),
617
+ );
618
+ // Both cached values need the failing loader; the decoded value remains usable.
619
+ const attachments: (Object | Link | PropertyValue)[] = await Array.fromAsync(
620
+ note.getAttachments({ contextLoader, suppressError: true }),
621
+ );
622
+ deepStrictEqual(attachments.map((attachment) => attachment.name), [
623
+ "Decoded image",
624
+ ]);
625
+ });
626
+
627
+ test("Activity.getObject() suppresses cached context loader failures", async () => {
628
+ const activity = await Activity.fromJsonLd(
629
+ embeddedPropertyDocument("object"),
630
+ );
631
+ const contextLoader = () =>
632
+ Promise.reject(new Error("Context loader failed"));
633
+ await rejects(
634
+ () => activity.getObject({ contextLoader }),
635
+ (error: unknown) => error instanceof Error && !(error instanceof TypeError),
636
+ );
637
+ deepStrictEqual(
638
+ await activity.getObject({ contextLoader, suppressError: true }),
639
+ null,
640
+ );
641
+ });
642
+
545
643
  test("Activity.clone()", async () => {
546
644
  const activity = new Activity({
547
645
  actor: new Person({
@@ -1197,6 +1295,105 @@ test("Note.quoteUrl", async () => {
1197
1295
  deepStrictEqual(loaded3.quoteUrl, new URL("https://example.com/object3"));
1198
1296
  });
1199
1297
 
1298
+ test("Note.quoteUrl (IRI-typed alias terms)", async () => {
1299
+ const jsonLd: Record<string, unknown> = {
1300
+ "@context": [
1301
+ "https://www.w3.org/ns/activitystreams",
1302
+ {
1303
+ fedibird: "http://fedibird.com/ns#",
1304
+ misskey: "https://misskey-hub.net/ns#",
1305
+ _misskey_quote: {
1306
+ "@id": "misskey:_misskey_quote",
1307
+ "@type": "@id",
1308
+ },
1309
+ quoteUri: {
1310
+ "@id": "fedibird:quoteUri",
1311
+ "@type": "@id",
1312
+ },
1313
+ },
1314
+ ],
1315
+ id: "https://example.com/notes/1",
1316
+ type: "Note",
1317
+ _misskey_quote: "https://example.com/notes/quoted",
1318
+ quoteUri: "https://example.com/notes/quoted2",
1319
+ };
1320
+
1321
+ const loaded = await Note.fromJsonLd(jsonLd);
1322
+ deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/notes/quoted"));
1323
+
1324
+ delete jsonLd._misskey_quote;
1325
+ const loaded2 = await Note.fromJsonLd(jsonLd);
1326
+ deepStrictEqual(
1327
+ loaded2.quoteUrl,
1328
+ new URL("https://example.com/notes/quoted2"),
1329
+ );
1330
+ });
1331
+
1332
+ test("Note.quoteUrl (IRI-typed primary term)", async () => {
1333
+ const context = [
1334
+ "https://www.w3.org/ns/activitystreams",
1335
+ {
1336
+ misskey: "https://misskey-hub.net/ns#",
1337
+ quoteUrl: { "@id": "as:quoteUrl", "@type": "@id" },
1338
+ _misskey_quote: "misskey:_misskey_quote",
1339
+ },
1340
+ ];
1341
+
1342
+ const loaded = await Note.fromJsonLd({
1343
+ "@context": context,
1344
+ type: "Note",
1345
+ quoteUrl: "https://example.com/object",
1346
+ });
1347
+ deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/object"));
1348
+
1349
+ // An IRI-valued quoteUrl still takes precedence over a plain-string alias:
1350
+ const loaded2 = await Note.fromJsonLd({
1351
+ "@context": context,
1352
+ type: "Note",
1353
+ quoteUrl: "https://example.com/object",
1354
+ _misskey_quote: "https://example.com/object2",
1355
+ });
1356
+ deepStrictEqual(loaded2.quoteUrl, new URL("https://example.com/object"));
1357
+ });
1358
+
1359
+ test("Note.quoteUrl (unparsable IRI-valued terms)", async () => {
1360
+ const context = [
1361
+ "https://www.w3.org/ns/activitystreams",
1362
+ {
1363
+ misskey: "https://misskey-hub.net/ns#",
1364
+ _misskey_quote: { "@id": "misskey:_misskey_quote", "@type": "@id" },
1365
+ },
1366
+ ];
1367
+ for (
1368
+ const quote of [
1369
+ { type: "Note", content: "An inlined quote without an id" },
1370
+ "_:b0",
1371
+ "",
1372
+ "notes/relative",
1373
+ ]
1374
+ ) {
1375
+ const loaded = await Note.fromJsonLd({
1376
+ "@context": context,
1377
+ id: "https://example.com/notes/1",
1378
+ type: "Note",
1379
+ content: "Hello",
1380
+ _misskey_quote: quote,
1381
+ });
1382
+ deepStrictEqual(loaded.quoteUrl, null, JSON.stringify(quote));
1383
+ deepStrictEqual(loaded.content, "Hello");
1384
+ }
1385
+
1386
+ const atUri = await Note.fromJsonLd({
1387
+ "@context": context,
1388
+ type: "Note",
1389
+ _misskey_quote: "at://did:plc:abc/app.bsky.feed.post/xyz",
1390
+ });
1391
+ deepStrictEqual(
1392
+ atUri.quoteUrl,
1393
+ new URL("at://" + encodeURIComponent("did:plc:abc/app.bsky.feed.post/xyz")),
1394
+ );
1395
+ });
1396
+
1200
1397
  test("Key.publicKey", async () => {
1201
1398
  const jwk = {
1202
1399
  kty: "RSA",