@fedify/vocab 2.5.0-dev.2314 → 2.5.0-dev.2332

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,10 +1,10 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { A as Object$1, B as QuoteAuthorization, C as Intent, D as Measure, E as Link, F as Person, G as Tombstone, H as Reject, I as Place, L as PropertyValue, N as OrderedCollectionPage, O as Multikey, P as Organization, R as Proposal, S as Image, T as InteractionRule, U as Service, V as QuoteRequest, W as Source, X as vocab_exports, _ as FeaturedCollection, a as Application, b as Group, d as CryptographicKey, f as Delete, g as FeatureRequest, h as FeatureAuthorization, i as Announce, j as Offer, k as Note, l as Commitment, m as Endpoints, n as Activity, p as Document, r as Agreement, s as Collection, t as Accept, u as Create, v as FeaturedItem, w as InteractionPolicy, x as Hashtag, y as Follow, z as Question } from "./vocab-hNQYgnUG.mjs";
4
- import { t as esm_default } from "./esm-DxaxLS8O.mjs";
3
+ import { A as Object$1, B as QuoteAuthorization, C as Intent, D as Measure, E as Link, F as Person, G as Tombstone, H as Reject, I as Place, L as PropertyValue, N as OrderedCollectionPage, O as Multikey, P as Organization, R as Proposal, S as Image, T as InteractionRule, U as Service, V as QuoteRequest, W as Source, X as vocab_exports, _ as FeaturedCollection, a as Application, b as Group, d as CryptographicKey, f as Delete, g as FeatureRequest, h as FeatureAuthorization, i as Announce, j as Offer, k as Note, l as Commitment, m as Endpoints, n as Activity, p as Document, r as Agreement, s as Collection, t as Accept, u as Create, v as FeaturedItem, w as InteractionPolicy, x as Hashtag, y as Follow, z as Question } from "./vocab-EFWWufqL.mjs";
4
+ import { t as esm_default } from "./esm-Cncdk-Cm.mjs";
5
5
  import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
6
6
  import { mockDocumentLoader, test } from "@fedify/fixture";
7
- import { deepStrictEqual, notDeepStrictEqual, ok, rejects, throws } from "node:assert/strict";
7
+ import { deepStrictEqual, notDeepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
8
8
  import { FetchError, LanguageString, decodeMultibase, getDocumentLoader, parseDecimal, parseIri } from "@fedify/vocab-runtime";
9
9
  import { configure, reset } from "@logtape/logtape";
10
10
  import { pascalCase } from "es-toolkit";
@@ -281,6 +281,61 @@ test("Note.fromJsonLd() ignores malformed language tags in nested objects", asyn
281
281
  const attachment = (await note.toJsonLd()).attachment;
282
282
  deepStrictEqual(attachment.nameMap, { en: "Valid" });
283
283
  });
