@fedify/vocab 2.1.26 → 2.1.28

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,9 +1,9 @@
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-CUCvA5FQ.mjs";
3
+ import { C as vocab_exports, S as Source, _ as Person, a as Collection, b as Question, c as Endpoints, d as Hashtag, f as Link, h as OrderedCollectionPage, l as Follow, m as Object$1, n as Activity, o as Create, p as Note, r as Announce, s as CryptographicKey, t as Accept, v as Place, y as PropertyValue } from "./vocab-ccd63sNg.mjs";
4
4
  import { t as assertInstanceOf } from "./utils-CE8Dk5hm.mjs";
5
5
  import { mockDocumentLoader, test } from "@fedify/fixture";
6
- import { deepStrictEqual, notDeepStrictEqual, ok, rejects, throws } from "node:assert/strict";
6
+ import { deepStrictEqual, notDeepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
7
7
  import { LanguageString, decodeMultibase } from "@fedify/vocab-runtime";
8
8
  import { pascalCase } from "es-toolkit";
9
9
  import { areAllScalarTypes, loadSchemaFiles } from "@fedify/vocab-tools";
@@ -190,6 +190,61 @@ test("Note.fromJsonLd() ignores malformed language tags in nested objects", asyn
190
190
  const attachment = (await note.toJsonLd()).attachment;
191
191
  deepStrictEqual(attachment.nameMap, { en: "Valid" });
192
192
  });
