@fedify/vocab 2.3.11-dev.2296 → 2.3.12-dev.2341
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.
- package/deno.json +1 -1
- package/dist/mod.cjs +784 -703
- package/dist/mod.d.cts +162 -81
- package/dist/mod.d.ts +162 -81
- package/dist/mod.js +785 -704
- package/dist-tests/{actor-CF5WdFSo.mjs → actor-4GO2ZpXq.mjs} +2 -2
- package/dist-tests/actor.test.mjs +2 -2
- package/dist-tests/lookup.test.mjs +2 -2
- package/dist-tests/type.test.mjs +1 -1
- package/dist-tests/{vocab-BI7riPro.mjs → vocab-d1yZSbXp.mjs} +784 -703
- package/dist-tests/vocab.test.mjs +225 -2
- package/package.json +4 -4
- package/src/vocab.test.ts +305 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
|
-
import { A as Question, C as Offer, D as Place, E as Person, F as Source, I as Tombstone, M as QuoteRequest, N as Reject, O as PropertyValue, S as Object$1, _ as InteractionPolicy, b as Measure, c as Create, d as Document, f as Endpoints, g as Intent, h as Hashtag, i as Announce, j as QuoteAuthorization, k as Proposal, l as CryptographicKey, n as Activity, o as Collection, p as Follow, r as Agreement, s as Commitment, t as Accept, u as Delete, v as InteractionRule, w as OrderedCollectionPage, x as Note, y as Link, z as vocab_exports } from "./vocab-
|
|
3
|
+
import { A as Question, C as Offer, D as Place, E as Person, F as Source, I as Tombstone, M as QuoteRequest, N as Reject, O as PropertyValue, S as Object$1, _ as InteractionPolicy, b as Measure, c as Create, d as Document, f as Endpoints, g as Intent, h as Hashtag, i as Announce, j as QuoteAuthorization, k as Proposal, l as CryptographicKey, n as Activity, o as Collection, p as Follow, r as Agreement, s as Commitment, t as Accept, u as Delete, v as InteractionRule, w as OrderedCollectionPage, x as Note, y as Link, z as vocab_exports } from "./vocab-d1yZSbXp.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 { configure, reset } from "@logtape/logtape";
|
|
8
8
|
import { LanguageString, decodeMultibase, parseDecimal } from "@fedify/vocab-runtime";
|
|
9
9
|
import { pascalCase } from "es-toolkit";
|
|
@@ -267,6 +267,61 @@ test("Note.fromJsonLd() ignores malformed language tags in nested objects", asyn
|
|
|
267
267
|
const attachment = (await note.toJsonLd()).attachment;
|
|
268
268
|
deepStrictEqual(attachment.nameMap, { en: "Valid" });
|
|
269
269
|
});
|
|
270
|
+
test("Note.fromJsonLd() normalizes extlang language tags", async () => {
|
|
271
|
+
const json = {
|
|
272
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
273
|
+
type: "Note",
|
|
274
|
+
content: "<p>早晨</p>",
|
|
275
|
+
contentMap: {
|
|
276
|
+
"zh-YUE": "<p>早晨</p>",
|
|
277
|
+
"yue": "<p>早晨呀</p>"
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const note = await Note.fromJsonLd(json);
|
|
281
|
+
deepStrictEqual(note.contents, [
|
|
282
|
+
"<p>早晨</p>",
|
|
283
|
+
new LanguageString("<p>早晨呀</p>", "yue"),
|
|
284
|
+
new LanguageString("<p>早晨</p>", "yue")
|
|
285
|
+
]);
|
|
286
|
+
deepStrictEqual(note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName), [
|
|
287
|
+
null,
|
|
288
|
+
"yue",
|
|
289
|
+
"yue"
|
|
290
|
+
]);
|
|
291
|
+
deepStrictEqual(await note.toJsonLd(), json);
|
|
292
|
+
deepStrictEqual((await note.toJsonLd({ format: "compact" })).contentMap, { yue: ["<p>早晨呀</p>", "<p>早晨</p>"] });
|
|
293
|
+
deepStrictEqual((await note.toJsonLd({ format: "expand" }))[0]["https://www.w3.org/ns/activitystreams#content"], [
|
|
294
|
+
{ "@value": "<p>早晨</p>" },
|
|
295
|
+
{
|
|
296
|
+
"@value": "<p>早晨呀</p>",
|
|
297
|
+
"@language": "yue"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"@value": "<p>早晨</p>",
|
|
301
|
+
"@language": "yue"
|
|
302
|
+
}
|
|
303
|
+
]);
|
|
304
|
+
const expandedNote = await Note.fromJsonLd({
|
|
305
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Note"],
|
|
306
|
+
"https://www.w3.org/ns/activitystreams#name": [{
|
|
307
|
+
"@value": "早晨",
|
|
308
|
+
"@language": "zh-yue-HK"
|
|
309
|
+
}]
|
|
310
|
+
});
|
|
311
|
+
deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
|
|
312
|
+
deepStrictEqual(expandedNote.names[0].locale.baseName, "yue-HK");
|
|
313
|
+
});
|
|
314
|
+
test("Note.fromJsonLd() caches original language tags", async () => {
|
|
315
|
+
const json = {
|
|
316
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
317
|
+
type: "Note",
|
|
318
|
+
contentMap: { "EN-us": "Hello" }
|
|
319
|
+
};
|
|
320
|
+
const note = await Note.fromJsonLd(json);
|
|
321
|
+
deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
|
|
322
|
+
deepStrictEqual(note.contents[0].locale.baseName, "en-US");
|
|
323
|
+
deepStrictEqual(await note.toJsonLd(), json);
|
|
324
|
+
});
|
|
270
325
|
test("Note.toJsonLd()", async () => {
|
|
271
326
|
const note = new Note({ tags: [new Hashtag({
|
|
272
327
|
name: "#Fedify",
|
|
@@ -1320,6 +1375,22 @@ test("Link.fromJsonLd()", async () => {
|
|
|
1320
1375
|
"href": "at://did:plc:ia76kvnndjutgedggx2ibrem"
|
|
1321
1376
|
})).href, new URL("at://did%3Aplc%3Aia76kvnndjutgedggx2ibrem"));
|
|
1322
1377
|
});
|
|
1378
|
+
test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
|
|
1379
|
+
const link = await Link.fromJsonLd({
|
|
1380
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1381
|
+
type: "Link",
|
|
1382
|
+
href: "https://example.com/yue",
|
|
1383
|
+
hreflang: "zh-YUE"
|
|
1384
|
+
});
|
|
1385
|
+
deepStrictEqual(link.language?.baseName, "yue");
|
|
1386
|
+
deepStrictEqual((await link.toJsonLd({ format: "compact" })).hreflang, "yue");
|
|
1387
|
+
await rejects(() => Link.fromJsonLd({
|
|
1388
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1389
|
+
type: "Link",
|
|
1390
|
+
href: "https://example.com/en",
|
|
1391
|
+
hreflang: "en-USA"
|
|
1392
|
+
}), RangeError);
|
|
1393
|
+
});
|
|
1323
1394
|
test("Person.fromJsonLd() with relative URLs", async () => {
|
|
1324
1395
|
deepStrictEqual((await (await Person.fromJsonLd({
|
|
1325
1396
|
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
|
|
@@ -1733,6 +1804,158 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
|
|
|
1733
1804
|
});
|
|
1734
1805
|
deepStrictEqual(create.clone({ object: newNote }).objectId, new URL("https://example.com/new-note"));
|
|
1735
1806
|
});
|
|
1807
|
+
for (const property of ["object", "attachment"]) for (const fetchOriginal of [false, true]) test(`clone() isolates ${property} dereferencing (${fetchOriginal ? "original" : "clone"} first)`, async () => {
|
|
1808
|
+
const embedded = {
|
|
1809
|
+
id: "https://b.example/notes/1",
|
|
1810
|
+
type: "Note",
|
|
1811
|
+
content: "embedded"
|
|
1812
|
+
};
|
|
1813
|
+
const json = {
|
|
1814
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1815
|
+
id: "https://a.example/activities/1",
|
|
1816
|
+
type: "Create",
|
|
1817
|
+
[property]: embedded
|
|
1818
|
+
};
|
|
1819
|
+
const original = await Create.fromJsonLd(json, { contextLoader: mockDocumentLoader });
|
|
1820
|
+
const copy = original.clone();
|
|
1821
|
+
const fetching = fetchOriginal ? original : copy;
|
|
1822
|
+
const untouched = fetchOriginal ? copy : original;
|
|
1823
|
+
let fetches = 0;
|
|
1824
|
+
const documentLoader = async (url) => {
|
|
1825
|
+
fetches++;
|
|
1826
|
+
return {
|
|
1827
|
+
contextUrl: null,
|
|
1828
|
+
documentUrl: url,
|
|
1829
|
+
document: {
|
|
1830
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1831
|
+
...embedded,
|
|
1832
|
+
content: "fetched"
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
};
|
|
1836
|
+
const options = {
|
|
1837
|
+
documentLoader,
|
|
1838
|
+
contextLoader: mockDocumentLoader
|
|
1839
|
+
};
|
|
1840
|
+
const fetched = property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0];
|
|
1841
|
+
assertInstanceOf(fetched, Note);
|
|
1842
|
+
deepStrictEqual(fetched.content, "fetched");
|
|
1843
|
+
const trusted = { crossOrigin: "trust" };
|
|
1844
|
+
const preserved = property === "object" ? await untouched.getObject(trusted) : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
|
|
1845
|
+
assertInstanceOf(preserved, Note);
|
|
1846
|
+
deepStrictEqual(preserved.content, "embedded");
|
|
1847
|
+
deepStrictEqual((await original.toJsonLd())[property], fetchOriginal ? {
|
|
1848
|
+
...embedded,
|
|
1849
|
+
content: "fetched"
|
|
1850
|
+
} : embedded);
|
|
1851
|
+
const nested = (await new Accept({ object: untouched }).toJsonLd()).object;
|
|
1852
|
+
deepStrictEqual(nested[property], embedded);
|
|
1853
|
+
strictEqual(property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0], fetched);
|
|
1854
|
+
deepStrictEqual(fetches, 1);
|
|
1855
|
+
const independentlyFetched = property === "object" ? await untouched.getObject(options) : (await Array.fromAsync(untouched.getAttachments(options)))[0];
|
|
1856
|
+
assertInstanceOf(independentlyFetched, Note);
|
|
1857
|
+
deepStrictEqual(independentlyFetched.content, "fetched");
|
|
1858
|
+
deepStrictEqual(fetches, 2);
|
|
1859
|
+
const warmedClone = fetching.clone();
|
|
1860
|
+
strictEqual(property === "object" ? await warmedClone.getObject(options) : (await Array.fromAsync(warmedClone.getAttachments(options)))[0], fetched);
|
|
1861
|
+
deepStrictEqual(fetches, 2);
|
|
1862
|
+
});
|
|
1863
|
+
for (const clone of [false, true]) for (const frozen of [false, true]) test(`${clone ? "clone()" : "constructor"} copies ${frozen ? "frozen" : "mutable"} plural arrays`, async () => {
|
|
1864
|
+
const items = [new URL("https://example.com/object"), new URL("https://example.com/object")];
|
|
1865
|
+
const before = items.slice();
|
|
1866
|
+
if (frozen) globalThis.Object.freeze(items);
|
|
1867
|
+
const base = new Collection({});
|
|
1868
|
+
const collection = clone ? base.clone({ items }) : new Collection({ items });
|
|
1869
|
+
const sibling = clone ? base.clone({ items }) : new Collection({ items });
|
|
1870
|
+
const fetched = await Array.fromAsync(collection.getItems({
|
|
1871
|
+
documentLoader: mockDocumentLoader,
|
|
1872
|
+
contextLoader: mockDocumentLoader
|
|
1873
|
+
}));
|
|
1874
|
+
deepStrictEqual(fetched.length, 2);
|
|
1875
|
+
deepStrictEqual(fetched.map((item) => item.name), ["Fetched object", "Fetched object"]);
|
|
1876
|
+
deepStrictEqual(items, before);
|
|
1877
|
+
strictEqual(items[0], before[0]);
|
|
1878
|
+
strictEqual(items[1], before[1]);
|
|
1879
|
+
deepStrictEqual((await sibling.toJsonLd()).items, before.map(String));
|
|
1880
|
+
if (!frozen) {
|
|
1881
|
+
items.push(new URL("https://example.com/extra"));
|
|
1882
|
+
deepStrictEqual(collection.itemIds, before);
|
|
1883
|
+
deepStrictEqual(sibling.itemIds, before);
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
test("clone() copies scalar arrays without changing sparse inputs", () => {
|
|
1887
|
+
const names = ["original"];
|
|
1888
|
+
const original = new Object$1({ names });
|
|
1889
|
+
names.push("caller mutation");
|
|
1890
|
+
deepStrictEqual(original.names, ["original"]);
|
|
1891
|
+
const copy = original.clone();
|
|
1892
|
+
copy.names.push("clone mutation");
|
|
1893
|
+
deepStrictEqual(original.names, ["original"]);
|
|
1894
|
+
deepStrictEqual(copy.names, ["original", "clone mutation"]);
|
|
1895
|
+
const replacements = ["replacement"];
|
|
1896
|
+
const replaced = original.clone({ names: replacements });
|
|
1897
|
+
replacements.push("caller mutation");
|
|
1898
|
+
deepStrictEqual(replaced.names, ["replacement"]);
|
|
1899
|
+
const sparseNames = new Array(2);
|
|
1900
|
+
sparseNames[1] = "kept";
|
|
1901
|
+
const sparse = new Object$1({ names: sparseNames });
|
|
1902
|
+
for (const instance of [
|
|
1903
|
+
sparse,
|
|
1904
|
+
sparse.clone(),
|
|
1905
|
+
sparse.clone({ names: sparse.names })
|
|
1906
|
+
]) {
|
|
1907
|
+
deepStrictEqual(instance.names.length, 2);
|
|
1908
|
+
deepStrictEqual(0 in instance.names, false);
|
|
1909
|
+
deepStrictEqual(instance.names[1], "kept");
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
test("clone() preserves embedded object identity and trust", async () => {
|
|
1913
|
+
const note = new Note({ id: new URL("https://b.example/note") });
|
|
1914
|
+
const original = new Create({
|
|
1915
|
+
id: new URL("https://a.example/create"),
|
|
1916
|
+
object: note
|
|
1917
|
+
});
|
|
1918
|
+
const documentLoader = async () => {
|
|
1919
|
+
throw new Error("Trusted embedded objects must not be fetched.");
|
|
1920
|
+
};
|
|
1921
|
+
for (const instance of [
|
|
1922
|
+
original,
|
|
1923
|
+
original.clone(),
|
|
1924
|
+
original.clone({ objects: [note] })
|
|
1925
|
+
]) {
|
|
1926
|
+
strictEqual(await instance.getObject({ documentLoader }), note);
|
|
1927
|
+
strictEqual(instance.objectId, note.id);
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
test("clone() isolates crossOrigin trust when fetching", async () => {
|
|
1931
|
+
const original = new Create({
|
|
1932
|
+
id: new URL("https://a.example/create"),
|
|
1933
|
+
object: new URL("https://b.example/note")
|
|
1934
|
+
});
|
|
1935
|
+
const copy = original.clone();
|
|
1936
|
+
const documentLoader = async (url) => ({
|
|
1937
|
+
contextUrl: null,
|
|
1938
|
+
documentUrl: url,
|
|
1939
|
+
document: {
|
|
1940
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1941
|
+
id: "https://other.example/note",
|
|
1942
|
+
type: "Note",
|
|
1943
|
+
content: "cross-origin"
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
const options = {
|
|
1947
|
+
documentLoader,
|
|
1948
|
+
contextLoader: mockDocumentLoader
|
|
1949
|
+
};
|
|
1950
|
+
const trusted = await copy.getObject({
|
|
1951
|
+
...options,
|
|
1952
|
+
crossOrigin: "trust"
|
|
1953
|
+
});
|
|
1954
|
+
assertInstanceOf(trusted, Note);
|
|
1955
|
+
deepStrictEqual(trusted.content, "cross-origin");
|
|
1956
|
+
deepStrictEqual(await original.getObject(options), null);
|
|
1957
|
+
deepStrictEqual(original.objectId, new URL("https://b.example/note"));
|
|
1958
|
+
});
|
|
1736
1959
|
test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
|
|
1737
1960
|
const crossOriginDocumentLoader = async (url) => {
|
|
1738
1961
|
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.3.
|
|
3
|
+
"version": "2.3.12-dev.2341+eb343ebf",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"es-toolkit": "1.46.1",
|
|
47
47
|
"jsonld": "^9.0.0",
|
|
48
48
|
"pkijs": "^3.3.3",
|
|
49
|
-
"@fedify/vocab-tools": "2.3.
|
|
50
|
-
"@fedify/
|
|
51
|
-
"@fedify/
|
|
49
|
+
"@fedify/vocab-tools": "2.3.12-dev.2341+eb343ebf",
|
|
50
|
+
"@fedify/webfinger": "2.3.12-dev.2341+eb343ebf",
|
|
51
|
+
"@fedify/vocab-runtime": "2.3.12-dev.2341+eb343ebf"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^22.17.0",
|
package/src/vocab.test.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
notDeepStrictEqual,
|
|
18
18
|
ok,
|
|
19
19
|
rejects,
|
|
20
|
+
strictEqual,
|
|
20
21
|
throws,
|
|
21
22
|
} from "node:assert/strict";
|
|
22
23
|
import { assertInstanceOf } from "./utils.ts";
|
|
@@ -404,6 +405,77 @@ test("Note.fromJsonLd() ignores malformed language tags in nested objects", asyn
|
|
|
404
405
|
deepStrictEqual(attachment.nameMap, { en: "Valid" });
|
|
405
406
|
});
|
|
406
407
|
|
|
408
|
+
test("Note.fromJsonLd() normalizes extlang language tags", async () => {
|
|
409
|
+
const json = {
|
|
410
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
411
|
+
type: "Note",
|
|
412
|
+
content: "<p>早晨</p>",
|
|
413
|
+
contentMap: {
|
|
414
|
+
"zh-YUE": "<p>早晨</p>",
|
|
415
|
+
"yue": "<p>早晨呀</p>",
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
const note = await Note.fromJsonLd(json);
|
|
419
|
+
// JSON-LD expansion sorts language map keys, so yue comes before zh-yue:
|
|
420
|
+
deepStrictEqual(note.contents, [
|
|
421
|
+
"<p>早晨</p>",
|
|
422
|
+
new LanguageString("<p>早晨呀</p>", "yue"),
|
|
423
|
+
new LanguageString("<p>早晨</p>", "yue"),
|
|
424
|
+
]);
|
|
425
|
+
deepStrictEqual(
|
|
426
|
+
note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName),
|
|
427
|
+
[null, "yue", "yue"],
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// The unmodified object is serialized from the cached original document:
|
|
431
|
+
deepStrictEqual(await note.toJsonLd(), json);
|
|
432
|
+
const compact = await note.toJsonLd({ format: "compact" }) as Record<
|
|
433
|
+
string,
|
|
434
|
+
unknown
|
|
435
|
+
>;
|
|
436
|
+
deepStrictEqual(compact.contentMap, {
|
|
437
|
+
yue: ["<p>早晨呀</p>", "<p>早晨</p>"],
|
|
438
|
+
});
|
|
439
|
+
const expanded = await note.toJsonLd({ format: "expand" });
|
|
440
|
+
deepStrictEqual(
|
|
441
|
+
(expanded as Record<string, unknown>[])[0][
|
|
442
|
+
"https://www.w3.org/ns/activitystreams#content"
|
|
443
|
+
],
|
|
444
|
+
[
|
|
445
|
+
{ "@value": "<p>早晨</p>" },
|
|
446
|
+
{ "@value": "<p>早晨呀</p>", "@language": "yue" },
|
|
447
|
+
{ "@value": "<p>早晨</p>", "@language": "yue" },
|
|
448
|
+
],
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const expandedNote = await Note.fromJsonLd({
|
|
452
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Note"],
|
|
453
|
+
"https://www.w3.org/ns/activitystreams#name": [
|
|
454
|
+
{ "@value": "早晨", "@language": "zh-yue-HK" },
|
|
455
|
+
],
|
|
456
|
+
});
|
|
457
|
+
deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
|
|
458
|
+
deepStrictEqual(
|
|
459
|
+
(expandedNote.names[0] as LanguageString).locale.baseName,
|
|
460
|
+
"yue-HK",
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("Note.fromJsonLd() caches original language tags", async () => {
|
|
465
|
+
const json = {
|
|
466
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
467
|
+
type: "Note",
|
|
468
|
+
contentMap: { "EN-us": "Hello" },
|
|
469
|
+
};
|
|
470
|
+
const note = await Note.fromJsonLd(json);
|
|
471
|
+
deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
|
|
472
|
+
deepStrictEqual(
|
|
473
|
+
(note.contents[0] as LanguageString).locale.baseName,
|
|
474
|
+
"en-US",
|
|
475
|
+
);
|
|
476
|
+
deepStrictEqual(await note.toJsonLd(), json);
|
|
477
|
+
});
|
|
478
|
+
|
|
407
479
|
test("Note.toJsonLd()", async () => {
|
|
408
480
|
const note = new Note({
|
|
409
481
|
tags: [
|
|
@@ -2008,6 +2080,29 @@ test("Link.fromJsonLd()", async () => {
|
|
|
2008
2080
|
);
|
|
2009
2081
|
});
|
|
2010
2082
|
|
|
2083
|
+
test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
|
|
2084
|
+
const link = await Link.fromJsonLd({
|
|
2085
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2086
|
+
type: "Link",
|
|
2087
|
+
href: "https://example.com/yue",
|
|
2088
|
+
hreflang: "zh-YUE",
|
|
2089
|
+
});
|
|
2090
|
+
deepStrictEqual(link.language?.baseName, "yue");
|
|
2091
|
+
const compact = await link.toJsonLd({ format: "compact" }) as Record<
|
|
2092
|
+
string,
|
|
2093
|
+
unknown
|
|
2094
|
+
>;
|
|
2095
|
+
deepStrictEqual(compact.hreflang, "yue");
|
|
2096
|
+
|
|
2097
|
+
await rejects(() =>
|
|
2098
|
+
Link.fromJsonLd({
|
|
2099
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2100
|
+
type: "Link",
|
|
2101
|
+
href: "https://example.com/en",
|
|
2102
|
+
hreflang: "en-USA",
|
|
2103
|
+
}), RangeError);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2011
2106
|
test("Person.fromJsonLd() with relative URLs", async () => {
|
|
2012
2107
|
const json = {
|
|
2013
2108
|
"@context": [
|
|
@@ -2593,6 +2688,216 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
|
|
|
2593
2688
|
);
|
|
2594
2689
|
});
|
|
2595
2690
|
|
|
2691
|
+
for (const property of ["object", "attachment"] as const) {
|
|
2692
|
+
for (const fetchOriginal of [false, true]) {
|
|
2693
|
+
test(
|
|
2694
|
+
`clone() isolates ${property} dereferencing (${
|
|
2695
|
+
fetchOriginal ? "original" : "clone"
|
|
2696
|
+
} first)`,
|
|
2697
|
+
async () => {
|
|
2698
|
+
const embedded = {
|
|
2699
|
+
id: "https://b.example/notes/1",
|
|
2700
|
+
type: "Note",
|
|
2701
|
+
content: "embedded",
|
|
2702
|
+
};
|
|
2703
|
+
const json = {
|
|
2704
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2705
|
+
id: "https://a.example/activities/1",
|
|
2706
|
+
type: "Create",
|
|
2707
|
+
[property]: embedded,
|
|
2708
|
+
};
|
|
2709
|
+
const original = await Create.fromJsonLd(json, {
|
|
2710
|
+
contextLoader: mockDocumentLoader,
|
|
2711
|
+
});
|
|
2712
|
+
const copy = original.clone();
|
|
2713
|
+
const fetching = fetchOriginal ? original : copy;
|
|
2714
|
+
const untouched = fetchOriginal ? copy : original;
|
|
2715
|
+
let fetches = 0;
|
|
2716
|
+
// deno-lint-ignore require-await
|
|
2717
|
+
const documentLoader = async (url: string) => {
|
|
2718
|
+
fetches++;
|
|
2719
|
+
return {
|
|
2720
|
+
contextUrl: null,
|
|
2721
|
+
documentUrl: url,
|
|
2722
|
+
document: {
|
|
2723
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2724
|
+
...embedded,
|
|
2725
|
+
content: "fetched",
|
|
2726
|
+
},
|
|
2727
|
+
};
|
|
2728
|
+
};
|
|
2729
|
+
const options = { documentLoader, contextLoader: mockDocumentLoader };
|
|
2730
|
+
const fetched = property === "object"
|
|
2731
|
+
? await fetching.getObject(options)
|
|
2732
|
+
: (await Array.fromAsync(fetching.getAttachments(options)))[0];
|
|
2733
|
+
assertInstanceOf(fetched, Note);
|
|
2734
|
+
deepStrictEqual(fetched.content, "fetched");
|
|
2735
|
+
|
|
2736
|
+
// Inspect the other instance before it fetches anything itself.
|
|
2737
|
+
const trusted = { crossOrigin: "trust" as const };
|
|
2738
|
+
const preserved = property === "object"
|
|
2739
|
+
? await untouched.getObject(trusted)
|
|
2740
|
+
: (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
|
|
2741
|
+
assertInstanceOf(preserved, Note);
|
|
2742
|
+
deepStrictEqual(preserved.content, "embedded");
|
|
2743
|
+
const serialized = await original.toJsonLd() as Record<string, unknown>;
|
|
2744
|
+
deepStrictEqual(
|
|
2745
|
+
serialized[property],
|
|
2746
|
+
fetchOriginal ? { ...embedded, content: "fetched" } : embedded,
|
|
2747
|
+
);
|
|
2748
|
+
const accept = new Accept({ object: untouched });
|
|
2749
|
+
const acceptJson = await accept.toJsonLd() as {
|
|
2750
|
+
object: Record<string, unknown>;
|
|
2751
|
+
};
|
|
2752
|
+
const nested = acceptJson.object;
|
|
2753
|
+
deepStrictEqual(nested[property], embedded);
|
|
2754
|
+
|
|
2755
|
+
// Successful lookups and their trust metadata belong to each instance.
|
|
2756
|
+
const cached = property === "object"
|
|
2757
|
+
? await fetching.getObject(options)
|
|
2758
|
+
: (await Array.fromAsync(fetching.getAttachments(options)))[0];
|
|
2759
|
+
strictEqual(cached, fetched);
|
|
2760
|
+
deepStrictEqual(fetches, 1);
|
|
2761
|
+
const independentlyFetched = property === "object"
|
|
2762
|
+
? await untouched.getObject(options)
|
|
2763
|
+
: (await Array.fromAsync(untouched.getAttachments(options)))[0];
|
|
2764
|
+
assertInstanceOf(independentlyFetched, Note);
|
|
2765
|
+
deepStrictEqual(independentlyFetched.content, "fetched");
|
|
2766
|
+
deepStrictEqual(fetches, 2);
|
|
2767
|
+
const warmedClone = fetching.clone();
|
|
2768
|
+
strictEqual(
|
|
2769
|
+
property === "object"
|
|
2770
|
+
? await warmedClone.getObject(options)
|
|
2771
|
+
: (await Array.fromAsync(warmedClone.getAttachments(options)))[0],
|
|
2772
|
+
fetched,
|
|
2773
|
+
);
|
|
2774
|
+
deepStrictEqual(fetches, 2);
|
|
2775
|
+
},
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
for (const clone of [false, true]) {
|
|
2781
|
+
for (const frozen of [false, true]) {
|
|
2782
|
+
test(
|
|
2783
|
+
`${clone ? "clone()" : "constructor"} copies ${
|
|
2784
|
+
frozen ? "frozen" : "mutable"
|
|
2785
|
+
} plural arrays`,
|
|
2786
|
+
async () => {
|
|
2787
|
+
const items = [
|
|
2788
|
+
new URL("https://example.com/object"),
|
|
2789
|
+
new URL("https://example.com/object"),
|
|
2790
|
+
];
|
|
2791
|
+
const before = items.slice();
|
|
2792
|
+
if (frozen) globalThis.Object.freeze(items);
|
|
2793
|
+
const base = new Collection({});
|
|
2794
|
+
const collection = clone
|
|
2795
|
+
? base.clone({ items })
|
|
2796
|
+
: new Collection({ items });
|
|
2797
|
+
const sibling = clone
|
|
2798
|
+
? base.clone({ items })
|
|
2799
|
+
: new Collection({ items });
|
|
2800
|
+
const fetched = await Array.fromAsync(collection.getItems({
|
|
2801
|
+
documentLoader: mockDocumentLoader,
|
|
2802
|
+
contextLoader: mockDocumentLoader,
|
|
2803
|
+
}));
|
|
2804
|
+
deepStrictEqual(fetched.length, 2);
|
|
2805
|
+
deepStrictEqual(fetched.map((item) => item.name), [
|
|
2806
|
+
"Fetched object",
|
|
2807
|
+
"Fetched object",
|
|
2808
|
+
]);
|
|
2809
|
+
deepStrictEqual(items, before);
|
|
2810
|
+
strictEqual(items[0], before[0]);
|
|
2811
|
+
strictEqual(items[1], before[1]);
|
|
2812
|
+
const siblingJson = await sibling.toJsonLd() as { items: unknown };
|
|
2813
|
+
deepStrictEqual(siblingJson.items, before.map(String));
|
|
2814
|
+
if (!frozen) {
|
|
2815
|
+
items.push(new URL("https://example.com/extra"));
|
|
2816
|
+
deepStrictEqual(collection.itemIds, before);
|
|
2817
|
+
deepStrictEqual(sibling.itemIds, before);
|
|
2818
|
+
}
|
|
2819
|
+
},
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
test("clone() copies scalar arrays without changing sparse inputs", () => {
|
|
2825
|
+
const names = ["original"];
|
|
2826
|
+
const original = new Object({ names });
|
|
2827
|
+
names.push("caller mutation");
|
|
2828
|
+
deepStrictEqual(original.names, ["original"]);
|
|
2829
|
+
const copy = original.clone();
|
|
2830
|
+
copy.names.push("clone mutation");
|
|
2831
|
+
deepStrictEqual(original.names, ["original"]);
|
|
2832
|
+
deepStrictEqual(copy.names, ["original", "clone mutation"]);
|
|
2833
|
+
const replacements = ["replacement"];
|
|
2834
|
+
const replaced = original.clone({ names: replacements });
|
|
2835
|
+
replacements.push("caller mutation");
|
|
2836
|
+
deepStrictEqual(replaced.names, ["replacement"]);
|
|
2837
|
+
|
|
2838
|
+
const sparseNames = new Array<string>(2);
|
|
2839
|
+
sparseNames[1] = "kept";
|
|
2840
|
+
const sparse = new Object({ names: sparseNames });
|
|
2841
|
+
for (
|
|
2842
|
+
const instance of [
|
|
2843
|
+
sparse,
|
|
2844
|
+
sparse.clone(),
|
|
2845
|
+
sparse.clone({ names: sparse.names }),
|
|
2846
|
+
]
|
|
2847
|
+
) {
|
|
2848
|
+
deepStrictEqual(instance.names.length, 2);
|
|
2849
|
+
deepStrictEqual(0 in instance.names, false);
|
|
2850
|
+
deepStrictEqual(instance.names[1], "kept");
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
test("clone() preserves embedded object identity and trust", async () => {
|
|
2855
|
+
const note = new Note({ id: new URL("https://b.example/note") });
|
|
2856
|
+
const original = new Create({
|
|
2857
|
+
id: new URL("https://a.example/create"),
|
|
2858
|
+
object: note,
|
|
2859
|
+
});
|
|
2860
|
+
// deno-lint-ignore require-await
|
|
2861
|
+
const documentLoader = async () => {
|
|
2862
|
+
throw new Error("Trusted embedded objects must not be fetched.");
|
|
2863
|
+
};
|
|
2864
|
+
for (
|
|
2865
|
+
const instance of [
|
|
2866
|
+
original,
|
|
2867
|
+
original.clone(),
|
|
2868
|
+
original.clone({ objects: [note] }),
|
|
2869
|
+
]
|
|
2870
|
+
) {
|
|
2871
|
+
strictEqual(await instance.getObject({ documentLoader }), note);
|
|
2872
|
+
strictEqual(instance.objectId, note.id);
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
|
|
2876
|
+
test("clone() isolates crossOrigin trust when fetching", async () => {
|
|
2877
|
+
const original = new Create({
|
|
2878
|
+
id: new URL("https://a.example/create"),
|
|
2879
|
+
object: new URL("https://b.example/note"),
|
|
2880
|
+
});
|
|
2881
|
+
const copy = original.clone();
|
|
2882
|
+
// deno-lint-ignore require-await
|
|
2883
|
+
const documentLoader = async (url: string) => ({
|
|
2884
|
+
contextUrl: null,
|
|
2885
|
+
documentUrl: url,
|
|
2886
|
+
document: {
|
|
2887
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2888
|
+
id: "https://other.example/note",
|
|
2889
|
+
type: "Note",
|
|
2890
|
+
content: "cross-origin",
|
|
2891
|
+
},
|
|
2892
|
+
});
|
|
2893
|
+
const options = { documentLoader, contextLoader: mockDocumentLoader };
|
|
2894
|
+
const trusted = await copy.getObject({ ...options, crossOrigin: "trust" });
|
|
2895
|
+
assertInstanceOf(trusted, Note);
|
|
2896
|
+
deepStrictEqual(trusted.content, "cross-origin");
|
|
2897
|
+
deepStrictEqual(await original.getObject(options), null);
|
|
2898
|
+
deepStrictEqual(original.objectId, new URL("https://b.example/note"));
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2596
2901
|
test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
|
|
2597
2902
|
// Create a mock document loader that returns objects with different origins
|
|
2598
2903
|
// deno-lint-ignore require-await
|