@fedify/vocab 2.0.30 → 2.0.31-dev.2297

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 { _ as PropertyValue, a as Create, b as Source, d as Note, f as Object$1, g as Place, h as Person, i as Collection, l as Hashtag, n as Announce, o as CryptographicKey, p as OrderedCollectionPage, s as Follow, t as Activity, u as Link, v as Question, x as vocab_exports } from "./vocab-Ci3g_-fe.mjs";
3
+ import { S as vocab_exports, _ as Place, a as Collection, c as Follow, d as Link, f as Note, g as Person, m as OrderedCollectionPage, n as Activity, o as Create, p as Object$1, r as Announce, s as CryptographicKey, t as Accept, u as Hashtag, v as PropertyValue, x as Source, y as Question } from "./vocab-ChQFW7FV.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";
@@ -839,6 +839,158 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
839
839
  });
840
840
  deepStrictEqual(create.clone({ object: newNote }).objectId, new URL("https://example.com/new-note"));
841
841
  });
842
+ for (const property of ["object", "attachment"]) for (const fetchOriginal of [false, true]) test(`clone() isolates ${property} dereferencing (${fetchOriginal ? "original" : "clone"} first)`, async () => {
843
+ const embedded = {
844
+ id: "https://b.example/notes/1",
845
+ type: "Note",
846
+ content: "embedded"
847
+ };
848
+ const json = {
849
+ "@context": "https://www.w3.org/ns/activitystreams",
850
+ id: "https://a.example/activities/1",
851
+ type: "Create",
852
+ [property]: embedded
853
+ };
854
+ const original = await Create.fromJsonLd(json, { contextLoader: mockDocumentLoader });
855
+ const copy = original.clone();
856
+ const fetching = fetchOriginal ? original : copy;
857
+ const untouched = fetchOriginal ? copy : original;
858
+ let fetches = 0;
859
+ const documentLoader = async (url) => {
860
+ fetches++;
861
+ return {
862
+ contextUrl: null,
863
+ documentUrl: url,
864
+ document: {
865
+ "@context": "https://www.w3.org/ns/activitystreams",
866
+ ...embedded,
867
+ content: "fetched"
868
+ }
869
+ };
870
+ };
871
+ const options = {
872
+ documentLoader,
873
+ contextLoader: mockDocumentLoader
874
+ };
875
+ const fetched = property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0];
876
+ assertInstanceOf(fetched, Note);
877
+ deepStrictEqual(fetched.content, "fetched");
878
+ const trusted = { crossOrigin: "trust" };
879
+ const preserved = property === "object" ? await untouched.getObject(trusted) : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
880
+ assertInstanceOf(preserved, Note);
881
+ deepStrictEqual(preserved.content, "embedded");
882
+ deepStrictEqual((await original.toJsonLd())[property], fetchOriginal ? {
883
+ ...embedded,
884
+ content: "fetched"
885
+ } : embedded);
886
+ const nested = (await new Accept({ object: untouched }).toJsonLd()).object;
887
+ deepStrictEqual(nested[property], embedded);
888
+ strictEqual(property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0], fetched);
889
+ deepStrictEqual(fetches, 1);
890
+ const independentlyFetched = property === "object" ? await untouched.getObject(options) : (await Array.fromAsync(untouched.getAttachments(options)))[0];
891
+ assertInstanceOf(independentlyFetched, Note);
892
+ deepStrictEqual(independentlyFetched.content, "fetched");
893
+ deepStrictEqual(fetches, 2);
894
+ const warmedClone = fetching.clone();
895
+ strictEqual(property === "object" ? await warmedClone.getObject(options) : (await Array.fromAsync(warmedClone.getAttachments(options)))[0], fetched);
896
+ deepStrictEqual(fetches, 2);
897
+ });
898
+ for (const clone of [false, true]) for (const frozen of [false, true]) test(`${clone ? "clone()" : "constructor"} copies ${frozen ? "frozen" : "mutable"} plural arrays`, async () => {
899
+ const items = [new URL("https://example.com/object"), new URL("https://example.com/object")];
900
+ const before = items.slice();
901
+ if (frozen) globalThis.Object.freeze(items);
902
+ const base = new Collection({});
903
+ const collection = clone ? base.clone({ items }) : new Collection({ items });
904
+ const sibling = clone ? base.clone({ items }) : new Collection({ items });
905
+ const fetched = await Array.fromAsync(collection.getItems({
906
+ documentLoader: mockDocumentLoader,
907
+ contextLoader: mockDocumentLoader
908
+ }));
909
+ deepStrictEqual(fetched.length, 2);
910
+ deepStrictEqual(fetched.map((item) => item.name), ["Fetched object", "Fetched object"]);
911
+ deepStrictEqual(items, before);
912
+ strictEqual(items[0], before[0]);
913
+ strictEqual(items[1], before[1]);
914
+ deepStrictEqual((await sibling.toJsonLd()).items, before.map(String));
915
+ if (!frozen) {
916
+ items.push(new URL("https://example.com/extra"));
917
+ deepStrictEqual(collection.itemIds, before);
918
+ deepStrictEqual(sibling.itemIds, before);
919
+ }
920
+ });
921
+ test("clone() copies scalar arrays without changing sparse inputs", () => {
922
+ const names = ["original"];
923
+ const original = new Object$1({ names });
924
+ names.push("caller mutation");
925
+ deepStrictEqual(original.names, ["original"]);
926
+ const copy = original.clone();
927
+ copy.names.push("clone mutation");
928
+ deepStrictEqual(original.names, ["original"]);
929
+ deepStrictEqual(copy.names, ["original", "clone mutation"]);
930
+ const replacements = ["replacement"];
931
+ const replaced = original.clone({ names: replacements });
932
+ replacements.push("caller mutation");
933
+ deepStrictEqual(replaced.names, ["replacement"]);
934
+ const sparseNames = new Array(2);
935
+ sparseNames[1] = "kept";
936
+ const sparse = new Object$1({ names: sparseNames });
937
+ for (const instance of [
938
+ sparse,
939
+ sparse.clone(),
940
+ sparse.clone({ names: sparse.names })
941
+ ]) {
942
+ deepStrictEqual(instance.names.length, 2);
943
+ deepStrictEqual(0 in instance.names, false);
944
+ deepStrictEqual(instance.names[1], "kept");
945
+ }
946
+ });
947
+ test("clone() preserves embedded object identity and trust", async () => {
948
+ const note = new Note({ id: new URL("https://b.example/note") });
949
+ const original = new Create({
950
+ id: new URL("https://a.example/create"),
951
+ object: note
952
+ });
953
+ const documentLoader = async () => {
954
+ throw new Error("Trusted embedded objects must not be fetched.");
955
+ };
956
+ for (const instance of [
957
+ original,
958
+ original.clone(),
959
+ original.clone({ objects: [note] })
960
+ ]) {
961
+ strictEqual(await instance.getObject({ documentLoader }), note);
962
+ strictEqual(instance.objectId, note.id);
963
+ }
964
+ });
965
+ test("clone() isolates crossOrigin trust when fetching", async () => {
966
+ const original = new Create({
967
+ id: new URL("https://a.example/create"),
968
+ object: new URL("https://b.example/note")
969
+ });
970
+ const copy = original.clone();
971
+ const documentLoader = async (url) => ({
972
+ contextUrl: null,
973
+ documentUrl: url,
974
+ document: {
975
+ "@context": "https://www.w3.org/ns/activitystreams",
976
+ id: "https://other.example/note",
977
+ type: "Note",
978
+ content: "cross-origin"
979
+ }
980
+ });
981
+ const options = {
982
+ documentLoader,
983
+ contextLoader: mockDocumentLoader
984
+ };
985
+ const trusted = await copy.getObject({
986
+ ...options,
987
+ crossOrigin: "trust"
988
+ });
989
+ assertInstanceOf(trusted, Note);
990
+ deepStrictEqual(trusted.content, "cross-origin");
991
+ deepStrictEqual(await original.getObject(options), null);
992
+ deepStrictEqual(original.objectId, new URL("https://b.example/note"));
993
+ });
842
994
  test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