193
+ test("Note.fromJsonLd() normalizes extlang language tags", async () => {
194
+ const json = {
195
+ "@context": "https://www.w3.org/ns/activitystreams",
196
+ type: "Note",
197
+ content: "<p>早晨</p>",
198
+ contentMap: {
199
+ "zh-YUE": "<p>早晨</p>",
200
+ "yue": "<p>早晨呀</p>"
201
+ }
202
+ };
203
+ const note = await Note.fromJsonLd(json);
204
+ deepStrictEqual(note.contents, [
205
+ "<p>早晨</p>",
206
+ new LanguageString("<p>早晨呀</p>", "yue"),
207
+ new LanguageString("<p>早晨</p>", "yue")
208
+ ]);
209
+ deepStrictEqual(note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName), [
210
+ null,
211
+ "yue",
212
+ "yue"
213
+ ]);
214
+ deepStrictEqual(await note.toJsonLd(), json);
215
+ deepStrictEqual((await note.toJsonLd({ format: "compact" })).contentMap, { yue: ["<p>早晨呀</p>", "<p>早晨</p>"] });
216
+ deepStrictEqual((await note.toJsonLd({ format: "expand" }))[0]["https://www.w3.org/ns/activitystreams#content"], [
217
+ { "@value": "<p>早晨</p>" },
218
+ {
219
+ "@value": "<p>早晨呀</p>",
220
+ "@language": "yue"
221
+ },
222
+ {
223
+ "@value": "<p>早晨</p>",
224
+ "@language": "yue"
225
+ }
226
+ ]);
227
+ const expandedNote = await Note.fromJsonLd({
228
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
229
+ "https://www.w3.org/ns/activitystreams#name": [{
230
+ "@value": "早晨",
231
+ "@language": "zh-yue-HK"
232
+ }]
233
+ });
234
+ deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
235
+ deepStrictEqual(expandedNote.names[0].locale.baseName, "yue-HK");
236
+ });
237
+ test("Note.fromJsonLd() caches original language tags", async () => {
238
+ const json = {
239
+ "@context": "https://www.w3.org/ns/activitystreams",
240
+ type: "Note",
241
+ contentMap: { "EN-us": "Hello" }
242
+ };
243
+ const note = await Note.fromJsonLd(json);
244
+ deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
245
+ deepStrictEqual(note.contents[0].locale.baseName, "en-US");
246
+ deepStrictEqual(await note.toJsonLd(), json);
247
+ });
193
248
  test("Note.toJsonLd()", async () => {
194
249
  const note = new Note({ tags: [new Hashtag({
195
250
  name: "#Fedify",
@@ -1007,6 +1062,22 @@ test("Link.fromJsonLd()", async () => {
1007
1062
  "href": "at://did:plc:ia76kvnndjutgedggx2ibrem"
1008
1063
  })).href, new URL("at://did%3Aplc%3Aia76kvnndjutgedggx2ibrem"));
1009
1064
  });
1065
+ test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
1066
+ const link = await Link.fromJsonLd({
1067
+ "@context": "https://www.w3.org/ns/activitystreams",
1068
+ type: "Link",
1069
+ href: "https://example.com/yue",
1070
+ hreflang: "zh-YUE"
1071
+ });
1072
+ deepStrictEqual(link.language?.baseName, "yue");
1073
+ deepStrictEqual((await link.toJsonLd({ format: "compact" })).hreflang, "yue");
1074
+ await rejects(() => Link.fromJsonLd({
1075
+ "@context": "https://www.w3.org/ns/activitystreams",
1076
+ type: "Link",
1077
+ href: "https://example.com/en",
1078
+ hreflang: "en-USA"
1079
+ }), RangeError);
1080
+ });
1010
1081
  test("Person.fromJsonLd() with relative URLs", async () => {
1011
1082
  deepStrictEqual((await (await Person.fromJsonLd({
1012
1083
  "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
@@ -1083,6 +1154,158 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
1083
1154
  });
1084
1155
  deepStrictEqual(create.clone({ object: newNote }).objectId, new URL("https://example.com/new-note"));
1085
1156
  });
1157
+ for (const property of ["object", "attachment"]) for (const fetchOriginal of [false, true]) test(`clone() isolates ${property} dereferencing (${fetchOriginal ? "original" : "clone"} first)`, async () => {
1158
+ const embedded = {
1159
+ id: "https://b.example/notes/1",
1160
+ type: "Note",
1161
+ content: "embedded"
1162
+ };
1163
+ const json = {
1164
+ "@context": "https://www.w3.org/ns/activitystreams",
1165
+ id: "https://a.example/activities/1",
1166
+ type: "Create",
1167
+ [property]: embedded
1168
+ };
1169
+ const original = await Create.fromJsonLd(json, { contextLoader: mockDocumentLoader });
1170
+ const copy = original.clone();
1171
+ const fetching = fetchOriginal ? original : copy;
1172
+ const untouched = fetchOriginal ? copy : original;
1173
+ let fetches = 0;
1174
+ const documentLoader = async (url) => {
1175
+ fetches++;
1176
+ return {
1177
+ contextUrl: null,
1178
+ documentUrl: url,
1179
+ document: {
1180
+ "@context": "https://www.w3.org/ns/activitystreams",
1181
+ ...embedded,
1182
+ content: "fetched"
1183
+ }
1184
+ };
1185
+ };
1186
+ const options = {
1187
+ documentLoader,
1188
+ contextLoader: mockDocumentLoader
1189
+ };
1190
+ const fetched = property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0];
1191
+ assertInstanceOf(fetched, Note);
1192
+ deepStrictEqual(fetched.content, "fetched");
1193
+ const trusted = { crossOrigin: "trust" };
1194
+ const preserved = property === "object" ? await untouched.getObject(trusted) : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
1195
+ assertInstanceOf(preserved, Note);
1196
+ deepStrictEqual(preserved.content, "embedded");
1197
+ deepStrictEqual((await original.toJsonLd())[property], fetchOriginal ? {
1198
+ ...embedded,
1199
+ content: "fetched"
1200
+ } : embedded);
1201
+ const nested = (await new Accept({ object: untouched }).toJsonLd()).object;
1202
+ deepStrictEqual(nested[property], embedded);
1203
+ strictEqual(property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0], fetched);
1204
+ deepStrictEqual(fetches, 1);
1205
+ const independentlyFetched = property === "object" ? await untouched.getObject(options) : (await Array.fromAsync(untouched.getAttachments(options)))[0];
1206
+ assertInstanceOf(independentlyFetched, Note);
1207
+ deepStrictEqual(independentlyFetched.content, "fetched");
1208
+ deepStrictEqual(fetches, 2);
1209
+ const warmedClone = fetching.clone();
1210
+ strictEqual(property === "object" ? await warmedClone.getObject(options) : (await Array.fromAsync(warmedClone.getAttachments(options)))[0], fetched);
1211
+ deepStrictEqual(fetches, 2);
1212
+ });
1213
+ for (const clone of [false, true]) for (const frozen of [false, true]) test(`${clone ? "clone()" : "constructor"} copies ${frozen ? "frozen" : "mutable"} plural arrays`, async () => {
1214
+ const items = [new URL("https://example.com/object"), new URL("https://example.com/object")];
1215
+ const before = items.slice();
1216
+ if (frozen) globalThis.Object.freeze(items);
1217
+ const base = new Collection({});
1218
+ const collection = clone ? base.clone({ items }) : new Collection({ items });
1219
+ const sibling = clone ? base.clone({ items }) : new Collection({ items });
1220
+ const fetched = await Array.fromAsync(collection.getItems({
1221
+ documentLoader: mockDocumentLoader,
1222
+ contextLoader: mockDocumentLoader
1223
+ }));
1224
+ deepStrictEqual(fetched.length, 2);
1225
+ deepStrictEqual(fetched.map((item) => item.name), ["Fetched object", "Fetched object"]);
1226
+ deepStrictEqual(items, before);
1227
+ strictEqual(items[0], before[0]);
1228
+ strictEqual(items[1], before[1]);
1229
+ deepStrictEqual((await sibling.toJsonLd()).items, before.map(String));
1230
+ if (!frozen) {
1231
+ items.push(new URL("https://example.com/extra"));
1232
+ deepStrictEqual(collection.itemIds, before);
1233
+ deepStrictEqual(sibling.itemIds, before);
1234
+ }
1235
+ });
1236
+ test("clone() copies scalar arrays without changing sparse inputs", () => {
1237
+ const names = ["original"];
1238
+ const original = new Object$1({ names });
1239
+ names.push("caller mutation");
1240
+ deepStrictEqual(original.names, ["original"]);
1241
+ const copy = original.clone();
1242
+ copy.names.push("clone mutation");
1243
+ deepStrictEqual(original.names, ["original"]);
1244
+ deepStrictEqual(copy.names, ["original", "clone mutation"]);
1245
+ const replacements = ["replacement"];
1246
+ const replaced = original.clone({ names: replacements });
1247
+ replacements.push("caller mutation");
1248
+ deepStrictEqual(replaced.names, ["replacement"]);
1249
+ const sparseNames = new Array(2);
1250
+ sparseNames[1] = "kept";
1251
+ const sparse = new Object$1({ names: sparseNames });
1252
+ for (const instance of [
1253
+ sparse,
1254
+ sparse.clone(),
1255
+ sparse.clone({ names: sparse.names })
1256
+ ]) {
1257
+ deepStrictEqual(instance.names.length, 2);
1258
+ deepStrictEqual(0 in instance.names, false);
1259
+ deepStrictEqual(instance.names[1], "kept");
1260
+ }
1261
+ });
1262
+ test("clone() preserves embedded object identity and trust", async () => {
1263
+ const note = new Note({ id: new URL("https://b.example/note") });
1264
+ const original = new Create({
1265
+ id: new URL("https://a.example/create"),
1266
+ object: note
1267
+ });
1268
+ const documentLoader = async () => {
1269
+ throw new Error("Trusted embedded objects must not be fetched.");
1270
+ };
1271
+ for (const instance of [
1272
+ original,
1273
+ original.clone(),
1274
+ original.clone({ objects: [note] })
1275
+ ]) {
1276
+ strictEqual(await instance.getObject({ documentLoader }), note);
1277
+ strictEqual(instance.objectId, note.id);
1278
+ }
1279
+ });
1280
+ test("clone() isolates crossOrigin trust when fetching", async () => {
1281
+ const original = new Create({
1282
+ id: new URL("https://a.example/create"),
1283
+ object: new URL("https://b.example/note")
1284
+ });
1285
+ const copy = original.clone();
1286
+ const documentLoader = async (url) => ({
1287
+ contextUrl: null,
1288
+ documentUrl: url,
1289
+ document: {
1290
+ "@context": "https://www.w3.org/ns/activitystreams",
1291
+ id: "https://other.example/note",
1292
+ type: "Note",
1293
+ content: "cross-origin"
1294
+ }
1295
+ });
1296
+ const options = {
1297
+ documentLoader,
1298
+ contextLoader: mockDocumentLoader
1299
+ };
1300
+ const trusted = await copy.getObject({
1301
+ ...options,
1302
+ crossOrigin: "trust"
1303
+ });
1304
+ assertInstanceOf(trusted, Note);
1305
+ deepStrictEqual(trusted.content, "cross-origin");
1306
+ deepStrictEqual(await original.getObject(options), null);
1307
+ deepStrictEqual(original.objectId, new URL("https://b.example/note"));
1308
+ });
1086
1309
  test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
1087
1310
  const crossOriginDocumentLoader = async (url) => {
1088
1311
  if (url === "https://different-origin.com/note") return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab",
3
- "version": "2.1.26",
3
+ "version": "2.1.28",
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.26",
50
- "@fedify/vocab-runtime": "2.1.26",
51
- "@fedify/vocab-tools": "2.1.26"
49
+ "@fedify/webfinger": "2.1.28",
50
+ "@fedify/vocab-tools": "2.1.28",
51
+ "@fedify/vocab-runtime": "2.1.28"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^22.17.0",
package/src/vocab.test.ts CHANGED
@@ -12,11 +12,13 @@ import {
12
12
  notDeepStrictEqual,
13
13
  ok,
14
14
  rejects,
15
+ strictEqual,
15
16
  throws,
16
17
  } from "node:assert/strict";
17
18
  import { assertInstanceOf } from "./utils.ts";
18
19
  import * as vocab from "./vocab.ts";
19
20
  import {
21
+ Accept,
20
22
  Activity,
21
23
  Announce,
22
24
  Collection,
@@ -305,6 +307,77 @@ test("Note.fromJsonLd() ignores malformed language tags in nested objects", asyn
305
307
  deepStrictEqual(attachment.nameMap, { en: "Valid" });
306
308
  });
307
309
 
310
+ test("Note.fromJsonLd() normalizes extlang language tags", async () => {
311
+ const json = {
312
+ "@context": "https://www.w3.org/ns/activitystreams",
313
+ type: "Note",
314
+ content: "<p>早晨</p>",
315
+ contentMap: {
316
+ "zh-YUE": "<p>早晨</p>",
317
+ "yue": "<p>早晨呀</p>",
318
+ },
319
+ };
320
+ const note = await Note.fromJsonLd(json);
321
+ // JSON-LD expansion sorts language map keys, so yue comes before zh-yue:
322
+ deepStrictEqual(note.contents, [
323
+ "<p>早晨</p>",
324
+ new LanguageString("<p>早晨呀</p>", "yue"),
325
+ new LanguageString("<p>早晨</p>", "yue"),
326
+ ]);
327
+ deepStrictEqual(
328
+ note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName),
329
+ [null, "yue", "yue"],
330
+ );
331
+
332
+ // The unmodified object is serialized from the cached original document:
333
+ deepStrictEqual(await note.toJsonLd(), json);
334
+ const compact = await note.toJsonLd({ format: "compact" }) as Record<
335
+ string,
336
+ unknown
337
+ >;
338
+ deepStrictEqual(compact.contentMap, {
339
+ yue: ["<p>早晨呀</p>", "<p>早晨</p>"],
340
+ });
341
+ const expanded = await note.toJsonLd({ format: "expand" });
342
+ deepStrictEqual(
343
+ (expanded as Record<string, unknown>[])[0][
344
+ "https://www.w3.org/ns/activitystreams#content"
345
+ ],
346
+ [
347
+ { "@value": "<p>早晨</p>" },
348
+ { "@value": "<p>早晨呀</p>", "@language": "yue" },
349
+ { "@value": "<p>早晨</p>", "@language": "yue" },
350
+ ],
351
+ );
352
+
353
+ const expandedNote = await Note.fromJsonLd({
354
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
355
+ "https://www.w3.org/ns/activitystreams#name": [
356
+ { "@value": "早晨", "@language": "zh-yue-HK" },
357
+ ],
358
+ });
359
+ deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
360
+ deepStrictEqual(
361
+ (expandedNote.names[0] as LanguageString).locale.baseName,
362
+ "yue-HK",
363
+ );
364
+ });
365
+
366
+ test("Note.fromJsonLd() caches original language tags", async () => {
367
+ const json = {
368
+ "@context": "https://www.w3.org/ns/activitystreams",
369
+ type: "Note",
370
+ contentMap: { "EN-us": "Hello" },
371
+ };
372
+ const note = await Note.fromJsonLd(json);
373
+ deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
374
+ deepStrictEqual(
375
+ (note.contents[0] as LanguageString).locale.baseName,
376
+ "en-US",
377
+ );
378
+ deepStrictEqual(await note.toJsonLd(), json);
379
+ });
380
+
308
381
  test("Note.toJsonLd()", async () => {
309
382
  const note = new Note({
310
383
  tags: [
@@ -1538,6 +1611,29 @@ test("Link.fromJsonLd()", async () => {
1538
1611
  );
1539
1612
  });
1540
1613
 
1614
+ test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
1615
+ const link = await Link.fromJsonLd({
1616
+ "@context": "https://www.w3.org/ns/activitystreams",
1617
+ type: "Link",
1618
+ href: "https://example.com/yue",
1619
+ hreflang: "zh-YUE",
1620
+ });
1621
+ deepStrictEqual(link.language?.baseName, "yue");
1622
+ const compact = await link.toJsonLd({ format: "compact" }) as Record<
1623
+ string,
1624
+ unknown
1625
+ >;
1626
+ deepStrictEqual(compact.hreflang, "yue");
1627
+
1628
+ await rejects(() =>
1629
+ Link.fromJsonLd({
1630
+ "@context": "https://www.w3.org/ns/activitystreams",
1631
+ type: "Link",
1632
+ href: "https://example.com/en",
1633
+ hreflang: "en-USA",
1634
+ }), RangeError);
1635
+ });
1636
+
1541
1637
  test("Person.fromJsonLd() with relative URLs", async () => {
1542
1638
  const json = {
1543
1639
  "@context": [
@@ -1668,6 +1764,216 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
1668
1764
  );
1669
1765
  });
1670
1766
 
1767
+ for (const property of ["object", "attachment"] as const) {
1768
+ for (const fetchOriginal of [false, true]) {
1769
+ test(
1770
+ `clone() isolates ${property} dereferencing (${
1771
+ fetchOriginal ? "original" : "clone"
1772
+ } first)`,
1773
+ async () => {
1774
+ const embedded = {
1775
+ id: "https://b.example/notes/1",
1776
+ type: "Note",
1777
+ content: "embedded",
1778
+ };
1779
+ const json = {
1780
+ "@context": "https://www.w3.org/ns/activitystreams",
1781
+ id: "https://a.example/activities/1",
1782
+ type: "Create",
1783
+ [property]: embedded,
1784
+ };
1785
+ const original = await Create.fromJsonLd(json, {
1786
+ contextLoader: mockDocumentLoader,
1787
+ });
1788
+ const copy = original.clone();
1789
+ const fetching = fetchOriginal ? original : copy;
1790
+ const untouched = fetchOriginal ? copy : original;
1791
+ let fetches = 0;
1792
+ // deno-lint-ignore require-await
1793
+ const documentLoader = async (url: string) => {
1794
+ fetches++;
1795
+ return {
1796
+ contextUrl: null,
1797
+ documentUrl: url,
1798
+ document: {
1799
+ "@context": "https://www.w3.org/ns/activitystreams",
1800
+ ...embedded,
1801
+ content: "fetched",
1802
+ },
1803
+ };
1804
+ };
1805
+ const options = { documentLoader, contextLoader: mockDocumentLoader };
1806
+ const fetched = property === "object"
1807
+ ? await fetching.getObject(options)
1808
+ : (await Array.fromAsync(fetching.getAttachments(options)))[0];
1809
+ assertInstanceOf(fetched, Note);
1810
+ deepStrictEqual(fetched.content, "fetched");
1811
+
1812
+ // Inspect the other instance before it fetches anything itself.
1813
+ const trusted = { crossOrigin: "trust" as const };
1814
+ const preserved = property === "object"
1815
+ ? await untouched.getObject(trusted)
1816
+ : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
1817
+ assertInstanceOf(preserved, Note);
1818
+ deepStrictEqual(preserved.content, "embedded");
1819
+ const serialized = await original.toJsonLd() as Record<string, unknown>;
1820
+ deepStrictEqual(
1821
+ serialized[property],
1822
+ fetchOriginal ? { ...embedded, content: "fetched" } : embedded,
1823
+ );
1824
+ const accept = new Accept({ object: untouched });
1825
+ const acceptJson = await accept.toJsonLd() as {
1826
+ object: Record<string, unknown>;
1827
+ };
1828
+ const nested = acceptJson.object;
1829
+ deepStrictEqual(nested[property], embedded);
1830
+
1831
+ // Successful lookups and their trust metadata belong to each instance.
1832
+ const cached = property === "object"
1833
+ ? await fetching.getObject(options)
1834
+ : (await Array.fromAsync(fetching.getAttachments(options)))[0];
1835
+ strictEqual(cached, fetched);
1836
+ deepStrictEqual(fetches, 1);
1837
+ const independentlyFetched = property === "object"
1838
+ ? await untouched.getObject(options)
1839
+ : (await Array.fromAsync(untouched.getAttachments(options)))[0];
1840
+ assertInstanceOf(independentlyFetched, Note);
1841
+ deepStrictEqual(independentlyFetched.content, "fetched");
1842
+ deepStrictEqual(fetches, 2);
1843
+ const warmedClone = fetching.clone();
1844
+ strictEqual(
1845
+ property === "object"
1846
+ ? await warmedClone.getObject(options)
1847
+ : (await Array.fromAsync(warmedClone.getAttachments(options)))[0],
1848
+ fetched,
1849
+ );
1850
+ deepStrictEqual(fetches, 2);
1851
+ },
1852
+ );
1853
+ }
1854
+ }
1855
+
1856
+ for (const clone of [false, true]) {
1857
+ for (const frozen of [false, true]) {
1858
+ test(
1859
+ `${clone ? "clone()" : "constructor"} copies ${
1860
+ frozen ? "frozen" : "mutable"
1861
+ } plural arrays`,
1862
+ async () => {
1863
+ const items = [
1864
+ new URL("https://example.com/object"),
1865
+ new URL("https://example.com/object"),
1866
+ ];
1867
+ const before = items.slice();
1868
+ if (frozen) globalThis.Object.freeze(items);
1869
+ const base = new Collection({});
1870
+ const collection = clone
1871
+ ? base.clone({ items })
1872
+ : new Collection({ items });
1873
+ const sibling = clone
1874
+ ? base.clone({ items })
1875
+ : new Collection({ items });
1876
+ const fetched = await Array.fromAsync(collection.getItems({
1877
+ documentLoader: mockDocumentLoader,
1878
+ contextLoader: mockDocumentLoader,
1879
+ }));
1880
+ deepStrictEqual(fetched.length, 2);
1881
+ deepStrictEqual(fetched.map((item) => item.name), [
1882
+ "Fetched object",
1883
+ "Fetched object",
1884
+ ]);
1885
+ deepStrictEqual(items, before);
1886
+ strictEqual(items[0], before[0]);
1887
+ strictEqual(items[1], before[1]);
1888
+ const siblingJson = await sibling.toJsonLd() as { items: unknown };
1889
+ deepStrictEqual(siblingJson.items, before.map(String));
1890
+ if (!frozen) {
1891
+ items.push(new URL("https://example.com/extra"));
1892
+ deepStrictEqual(collection.itemIds, before);
1893
+ deepStrictEqual(sibling.itemIds, before);
1894
+ }
1895
+ },
1896
+ );
1897
+ }
1898
+ }
1899
+
1900
+ test("clone() copies scalar arrays without changing sparse inputs", () => {
1901
+ const names = ["original"];
1902
+ const original = new Object({ names });
1903
+ names.push("caller mutation");
1904
+ deepStrictEqual(original.names, ["original"]);
1905
+ const copy = original.clone();
1906
+ copy.names.push("clone mutation");
1907
+ deepStrictEqual(original.names, ["original"]);
1908
+ deepStrictEqual(copy.names, ["original", "clone mutation"]);
1909
+ const replacements = ["replacement"];
1910
+ const replaced = original.clone({ names: replacements });
1911
+ replacements.push("caller mutation");
1912
+ deepStrictEqual(replaced.names, ["replacement"]);
1913
+
1914
+ const sparseNames = new Array<string>(2);
1915
+ sparseNames[1] = "kept";
1916
+ const sparse = new Object({ names: sparseNames });
1917
+ for (
1918
+ const instance of [
1919
+ sparse,
1920
+ sparse.clone(),
1921
+ sparse.clone({ names: sparse.names }),
1922
+ ]
1923
+ ) {
1924
+ deepStrictEqual(instance.names.length, 2);
1925
+ deepStrictEqual(0 in instance.names, false);
1926
+ deepStrictEqual(instance.names[1], "kept");
1927
+ }
1928
+ });
1929
+
1930
+ test("clone() preserves embedded object identity and trust", async () => {
1931
+ const note = new Note({ id: new URL("https://b.example/note") });
1932
+ const original = new Create({
1933
+ id: new URL("https://a.example/create"),
1934
+ object: note,
1935
+ });
1936
+ // deno-lint-ignore require-await
1937
+ const documentLoader = async () => {
1938
+ throw new Error("Trusted embedded objects must not be fetched.");
1939
+ };
1940
+ for (
1941
+ const instance of [
1942
+ original,
1943
+ original.clone(),
1944
+ original.clone({ objects: [note] }),
1945
+ ]
1946
+ ) {
1947
+ strictEqual(await instance.getObject({ documentLoader }), note);
1948
+ strictEqual(instance.objectId, note.id);
1949
+ }
1950
+ });
1951
+
1952
+ test("clone() isolates crossOrigin trust when fetching", async () => {
1953
+ const original = new Create({
1954
+ id: new URL("https://a.example/create"),
1955
+ object: new URL("https://b.example/note"),
1956
+ });
1957
+ const copy = original.clone();
1958
+ // deno-lint-ignore require-await
1959
+ const documentLoader = async (url: string) => ({
1960
+ contextUrl: null,
1961
+ documentUrl: url,
1962
+ document: {
1963
+ "@context": "https://www.w3.org/ns/activitystreams",
1964
+ id: "https://other.example/note",
1965
+ type: "Note",
1966
+ content: "cross-origin",
1967
+ },
1968
+ });
1969
+ const options = { documentLoader, contextLoader: mockDocumentLoader };
1970
+ const trusted = await copy.getObject({ ...options, crossOrigin: "trust" });
1971
+ assertInstanceOf(trusted, Note);
1972
+ deepStrictEqual(trusted.content, "cross-origin");
1973
+ deepStrictEqual(await original.getObject(options), null);
1974
+ deepStrictEqual(original.objectId, new URL("https://b.example/note"));
1975
+ });
1976
+
1671
1977
  test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
1672
1978
  // Create a mock document loader that returns objects with different origins
1673
1979
  // deno-lint-ignore require-await