284
+ test("Note.fromJsonLd() normalizes extlang language tags", async () => {
285
+ const json = {
286
+ "@context": "https://www.w3.org/ns/activitystreams",
287
+ type: "Note",
288
+ content: "<p>早晨</p>",
289
+ contentMap: {
290
+ "zh-YUE": "<p>早晨</p>",
291
+ "yue": "<p>早晨呀</p>"
292
+ }
293
+ };
294
+ const note = await Note.fromJsonLd(json);
295
+ deepStrictEqual(note.contents, [
296
+ "<p>早晨</p>",
297
+ new LanguageString("<p>早晨呀</p>", "yue"),
298
+ new LanguageString("<p>早晨</p>", "yue")
299
+ ]);
300
+ deepStrictEqual(note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName), [
301
+ null,
302
+ "yue",
303
+ "yue"
304
+ ]);
305
+ deepStrictEqual(await note.toJsonLd(), json);
306
+ deepStrictEqual((await note.toJsonLd({ format: "compact" })).contentMap, { yue: ["<p>早晨呀</p>", "<p>早晨</p>"] });
307
+ deepStrictEqual((await note.toJsonLd({ format: "expand" }))[0]["https://www.w3.org/ns/activitystreams#content"], [
308
+ { "@value": "<p>早晨</p>" },
309
+ {
310
+ "@value": "<p>早晨呀</p>",
311
+ "@language": "yue"
312
+ },
313
+ {
314
+ "@value": "<p>早晨</p>",
315
+ "@language": "yue"
316
+ }
317
+ ]);
318
+ const expandedNote = await Note.fromJsonLd({
319
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
320
+ "https://www.w3.org/ns/activitystreams#name": [{
321
+ "@value": "早晨",
322
+ "@language": "zh-yue-HK"
323
+ }]
324
+ });
325
+ deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
326
+ deepStrictEqual(expandedNote.names[0].locale.baseName, "yue-HK");
327
+ });
328
+ test("Note.fromJsonLd() caches original language tags", async () => {
329
+ const json = {
330
+ "@context": "https://www.w3.org/ns/activitystreams",
331
+ type: "Note",
332
+ contentMap: { "EN-us": "Hello" }
333
+ };
334
+ const note = await Note.fromJsonLd(json);
335
+ deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
336
+ deepStrictEqual(note.contents[0].locale.baseName, "en-US");
337
+ deepStrictEqual(await note.toJsonLd(), json);
338
+ });
284
339
  test("Note.toJsonLd()", async () => {
285
340
  const note = new Note({ tags: [new Hashtag({
286
341
  name: "#Fedify",
@@ -2557,6 +2612,22 @@ test("Link.fromJsonLd()", async () => {
2557
2612
  "href": "at://did:plc:ia76kvnndjutgedggx2ibrem"
2558
2613
  })).href, new URL("at://did%3Aplc%3Aia76kvnndjutgedggx2ibrem"));
2559
2614
  });
2615
+ test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
2616
+ const link = await Link.fromJsonLd({
2617
+ "@context": "https://www.w3.org/ns/activitystreams",
2618
+ type: "Link",
2619
+ href: "https://example.com/yue",
2620
+ hreflang: "zh-YUE"
2621
+ });
2622
+ deepStrictEqual(link.language?.baseName, "yue");
2623
+ deepStrictEqual((await link.toJsonLd({ format: "compact" })).hreflang, "yue");
2624
+ await rejects(() => Link.fromJsonLd({
2625
+ "@context": "https://www.w3.org/ns/activitystreams",
2626
+ type: "Link",
2627
+ href: "https://example.com/en",
2628
+ hreflang: "en-USA"
2629
+ }), RangeError);
2630
+ });
2560
2631
  test("Person.fromJsonLd() with relative URLs", async () => {
2561
2632
  deepStrictEqual((await (await Person.fromJsonLd({
2562
2633
  "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
@@ -2970,6 +3041,158 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
2970
3041
  });
2971
3042
  deepStrictEqual(create.clone({ object: newNote }).objectId, new URL("https://example.com/new-note"));
2972
3043
  });
3044
+ for (const property of ["object", "attachment"]) for (const fetchOriginal of [false, true]) test(`clone() isolates ${property} dereferencing (${fetchOriginal ? "original" : "clone"} first)`, async () => {
3045
+ const embedded = {
3046
+ id: "https://b.example/notes/1",
3047
+ type: "Note",
3048
+ content: "embedded"
3049
+ };
3050
+ const json = {
3051
+ "@context": "https://www.w3.org/ns/activitystreams",
3052
+ id: "https://a.example/activities/1",
3053
+ type: "Create",
3054
+ [property]: embedded
3055
+ };
3056
+ const original = await Create.fromJsonLd(json, { contextLoader: mockDocumentLoader });
3057
+ const copy = original.clone();
3058
+ const fetching = fetchOriginal ? original : copy;
3059
+ const untouched = fetchOriginal ? copy : original;
3060
+ let fetches = 0;
3061
+ const documentLoader = async (url) => {
3062
+ fetches++;
3063
+ return {
3064
+ contextUrl: null,
3065
+ documentUrl: url,
3066
+ document: {
3067
+ "@context": "https://www.w3.org/ns/activitystreams",
3068
+ ...embedded,
3069
+ content: "fetched"
3070
+ }
3071
+ };
3072
+ };
3073
+ const options = {
3074
+ documentLoader,
3075
+ contextLoader: mockDocumentLoader
3076
+ };
3077
+ const fetched = property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0];
3078
+ assertInstanceOf(fetched, Note);
3079
+ deepStrictEqual(fetched.content, "fetched");
3080
+ const trusted = { crossOrigin: "trust" };
3081
+ const preserved = property === "object" ? await untouched.getObject(trusted) : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
3082
+ assertInstanceOf(preserved, Note);
3083
+ deepStrictEqual(preserved.content, "embedded");
3084
+ deepStrictEqual((await original.toJsonLd())[property], fetchOriginal ? {
3085
+ ...embedded,
3086
+ content: "fetched"
3087
+ } : embedded);
3088
+ const nested = (await new Accept({ object: untouched }).toJsonLd()).object;
3089
+ deepStrictEqual(nested[property], embedded);
3090
+ strictEqual(property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0], fetched);
3091
+ deepStrictEqual(fetches, 1);
3092
+ const independentlyFetched = property === "object" ? await untouched.getObject(options) : (await Array.fromAsync(untouched.getAttachments(options)))[0];
3093
+ assertInstanceOf(independentlyFetched, Note);
3094
+ deepStrictEqual(independentlyFetched.content, "fetched");
3095
+ deepStrictEqual(fetches, 2);
3096
+ const warmedClone = fetching.clone();
3097
+ strictEqual(property === "object" ? await warmedClone.getObject(options) : (await Array.fromAsync(warmedClone.getAttachments(options)))[0], fetched);
3098
+ deepStrictEqual(fetches, 2);
3099
+ });
3100
+ for (const clone of [false, true]) for (const frozen of [false, true]) test(`${clone ? "clone()" : "constructor"} copies ${frozen ? "frozen" : "mutable"} plural arrays`, async () => {
3101
+ const items = [new URL("https://example.com/object"), new URL("https://example.com/object")];
3102
+ const before = items.slice();
3103
+ if (frozen) globalThis.Object.freeze(items);
3104
+ const base = new Collection({});
3105
+ const collection = clone ? base.clone({ items }) : new Collection({ items });
3106
+ const sibling = clone ? base.clone({ items }) : new Collection({ items });
3107
+ const fetched = await Array.fromAsync(collection.getItems({
3108
+ documentLoader: mockDocumentLoader,
3109
+ contextLoader: mockDocumentLoader
3110
+ }));
3111
+ deepStrictEqual(fetched.length, 2);
3112
+ deepStrictEqual(fetched.map((item) => item.name), ["Fetched object", "Fetched object"]);
3113
+ deepStrictEqual(items, before);
3114
+ strictEqual(items[0], before[0]);
3115
+ strictEqual(items[1], before[1]);
3116
+ deepStrictEqual((await sibling.toJsonLd()).items, before.map(String));
3117
+ if (!frozen) {
3118
+ items.push(new URL("https://example.com/extra"));
3119
+ deepStrictEqual(collection.itemIds, before);
3120
+ deepStrictEqual(sibling.itemIds, before);
3121
+ }
3122
+ });
3123
+ test("clone() copies scalar arrays without changing sparse inputs", () => {
3124
+ const names = ["original"];
3125
+ const original = new Object$1({ names });
3126
+ names.push("caller mutation");
3127
+ deepStrictEqual(original.names, ["original"]);
3128
+ const copy = original.clone();
3129
+ copy.names.push("clone mutation");
3130
+ deepStrictEqual(original.names, ["original"]);
3131
+ deepStrictEqual(copy.names, ["original", "clone mutation"]);
3132
+ const replacements = ["replacement"];
3133
+ const replaced = original.clone({ names: replacements });
3134
+ replacements.push("caller mutation");
3135
+ deepStrictEqual(replaced.names, ["replacement"]);
3136
+ const sparseNames = new Array(2);
3137
+ sparseNames[1] = "kept";
3138
+ const sparse = new Object$1({ names: sparseNames });
3139
+ for (const instance of [
3140
+ sparse,
3141
+ sparse.clone(),
3142
+ sparse.clone({ names: sparse.names })
3143
+ ]) {
3144
+ deepStrictEqual(instance.names.length, 2);
3145
+ deepStrictEqual(0 in instance.names, false);
3146
+ deepStrictEqual(instance.names[1], "kept");
3147
+ }
3148
+ });
3149
+ test("clone() preserves embedded object identity and trust", async () => {
3150
+ const note = new Note({ id: new URL("https://b.example/note") });
3151
+ const original = new Create({
3152
+ id: new URL("https://a.example/create"),
3153
+ object: note
3154
+ });
3155
+ const documentLoader = async () => {
3156
+ throw new Error("Trusted embedded objects must not be fetched.");
3157
+ };
3158
+ for (const instance of [
3159
+ original,
3160
+ original.clone(),
3161
+ original.clone({ objects: [note] })
3162
+ ]) {
3163
+ strictEqual(await instance.getObject({ documentLoader }), note);
3164
+ strictEqual(instance.objectId, note.id);
3165
+ }
3166
+ });
3167
+ test("clone() isolates crossOrigin trust when fetching", async () => {
3168
+ const original = new Create({
3169
+ id: new URL("https://a.example/create"),
3170
+ object: new URL("https://b.example/note")
3171
+ });
3172
+ const copy = original.clone();
3173
+ const documentLoader = async (url) => ({
3174
+ contextUrl: null,
3175
+ documentUrl: url,
3176
+ document: {
3177
+ "@context": "https://www.w3.org/ns/activitystreams",
3178
+ id: "https://other.example/note",
3179
+ type: "Note",
3180
+ content: "cross-origin"
3181
+ }
3182
+ });
3183
+ const options = {
3184
+ documentLoader,
3185
+ contextLoader: mockDocumentLoader
3186
+ };
3187
+ const trusted = await copy.getObject({
3188
+ ...options,
3189
+ crossOrigin: "trust"
3190
+ });
3191
+ assertInstanceOf(trusted, Note);
3192
+ deepStrictEqual(trusted.content, "cross-origin");
3193
+ deepStrictEqual(await original.getObject(options), null);
3194
+ deepStrictEqual(original.objectId, new URL("https://b.example/note"));
3195
+ });
2973
3196
  test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
2974
3197
  const crossOriginDocumentLoader = async (url) => {
2975
3198
  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.5.0-dev.2314+d73be480",
3
+ "version": "2.5.0-dev.2332+d4e3f91f",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,9 +43,9 @@
43
43
  "jsonld": "^9.0.0",
44
44
  "pkijs": "^3.3.3",
45
45
  "temporal-polyfill": "^1.0.1",
46
- "@fedify/webfinger": "2.5.0-dev.2314+d73be480",
47
- "@fedify/vocab-runtime": "2.5.0-dev.2314+d73be480",
48
- "@fedify/vocab-tools": "2.5.0-dev.2314+d73be480"
46
+ "@fedify/vocab-tools": "2.5.0-dev.2332+d4e3f91f",
47
+ "@fedify/webfinger": "2.5.0-dev.2332+d4e3f91f",
48
+ "@fedify/vocab-runtime": "2.5.0-dev.2332+d4e3f91f"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^24.12.2",
package/src/vocab.test.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  notDeepStrictEqual,
24
24
  ok,
25
25
  rejects,
26
+ strictEqual,
26
27
  throws,
27
28
  } from "node:assert/strict";
28
29
  import { inspect } from "node:util";
@@ -438,6 +439,77 @@ test("Note.fromJsonLd() ignores malformed language tags in nested objects", asyn
438
439
  deepStrictEqual(attachment.nameMap, { en: "Valid" });
439
440
  });
440
441
 
442
+ test("Note.fromJsonLd() normalizes extlang language tags", async () => {
443
+ const json = {
444
+ "@context": "https://www.w3.org/ns/activitystreams",
445
+ type: "Note",
446
+ content: "<p>早晨</p>",
447
+ contentMap: {
448
+ "zh-YUE": "<p>早晨</p>",
449
+ "yue": "<p>早晨呀</p>",
450
+ },
451
+ };
452
+ const note = await Note.fromJsonLd(json);
453
+ // JSON-LD expansion sorts language map keys, so yue comes before zh-yue:
454
+ deepStrictEqual(note.contents, [
455
+ "<p>早晨</p>",
456
+ new LanguageString("<p>早晨呀</p>", "yue"),
457
+ new LanguageString("<p>早晨</p>", "yue"),
458
+ ]);
459
+ deepStrictEqual(
460
+ note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName),
461
+ [null, "yue", "yue"],
462
+ );
463
+
464
+ // The unmodified object is serialized from the cached original document:
465
+ deepStrictEqual(await note.toJsonLd(), json);
466
+ const compact = await note.toJsonLd({ format: "compact" }) as Record<
467
+ string,
468
+ unknown
469
+ >;
470
+ deepStrictEqual(compact.contentMap, {
471
+ yue: ["<p>早晨呀</p>", "<p>早晨</p>"],
472
+ });
473
+ const expanded = await note.toJsonLd({ format: "expand" });
474
+ deepStrictEqual(
475
+ (expanded as Record<string, unknown>[])[0][
476
+ "https://www.w3.org/ns/activitystreams#content"
477
+ ],
478
+ [
479
+ { "@value": "<p>早晨</p>" },
480
+ { "@value": "<p>早晨呀</p>", "@language": "yue" },
481
+ { "@value": "<p>早晨</p>", "@language": "yue" },
482
+ ],
483
+ );
484
+
485
+ const expandedNote = await Note.fromJsonLd({
486
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
487
+ "https://www.w3.org/ns/activitystreams#name": [
488
+ { "@value": "早晨", "@language": "zh-yue-HK" },
489
+ ],
490
+ });
491
+ deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
492
+ deepStrictEqual(
493
+ (expandedNote.names[0] as LanguageString).locale.baseName,
494
+ "yue-HK",
495
+ );
496
+ });
497
+
498
+ test("Note.fromJsonLd() caches original language tags", async () => {
499
+ const json = {
500
+ "@context": "https://www.w3.org/ns/activitystreams",
501
+ type: "Note",
502
+ contentMap: { "EN-us": "Hello" },
503
+ };
504
+ const note = await Note.fromJsonLd(json);
505
+ deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
506
+ deepStrictEqual(
507
+ (note.contents[0] as LanguageString).locale.baseName,
508
+ "en-US",
509
+ );
510
+ deepStrictEqual(await note.toJsonLd(), json);
511
+ });
512
+
441
513
  test("Note.toJsonLd()", async () => {
442
514
  const note = new Note({
443
515
  tags: [
@@ -3852,6 +3924,29 @@ test("Link.fromJsonLd()", async () => {
3852
3924
  );
3853
3925
  });
3854
3926
 
3927
+ test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
3928
+ const link = await Link.fromJsonLd({
3929
+ "@context": "https://www.w3.org/ns/activitystreams",
3930
+ type: "Link",
3931
+ href: "https://example.com/yue",
3932
+ hreflang: "zh-YUE",
3933
+ });
3934
+ deepStrictEqual(link.language?.baseName, "yue");
3935
+ const compact = await link.toJsonLd({ format: "compact" }) as Record<
3936
+ string,
3937
+ unknown
3938
+ >;
3939
+ deepStrictEqual(compact.hreflang, "yue");
3940
+
3941
+ await rejects(() =>
3942
+ Link.fromJsonLd({
3943
+ "@context": "https://www.w3.org/ns/activitystreams",
3944
+ type: "Link",
3945
+ href: "https://example.com/en",
3946
+ hreflang: "en-USA",
3947
+ }), RangeError);
3948
+ });
3949
+
3855
3950
  test("Person.fromJsonLd() with relative URLs", async () => {
3856
3951
  const json = {
3857
3952
  "@context": [
@@ -4437,6 +4532,216 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
4437
4532
  );
4438
4533
  });
4439
4534
 
4535
+ for (const property of ["object", "attachment"] as const) {
4536
+ for (const fetchOriginal of [false, true]) {
4537
+ test(
4538
+ `clone() isolates ${property} dereferencing (${
4539
+ fetchOriginal ? "original" : "clone"
4540
+ } first)`,
4541
+ async () => {
4542
+ const embedded = {
4543
+ id: "https://b.example/notes/1",
4544
+ type: "Note",
4545
+ content: "embedded",
4546
+ };
4547
+ const json = {
4548
+ "@context": "https://www.w3.org/ns/activitystreams",
4549
+ id: "https://a.example/activities/1",
4550
+ type: "Create",
4551
+ [property]: embedded,
4552
+ };
4553
+ const original = await Create.fromJsonLd(json, {
4554
+ contextLoader: mockDocumentLoader,
4555
+ });
4556
+ const copy = original.clone();
4557
+ const fetching = fetchOriginal ? original : copy;
4558
+ const untouched = fetchOriginal ? copy : original;
4559
+ let fetches = 0;
4560
+ // deno-lint-ignore require-await
4561
+ const documentLoader = async (url: string) => {
4562
+ fetches++;
4563
+ return {
4564
+ contextUrl: null,
4565
+ documentUrl: url,
4566
+ document: {
4567
+ "@context": "https://www.w3.org/ns/activitystreams",
4568
+ ...embedded,
4569
+ content: "fetched",
4570
+ },
4571
+ };
4572
+ };
4573
+ const options = { documentLoader, contextLoader: mockDocumentLoader };
4574
+ const fetched = property === "object"
4575
+ ? await fetching.getObject(options)
4576
+ : (await Array.fromAsync(fetching.getAttachments(options)))[0];
4577
+ assertInstanceOf(fetched, Note);
4578
+ deepStrictEqual(fetched.content, "fetched");
4579
+
4580
+ // Inspect the other instance before it fetches anything itself.
4581
+ const trusted = { crossOrigin: "trust" as const };
4582
+ const preserved = property === "object"
4583
+ ? await untouched.getObject(trusted)
4584
+ : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
4585
+ assertInstanceOf(preserved, Note);
4586
+ deepStrictEqual(preserved.content, "embedded");
4587
+ const serialized = await original.toJsonLd() as Record<string, unknown>;
4588
+ deepStrictEqual(
4589
+ serialized[property],
4590
+ fetchOriginal ? { ...embedded, content: "fetched" } : embedded,
4591
+ );
4592
+ const accept = new Accept({ object: untouched });
4593
+ const acceptJson = await accept.toJsonLd() as {
4594
+ object: Record<string, unknown>;
4595
+ };
4596
+ const nested = acceptJson.object;
4597
+ deepStrictEqual(nested[property], embedded);
4598
+
4599
+ // Successful lookups and their trust metadata belong to each instance.
4600
+ const cached = property === "object"
4601
+ ? await fetching.getObject(options)
4602
+ : (await Array.fromAsync(fetching.getAttachments(options)))[0];
4603
+ strictEqual(cached, fetched);
4604
+ deepStrictEqual(fetches, 1);
4605
+ const independentlyFetched = property === "object"
4606
+ ? await untouched.getObject(options)
4607
+ : (await Array.fromAsync(untouched.getAttachments(options)))[0];
4608
+ assertInstanceOf(independentlyFetched, Note);
4609
+ deepStrictEqual(independentlyFetched.content, "fetched");
4610
+ deepStrictEqual(fetches, 2);
4611
+ const warmedClone = fetching.clone();
4612
+ strictEqual(
4613
+ property === "object"
4614
+ ? await warmedClone.getObject(options)
4615
+ : (await Array.fromAsync(warmedClone.getAttachments(options)))[0],
4616
+ fetched,
4617
+ );
4618
+ deepStrictEqual(fetches, 2);
4619
+ },
4620
+ );
4621
+ }
4622
+ }
4623
+
4624
+ for (const clone of [false, true]) {
4625
+ for (const frozen of [false, true]) {
4626
+ test(
4627
+ `${clone ? "clone()" : "constructor"} copies ${
4628
+ frozen ? "frozen" : "mutable"
4629
+ } plural arrays`,
4630
+ async () => {
4631
+ const items = [
4632
+ new URL("https://example.com/object"),
4633
+ new URL("https://example.com/object"),
4634
+ ];
4635
+ const before = items.slice();
4636
+ if (frozen) globalThis.Object.freeze(items);
4637
+ const base = new Collection({});
4638
+ const collection = clone
4639
+ ? base.clone({ items })
4640
+ : new Collection({ items });
4641
+ const sibling = clone
4642
+ ? base.clone({ items })
4643
+ : new Collection({ items });
4644
+ const fetched = await Array.fromAsync(collection.getItems({
4645
+ documentLoader: mockDocumentLoader,
4646
+ contextLoader: mockDocumentLoader,
4647
+ }));
4648
+ deepStrictEqual(fetched.length, 2);
4649
+ deepStrictEqual(fetched.map((item) => item.name), [
4650
+ "Fetched object",
4651
+ "Fetched object",
4652
+ ]);
4653
+ deepStrictEqual(items, before);
4654
+ strictEqual(items[0], before[0]);
4655
+ strictEqual(items[1], before[1]);
4656
+ const siblingJson = await sibling.toJsonLd() as { items: unknown };
4657
+ deepStrictEqual(siblingJson.items, before.map(String));
4658
+ if (!frozen) {
4659
+ items.push(new URL("https://example.com/extra"));
4660
+ deepStrictEqual(collection.itemIds, before);
4661
+ deepStrictEqual(sibling.itemIds, before);
4662
+ }
4663
+ },
4664
+ );
4665
+ }
4666
+ }
4667
+
4668
+ test("clone() copies scalar arrays without changing sparse inputs", () => {
4669
+ const names = ["original"];
4670
+ const original = new Object({ names });
4671
+ names.push("caller mutation");
4672
+ deepStrictEqual(original.names, ["original"]);
4673
+ const copy = original.clone();
4674
+ copy.names.push("clone mutation");
4675
+ deepStrictEqual(original.names, ["original"]);
4676
+ deepStrictEqual(copy.names, ["original", "clone mutation"]);
4677
+ const replacements = ["replacement"];
4678
+ const replaced = original.clone({ names: replacements });
4679
+ replacements.push("caller mutation");
4680
+ deepStrictEqual(replaced.names, ["replacement"]);
4681
+
4682
+ const sparseNames = new Array<string>(2);
4683
+ sparseNames[1] = "kept";
4684
+ const sparse = new Object({ names: sparseNames });
4685
+ for (
4686
+ const instance of [
4687
+ sparse,
4688
+ sparse.clone(),
4689
+ sparse.clone({ names: sparse.names }),
4690
+ ]
4691
+ ) {
4692
+ deepStrictEqual(instance.names.length, 2);
4693
+ deepStrictEqual(0 in instance.names, false);
4694
+ deepStrictEqual(instance.names[1], "kept");
4695
+ }
4696
+ });
4697
+
4698
+ test("clone() preserves embedded object identity and trust", async () => {
4699
+ const note = new Note({ id: new URL("https://b.example/note") });
4700
+ const original = new Create({
4701
+ id: new URL("https://a.example/create"),
4702
+ object: note,
4703
+ });
4704
+ // deno-lint-ignore require-await
4705
+ const documentLoader = async () => {
4706
+ throw new Error("Trusted embedded objects must not be fetched.");
4707
+ };
4708
+ for (
4709
+ const instance of [
4710
+ original,
4711
+ original.clone(),
4712
+ original.clone({ objects: [note] }),
4713
+ ]
4714
+ ) {
4715
+ strictEqual(await instance.getObject({ documentLoader }), note);
4716
+ strictEqual(instance.objectId, note.id);
4717
+ }
4718
+ });
4719
+
4720
+ test("clone() isolates crossOrigin trust when fetching", async () => {
4721
+ const original = new Create({
4722
+ id: new URL("https://a.example/create"),
4723
+ object: new URL("https://b.example/note"),
4724
+ });
4725
+ const copy = original.clone();
4726
+ // deno-lint-ignore require-await
4727
+ const documentLoader = async (url: string) => ({
4728
+ contextUrl: null,
4729
+ documentUrl: url,
4730
+ document: {
4731
+ "@context": "https://www.w3.org/ns/activitystreams",
4732
+ id: "https://other.example/note",
4733
+ type: "Note",
4734
+ content: "cross-origin",
4735
+ },
4736
+ });
4737
+ const options = { documentLoader, contextLoader: mockDocumentLoader };
4738
+ const trusted = await copy.getObject({ ...options, crossOrigin: "trust" });
4739
+ assertInstanceOf(trusted, Note);
4740
+ deepStrictEqual(trusted.content, "cross-origin");
4741
+ deepStrictEqual(await original.getObject(options), null);
4742
+ deepStrictEqual(original.objectId, new URL("https://b.example/note"));
4743
+ });
4744
+
4440
4745
  test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
4441
4746
  // Create a mock document loader that returns objects with different origins
4442
4747
  // deno-lint-ignore require-await