843
995
  const crossOriginDocumentLoader = async (url) => {
844
996
  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.0.30",
3
+ "version": "2.0.31-dev.2297+58cbc917",
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/vocab-tools": "2.0.30",
50
- "@fedify/vocab-runtime": "2.0.30",
51
- "@fedify/webfinger": "2.0.30"
49
+ "@fedify/vocab-tools": "2.0.31-dev.2297+58cbc917",
50
+ "@fedify/webfinger": "2.0.31-dev.2297+58cbc917",
51
+ "@fedify/vocab-runtime": "2.0.31-dev.2297+58cbc917"
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,
@@ -1279,6 +1281,216 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
1279
1281
  );
1280
1282
  });
1281
1283
 
1284
+ for (const property of ["object", "attachment"] as const) {
1285
+ for (const fetchOriginal of [false, true]) {
1286
+ test(
1287
+ `clone() isolates ${property} dereferencing (${
1288
+ fetchOriginal ? "original" : "clone"
1289
+ } first)`,
1290
+ async () => {
1291
+ const embedded = {
1292
+ id: "https://b.example/notes/1",
1293
+ type: "Note",
1294
+ content: "embedded",
1295
+ };
1296
+ const json = {
1297
+ "@context": "https://www.w3.org/ns/activitystreams",
1298
+ id: "https://a.example/activities/1",
1299
+ type: "Create",
1300
+ [property]: embedded,
1301
+ };
1302
+ const original = await Create.fromJsonLd(json, {
1303
+ contextLoader: mockDocumentLoader,
1304
+ });
1305
+ const copy = original.clone();
1306
+ const fetching = fetchOriginal ? original : copy;
1307
+ const untouched = fetchOriginal ? copy : original;
1308
+ let fetches = 0;
1309
+ // deno-lint-ignore require-await
1310
+ const documentLoader = async (url: string) => {
1311
+ fetches++;
1312
+ return {
1313
+ contextUrl: null,
1314
+ documentUrl: url,
1315
+ document: {
1316
+ "@context": "https://www.w3.org/ns/activitystreams",
1317
+ ...embedded,
1318
+ content: "fetched",
1319
+ },
1320
+ };
1321
+ };
1322
+ const options = { documentLoader, contextLoader: mockDocumentLoader };
1323
+ const fetched = property === "object"
1324
+ ? await fetching.getObject(options)
1325
+ : (await Array.fromAsync(fetching.getAttachments(options)))[0];
1326
+ assertInstanceOf(fetched, Note);
1327
+ deepStrictEqual(fetched.content, "fetched");
1328
+
1329
+ // Inspect the other instance before it fetches anything itself.
1330
+ const trusted = { crossOrigin: "trust" as const };
1331
+ const preserved = property === "object"
1332
+ ? await untouched.getObject(trusted)
1333
+ : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
1334
+ assertInstanceOf(preserved, Note);
1335
+ deepStrictEqual(preserved.content, "embedded");
1336
+ const serialized = await original.toJsonLd() as Record<string, unknown>;
1337
+ deepStrictEqual(
1338
+ serialized[property],
1339
+ fetchOriginal ? { ...embedded, content: "fetched" } : embedded,
1340
+ );
1341
+ const accept = new Accept({ object: untouched });
1342
+ const acceptJson = await accept.toJsonLd() as {
1343
+ object: Record<string, unknown>;
1344
+ };
1345
+ const nested = acceptJson.object;
1346
+ deepStrictEqual(nested[property], embedded);
1347
+
1348
+ // Successful lookups and their trust metadata belong to each instance.
1349
+ const cached = property === "object"
1350
+ ? await fetching.getObject(options)
1351
+ : (await Array.fromAsync(fetching.getAttachments(options)))[0];
1352
+ strictEqual(cached, fetched);
1353
+ deepStrictEqual(fetches, 1);
1354
+ const independentlyFetched = property === "object"
1355
+ ? await untouched.getObject(options)
1356
+ : (await Array.fromAsync(untouched.getAttachments(options)))[0];
1357
+ assertInstanceOf(independentlyFetched, Note);
1358
+ deepStrictEqual(independentlyFetched.content, "fetched");
1359
+ deepStrictEqual(fetches, 2);
1360
+ const warmedClone = fetching.clone();
1361
+ strictEqual(
1362
+ property === "object"
1363
+ ? await warmedClone.getObject(options)
1364
+ : (await Array.fromAsync(warmedClone.getAttachments(options)))[0],
1365
+ fetched,
1366
+ );
1367
+ deepStrictEqual(fetches, 2);
1368
+ },
1369
+ );
1370
+ }
1371
+ }
1372
+
1373
+ for (const clone of [false, true]) {
1374
+ for (const frozen of [false, true]) {
1375
+ test(
1376
+ `${clone ? "clone()" : "constructor"} copies ${
1377
+ frozen ? "frozen" : "mutable"
1378
+ } plural arrays`,
1379
+ async () => {
1380
+ const items = [
1381
+ new URL("https://example.com/object"),
1382
+ new URL("https://example.com/object"),
1383
+ ];
1384
+ const before = items.slice();
1385
+ if (frozen) globalThis.Object.freeze(items);
1386
+ const base = new Collection({});
1387
+ const collection = clone
1388
+ ? base.clone({ items })
1389
+ : new Collection({ items });
1390
+ const sibling = clone
1391
+ ? base.clone({ items })
1392
+ : new Collection({ items });
1393
+ const fetched = await Array.fromAsync(collection.getItems({
1394
+ documentLoader: mockDocumentLoader,
1395
+ contextLoader: mockDocumentLoader,
1396
+ }));
1397
+ deepStrictEqual(fetched.length, 2);
1398
+ deepStrictEqual(fetched.map((item) => item.name), [
1399
+ "Fetched object",
1400
+ "Fetched object",
1401
+ ]);
1402
+ deepStrictEqual(items, before);
1403
+ strictEqual(items[0], before[0]);
1404
+ strictEqual(items[1], before[1]);
1405
+ const siblingJson = await sibling.toJsonLd() as { items: unknown };
1406
+ deepStrictEqual(siblingJson.items, before.map(String));
1407
+ if (!frozen) {
1408
+ items.push(new URL("https://example.com/extra"));
1409
+ deepStrictEqual(collection.itemIds, before);
1410
+ deepStrictEqual(sibling.itemIds, before);
1411
+ }
1412
+ },
1413
+ );
1414
+ }
1415
+ }
1416
+
1417
+ test("clone() copies scalar arrays without changing sparse inputs", () => {
1418
+ const names = ["original"];
1419
+ const original = new Object({ names });
1420
+ names.push("caller mutation");
1421
+ deepStrictEqual(original.names, ["original"]);
1422
+ const copy = original.clone();
1423
+ copy.names.push("clone mutation");
1424
+ deepStrictEqual(original.names, ["original"]);
1425
+ deepStrictEqual(copy.names, ["original", "clone mutation"]);
1426
+ const replacements = ["replacement"];
1427
+ const replaced = original.clone({ names: replacements });
1428
+ replacements.push("caller mutation");
1429
+ deepStrictEqual(replaced.names, ["replacement"]);
1430
+
1431
+ const sparseNames = new Array<string>(2);
1432
+ sparseNames[1] = "kept";
1433
+ const sparse = new Object({ names: sparseNames });
1434
+ for (
1435
+ const instance of [
1436
+ sparse,
1437
+ sparse.clone(),
1438
+ sparse.clone({ names: sparse.names }),
1439
+ ]
1440
+ ) {
1441
+ deepStrictEqual(instance.names.length, 2);
1442
+ deepStrictEqual(0 in instance.names, false);
1443
+ deepStrictEqual(instance.names[1], "kept");
1444
+ }
1445
+ });
1446
+
1447
+ test("clone() preserves embedded object identity and trust", async () => {
1448
+ const note = new Note({ id: new URL("https://b.example/note") });
1449
+ const original = new Create({
1450
+ id: new URL("https://a.example/create"),
1451
+ object: note,
1452
+ });
1453
+ // deno-lint-ignore require-await
1454
+ const documentLoader = async () => {
1455
+ throw new Error("Trusted embedded objects must not be fetched.");
1456
+ };
1457
+ for (
1458
+ const instance of [
1459
+ original,
1460
+ original.clone(),
1461
+ original.clone({ objects: [note] }),
1462
+ ]
1463
+ ) {
1464
+ strictEqual(await instance.getObject({ documentLoader }), note);
1465
+ strictEqual(instance.objectId, note.id);
1466
+ }
1467
+ });
1468
+
1469
+ test("clone() isolates crossOrigin trust when fetching", async () => {
1470
+ const original = new Create({
1471
+ id: new URL("https://a.example/create"),
1472
+ object: new URL("https://b.example/note"),
1473
+ });
1474
+ const copy = original.clone();
1475
+ // deno-lint-ignore require-await
1476
+ const documentLoader = async (url: string) => ({
1477
+ contextUrl: null,
1478
+ documentUrl: url,
1479
+ document: {
1480
+ "@context": "https://www.w3.org/ns/activitystreams",
1481
+ id: "https://other.example/note",
1482
+ type: "Note",
1483
+ content: "cross-origin",
1484
+ },
1485
+ });
1486
+ const options = { documentLoader, contextLoader: mockDocumentLoader };
1487
+ const trusted = await copy.getObject({ ...options, crossOrigin: "trust" });
1488
+ assertInstanceOf(trusted, Note);
1489
+ deepStrictEqual(trusted.content, "cross-origin");
1490
+ deepStrictEqual(await original.getObject(options), null);
1491
+ deepStrictEqual(original.objectId, new URL("https://b.example/note"));
1492
+ });
1493
+
1282
1494
  test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
1283
1495
  // Create a mock document loader that returns objects with different origins
1284
1496
  // deno-lint-ignore require-await